mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-05-30 22:11:02 +00:00
Adding Bridges code as git subtree. (#2515)
* Add instructions. * Squashed 'bridges/' content from commit 345e84a21 git-subtree-dir: bridges git-subtree-split: 345e84a2146b56628e9888c9f5e129cb40e868a9 * Remove bridges workspace file to avoid confusing Cargo. * Add some bridges primitives to Polkadot workspace. * Improve docs.
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "relay-ethereum-client"
|
||||
version = "0.1.0"
|
||||
authors = ["Parity Technologies <admin@parity.io>"]
|
||||
edition = "2018"
|
||||
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
|
||||
|
||||
[dependencies]
|
||||
bp-eth-poa = { path = "../../primitives/ethereum-poa" }
|
||||
codec = { package = "parity-scale-codec", version = "2.0.0" }
|
||||
headers-relay = { path = "../headers-relay" }
|
||||
hex-literal = "0.3"
|
||||
jsonrpsee = { git = "https://github.com/svyatonik/jsonrpsee.git", branch = "shared-client-in-rpc-api", default-features = false, features = ["http"] }
|
||||
libsecp256k1 = { version = "0.3.4", default-features = false, features = ["hmac"] }
|
||||
log = "0.4.11"
|
||||
relay-utils = { path = "../utils" }
|
||||
web3 = { version = "0.15", git = "https://github.com/tomusdrw/rust-web3.git", branch ="td-ethabi", default-features = false }
|
||||
@@ -0,0 +1,172 @@
|
||||
// Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::rpc::Ethereum;
|
||||
use crate::types::{
|
||||
Address, Bytes, CallRequest, Header, HeaderWithTransactions, Receipt, SignedRawTx, SyncState, Transaction,
|
||||
TransactionHash, H256, U256,
|
||||
};
|
||||
use crate::{ConnectionParams, Error, Result};
|
||||
|
||||
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 {
|
||||
params: ConnectionParams,
|
||||
client: RpcClient,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Create a new Ethereum RPC Client.
|
||||
pub fn new(params: ConnectionParams) -> Self {
|
||||
Self {
|
||||
client: Self::build_client(¶ms),
|
||||
params,
|
||||
}
|
||||
}
|
||||
|
||||
/// Build client to use in connection.
|
||||
fn build_client(params: &ConnectionParams) -> RpcClient {
|
||||
let uri = format!("http://{}:{}", params.host, params.port);
|
||||
let transport = HttpTransportClient::new(&uri);
|
||||
let raw_client = RawClient::new(transport);
|
||||
raw_client.into()
|
||||
}
|
||||
|
||||
/// Reopen client connection.
|
||||
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<U256> {
|
||||
Ok(Ethereum::estimate_gas(&self.client, call_request).await?)
|
||||
}
|
||||
|
||||
/// Retrieve number of the best known block from the Ethereum node.
|
||||
pub async fn best_block_number(&self) -> Result<u64> {
|
||||
Ok(Ethereum::block_number(&self.client).await?.as_u64())
|
||||
}
|
||||
|
||||
/// Retrieve number of the best known block from the Ethereum node.
|
||||
pub async fn header_by_number(&self, block_number: u64) -> Result<Header> {
|
||||
let get_full_tx_objects = false;
|
||||
let header = Ethereum::get_block_by_number(&self.client, block_number, get_full_tx_objects).await?;
|
||||
match header.number.is_some() && header.hash.is_some() && header.logs_bloom.is_some() {
|
||||
true => Ok(header),
|
||||
false => Err(Error::IncompleteHeader),
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieve block header by its hash from Ethereum node.
|
||||
pub async fn header_by_hash(&self, hash: H256) -> Result<Header> {
|
||||
let get_full_tx_objects = false;
|
||||
let header = Ethereum::get_block_by_hash(&self.client, hash, get_full_tx_objects).await?;
|
||||
match header.number.is_some() && header.hash.is_some() && header.logs_bloom.is_some() {
|
||||
true => Ok(header),
|
||||
false => Err(Error::IncompleteHeader),
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieve block header and its transactions by its number from Ethereum node.
|
||||
pub async fn header_by_number_with_transactions(&self, number: u64) -> Result<HeaderWithTransactions> {
|
||||
let get_full_tx_objects = true;
|
||||
let header = Ethereum::get_block_by_number_with_transactions(&self.client, number, get_full_tx_objects).await?;
|
||||
|
||||
let is_complete_header = header.number.is_some() && header.hash.is_some() && header.logs_bloom.is_some();
|
||||
if !is_complete_header {
|
||||
return Err(Error::IncompleteHeader);
|
||||
}
|
||||
|
||||
let is_complete_transactions = header.transactions.iter().all(|tx| tx.raw.is_some());
|
||||
if !is_complete_transactions {
|
||||
return Err(Error::IncompleteTransaction);
|
||||
}
|
||||
|
||||
Ok(header)
|
||||
}
|
||||
|
||||
/// Retrieve block header and its transactions by its hash from Ethereum node.
|
||||
pub async fn header_by_hash_with_transactions(&self, hash: H256) -> Result<HeaderWithTransactions> {
|
||||
let get_full_tx_objects = true;
|
||||
let header = Ethereum::get_block_by_hash_with_transactions(&self.client, hash, get_full_tx_objects).await?;
|
||||
|
||||
let is_complete_header = header.number.is_some() && header.hash.is_some() && header.logs_bloom.is_some();
|
||||
if !is_complete_header {
|
||||
return Err(Error::IncompleteHeader);
|
||||
}
|
||||
|
||||
let is_complete_transactions = header.transactions.iter().all(|tx| tx.raw.is_some());
|
||||
if !is_complete_transactions {
|
||||
return Err(Error::IncompleteTransaction);
|
||||
}
|
||||
|
||||
Ok(header)
|
||||
}
|
||||
|
||||
/// Retrieve transaction by its hash from Ethereum node.
|
||||
pub async fn transaction_by_hash(&self, hash: H256) -> Result<Option<Transaction>> {
|
||||
Ok(Ethereum::transaction_by_hash(&self.client, hash).await?)
|
||||
}
|
||||
|
||||
/// Retrieve transaction receipt by transaction hash.
|
||||
pub async fn transaction_receipt(&self, transaction_hash: H256) -> Result<Receipt> {
|
||||
Ok(Ethereum::get_transaction_receipt(&self.client, transaction_hash).await?)
|
||||
}
|
||||
|
||||
/// Get the nonce of the given account.
|
||||
pub async fn account_nonce(&self, address: Address) -> Result<U256> {
|
||||
Ok(Ethereum::get_transaction_count(&self.client, address).await?)
|
||||
}
|
||||
|
||||
/// Submit an Ethereum transaction.
|
||||
///
|
||||
/// The transaction must already be signed before sending it through this method.
|
||||
pub async fn submit_transaction(&self, signed_raw_tx: SignedRawTx) -> Result<TransactionHash> {
|
||||
let transaction = Bytes(signed_raw_tx);
|
||||
let tx_hash = Ethereum::submit_transaction(&self.client, transaction).await?;
|
||||
log::trace!(target: "bridge", "Sent transaction to Ethereum node: {:?}", tx_hash);
|
||||
Ok(tx_hash)
|
||||
}
|
||||
|
||||
/// Call Ethereum smart contract.
|
||||
pub async fn eth_call(&self, call_transaction: CallRequest) -> Result<Bytes> {
|
||||
Ok(Ethereum::call(&self.client, call_transaction).await?)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
// Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Ethereum node RPC errors.
|
||||
|
||||
use crate::types::U256;
|
||||
|
||||
use jsonrpsee::client::RequestError;
|
||||
use relay_utils::MaybeConnectionError;
|
||||
|
||||
/// Result type used by Ethereum client.
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Errors that can occur only when interacting with
|
||||
/// an Ethereum node through RPC.
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// An error that can occur when making an HTTP request to
|
||||
/// an JSON-RPC client.
|
||||
Request(RequestError),
|
||||
/// Failed to parse response.
|
||||
ResponseParseFailed(String),
|
||||
/// We have received a header with missing fields.
|
||||
IncompleteHeader,
|
||||
/// We have received a transaction missing a `raw` field.
|
||||
IncompleteTransaction,
|
||||
/// An invalid Substrate block number was received from
|
||||
/// an Ethereum node.
|
||||
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<RequestError> for Error {
|
||||
fn from(error: RequestError) -> Self {
|
||||
Error::Request(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl MaybeConnectionError for Error {
|
||||
fn is_connection_error(&self) -> bool {
|
||||
matches!(
|
||||
*self,
|
||||
Error::Request(RequestError::TransportError(_)) | Error::ClientNotSynced(_),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for Error {
|
||||
fn to_string(&self) -> String {
|
||||
match self {
|
||||
Self::Request(e) => e.to_string(),
|
||||
Self::ResponseParseFailed(e) => e.to_string(),
|
||||
Self::IncompleteHeader => {
|
||||
"Incomplete Ethereum Header Received (missing some of required fields - hash, number, logs_bloom)"
|
||||
.to_string()
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Tools to interact with (Open) Ethereum node using RPC methods.
|
||||
|
||||
#![warn(missing_docs)]
|
||||
|
||||
mod client;
|
||||
mod error;
|
||||
mod rpc;
|
||||
mod sign;
|
||||
|
||||
pub use crate::client::Client;
|
||||
pub use crate::error::{Error, Result};
|
||||
pub use crate::sign::{sign_and_submit_transaction, SigningParams};
|
||||
|
||||
pub mod types;
|
||||
|
||||
/// Ethereum connection params.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ConnectionParams {
|
||||
/// Ethereum RPC host.
|
||||
pub host: String,
|
||||
/// Ethereum RPC port.
|
||||
pub port: u16,
|
||||
}
|
||||
|
||||
impl Default for ConnectionParams {
|
||||
fn default() -> Self {
|
||||
ConnectionParams {
|
||||
host: "localhost".into(),
|
||||
port: 8545,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Ethereum node RPC interface.
|
||||
|
||||
// The compiler doesn't think we're using the
|
||||
// code from rpc_api!
|
||||
#![allow(dead_code)]
|
||||
#![allow(unused_variables)]
|
||||
|
||||
use crate::types::{
|
||||
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)]
|
||||
fn block_number() -> U64;
|
||||
#[rpc(method = "eth_getBlockByNumber", positional_params)]
|
||||
fn get_block_by_number(block_number: U64, full_tx_objs: bool) -> Header;
|
||||
#[rpc(method = "eth_getBlockByHash", positional_params)]
|
||||
fn get_block_by_hash(hash: H256, full_tx_objs: bool) -> Header;
|
||||
#[rpc(method = "eth_getBlockByNumber", positional_params)]
|
||||
fn get_block_by_number_with_transactions(number: U64, full_tx_objs: bool) -> HeaderWithTransactions;
|
||||
#[rpc(method = "eth_getBlockByHash", positional_params)]
|
||||
fn get_block_by_hash_with_transactions(hash: H256, full_tx_objs: bool) -> HeaderWithTransactions;
|
||||
#[rpc(method = "eth_getTransactionByHash", positional_params)]
|
||||
fn transaction_by_hash(hash: H256) -> Option<Transaction>;
|
||||
#[rpc(method = "eth_getTransactionReceipt", positional_params)]
|
||||
fn get_transaction_receipt(transaction_hash: H256) -> Receipt;
|
||||
#[rpc(method = "eth_getTransactionCount", positional_params)]
|
||||
fn get_transaction_count(address: Address) -> U256;
|
||||
#[rpc(method = "eth_submitTransaction", positional_params)]
|
||||
fn submit_transaction(transaction: Bytes) -> TransactionHash;
|
||||
#[rpc(method = "eth_call", positional_params)]
|
||||
fn call(transaction_call: CallRequest) -> Bytes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
// Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::types::{Address, CallRequest, U256};
|
||||
use crate::{Client, Result};
|
||||
use bp_eth_poa::signatures::{secret_to_address, SignTransaction};
|
||||
use hex_literal::hex;
|
||||
use secp256k1::SecretKey;
|
||||
|
||||
/// Ethereum signing params.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SigningParams {
|
||||
/// Ethereum chain id.
|
||||
pub chain_id: u64,
|
||||
/// Ethereum transactions signer.
|
||||
pub signer: SecretKey,
|
||||
/// Gas price we agree to pay.
|
||||
pub gas_price: U256,
|
||||
}
|
||||
|
||||
impl Default for SigningParams {
|
||||
fn default() -> Self {
|
||||
SigningParams {
|
||||
chain_id: 0x11, // Parity dev chain
|
||||
// account that has a lot of ether when we run instant seal engine
|
||||
// address: 0x00a329c0648769a73afac7f9381e08fb43dbea72
|
||||
// secret: 0x4d5db4107d237df6a3d58ee5f70ae63d73d7658d4026f2eefd2f204c81682cb7
|
||||
signer: SecretKey::parse(&hex!(
|
||||
"4d5db4107d237df6a3d58ee5f70ae63d73d7658d4026f2eefd2f204c81682cb7"
|
||||
))
|
||||
.expect("secret is hardcoded, thus valid; qed"),
|
||||
gas_price: 8_000_000_000u64.into(), // 8 Gwei
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sign and submit tranaction using given Ethereum client.
|
||||
pub async fn sign_and_submit_transaction(
|
||||
client: &Client,
|
||||
params: &SigningParams,
|
||||
contract_address: Option<Address>,
|
||||
nonce: Option<U256>,
|
||||
double_gas: bool,
|
||||
encoded_call: Vec<u8>,
|
||||
) -> Result<()> {
|
||||
let nonce = if let Some(n) = nonce {
|
||||
n
|
||||
} else {
|
||||
let address: Address = secret_to_address(¶ms.signer);
|
||||
client.account_nonce(address).await?
|
||||
};
|
||||
|
||||
let call_request = CallRequest {
|
||||
to: contract_address,
|
||||
data: Some(encoded_call.clone().into()),
|
||||
..Default::default()
|
||||
};
|
||||
let gas = client.estimate_gas(call_request).await?;
|
||||
|
||||
let raw_transaction = bp_eth_poa::UnsignedTransaction {
|
||||
nonce,
|
||||
to: contract_address,
|
||||
value: U256::zero(),
|
||||
gas: if double_gas { gas.saturating_mul(2.into()) } else { gas },
|
||||
gas_price: params.gas_price,
|
||||
payload: encoded_call,
|
||||
}
|
||||
.sign_by(¶ms.signer, Some(params.chain_id));
|
||||
|
||||
let _ = client.submit_transaction(raw_transaction).await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Common types that are used in relay <-> Ethereum node communications.
|
||||
|
||||
use headers_relay::sync_types::SourceHeader;
|
||||
|
||||
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.
|
||||
pub const HEADER_ID_PROOF: &str = "checked on retrieval; qed";
|
||||
|
||||
/// Ethereum transaction hash type.
|
||||
pub type HeaderHash = H256;
|
||||
|
||||
/// Ethereum transaction hash type.
|
||||
pub type TransactionHash = H256;
|
||||
|
||||
/// Ethereum transaction type.
|
||||
pub type Transaction = web3::types::Transaction;
|
||||
|
||||
/// Ethereum header type.
|
||||
pub type Header = web3::types::Block<H256>;
|
||||
|
||||
/// Ethereum header type used in headers sync.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct SyncHeader(Header);
|
||||
|
||||
impl std::ops::Deref for SyncHeader {
|
||||
type Target = Header;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Ethereum header with transactions type.
|
||||
pub type HeaderWithTransactions = web3::types::Block<Transaction>;
|
||||
|
||||
/// Ethereum transaction receipt type.
|
||||
pub type Receipt = web3::types::TransactionReceipt;
|
||||
|
||||
/// Ethereum header ID.
|
||||
pub type HeaderId = relay_utils::HeaderId<H256, u64>;
|
||||
|
||||
/// A raw Ethereum transaction that's been signed.
|
||||
pub type SignedRawTx = Vec<u8>;
|
||||
|
||||
impl From<Header> for SyncHeader {
|
||||
fn from(header: Header) -> Self {
|
||||
Self(header)
|
||||
}
|
||||
}
|
||||
|
||||
impl SourceHeader<H256, u64> for SyncHeader {
|
||||
fn id(&self) -> HeaderId {
|
||||
relay_utils::HeaderId(
|
||||
self.number.expect(HEADER_ID_PROOF).as_u64(),
|
||||
self.hash.expect(HEADER_ID_PROOF),
|
||||
)
|
||||
}
|
||||
|
||||
fn parent_id(&self) -> HeaderId {
|
||||
relay_utils::HeaderId(self.number.expect(HEADER_ID_PROOF).as_u64() - 1, self.parent_hash)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
[package]
|
||||
name = "ethereum-poa-relay"
|
||||
version = "0.1.0"
|
||||
authors = ["Parity Technologies <admin@parity.io>"]
|
||||
edition = "2018"
|
||||
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
|
||||
|
||||
[dependencies]
|
||||
ansi_term = "0.12"
|
||||
async-std = "1.9.0"
|
||||
async-trait = "0.1.42"
|
||||
clap = { version = "2.33.3", features = ["yaml"] }
|
||||
codec = { package = "parity-scale-codec", version = "2.0.0" }
|
||||
env_logger = "0.8.3"
|
||||
ethabi = { git = "https://github.com/paritytech/ethabi.git", branch = "td-eth-types-11" }
|
||||
ethabi-contract = { git = "https://github.com/paritytech/ethabi.git", branch = "td-eth-types-11" }
|
||||
ethabi-derive = { git = "https://github.com/paritytech/ethabi.git", branch = "td-eth-types-11" }
|
||||
futures = "0.3.12"
|
||||
hex = "0.4"
|
||||
hex-literal = "0.3"
|
||||
libsecp256k1 = { version = "0.3.4", default-features = false, features = ["hmac"] }
|
||||
log = "0.4.14"
|
||||
num-traits = "0.2"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0.62"
|
||||
time = "0.2"
|
||||
|
||||
# Bridge dependencies
|
||||
|
||||
bp-currency-exchange = { path = "../../primitives/currency-exchange" }
|
||||
bp-eth-poa = { path = "../../primitives/ethereum-poa" }
|
||||
exchange-relay = { path = "../exchange-relay" }
|
||||
headers-relay = { path = "../headers-relay" }
|
||||
messages-relay = { path = "../messages-relay" }
|
||||
relay-ethereum-client = { path = "../ethereum-client" }
|
||||
relay-rialto-client = { path = "../rialto-client" }
|
||||
relay-substrate-client = { path = "../substrate-client" }
|
||||
relay-utils = { path = "../utils" }
|
||||
rialto-runtime = { path = "../../bin/rialto/runtime" }
|
||||
|
||||
# Substrate Dependencies
|
||||
|
||||
frame-system = { git = "https://github.com/paritytech/substrate.git", branch = "master" }
|
||||
pallet-transaction-payment = { git = "https://github.com/paritytech/substrate.git", branch = "master" }
|
||||
sp-core = { git = "https://github.com/paritytech/substrate.git", branch = "master" }
|
||||
sp-keyring = { git = "https://github.com/paritytech/substrate.git", branch = "master" }
|
||||
sp-runtime = { git = "https://github.com/paritytech/substrate.git", branch = "master" }
|
||||
substrate-prometheus-endpoint = { git = "https://github.com/paritytech/substrate.git", branch = "master" }
|
||||
@@ -0,0 +1,7 @@
|
||||
# PoA <> Substrate Bridge
|
||||
|
||||
**DISCLAIMER:** *we recommend not using the bridge in "production" (to bridge significant amounts) just yet.
|
||||
it's missing a code audit and should still be considered alpha. we can't rule out that there are bugs that might result in loss of the bridged amounts.
|
||||
we'll update this disclaimer once that changes*
|
||||
|
||||
These docs are very incomplete yet. Describe high-level goals here in the (near) future.
|
||||
@@ -0,0 +1,167 @@
|
||||
[
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes",
|
||||
"name": "rawInitialHeader",
|
||||
"type": "bytes"
|
||||
},
|
||||
{
|
||||
"internalType": "uint64",
|
||||
"name": "initialValidatorsSetId",
|
||||
"type": "uint64"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes",
|
||||
"name": "initialValidatorsSet",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "constructor"
|
||||
},
|
||||
{
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "fallback"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "bestKnownHeader",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "",
|
||||
"type": "bytes32"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "finalityTargetNumber",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "finalityTargetHash",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes",
|
||||
"name": "rawFinalityProof",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"name": "importFinalityProof",
|
||||
"outputs": [],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes",
|
||||
"name": "rawHeader1",
|
||||
"type": "bytes"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes",
|
||||
"name": "rawHeader2",
|
||||
"type": "bytes"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes",
|
||||
"name": "rawHeader3",
|
||||
"type": "bytes"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes",
|
||||
"name": "rawHeader4",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"name": "importHeaders",
|
||||
"outputs": [],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "incompleteHeaders",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint256[]",
|
||||
"name": "",
|
||||
"type": "uint256[]"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes32[]",
|
||||
"name": "",
|
||||
"type": "bytes32[]"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes",
|
||||
"name": "rawHeader1",
|
||||
"type": "bytes"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes",
|
||||
"name": "rawHeader2",
|
||||
"type": "bytes"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes",
|
||||
"name": "rawHeader3",
|
||||
"type": "bytes"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes",
|
||||
"name": "rawHeader4",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"name": "isIncompleteHeaders",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "headerHash",
|
||||
"type": "bytes32"
|
||||
}
|
||||
],
|
||||
"name": "isKnownHeader",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
}
|
||||
]
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,5 @@
|
||||
Last Change Date: 2020-07-30
|
||||
Solc version: 0.6.6+commit.6c089d02.Linux.g++
|
||||
Source hash (keccak256): 0xea5d6d744f69157adc2857166792aca139c0b5b186ba89c1011358fbcad90d7e
|
||||
Source gist: https://github.com/svyatonik/substrate-bridge-sol/blob/6456d3e016c95cd5e6d5e817c23e9e69e739aa78/substrate-bridge.sol
|
||||
Compiler flags used (command to produce the file): `docker run -i ethereum/solc:0.6.6 --optimize --bin - < substrate-bridge.sol`
|
||||
@@ -0,0 +1,166 @@
|
||||
name: ethsub-bridge
|
||||
version: "0.1.0"
|
||||
author: Parity Technologies <admin@parity.io>
|
||||
about: Parity Ethereum (PoA) <-> Substrate bridge
|
||||
subcommands:
|
||||
- eth-to-sub:
|
||||
about: Synchronize headers from Ethereum node to Substrate node.
|
||||
args:
|
||||
- eth-host: ð-host
|
||||
long: eth-host
|
||||
value_name: ETH_HOST
|
||||
help: Connect to Ethereum node at given host.
|
||||
takes_value: true
|
||||
- eth-port: ð-port
|
||||
long: eth-port
|
||||
value_name: ETH_PORT
|
||||
help: Connect to Ethereum node at given port.
|
||||
takes_value: true
|
||||
- sub-host: &sub-host
|
||||
long: sub-host
|
||||
value_name: SUB_HOST
|
||||
help: Connect to Substrate node at given host.
|
||||
takes_value: true
|
||||
- sub-port: &sub-port
|
||||
long: sub-port
|
||||
value_name: SUB_PORT
|
||||
help: Connect to Substrate node websocket server at given port.
|
||||
takes_value: true
|
||||
- sub-tx-mode:
|
||||
long: sub-tx-mode
|
||||
value_name: MODE
|
||||
help: Submit headers using signed (default) or unsigned transactions. Third mode - backup - submits signed transactions only when we believe that sync has stalled.
|
||||
takes_value: true
|
||||
possible_values:
|
||||
- signed
|
||||
- unsigned
|
||||
- backup
|
||||
- sub-signer: &sub-signer
|
||||
long: sub-signer
|
||||
value_name: SUB_SIGNER
|
||||
help: The SURI of secret key to use when transactions are submitted to the Substrate node.
|
||||
- sub-signer-password: &sub-signer-password
|
||||
long: sub-signer-password
|
||||
value_name: SUB_SIGNER_PASSWORD
|
||||
help: The password for the SURI of secret key to use when transactions are submitted to the Substrate node.
|
||||
- sub-pallet-instance: &sub-pallet-instance
|
||||
long: instance
|
||||
short: i
|
||||
value_name: PALLET_INSTANCE
|
||||
help: The instance of the bridge pallet the relay should follow.
|
||||
takes_value: true
|
||||
case_insensitive: true
|
||||
possible_values:
|
||||
- Rialto
|
||||
- Kovan
|
||||
default_value: Rialto
|
||||
- no-prometheus: &no-prometheus
|
||||
long: no-prometheus
|
||||
help: Do not expose a Prometheus metric endpoint.
|
||||
- prometheus-host: &prometheus-host
|
||||
long: prometheus-host
|
||||
value_name: PROMETHEUS_HOST
|
||||
help: Expose Prometheus endpoint at given interface.
|
||||
- prometheus-port: &prometheus-port
|
||||
long: prometheus-port
|
||||
value_name: PROMETHEUS_PORT
|
||||
help: Expose Prometheus endpoint at given port.
|
||||
- sub-to-eth:
|
||||
about: Synchronize headers from Substrate node to Ethereum node.
|
||||
args:
|
||||
- eth-host: *eth-host
|
||||
- eth-port: *eth-port
|
||||
- eth-contract:
|
||||
long: eth-contract
|
||||
value_name: ETH_CONTRACT
|
||||
help: Address of deployed bridge contract.
|
||||
takes_value: true
|
||||
- eth-chain-id: ð-chain-id
|
||||
long: eth-chain-id
|
||||
value_name: ETH_CHAIN_ID
|
||||
help: Chain ID to use for signing.
|
||||
- eth-signer: ð-signer
|
||||
long: eth-signer
|
||||
value_name: ETH_SIGNER
|
||||
help: Hex-encoded secret to use when transactions are submitted to the Ethereum node.
|
||||
- sub-host: *sub-host
|
||||
- sub-port: *sub-port
|
||||
- no-prometheus: *no-prometheus
|
||||
- prometheus-host: *prometheus-host
|
||||
- prometheus-port: *prometheus-port
|
||||
- eth-deploy-contract:
|
||||
about: Deploy Bridge contract on Ethereum node.
|
||||
args:
|
||||
- eth-host: *eth-host
|
||||
- eth-port: *eth-port
|
||||
- eth-signer: *eth-signer
|
||||
- eth-chain-id: *eth-chain-id
|
||||
- eth-contract-code:
|
||||
long: eth-contract-code
|
||||
value_name: ETH_CONTRACT_CODE
|
||||
help: Bytecode of bridge contract.
|
||||
takes_value: true
|
||||
- sub-host: *sub-host
|
||||
- sub-port: *sub-port
|
||||
- sub-authorities-set-id:
|
||||
long: sub-authorities-set-id
|
||||
value_name: SUB_AUTHORITIES_SET_ID
|
||||
help: ID of initial GRANDPA authorities set.
|
||||
takes_value: true
|
||||
- sub-authorities-set:
|
||||
long: sub-authorities-set
|
||||
value_name: SUB_AUTHORITIES_SET
|
||||
help: Encoded initial GRANDPA authorities set.
|
||||
takes_value: true
|
||||
- sub-initial-header:
|
||||
long: sub-initial-header
|
||||
value_name: SUB_INITIAL_HEADER
|
||||
help: Encoded initial Substrate header.
|
||||
takes_value: true
|
||||
- eth-submit-exchange-tx:
|
||||
about: Submit lock funds transaction to Ethereum node.
|
||||
args:
|
||||
- eth-host: *eth-host
|
||||
- eth-port: *eth-port
|
||||
- eth-nonce:
|
||||
long: eth-nonce
|
||||
value_name: ETH_NONCE
|
||||
help: Nonce that have to be used when building transaction. If not specified, read from PoA node.
|
||||
takes_value: true
|
||||
- eth-signer: *eth-signer
|
||||
- eth-chain-id: *eth-chain-id
|
||||
- eth-amount:
|
||||
long: eth-amount
|
||||
value_name: ETH_AMOUNT
|
||||
help: Amount of ETH to lock (in wei).
|
||||
takes_value: true
|
||||
- sub-recipient:
|
||||
long: sub-recipient
|
||||
value_name: SUB_RECIPIENT
|
||||
help: Hex-encoded Public key of funds recipient in Substrate chain.
|
||||
takes_value: true
|
||||
- eth-exchange-sub:
|
||||
about: Submit proof of PoA lock funds transaction to Substrate node.
|
||||
args:
|
||||
- eth-host: *eth-host
|
||||
- eth-port: *eth-port
|
||||
- eth-start-with-block:
|
||||
long: eth-start-with-block
|
||||
value_name: ETH_START_WITH_BLOCK
|
||||
help: Auto-relay transactions starting with given block number. If not specified, starts with best finalized Ethereum block (known to Substrate node) transactions.
|
||||
takes_value: true
|
||||
conflicts_with:
|
||||
- eth-tx-hash
|
||||
- eth-tx-hash:
|
||||
long: eth-tx-hash
|
||||
value_name: ETH_TX_HASH
|
||||
help: Hash of the lock funds transaction.
|
||||
takes_value: true
|
||||
- sub-host: *sub-host
|
||||
- sub-port: *sub-port
|
||||
- sub-signer: *sub-signer
|
||||
- sub-signer-password: *sub-signer-password
|
||||
- sub-pallet-instance: *sub-pallet-instance
|
||||
- no-prometheus: *no-prometheus
|
||||
- prometheus-host: *prometheus-host
|
||||
- prometheus-port: *prometheus-port
|
||||
@@ -0,0 +1,653 @@
|
||||
// Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::rpc_errors::RpcError;
|
||||
use crate::substrate_sync_loop::QueuedRialtoHeader;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use bp_eth_poa::signatures::secret_to_address;
|
||||
use codec::{Decode, Encode};
|
||||
use ethabi::FunctionOutputDecoder;
|
||||
use headers_relay::sync_types::SubmittedHeaders;
|
||||
use relay_ethereum_client::{
|
||||
sign_and_submit_transaction,
|
||||
types::{Address, CallRequest, HeaderId as EthereumHeaderId, Receipt, H256, U256},
|
||||
Client as EthereumClient, Error as EthereumNodeError, SigningParams as EthereumSigningParams,
|
||||
};
|
||||
use relay_rialto_client::HeaderId as RialtoHeaderId;
|
||||
use relay_utils::{HeaderId, MaybeConnectionError};
|
||||
use sp_runtime::Justification;
|
||||
use std::collections::HashSet;
|
||||
|
||||
// to encode/decode contract calls
|
||||
ethabi_contract::use_contract!(bridge_contract, "res/substrate-bridge-abi.json");
|
||||
|
||||
type RpcResult<T> = std::result::Result<T, RpcError>;
|
||||
|
||||
/// A trait which contains methods that work by using multiple low-level RPCs, or more complicated
|
||||
/// interactions involving, for example, an Ethereum contract.
|
||||
#[async_trait]
|
||||
pub trait EthereumHighLevelRpc {
|
||||
/// Returns best Substrate block that PoA chain knows of.
|
||||
async fn best_substrate_block(&self, contract_address: Address) -> RpcResult<RialtoHeaderId>;
|
||||
|
||||
/// Returns true if Substrate header is known to Ethereum node.
|
||||
async fn substrate_header_known(
|
||||
&self,
|
||||
contract_address: Address,
|
||||
id: RialtoHeaderId,
|
||||
) -> RpcResult<(RialtoHeaderId, bool)>;
|
||||
|
||||
/// Submits Substrate headers to Ethereum contract.
|
||||
async fn submit_substrate_headers(
|
||||
&self,
|
||||
params: EthereumSigningParams,
|
||||
contract_address: Address,
|
||||
headers: Vec<QueuedRialtoHeader>,
|
||||
) -> SubmittedHeaders<RialtoHeaderId, RpcError>;
|
||||
|
||||
/// Returns ids of incomplete Substrate headers.
|
||||
async fn incomplete_substrate_headers(&self, contract_address: Address) -> RpcResult<HashSet<RialtoHeaderId>>;
|
||||
|
||||
/// Complete Substrate header.
|
||||
async fn complete_substrate_header(
|
||||
&self,
|
||||
params: EthereumSigningParams,
|
||||
contract_address: Address,
|
||||
id: RialtoHeaderId,
|
||||
justification: Justification,
|
||||
) -> RpcResult<RialtoHeaderId>;
|
||||
|
||||
/// Submit ethereum transaction.
|
||||
async fn submit_ethereum_transaction(
|
||||
&self,
|
||||
params: &EthereumSigningParams,
|
||||
contract_address: Option<Address>,
|
||||
nonce: Option<U256>,
|
||||
double_gas: bool,
|
||||
encoded_call: Vec<u8>,
|
||||
) -> RpcResult<()>;
|
||||
|
||||
/// Retrieve transactions receipts for given block.
|
||||
async fn transaction_receipts(
|
||||
&self,
|
||||
id: EthereumHeaderId,
|
||||
transactions: Vec<H256>,
|
||||
) -> RpcResult<(EthereumHeaderId, Vec<Receipt>)>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl EthereumHighLevelRpc for EthereumClient {
|
||||
async fn best_substrate_block(&self, contract_address: Address) -> RpcResult<RialtoHeaderId> {
|
||||
let (encoded_call, call_decoder) = bridge_contract::functions::best_known_header::call();
|
||||
let call_request = CallRequest {
|
||||
to: Some(contract_address),
|
||||
data: Some(encoded_call.into()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let call_result = self.eth_call(call_request).await?;
|
||||
let (number, raw_hash) = call_decoder.decode(&call_result.0)?;
|
||||
let hash = rialto_runtime::Hash::decode(&mut &raw_hash[..])?;
|
||||
|
||||
if number != number.low_u32().into() {
|
||||
return Err(RpcError::Ethereum(EthereumNodeError::InvalidSubstrateBlockNumber));
|
||||
}
|
||||
|
||||
Ok(HeaderId(number.low_u32(), hash))
|
||||
}
|
||||
|
||||
async fn substrate_header_known(
|
||||
&self,
|
||||
contract_address: Address,
|
||||
id: RialtoHeaderId,
|
||||
) -> RpcResult<(RialtoHeaderId, bool)> {
|
||||
let (encoded_call, call_decoder) = bridge_contract::functions::is_known_header::call(id.1);
|
||||
let call_request = CallRequest {
|
||||
to: Some(contract_address),
|
||||
data: Some(encoded_call.into()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let call_result = self.eth_call(call_request).await?;
|
||||
let is_known_block = call_decoder.decode(&call_result.0)?;
|
||||
|
||||
Ok((id, is_known_block))
|
||||
}
|
||||
|
||||
async fn submit_substrate_headers(
|
||||
&self,
|
||||
params: EthereumSigningParams,
|
||||
contract_address: Address,
|
||||
headers: Vec<QueuedRialtoHeader>,
|
||||
) -> SubmittedHeaders<RialtoHeaderId, RpcError> {
|
||||
// read nonce of signer
|
||||
let address: Address = secret_to_address(¶ms.signer);
|
||||
let nonce = match self.account_nonce(address).await {
|
||||
Ok(nonce) => nonce,
|
||||
Err(error) => {
|
||||
return SubmittedHeaders {
|
||||
submitted: Vec::new(),
|
||||
incomplete: Vec::new(),
|
||||
rejected: headers.iter().rev().map(|header| header.id()).collect(),
|
||||
fatal_error: Some(error.into()),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// submit headers. Note that we're cloning self here. It is ok, because
|
||||
// cloning `jsonrpsee::Client` only clones reference to background threads
|
||||
submit_substrate_headers(
|
||||
EthereumHeadersSubmitter {
|
||||
client: self.clone(),
|
||||
params,
|
||||
contract_address,
|
||||
nonce,
|
||||
},
|
||||
headers,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn incomplete_substrate_headers(&self, contract_address: Address) -> RpcResult<HashSet<RialtoHeaderId>> {
|
||||
let (encoded_call, call_decoder) = bridge_contract::functions::incomplete_headers::call();
|
||||
let call_request = CallRequest {
|
||||
to: Some(contract_address),
|
||||
data: Some(encoded_call.into()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let call_result = self.eth_call(call_request).await?;
|
||||
|
||||
// Q: Is is correct to call these "incomplete_ids"?
|
||||
let (incomplete_headers_numbers, incomplete_headers_hashes) = call_decoder.decode(&call_result.0)?;
|
||||
let incomplete_ids = incomplete_headers_numbers
|
||||
.into_iter()
|
||||
.zip(incomplete_headers_hashes)
|
||||
.filter_map(|(number, hash)| {
|
||||
if number != number.low_u32().into() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(HeaderId(number.low_u32(), hash))
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(incomplete_ids)
|
||||
}
|
||||
|
||||
async fn complete_substrate_header(
|
||||
&self,
|
||||
params: EthereumSigningParams,
|
||||
contract_address: Address,
|
||||
id: RialtoHeaderId,
|
||||
justification: Justification,
|
||||
) -> RpcResult<RialtoHeaderId> {
|
||||
let _ = self
|
||||
.submit_ethereum_transaction(
|
||||
¶ms,
|
||||
Some(contract_address),
|
||||
None,
|
||||
false,
|
||||
bridge_contract::functions::import_finality_proof::encode_input(id.0, id.1, justification),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
async fn submit_ethereum_transaction(
|
||||
&self,
|
||||
params: &EthereumSigningParams,
|
||||
contract_address: Option<Address>,
|
||||
nonce: Option<U256>,
|
||||
double_gas: bool,
|
||||
encoded_call: Vec<u8>,
|
||||
) -> RpcResult<()> {
|
||||
sign_and_submit_transaction(self, params, contract_address, nonce, double_gas, encoded_call)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn transaction_receipts(
|
||||
&self,
|
||||
id: EthereumHeaderId,
|
||||
transactions: Vec<H256>,
|
||||
) -> RpcResult<(EthereumHeaderId, Vec<Receipt>)> {
|
||||
let mut transaction_receipts = Vec::with_capacity(transactions.len());
|
||||
for transaction in transactions {
|
||||
let transaction_receipt = self.transaction_receipt(transaction).await?;
|
||||
transaction_receipts.push(transaction_receipt);
|
||||
}
|
||||
Ok((id, transaction_receipts))
|
||||
}
|
||||
}
|
||||
|
||||
/// Max number of headers which can be sent to Solidity contract.
|
||||
pub const HEADERS_BATCH: usize = 4;
|
||||
|
||||
/// Substrate headers to send to the Ethereum light client.
|
||||
///
|
||||
/// The Solidity contract can only accept a fixed number of headers in one go.
|
||||
/// This struct is meant to encapsulate this limitation.
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(Clone))]
|
||||
pub struct HeadersBatch {
|
||||
pub header1: QueuedRialtoHeader,
|
||||
pub header2: Option<QueuedRialtoHeader>,
|
||||
pub header3: Option<QueuedRialtoHeader>,
|
||||
pub header4: Option<QueuedRialtoHeader>,
|
||||
}
|
||||
|
||||
impl HeadersBatch {
|
||||
/// Create new headers from given header & ids collections.
|
||||
///
|
||||
/// This method will pop `HEADERS_BATCH` items from both collections
|
||||
/// and construct `Headers` object and a vector of `RialtoHeaderId`s.
|
||||
pub fn pop_from(
|
||||
headers: &mut Vec<QueuedRialtoHeader>,
|
||||
ids: &mut Vec<RialtoHeaderId>,
|
||||
) -> Result<(Self, Vec<RialtoHeaderId>), ()> {
|
||||
if headers.len() != ids.len() {
|
||||
log::error!(target: "bridge", "Collection size mismatch ({} vs {})", headers.len(), ids.len());
|
||||
return Err(());
|
||||
}
|
||||
|
||||
let header1 = headers.pop().ok_or(())?;
|
||||
let header2 = headers.pop();
|
||||
let header3 = headers.pop();
|
||||
let header4 = headers.pop();
|
||||
|
||||
let mut submitting_ids = Vec::with_capacity(HEADERS_BATCH);
|
||||
for _ in 0..HEADERS_BATCH {
|
||||
submitting_ids.extend(ids.pop().iter());
|
||||
}
|
||||
|
||||
Ok((
|
||||
Self {
|
||||
header1,
|
||||
header2,
|
||||
header3,
|
||||
header4,
|
||||
},
|
||||
submitting_ids,
|
||||
))
|
||||
}
|
||||
|
||||
/// Returns unified array of headers.
|
||||
///
|
||||
/// The first element is always `Some`.
|
||||
fn headers(&self) -> [Option<&QueuedRialtoHeader>; HEADERS_BATCH] {
|
||||
[
|
||||
Some(&self.header1),
|
||||
self.header2.as_ref(),
|
||||
self.header3.as_ref(),
|
||||
self.header4.as_ref(),
|
||||
]
|
||||
}
|
||||
|
||||
/// Encodes all headers. If header is not present an empty vector will be returned.
|
||||
pub fn encode(&self) -> [Vec<u8>; HEADERS_BATCH] {
|
||||
let encode = |h: &QueuedRialtoHeader| h.header().encode();
|
||||
let headers = self.headers();
|
||||
[
|
||||
headers[0].map(encode).unwrap_or_default(),
|
||||
headers[1].map(encode).unwrap_or_default(),
|
||||
headers[2].map(encode).unwrap_or_default(),
|
||||
headers[3].map(encode).unwrap_or_default(),
|
||||
]
|
||||
}
|
||||
/// Returns number of contained headers.
|
||||
pub fn len(&self) -> usize {
|
||||
let is_set = |h: &Option<&QueuedRialtoHeader>| if h.is_some() { 1 } else { 0 };
|
||||
self.headers().iter().map(is_set).sum()
|
||||
}
|
||||
|
||||
/// Remove headers starting from `idx` (0-based) from this collection.
|
||||
///
|
||||
/// The collection will be left with `[0, idx)` headers.
|
||||
/// Returns `Err` when `idx == 0`, since `Headers` must contain at least one header,
|
||||
/// or when `idx > HEADERS_BATCH`.
|
||||
pub fn split_off(&mut self, idx: usize) -> Result<(), ()> {
|
||||
if idx == 0 || idx > HEADERS_BATCH {
|
||||
return Err(());
|
||||
}
|
||||
let mut vals: [_; HEADERS_BATCH] = [&mut None, &mut self.header2, &mut self.header3, &mut self.header4];
|
||||
for val in vals.iter_mut().skip(idx) {
|
||||
**val = None;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Substrate headers submitter API.
|
||||
#[async_trait]
|
||||
trait HeadersSubmitter {
|
||||
/// Returns Ok(0) if all given not-yet-imported headers are complete.
|
||||
/// Returns Ok(index != 0) where index is 1-based index of first header that is incomplete.
|
||||
///
|
||||
/// Returns Err(()) if contract has rejected headers. This means that the contract is
|
||||
/// unable to import first header (e.g. it may already be imported).
|
||||
async fn is_headers_incomplete(&self, headers: &HeadersBatch) -> RpcResult<usize>;
|
||||
|
||||
/// Submit given headers to Ethereum node.
|
||||
async fn submit_headers(&mut self, headers: HeadersBatch) -> RpcResult<()>;
|
||||
}
|
||||
|
||||
/// Implementation of Substrate headers submitter that sends headers to running Ethereum node.
|
||||
struct EthereumHeadersSubmitter {
|
||||
client: EthereumClient,
|
||||
params: EthereumSigningParams,
|
||||
contract_address: Address,
|
||||
nonce: U256,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl HeadersSubmitter for EthereumHeadersSubmitter {
|
||||
async fn is_headers_incomplete(&self, headers: &HeadersBatch) -> RpcResult<usize> {
|
||||
let [h1, h2, h3, h4] = headers.encode();
|
||||
let (encoded_call, call_decoder) = bridge_contract::functions::is_incomplete_headers::call(h1, h2, h3, h4);
|
||||
let call_request = CallRequest {
|
||||
to: Some(self.contract_address),
|
||||
data: Some(encoded_call.into()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let call_result = self.client.eth_call(call_request).await?;
|
||||
let incomplete_index: U256 = call_decoder.decode(&call_result.0)?;
|
||||
if incomplete_index > HEADERS_BATCH.into() {
|
||||
return Err(RpcError::Ethereum(EthereumNodeError::InvalidIncompleteIndex));
|
||||
}
|
||||
|
||||
Ok(incomplete_index.low_u32() as _)
|
||||
}
|
||||
|
||||
async fn submit_headers(&mut self, headers: HeadersBatch) -> RpcResult<()> {
|
||||
let [h1, h2, h3, h4] = headers.encode();
|
||||
let result = self
|
||||
.client
|
||||
.submit_ethereum_transaction(
|
||||
&self.params,
|
||||
Some(self.contract_address),
|
||||
Some(self.nonce),
|
||||
false,
|
||||
bridge_contract::functions::import_headers::encode_input(h1, h2, h3, h4),
|
||||
)
|
||||
.await;
|
||||
|
||||
if result.is_ok() {
|
||||
self.nonce += U256::one();
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
/// Submit multiple Substrate headers.
|
||||
async fn submit_substrate_headers(
|
||||
mut header_submitter: impl HeadersSubmitter,
|
||||
mut headers: Vec<QueuedRialtoHeader>,
|
||||
) -> SubmittedHeaders<RialtoHeaderId, RpcError> {
|
||||
let mut submitted_headers = SubmittedHeaders::default();
|
||||
|
||||
let mut ids = headers.iter().map(|header| header.id()).rev().collect::<Vec<_>>();
|
||||
headers.reverse();
|
||||
|
||||
while !headers.is_empty() {
|
||||
let (headers, submitting_ids) =
|
||||
HeadersBatch::pop_from(&mut headers, &mut ids).expect("Headers and ids are not empty; qed");
|
||||
|
||||
submitted_headers.fatal_error =
|
||||
submit_substrate_headers_batch(&mut header_submitter, &mut submitted_headers, submitting_ids, headers)
|
||||
.await;
|
||||
|
||||
if submitted_headers.fatal_error.is_some() {
|
||||
ids.reverse();
|
||||
submitted_headers.rejected.extend(ids);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
submitted_headers
|
||||
}
|
||||
|
||||
/// Submit 4 Substrate headers in single PoA transaction.
|
||||
async fn submit_substrate_headers_batch(
|
||||
header_submitter: &mut impl HeadersSubmitter,
|
||||
submitted_headers: &mut SubmittedHeaders<RialtoHeaderId, RpcError>,
|
||||
mut ids: Vec<RialtoHeaderId>,
|
||||
mut headers: HeadersBatch,
|
||||
) -> Option<RpcError> {
|
||||
debug_assert_eq!(ids.len(), headers.len(),);
|
||||
|
||||
// if parent of first header is either incomplete, or rejected, we assume that contract
|
||||
// will reject this header as well
|
||||
let parent_id = headers.header1.parent_id();
|
||||
if submitted_headers.rejected.contains(&parent_id) || submitted_headers.incomplete.contains(&parent_id) {
|
||||
submitted_headers.rejected.extend(ids);
|
||||
return None;
|
||||
}
|
||||
|
||||
// check if headers are incomplete
|
||||
let incomplete_header_index = match header_submitter.is_headers_incomplete(&headers).await {
|
||||
// All headers valid
|
||||
Ok(0) => None,
|
||||
Ok(incomplete_header_index) => Some(incomplete_header_index),
|
||||
Err(error) => {
|
||||
// contract has rejected all headers => we do not want to submit it
|
||||
submitted_headers.rejected.extend(ids);
|
||||
if error.is_connection_error() {
|
||||
return Some(error);
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Modify `ids` and `headers` to only contain values that are going to be accepted.
|
||||
let rejected = if let Some(idx) = incomplete_header_index {
|
||||
let len = std::cmp::min(idx, ids.len());
|
||||
headers
|
||||
.split_off(len)
|
||||
.expect("len > 0, the case where all headers are valid is converted to None; qed");
|
||||
ids.split_off(len)
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let submitted = ids;
|
||||
let submit_result = header_submitter.submit_headers(headers).await;
|
||||
match submit_result {
|
||||
Ok(_) => {
|
||||
if incomplete_header_index.is_some() {
|
||||
submitted_headers.incomplete.extend(submitted.iter().last().cloned());
|
||||
}
|
||||
submitted_headers.submitted.extend(submitted);
|
||||
submitted_headers.rejected.extend(rejected);
|
||||
None
|
||||
}
|
||||
Err(error) => {
|
||||
submitted_headers.rejected.extend(submitted);
|
||||
submitted_headers.rejected.extend(rejected);
|
||||
Some(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use sp_runtime::traits::Header;
|
||||
|
||||
struct TestHeadersSubmitter {
|
||||
incomplete: Vec<RialtoHeaderId>,
|
||||
failed: Vec<RialtoHeaderId>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl HeadersSubmitter for TestHeadersSubmitter {
|
||||
async fn is_headers_incomplete(&self, headers: &HeadersBatch) -> RpcResult<usize> {
|
||||
if self.incomplete.iter().any(|i| i.0 == headers.header1.id().0) {
|
||||
Ok(1)
|
||||
} else {
|
||||
Ok(0)
|
||||
}
|
||||
}
|
||||
|
||||
async fn submit_headers(&mut self, headers: HeadersBatch) -> RpcResult<()> {
|
||||
if self.failed.iter().any(|i| i.0 == headers.header1.id().0) {
|
||||
Err(RpcError::Ethereum(EthereumNodeError::InvalidSubstrateBlockNumber))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn header(number: rialto_runtime::BlockNumber) -> QueuedRialtoHeader {
|
||||
QueuedRialtoHeader::new(
|
||||
rialto_runtime::Header::new(
|
||||
number,
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
if number == 0 {
|
||||
Default::default()
|
||||
} else {
|
||||
header(number - 1).id().1
|
||||
},
|
||||
Default::default(),
|
||||
)
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn descendants_of_incomplete_headers_are_not_submitted() {
|
||||
let submitted_headers = async_std::task::block_on(submit_substrate_headers(
|
||||
TestHeadersSubmitter {
|
||||
incomplete: vec![header(5).id()],
|
||||
failed: vec![],
|
||||
},
|
||||
vec![header(5), header(6)],
|
||||
));
|
||||
assert_eq!(submitted_headers.submitted, vec![header(5).id()]);
|
||||
assert_eq!(submitted_headers.incomplete, vec![header(5).id()]);
|
||||
assert_eq!(submitted_headers.rejected, vec![header(6).id()]);
|
||||
assert!(submitted_headers.fatal_error.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn headers_after_fatal_error_are_not_submitted() {
|
||||
let submitted_headers = async_std::task::block_on(submit_substrate_headers(
|
||||
TestHeadersSubmitter {
|
||||
incomplete: vec![],
|
||||
failed: vec![header(9).id()],
|
||||
},
|
||||
vec![
|
||||
header(5),
|
||||
header(6),
|
||||
header(7),
|
||||
header(8),
|
||||
header(9),
|
||||
header(10),
|
||||
header(11),
|
||||
],
|
||||
));
|
||||
assert_eq!(
|
||||
submitted_headers.submitted,
|
||||
vec![header(5).id(), header(6).id(), header(7).id(), header(8).id()]
|
||||
);
|
||||
assert_eq!(submitted_headers.incomplete, vec![]);
|
||||
assert_eq!(
|
||||
submitted_headers.rejected,
|
||||
vec![header(9).id(), header(10).id(), header(11).id(),]
|
||||
);
|
||||
assert!(submitted_headers.fatal_error.is_some());
|
||||
}
|
||||
|
||||
fn headers_batch() -> HeadersBatch {
|
||||
let mut init_headers = vec![header(1), header(2), header(3), header(4), header(5)];
|
||||
init_headers.reverse();
|
||||
let mut init_ids = init_headers.iter().map(|h| h.id()).collect();
|
||||
let (headers, ids) = HeadersBatch::pop_from(&mut init_headers, &mut init_ids).unwrap();
|
||||
assert_eq!(init_headers, vec![header(5)]);
|
||||
assert_eq!(init_ids, vec![header(5).id()]);
|
||||
assert_eq!(
|
||||
ids,
|
||||
vec![header(1).id(), header(2).id(), header(3).id(), header(4).id()]
|
||||
);
|
||||
headers
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn headers_batch_len() {
|
||||
let headers = headers_batch();
|
||||
assert_eq!(headers.len(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn headers_batch_encode() {
|
||||
let headers = headers_batch();
|
||||
assert_eq!(
|
||||
headers.encode(),
|
||||
[
|
||||
header(1).header().encode(),
|
||||
header(2).header().encode(),
|
||||
header(3).header().encode(),
|
||||
header(4).header().encode(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn headers_batch_split_off() {
|
||||
// given
|
||||
let mut headers = headers_batch();
|
||||
|
||||
// when
|
||||
assert!(headers.split_off(0).is_err());
|
||||
assert_eq!(headers.header1, header(1));
|
||||
assert!(headers.header2.is_some());
|
||||
assert!(headers.header3.is_some());
|
||||
assert!(headers.header4.is_some());
|
||||
|
||||
// when
|
||||
let mut h = headers.clone();
|
||||
h.split_off(1).unwrap();
|
||||
assert!(h.header2.is_none());
|
||||
assert!(h.header3.is_none());
|
||||
assert!(h.header4.is_none());
|
||||
|
||||
// when
|
||||
let mut h = headers.clone();
|
||||
h.split_off(2).unwrap();
|
||||
assert!(h.header2.is_some());
|
||||
assert!(h.header3.is_none());
|
||||
assert!(h.header4.is_none());
|
||||
|
||||
// when
|
||||
let mut h = headers.clone();
|
||||
h.split_off(3).unwrap();
|
||||
assert!(h.header2.is_some());
|
||||
assert!(h.header3.is_some());
|
||||
assert!(h.header4.is_none());
|
||||
|
||||
// when
|
||||
let mut h = headers;
|
||||
h.split_off(4).unwrap();
|
||||
assert!(h.header2.is_some());
|
||||
assert!(h.header3.is_some());
|
||||
assert!(h.header4.is_some());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
// Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::ethereum_client::{bridge_contract, EthereumHighLevelRpc};
|
||||
use crate::rpc_errors::RpcError;
|
||||
|
||||
use codec::{Decode, Encode};
|
||||
use num_traits::Zero;
|
||||
use relay_ethereum_client::{
|
||||
Client as EthereumClient, ConnectionParams as EthereumConnectionParams, SigningParams as EthereumSigningParams,
|
||||
};
|
||||
use relay_rialto_client::{HeaderId as RialtoHeaderId, Rialto};
|
||||
use relay_substrate_client::{
|
||||
Client as SubstrateClient, ConnectionParams as SubstrateConnectionParams, OpaqueGrandpaAuthoritiesSet,
|
||||
};
|
||||
use relay_utils::HeaderId;
|
||||
|
||||
/// Ethereum synchronization parameters.
|
||||
#[derive(Debug)]
|
||||
pub struct EthereumDeployContractParams {
|
||||
/// Ethereum connection params.
|
||||
pub eth_params: EthereumConnectionParams,
|
||||
/// Ethereum signing params.
|
||||
pub eth_sign: EthereumSigningParams,
|
||||
/// Ethereum contract bytecode.
|
||||
pub eth_contract_code: Vec<u8>,
|
||||
/// Substrate connection params.
|
||||
pub sub_params: SubstrateConnectionParams,
|
||||
/// Initial authorities set id.
|
||||
pub sub_initial_authorities_set_id: Option<u64>,
|
||||
/// Initial authorities set.
|
||||
pub sub_initial_authorities_set: Option<Vec<u8>>,
|
||||
/// Initial header.
|
||||
pub sub_initial_header: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
/// Deploy Bridge contract on Ethereum chain.
|
||||
pub fn run(params: EthereumDeployContractParams) {
|
||||
let mut local_pool = futures::executor::LocalPool::new();
|
||||
|
||||
let EthereumDeployContractParams {
|
||||
eth_params,
|
||||
eth_sign,
|
||||
sub_params,
|
||||
sub_initial_authorities_set_id,
|
||||
sub_initial_authorities_set,
|
||||
sub_initial_header,
|
||||
eth_contract_code,
|
||||
} = params;
|
||||
|
||||
let result = local_pool.run_until(async move {
|
||||
let eth_client = EthereumClient::new(eth_params);
|
||||
let sub_client = SubstrateClient::<Rialto>::new(sub_params).await.map_err(RpcError::Substrate)?;
|
||||
|
||||
let (initial_header_id, initial_header) = prepare_initial_header(&sub_client, sub_initial_header).await?;
|
||||
let initial_set_id = sub_initial_authorities_set_id.unwrap_or(0);
|
||||
let initial_set = prepare_initial_authorities_set(
|
||||
&sub_client,
|
||||
initial_header_id.1,
|
||||
sub_initial_authorities_set,
|
||||
).await?;
|
||||
|
||||
log::info!(
|
||||
target: "bridge",
|
||||
"Deploying Ethereum contract.\r\n\tInitial header: {:?}\r\n\tInitial header id: {:?}\r\n\tInitial header encoded: {}\r\n\tInitial authorities set ID: {}\r\n\tInitial authorities set: {}",
|
||||
initial_header,
|
||||
initial_header_id,
|
||||
hex::encode(&initial_header),
|
||||
initial_set_id,
|
||||
hex::encode(&initial_set),
|
||||
);
|
||||
|
||||
deploy_bridge_contract(
|
||||
ð_client,
|
||||
ð_sign,
|
||||
eth_contract_code,
|
||||
initial_header,
|
||||
initial_set_id,
|
||||
initial_set,
|
||||
).await
|
||||
});
|
||||
|
||||
if let Err(error) = result {
|
||||
log::error!(target: "bridge", "{}", error);
|
||||
}
|
||||
}
|
||||
|
||||
/// Prepare initial header.
|
||||
async fn prepare_initial_header(
|
||||
sub_client: &SubstrateClient<Rialto>,
|
||||
sub_initial_header: Option<Vec<u8>>,
|
||||
) -> Result<(RialtoHeaderId, Vec<u8>), String> {
|
||||
match sub_initial_header {
|
||||
Some(raw_initial_header) => match rialto_runtime::Header::decode(&mut &raw_initial_header[..]) {
|
||||
Ok(initial_header) => Ok((
|
||||
HeaderId(initial_header.number, initial_header.hash()),
|
||||
raw_initial_header,
|
||||
)),
|
||||
Err(error) => Err(format!("Error decoding initial header: {}", error)),
|
||||
},
|
||||
None => {
|
||||
let initial_header = sub_client.header_by_number(Zero::zero()).await;
|
||||
initial_header
|
||||
.map(|header| (HeaderId(Zero::zero(), header.hash()), header.encode()))
|
||||
.map_err(|error| format!("Error reading Substrate genesis header: {:?}", error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Prepare initial GRANDPA authorities set.
|
||||
async fn prepare_initial_authorities_set(
|
||||
sub_client: &SubstrateClient<Rialto>,
|
||||
sub_initial_header_hash: rialto_runtime::Hash,
|
||||
sub_initial_authorities_set: Option<Vec<u8>>,
|
||||
) -> Result<OpaqueGrandpaAuthoritiesSet, String> {
|
||||
let initial_authorities_set = match sub_initial_authorities_set {
|
||||
Some(initial_authorities_set) => Ok(initial_authorities_set),
|
||||
None => sub_client.grandpa_authorities_set(sub_initial_header_hash).await,
|
||||
};
|
||||
|
||||
initial_authorities_set.map_err(|error| format!("Error reading GRANDPA authorities set: {:?}", error))
|
||||
}
|
||||
|
||||
/// Deploy bridge contract to Ethereum chain.
|
||||
async fn deploy_bridge_contract(
|
||||
eth_client: &EthereumClient,
|
||||
params: &EthereumSigningParams,
|
||||
contract_code: Vec<u8>,
|
||||
initial_header: Vec<u8>,
|
||||
initial_set_id: u64,
|
||||
initial_authorities: Vec<u8>,
|
||||
) -> Result<(), String> {
|
||||
eth_client
|
||||
.submit_ethereum_transaction(
|
||||
params,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
bridge_contract::constructor(contract_code, initial_header, initial_set_id, initial_authorities),
|
||||
)
|
||||
.await
|
||||
.map_err(|error| format!("Error deploying contract: {:?}", error))
|
||||
}
|
||||
@@ -0,0 +1,394 @@
|
||||
// Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Relaying proofs of PoA -> Substrate exchange transactions.
|
||||
|
||||
use crate::instances::BridgeInstance;
|
||||
use crate::rialto_client::{SubmitEthereumExchangeTransactionProof, SubstrateHighLevelRpc};
|
||||
use crate::rpc_errors::RpcError;
|
||||
use crate::substrate_types::into_substrate_ethereum_receipt;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use bp_currency_exchange::MaybeLockFundsTransaction;
|
||||
use exchange_relay::exchange::{
|
||||
relay_single_transaction_proof, SourceBlock, SourceClient, SourceTransaction, TargetClient,
|
||||
TransactionProofPipeline,
|
||||
};
|
||||
use exchange_relay::exchange_loop::{run as run_loop, InMemoryStorage};
|
||||
use relay_ethereum_client::{
|
||||
types::{
|
||||
HeaderId as EthereumHeaderId, HeaderWithTransactions as EthereumHeaderWithTransactions,
|
||||
Transaction as EthereumTransaction, TransactionHash as EthereumTransactionHash, H256, HEADER_ID_PROOF,
|
||||
},
|
||||
Client as EthereumClient, ConnectionParams as EthereumConnectionParams,
|
||||
};
|
||||
use relay_rialto_client::{Rialto, SigningParams as RialtoSigningParams};
|
||||
use relay_substrate_client::{
|
||||
Chain as SubstrateChain, Client as SubstrateClient, ConnectionParams as SubstrateConnectionParams,
|
||||
};
|
||||
use relay_utils::{metrics::MetricsParams, relay_loop::Client as RelayClient, HeaderId};
|
||||
use rialto_runtime::exchange::EthereumTransactionInclusionProof;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
/// Interval at which we ask Ethereum node for updates.
|
||||
const ETHEREUM_TICK_INTERVAL: Duration = Duration::from_secs(10);
|
||||
|
||||
/// Exchange relay mode.
|
||||
#[derive(Debug)]
|
||||
pub enum ExchangeRelayMode {
|
||||
/// Relay single transaction and quit.
|
||||
Single(EthereumTransactionHash),
|
||||
/// Auto-relay transactions starting with given block.
|
||||
Auto(Option<u64>),
|
||||
}
|
||||
|
||||
/// PoA exchange transaction relay params.
|
||||
#[derive(Debug)]
|
||||
pub struct EthereumExchangeParams {
|
||||
/// Ethereum connection params.
|
||||
pub eth_params: EthereumConnectionParams,
|
||||
/// Substrate connection params.
|
||||
pub sub_params: SubstrateConnectionParams,
|
||||
/// Substrate signing params.
|
||||
pub sub_sign: RialtoSigningParams,
|
||||
/// Relay working mode.
|
||||
pub mode: ExchangeRelayMode,
|
||||
/// Metrics parameters.
|
||||
pub metrics_params: Option<MetricsParams>,
|
||||
/// Instance of the bridge pallet being synchronized.
|
||||
pub instance: Arc<dyn BridgeInstance>,
|
||||
}
|
||||
|
||||
/// Ethereum to Substrate exchange pipeline.
|
||||
struct EthereumToSubstrateExchange;
|
||||
|
||||
impl TransactionProofPipeline for EthereumToSubstrateExchange {
|
||||
const SOURCE_NAME: &'static str = "Ethereum";
|
||||
const TARGET_NAME: &'static str = "Substrate";
|
||||
|
||||
type Block = EthereumSourceBlock;
|
||||
type TransactionProof = EthereumTransactionInclusionProof;
|
||||
}
|
||||
|
||||
/// Ethereum source block.
|
||||
struct EthereumSourceBlock(EthereumHeaderWithTransactions);
|
||||
|
||||
impl SourceBlock for EthereumSourceBlock {
|
||||
type Hash = H256;
|
||||
type Number = u64;
|
||||
type Transaction = EthereumSourceTransaction;
|
||||
|
||||
fn id(&self) -> EthereumHeaderId {
|
||||
HeaderId(
|
||||
self.0.number.expect(HEADER_ID_PROOF).as_u64(),
|
||||
self.0.hash.expect(HEADER_ID_PROOF),
|
||||
)
|
||||
}
|
||||
|
||||
fn transactions(&self) -> Vec<Self::Transaction> {
|
||||
self.0
|
||||
.transactions
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(EthereumSourceTransaction)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Ethereum source transaction.
|
||||
struct EthereumSourceTransaction(EthereumTransaction);
|
||||
|
||||
impl SourceTransaction for EthereumSourceTransaction {
|
||||
type Hash = EthereumTransactionHash;
|
||||
|
||||
fn hash(&self) -> Self::Hash {
|
||||
self.0.hash
|
||||
}
|
||||
}
|
||||
|
||||
/// Ethereum node as transactions proof source.
|
||||
#[derive(Clone)]
|
||||
struct EthereumTransactionsSource {
|
||||
client: EthereumClient,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl RelayClient for EthereumTransactionsSource {
|
||||
type Error = RpcError;
|
||||
|
||||
async fn reconnect(&mut self) -> Result<(), RpcError> {
|
||||
self.client.reconnect();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl SourceClient<EthereumToSubstrateExchange> for EthereumTransactionsSource {
|
||||
async fn tick(&self) {
|
||||
async_std::task::sleep(ETHEREUM_TICK_INTERVAL).await;
|
||||
}
|
||||
|
||||
async fn block_by_hash(&self, hash: H256) -> Result<EthereumSourceBlock, RpcError> {
|
||||
self.client
|
||||
.header_by_hash_with_transactions(hash)
|
||||
.await
|
||||
.map(EthereumSourceBlock)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn block_by_number(&self, number: u64) -> Result<EthereumSourceBlock, RpcError> {
|
||||
self.client
|
||||
.header_by_number_with_transactions(number)
|
||||
.await
|
||||
.map(EthereumSourceBlock)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn transaction_block(
|
||||
&self,
|
||||
hash: &EthereumTransactionHash,
|
||||
) -> Result<Option<(EthereumHeaderId, usize)>, RpcError> {
|
||||
let eth_tx = match self.client.transaction_by_hash(*hash).await? {
|
||||
Some(eth_tx) => eth_tx,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
// we need transaction to be mined => check if it is included in the block
|
||||
let (eth_header_id, eth_tx_index) = match (eth_tx.block_number, eth_tx.block_hash, eth_tx.transaction_index) {
|
||||
(Some(block_number), Some(block_hash), Some(transaction_index)) => (
|
||||
HeaderId(block_number.as_u64(), block_hash),
|
||||
transaction_index.as_u64() as _,
|
||||
),
|
||||
_ => return Ok(None),
|
||||
};
|
||||
|
||||
Ok(Some((eth_header_id, eth_tx_index)))
|
||||
}
|
||||
|
||||
async fn transaction_proof(
|
||||
&self,
|
||||
block: &EthereumSourceBlock,
|
||||
tx_index: usize,
|
||||
) -> Result<EthereumTransactionInclusionProof, RpcError> {
|
||||
const TRANSACTION_HAS_RAW_FIELD_PROOF: &str = "RPC level checks that transactions from Ethereum\
|
||||
node are having `raw` field; qed";
|
||||
const BLOCK_HAS_HASH_FIELD_PROOF: &str = "RPC level checks that block has `hash` field; qed";
|
||||
|
||||
let mut transaction_proof = Vec::with_capacity(block.0.transactions.len());
|
||||
for tx in &block.0.transactions {
|
||||
let raw_tx_receipt = self
|
||||
.client
|
||||
.transaction_receipt(tx.hash)
|
||||
.await
|
||||
.map(|receipt| into_substrate_ethereum_receipt(&receipt))
|
||||
.map(|receipt| receipt.rlp())?;
|
||||
let raw_tx = tx.raw.clone().expect(TRANSACTION_HAS_RAW_FIELD_PROOF).0;
|
||||
transaction_proof.push((raw_tx, raw_tx_receipt));
|
||||
}
|
||||
|
||||
Ok(EthereumTransactionInclusionProof {
|
||||
block: block.0.hash.expect(BLOCK_HAS_HASH_FIELD_PROOF),
|
||||
index: tx_index as _,
|
||||
proof: transaction_proof,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Substrate node as transactions proof target.
|
||||
#[derive(Clone)]
|
||||
struct SubstrateTransactionsTarget {
|
||||
client: SubstrateClient<Rialto>,
|
||||
sign_params: RialtoSigningParams,
|
||||
bridge_instance: Arc<dyn BridgeInstance>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl RelayClient for SubstrateTransactionsTarget {
|
||||
type Error = RpcError;
|
||||
|
||||
async fn reconnect(&mut self) -> Result<(), RpcError> {
|
||||
Ok(self.client.reconnect().await?)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl TargetClient<EthereumToSubstrateExchange> for SubstrateTransactionsTarget {
|
||||
async fn tick(&self) {
|
||||
async_std::task::sleep(Rialto::AVERAGE_BLOCK_INTERVAL).await;
|
||||
}
|
||||
|
||||
async fn is_header_known(&self, id: &EthereumHeaderId) -> Result<bool, RpcError> {
|
||||
self.client.ethereum_header_known(*id).await
|
||||
}
|
||||
|
||||
async fn is_header_finalized(&self, id: &EthereumHeaderId) -> Result<bool, RpcError> {
|
||||
// we check if header is finalized by simple comparison of the header number and
|
||||
// number of best finalized PoA header known to Substrate node.
|
||||
//
|
||||
// this may lead to failure in tx proof import if PoA reorganization has happened
|
||||
// after we have checked that our tx has been included into given block
|
||||
//
|
||||
// the fix is easy, but since this code is mostly developed for demonstration purposes,
|
||||
// I'm leaving this KISS-based design here
|
||||
let best_finalized_ethereum_block = self.client.best_ethereum_finalized_block().await?;
|
||||
Ok(id.0 <= best_finalized_ethereum_block.0)
|
||||
}
|
||||
|
||||
async fn best_finalized_header_id(&self) -> Result<EthereumHeaderId, RpcError> {
|
||||
// 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
|
||||
}
|
||||
|
||||
async fn filter_transaction_proof(&self, proof: &EthereumTransactionInclusionProof) -> Result<bool, RpcError> {
|
||||
// let's try to parse transaction locally
|
||||
let (raw_tx, raw_tx_receipt) = &proof.proof[proof.index as usize];
|
||||
let parse_result = rialto_runtime::exchange::EthTransaction::parse(raw_tx);
|
||||
if parse_result.is_err() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// now let's check if transaction is successful
|
||||
match bp_eth_poa::Receipt::is_successful_raw_receipt(raw_tx_receipt) {
|
||||
Ok(true) => (),
|
||||
_ => return Ok(false),
|
||||
}
|
||||
|
||||
// seems that transaction is relayable - let's check if runtime is able to import it
|
||||
// (we can't if e.g. header is pruned or there's some issue with tx data)
|
||||
self.client.verify_exchange_transaction_proof(proof.clone()).await
|
||||
}
|
||||
|
||||
async fn submit_transaction_proof(&self, proof: EthereumTransactionInclusionProof) -> Result<(), RpcError> {
|
||||
let (sign_params, bridge_instance) = (self.sign_params.clone(), self.bridge_instance.clone());
|
||||
self.client
|
||||
.submit_exchange_transaction_proof(sign_params, bridge_instance, proof)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
/// Relay exchange transaction proof(s) to Substrate node.
|
||||
pub fn run(params: EthereumExchangeParams) {
|
||||
match params.mode {
|
||||
ExchangeRelayMode::Single(eth_tx_hash) => run_single_transaction_relay(params, eth_tx_hash),
|
||||
ExchangeRelayMode::Auto(eth_start_with_block_number) => {
|
||||
run_auto_transactions_relay_loop(params, eth_start_with_block_number)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Run single transaction proof relay and stop.
|
||||
fn run_single_transaction_relay(params: EthereumExchangeParams, eth_tx_hash: H256) {
|
||||
let mut local_pool = futures::executor::LocalPool::new();
|
||||
|
||||
let EthereumExchangeParams {
|
||||
eth_params,
|
||||
sub_params,
|
||||
sub_sign,
|
||||
instance,
|
||||
..
|
||||
} = params;
|
||||
|
||||
let result = local_pool.run_until(async move {
|
||||
let eth_client = EthereumClient::new(eth_params);
|
||||
let sub_client = SubstrateClient::<Rialto>::new(sub_params)
|
||||
.await
|
||||
.map_err(RpcError::Substrate)?;
|
||||
|
||||
let source = EthereumTransactionsSource { client: eth_client };
|
||||
let target = SubstrateTransactionsTarget {
|
||||
client: sub_client,
|
||||
sign_params: sub_sign,
|
||||
bridge_instance: instance,
|
||||
};
|
||||
|
||||
relay_single_transaction_proof(&source, &target, eth_tx_hash).await
|
||||
});
|
||||
|
||||
match result {
|
||||
Ok(_) => {
|
||||
log::info!(
|
||||
target: "bridge",
|
||||
"Ethereum transaction {} proof has been successfully submitted to Substrate node",
|
||||
eth_tx_hash,
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(
|
||||
target: "bridge",
|
||||
"Error submitting Ethereum transaction {} proof to Substrate node: {}",
|
||||
eth_tx_hash,
|
||||
err,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Run auto-relay loop.
|
||||
fn run_auto_transactions_relay_loop(params: EthereumExchangeParams, eth_start_with_block_number: Option<u64>) {
|
||||
let EthereumExchangeParams {
|
||||
eth_params,
|
||||
sub_params,
|
||||
sub_sign,
|
||||
metrics_params,
|
||||
instance,
|
||||
..
|
||||
} = params;
|
||||
|
||||
let do_run_loop = move || -> Result<(), String> {
|
||||
let eth_client = EthereumClient::new(eth_params);
|
||||
let sub_client = async_std::task::block_on(SubstrateClient::<Rialto>::new(sub_params))
|
||||
.map_err(|err| format!("Error starting Substrate client: {:?}", err))?;
|
||||
|
||||
let eth_start_with_block_number = match eth_start_with_block_number {
|
||||
Some(eth_start_with_block_number) => eth_start_with_block_number,
|
||||
None => {
|
||||
async_std::task::block_on(sub_client.best_ethereum_finalized_block())
|
||||
.map_err(|err| {
|
||||
format!(
|
||||
"Error retrieving best finalized Ethereum block from Substrate node: {:?}",
|
||||
err
|
||||
)
|
||||
})?
|
||||
.0
|
||||
}
|
||||
};
|
||||
|
||||
run_loop(
|
||||
InMemoryStorage::new(eth_start_with_block_number),
|
||||
EthereumTransactionsSource { client: eth_client },
|
||||
SubstrateTransactionsTarget {
|
||||
client: sub_client,
|
||||
sign_params: sub_sign,
|
||||
bridge_instance: instance,
|
||||
},
|
||||
metrics_params,
|
||||
futures::future::pending(),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
};
|
||||
|
||||
if let Err(err) = do_run_loop() {
|
||||
log::error!(
|
||||
target: "bridge",
|
||||
"Error auto-relaying Ethereum transactions proofs to Substrate node: {}",
|
||||
err,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
// Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Submitting Ethereum -> Substrate exchange transactions.
|
||||
|
||||
use bp_eth_poa::{
|
||||
signatures::{secret_to_address, SignTransaction},
|
||||
UnsignedTransaction,
|
||||
};
|
||||
use relay_ethereum_client::{
|
||||
types::{CallRequest, U256},
|
||||
Client as EthereumClient, ConnectionParams as EthereumConnectionParams, SigningParams as EthereumSigningParams,
|
||||
};
|
||||
use rialto_runtime::exchange::LOCK_FUNDS_ADDRESS;
|
||||
|
||||
/// Ethereum exchange transaction params.
|
||||
#[derive(Debug)]
|
||||
pub struct EthereumExchangeSubmitParams {
|
||||
/// Ethereum connection params.
|
||||
pub eth_params: EthereumConnectionParams,
|
||||
/// Ethereum signing params.
|
||||
pub eth_sign: EthereumSigningParams,
|
||||
/// Ethereum signer nonce.
|
||||
pub eth_nonce: Option<U256>,
|
||||
/// Amount of Ethereum tokens to lock.
|
||||
pub eth_amount: U256,
|
||||
/// Funds recipient on Substrate side.
|
||||
pub sub_recipient: [u8; 32],
|
||||
}
|
||||
|
||||
/// Submit single Ethereum -> Substrate exchange transaction.
|
||||
pub fn run(params: EthereumExchangeSubmitParams) {
|
||||
let mut local_pool = futures::executor::LocalPool::new();
|
||||
|
||||
let EthereumExchangeSubmitParams {
|
||||
eth_params,
|
||||
eth_sign,
|
||||
eth_nonce,
|
||||
eth_amount,
|
||||
sub_recipient,
|
||||
} = params;
|
||||
|
||||
let result: Result<_, String> = local_pool.run_until(async move {
|
||||
let eth_client = EthereumClient::new(eth_params);
|
||||
|
||||
let eth_signer_address = secret_to_address(ð_sign.signer);
|
||||
let sub_recipient_encoded = sub_recipient;
|
||||
let nonce = match eth_nonce {
|
||||
Some(eth_nonce) => eth_nonce,
|
||||
None => eth_client
|
||||
.account_nonce(eth_signer_address)
|
||||
.await
|
||||
.map_err(|err| format!("error fetching acount nonce: {:?}", err))?,
|
||||
};
|
||||
let gas = eth_client
|
||||
.estimate_gas(CallRequest {
|
||||
from: Some(eth_signer_address),
|
||||
to: Some(LOCK_FUNDS_ADDRESS.into()),
|
||||
value: Some(eth_amount),
|
||||
data: Some(sub_recipient_encoded.to_vec().into()),
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.map_err(|err| format!("error estimating gas requirements: {:?}", err))?;
|
||||
let eth_tx_unsigned = UnsignedTransaction {
|
||||
nonce,
|
||||
gas_price: eth_sign.gas_price,
|
||||
gas,
|
||||
to: Some(LOCK_FUNDS_ADDRESS.into()),
|
||||
value: eth_amount,
|
||||
payload: sub_recipient_encoded.to_vec(),
|
||||
};
|
||||
let eth_tx_signed = eth_tx_unsigned
|
||||
.clone()
|
||||
.sign_by(ð_sign.signer, Some(eth_sign.chain_id));
|
||||
eth_client
|
||||
.submit_transaction(eth_tx_signed)
|
||||
.await
|
||||
.map_err(|err| format!("error submitting transaction: {:?}", err))?;
|
||||
|
||||
Ok(eth_tx_unsigned)
|
||||
});
|
||||
|
||||
match result {
|
||||
Ok(eth_tx_unsigned) => {
|
||||
log::info!(
|
||||
target: "bridge",
|
||||
"Exchange transaction has been submitted to Ethereum node: {:?}",
|
||||
eth_tx_unsigned,
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(
|
||||
target: "bridge",
|
||||
"Error submitting exchange transaction to Ethereum node: {}",
|
||||
err,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
// Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Ethereum PoA -> Rialto-Substrate synchronization.
|
||||
|
||||
use crate::ethereum_client::EthereumHighLevelRpc;
|
||||
use crate::instances::BridgeInstance;
|
||||
use crate::rialto_client::{SubmitEthereumHeaders, SubstrateHighLevelRpc};
|
||||
use crate::rpc_errors::RpcError;
|
||||
use crate::substrate_types::{into_substrate_ethereum_header, into_substrate_ethereum_receipts};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use codec::Encode;
|
||||
use headers_relay::{
|
||||
sync::{HeadersSyncParams, TargetTransactionMode},
|
||||
sync_loop::{SourceClient, TargetClient},
|
||||
sync_types::{HeadersSyncPipeline, QueuedHeader, SourceHeader, SubmittedHeaders},
|
||||
};
|
||||
use relay_ethereum_client::{
|
||||
types::{HeaderHash, HeaderId as EthereumHeaderId, Receipt, SyncHeader as Header},
|
||||
Client as EthereumClient, ConnectionParams as EthereumConnectionParams,
|
||||
};
|
||||
use relay_rialto_client::{Rialto, SigningParams as RialtoSigningParams};
|
||||
use relay_substrate_client::{
|
||||
Chain as SubstrateChain, Client as SubstrateClient, ConnectionParams as SubstrateConnectionParams,
|
||||
};
|
||||
use relay_utils::{metrics::MetricsParams, relay_loop::Client as RelayClient};
|
||||
|
||||
use std::fmt::Debug;
|
||||
use std::{collections::HashSet, sync::Arc, time::Duration};
|
||||
|
||||
pub mod consts {
|
||||
use super::*;
|
||||
|
||||
/// Interval at which we check new Ethereum headers when we are synced/almost synced.
|
||||
pub const ETHEREUM_TICK_INTERVAL: Duration = Duration::from_secs(10);
|
||||
/// Max number of headers in single submit transaction.
|
||||
pub const MAX_HEADERS_IN_SINGLE_SUBMIT: usize = 32;
|
||||
/// Max total size of headers in single submit transaction. This only affects signed
|
||||
/// submissions, when several headers are submitted at once. 4096 is the maximal **expected**
|
||||
/// size of the Ethereum header + transactions receipts (if they're required).
|
||||
pub const MAX_HEADERS_SIZE_IN_SINGLE_SUBMIT: usize = MAX_HEADERS_IN_SINGLE_SUBMIT * 4096;
|
||||
/// Max Ethereum headers we want to have in all 'before-submitted' states.
|
||||
pub const MAX_FUTURE_HEADERS_TO_DOWNLOAD: usize = 128;
|
||||
/// Max Ethereum headers count we want to have in 'submitted' state.
|
||||
pub const MAX_SUBMITTED_HEADERS: usize = 128;
|
||||
/// Max depth of in-memory headers in all states. Past this depth they will be forgotten (pruned).
|
||||
pub const PRUNE_DEPTH: u32 = 4096;
|
||||
}
|
||||
|
||||
/// Ethereum synchronization parameters.
|
||||
#[derive(Debug)]
|
||||
pub struct EthereumSyncParams {
|
||||
/// Ethereum connection params.
|
||||
pub eth_params: EthereumConnectionParams,
|
||||
/// Substrate connection params.
|
||||
pub sub_params: SubstrateConnectionParams,
|
||||
/// Substrate signing params.
|
||||
pub sub_sign: RialtoSigningParams,
|
||||
/// Synchronization parameters.
|
||||
pub sync_params: HeadersSyncParams,
|
||||
/// Metrics parameters.
|
||||
pub metrics_params: Option<MetricsParams>,
|
||||
/// Instance of the bridge pallet being synchronized.
|
||||
pub instance: Arc<dyn BridgeInstance>,
|
||||
}
|
||||
|
||||
/// Ethereum synchronization pipeline.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
pub struct EthereumHeadersSyncPipeline;
|
||||
|
||||
impl HeadersSyncPipeline for EthereumHeadersSyncPipeline {
|
||||
const SOURCE_NAME: &'static str = "Ethereum";
|
||||
const TARGET_NAME: &'static str = "Substrate";
|
||||
|
||||
type Hash = HeaderHash;
|
||||
type Number = u64;
|
||||
type Header = Header;
|
||||
type Extra = Vec<Receipt>;
|
||||
type Completion = ();
|
||||
|
||||
fn estimate_size(source: &QueuedHeader<Self>) -> usize {
|
||||
into_substrate_ethereum_header(source.header()).encode().len()
|
||||
+ into_substrate_ethereum_receipts(source.extra())
|
||||
.map(|extra| extra.encode().len())
|
||||
.unwrap_or(0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Queued ethereum header ID.
|
||||
pub type QueuedEthereumHeader = QueuedHeader<EthereumHeadersSyncPipeline>;
|
||||
|
||||
/// Ethereum client as headers source.
|
||||
#[derive(Clone)]
|
||||
struct EthereumHeadersSource {
|
||||
/// Ethereum node client.
|
||||
client: EthereumClient,
|
||||
}
|
||||
|
||||
impl EthereumHeadersSource {
|
||||
fn new(client: EthereumClient) -> Self {
|
||||
Self { client }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl RelayClient for EthereumHeadersSource {
|
||||
type Error = RpcError;
|
||||
|
||||
async fn reconnect(&mut self) -> Result<(), RpcError> {
|
||||
self.client.reconnect();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl SourceClient<EthereumHeadersSyncPipeline> for EthereumHeadersSource {
|
||||
async fn best_block_number(&self) -> Result<u64, RpcError> {
|
||||
// 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)
|
||||
}
|
||||
|
||||
async fn header_by_hash(&self, hash: HeaderHash) -> Result<Header, RpcError> {
|
||||
self.client
|
||||
.header_by_hash(hash)
|
||||
.await
|
||||
.map(Into::into)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn header_by_number(&self, number: u64) -> Result<Header, RpcError> {
|
||||
self.client
|
||||
.header_by_number(number)
|
||||
.await
|
||||
.map(Into::into)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn header_completion(&self, id: EthereumHeaderId) -> Result<(EthereumHeaderId, Option<()>), RpcError> {
|
||||
Ok((id, None))
|
||||
}
|
||||
|
||||
async fn header_extra(
|
||||
&self,
|
||||
id: EthereumHeaderId,
|
||||
header: QueuedEthereumHeader,
|
||||
) -> Result<(EthereumHeaderId, Vec<Receipt>), RpcError> {
|
||||
self.client
|
||||
.transaction_receipts(id, header.header().transactions.clone())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct SubstrateHeadersTarget {
|
||||
/// Substrate node client.
|
||||
client: SubstrateClient<Rialto>,
|
||||
/// Whether we want to submit signed (true), or unsigned (false) transactions.
|
||||
sign_transactions: bool,
|
||||
/// Substrate signing params.
|
||||
sign_params: RialtoSigningParams,
|
||||
/// Bridge instance used in Ethereum to Substrate sync.
|
||||
bridge_instance: Arc<dyn BridgeInstance>,
|
||||
}
|
||||
|
||||
impl SubstrateHeadersTarget {
|
||||
fn new(
|
||||
client: SubstrateClient<Rialto>,
|
||||
sign_transactions: bool,
|
||||
sign_params: RialtoSigningParams,
|
||||
bridge_instance: Arc<dyn BridgeInstance>,
|
||||
) -> Self {
|
||||
Self {
|
||||
client,
|
||||
sign_transactions,
|
||||
sign_params,
|
||||
bridge_instance,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl RelayClient for SubstrateHeadersTarget {
|
||||
type Error = RpcError;
|
||||
|
||||
async fn reconnect(&mut self) -> Result<(), RpcError> {
|
||||
Ok(self.client.reconnect().await?)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl TargetClient<EthereumHeadersSyncPipeline> for SubstrateHeadersTarget {
|
||||
async fn best_header_id(&self) -> Result<EthereumHeaderId, RpcError> {
|
||||
// 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
|
||||
}
|
||||
|
||||
async fn is_known_header(&self, id: EthereumHeaderId) -> Result<(EthereumHeaderId, bool), RpcError> {
|
||||
Ok((id, self.client.ethereum_header_known(id).await?))
|
||||
}
|
||||
|
||||
async fn submit_headers(&self, headers: Vec<QueuedEthereumHeader>) -> SubmittedHeaders<EthereumHeaderId, RpcError> {
|
||||
let (sign_params, bridge_instance, sign_transactions) = (
|
||||
self.sign_params.clone(),
|
||||
self.bridge_instance.clone(),
|
||||
self.sign_transactions,
|
||||
);
|
||||
self.client
|
||||
.submit_ethereum_headers(sign_params, bridge_instance, headers, sign_transactions)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn incomplete_headers_ids(&self) -> Result<HashSet<EthereumHeaderId>, RpcError> {
|
||||
Ok(HashSet::new())
|
||||
}
|
||||
|
||||
#[allow(clippy::unit_arg)]
|
||||
async fn complete_header(&self, id: EthereumHeaderId, _completion: ()) -> Result<EthereumHeaderId, RpcError> {
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
async fn requires_extra(&self, header: QueuedEthereumHeader) -> Result<(EthereumHeaderId, bool), RpcError> {
|
||||
// we can minimize number of receipts_check calls by checking header
|
||||
// logs bloom here, but it may give us false positives (when authorities
|
||||
// source is contract, we never need any logs)
|
||||
let id = header.header().id();
|
||||
let sub_eth_header = into_substrate_ethereum_header(header.header());
|
||||
Ok((id, self.client.ethereum_receipts_required(sub_eth_header).await?))
|
||||
}
|
||||
}
|
||||
|
||||
/// Run Ethereum headers synchronization.
|
||||
pub fn run(params: EthereumSyncParams) -> Result<(), RpcError> {
|
||||
let EthereumSyncParams {
|
||||
eth_params,
|
||||
sub_params,
|
||||
sub_sign,
|
||||
sync_params,
|
||||
metrics_params,
|
||||
instance,
|
||||
} = params;
|
||||
|
||||
let eth_client = EthereumClient::new(eth_params);
|
||||
let sub_client = async_std::task::block_on(async { SubstrateClient::<Rialto>::new(sub_params).await })?;
|
||||
|
||||
let sign_sub_transactions = match sync_params.target_tx_mode {
|
||||
TargetTransactionMode::Signed | TargetTransactionMode::Backup => true,
|
||||
TargetTransactionMode::Unsigned => false,
|
||||
};
|
||||
|
||||
let source = EthereumHeadersSource::new(eth_client);
|
||||
let target = SubstrateHeadersTarget::new(sub_client, sign_sub_transactions, sub_sign, instance);
|
||||
|
||||
headers_relay::sync_loop::run(
|
||||
source,
|
||||
consts::ETHEREUM_TICK_INTERVAL,
|
||||
target,
|
||||
Rialto::AVERAGE_BLOCK_INTERVAL,
|
||||
(),
|
||||
sync_params,
|
||||
metrics_params,
|
||||
futures::future::pending(),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
// Copyright 2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! The PoA Bridge Pallet provides a way to include multiple instances of itself in a runtime. When
|
||||
//! synchronizing a Substrate chain which can include multiple instances of the bridge pallet we
|
||||
//! must somehow decide which of the instances to sync.
|
||||
//!
|
||||
//! Note that each instance of the bridge pallet is coupled with an instance of the currency exchange
|
||||
//! pallet. We must also have a way to create `Call`s for the correct currency exchange instance.
|
||||
//!
|
||||
//! This module helps by preparing the correct `Call`s for each of the different pallet instances.
|
||||
|
||||
use crate::ethereum_sync_loop::QueuedEthereumHeader;
|
||||
use crate::substrate_types::{into_substrate_ethereum_header, into_substrate_ethereum_receipts};
|
||||
|
||||
use rialto_runtime::exchange::EthereumTransactionInclusionProof as Proof;
|
||||
use rialto_runtime::Call;
|
||||
|
||||
/// Interface for `Calls` which are needed to correctly sync the bridge.
|
||||
///
|
||||
/// Each instance of the bridge and currency exchange pallets in the bridge runtime requires similar
|
||||
/// but slightly different `Call` in order to be synchronized.
|
||||
pub trait BridgeInstance: Send + Sync + std::fmt::Debug {
|
||||
/// Used to build a `Call` for importing signed headers to a Substrate runtime.
|
||||
fn build_signed_header_call(&self, headers: Vec<QueuedEthereumHeader>) -> Call;
|
||||
/// Used to build a `Call` for importing an unsigned header to a Substrate runtime.
|
||||
fn build_unsigned_header_call(&self, header: QueuedEthereumHeader) -> Call;
|
||||
/// Used to build a `Call` for importing peer transactions to a Substrate runtime.
|
||||
fn build_currency_exchange_call(&self, proof: Proof) -> Call;
|
||||
}
|
||||
|
||||
/// Corresponds to the Rialto instance used in the bridge runtime.
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct RialtoPoA;
|
||||
|
||||
impl BridgeInstance for RialtoPoA {
|
||||
fn build_signed_header_call(&self, headers: Vec<QueuedEthereumHeader>) -> Call {
|
||||
let pallet_call = rialto_runtime::BridgeEthPoACall::import_signed_headers(
|
||||
headers
|
||||
.into_iter()
|
||||
.map(|header| {
|
||||
(
|
||||
into_substrate_ethereum_header(&header.header()),
|
||||
into_substrate_ethereum_receipts(header.extra()),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
|
||||
rialto_runtime::Call::BridgeRialtoPoA(pallet_call)
|
||||
}
|
||||
|
||||
fn build_unsigned_header_call(&self, header: QueuedEthereumHeader) -> Call {
|
||||
let pallet_call = rialto_runtime::BridgeEthPoACall::import_unsigned_header(
|
||||
into_substrate_ethereum_header(&header.header()),
|
||||
into_substrate_ethereum_receipts(header.extra()),
|
||||
);
|
||||
|
||||
rialto_runtime::Call::BridgeRialtoPoA(pallet_call)
|
||||
}
|
||||
|
||||
fn build_currency_exchange_call(&self, proof: Proof) -> Call {
|
||||
let pallet_call = rialto_runtime::BridgeCurrencyExchangeCall::import_peer_transaction(proof);
|
||||
rialto_runtime::Call::BridgeRialtoCurrencyExchange(pallet_call)
|
||||
}
|
||||
}
|
||||
|
||||
/// Corresponds to the Kovan instance used in the bridge runtime.
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct Kovan;
|
||||
|
||||
impl BridgeInstance for Kovan {
|
||||
fn build_signed_header_call(&self, headers: Vec<QueuedEthereumHeader>) -> Call {
|
||||
let pallet_call = rialto_runtime::BridgeEthPoACall::import_signed_headers(
|
||||
headers
|
||||
.into_iter()
|
||||
.map(|header| {
|
||||
(
|
||||
into_substrate_ethereum_header(header.header()),
|
||||
into_substrate_ethereum_receipts(header.extra()),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
|
||||
rialto_runtime::Call::BridgeKovan(pallet_call)
|
||||
}
|
||||
|
||||
fn build_unsigned_header_call(&self, header: QueuedEthereumHeader) -> Call {
|
||||
let pallet_call = rialto_runtime::BridgeEthPoACall::import_unsigned_header(
|
||||
into_substrate_ethereum_header(header.header()),
|
||||
into_substrate_ethereum_receipts(header.extra()),
|
||||
);
|
||||
|
||||
rialto_runtime::Call::BridgeKovan(pallet_call)
|
||||
}
|
||||
|
||||
fn build_currency_exchange_call(&self, proof: Proof) -> Call {
|
||||
let pallet_call = rialto_runtime::BridgeCurrencyExchangeCall::import_peer_transaction(proof);
|
||||
rialto_runtime::Call::BridgeKovanCurrencyExchange(pallet_call)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,409 @@
|
||||
// Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
#![recursion_limit = "1024"]
|
||||
|
||||
mod ethereum_client;
|
||||
mod ethereum_deploy_contract;
|
||||
mod ethereum_exchange;
|
||||
mod ethereum_exchange_submit;
|
||||
mod ethereum_sync_loop;
|
||||
mod instances;
|
||||
mod rialto_client;
|
||||
mod rpc_errors;
|
||||
mod substrate_sync_loop;
|
||||
mod substrate_types;
|
||||
|
||||
use ethereum_deploy_contract::EthereumDeployContractParams;
|
||||
use ethereum_exchange::EthereumExchangeParams;
|
||||
use ethereum_exchange_submit::EthereumExchangeSubmitParams;
|
||||
use ethereum_sync_loop::EthereumSyncParams;
|
||||
use headers_relay::sync::TargetTransactionMode;
|
||||
use hex_literal::hex;
|
||||
use instances::{BridgeInstance, Kovan, RialtoPoA};
|
||||
use relay_utils::{initialize::initialize_relay, metrics::MetricsParams};
|
||||
use secp256k1::SecretKey;
|
||||
use sp_core::crypto::Pair;
|
||||
use substrate_sync_loop::SubstrateSyncParams;
|
||||
|
||||
use headers_relay::sync::HeadersSyncParams;
|
||||
use relay_ethereum_client::{ConnectionParams as EthereumConnectionParams, SigningParams as EthereumSigningParams};
|
||||
use relay_rialto_client::SigningParams as RialtoSigningParams;
|
||||
use relay_substrate_client::ConnectionParams as SubstrateConnectionParams;
|
||||
use std::sync::Arc;
|
||||
|
||||
fn main() {
|
||||
initialize_relay();
|
||||
|
||||
let yaml = clap::load_yaml!("cli.yml");
|
||||
let matches = clap::App::from_yaml(yaml).get_matches();
|
||||
match matches.subcommand() {
|
||||
("eth-to-sub", Some(eth_to_sub_matches)) => {
|
||||
log::info!(target: "bridge", "Starting ETH ➡ SUB relay.");
|
||||
if ethereum_sync_loop::run(match ethereum_sync_params(ð_to_sub_matches) {
|
||||
Ok(ethereum_sync_params) => ethereum_sync_params,
|
||||
Err(err) => {
|
||||
log::error!(target: "bridge", "Error parsing parameters: {}", err);
|
||||
return;
|
||||
}
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
log::error!(target: "bridge", "Unable to get Substrate genesis block for Ethereum sync.");
|
||||
};
|
||||
}
|
||||
("sub-to-eth", Some(sub_to_eth_matches)) => {
|
||||
log::info!(target: "bridge", "Starting SUB ➡ ETH relay.");
|
||||
if substrate_sync_loop::run(match substrate_sync_params(&sub_to_eth_matches) {
|
||||
Ok(substrate_sync_params) => substrate_sync_params,
|
||||
Err(err) => {
|
||||
log::error!(target: "bridge", "Error parsing parameters: {}", err);
|
||||
return;
|
||||
}
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
log::error!(target: "bridge", "Unable to get Substrate genesis block for Substrate sync.");
|
||||
};
|
||||
}
|
||||
("eth-deploy-contract", Some(eth_deploy_matches)) => {
|
||||
log::info!(target: "bridge", "Deploying ETH contracts.");
|
||||
ethereum_deploy_contract::run(match ethereum_deploy_contract_params(ð_deploy_matches) {
|
||||
Ok(ethereum_deploy_params) => ethereum_deploy_params,
|
||||
Err(err) => {
|
||||
log::error!(target: "bridge", "Error during contract deployment: {}", err);
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
("eth-submit-exchange-tx", Some(eth_exchange_submit_matches)) => {
|
||||
log::info!(target: "bridge", "Submitting ETH ➡ SUB exchange transaction.");
|
||||
ethereum_exchange_submit::run(match ethereum_exchange_submit_params(ð_exchange_submit_matches) {
|
||||
Ok(eth_exchange_submit_params) => eth_exchange_submit_params,
|
||||
Err(err) => {
|
||||
log::error!(target: "bridge", "Error submitting Eethereum exchange transaction: {}", err);
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
("eth-exchange-sub", Some(eth_exchange_matches)) => {
|
||||
log::info!(target: "bridge", "Starting ETH ➡ SUB exchange transactions relay.");
|
||||
ethereum_exchange::run(match ethereum_exchange_params(ð_exchange_matches) {
|
||||
Ok(eth_exchange_params) => eth_exchange_params,
|
||||
Err(err) => {
|
||||
log::error!(target: "bridge", "Error relaying Ethereum transactions proofs: {}", err);
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
("", _) => {
|
||||
log::error!(target: "bridge", "No subcommand specified");
|
||||
}
|
||||
_ => unreachable!("all possible subcommands are checked above; qed"),
|
||||
}
|
||||
}
|
||||
|
||||
fn ethereum_connection_params(matches: &clap::ArgMatches) -> Result<EthereumConnectionParams, String> {
|
||||
let mut params = EthereumConnectionParams::default();
|
||||
if let Some(eth_host) = matches.value_of("eth-host") {
|
||||
params.host = eth_host.into();
|
||||
}
|
||||
if let Some(eth_port) = matches.value_of("eth-port") {
|
||||
params.port = eth_port
|
||||
.parse()
|
||||
.map_err(|e| format!("Failed to parse eth-port: {}", e))?;
|
||||
}
|
||||
Ok(params)
|
||||
}
|
||||
|
||||
fn ethereum_signing_params(matches: &clap::ArgMatches) -> Result<EthereumSigningParams, String> {
|
||||
let mut params = EthereumSigningParams::default();
|
||||
if let Some(eth_signer) = matches.value_of("eth-signer") {
|
||||
params.signer =
|
||||
SecretKey::parse_slice(&hex::decode(eth_signer).map_err(|e| format!("Failed to parse eth-signer: {}", e))?)
|
||||
.map_err(|e| format!("Invalid eth-signer: {}", e))?;
|
||||
}
|
||||
if let Some(eth_chain_id) = matches.value_of("eth-chain-id") {
|
||||
params.chain_id = eth_chain_id
|
||||
.parse::<u64>()
|
||||
.map_err(|e| format!("Failed to parse eth-chain-id: {}", e))?;
|
||||
}
|
||||
Ok(params)
|
||||
}
|
||||
|
||||
fn substrate_connection_params(matches: &clap::ArgMatches) -> Result<SubstrateConnectionParams, String> {
|
||||
let mut params = SubstrateConnectionParams::default();
|
||||
if let Some(sub_host) = matches.value_of("sub-host") {
|
||||
params.host = sub_host.into();
|
||||
}
|
||||
if let Some(sub_port) = matches.value_of("sub-port") {
|
||||
params.port = sub_port
|
||||
.parse()
|
||||
.map_err(|e| format!("Failed to parse sub-port: {}", e))?;
|
||||
}
|
||||
Ok(params)
|
||||
}
|
||||
|
||||
fn rialto_signing_params(matches: &clap::ArgMatches) -> Result<RialtoSigningParams, String> {
|
||||
let mut params = RialtoSigningParams::default();
|
||||
if let Some(sub_signer) = matches.value_of("sub-signer") {
|
||||
let sub_signer_password = matches.value_of("sub-signer-password");
|
||||
params.signer = sp_core::sr25519::Pair::from_string(sub_signer, sub_signer_password)
|
||||
.map_err(|e| format!("Failed to parse sub-signer: {:?}", e))?;
|
||||
}
|
||||
Ok(params)
|
||||
}
|
||||
|
||||
fn ethereum_sync_params(matches: &clap::ArgMatches) -> Result<EthereumSyncParams, String> {
|
||||
use crate::ethereum_sync_loop::consts::*;
|
||||
|
||||
let mut sync_params = HeadersSyncParams {
|
||||
max_future_headers_to_download: MAX_FUTURE_HEADERS_TO_DOWNLOAD,
|
||||
max_headers_in_submitted_status: MAX_SUBMITTED_HEADERS,
|
||||
max_headers_in_single_submit: MAX_HEADERS_IN_SINGLE_SUBMIT,
|
||||
max_headers_size_in_single_submit: MAX_HEADERS_SIZE_IN_SINGLE_SUBMIT,
|
||||
prune_depth: PRUNE_DEPTH,
|
||||
target_tx_mode: TargetTransactionMode::Signed,
|
||||
};
|
||||
|
||||
match matches.value_of("sub-tx-mode") {
|
||||
Some("signed") => sync_params.target_tx_mode = TargetTransactionMode::Signed,
|
||||
Some("unsigned") => {
|
||||
sync_params.target_tx_mode = TargetTransactionMode::Unsigned;
|
||||
|
||||
// tx pool won't accept too much unsigned transactions
|
||||
sync_params.max_headers_in_submitted_status = 10;
|
||||
}
|
||||
Some("backup") => sync_params.target_tx_mode = TargetTransactionMode::Backup,
|
||||
Some(mode) => return Err(format!("Invalid sub-tx-mode: {}", mode)),
|
||||
None => sync_params.target_tx_mode = TargetTransactionMode::Signed,
|
||||
}
|
||||
|
||||
let params = EthereumSyncParams {
|
||||
eth_params: ethereum_connection_params(matches)?,
|
||||
sub_params: substrate_connection_params(matches)?,
|
||||
sub_sign: rialto_signing_params(matches)?,
|
||||
metrics_params: metrics_params(matches)?,
|
||||
instance: instance_params(matches)?,
|
||||
sync_params,
|
||||
};
|
||||
|
||||
log::debug!(target: "bridge", "Ethereum sync params: {:?}", params);
|
||||
|
||||
Ok(params)
|
||||
}
|
||||
|
||||
fn substrate_sync_params(matches: &clap::ArgMatches) -> Result<SubstrateSyncParams, String> {
|
||||
use crate::substrate_sync_loop::consts::*;
|
||||
|
||||
let eth_contract_address: relay_ethereum_client::types::Address =
|
||||
if let Some(eth_contract) = matches.value_of("eth-contract") {
|
||||
eth_contract.parse().map_err(|e| format!("{}", e))?
|
||||
} else {
|
||||
"731a10897d267e19b34503ad902d0a29173ba4b1"
|
||||
.parse()
|
||||
.expect("address is hardcoded, thus valid; qed")
|
||||
};
|
||||
|
||||
let params = SubstrateSyncParams {
|
||||
sub_params: substrate_connection_params(matches)?,
|
||||
eth_params: ethereum_connection_params(matches)?,
|
||||
eth_sign: ethereum_signing_params(matches)?,
|
||||
metrics_params: metrics_params(matches)?,
|
||||
sync_params: HeadersSyncParams {
|
||||
max_future_headers_to_download: MAX_FUTURE_HEADERS_TO_DOWNLOAD,
|
||||
max_headers_in_submitted_status: MAX_SUBMITTED_HEADERS,
|
||||
max_headers_in_single_submit: MAX_SUBMITTED_HEADERS,
|
||||
max_headers_size_in_single_submit: std::usize::MAX,
|
||||
prune_depth: PRUNE_DEPTH,
|
||||
target_tx_mode: TargetTransactionMode::Signed,
|
||||
},
|
||||
eth_contract_address,
|
||||
};
|
||||
|
||||
log::debug!(target: "bridge", "Substrate sync params: {:?}", params);
|
||||
|
||||
Ok(params)
|
||||
}
|
||||
|
||||
fn ethereum_deploy_contract_params(matches: &clap::ArgMatches) -> Result<EthereumDeployContractParams, String> {
|
||||
let eth_contract_code = parse_hex_argument(matches, "eth-contract-code")?.unwrap_or_else(|| {
|
||||
hex::decode(include_str!("../res/substrate-bridge-bytecode.hex")).expect("code is hardcoded, thus valid; qed")
|
||||
});
|
||||
let sub_initial_authorities_set_id = match matches.value_of("sub-authorities-set-id") {
|
||||
Some(sub_initial_authorities_set_id) => Some(
|
||||
sub_initial_authorities_set_id
|
||||
.parse()
|
||||
.map_err(|e| format!("Failed to parse sub-authorities-set-id: {}", e))?,
|
||||
),
|
||||
None => None,
|
||||
};
|
||||
let sub_initial_authorities_set = parse_hex_argument(matches, "sub-authorities-set")?;
|
||||
let sub_initial_header = parse_hex_argument(matches, "sub-initial-header")?;
|
||||
|
||||
let params = EthereumDeployContractParams {
|
||||
eth_params: ethereum_connection_params(matches)?,
|
||||
eth_sign: ethereum_signing_params(matches)?,
|
||||
sub_params: substrate_connection_params(matches)?,
|
||||
sub_initial_authorities_set_id,
|
||||
sub_initial_authorities_set,
|
||||
sub_initial_header,
|
||||
eth_contract_code,
|
||||
};
|
||||
|
||||
log::debug!(target: "bridge", "Deploy params: {:?}", params);
|
||||
|
||||
Ok(params)
|
||||
}
|
||||
|
||||
fn ethereum_exchange_submit_params(matches: &clap::ArgMatches) -> Result<EthereumExchangeSubmitParams, String> {
|
||||
let eth_nonce = if let Some(eth_nonce) = matches.value_of("eth-nonce") {
|
||||
Some(
|
||||
relay_ethereum_client::types::U256::from_dec_str(ð_nonce)
|
||||
.map_err(|e| format!("Failed to parse eth-nonce: {}", e))?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let eth_amount = if let Some(eth_amount) = matches.value_of("eth-amount") {
|
||||
eth_amount
|
||||
.parse()
|
||||
.map_err(|e| format!("Failed to parse eth-amount: {}", e))?
|
||||
} else {
|
||||
// This is in Wei, represents 1 ETH
|
||||
1_000_000_000_000_000_000_u64.into()
|
||||
};
|
||||
|
||||
// This is the well-known Substrate account of Ferdie
|
||||
let default_recepient = hex!("1cbd2d43530a44705ad088af313e18f80b53ef16b36177cd4b77b846f2a5f07c");
|
||||
|
||||
let sub_recipient = if let Some(sub_recipient) = matches.value_of("sub-recipient") {
|
||||
hex::decode(&sub_recipient)
|
||||
.map_err(|err| err.to_string())
|
||||
.and_then(|vsub_recipient| {
|
||||
let expected_len = default_recepient.len();
|
||||
if expected_len != vsub_recipient.len() {
|
||||
Err(format!("invalid length. Expected {} bytes", expected_len))
|
||||
} else {
|
||||
let mut sub_recipient = default_recepient;
|
||||
sub_recipient.copy_from_slice(&vsub_recipient[..expected_len]);
|
||||
Ok(sub_recipient)
|
||||
}
|
||||
})
|
||||
.map_err(|e| format!("Failed to parse sub-recipient: {}", e))?
|
||||
} else {
|
||||
default_recepient
|
||||
};
|
||||
|
||||
let params = EthereumExchangeSubmitParams {
|
||||
eth_params: ethereum_connection_params(matches)?,
|
||||
eth_sign: ethereum_signing_params(matches)?,
|
||||
eth_nonce,
|
||||
eth_amount,
|
||||
sub_recipient,
|
||||
};
|
||||
|
||||
log::debug!(target: "bridge", "Submit Ethereum exchange tx params: {:?}", params);
|
||||
|
||||
Ok(params)
|
||||
}
|
||||
|
||||
fn ethereum_exchange_params(matches: &clap::ArgMatches) -> Result<EthereumExchangeParams, String> {
|
||||
let mode = match matches.value_of("eth-tx-hash") {
|
||||
Some(eth_tx_hash) => ethereum_exchange::ExchangeRelayMode::Single(
|
||||
eth_tx_hash
|
||||
.parse()
|
||||
.map_err(|e| format!("Failed to parse eth-tx-hash: {}", e))?,
|
||||
),
|
||||
None => ethereum_exchange::ExchangeRelayMode::Auto(match matches.value_of("eth-start-with-block") {
|
||||
Some(eth_start_with_block) => Some(
|
||||
eth_start_with_block
|
||||
.parse()
|
||||
.map_err(|e| format!("Failed to parse eth-start-with-block: {}", e))?,
|
||||
),
|
||||
None => None,
|
||||
}),
|
||||
};
|
||||
|
||||
let params = EthereumExchangeParams {
|
||||
eth_params: ethereum_connection_params(matches)?,
|
||||
sub_params: substrate_connection_params(matches)?,
|
||||
sub_sign: rialto_signing_params(matches)?,
|
||||
metrics_params: metrics_params(matches)?,
|
||||
instance: instance_params(matches)?,
|
||||
mode,
|
||||
};
|
||||
|
||||
log::debug!(target: "bridge", "Ethereum exchange params: {:?}", params);
|
||||
|
||||
Ok(params)
|
||||
}
|
||||
|
||||
fn metrics_params(matches: &clap::ArgMatches) -> Result<Option<MetricsParams>, String> {
|
||||
if matches.is_present("no-prometheus") {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let mut metrics_params = MetricsParams::default();
|
||||
|
||||
if let Some(prometheus_host) = matches.value_of("prometheus-host") {
|
||||
metrics_params.host = prometheus_host.into();
|
||||
}
|
||||
if let Some(prometheus_port) = matches.value_of("prometheus-port") {
|
||||
metrics_params.port = prometheus_port
|
||||
.parse()
|
||||
.map_err(|e| format!("Failed to parse prometheus-port: {}", e))?;
|
||||
}
|
||||
|
||||
Ok(Some(metrics_params))
|
||||
}
|
||||
|
||||
fn instance_params(matches: &clap::ArgMatches) -> Result<Arc<dyn BridgeInstance>, String> {
|
||||
let instance = if let Some(instance) = matches.value_of("sub-pallet-instance") {
|
||||
match instance.to_lowercase().as_str() {
|
||||
"rialto" => Arc::new(RialtoPoA) as Arc<dyn BridgeInstance>,
|
||||
"kovan" => Arc::new(Kovan),
|
||||
_ => return Err("Unsupported bridge pallet instance".to_string()),
|
||||
}
|
||||
} else {
|
||||
unreachable!("CLI config enforces a default instance, can never be None")
|
||||
};
|
||||
|
||||
Ok(instance)
|
||||
}
|
||||
|
||||
fn parse_hex_argument(matches: &clap::ArgMatches, arg: &str) -> Result<Option<Vec<u8>>, String> {
|
||||
match matches.value_of(arg) {
|
||||
Some(value) => Ok(Some(
|
||||
hex::decode(value).map_err(|e| format!("Failed to parse {}: {}", arg, e))?,
|
||||
)),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
// Details: https://github.com/paritytech/parity-bridges-common/issues/118
|
||||
#[test]
|
||||
fn async_std_sleep_works() {
|
||||
let mut local_pool = futures::executor::LocalPool::new();
|
||||
local_pool.run_until(async move {
|
||||
async_std::task::sleep(std::time::Duration::from_secs(1)).await;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
// Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::ethereum_sync_loop::QueuedEthereumHeader;
|
||||
use crate::instances::BridgeInstance;
|
||||
use crate::rpc_errors::RpcError;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use bp_eth_poa::AuraHeader as SubstrateEthereumHeader;
|
||||
use codec::{Decode, Encode};
|
||||
use headers_relay::sync_types::SubmittedHeaders;
|
||||
use relay_ethereum_client::types::HeaderId as EthereumHeaderId;
|
||||
use relay_rialto_client::{Rialto, SigningParams as RialtoSigningParams};
|
||||
use relay_substrate_client::{Client as SubstrateClient, TransactionSignScheme};
|
||||
use relay_utils::HeaderId;
|
||||
use sp_core::{crypto::Pair, Bytes};
|
||||
use std::{collections::VecDeque, sync::Arc};
|
||||
|
||||
const ETH_API_IMPORT_REQUIRES_RECEIPTS: &str = "RialtoPoAHeaderApi_is_import_requires_receipts";
|
||||
const ETH_API_IS_KNOWN_BLOCK: &str = "RialtoPoAHeaderApi_is_known_block";
|
||||
const ETH_API_BEST_BLOCK: &str = "RialtoPoAHeaderApi_best_block";
|
||||
const ETH_API_BEST_FINALIZED_BLOCK: &str = "RialtoPoAHeaderApi_finalized_block";
|
||||
const EXCH_API_FILTER_TRANSACTION_PROOF: &str = "RialtoCurrencyExchangeApi_filter_transaction_proof";
|
||||
|
||||
type RpcResult<T> = std::result::Result<T, RpcError>;
|
||||
|
||||
/// A trait which contains methods that work by using multiple low-level RPCs, or more complicated
|
||||
/// interactions involving, for example, an Ethereum bridge module.
|
||||
#[async_trait]
|
||||
pub trait SubstrateHighLevelRpc {
|
||||
/// Returns best Ethereum block that Substrate runtime knows of.
|
||||
async fn best_ethereum_block(&self) -> RpcResult<EthereumHeaderId>;
|
||||
/// Returns best finalized Ethereum block that Substrate runtime knows of.
|
||||
async fn best_ethereum_finalized_block(&self) -> RpcResult<EthereumHeaderId>;
|
||||
/// Returns whether or not transactions receipts are required for Ethereum header submission.
|
||||
async fn ethereum_receipts_required(&self, header: SubstrateEthereumHeader) -> RpcResult<bool>;
|
||||
/// Returns whether or not the given Ethereum header is known to the Substrate runtime.
|
||||
async fn ethereum_header_known(&self, header_id: EthereumHeaderId) -> RpcResult<bool>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl SubstrateHighLevelRpc for SubstrateClient<Rialto> {
|
||||
async fn best_ethereum_block(&self) -> RpcResult<EthereumHeaderId> {
|
||||
let call = ETH_API_BEST_BLOCK.to_string();
|
||||
let data = Bytes(Vec::new());
|
||||
|
||||
let encoded_response = self.state_call(call, data, None).await?;
|
||||
let decoded_response: (u64, bp_eth_poa::H256) = Decode::decode(&mut &encoded_response.0[..])?;
|
||||
|
||||
let best_header_id = HeaderId(decoded_response.0, decoded_response.1);
|
||||
Ok(best_header_id)
|
||||
}
|
||||
|
||||
async fn best_ethereum_finalized_block(&self) -> RpcResult<EthereumHeaderId> {
|
||||
let call = ETH_API_BEST_FINALIZED_BLOCK.to_string();
|
||||
let data = Bytes(Vec::new());
|
||||
|
||||
let encoded_response = self.state_call(call, data, None).await?;
|
||||
let decoded_response: (u64, bp_eth_poa::H256) = Decode::decode(&mut &encoded_response.0[..])?;
|
||||
|
||||
let best_header_id = HeaderId(decoded_response.0, decoded_response.1);
|
||||
Ok(best_header_id)
|
||||
}
|
||||
|
||||
async fn ethereum_receipts_required(&self, header: SubstrateEthereumHeader) -> RpcResult<bool> {
|
||||
let call = ETH_API_IMPORT_REQUIRES_RECEIPTS.to_string();
|
||||
let data = Bytes(header.encode());
|
||||
|
||||
let encoded_response = self.state_call(call, data, None).await?;
|
||||
let receipts_required: bool = Decode::decode(&mut &encoded_response.0[..])?;
|
||||
|
||||
Ok(receipts_required)
|
||||
}
|
||||
|
||||
// The Substrate module could prune old headers. So this function could return false even
|
||||
// if header is synced. And we'll mark corresponding Ethereum header as Orphan.
|
||||
//
|
||||
// But when we read the best header from Substrate next time, we will know that
|
||||
// there's a better header. This Orphan will either be marked as synced, or
|
||||
// eventually pruned.
|
||||
async fn ethereum_header_known(&self, header_id: EthereumHeaderId) -> RpcResult<bool> {
|
||||
let call = ETH_API_IS_KNOWN_BLOCK.to_string();
|
||||
let data = Bytes(header_id.1.encode());
|
||||
|
||||
let encoded_response = self.state_call(call, data, None).await?;
|
||||
let is_known_block: bool = Decode::decode(&mut &encoded_response.0[..])?;
|
||||
|
||||
Ok(is_known_block)
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for RPC calls which are used to submit Ethereum headers to a Substrate
|
||||
/// runtime. These are typically calls which use a combination of other low-level RPC
|
||||
/// calls.
|
||||
#[async_trait]
|
||||
pub trait SubmitEthereumHeaders {
|
||||
/// Submits Ethereum header to Substrate runtime.
|
||||
async fn submit_ethereum_headers(
|
||||
&self,
|
||||
params: RialtoSigningParams,
|
||||
instance: Arc<dyn BridgeInstance>,
|
||||
headers: Vec<QueuedEthereumHeader>,
|
||||
sign_transactions: bool,
|
||||
) -> SubmittedHeaders<EthereumHeaderId, RpcError>;
|
||||
|
||||
/// Submits signed Ethereum header to Substrate runtime.
|
||||
async fn submit_signed_ethereum_headers(
|
||||
&self,
|
||||
params: RialtoSigningParams,
|
||||
instance: Arc<dyn BridgeInstance>,
|
||||
headers: Vec<QueuedEthereumHeader>,
|
||||
) -> SubmittedHeaders<EthereumHeaderId, RpcError>;
|
||||
|
||||
/// Submits unsigned Ethereum header to Substrate runtime.
|
||||
async fn submit_unsigned_ethereum_headers(
|
||||
&self,
|
||||
instance: Arc<dyn BridgeInstance>,
|
||||
headers: Vec<QueuedEthereumHeader>,
|
||||
) -> SubmittedHeaders<EthereumHeaderId, RpcError>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl SubmitEthereumHeaders for SubstrateClient<Rialto> {
|
||||
async fn submit_ethereum_headers(
|
||||
&self,
|
||||
params: RialtoSigningParams,
|
||||
instance: Arc<dyn BridgeInstance>,
|
||||
headers: Vec<QueuedEthereumHeader>,
|
||||
sign_transactions: bool,
|
||||
) -> SubmittedHeaders<EthereumHeaderId, RpcError> {
|
||||
if sign_transactions {
|
||||
self.submit_signed_ethereum_headers(params, instance, headers).await
|
||||
} else {
|
||||
self.submit_unsigned_ethereum_headers(instance, headers).await
|
||||
}
|
||||
}
|
||||
|
||||
async fn submit_signed_ethereum_headers(
|
||||
&self,
|
||||
params: RialtoSigningParams,
|
||||
instance: Arc<dyn BridgeInstance>,
|
||||
headers: Vec<QueuedEthereumHeader>,
|
||||
) -> SubmittedHeaders<EthereumHeaderId, RpcError> {
|
||||
let ids = headers.iter().map(|header| header.id()).collect();
|
||||
let submission_result = async {
|
||||
let account_id = params.signer.public().as_array_ref().clone().into();
|
||||
let nonce = self.next_account_index(account_id).await?;
|
||||
|
||||
let call = instance.build_signed_header_call(headers);
|
||||
let transaction = Rialto::sign_transaction(self, ¶ms.signer, nonce, call);
|
||||
|
||||
let _ = self.submit_extrinsic(Bytes(transaction.encode())).await?;
|
||||
Ok(())
|
||||
}
|
||||
.await;
|
||||
|
||||
match submission_result {
|
||||
Ok(_) => SubmittedHeaders {
|
||||
submitted: ids,
|
||||
incomplete: Vec::new(),
|
||||
rejected: Vec::new(),
|
||||
fatal_error: None,
|
||||
},
|
||||
Err(error) => SubmittedHeaders {
|
||||
submitted: Vec::new(),
|
||||
incomplete: Vec::new(),
|
||||
rejected: ids,
|
||||
fatal_error: Some(error),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async fn submit_unsigned_ethereum_headers(
|
||||
&self,
|
||||
instance: Arc<dyn BridgeInstance>,
|
||||
headers: Vec<QueuedEthereumHeader>,
|
||||
) -> SubmittedHeaders<EthereumHeaderId, RpcError> {
|
||||
let mut ids = headers.iter().map(|header| header.id()).collect::<VecDeque<_>>();
|
||||
let mut submitted_headers = SubmittedHeaders::default();
|
||||
|
||||
for header in headers {
|
||||
let id = ids.pop_front().expect("both collections have same size; qed");
|
||||
|
||||
let call = instance.build_unsigned_header_call(header);
|
||||
let transaction = create_unsigned_submit_transaction(call);
|
||||
|
||||
match self.submit_extrinsic(Bytes(transaction.encode())).await {
|
||||
Ok(_) => submitted_headers.submitted.push(id),
|
||||
Err(error) => {
|
||||
submitted_headers.rejected.push(id);
|
||||
submitted_headers.rejected.extend(ids);
|
||||
submitted_headers.fatal_error = Some(error.into());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
submitted_headers
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for RPC calls which are used to submit proof of Ethereum exchange transaction to a
|
||||
/// Substrate runtime. These are typically calls which use a combination of other low-level RPC
|
||||
/// calls.
|
||||
#[async_trait]
|
||||
pub trait SubmitEthereumExchangeTransactionProof {
|
||||
/// Pre-verify Ethereum exchange transaction proof.
|
||||
async fn verify_exchange_transaction_proof(
|
||||
&self,
|
||||
proof: rialto_runtime::exchange::EthereumTransactionInclusionProof,
|
||||
) -> RpcResult<bool>;
|
||||
/// Submits Ethereum exchange transaction proof to Substrate runtime.
|
||||
async fn submit_exchange_transaction_proof(
|
||||
&self,
|
||||
params: RialtoSigningParams,
|
||||
instance: Arc<dyn BridgeInstance>,
|
||||
proof: rialto_runtime::exchange::EthereumTransactionInclusionProof,
|
||||
) -> RpcResult<()>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl SubmitEthereumExchangeTransactionProof for SubstrateClient<Rialto> {
|
||||
async fn verify_exchange_transaction_proof(
|
||||
&self,
|
||||
proof: rialto_runtime::exchange::EthereumTransactionInclusionProof,
|
||||
) -> RpcResult<bool> {
|
||||
let call = EXCH_API_FILTER_TRANSACTION_PROOF.to_string();
|
||||
let data = Bytes(proof.encode());
|
||||
|
||||
let encoded_response = self.state_call(call, data, None).await?;
|
||||
let is_allowed: bool = Decode::decode(&mut &encoded_response.0[..])?;
|
||||
|
||||
Ok(is_allowed)
|
||||
}
|
||||
|
||||
async fn submit_exchange_transaction_proof(
|
||||
&self,
|
||||
params: RialtoSigningParams,
|
||||
instance: Arc<dyn BridgeInstance>,
|
||||
proof: rialto_runtime::exchange::EthereumTransactionInclusionProof,
|
||||
) -> RpcResult<()> {
|
||||
let account_id = params.signer.public().as_array_ref().clone().into();
|
||||
let nonce = self.next_account_index(account_id).await?;
|
||||
|
||||
let call = instance.build_currency_exchange_call(proof);
|
||||
let transaction = Rialto::sign_transaction(self, ¶ms.signer, nonce, call);
|
||||
|
||||
let _ = self.submit_extrinsic(Bytes(transaction.encode())).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Create unsigned Substrate transaction for submitting Ethereum header.
|
||||
fn create_unsigned_submit_transaction(call: rialto_runtime::Call) -> rialto_runtime::UncheckedExtrinsic {
|
||||
rialto_runtime::UncheckedExtrinsic::new_unsigned(call)
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
// Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
use relay_ethereum_client::Error as EthereumNodeError;
|
||||
use relay_substrate_client::Error as SubstrateNodeError;
|
||||
use relay_utils::MaybeConnectionError;
|
||||
|
||||
/// Contains common errors that can occur when
|
||||
/// interacting with a Substrate or Ethereum node
|
||||
/// through RPC.
|
||||
#[derive(Debug)]
|
||||
pub enum RpcError {
|
||||
/// The arguments to the RPC method failed to serialize.
|
||||
Serialization(serde_json::Error),
|
||||
/// An error occured when interacting with an Ethereum node.
|
||||
Ethereum(EthereumNodeError),
|
||||
/// An error occured when interacting with a Substrate node.
|
||||
Substrate(SubstrateNodeError),
|
||||
}
|
||||
|
||||
impl From<RpcError> for String {
|
||||
fn from(err: RpcError) -> Self {
|
||||
match err {
|
||||
RpcError::Serialization(e) => e.to_string(),
|
||||
RpcError::Ethereum(e) => e.to_string(),
|
||||
RpcError::Substrate(e) => e.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for RpcError {
|
||||
fn from(err: serde_json::Error) -> Self {
|
||||
Self::Serialization(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EthereumNodeError> for RpcError {
|
||||
fn from(err: EthereumNodeError) -> Self {
|
||||
Self::Ethereum(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SubstrateNodeError> for RpcError {
|
||||
fn from(err: SubstrateNodeError) -> Self {
|
||||
Self::Substrate(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ethabi::Error> for RpcError {
|
||||
fn from(err: ethabi::Error) -> Self {
|
||||
Self::Ethereum(EthereumNodeError::ResponseParseFailed(format!("{}", err)))
|
||||
}
|
||||
}
|
||||
|
||||
impl MaybeConnectionError for RpcError {
|
||||
fn is_connection_error(&self) -> bool {
|
||||
match self {
|
||||
RpcError::Ethereum(ref error) => error.is_connection_error(),
|
||||
RpcError::Substrate(ref error) => error.is_connection_error(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<codec::Error> for RpcError {
|
||||
fn from(err: codec::Error) -> Self {
|
||||
Self::Substrate(SubstrateNodeError::ResponseParseFailed(err))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
// Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Rialto-Substrate -> Ethereum PoA synchronization.
|
||||
|
||||
use crate::ethereum_client::EthereumHighLevelRpc;
|
||||
use crate::rpc_errors::RpcError;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use codec::Encode;
|
||||
use headers_relay::{
|
||||
sync::HeadersSyncParams,
|
||||
sync_loop::TargetClient,
|
||||
sync_types::{HeadersSyncPipeline, QueuedHeader, SourceHeader, SubmittedHeaders},
|
||||
};
|
||||
use relay_ethereum_client::{
|
||||
types::Address, Client as EthereumClient, ConnectionParams as EthereumConnectionParams,
|
||||
SigningParams as EthereumSigningParams,
|
||||
};
|
||||
use relay_rialto_client::{HeaderId as RialtoHeaderId, Rialto, SyncHeader as RialtoSyncHeader};
|
||||
use relay_substrate_client::{
|
||||
headers_source::HeadersSource, Chain as SubstrateChain, Client as SubstrateClient,
|
||||
ConnectionParams as SubstrateConnectionParams,
|
||||
};
|
||||
use relay_utils::{metrics::MetricsParams, relay_loop::Client as RelayClient};
|
||||
use sp_runtime::Justification;
|
||||
|
||||
use std::fmt::Debug;
|
||||
use std::{collections::HashSet, time::Duration};
|
||||
|
||||
pub mod consts {
|
||||
use super::*;
|
||||
|
||||
/// Interval at which we check new Ethereum blocks.
|
||||
pub const ETHEREUM_TICK_INTERVAL: Duration = Duration::from_secs(5);
|
||||
/// Max Ethereum headers we want to have in all 'before-submitted' states.
|
||||
pub const MAX_FUTURE_HEADERS_TO_DOWNLOAD: usize = 8;
|
||||
/// Max Ethereum headers count we want to have in 'submitted' state.
|
||||
pub const MAX_SUBMITTED_HEADERS: usize = 4;
|
||||
/// Max depth of in-memory headers in all states. Past this depth they will be forgotten (pruned).
|
||||
pub const PRUNE_DEPTH: u32 = 256;
|
||||
}
|
||||
|
||||
/// Substrate synchronization parameters.
|
||||
#[derive(Debug)]
|
||||
pub struct SubstrateSyncParams {
|
||||
/// Substrate connection params.
|
||||
pub sub_params: SubstrateConnectionParams,
|
||||
/// Ethereum connection params.
|
||||
pub eth_params: EthereumConnectionParams,
|
||||
/// Ethereum signing params.
|
||||
pub eth_sign: EthereumSigningParams,
|
||||
/// Ethereum bridge contract address.
|
||||
pub eth_contract_address: Address,
|
||||
/// Synchronization parameters.
|
||||
pub sync_params: HeadersSyncParams,
|
||||
/// Metrics parameters.
|
||||
pub metrics_params: Option<MetricsParams>,
|
||||
}
|
||||
|
||||
/// Substrate synchronization pipeline.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
pub struct SubstrateHeadersSyncPipeline;
|
||||
|
||||
impl HeadersSyncPipeline for SubstrateHeadersSyncPipeline {
|
||||
const SOURCE_NAME: &'static str = "Substrate";
|
||||
const TARGET_NAME: &'static str = "Ethereum";
|
||||
|
||||
type Hash = rialto_runtime::Hash;
|
||||
type Number = rialto_runtime::BlockNumber;
|
||||
type Header = RialtoSyncHeader;
|
||||
type Extra = ();
|
||||
type Completion = Justification;
|
||||
|
||||
fn estimate_size(source: &QueuedHeader<Self>) -> usize {
|
||||
source.header().encode().len()
|
||||
}
|
||||
}
|
||||
|
||||
/// Queued substrate header ID.
|
||||
pub type QueuedRialtoHeader = QueuedHeader<SubstrateHeadersSyncPipeline>;
|
||||
|
||||
/// Rialto node as headers source.
|
||||
type SubstrateHeadersSource = HeadersSource<Rialto, SubstrateHeadersSyncPipeline>;
|
||||
|
||||
/// Ethereum client as Substrate headers target.
|
||||
#[derive(Clone)]
|
||||
struct EthereumHeadersTarget {
|
||||
/// Ethereum node client.
|
||||
client: EthereumClient,
|
||||
/// Bridge contract address.
|
||||
contract: Address,
|
||||
/// Ethereum signing params.
|
||||
sign_params: EthereumSigningParams,
|
||||
}
|
||||
|
||||
impl EthereumHeadersTarget {
|
||||
fn new(client: EthereumClient, contract: Address, sign_params: EthereumSigningParams) -> Self {
|
||||
Self {
|
||||
client,
|
||||
contract,
|
||||
sign_params,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl RelayClient for EthereumHeadersTarget {
|
||||
type Error = RpcError;
|
||||
|
||||
async fn reconnect(&mut self) -> Result<(), RpcError> {
|
||||
self.client.reconnect();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl TargetClient<SubstrateHeadersSyncPipeline> for EthereumHeadersTarget {
|
||||
async fn best_header_id(&self) -> Result<RialtoHeaderId, RpcError> {
|
||||
// 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
|
||||
}
|
||||
|
||||
async fn is_known_header(&self, id: RialtoHeaderId) -> Result<(RialtoHeaderId, bool), RpcError> {
|
||||
self.client.substrate_header_known(self.contract, id).await
|
||||
}
|
||||
|
||||
async fn submit_headers(&self, headers: Vec<QueuedRialtoHeader>) -> SubmittedHeaders<RialtoHeaderId, RpcError> {
|
||||
self.client
|
||||
.submit_substrate_headers(self.sign_params.clone(), self.contract, headers)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn incomplete_headers_ids(&self) -> Result<HashSet<RialtoHeaderId>, RpcError> {
|
||||
self.client.incomplete_substrate_headers(self.contract).await
|
||||
}
|
||||
|
||||
async fn complete_header(&self, id: RialtoHeaderId, completion: Justification) -> Result<RialtoHeaderId, RpcError> {
|
||||
self.client
|
||||
.complete_substrate_header(self.sign_params.clone(), self.contract, id, completion)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn requires_extra(&self, header: QueuedRialtoHeader) -> Result<(RialtoHeaderId, bool), RpcError> {
|
||||
Ok((header.header().id(), false))
|
||||
}
|
||||
}
|
||||
|
||||
/// Run Substrate headers synchronization.
|
||||
pub fn run(params: SubstrateSyncParams) -> Result<(), RpcError> {
|
||||
let SubstrateSyncParams {
|
||||
sub_params,
|
||||
eth_params,
|
||||
eth_sign,
|
||||
eth_contract_address,
|
||||
sync_params,
|
||||
metrics_params,
|
||||
} = params;
|
||||
|
||||
let eth_client = EthereumClient::new(eth_params);
|
||||
let sub_client = async_std::task::block_on(async { SubstrateClient::<Rialto>::new(sub_params).await })?;
|
||||
|
||||
let target = EthereumHeadersTarget::new(eth_client, eth_contract_address, eth_sign);
|
||||
let source = SubstrateHeadersSource::new(sub_client);
|
||||
|
||||
headers_relay::sync_loop::run(
|
||||
source,
|
||||
Rialto::AVERAGE_BLOCK_INTERVAL,
|
||||
target,
|
||||
consts::ETHEREUM_TICK_INTERVAL,
|
||||
(),
|
||||
sync_params,
|
||||
metrics_params,
|
||||
futures::future::pending(),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// Copyright 2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Converting between Ethereum headers and bridge module types.
|
||||
|
||||
use bp_eth_poa::{
|
||||
AuraHeader as SubstrateEthereumHeader, LogEntry as SubstrateEthereumLogEntry, Receipt as SubstrateEthereumReceipt,
|
||||
TransactionOutcome as SubstrateEthereumTransactionOutcome,
|
||||
};
|
||||
use relay_ethereum_client::types::{
|
||||
Header as EthereumHeader, Receipt as EthereumReceipt, HEADER_ID_PROOF as ETHEREUM_HEADER_ID_PROOF,
|
||||
};
|
||||
|
||||
/// Convert Ethereum header into Ethereum header for Substrate.
|
||||
pub fn into_substrate_ethereum_header(header: &EthereumHeader) -> SubstrateEthereumHeader {
|
||||
SubstrateEthereumHeader {
|
||||
parent_hash: header.parent_hash,
|
||||
timestamp: header.timestamp.as_u64(),
|
||||
number: header.number.expect(ETHEREUM_HEADER_ID_PROOF).as_u64(),
|
||||
author: header.author,
|
||||
transactions_root: header.transactions_root,
|
||||
uncles_hash: header.uncles_hash,
|
||||
extra_data: header.extra_data.0.clone(),
|
||||
state_root: header.state_root,
|
||||
receipts_root: header.receipts_root,
|
||||
log_bloom: header.logs_bloom.unwrap_or_default().data().into(),
|
||||
gas_used: header.gas_used,
|
||||
gas_limit: header.gas_limit,
|
||||
difficulty: header.difficulty,
|
||||
seal: header.seal_fields.iter().map(|s| s.0.clone()).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert Ethereum transactions receipts into Ethereum transactions receipts for Substrate.
|
||||
pub fn into_substrate_ethereum_receipts(
|
||||
receipts: &Option<Vec<EthereumReceipt>>,
|
||||
) -> Option<Vec<SubstrateEthereumReceipt>> {
|
||||
receipts
|
||||
.as_ref()
|
||||
.map(|receipts| receipts.iter().map(into_substrate_ethereum_receipt).collect())
|
||||
}
|
||||
|
||||
/// Convert Ethereum transactions receipt into Ethereum transactions receipt for Substrate.
|
||||
pub fn into_substrate_ethereum_receipt(receipt: &EthereumReceipt) -> SubstrateEthereumReceipt {
|
||||
SubstrateEthereumReceipt {
|
||||
gas_used: receipt.cumulative_gas_used,
|
||||
log_bloom: receipt.logs_bloom.data().into(),
|
||||
logs: receipt
|
||||
.logs
|
||||
.iter()
|
||||
.map(|log_entry| SubstrateEthereumLogEntry {
|
||||
address: log_entry.address,
|
||||
topics: log_entry.topics.clone(),
|
||||
data: log_entry.data.0.clone(),
|
||||
})
|
||||
.collect(),
|
||||
outcome: match (receipt.status, receipt.root) {
|
||||
(Some(status), None) => SubstrateEthereumTransactionOutcome::StatusCode(status.as_u64() as u8),
|
||||
(None, Some(root)) => SubstrateEthereumTransactionOutcome::StateRoot(root),
|
||||
_ => SubstrateEthereumTransactionOutcome::Unknown,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "exchange-relay"
|
||||
version = "0.1.0"
|
||||
authors = ["Parity Technologies <admin@parity.io>"]
|
||||
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"
|
||||
log = "0.4.11"
|
||||
num-traits = "0.2"
|
||||
parking_lot = "0.11.0"
|
||||
relay-utils = { path = "../utils" }
|
||||
@@ -0,0 +1,916 @@
|
||||
// Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Relaying proofs of exchange transaction.
|
||||
|
||||
use async_trait::async_trait;
|
||||
use relay_utils::{
|
||||
relay_loop::Client as RelayClient, FailedClient, MaybeConnectionError, StringifiedMaybeConnectionError,
|
||||
};
|
||||
use std::{
|
||||
fmt::{Debug, Display},
|
||||
string::ToString,
|
||||
};
|
||||
|
||||
/// Transaction proof pipeline.
|
||||
pub trait TransactionProofPipeline {
|
||||
/// Name of the transaction proof source.
|
||||
const SOURCE_NAME: &'static str;
|
||||
/// Name of the transaction proof target.
|
||||
const TARGET_NAME: &'static str;
|
||||
|
||||
/// Block type.
|
||||
type Block: SourceBlock;
|
||||
/// Transaction inclusion proof type.
|
||||
type TransactionProof;
|
||||
}
|
||||
|
||||
/// Block that is participating in exchange.
|
||||
pub trait SourceBlock {
|
||||
/// Block hash type.
|
||||
type Hash: Clone + Debug + Display;
|
||||
/// Block number type.
|
||||
type Number: Debug
|
||||
+ Display
|
||||
+ Clone
|
||||
+ Copy
|
||||
+ Into<u64>
|
||||
+ std::cmp::Ord
|
||||
+ std::ops::Add<Output = Self::Number>
|
||||
+ num_traits::One;
|
||||
/// Block transaction.
|
||||
type Transaction: SourceTransaction;
|
||||
|
||||
/// Return hash of the block.
|
||||
fn id(&self) -> relay_utils::HeaderId<Self::Hash, Self::Number>;
|
||||
/// Return block transactions iterator.
|
||||
fn transactions(&self) -> Vec<Self::Transaction>;
|
||||
}
|
||||
|
||||
/// Transaction that is participating in exchange.
|
||||
pub trait SourceTransaction {
|
||||
/// Transaction hash type.
|
||||
type Hash: Debug + Display;
|
||||
|
||||
/// Return transaction hash.
|
||||
fn hash(&self) -> Self::Hash;
|
||||
}
|
||||
|
||||
/// Block hash for given pipeline.
|
||||
pub type BlockHashOf<P> = <<P as TransactionProofPipeline>::Block as SourceBlock>::Hash;
|
||||
|
||||
/// Block number for given pipeline.
|
||||
pub type BlockNumberOf<P> = <<P as TransactionProofPipeline>::Block as SourceBlock>::Number;
|
||||
|
||||
/// Transaction hash for given pipeline.
|
||||
pub type TransactionOf<P> = <<P as TransactionProofPipeline>::Block as SourceBlock>::Transaction;
|
||||
|
||||
/// Transaction hash for given pipeline.
|
||||
pub type TransactionHashOf<P> = <TransactionOf<P> as SourceTransaction>::Hash;
|
||||
|
||||
/// Header id.
|
||||
pub type HeaderId<P> = relay_utils::HeaderId<BlockHashOf<P>, BlockNumberOf<P>>;
|
||||
|
||||
/// Source client API.
|
||||
#[async_trait]
|
||||
pub trait SourceClient<P: TransactionProofPipeline>: RelayClient {
|
||||
/// Sleep until exchange-related data is (probably) updated.
|
||||
async fn tick(&self);
|
||||
/// Get block by hash.
|
||||
async fn block_by_hash(&self, hash: BlockHashOf<P>) -> Result<P::Block, Self::Error>;
|
||||
/// Get canonical block by number.
|
||||
async fn block_by_number(&self, number: BlockNumberOf<P>) -> Result<P::Block, Self::Error>;
|
||||
/// Return block + index where transaction has been **mined**. May return `Ok(None)` if transaction
|
||||
/// is unknown to the source node.
|
||||
async fn transaction_block(&self, hash: &TransactionHashOf<P>)
|
||||
-> Result<Option<(HeaderId<P>, usize)>, Self::Error>;
|
||||
/// Prepare transaction proof.
|
||||
async fn transaction_proof(&self, block: &P::Block, tx_index: usize) -> Result<P::TransactionProof, Self::Error>;
|
||||
}
|
||||
|
||||
/// Target client API.
|
||||
#[async_trait]
|
||||
pub trait TargetClient<P: TransactionProofPipeline>: RelayClient {
|
||||
/// Sleep until exchange-related data is (probably) updated.
|
||||
async fn tick(&self);
|
||||
/// Returns `Ok(true)` if header is known to the target node.
|
||||
async fn is_header_known(&self, id: &HeaderId<P>) -> Result<bool, Self::Error>;
|
||||
/// Returns `Ok(true)` if header is finalized by the target node.
|
||||
async fn is_header_finalized(&self, id: &HeaderId<P>) -> Result<bool, Self::Error>;
|
||||
/// Returns best finalized header id.
|
||||
async fn best_finalized_header_id(&self) -> Result<HeaderId<P>, Self::Error>;
|
||||
/// Returns `Ok(true)` if transaction proof is need to be relayed.
|
||||
async fn filter_transaction_proof(&self, proof: &P::TransactionProof) -> Result<bool, Self::Error>;
|
||||
/// Submits transaction proof to the target node.
|
||||
async fn submit_transaction_proof(&self, proof: P::TransactionProof) -> Result<(), Self::Error>;
|
||||
}
|
||||
|
||||
/// Block transaction statistics.
|
||||
#[derive(Debug, Default)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
pub struct RelayedBlockTransactions {
|
||||
/// Total number of transactions processed (either relayed or ignored) so far.
|
||||
pub processed: usize,
|
||||
/// Total number of transactions successfully relayed so far.
|
||||
pub relayed: usize,
|
||||
/// Total number of transactions that we have failed to relay so far.
|
||||
pub failed: usize,
|
||||
}
|
||||
|
||||
/// Relay all suitable transactions from single block.
|
||||
///
|
||||
/// If connection error occurs, returns Err with number of successfully processed transactions.
|
||||
/// If some other error occurs, it is ignored and other transactions are processed.
|
||||
///
|
||||
/// All transaction-level traces are written by this function. This function is not tracing
|
||||
/// any information about block.
|
||||
pub async fn relay_block_transactions<P: TransactionProofPipeline>(
|
||||
source_client: &impl SourceClient<P>,
|
||||
target_client: &impl TargetClient<P>,
|
||||
source_block: &P::Block,
|
||||
mut relayed_transactions: RelayedBlockTransactions,
|
||||
) -> Result<RelayedBlockTransactions, (FailedClient, RelayedBlockTransactions)> {
|
||||
let transactions_to_process = source_block
|
||||
.transactions()
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.skip(relayed_transactions.processed);
|
||||
for (source_tx_index, source_tx) in transactions_to_process {
|
||||
let result = async {
|
||||
let source_tx_id = format!("{}/{}", source_block.id().1, source_tx_index);
|
||||
let source_tx_proof =
|
||||
prepare_transaction_proof(source_client, &source_tx_id, source_block, source_tx_index)
|
||||
.await
|
||||
.map_err(|e| (FailedClient::Source, e))?;
|
||||
|
||||
let needs_to_be_relayed =
|
||||
target_client
|
||||
.filter_transaction_proof(&source_tx_proof)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
(
|
||||
FailedClient::Target,
|
||||
StringifiedMaybeConnectionError::new(
|
||||
err.is_connection_error(),
|
||||
format!("Transaction filtering has failed with {:?}", err),
|
||||
),
|
||||
)
|
||||
})?;
|
||||
|
||||
if !needs_to_be_relayed {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
relay_ready_transaction_proof(target_client, &source_tx_id, source_tx_proof)
|
||||
.await
|
||||
.map(|_| true)
|
||||
.map_err(|e| (FailedClient::Target, e))
|
||||
}
|
||||
.await;
|
||||
|
||||
// We have two options here:
|
||||
// 1) retry with the same transaction later;
|
||||
// 2) report error and proceed with next transaction.
|
||||
//
|
||||
// Option#1 may seems better, but:
|
||||
// 1) we do not track if transaction is mined (without an error) by the target node;
|
||||
// 2) error could be irrecoverable (e.g. when block is already pruned by bridge module or tx
|
||||
// has invalid format) && we'll end up in infinite loop of retrying the same transaction proof.
|
||||
//
|
||||
// So we're going with option#2 here (the only exception are connection errors).
|
||||
match result {
|
||||
Ok(false) => {
|
||||
relayed_transactions.processed += 1;
|
||||
}
|
||||
Ok(true) => {
|
||||
log::info!(
|
||||
target: "bridge",
|
||||
"{} transaction {} proof has been successfully submitted to {} node",
|
||||
P::SOURCE_NAME,
|
||||
source_tx.hash(),
|
||||
P::TARGET_NAME,
|
||||
);
|
||||
|
||||
relayed_transactions.processed += 1;
|
||||
relayed_transactions.relayed += 1;
|
||||
}
|
||||
Err((failed_client, err)) => {
|
||||
log::error!(
|
||||
target: "bridge",
|
||||
"Error relaying {} transaction {} proof to {} node: {}. {}",
|
||||
P::SOURCE_NAME,
|
||||
source_tx.hash(),
|
||||
P::TARGET_NAME,
|
||||
err.to_string(),
|
||||
if err.is_connection_error() {
|
||||
"Going to retry after delay..."
|
||||
} else {
|
||||
"You may need to submit proof of this transaction manually"
|
||||
},
|
||||
);
|
||||
|
||||
if err.is_connection_error() {
|
||||
return Err((failed_client, relayed_transactions));
|
||||
}
|
||||
|
||||
relayed_transactions.processed += 1;
|
||||
relayed_transactions.failed += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(relayed_transactions)
|
||||
}
|
||||
|
||||
/// Relay single transaction proof.
|
||||
pub async fn relay_single_transaction_proof<P: TransactionProofPipeline>(
|
||||
source_client: &impl SourceClient<P>,
|
||||
target_client: &impl TargetClient<P>,
|
||||
source_tx_hash: TransactionHashOf<P>,
|
||||
) -> Result<(), String> {
|
||||
// wait for transaction and header on source node
|
||||
let (source_header_id, source_tx_index) = wait_transaction_mined(source_client, &source_tx_hash).await?;
|
||||
let source_block = source_client.block_by_hash(source_header_id.1.clone()).await;
|
||||
let source_block = source_block.map_err(|err| {
|
||||
format!(
|
||||
"Error retrieving block {} from {} node: {:?}",
|
||||
source_header_id.1,
|
||||
P::SOURCE_NAME,
|
||||
err,
|
||||
)
|
||||
})?;
|
||||
|
||||
// wait for transaction and header on target node
|
||||
wait_header_imported(target_client, &source_header_id).await?;
|
||||
wait_header_finalized(target_client, &source_header_id).await?;
|
||||
|
||||
// and finally - prepare and submit transaction proof to target node
|
||||
let source_tx_id = format!("{}", source_tx_hash);
|
||||
relay_ready_transaction_proof(
|
||||
target_client,
|
||||
&source_tx_id,
|
||||
prepare_transaction_proof(source_client, &source_tx_id, &source_block, source_tx_index)
|
||||
.await
|
||||
.map_err(|err| err.to_string())?,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| err.to_string())
|
||||
}
|
||||
|
||||
/// Prepare transaction proof.
|
||||
async fn prepare_transaction_proof<P: TransactionProofPipeline>(
|
||||
source_client: &impl SourceClient<P>,
|
||||
source_tx_id: &str,
|
||||
source_block: &P::Block,
|
||||
source_tx_index: usize,
|
||||
) -> Result<P::TransactionProof, StringifiedMaybeConnectionError> {
|
||||
source_client
|
||||
.transaction_proof(source_block, source_tx_index)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
StringifiedMaybeConnectionError::new(
|
||||
err.is_connection_error(),
|
||||
format!(
|
||||
"Error building transaction {} proof on {} node: {:?}",
|
||||
source_tx_id,
|
||||
P::SOURCE_NAME,
|
||||
err,
|
||||
),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Relay prepared proof of transaction.
|
||||
async fn relay_ready_transaction_proof<P: TransactionProofPipeline>(
|
||||
target_client: &impl TargetClient<P>,
|
||||
source_tx_id: &str,
|
||||
source_tx_proof: P::TransactionProof,
|
||||
) -> Result<(), StringifiedMaybeConnectionError> {
|
||||
target_client
|
||||
.submit_transaction_proof(source_tx_proof)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
StringifiedMaybeConnectionError::new(
|
||||
err.is_connection_error(),
|
||||
format!(
|
||||
"Error submitting transaction {} proof to {} node: {:?}",
|
||||
source_tx_id,
|
||||
P::TARGET_NAME,
|
||||
err,
|
||||
),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Wait until transaction is mined by source node.
|
||||
async fn wait_transaction_mined<P: TransactionProofPipeline>(
|
||||
source_client: &impl SourceClient<P>,
|
||||
source_tx_hash: &TransactionHashOf<P>,
|
||||
) -> Result<(HeaderId<P>, usize), String> {
|
||||
loop {
|
||||
let source_header_and_tx = source_client.transaction_block(&source_tx_hash).await.map_err(|err| {
|
||||
format!(
|
||||
"Error retrieving transaction {} from {} node: {:?}",
|
||||
source_tx_hash,
|
||||
P::SOURCE_NAME,
|
||||
err,
|
||||
)
|
||||
})?;
|
||||
match source_header_and_tx {
|
||||
Some((source_header_id, source_tx)) => {
|
||||
log::info!(
|
||||
target: "bridge",
|
||||
"Transaction {} is retrieved from {} node. Continuing...",
|
||||
source_tx_hash,
|
||||
P::SOURCE_NAME,
|
||||
);
|
||||
|
||||
return Ok((source_header_id, source_tx));
|
||||
}
|
||||
None => {
|
||||
log::info!(
|
||||
target: "bridge",
|
||||
"Waiting for transaction {} to be mined by {} node...",
|
||||
source_tx_hash,
|
||||
P::SOURCE_NAME,
|
||||
);
|
||||
|
||||
source_client.tick().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Wait until target node imports required header.
|
||||
async fn wait_header_imported<P: TransactionProofPipeline>(
|
||||
target_client: &impl TargetClient<P>,
|
||||
source_header_id: &HeaderId<P>,
|
||||
) -> Result<(), String> {
|
||||
loop {
|
||||
let is_header_known = target_client.is_header_known(&source_header_id).await.map_err(|err| {
|
||||
format!(
|
||||
"Failed to check existence of header {}/{} on {} node: {:?}",
|
||||
source_header_id.0,
|
||||
source_header_id.1,
|
||||
P::TARGET_NAME,
|
||||
err,
|
||||
)
|
||||
})?;
|
||||
match is_header_known {
|
||||
true => {
|
||||
log::info!(
|
||||
target: "bridge",
|
||||
"Header {}/{} is known to {} node. Continuing.",
|
||||
source_header_id.0,
|
||||
source_header_id.1,
|
||||
P::TARGET_NAME,
|
||||
);
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
false => {
|
||||
log::info!(
|
||||
target: "bridge",
|
||||
"Waiting for header {}/{} to be imported by {} node...",
|
||||
source_header_id.0,
|
||||
source_header_id.1,
|
||||
P::TARGET_NAME,
|
||||
);
|
||||
|
||||
target_client.tick().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Wait until target node finalizes required header.
|
||||
async fn wait_header_finalized<P: TransactionProofPipeline>(
|
||||
target_client: &impl TargetClient<P>,
|
||||
source_header_id: &HeaderId<P>,
|
||||
) -> Result<(), String> {
|
||||
loop {
|
||||
let is_header_finalized = target_client
|
||||
.is_header_finalized(&source_header_id)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
format!(
|
||||
"Failed to check finality of header {}/{} on {} node: {:?}",
|
||||
source_header_id.0,
|
||||
source_header_id.1,
|
||||
P::TARGET_NAME,
|
||||
err,
|
||||
)
|
||||
})?;
|
||||
match is_header_finalized {
|
||||
true => {
|
||||
log::info!(
|
||||
target: "bridge",
|
||||
"Header {}/{} is finalizd by {} node. Continuing.",
|
||||
source_header_id.0,
|
||||
source_header_id.1,
|
||||
P::TARGET_NAME,
|
||||
);
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
false => {
|
||||
log::info!(
|
||||
target: "bridge",
|
||||
"Waiting for header {}/{} to be finalized by {} node...",
|
||||
source_header_id.0,
|
||||
source_header_id.1,
|
||||
P::TARGET_NAME,
|
||||
);
|
||||
|
||||
target_client.tick().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
|
||||
use parking_lot::Mutex;
|
||||
use relay_utils::HeaderId;
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
pub fn test_block_id() -> TestHeaderId {
|
||||
HeaderId(1, 1)
|
||||
}
|
||||
|
||||
pub fn test_next_block_id() -> TestHeaderId {
|
||||
HeaderId(2, 2)
|
||||
}
|
||||
|
||||
pub fn test_transaction_hash(tx_index: u64) -> TestTransactionHash {
|
||||
200 + tx_index
|
||||
}
|
||||
|
||||
pub fn test_transaction(tx_index: u64) -> TestTransaction {
|
||||
TestTransaction(test_transaction_hash(tx_index))
|
||||
}
|
||||
|
||||
pub fn test_block() -> TestBlock {
|
||||
TestBlock(test_block_id(), vec![test_transaction(0)])
|
||||
}
|
||||
|
||||
pub fn test_next_block() -> TestBlock {
|
||||
TestBlock(test_next_block_id(), vec![test_transaction(1)])
|
||||
}
|
||||
|
||||
pub type TestBlockNumber = u64;
|
||||
pub type TestBlockHash = u64;
|
||||
pub type TestTransactionHash = u64;
|
||||
pub type TestHeaderId = HeaderId<TestBlockHash, TestBlockNumber>;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct TestError(pub bool);
|
||||
|
||||
impl MaybeConnectionError for TestError {
|
||||
fn is_connection_error(&self) -> bool {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TestTransactionProofPipeline;
|
||||
|
||||
impl TransactionProofPipeline for TestTransactionProofPipeline {
|
||||
const SOURCE_NAME: &'static str = "TestSource";
|
||||
const TARGET_NAME: &'static str = "TestTarget";
|
||||
|
||||
type Block = TestBlock;
|
||||
type TransactionProof = TestTransactionProof;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TestBlock(pub TestHeaderId, pub Vec<TestTransaction>);
|
||||
|
||||
impl SourceBlock for TestBlock {
|
||||
type Hash = TestBlockHash;
|
||||
type Number = TestBlockNumber;
|
||||
type Transaction = TestTransaction;
|
||||
|
||||
fn id(&self) -> TestHeaderId {
|
||||
self.0
|
||||
}
|
||||
|
||||
fn transactions(&self) -> Vec<TestTransaction> {
|
||||
self.1.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TestTransaction(pub TestTransactionHash);
|
||||
|
||||
impl SourceTransaction for TestTransaction {
|
||||
type Hash = TestTransactionHash;
|
||||
|
||||
fn hash(&self) -> Self::Hash {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct TestTransactionProof(pub TestTransactionHash);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TestTransactionsSource {
|
||||
pub on_tick: Arc<dyn Fn(&mut TestTransactionsSourceData) + Send + Sync>,
|
||||
pub data: Arc<Mutex<TestTransactionsSourceData>>,
|
||||
}
|
||||
|
||||
pub struct TestTransactionsSourceData {
|
||||
pub block: Result<TestBlock, TestError>,
|
||||
pub transaction_block: Result<Option<(TestHeaderId, usize)>, TestError>,
|
||||
pub proofs_to_fail: HashMap<TestTransactionHash, TestError>,
|
||||
}
|
||||
|
||||
impl TestTransactionsSource {
|
||||
pub fn new(on_tick: Box<dyn Fn(&mut TestTransactionsSourceData) + Send + Sync>) -> Self {
|
||||
Self {
|
||||
on_tick: Arc::new(on_tick),
|
||||
data: Arc::new(Mutex::new(TestTransactionsSourceData {
|
||||
block: Ok(test_block()),
|
||||
transaction_block: Ok(Some((test_block_id(), 0))),
|
||||
proofs_to_fail: HashMap::new(),
|
||||
})),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl RelayClient for TestTransactionsSource {
|
||||
type Error = TestError;
|
||||
|
||||
async fn reconnect(&mut self) -> Result<(), TestError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl SourceClient<TestTransactionProofPipeline> for TestTransactionsSource {
|
||||
async fn tick(&self) {
|
||||
(self.on_tick)(&mut *self.data.lock())
|
||||
}
|
||||
|
||||
async fn block_by_hash(&self, _: TestBlockHash) -> Result<TestBlock, TestError> {
|
||||
self.data.lock().block.clone()
|
||||
}
|
||||
|
||||
async fn block_by_number(&self, _: TestBlockNumber) -> Result<TestBlock, TestError> {
|
||||
self.data.lock().block.clone()
|
||||
}
|
||||
|
||||
async fn transaction_block(&self, _: &TestTransactionHash) -> Result<Option<(TestHeaderId, usize)>, TestError> {
|
||||
self.data.lock().transaction_block.clone()
|
||||
}
|
||||
|
||||
async fn transaction_proof(&self, block: &TestBlock, index: usize) -> Result<TestTransactionProof, TestError> {
|
||||
let tx_hash = block.1[index].hash();
|
||||
let proof_error = self.data.lock().proofs_to_fail.get(&tx_hash).cloned();
|
||||
if let Some(err) = proof_error {
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
Ok(TestTransactionProof(tx_hash))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TestTransactionsTarget {
|
||||
pub on_tick: Arc<dyn Fn(&mut TestTransactionsTargetData) + Send + Sync>,
|
||||
pub data: Arc<Mutex<TestTransactionsTargetData>>,
|
||||
}
|
||||
|
||||
pub struct TestTransactionsTargetData {
|
||||
pub is_header_known: Result<bool, TestError>,
|
||||
pub is_header_finalized: Result<bool, TestError>,
|
||||
pub best_finalized_header_id: Result<TestHeaderId, TestError>,
|
||||
pub transactions_to_accept: HashSet<TestTransactionHash>,
|
||||
pub submitted_proofs: Vec<TestTransactionProof>,
|
||||
}
|
||||
|
||||
impl TestTransactionsTarget {
|
||||
pub fn new(on_tick: Box<dyn Fn(&mut TestTransactionsTargetData) + Send + Sync>) -> Self {
|
||||
Self {
|
||||
on_tick: Arc::new(on_tick),
|
||||
data: Arc::new(Mutex::new(TestTransactionsTargetData {
|
||||
is_header_known: Ok(true),
|
||||
is_header_finalized: Ok(true),
|
||||
best_finalized_header_id: Ok(test_block_id()),
|
||||
transactions_to_accept: vec![test_transaction_hash(0)].into_iter().collect(),
|
||||
submitted_proofs: Vec::new(),
|
||||
})),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl RelayClient for TestTransactionsTarget {
|
||||
type Error = TestError;
|
||||
|
||||
async fn reconnect(&mut self) -> Result<(), TestError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl TargetClient<TestTransactionProofPipeline> for TestTransactionsTarget {
|
||||
async fn tick(&self) {
|
||||
(self.on_tick)(&mut *self.data.lock())
|
||||
}
|
||||
|
||||
async fn is_header_known(&self, _: &TestHeaderId) -> Result<bool, TestError> {
|
||||
self.data.lock().is_header_known.clone()
|
||||
}
|
||||
|
||||
async fn is_header_finalized(&self, _: &TestHeaderId) -> Result<bool, TestError> {
|
||||
self.data.lock().is_header_finalized.clone()
|
||||
}
|
||||
|
||||
async fn best_finalized_header_id(&self) -> Result<TestHeaderId, TestError> {
|
||||
self.data.lock().best_finalized_header_id.clone()
|
||||
}
|
||||
|
||||
async fn filter_transaction_proof(&self, proof: &TestTransactionProof) -> Result<bool, TestError> {
|
||||
Ok(self.data.lock().transactions_to_accept.contains(&proof.0))
|
||||
}
|
||||
|
||||
async fn submit_transaction_proof(&self, proof: TestTransactionProof) -> Result<(), TestError> {
|
||||
self.data.lock().submitted_proofs.push(proof);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_relay_single_success(source: &TestTransactionsSource, target: &TestTransactionsTarget) {
|
||||
assert_eq!(
|
||||
async_std::task::block_on(relay_single_transaction_proof(source, target, test_transaction_hash(0),)),
|
||||
Ok(()),
|
||||
);
|
||||
assert_eq!(
|
||||
target.data.lock().submitted_proofs,
|
||||
vec![TestTransactionProof(test_transaction_hash(0))],
|
||||
);
|
||||
}
|
||||
|
||||
fn ensure_relay_single_failure(source: TestTransactionsSource, target: TestTransactionsTarget) {
|
||||
assert!(async_std::task::block_on(relay_single_transaction_proof(
|
||||
&source,
|
||||
&target,
|
||||
test_transaction_hash(0),
|
||||
))
|
||||
.is_err(),);
|
||||
assert!(target.data.lock().submitted_proofs.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ready_transaction_proof_relayed_immediately() {
|
||||
let source = TestTransactionsSource::new(Box::new(|_| unreachable!("no ticks allowed")));
|
||||
let target = TestTransactionsTarget::new(Box::new(|_| unreachable!("no ticks allowed")));
|
||||
ensure_relay_single_success(&source, &target)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relay_transaction_proof_waits_for_transaction_to_be_mined() {
|
||||
let source = TestTransactionsSource::new(Box::new(|source_data| {
|
||||
assert_eq!(source_data.transaction_block, Ok(None));
|
||||
source_data.transaction_block = Ok(Some((test_block_id(), 0)));
|
||||
}));
|
||||
let target = TestTransactionsTarget::new(Box::new(|_| unreachable!("no ticks allowed")));
|
||||
|
||||
// transaction is not yet mined, but will be available after first wait (tick)
|
||||
source.data.lock().transaction_block = Ok(None);
|
||||
|
||||
ensure_relay_single_success(&source, &target)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relay_transaction_fails_when_transaction_retrieval_fails() {
|
||||
let source = TestTransactionsSource::new(Box::new(|_| unreachable!("no ticks allowed")));
|
||||
let target = TestTransactionsTarget::new(Box::new(|_| unreachable!("no ticks allowed")));
|
||||
|
||||
source.data.lock().transaction_block = Err(TestError(false));
|
||||
|
||||
ensure_relay_single_failure(source, target)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relay_transaction_fails_when_proof_retrieval_fails() {
|
||||
let source = TestTransactionsSource::new(Box::new(|_| unreachable!("no ticks allowed")));
|
||||
let target = TestTransactionsTarget::new(Box::new(|_| unreachable!("no ticks allowed")));
|
||||
|
||||
source
|
||||
.data
|
||||
.lock()
|
||||
.proofs_to_fail
|
||||
.insert(test_transaction_hash(0), TestError(false));
|
||||
|
||||
ensure_relay_single_failure(source, target)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relay_transaction_proof_waits_for_header_to_be_imported() {
|
||||
let source = TestTransactionsSource::new(Box::new(|_| unreachable!("no ticks allowed")));
|
||||
let target = TestTransactionsTarget::new(Box::new(|target_data| {
|
||||
assert_eq!(target_data.is_header_known, Ok(false));
|
||||
target_data.is_header_known = Ok(true);
|
||||
}));
|
||||
|
||||
// header is not yet imported, but will be available after first wait (tick)
|
||||
target.data.lock().is_header_known = Ok(false);
|
||||
|
||||
ensure_relay_single_success(&source, &target)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relay_transaction_proof_fails_when_is_header_known_fails() {
|
||||
let source = TestTransactionsSource::new(Box::new(|_| unreachable!("no ticks allowed")));
|
||||
let target = TestTransactionsTarget::new(Box::new(|_| unreachable!("no ticks allowed")));
|
||||
|
||||
target.data.lock().is_header_known = Err(TestError(false));
|
||||
|
||||
ensure_relay_single_failure(source, target)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relay_transaction_proof_waits_for_header_to_be_finalized() {
|
||||
let source = TestTransactionsSource::new(Box::new(|_| unreachable!("no ticks allowed")));
|
||||
let target = TestTransactionsTarget::new(Box::new(|target_data| {
|
||||
assert_eq!(target_data.is_header_finalized, Ok(false));
|
||||
target_data.is_header_finalized = Ok(true);
|
||||
}));
|
||||
|
||||
// header is not yet finalized, but will be available after first wait (tick)
|
||||
target.data.lock().is_header_finalized = Ok(false);
|
||||
|
||||
ensure_relay_single_success(&source, &target)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relay_transaction_proof_fails_when_is_header_finalized_fails() {
|
||||
let source = TestTransactionsSource::new(Box::new(|_| unreachable!("no ticks allowed")));
|
||||
let target = TestTransactionsTarget::new(Box::new(|_| unreachable!("no ticks allowed")));
|
||||
|
||||
target.data.lock().is_header_finalized = Err(TestError(false));
|
||||
|
||||
ensure_relay_single_failure(source, target)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relay_transaction_proof_fails_when_target_node_rejects_proof() {
|
||||
let source = TestTransactionsSource::new(Box::new(|_| unreachable!("no ticks allowed")));
|
||||
let target = TestTransactionsTarget::new(Box::new(|_| unreachable!("no ticks allowed")));
|
||||
|
||||
target
|
||||
.data
|
||||
.lock()
|
||||
.transactions_to_accept
|
||||
.remove(&test_transaction_hash(0));
|
||||
|
||||
ensure_relay_single_success(&source, &target)
|
||||
}
|
||||
|
||||
fn test_relay_block_transactions(
|
||||
source: &TestTransactionsSource,
|
||||
target: &TestTransactionsTarget,
|
||||
pre_relayed: RelayedBlockTransactions,
|
||||
) -> Result<RelayedBlockTransactions, RelayedBlockTransactions> {
|
||||
async_std::task::block_on(relay_block_transactions(
|
||||
source,
|
||||
target,
|
||||
&TestBlock(
|
||||
test_block_id(),
|
||||
vec![test_transaction(0), test_transaction(1), test_transaction(2)],
|
||||
),
|
||||
pre_relayed,
|
||||
))
|
||||
.map_err(|(_, transactions)| transactions)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relay_block_transactions_process_all_transactions() {
|
||||
let source = TestTransactionsSource::new(Box::new(|_| unreachable!("no ticks allowed")));
|
||||
let target = TestTransactionsTarget::new(Box::new(|_| unreachable!("no ticks allowed")));
|
||||
|
||||
// let's only accept tx#1
|
||||
target
|
||||
.data
|
||||
.lock()
|
||||
.transactions_to_accept
|
||||
.remove(&test_transaction_hash(0));
|
||||
target
|
||||
.data
|
||||
.lock()
|
||||
.transactions_to_accept
|
||||
.insert(test_transaction_hash(1));
|
||||
|
||||
let relayed_transactions = test_relay_block_transactions(&source, &target, Default::default());
|
||||
assert_eq!(
|
||||
relayed_transactions,
|
||||
Ok(RelayedBlockTransactions {
|
||||
processed: 3,
|
||||
relayed: 1,
|
||||
failed: 0,
|
||||
}),
|
||||
);
|
||||
assert_eq!(
|
||||
target.data.lock().submitted_proofs,
|
||||
vec![TestTransactionProof(test_transaction_hash(1))],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relay_block_transactions_ignores_transaction_failure() {
|
||||
let source = TestTransactionsSource::new(Box::new(|_| unreachable!("no ticks allowed")));
|
||||
let target = TestTransactionsTarget::new(Box::new(|_| unreachable!("no ticks allowed")));
|
||||
|
||||
// let's reject proof for tx#0
|
||||
source
|
||||
.data
|
||||
.lock()
|
||||
.proofs_to_fail
|
||||
.insert(test_transaction_hash(0), TestError(false));
|
||||
|
||||
let relayed_transactions = test_relay_block_transactions(&source, &target, Default::default());
|
||||
assert_eq!(
|
||||
relayed_transactions,
|
||||
Ok(RelayedBlockTransactions {
|
||||
processed: 3,
|
||||
relayed: 0,
|
||||
failed: 1,
|
||||
}),
|
||||
);
|
||||
assert_eq!(target.data.lock().submitted_proofs, vec![],);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relay_block_transactions_fails_on_connection_error() {
|
||||
let source = TestTransactionsSource::new(Box::new(|_| unreachable!("no ticks allowed")));
|
||||
let target = TestTransactionsTarget::new(Box::new(|_| unreachable!("no ticks allowed")));
|
||||
|
||||
// fail with connection error when preparing proof for tx#1
|
||||
source
|
||||
.data
|
||||
.lock()
|
||||
.proofs_to_fail
|
||||
.insert(test_transaction_hash(1), TestError(true));
|
||||
|
||||
let relayed_transactions = test_relay_block_transactions(&source, &target, Default::default());
|
||||
assert_eq!(
|
||||
relayed_transactions,
|
||||
Err(RelayedBlockTransactions {
|
||||
processed: 1,
|
||||
relayed: 1,
|
||||
failed: 0,
|
||||
}),
|
||||
);
|
||||
assert_eq!(
|
||||
target.data.lock().submitted_proofs,
|
||||
vec![TestTransactionProof(test_transaction_hash(0))],
|
||||
);
|
||||
|
||||
// now do not fail on tx#2
|
||||
source.data.lock().proofs_to_fail.clear();
|
||||
// and also relay tx#3
|
||||
target
|
||||
.data
|
||||
.lock()
|
||||
.transactions_to_accept
|
||||
.insert(test_transaction_hash(2));
|
||||
|
||||
let relayed_transactions = test_relay_block_transactions(&source, &target, relayed_transactions.unwrap_err());
|
||||
assert_eq!(
|
||||
relayed_transactions,
|
||||
Ok(RelayedBlockTransactions {
|
||||
processed: 3,
|
||||
relayed: 2,
|
||||
failed: 0,
|
||||
}),
|
||||
);
|
||||
assert_eq!(
|
||||
target.data.lock().submitted_proofs,
|
||||
vec![
|
||||
TestTransactionProof(test_transaction_hash(0)),
|
||||
TestTransactionProof(test_transaction_hash(2))
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
// Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Relaying proofs of exchange transactions.
|
||||
|
||||
use crate::exchange::{
|
||||
relay_block_transactions, BlockNumberOf, RelayedBlockTransactions, SourceClient, TargetClient,
|
||||
TransactionProofPipeline,
|
||||
};
|
||||
use crate::exchange_loop_metrics::ExchangeLoopMetrics;
|
||||
|
||||
use backoff::backoff::Backoff;
|
||||
use futures::{future::FutureExt, select};
|
||||
use num_traits::One;
|
||||
use relay_utils::{
|
||||
metrics::{start as metrics_start, GlobalMetrics, MetricsParams},
|
||||
retry_backoff, FailedClient, MaybeConnectionError,
|
||||
};
|
||||
use std::future::Future;
|
||||
|
||||
/// Transactions proofs relay state.
|
||||
#[derive(Debug)]
|
||||
pub struct TransactionProofsRelayState<BlockNumber> {
|
||||
/// Number of last header we have processed so far.
|
||||
pub best_processed_header_number: BlockNumber,
|
||||
}
|
||||
|
||||
/// Transactions proofs relay storage.
|
||||
pub trait TransactionProofsRelayStorage: Clone {
|
||||
/// Associated block number.
|
||||
type BlockNumber;
|
||||
|
||||
/// Get relay state.
|
||||
fn state(&self) -> TransactionProofsRelayState<Self::BlockNumber>;
|
||||
/// Update relay state.
|
||||
fn set_state(&mut self, state: &TransactionProofsRelayState<Self::BlockNumber>);
|
||||
}
|
||||
|
||||
/// In-memory storage for auto-relay loop.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct InMemoryStorage<BlockNumber> {
|
||||
best_processed_header_number: BlockNumber,
|
||||
}
|
||||
|
||||
impl<BlockNumber> InMemoryStorage<BlockNumber> {
|
||||
/// Created new in-memory storage with given best processed block number.
|
||||
pub fn new(best_processed_header_number: BlockNumber) -> Self {
|
||||
InMemoryStorage {
|
||||
best_processed_header_number,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<BlockNumber: Clone + Copy> TransactionProofsRelayStorage for InMemoryStorage<BlockNumber> {
|
||||
type BlockNumber = BlockNumber;
|
||||
|
||||
fn state(&self) -> TransactionProofsRelayState<BlockNumber> {
|
||||
TransactionProofsRelayState {
|
||||
best_processed_header_number: self.best_processed_header_number,
|
||||
}
|
||||
}
|
||||
|
||||
fn set_state(&mut self, state: &TransactionProofsRelayState<BlockNumber>) {
|
||||
self.best_processed_header_number = state.best_processed_header_number;
|
||||
}
|
||||
}
|
||||
|
||||
/// Run proofs synchronization.
|
||||
pub fn run<P: TransactionProofPipeline>(
|
||||
storage: impl TransactionProofsRelayStorage<BlockNumber = BlockNumberOf<P>>,
|
||||
source_client: impl SourceClient<P>,
|
||||
target_client: impl TargetClient<P>,
|
||||
metrics_params: Option<MetricsParams>,
|
||||
exit_signal: impl Future<Output = ()>,
|
||||
) {
|
||||
let exit_signal = exit_signal.shared();
|
||||
let metrics_global = GlobalMetrics::default();
|
||||
let metrics_exch = ExchangeLoopMetrics::default();
|
||||
let metrics_enabled = metrics_params.is_some();
|
||||
metrics_start(
|
||||
format!("{}_to_{}_Exchange", P::SOURCE_NAME, P::TARGET_NAME),
|
||||
metrics_params,
|
||||
&metrics_global,
|
||||
&metrics_exch,
|
||||
);
|
||||
|
||||
relay_utils::relay_loop::run(
|
||||
relay_utils::relay_loop::RECONNECT_DELAY,
|
||||
source_client,
|
||||
target_client,
|
||||
|source_client, target_client| {
|
||||
run_until_connection_lost(
|
||||
storage.clone(),
|
||||
source_client,
|
||||
target_client,
|
||||
if metrics_enabled {
|
||||
Some(metrics_global.clone())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
if metrics_enabled {
|
||||
Some(metrics_exch.clone())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
exit_signal.clone(),
|
||||
)
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Run proofs synchronization.
|
||||
async fn run_until_connection_lost<P: TransactionProofPipeline>(
|
||||
mut storage: impl TransactionProofsRelayStorage<BlockNumber = BlockNumberOf<P>>,
|
||||
source_client: impl SourceClient<P>,
|
||||
target_client: impl TargetClient<P>,
|
||||
metrics_global: Option<GlobalMetrics>,
|
||||
metrics_exch: Option<ExchangeLoopMetrics>,
|
||||
exit_signal: impl Future<Output = ()>,
|
||||
) -> Result<(), FailedClient> {
|
||||
let mut retry_backoff = retry_backoff();
|
||||
let mut state = storage.state();
|
||||
let mut current_finalized_block = None;
|
||||
|
||||
let exit_signal = exit_signal.fuse();
|
||||
|
||||
futures::pin_mut!(exit_signal);
|
||||
|
||||
loop {
|
||||
let iteration_result = run_loop_iteration(
|
||||
&mut storage,
|
||||
&source_client,
|
||||
&target_client,
|
||||
&mut state,
|
||||
&mut current_finalized_block,
|
||||
metrics_exch.as_ref(),
|
||||
)
|
||||
.await;
|
||||
|
||||
if let Some(ref metrics_global) = metrics_global {
|
||||
metrics_global.update().await;
|
||||
}
|
||||
|
||||
if let Err((is_connection_error, failed_client)) = iteration_result {
|
||||
if is_connection_error {
|
||||
return Err(failed_client);
|
||||
}
|
||||
|
||||
let retry_timeout = retry_backoff
|
||||
.next_backoff()
|
||||
.unwrap_or(relay_utils::relay_loop::RECONNECT_DELAY);
|
||||
select! {
|
||||
_ = async_std::task::sleep(retry_timeout).fuse() => {},
|
||||
_ = exit_signal => return Ok(()),
|
||||
}
|
||||
} else {
|
||||
retry_backoff.reset();
|
||||
|
||||
select! {
|
||||
_ = source_client.tick().fuse() => {},
|
||||
_ = exit_signal => return Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Run exchange loop until we need to break.
|
||||
async fn run_loop_iteration<P: TransactionProofPipeline>(
|
||||
storage: &mut impl TransactionProofsRelayStorage<BlockNumber = BlockNumberOf<P>>,
|
||||
source_client: &impl SourceClient<P>,
|
||||
target_client: &impl TargetClient<P>,
|
||||
state: &mut TransactionProofsRelayState<BlockNumberOf<P>>,
|
||||
current_finalized_block: &mut Option<(P::Block, RelayedBlockTransactions)>,
|
||||
exchange_loop_metrics: Option<&ExchangeLoopMetrics>,
|
||||
) -> Result<(), (bool, FailedClient)> {
|
||||
let best_finalized_header_id = match target_client.best_finalized_header_id().await {
|
||||
Ok(best_finalized_header_id) => {
|
||||
log::debug!(
|
||||
target: "bridge",
|
||||
"Got best finalized {} block from {} node: {:?}",
|
||||
P::SOURCE_NAME,
|
||||
P::TARGET_NAME,
|
||||
best_finalized_header_id,
|
||||
);
|
||||
|
||||
best_finalized_header_id
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(
|
||||
target: "bridge",
|
||||
"Failed to retrieve best {} header id from {} node: {:?}. Going to retry...",
|
||||
P::SOURCE_NAME,
|
||||
P::TARGET_NAME,
|
||||
err,
|
||||
);
|
||||
|
||||
return Err((err.is_connection_error(), FailedClient::Target));
|
||||
}
|
||||
};
|
||||
|
||||
loop {
|
||||
// if we already have some finalized block body, try to relay its transactions
|
||||
if let Some((block, relayed_transactions)) = current_finalized_block.take() {
|
||||
let result = relay_block_transactions(source_client, target_client, &block, relayed_transactions).await;
|
||||
|
||||
match result {
|
||||
Ok(relayed_transactions) => {
|
||||
log::info!(
|
||||
target: "bridge",
|
||||
"Relay has processed {} block #{}. Total/Relayed/Failed transactions: {}/{}/{}",
|
||||
P::SOURCE_NAME,
|
||||
state.best_processed_header_number,
|
||||
relayed_transactions.processed,
|
||||
relayed_transactions.relayed,
|
||||
relayed_transactions.failed,
|
||||
);
|
||||
|
||||
state.best_processed_header_number = state.best_processed_header_number + One::one();
|
||||
storage.set_state(state);
|
||||
|
||||
if let Some(ref exchange_loop_metrics) = exchange_loop_metrics {
|
||||
exchange_loop_metrics.update::<P>(
|
||||
state.best_processed_header_number,
|
||||
best_finalized_header_id.0,
|
||||
relayed_transactions,
|
||||
);
|
||||
}
|
||||
|
||||
// we have just updated state => proceed to next block retrieval
|
||||
}
|
||||
Err((failed_client, relayed_transactions)) => {
|
||||
*current_finalized_block = Some((block, relayed_transactions));
|
||||
return Err((true, failed_client));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// we may need to retrieve finalized block body from source node
|
||||
if best_finalized_header_id.0 > state.best_processed_header_number {
|
||||
let next_block_number = state.best_processed_header_number + One::one();
|
||||
let result = source_client.block_by_number(next_block_number).await;
|
||||
|
||||
match result {
|
||||
Ok(block) => {
|
||||
*current_finalized_block = Some((block, RelayedBlockTransactions::default()));
|
||||
|
||||
// we have received new finalized block => go back to relay its transactions
|
||||
continue;
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(
|
||||
target: "bridge",
|
||||
"Failed to retrieve canonical block #{} from {} node: {:?}. Going to retry...",
|
||||
next_block_number,
|
||||
P::SOURCE_NAME,
|
||||
err,
|
||||
);
|
||||
|
||||
return Err((err.is_connection_error(), FailedClient::Source));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// there are no any transactions we need to relay => wait for new data
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::exchange::tests::{
|
||||
test_next_block, test_next_block_id, test_transaction_hash, TestTransactionProof, TestTransactionsSource,
|
||||
TestTransactionsTarget,
|
||||
};
|
||||
use futures::{future::FutureExt, stream::StreamExt};
|
||||
|
||||
#[test]
|
||||
fn exchange_loop_is_able_to_relay_proofs() {
|
||||
let storage = InMemoryStorage {
|
||||
best_processed_header_number: 0,
|
||||
};
|
||||
let target = TestTransactionsTarget::new(Box::new(|_| unreachable!("no target ticks allowed")));
|
||||
let target_data = target.data.clone();
|
||||
let (exit_sender, exit_receiver) = futures::channel::mpsc::unbounded();
|
||||
|
||||
let source = TestTransactionsSource::new(Box::new(move |data| {
|
||||
let transaction1_relayed = target_data
|
||||
.lock()
|
||||
.submitted_proofs
|
||||
.contains(&TestTransactionProof(test_transaction_hash(0)));
|
||||
let transaction2_relayed = target_data
|
||||
.lock()
|
||||
.submitted_proofs
|
||||
.contains(&TestTransactionProof(test_transaction_hash(1)));
|
||||
match (transaction1_relayed, transaction2_relayed) {
|
||||
(true, true) => exit_sender.unbounded_send(()).unwrap(),
|
||||
(true, false) => {
|
||||
data.block = Ok(test_next_block());
|
||||
target_data.lock().best_finalized_header_id = Ok(test_next_block_id());
|
||||
target_data
|
||||
.lock()
|
||||
.transactions_to_accept
|
||||
.insert(test_transaction_hash(1));
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}));
|
||||
|
||||
run(
|
||||
storage,
|
||||
source,
|
||||
target,
|
||||
None,
|
||||
exit_receiver.into_future().map(|(_, _)| ()),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
// Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Metrics for currency-exchange relay loop.
|
||||
|
||||
use crate::exchange::{BlockNumberOf, RelayedBlockTransactions, TransactionProofPipeline};
|
||||
use relay_utils::metrics::{register, Counter, CounterVec, GaugeVec, Metrics, Opts, Registry, U64};
|
||||
|
||||
/// Exchange transactions relay metrics.
|
||||
#[derive(Clone)]
|
||||
pub struct ExchangeLoopMetrics {
|
||||
/// Best finalized block numbers - "processed" and "known".
|
||||
best_block_numbers: GaugeVec<U64>,
|
||||
/// Number of processed blocks ("total").
|
||||
processed_blocks: Counter<U64>,
|
||||
/// Number of processed transactions ("total", "relayed" and "failed").
|
||||
processed_transactions: CounterVec<U64>,
|
||||
}
|
||||
|
||||
impl Metrics for ExchangeLoopMetrics {
|
||||
fn register(&self, registry: &Registry) -> Result<(), String> {
|
||||
register(self.best_block_numbers.clone(), registry).map_err(|e| e.to_string())?;
|
||||
register(self.processed_blocks.clone(), registry).map_err(|e| e.to_string())?;
|
||||
register(self.processed_transactions.clone(), registry).map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ExchangeLoopMetrics {
|
||||
fn default() -> Self {
|
||||
ExchangeLoopMetrics {
|
||||
best_block_numbers: GaugeVec::new(
|
||||
Opts::new("best_block_numbers", "Best finalized block numbers"),
|
||||
&["type"],
|
||||
)
|
||||
.expect("metric is static and thus valid; qed"),
|
||||
processed_blocks: Counter::new("processed_blocks", "Total number of processed blocks")
|
||||
.expect("metric is static and thus valid; qed"),
|
||||
processed_transactions: CounterVec::new(
|
||||
Opts::new("processed_transactions", "Total number of processed transactions"),
|
||||
&["type"],
|
||||
)
|
||||
.expect("metric is static and thus valid; qed"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ExchangeLoopMetrics {
|
||||
/// Update metrics when single block is relayed.
|
||||
pub fn update<P: TransactionProofPipeline>(
|
||||
&self,
|
||||
best_processed_block_number: BlockNumberOf<P>,
|
||||
best_known_block_number: BlockNumberOf<P>,
|
||||
relayed_transactions: RelayedBlockTransactions,
|
||||
) {
|
||||
self.best_block_numbers
|
||||
.with_label_values(&["processed"])
|
||||
.set(best_processed_block_number.into());
|
||||
self.best_block_numbers
|
||||
.with_label_values(&["known"])
|
||||
.set(best_known_block_number.into());
|
||||
|
||||
self.processed_blocks.inc();
|
||||
|
||||
self.processed_transactions
|
||||
.with_label_values(&["total"])
|
||||
.inc_by(relayed_transactions.processed as _);
|
||||
self.processed_transactions
|
||||
.with_label_values(&["relayed"])
|
||||
.inc_by(relayed_transactions.relayed as _);
|
||||
self.processed_transactions
|
||||
.with_label_values(&["failed"])
|
||||
.inc_by(relayed_transactions.failed as _);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Relaying [`currency-exchange`](../pallet_bridge_currency_exchange/index.html) application
|
||||
//! specific data. Currency exchange application allows exchanging tokens between bridged chains.
|
||||
//! This module provides entrypoints for crafting and submitting (single and multiple)
|
||||
//! proof-of-exchange-at-source-chain transaction(s) to target chain.
|
||||
|
||||
#![warn(missing_docs)]
|
||||
|
||||
pub mod exchange;
|
||||
pub mod exchange_loop;
|
||||
pub mod exchange_loop_metrics;
|
||||
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "headers-relay"
|
||||
version = "0.1.0"
|
||||
authors = ["Parity Technologies <admin@parity.io>"]
|
||||
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" }
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,33 @@
|
||||
// Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Relaying source chain headers to target chain. This module provides entrypoint
|
||||
//! that starts reading new headers from source chain and submit these headers as
|
||||
//! module/contract transactions to the target chain. Module/contract on the target
|
||||
//! chain is a light-client of the source chain. All other trustless bridge
|
||||
//! applications are built using this light-client, so running headers-relay is
|
||||
//! essential for running all other bridge applications.
|
||||
|
||||
// required for futures::select!
|
||||
#![recursion_limit = "1024"]
|
||||
#![warn(missing_docs)]
|
||||
|
||||
pub mod headers;
|
||||
pub mod sync;
|
||||
pub mod sync_loop;
|
||||
pub mod sync_loop_metrics;
|
||||
pub mod sync_loop_tests;
|
||||
pub mod sync_types;
|
||||
@@ -0,0 +1,523 @@
|
||||
// Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Headers synchronization context. This structure wraps headers queue and is
|
||||
//! able to choose: which headers to read from the source chain? Which headers
|
||||
//! to submit to the target chain? The context makes decisions basing on parameters
|
||||
//! passed using `HeadersSyncParams` structure.
|
||||
|
||||
use crate::headers::QueuedHeaders;
|
||||
use crate::sync_types::{HeaderIdOf, HeaderStatus, HeadersSyncPipeline, QueuedHeader};
|
||||
use num_traits::{One, Saturating, Zero};
|
||||
|
||||
/// Common sync params.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HeadersSyncParams {
|
||||
/// Maximal number of ethereum headers to pre-download.
|
||||
pub max_future_headers_to_download: usize,
|
||||
/// Maximal number of active (we believe) submit header transactions.
|
||||
pub max_headers_in_submitted_status: usize,
|
||||
/// Maximal number of headers in single submit request.
|
||||
pub max_headers_in_single_submit: usize,
|
||||
/// Maximal total headers size in single submit request.
|
||||
pub max_headers_size_in_single_submit: usize,
|
||||
/// We only may store and accept (from Ethereum node) headers that have
|
||||
/// number >= than best_substrate_header.number - prune_depth.
|
||||
pub prune_depth: u32,
|
||||
/// Target transactions mode.
|
||||
pub target_tx_mode: TargetTransactionMode,
|
||||
}
|
||||
|
||||
/// Target transaction mode.
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum TargetTransactionMode {
|
||||
/// Submit new headers using signed transactions.
|
||||
Signed,
|
||||
/// Submit new headers using unsigned transactions.
|
||||
Unsigned,
|
||||
/// Submit new headers using signed transactions, but only when we
|
||||
/// believe that sync has stalled.
|
||||
Backup,
|
||||
}
|
||||
|
||||
/// Headers synchronization context.
|
||||
#[derive(Debug)]
|
||||
pub struct HeadersSync<P: HeadersSyncPipeline> {
|
||||
/// Synchronization parameters.
|
||||
params: HeadersSyncParams,
|
||||
/// Best header number known to source node.
|
||||
source_best_number: Option<P::Number>,
|
||||
/// Best header known to target node.
|
||||
target_best_header: Option<HeaderIdOf<P>>,
|
||||
/// Headers queue.
|
||||
headers: QueuedHeaders<P>,
|
||||
/// Pause headers submission.
|
||||
pause_submit: bool,
|
||||
}
|
||||
|
||||
impl<P: HeadersSyncPipeline> HeadersSync<P> {
|
||||
/// Creates new headers synchronizer.
|
||||
pub fn new(params: HeadersSyncParams) -> Self {
|
||||
HeadersSync {
|
||||
headers: QueuedHeaders::default(),
|
||||
params,
|
||||
source_best_number: None,
|
||||
target_best_header: None,
|
||||
pause_submit: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return best header number known to source node.
|
||||
pub fn source_best_number(&self) -> Option<P::Number> {
|
||||
self.source_best_number
|
||||
}
|
||||
|
||||
/// Best header known to target node.
|
||||
pub fn target_best_header(&self) -> Option<HeaderIdOf<P>> {
|
||||
self.target_best_header
|
||||
}
|
||||
|
||||
/// Returns true if we have synced almost all known headers.
|
||||
pub fn is_almost_synced(&self) -> bool {
|
||||
match self.source_best_number {
|
||||
Some(source_best_number) => self
|
||||
.target_best_header
|
||||
.map(|best| source_best_number.saturating_sub(best.0) < 4.into())
|
||||
.unwrap_or(false),
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns synchronization status.
|
||||
pub fn status(&self) -> (&Option<HeaderIdOf<P>>, &Option<P::Number>) {
|
||||
(&self.target_best_header, &self.source_best_number)
|
||||
}
|
||||
|
||||
/// Returns reference to the headers queue.
|
||||
pub fn headers(&self) -> &QueuedHeaders<P> {
|
||||
&self.headers
|
||||
}
|
||||
|
||||
/// Returns mutable reference to the headers queue.
|
||||
pub fn headers_mut(&mut self) -> &mut QueuedHeaders<P> {
|
||||
&mut self.headers
|
||||
}
|
||||
|
||||
/// Select header that needs to be downloaded from the source node.
|
||||
pub fn select_new_header_to_download(&self) -> Option<P::Number> {
|
||||
// if we haven't received best header from source node yet, there's nothing we can download
|
||||
let source_best_number = self.source_best_number?;
|
||||
|
||||
// if we haven't received known best header from target node yet, there's nothing we can download
|
||||
let target_best_header = self.target_best_header.as_ref()?;
|
||||
|
||||
// if there's too many headers in the queue, stop downloading
|
||||
let in_memory_headers = self.headers.total_headers();
|
||||
if in_memory_headers >= self.params.max_future_headers_to_download {
|
||||
return None;
|
||||
}
|
||||
|
||||
// if queue is empty and best header on target is > than best header on source,
|
||||
// then we shoud reorg
|
||||
let best_queued_number = self.headers.best_queued_number();
|
||||
if best_queued_number.is_zero() && source_best_number < target_best_header.0 {
|
||||
return Some(source_best_number);
|
||||
}
|
||||
|
||||
// we assume that there were no reorgs if we have already downloaded best header
|
||||
let best_downloaded_number = std::cmp::max(
|
||||
std::cmp::max(best_queued_number, self.headers.best_synced_number()),
|
||||
target_best_header.0,
|
||||
);
|
||||
if best_downloaded_number >= source_best_number {
|
||||
return None;
|
||||
}
|
||||
|
||||
// download new header
|
||||
Some(best_downloaded_number + One::one())
|
||||
}
|
||||
|
||||
/// Selech orphan header to downoload.
|
||||
pub fn select_orphan_header_to_download(&self) -> Option<&QueuedHeader<P>> {
|
||||
let orphan_header = self.headers.header(HeaderStatus::Orphan)?;
|
||||
|
||||
// we consider header orphan until we'll find it ancestor that is known to the target node
|
||||
// => we may get orphan header while we ask target node whether it knows its parent
|
||||
// => let's avoid fetching duplicate headers
|
||||
let parent_id = orphan_header.parent_id();
|
||||
if self.headers.status(&parent_id) != HeaderStatus::Unknown {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(orphan_header)
|
||||
}
|
||||
|
||||
/// Select headers that need to be submitted to the target node.
|
||||
pub fn select_headers_to_submit(&self, stalled: bool) -> Option<Vec<&QueuedHeader<P>>> {
|
||||
// maybe we have paused new headers submit?
|
||||
if self.pause_submit {
|
||||
return None;
|
||||
}
|
||||
|
||||
// if we operate in backup mode, we only submit headers when sync has stalled
|
||||
if self.params.target_tx_mode == TargetTransactionMode::Backup && !stalled {
|
||||
return None;
|
||||
}
|
||||
|
||||
let headers_in_submit_status = self.headers.headers_in_status(HeaderStatus::Submitted);
|
||||
let headers_to_submit_count = self
|
||||
.params
|
||||
.max_headers_in_submitted_status
|
||||
.checked_sub(headers_in_submit_status)?;
|
||||
|
||||
let mut total_size = 0;
|
||||
let mut total_headers = 0;
|
||||
self.headers.headers(HeaderStatus::Ready, |header| {
|
||||
if total_headers == headers_to_submit_count {
|
||||
return false;
|
||||
}
|
||||
if total_headers == self.params.max_headers_in_single_submit {
|
||||
return false;
|
||||
}
|
||||
|
||||
let encoded_size = P::estimate_size(header);
|
||||
if total_headers != 0 && total_size + encoded_size > self.params.max_headers_size_in_single_submit {
|
||||
return false;
|
||||
}
|
||||
|
||||
total_size += encoded_size;
|
||||
total_headers += 1;
|
||||
|
||||
true
|
||||
})
|
||||
}
|
||||
|
||||
/// Receive new target header number from the source node.
|
||||
pub fn source_best_header_number_response(&mut self, best_header_number: P::Number) {
|
||||
log::debug!(
|
||||
target: "bridge",
|
||||
"Received best header number from {} node: {}",
|
||||
P::SOURCE_NAME,
|
||||
best_header_number,
|
||||
);
|
||||
self.source_best_number = Some(best_header_number);
|
||||
}
|
||||
|
||||
/// Receive new best header from the target node.
|
||||
/// Returns true if it is different from the previous block known to us.
|
||||
pub fn target_best_header_response(&mut self, best_header: HeaderIdOf<P>) -> bool {
|
||||
log::debug!(
|
||||
target: "bridge",
|
||||
"Received best known header from {}: {:?}",
|
||||
P::TARGET_NAME,
|
||||
best_header,
|
||||
);
|
||||
|
||||
// early return if it is still the same
|
||||
if self.target_best_header == Some(best_header) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// remember that this header is now known to the Substrate runtime
|
||||
self.headers.target_best_header_response(&best_header);
|
||||
|
||||
// prune ancient headers
|
||||
self.headers
|
||||
.prune(best_header.0.saturating_sub(self.params.prune_depth.into()));
|
||||
|
||||
// finally remember the best header itself
|
||||
self.target_best_header = Some(best_header);
|
||||
|
||||
// we are ready to submit headers again
|
||||
if self.pause_submit {
|
||||
log::debug!(
|
||||
target: "bridge",
|
||||
"Ready to submit {} headers to {} node again!",
|
||||
P::SOURCE_NAME,
|
||||
P::TARGET_NAME,
|
||||
);
|
||||
|
||||
self.pause_submit = false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// Pause headers submit until best header will be updated on target node.
|
||||
pub fn pause_submit(&mut self) {
|
||||
log::debug!(
|
||||
target: "bridge",
|
||||
"Stopping submitting {} headers to {} node. Waiting for {} submitted headers to be accepted",
|
||||
P::SOURCE_NAME,
|
||||
P::TARGET_NAME,
|
||||
self.headers.headers_in_status(HeaderStatus::Submitted),
|
||||
);
|
||||
|
||||
self.pause_submit = true;
|
||||
}
|
||||
|
||||
/// Restart synchronization.
|
||||
pub fn restart(&mut self) {
|
||||
self.source_best_number = None;
|
||||
self.target_best_header = None;
|
||||
self.headers.clear();
|
||||
self.pause_submit = false;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
use crate::headers::tests::{header, id};
|
||||
use crate::sync_loop_tests::{TestHash, TestHeadersSyncPipeline, TestNumber};
|
||||
use crate::sync_types::HeaderStatus;
|
||||
use relay_utils::HeaderId;
|
||||
|
||||
fn side_hash(number: TestNumber) -> TestHash {
|
||||
1000 + number
|
||||
}
|
||||
|
||||
pub fn default_sync_params() -> HeadersSyncParams {
|
||||
HeadersSyncParams {
|
||||
max_future_headers_to_download: 128,
|
||||
max_headers_in_submitted_status: 128,
|
||||
max_headers_in_single_submit: 32,
|
||||
max_headers_size_in_single_submit: 131_072,
|
||||
prune_depth: 4096,
|
||||
target_tx_mode: TargetTransactionMode::Signed,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_new_header_to_download_works() {
|
||||
let mut eth_sync = HeadersSync::<TestHeadersSyncPipeline>::new(default_sync_params());
|
||||
|
||||
// both best && target headers are unknown
|
||||
assert_eq!(eth_sync.select_new_header_to_download(), None);
|
||||
|
||||
// best header is known, target header is unknown
|
||||
eth_sync.target_best_header = Some(HeaderId(0, Default::default()));
|
||||
assert_eq!(eth_sync.select_new_header_to_download(), None);
|
||||
|
||||
// target header is known, best header is unknown
|
||||
eth_sync.target_best_header = None;
|
||||
eth_sync.source_best_number = Some(100);
|
||||
assert_eq!(eth_sync.select_new_header_to_download(), None);
|
||||
|
||||
// when our best block has the same number as the target
|
||||
eth_sync.target_best_header = Some(HeaderId(100, Default::default()));
|
||||
assert_eq!(eth_sync.select_new_header_to_download(), None);
|
||||
|
||||
// when we actually need a new header
|
||||
eth_sync.source_best_number = Some(101);
|
||||
assert_eq!(eth_sync.select_new_header_to_download(), Some(101));
|
||||
|
||||
// when we have to reorganize to longer fork
|
||||
eth_sync.source_best_number = Some(100);
|
||||
eth_sync.target_best_header = Some(HeaderId(200, Default::default()));
|
||||
assert_eq!(eth_sync.select_new_header_to_download(), Some(100));
|
||||
|
||||
// when there are too many headers scheduled for submitting
|
||||
for i in 1..1000 {
|
||||
eth_sync.headers.header_response(header(i).header().clone());
|
||||
}
|
||||
assert_eq!(eth_sync.select_new_header_to_download(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_new_header_to_download_works_with_empty_queue() {
|
||||
let mut eth_sync = HeadersSync::<TestHeadersSyncPipeline>::new(default_sync_params());
|
||||
eth_sync.source_best_header_number_response(100);
|
||||
|
||||
// when queue is not empty => everything goes as usually
|
||||
eth_sync.target_best_header_response(header(10).id());
|
||||
eth_sync.headers_mut().header_response(header(11).header().clone());
|
||||
eth_sync.headers_mut().maybe_extra_response(&header(11).id(), false);
|
||||
assert_eq!(eth_sync.select_new_header_to_download(), Some(12));
|
||||
|
||||
// but then queue is drained
|
||||
eth_sync.headers_mut().target_best_header_response(&header(11).id());
|
||||
|
||||
// even though it's empty, we know that header#11 is synced
|
||||
assert_eq!(eth_sync.headers().best_queued_number(), 0);
|
||||
assert_eq!(eth_sync.headers().best_synced_number(), 11);
|
||||
assert_eq!(eth_sync.select_new_header_to_download(), Some(12));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_without_reorgs_works() {
|
||||
let mut eth_sync = HeadersSync::new(default_sync_params());
|
||||
eth_sync.params.max_headers_in_submitted_status = 1;
|
||||
|
||||
// ethereum reports best header #102
|
||||
eth_sync.source_best_header_number_response(102);
|
||||
|
||||
// substrate reports that it is at block #100
|
||||
eth_sync.target_best_header_response(id(100));
|
||||
|
||||
// block #101 is downloaded first
|
||||
assert_eq!(eth_sync.select_new_header_to_download(), Some(101));
|
||||
eth_sync.headers.header_response(header(101).header().clone());
|
||||
|
||||
// now header #101 is ready to be submitted
|
||||
assert_eq!(eth_sync.headers.header(HeaderStatus::MaybeExtra), Some(&header(101)));
|
||||
eth_sync.headers.maybe_extra_response(&id(101), false);
|
||||
assert_eq!(eth_sync.headers.header(HeaderStatus::Ready), Some(&header(101)));
|
||||
assert_eq!(eth_sync.select_headers_to_submit(false), Some(vec![&header(101)]));
|
||||
|
||||
// and header #102 is ready to be downloaded
|
||||
assert_eq!(eth_sync.select_new_header_to_download(), Some(102));
|
||||
eth_sync.headers.header_response(header(102).header().clone());
|
||||
|
||||
// receive submission confirmation
|
||||
eth_sync.headers.headers_submitted(vec![id(101)]);
|
||||
|
||||
// we have nothing to submit because previous header hasn't been confirmed yet
|
||||
// (and we allow max 1 submit transaction in the wild)
|
||||
assert_eq!(eth_sync.headers.header(HeaderStatus::MaybeExtra), Some(&header(102)));
|
||||
eth_sync.headers.maybe_extra_response(&id(102), false);
|
||||
assert_eq!(eth_sync.headers.header(HeaderStatus::Ready), Some(&header(102)));
|
||||
assert_eq!(eth_sync.select_headers_to_submit(false), None);
|
||||
|
||||
// substrate reports that it has imported block #101
|
||||
eth_sync.target_best_header_response(id(101));
|
||||
|
||||
// and we are ready to submit #102
|
||||
assert_eq!(eth_sync.select_headers_to_submit(false), Some(vec![&header(102)]));
|
||||
eth_sync.headers.headers_submitted(vec![id(102)]);
|
||||
|
||||
// substrate reports that it has imported block #102
|
||||
eth_sync.target_best_header_response(id(102));
|
||||
|
||||
// and we have nothing to download
|
||||
assert_eq!(eth_sync.select_new_header_to_download(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_with_orphan_headers_work() {
|
||||
let mut eth_sync = HeadersSync::new(default_sync_params());
|
||||
|
||||
// ethereum reports best header #102
|
||||
eth_sync.source_best_header_number_response(102);
|
||||
|
||||
// substrate reports that it is at block #100, but it isn't part of best chain
|
||||
eth_sync.target_best_header_response(HeaderId(100, side_hash(100)));
|
||||
|
||||
// block #101 is downloaded first
|
||||
assert_eq!(eth_sync.select_new_header_to_download(), Some(101));
|
||||
eth_sync.headers.header_response(header(101).header().clone());
|
||||
|
||||
// we can't submit header #101, because its parent status is unknown
|
||||
assert_eq!(eth_sync.select_headers_to_submit(false), None);
|
||||
|
||||
// instead we are trying to determine status of its parent (#100)
|
||||
assert_eq!(eth_sync.headers.header(HeaderStatus::MaybeOrphan), Some(&header(101)));
|
||||
|
||||
// and the status is still unknown
|
||||
eth_sync.headers.maybe_orphan_response(&id(100), false);
|
||||
|
||||
// so we consider #101 orphaned now && will download its parent - #100
|
||||
assert_eq!(eth_sync.headers.header(HeaderStatus::Orphan), Some(&header(101)));
|
||||
eth_sync.headers.header_response(header(100).header().clone());
|
||||
|
||||
// #101 is now Orphan and #100 is MaybeOrphan => we do not want to retrieve
|
||||
// header #100 again
|
||||
assert_eq!(eth_sync.headers.header(HeaderStatus::Orphan), Some(&header(101)));
|
||||
assert_eq!(eth_sync.select_orphan_header_to_download(), None);
|
||||
|
||||
// we can't submit header #100, because its parent status is unknown
|
||||
assert_eq!(eth_sync.select_headers_to_submit(false), None);
|
||||
|
||||
// instead we are trying to determine status of its parent (#99)
|
||||
assert_eq!(eth_sync.headers.header(HeaderStatus::MaybeOrphan), Some(&header(100)));
|
||||
|
||||
// and the status is known, so we move previously orphaned #100 and #101 to ready queue
|
||||
eth_sync.headers.maybe_orphan_response(&id(99), true);
|
||||
|
||||
// and we are ready to submit #100
|
||||
assert_eq!(eth_sync.headers.header(HeaderStatus::MaybeExtra), Some(&header(100)));
|
||||
eth_sync.headers.maybe_extra_response(&id(100), false);
|
||||
assert_eq!(eth_sync.select_headers_to_submit(false), Some(vec![&header(100)]));
|
||||
eth_sync.headers.headers_submitted(vec![id(100)]);
|
||||
|
||||
// and we are ready to submit #101
|
||||
assert_eq!(eth_sync.headers.header(HeaderStatus::MaybeExtra), Some(&header(101)));
|
||||
eth_sync.headers.maybe_extra_response(&id(101), false);
|
||||
assert_eq!(eth_sync.select_headers_to_submit(false), Some(vec![&header(101)]));
|
||||
eth_sync.headers.headers_submitted(vec![id(101)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pruning_happens_on_target_best_header_response() {
|
||||
let mut eth_sync = HeadersSync::<TestHeadersSyncPipeline>::new(default_sync_params());
|
||||
eth_sync.params.prune_depth = 50;
|
||||
eth_sync.target_best_header_response(id(100));
|
||||
assert_eq!(eth_sync.headers.prune_border(), 50);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_submitting_headers_in_backup_mode_when_stalled() {
|
||||
let mut eth_sync = HeadersSync::new(default_sync_params());
|
||||
eth_sync.params.target_tx_mode = TargetTransactionMode::Backup;
|
||||
|
||||
// ethereum reports best header #102
|
||||
eth_sync.source_best_header_number_response(102);
|
||||
|
||||
// substrate reports that it is at block #100
|
||||
eth_sync.target_best_header_response(id(100));
|
||||
|
||||
// block #101 is downloaded first
|
||||
eth_sync.headers.header_response(header(101).header().clone());
|
||||
eth_sync.headers.maybe_extra_response(&id(101), false);
|
||||
|
||||
// ensure that headers are not submitted when sync is not stalled
|
||||
assert_eq!(eth_sync.select_headers_to_submit(false), None);
|
||||
|
||||
// ensure that headers are not submitted when sync is stalled
|
||||
assert_eq!(eth_sync.select_headers_to_submit(true), Some(vec![&header(101)]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn does_not_select_new_headers_to_submit_when_submit_is_paused() {
|
||||
let mut eth_sync = HeadersSync::new(default_sync_params());
|
||||
eth_sync.params.max_headers_in_submitted_status = 1;
|
||||
|
||||
// ethereum reports best header #102 and substrate is at #100
|
||||
eth_sync.source_best_header_number_response(102);
|
||||
eth_sync.target_best_header_response(id(100));
|
||||
|
||||
// let's prepare #101 and #102 for submitting
|
||||
eth_sync.headers.header_response(header(101).header().clone());
|
||||
eth_sync.headers.maybe_extra_response(&id(101), false);
|
||||
eth_sync.headers.header_response(header(102).header().clone());
|
||||
eth_sync.headers.maybe_extra_response(&id(102), false);
|
||||
|
||||
// when submit is not paused, we're ready to submit #101
|
||||
assert_eq!(eth_sync.select_headers_to_submit(false), Some(vec![&header(101)]));
|
||||
|
||||
// when submit is paused, we're not ready to submit anything
|
||||
eth_sync.pause_submit();
|
||||
assert_eq!(eth_sync.select_headers_to_submit(false), None);
|
||||
|
||||
// if best header on substrate node isn't updated, we still not submitting anything
|
||||
eth_sync.target_best_header_response(id(100));
|
||||
assert_eq!(eth_sync.select_headers_to_submit(false), None);
|
||||
|
||||
// but after it is actually updated, we are ready to submit
|
||||
eth_sync.target_best_header_response(id(101));
|
||||
assert_eq!(eth_sync.select_headers_to_submit(false), Some(vec![&header(102)]));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,654 @@
|
||||
// Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Entrypoint for running headers synchronization loop.
|
||||
|
||||
use crate::sync::{HeadersSync, HeadersSyncParams};
|
||||
use crate::sync_loop_metrics::SyncLoopMetrics;
|
||||
use crate::sync_types::{HeaderIdOf, HeaderStatus, HeadersSyncPipeline, QueuedHeader, SubmittedHeaders};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use futures::{future::FutureExt, stream::StreamExt};
|
||||
use num_traits::{Saturating, Zero};
|
||||
use relay_utils::{
|
||||
format_ids, interval,
|
||||
metrics::{start as metrics_start, GlobalMetrics, MetricsParams},
|
||||
process_future_result,
|
||||
relay_loop::Client as RelayClient,
|
||||
retry_backoff, FailedClient, MaybeConnectionError, StringifiedMaybeConnectionError,
|
||||
};
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
future::Future,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
/// When we submit headers to target node, but see no updates of best
|
||||
/// source block known to target node during STALL_SYNC_TIMEOUT seconds,
|
||||
/// we consider that our headers are rejected because there has been reorg in target chain.
|
||||
/// This reorg could invalidate our knowledge about sync process (i.e. we have asked if
|
||||
/// HeaderA is known to target, but then reorg happened and the answer is different
|
||||
/// now) => we need to reset sync.
|
||||
/// The other option is to receive **EVERY** best target header and check if it is
|
||||
/// direct child of previous best header. But: (1) subscription doesn't guarantee that
|
||||
/// the subscriber will receive every best header (2) reorg won't always lead to sync
|
||||
/// stall and restart is a heavy operation (we forget all in-memory headers).
|
||||
const STALL_SYNC_TIMEOUT: Duration = Duration::from_secs(5 * 60);
|
||||
/// Delay after we have seen update of best source header at target node,
|
||||
/// for us to treat sync stalled. ONLY when relay operates in backup mode.
|
||||
const BACKUP_STALL_SYNC_TIMEOUT: Duration = Duration::from_secs(10 * 60);
|
||||
/// Interval between calling sync maintain procedure.
|
||||
const MAINTAIN_INTERVAL: Duration = Duration::from_secs(30);
|
||||
|
||||
/// Source client trait.
|
||||
#[async_trait]
|
||||
pub trait SourceClient<P: HeadersSyncPipeline>: RelayClient {
|
||||
/// Get best block number.
|
||||
async fn best_block_number(&self) -> Result<P::Number, Self::Error>;
|
||||
|
||||
/// Get header by hash.
|
||||
async fn header_by_hash(&self, hash: P::Hash) -> Result<P::Header, Self::Error>;
|
||||
|
||||
/// Get canonical header by number.
|
||||
async fn header_by_number(&self, number: P::Number) -> Result<P::Header, Self::Error>;
|
||||
|
||||
/// Get completion data by header hash.
|
||||
async fn header_completion(&self, id: HeaderIdOf<P>)
|
||||
-> Result<(HeaderIdOf<P>, Option<P::Completion>), Self::Error>;
|
||||
|
||||
/// Get extra data by header hash.
|
||||
async fn header_extra(
|
||||
&self,
|
||||
id: HeaderIdOf<P>,
|
||||
header: QueuedHeader<P>,
|
||||
) -> Result<(HeaderIdOf<P>, P::Extra), Self::Error>;
|
||||
}
|
||||
|
||||
/// Target client trait.
|
||||
#[async_trait]
|
||||
pub trait TargetClient<P: HeadersSyncPipeline>: RelayClient {
|
||||
/// Returns ID of best header known to the target node.
|
||||
async fn best_header_id(&self) -> Result<HeaderIdOf<P>, Self::Error>;
|
||||
|
||||
/// Returns true if header is known to the target node.
|
||||
async fn is_known_header(&self, id: HeaderIdOf<P>) -> Result<(HeaderIdOf<P>, bool), Self::Error>;
|
||||
|
||||
/// Submit headers.
|
||||
async fn submit_headers(&self, headers: Vec<QueuedHeader<P>>) -> SubmittedHeaders<HeaderIdOf<P>, Self::Error>;
|
||||
|
||||
/// Returns ID of headers that require to be 'completed' before children can be submitted.
|
||||
async fn incomplete_headers_ids(&self) -> Result<HashSet<HeaderIdOf<P>>, Self::Error>;
|
||||
|
||||
/// Submit completion data for header.
|
||||
async fn complete_header(&self, id: HeaderIdOf<P>, completion: P::Completion)
|
||||
-> Result<HeaderIdOf<P>, Self::Error>;
|
||||
|
||||
/// Returns true if header requires extra data to be submitted.
|
||||
async fn requires_extra(&self, header: QueuedHeader<P>) -> Result<(HeaderIdOf<P>, bool), Self::Error>;
|
||||
}
|
||||
|
||||
/// Synchronization maintain procedure.
|
||||
#[async_trait]
|
||||
pub trait SyncMaintain<P: HeadersSyncPipeline>: Clone + Send + Sync {
|
||||
/// Run custom maintain procedures. This is guaranteed to be called when both source and target
|
||||
/// clients are unoccupied.
|
||||
async fn maintain(&self, _sync: &mut HeadersSync<P>) {}
|
||||
}
|
||||
|
||||
impl<P: HeadersSyncPipeline> SyncMaintain<P> for () {}
|
||||
|
||||
/// Run headers synchronization.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn run<P: HeadersSyncPipeline, TC: TargetClient<P>>(
|
||||
source_client: impl SourceClient<P>,
|
||||
source_tick: Duration,
|
||||
target_client: TC,
|
||||
target_tick: Duration,
|
||||
sync_maintain: impl SyncMaintain<P>,
|
||||
sync_params: HeadersSyncParams,
|
||||
metrics_params: Option<MetricsParams>,
|
||||
exit_signal: impl Future<Output = ()>,
|
||||
) {
|
||||
let exit_signal = exit_signal.shared();
|
||||
|
||||
let metrics_global = GlobalMetrics::default();
|
||||
let metrics_sync = SyncLoopMetrics::default();
|
||||
let metrics_enabled = metrics_params.is_some();
|
||||
metrics_start(
|
||||
format!("{}_to_{}_Sync", P::SOURCE_NAME, P::TARGET_NAME),
|
||||
metrics_params,
|
||||
&metrics_global,
|
||||
&metrics_sync,
|
||||
);
|
||||
|
||||
relay_utils::relay_loop::run(
|
||||
relay_utils::relay_loop::RECONNECT_DELAY,
|
||||
source_client,
|
||||
target_client,
|
||||
|source_client, target_client| {
|
||||
run_until_connection_lost(
|
||||
source_client,
|
||||
source_tick,
|
||||
target_client,
|
||||
target_tick,
|
||||
sync_maintain.clone(),
|
||||
sync_params.clone(),
|
||||
if metrics_enabled {
|
||||
Some(metrics_global.clone())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
if metrics_enabled {
|
||||
Some(metrics_sync.clone())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
exit_signal.clone(),
|
||||
)
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Run headers synchronization.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn run_until_connection_lost<P: HeadersSyncPipeline, TC: TargetClient<P>>(
|
||||
source_client: impl SourceClient<P>,
|
||||
source_tick: Duration,
|
||||
target_client: TC,
|
||||
target_tick: Duration,
|
||||
sync_maintain: impl SyncMaintain<P>,
|
||||
sync_params: HeadersSyncParams,
|
||||
metrics_global: Option<GlobalMetrics>,
|
||||
metrics_sync: Option<SyncLoopMetrics>,
|
||||
exit_signal: impl Future<Output = ()>,
|
||||
) -> Result<(), FailedClient> {
|
||||
let mut progress_context = (Instant::now(), None, None);
|
||||
|
||||
let mut sync = HeadersSync::<P>::new(sync_params);
|
||||
let mut stall_countdown = None;
|
||||
let mut last_update_time = Instant::now();
|
||||
|
||||
let mut source_retry_backoff = retry_backoff();
|
||||
let mut source_client_is_online = false;
|
||||
let mut source_best_block_number_required = false;
|
||||
let source_best_block_number_future = source_client.best_block_number().fuse();
|
||||
let source_new_header_future = futures::future::Fuse::terminated();
|
||||
let source_orphan_header_future = futures::future::Fuse::terminated();
|
||||
let source_extra_future = futures::future::Fuse::terminated();
|
||||
let source_completion_future = futures::future::Fuse::terminated();
|
||||
let source_go_offline_future = futures::future::Fuse::terminated();
|
||||
let source_tick_stream = interval(source_tick).fuse();
|
||||
|
||||
let mut target_retry_backoff = retry_backoff();
|
||||
let mut target_client_is_online = false;
|
||||
let mut target_best_block_required = false;
|
||||
let mut target_incomplete_headers_required = true;
|
||||
let target_best_block_future = target_client.best_header_id().fuse();
|
||||
let target_incomplete_headers_future = futures::future::Fuse::terminated();
|
||||
let target_extra_check_future = futures::future::Fuse::terminated();
|
||||
let target_existence_status_future = futures::future::Fuse::terminated();
|
||||
let target_submit_header_future = futures::future::Fuse::terminated();
|
||||
let target_complete_header_future = futures::future::Fuse::terminated();
|
||||
let target_go_offline_future = futures::future::Fuse::terminated();
|
||||
let target_tick_stream = interval(target_tick).fuse();
|
||||
|
||||
let mut maintain_required = false;
|
||||
let maintain_stream = interval(MAINTAIN_INTERVAL).fuse();
|
||||
|
||||
let exit_signal = exit_signal.fuse();
|
||||
|
||||
futures::pin_mut!(
|
||||
source_best_block_number_future,
|
||||
source_new_header_future,
|
||||
source_orphan_header_future,
|
||||
source_extra_future,
|
||||
source_completion_future,
|
||||
source_go_offline_future,
|
||||
source_tick_stream,
|
||||
target_best_block_future,
|
||||
target_incomplete_headers_future,
|
||||
target_extra_check_future,
|
||||
target_existence_status_future,
|
||||
target_submit_header_future,
|
||||
target_complete_header_future,
|
||||
target_go_offline_future,
|
||||
target_tick_stream,
|
||||
maintain_stream,
|
||||
exit_signal
|
||||
);
|
||||
|
||||
loop {
|
||||
futures::select! {
|
||||
source_best_block_number = source_best_block_number_future => {
|
||||
source_best_block_number_required = false;
|
||||
|
||||
source_client_is_online = process_future_result(
|
||||
source_best_block_number,
|
||||
&mut source_retry_backoff,
|
||||
|source_best_block_number| sync.source_best_header_number_response(source_best_block_number),
|
||||
&mut source_go_offline_future,
|
||||
async_std::task::sleep,
|
||||
|| format!("Error retrieving best header number from {}", P::SOURCE_NAME),
|
||||
).fail_if_connection_error(FailedClient::Source)?;
|
||||
},
|
||||
source_new_header = source_new_header_future => {
|
||||
source_client_is_online = process_future_result(
|
||||
source_new_header,
|
||||
&mut source_retry_backoff,
|
||||
|source_new_header| sync.headers_mut().header_response(source_new_header),
|
||||
&mut source_go_offline_future,
|
||||
async_std::task::sleep,
|
||||
|| format!("Error retrieving header from {} node", P::SOURCE_NAME),
|
||||
).fail_if_connection_error(FailedClient::Source)?;
|
||||
},
|
||||
source_orphan_header = source_orphan_header_future => {
|
||||
source_client_is_online = process_future_result(
|
||||
source_orphan_header,
|
||||
&mut source_retry_backoff,
|
||||
|source_orphan_header| sync.headers_mut().header_response(source_orphan_header),
|
||||
&mut source_go_offline_future,
|
||||
async_std::task::sleep,
|
||||
|| format!("Error retrieving orphan header from {} node", P::SOURCE_NAME),
|
||||
).fail_if_connection_error(FailedClient::Source)?;
|
||||
},
|
||||
source_extra = source_extra_future => {
|
||||
source_client_is_online = process_future_result(
|
||||
source_extra,
|
||||
&mut source_retry_backoff,
|
||||
|(header, extra)| sync.headers_mut().extra_response(&header, extra),
|
||||
&mut source_go_offline_future,
|
||||
async_std::task::sleep,
|
||||
|| format!("Error retrieving extra data from {} node", P::SOURCE_NAME),
|
||||
).fail_if_connection_error(FailedClient::Source)?;
|
||||
},
|
||||
source_completion = source_completion_future => {
|
||||
source_client_is_online = process_future_result(
|
||||
source_completion,
|
||||
&mut source_retry_backoff,
|
||||
|(header, completion)| sync.headers_mut().completion_response(&header, completion),
|
||||
&mut source_go_offline_future,
|
||||
async_std::task::sleep,
|
||||
|| format!("Error retrieving completion data from {} node", P::SOURCE_NAME),
|
||||
).fail_if_connection_error(FailedClient::Source)?;
|
||||
},
|
||||
_ = source_go_offline_future => {
|
||||
source_client_is_online = true;
|
||||
},
|
||||
_ = source_tick_stream.next() => {
|
||||
if sync.is_almost_synced() {
|
||||
source_best_block_number_required = true;
|
||||
}
|
||||
},
|
||||
target_best_block = target_best_block_future => {
|
||||
target_best_block_required = false;
|
||||
|
||||
target_client_is_online = process_future_result(
|
||||
target_best_block,
|
||||
&mut target_retry_backoff,
|
||||
|target_best_block| {
|
||||
let head_updated = sync.target_best_header_response(target_best_block);
|
||||
if head_updated {
|
||||
last_update_time = Instant::now();
|
||||
}
|
||||
match head_updated {
|
||||
// IF head is updated AND there are still our transactions:
|
||||
// => restart stall countdown timer
|
||||
true if sync.headers().headers_in_status(HeaderStatus::Submitted) != 0 =>
|
||||
stall_countdown = Some(Instant::now()),
|
||||
// IF head is updated AND there are no our transactions:
|
||||
// => stop stall countdown timer
|
||||
true => stall_countdown = None,
|
||||
// IF head is not updated AND stall countdown is not yet completed
|
||||
// => do nothing
|
||||
false if stall_countdown
|
||||
.map(|stall_countdown| stall_countdown.elapsed() < STALL_SYNC_TIMEOUT)
|
||||
.unwrap_or(true)
|
||||
=> (),
|
||||
// IF head is not updated AND stall countdown has completed
|
||||
// => restart sync
|
||||
false => {
|
||||
log::info!(
|
||||
target: "bridge",
|
||||
"Sync has stalled. Restarting {} headers synchronization.",
|
||||
P::SOURCE_NAME,
|
||||
);
|
||||
stall_countdown = None;
|
||||
sync.restart();
|
||||
},
|
||||
}
|
||||
},
|
||||
&mut target_go_offline_future,
|
||||
async_std::task::sleep,
|
||||
|| format!("Error retrieving best known {} header from {} node", P::SOURCE_NAME, P::TARGET_NAME),
|
||||
).fail_if_connection_error(FailedClient::Target)?;
|
||||
},
|
||||
incomplete_headers_ids = target_incomplete_headers_future => {
|
||||
target_incomplete_headers_required = false;
|
||||
|
||||
target_client_is_online = process_future_result(
|
||||
incomplete_headers_ids,
|
||||
&mut target_retry_backoff,
|
||||
|incomplete_headers_ids| sync.headers_mut().incomplete_headers_response(incomplete_headers_ids),
|
||||
&mut target_go_offline_future,
|
||||
async_std::task::sleep,
|
||||
|| format!("Error retrieving incomplete headers from {} node", P::TARGET_NAME),
|
||||
).fail_if_connection_error(FailedClient::Target)?;
|
||||
},
|
||||
target_existence_status = target_existence_status_future => {
|
||||
target_client_is_online = process_future_result(
|
||||
target_existence_status,
|
||||
&mut target_retry_backoff,
|
||||
|(target_header, target_existence_status)| sync
|
||||
.headers_mut()
|
||||
.maybe_orphan_response(&target_header, target_existence_status),
|
||||
&mut target_go_offline_future,
|
||||
async_std::task::sleep,
|
||||
|| format!("Error retrieving existence status from {} node", P::TARGET_NAME),
|
||||
).fail_if_connection_error(FailedClient::Target)?;
|
||||
},
|
||||
submitted_headers = target_submit_header_future => {
|
||||
// following line helps Rust understand the type of `submitted_headers` :/
|
||||
let submitted_headers: SubmittedHeaders<HeaderIdOf<P>, TC::Error> = submitted_headers;
|
||||
let submitted_headers_str = format!("{}", submitted_headers);
|
||||
let all_headers_rejected = submitted_headers.submitted.is_empty()
|
||||
&& submitted_headers.incomplete.is_empty();
|
||||
let has_submitted_headers = sync.headers().headers_in_status(HeaderStatus::Submitted) != 0;
|
||||
|
||||
let maybe_fatal_error = match submitted_headers.fatal_error {
|
||||
Some(fatal_error) => Err(StringifiedMaybeConnectionError::new(
|
||||
fatal_error.is_connection_error(),
|
||||
format!("{:?}", fatal_error),
|
||||
)),
|
||||
None if all_headers_rejected && !has_submitted_headers =>
|
||||
Err(StringifiedMaybeConnectionError::new(false, "All headers were rejected".into())),
|
||||
None => Ok(()),
|
||||
};
|
||||
|
||||
let no_fatal_error = maybe_fatal_error.is_ok();
|
||||
target_client_is_online = process_future_result(
|
||||
maybe_fatal_error,
|
||||
&mut target_retry_backoff,
|
||||
|_| {},
|
||||
&mut target_go_offline_future,
|
||||
async_std::task::sleep,
|
||||
|| format!("Error submitting headers to {} node", P::TARGET_NAME),
|
||||
).fail_if_connection_error(FailedClient::Target)?;
|
||||
|
||||
log::debug!(target: "bridge", "Header submit result: {}", submitted_headers_str);
|
||||
|
||||
sync.headers_mut().headers_submitted(submitted_headers.submitted);
|
||||
sync.headers_mut().add_incomplete_headers(false, submitted_headers.incomplete);
|
||||
|
||||
// when there's no fatal error, but node has rejected all our headers we may
|
||||
// want to pause until our submitted headers will be accepted
|
||||
if no_fatal_error && all_headers_rejected && has_submitted_headers {
|
||||
sync.pause_submit();
|
||||
}
|
||||
},
|
||||
target_complete_header_result = target_complete_header_future => {
|
||||
target_client_is_online = process_future_result(
|
||||
target_complete_header_result,
|
||||
&mut target_retry_backoff,
|
||||
|completed_header| sync.headers_mut().header_completed(&completed_header),
|
||||
&mut target_go_offline_future,
|
||||
async_std::task::sleep,
|
||||
|| format!("Error completing headers at {}", P::TARGET_NAME),
|
||||
).fail_if_connection_error(FailedClient::Target)?;
|
||||
},
|
||||
target_extra_check_result = target_extra_check_future => {
|
||||
target_client_is_online = process_future_result(
|
||||
target_extra_check_result,
|
||||
&mut target_retry_backoff,
|
||||
|(header, extra_check_result)| sync
|
||||
.headers_mut()
|
||||
.maybe_extra_response(&header, extra_check_result),
|
||||
&mut target_go_offline_future,
|
||||
async_std::task::sleep,
|
||||
|| format!("Error retrieving receipts requirement from {} node", P::TARGET_NAME),
|
||||
).fail_if_connection_error(FailedClient::Target)?;
|
||||
},
|
||||
_ = target_go_offline_future => {
|
||||
target_client_is_online = true;
|
||||
},
|
||||
_ = target_tick_stream.next() => {
|
||||
target_best_block_required = true;
|
||||
target_incomplete_headers_required = true;
|
||||
},
|
||||
|
||||
_ = maintain_stream.next() => {
|
||||
maintain_required = true;
|
||||
},
|
||||
_ = exit_signal => {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
// update metrics
|
||||
if let Some(ref metrics_global) = metrics_global {
|
||||
metrics_global.update().await;
|
||||
}
|
||||
if let Some(ref metrics_sync) = metrics_sync {
|
||||
metrics_sync.update(&sync);
|
||||
}
|
||||
|
||||
// print progress
|
||||
progress_context = print_sync_progress(progress_context, &sync);
|
||||
|
||||
// run maintain procedures
|
||||
if maintain_required && source_client_is_online && target_client_is_online {
|
||||
log::debug!(target: "bridge", "Maintaining headers sync loop");
|
||||
maintain_required = false;
|
||||
sync_maintain.maintain(&mut sync).await;
|
||||
}
|
||||
|
||||
// If the target client is accepting requests we update the requests that
|
||||
// we want it to run
|
||||
if !maintain_required && target_client_is_online {
|
||||
// NOTE: Is is important to reset this so that we only have one
|
||||
// request being processed by the client at a time. This prevents
|
||||
// race conditions like receiving two transactions with the same
|
||||
// nonce from the client.
|
||||
target_client_is_online = false;
|
||||
|
||||
// The following is how we prioritize requests:
|
||||
//
|
||||
// 1. Get best block
|
||||
// - Stops us from downloading or submitting new blocks
|
||||
// - Only called rarely
|
||||
//
|
||||
// 2. Get incomplete headers
|
||||
// - Stops us from submitting new blocks
|
||||
// - Only called rarely
|
||||
//
|
||||
// 3. Get complete headers
|
||||
// - Stops us from submitting new blocks
|
||||
//
|
||||
// 4. Check if we need extra data from source
|
||||
// - Stops us from downloading or submitting new blocks
|
||||
//
|
||||
// 5. Check existence of header
|
||||
// - Stops us from submitting new blocks
|
||||
//
|
||||
// 6. Submit header
|
||||
|
||||
if target_best_block_required {
|
||||
log::debug!(target: "bridge", "Asking {} about best block", P::TARGET_NAME);
|
||||
target_best_block_future.set(target_client.best_header_id().fuse());
|
||||
} else if target_incomplete_headers_required {
|
||||
log::debug!(target: "bridge", "Asking {} about incomplete headers", P::TARGET_NAME);
|
||||
target_incomplete_headers_future.set(target_client.incomplete_headers_ids().fuse());
|
||||
} else if let Some((id, completion)) = sync.headers_mut().header_to_complete() {
|
||||
log::debug!(
|
||||
target: "bridge",
|
||||
"Going to complete header: {:?}",
|
||||
id,
|
||||
);
|
||||
|
||||
target_complete_header_future.set(target_client.complete_header(id, completion.clone()).fuse());
|
||||
} else if let Some(header) = sync.headers().header(HeaderStatus::MaybeExtra) {
|
||||
log::debug!(
|
||||
target: "bridge",
|
||||
"Checking if header submission requires extra: {:?}",
|
||||
header.id(),
|
||||
);
|
||||
|
||||
target_extra_check_future.set(target_client.requires_extra(header.clone()).fuse());
|
||||
} else if let Some(header) = sync.headers().header(HeaderStatus::MaybeOrphan) {
|
||||
// for MaybeOrphan we actually ask for parent' header existence
|
||||
let parent_id = header.parent_id();
|
||||
|
||||
log::debug!(
|
||||
target: "bridge",
|
||||
"Asking {} node for existence of: {:?}",
|
||||
P::TARGET_NAME,
|
||||
parent_id,
|
||||
);
|
||||
|
||||
target_existence_status_future.set(target_client.is_known_header(parent_id).fuse());
|
||||
} else if let Some(headers) =
|
||||
sync.select_headers_to_submit(last_update_time.elapsed() > BACKUP_STALL_SYNC_TIMEOUT)
|
||||
{
|
||||
log::debug!(
|
||||
target: "bridge",
|
||||
"Submitting {} header(s) to {} node: {:?}",
|
||||
headers.len(),
|
||||
P::TARGET_NAME,
|
||||
format_ids(headers.iter().map(|header| header.id())),
|
||||
);
|
||||
|
||||
let headers = headers.into_iter().cloned().collect();
|
||||
target_submit_header_future.set(target_client.submit_headers(headers).fuse());
|
||||
|
||||
// remember that we have submitted some headers
|
||||
if stall_countdown.is_none() {
|
||||
stall_countdown = Some(Instant::now());
|
||||
}
|
||||
} else {
|
||||
target_client_is_online = true;
|
||||
}
|
||||
}
|
||||
|
||||
// If the source client is accepting requests we update the requests that
|
||||
// we want it to run
|
||||
if !maintain_required && source_client_is_online {
|
||||
// NOTE: Is is important to reset this so that we only have one
|
||||
// request being processed by the client at a time. This prevents
|
||||
// race conditions like receiving two transactions with the same
|
||||
// nonce from the client.
|
||||
source_client_is_online = false;
|
||||
|
||||
// The following is how we prioritize requests:
|
||||
//
|
||||
// 1. Get best block
|
||||
// - Stops us from downloading or submitting new blocks
|
||||
// - Only called rarely
|
||||
//
|
||||
// 2. Download completion data
|
||||
// - Stops us from submitting new blocks
|
||||
//
|
||||
// 3. Download extra data
|
||||
// - Stops us from submitting new blocks
|
||||
//
|
||||
// 4. Download missing headers
|
||||
// - Stops us from downloading or submitting new blocks
|
||||
//
|
||||
// 5. Downloading new headers
|
||||
|
||||
if source_best_block_number_required {
|
||||
log::debug!(target: "bridge", "Asking {} node about best block number", P::SOURCE_NAME);
|
||||
source_best_block_number_future.set(source_client.best_block_number().fuse());
|
||||
} else if let Some(id) = sync.headers_mut().incomplete_header() {
|
||||
log::debug!(
|
||||
target: "bridge",
|
||||
"Retrieving completion data for header: {:?}",
|
||||
id,
|
||||
);
|
||||
source_completion_future.set(source_client.header_completion(id).fuse());
|
||||
} else if let Some(header) = sync.headers().header(HeaderStatus::Extra) {
|
||||
let id = header.id();
|
||||
log::debug!(
|
||||
target: "bridge",
|
||||
"Retrieving extra data for header: {:?}",
|
||||
id,
|
||||
);
|
||||
source_extra_future.set(source_client.header_extra(id, header.clone()).fuse());
|
||||
} else if let Some(header) = sync.select_orphan_header_to_download() {
|
||||
// for Orphan we actually ask for parent' header
|
||||
let parent_id = header.parent_id();
|
||||
|
||||
// if we have end up with orphan header#0, then we are misconfigured
|
||||
if parent_id.0.is_zero() {
|
||||
log::error!(
|
||||
target: "bridge",
|
||||
"Misconfiguration. Genesis {} header is considered orphan by {} node",
|
||||
P::SOURCE_NAME,
|
||||
P::TARGET_NAME,
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
log::debug!(
|
||||
target: "bridge",
|
||||
"Going to download orphan header from {} node: {:?}",
|
||||
P::SOURCE_NAME,
|
||||
parent_id,
|
||||
);
|
||||
|
||||
source_orphan_header_future.set(source_client.header_by_hash(parent_id.1).fuse());
|
||||
} else if let Some(id) = sync.select_new_header_to_download() {
|
||||
log::debug!(
|
||||
target: "bridge",
|
||||
"Going to download new header from {} node: {:?}",
|
||||
P::SOURCE_NAME,
|
||||
id,
|
||||
);
|
||||
|
||||
source_new_header_future.set(source_client.header_by_number(id).fuse());
|
||||
} else {
|
||||
source_client_is_online = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Print synchronization progress.
|
||||
fn print_sync_progress<P: HeadersSyncPipeline>(
|
||||
progress_context: (Instant, Option<P::Number>, Option<P::Number>),
|
||||
eth_sync: &HeadersSync<P>,
|
||||
) -> (Instant, Option<P::Number>, Option<P::Number>) {
|
||||
let (prev_time, prev_best_header, prev_target_header) = progress_context;
|
||||
let now_time = Instant::now();
|
||||
let (now_best_header, now_target_header) = eth_sync.status();
|
||||
|
||||
let need_update = now_time - prev_time > Duration::from_secs(10)
|
||||
|| match (prev_best_header, now_best_header) {
|
||||
(Some(prev_best_header), Some(now_best_header)) => {
|
||||
now_best_header.0.saturating_sub(prev_best_header) > 10.into()
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
if !need_update {
|
||||
return (prev_time, prev_best_header, prev_target_header);
|
||||
}
|
||||
|
||||
log::info!(
|
||||
target: "bridge",
|
||||
"Synced {:?} of {:?} headers",
|
||||
now_best_header.map(|id| id.0),
|
||||
now_target_header,
|
||||
);
|
||||
(now_time, now_best_header.clone().map(|id| id.0), *now_target_header)
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Metrics for headers synchronization relay loop.
|
||||
|
||||
use crate::sync::HeadersSync;
|
||||
use crate::sync_types::{HeaderStatus, HeadersSyncPipeline};
|
||||
|
||||
use num_traits::Zero;
|
||||
use relay_utils::metrics::{register, GaugeVec, Metrics, Opts, Registry, U64};
|
||||
|
||||
/// Headers sync metrics.
|
||||
#[derive(Clone)]
|
||||
pub struct SyncLoopMetrics {
|
||||
/// Best syncing headers at "source" and "target" nodes.
|
||||
best_block_numbers: GaugeVec<U64>,
|
||||
/// Number of headers in given states (see `HeaderStatus`).
|
||||
blocks_in_state: GaugeVec<U64>,
|
||||
}
|
||||
|
||||
impl Metrics for SyncLoopMetrics {
|
||||
fn register(&self, registry: &Registry) -> Result<(), String> {
|
||||
register(self.best_block_numbers.clone(), registry).map_err(|e| e.to_string())?;
|
||||
register(self.blocks_in_state.clone(), registry).map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SyncLoopMetrics {
|
||||
fn default() -> Self {
|
||||
SyncLoopMetrics {
|
||||
best_block_numbers: GaugeVec::new(
|
||||
Opts::new("best_block_numbers", "Best block numbers on source and target nodes"),
|
||||
&["node"],
|
||||
)
|
||||
.expect("metric is static and thus valid; qed"),
|
||||
blocks_in_state: GaugeVec::new(
|
||||
Opts::new("blocks_in_state", "Number of blocks in given state"),
|
||||
&["state"],
|
||||
)
|
||||
.expect("metric is static and thus valid; qed"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SyncLoopMetrics {
|
||||
/// Update metrics.
|
||||
pub fn update<P: HeadersSyncPipeline>(&self, sync: &HeadersSync<P>) {
|
||||
let headers = sync.headers();
|
||||
let source_best_number = sync.source_best_number().unwrap_or_else(Zero::zero);
|
||||
let target_best_number = sync.target_best_header().map(|id| id.0).unwrap_or_else(Zero::zero);
|
||||
|
||||
self.best_block_numbers
|
||||
.with_label_values(&["source"])
|
||||
.set(source_best_number.into());
|
||||
self.best_block_numbers
|
||||
.with_label_values(&["target"])
|
||||
.set(target_best_number.into());
|
||||
|
||||
self.blocks_in_state
|
||||
.with_label_values(&["maybe_orphan"])
|
||||
.set(headers.headers_in_status(HeaderStatus::MaybeOrphan) as _);
|
||||
self.blocks_in_state
|
||||
.with_label_values(&["orphan"])
|
||||
.set(headers.headers_in_status(HeaderStatus::Orphan) as _);
|
||||
self.blocks_in_state
|
||||
.with_label_values(&["maybe_extra"])
|
||||
.set(headers.headers_in_status(HeaderStatus::MaybeExtra) as _);
|
||||
self.blocks_in_state
|
||||
.with_label_values(&["extra"])
|
||||
.set(headers.headers_in_status(HeaderStatus::Extra) as _);
|
||||
self.blocks_in_state
|
||||
.with_label_values(&["ready"])
|
||||
.set(headers.headers_in_status(HeaderStatus::Ready) as _);
|
||||
self.blocks_in_state
|
||||
.with_label_values(&["incomplete"])
|
||||
.set(headers.headers_in_status(HeaderStatus::Incomplete) as _);
|
||||
self.blocks_in_state
|
||||
.with_label_values(&["submitted"])
|
||||
.set(headers.headers_in_status(HeaderStatus::Submitted) as _);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,593 @@
|
||||
// Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
#![cfg(test)]
|
||||
|
||||
use crate::sync_loop::{run, SourceClient, TargetClient};
|
||||
use crate::sync_types::{HeadersSyncPipeline, QueuedHeader, SourceHeader, SubmittedHeaders};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use backoff::backoff::Backoff;
|
||||
use futures::{future::FutureExt, stream::StreamExt};
|
||||
use parking_lot::Mutex;
|
||||
use relay_utils::{
|
||||
process_future_result, relay_loop::Client as RelayClient, retry_backoff, HeaderId, MaybeConnectionError,
|
||||
};
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
pub type TestNumber = u64;
|
||||
pub type TestHash = u64;
|
||||
pub type TestHeaderId = HeaderId<TestHash, TestNumber>;
|
||||
pub type TestExtra = u64;
|
||||
pub type TestCompletion = u64;
|
||||
pub type TestQueuedHeader = QueuedHeader<TestHeadersSyncPipeline>;
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq)]
|
||||
pub struct TestHeader {
|
||||
pub hash: TestHash,
|
||||
pub number: TestNumber,
|
||||
pub parent_hash: TestHash,
|
||||
}
|
||||
|
||||
impl SourceHeader<TestHash, TestNumber> for TestHeader {
|
||||
fn id(&self) -> TestHeaderId {
|
||||
HeaderId(self.number, self.hash)
|
||||
}
|
||||
|
||||
fn parent_id(&self) -> TestHeaderId {
|
||||
HeaderId(self.number - 1, self.parent_hash)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct TestError(bool);
|
||||
|
||||
impl MaybeConnectionError for TestError {
|
||||
fn is_connection_error(&self) -> bool {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct TestHeadersSyncPipeline;
|
||||
|
||||
impl HeadersSyncPipeline for TestHeadersSyncPipeline {
|
||||
const SOURCE_NAME: &'static str = "Source";
|
||||
const TARGET_NAME: &'static str = "Target";
|
||||
|
||||
type Hash = TestHash;
|
||||
type Number = TestNumber;
|
||||
type Header = TestHeader;
|
||||
type Extra = TestExtra;
|
||||
type Completion = TestCompletion;
|
||||
|
||||
fn estimate_size(_: &TestQueuedHeader) -> usize {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
enum SourceMethod {
|
||||
BestBlockNumber,
|
||||
HeaderByHash(TestHash),
|
||||
HeaderByNumber(TestNumber),
|
||||
HeaderCompletion(TestHeaderId),
|
||||
HeaderExtra(TestHeaderId, TestQueuedHeader),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Source {
|
||||
data: Arc<Mutex<SourceData>>,
|
||||
on_method_call: Arc<dyn Fn(SourceMethod, &mut SourceData) + Send + Sync>,
|
||||
}
|
||||
|
||||
struct SourceData {
|
||||
best_block_number: Result<TestNumber, TestError>,
|
||||
header_by_hash: HashMap<TestHash, TestHeader>,
|
||||
header_by_number: HashMap<TestNumber, TestHeader>,
|
||||
provides_completion: bool,
|
||||
provides_extra: bool,
|
||||
}
|
||||
|
||||
impl Source {
|
||||
pub fn new(
|
||||
best_block_id: TestHeaderId,
|
||||
headers: Vec<(bool, TestHeader)>,
|
||||
on_method_call: impl Fn(SourceMethod, &mut SourceData) + Send + Sync + 'static,
|
||||
) -> Self {
|
||||
Source {
|
||||
data: Arc::new(Mutex::new(SourceData {
|
||||
best_block_number: Ok(best_block_id.0),
|
||||
header_by_hash: headers
|
||||
.iter()
|
||||
.map(|(_, header)| (header.hash, header.clone()))
|
||||
.collect(),
|
||||
header_by_number: headers
|
||||
.iter()
|
||||
.filter_map(|(is_canonical, header)| {
|
||||
if *is_canonical {
|
||||
Some((header.hash, header.clone()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
provides_completion: true,
|
||||
provides_extra: true,
|
||||
})),
|
||||
on_method_call: Arc::new(on_method_call),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl RelayClient for Source {
|
||||
type Error = TestError;
|
||||
|
||||
async fn reconnect(&mut self) -> Result<(), TestError> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl SourceClient<TestHeadersSyncPipeline> for Source {
|
||||
async fn best_block_number(&self) -> Result<TestNumber, TestError> {
|
||||
let mut data = self.data.lock();
|
||||
(self.on_method_call)(SourceMethod::BestBlockNumber, &mut *data);
|
||||
data.best_block_number.clone()
|
||||
}
|
||||
|
||||
async fn header_by_hash(&self, hash: TestHash) -> Result<TestHeader, TestError> {
|
||||
let mut data = self.data.lock();
|
||||
(self.on_method_call)(SourceMethod::HeaderByHash(hash), &mut *data);
|
||||
data.header_by_hash.get(&hash).cloned().ok_or(TestError(false))
|
||||
}
|
||||
|
||||
async fn header_by_number(&self, number: TestNumber) -> Result<TestHeader, TestError> {
|
||||
let mut data = self.data.lock();
|
||||
(self.on_method_call)(SourceMethod::HeaderByNumber(number), &mut *data);
|
||||
data.header_by_number.get(&number).cloned().ok_or(TestError(false))
|
||||
}
|
||||
|
||||
async fn header_completion(&self, id: TestHeaderId) -> Result<(TestHeaderId, Option<TestCompletion>), TestError> {
|
||||
let mut data = self.data.lock();
|
||||
(self.on_method_call)(SourceMethod::HeaderCompletion(id), &mut *data);
|
||||
if data.provides_completion {
|
||||
Ok((id, Some(test_completion(id))))
|
||||
} else {
|
||||
Ok((id, None))
|
||||
}
|
||||
}
|
||||
|
||||
async fn header_extra(
|
||||
&self,
|
||||
id: TestHeaderId,
|
||||
header: TestQueuedHeader,
|
||||
) -> Result<(TestHeaderId, TestExtra), TestError> {
|
||||
let mut data = self.data.lock();
|
||||
(self.on_method_call)(SourceMethod::HeaderExtra(id, header), &mut *data);
|
||||
if data.provides_extra {
|
||||
Ok((id, test_extra(id)))
|
||||
} else {
|
||||
Err(TestError(false))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum TargetMethod {
|
||||
BestHeaderId,
|
||||
IsKnownHeader(TestHeaderId),
|
||||
SubmitHeaders(Vec<TestQueuedHeader>),
|
||||
IncompleteHeadersIds,
|
||||
CompleteHeader(TestHeaderId, TestCompletion),
|
||||
RequiresExtra(TestQueuedHeader),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Target {
|
||||
data: Arc<Mutex<TargetData>>,
|
||||
on_method_call: Arc<dyn Fn(TargetMethod, &mut TargetData) + Send + Sync>,
|
||||
}
|
||||
|
||||
struct TargetData {
|
||||
best_header_id: Result<TestHeaderId, TestError>,
|
||||
is_known_header_by_hash: HashMap<TestHash, bool>,
|
||||
submitted_headers: HashMap<TestHash, TestQueuedHeader>,
|
||||
submit_headers_result: Option<SubmittedHeaders<TestHeaderId, TestError>>,
|
||||
completed_headers: HashMap<TestHash, TestCompletion>,
|
||||
requires_completion: bool,
|
||||
requires_extra: bool,
|
||||
}
|
||||
|
||||
impl Target {
|
||||
pub fn new(
|
||||
best_header_id: TestHeaderId,
|
||||
headers: Vec<TestHeaderId>,
|
||||
on_method_call: impl Fn(TargetMethod, &mut TargetData) + Send + Sync + 'static,
|
||||
) -> Self {
|
||||
Target {
|
||||
data: Arc::new(Mutex::new(TargetData {
|
||||
best_header_id: Ok(best_header_id),
|
||||
is_known_header_by_hash: headers.iter().map(|header| (header.1, true)).collect(),
|
||||
submitted_headers: HashMap::new(),
|
||||
submit_headers_result: None,
|
||||
completed_headers: HashMap::new(),
|
||||
requires_completion: false,
|
||||
requires_extra: false,
|
||||
})),
|
||||
on_method_call: Arc::new(on_method_call),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl RelayClient for Target {
|
||||
type Error = TestError;
|
||||
|
||||
async fn reconnect(&mut self) -> Result<(), TestError> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl TargetClient<TestHeadersSyncPipeline> for Target {
|
||||
async fn best_header_id(&self) -> Result<TestHeaderId, TestError> {
|
||||
let mut data = self.data.lock();
|
||||
(self.on_method_call)(TargetMethod::BestHeaderId, &mut *data);
|
||||
data.best_header_id.clone()
|
||||
}
|
||||
|
||||
async fn is_known_header(&self, id: TestHeaderId) -> Result<(TestHeaderId, bool), TestError> {
|
||||
let mut data = self.data.lock();
|
||||
(self.on_method_call)(TargetMethod::IsKnownHeader(id), &mut *data);
|
||||
data.is_known_header_by_hash
|
||||
.get(&id.1)
|
||||
.cloned()
|
||||
.map(|is_known_header| Ok((id, is_known_header)))
|
||||
.unwrap_or(Ok((id, false)))
|
||||
}
|
||||
|
||||
async fn submit_headers(&self, headers: Vec<TestQueuedHeader>) -> SubmittedHeaders<TestHeaderId, TestError> {
|
||||
let mut data = self.data.lock();
|
||||
(self.on_method_call)(TargetMethod::SubmitHeaders(headers.clone()), &mut *data);
|
||||
data.submitted_headers
|
||||
.extend(headers.iter().map(|header| (header.id().1, header.clone())));
|
||||
data.submit_headers_result.take().expect("test must accept headers")
|
||||
}
|
||||
|
||||
async fn incomplete_headers_ids(&self) -> Result<HashSet<TestHeaderId>, TestError> {
|
||||
let mut data = self.data.lock();
|
||||
(self.on_method_call)(TargetMethod::IncompleteHeadersIds, &mut *data);
|
||||
if data.requires_completion {
|
||||
Ok(data
|
||||
.submitted_headers
|
||||
.iter()
|
||||
.filter(|(hash, _)| !data.completed_headers.contains_key(hash))
|
||||
.map(|(_, header)| header.id())
|
||||
.collect())
|
||||
} else {
|
||||
Ok(HashSet::new())
|
||||
}
|
||||
}
|
||||
|
||||
async fn complete_header(&self, id: TestHeaderId, completion: TestCompletion) -> Result<TestHeaderId, TestError> {
|
||||
let mut data = self.data.lock();
|
||||
(self.on_method_call)(TargetMethod::CompleteHeader(id, completion), &mut *data);
|
||||
data.completed_headers.insert(id.1, completion);
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
async fn requires_extra(&self, header: TestQueuedHeader) -> Result<(TestHeaderId, bool), TestError> {
|
||||
let mut data = self.data.lock();
|
||||
(self.on_method_call)(TargetMethod::RequiresExtra(header.clone()), &mut *data);
|
||||
if data.requires_extra {
|
||||
Ok((header.id(), true))
|
||||
} else {
|
||||
Ok((header.id(), false))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn test_tick() -> Duration {
|
||||
// in ideal world that should have been Duration::from_millis(0), because we do not want
|
||||
// to sleep in tests at all, but that could lead to `select! {}` always waking on tick
|
||||
// => not doing actual job
|
||||
Duration::from_millis(10)
|
||||
}
|
||||
|
||||
fn test_id(number: TestNumber) -> TestHeaderId {
|
||||
HeaderId(number, number)
|
||||
}
|
||||
|
||||
fn test_header(number: TestNumber) -> TestHeader {
|
||||
let id = test_id(number);
|
||||
TestHeader {
|
||||
hash: id.1,
|
||||
number: id.0,
|
||||
parent_hash: if number == 0 {
|
||||
TestHash::default()
|
||||
} else {
|
||||
test_id(number - 1).1
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn test_forked_id(number: TestNumber, forked_from: TestNumber) -> TestHeaderId {
|
||||
const FORK_OFFSET: TestNumber = 1000;
|
||||
|
||||
if number == forked_from {
|
||||
HeaderId(number, number)
|
||||
} else {
|
||||
HeaderId(number, FORK_OFFSET + number)
|
||||
}
|
||||
}
|
||||
|
||||
fn test_forked_header(number: TestNumber, forked_from: TestNumber) -> TestHeader {
|
||||
let id = test_forked_id(number, forked_from);
|
||||
TestHeader {
|
||||
hash: id.1,
|
||||
number: id.0,
|
||||
parent_hash: if number == 0 {
|
||||
TestHash::default()
|
||||
} else {
|
||||
test_forked_id(number - 1, forked_from).1
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn test_completion(id: TestHeaderId) -> TestCompletion {
|
||||
id.0
|
||||
}
|
||||
|
||||
fn test_extra(id: TestHeaderId) -> TestExtra {
|
||||
id.0
|
||||
}
|
||||
|
||||
fn source_reject_completion(method: &SourceMethod) {
|
||||
if let SourceMethod::HeaderCompletion(_) = method {
|
||||
unreachable!("HeaderCompletion request is not expected")
|
||||
}
|
||||
}
|
||||
|
||||
fn source_reject_extra(method: &SourceMethod) {
|
||||
if let SourceMethod::HeaderExtra(_, _) = method {
|
||||
unreachable!("HeaderExtra request is not expected")
|
||||
}
|
||||
}
|
||||
|
||||
fn target_accept_all_headers(method: &TargetMethod, data: &mut TargetData, requires_extra: bool) {
|
||||
if let TargetMethod::SubmitHeaders(ref submitted) = method {
|
||||
assert_eq!(submitted.iter().all(|header| header.extra().is_some()), requires_extra,);
|
||||
|
||||
data.submit_headers_result = Some(SubmittedHeaders {
|
||||
submitted: submitted.iter().map(|header| header.id()).collect(),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn target_signal_exit_when_header_submitted(
|
||||
method: &TargetMethod,
|
||||
header_id: TestHeaderId,
|
||||
exit_signal: &futures::channel::mpsc::UnboundedSender<()>,
|
||||
) {
|
||||
if let TargetMethod::SubmitHeaders(ref submitted) = method {
|
||||
if submitted.iter().any(|header| header.id() == header_id) {
|
||||
exit_signal.unbounded_send(()).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn target_signal_exit_when_header_completed(
|
||||
method: &TargetMethod,
|
||||
header_id: TestHeaderId,
|
||||
exit_signal: &futures::channel::mpsc::UnboundedSender<()>,
|
||||
) {
|
||||
if let TargetMethod::CompleteHeader(completed_id, _) = method {
|
||||
if *completed_id == header_id {
|
||||
exit_signal.unbounded_send(()).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run_backoff_test(result: Result<(), TestError>) -> (Duration, Duration) {
|
||||
let mut backoff = retry_backoff();
|
||||
|
||||
// no randomness in tests (otherwise intervals may overlap => asserts are failing)
|
||||
backoff.randomization_factor = 0f64;
|
||||
|
||||
// increase backoff's current interval
|
||||
let interval1 = backoff.next_backoff().unwrap();
|
||||
let interval2 = backoff.next_backoff().unwrap();
|
||||
assert!(interval2 > interval1);
|
||||
|
||||
// successful future result leads to backoff's reset
|
||||
let go_offline_future = futures::future::Fuse::terminated();
|
||||
futures::pin_mut!(go_offline_future);
|
||||
|
||||
process_future_result(
|
||||
result,
|
||||
&mut backoff,
|
||||
|_| {},
|
||||
&mut go_offline_future,
|
||||
async_std::task::sleep,
|
||||
|| "Test error".into(),
|
||||
);
|
||||
|
||||
(interval2, backoff.next_backoff().unwrap())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn process_future_result_resets_backoff_on_success() {
|
||||
let (interval2, interval_after_reset) = run_backoff_test(Ok(()));
|
||||
assert!(interval2 > interval_after_reset);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn process_future_result_resets_backoff_on_connection_error() {
|
||||
let (interval2, interval_after_reset) = run_backoff_test(Err(TestError(true)));
|
||||
assert!(interval2 > interval_after_reset);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn process_future_result_does_not_reset_backoff_on_non_connection_error() {
|
||||
let (interval2, interval_after_reset) = run_backoff_test(Err(TestError(false)));
|
||||
assert!(interval2 < interval_after_reset);
|
||||
}
|
||||
|
||||
struct SyncLoopTestParams {
|
||||
best_source_header: TestHeader,
|
||||
headers_on_source: Vec<(bool, TestHeader)>,
|
||||
best_target_header: TestHeader,
|
||||
headers_on_target: Vec<TestHeader>,
|
||||
target_requires_extra: bool,
|
||||
target_requires_completion: bool,
|
||||
stop_at: TestHeaderId,
|
||||
}
|
||||
|
||||
fn run_sync_loop_test(params: SyncLoopTestParams) {
|
||||
let (exit_sender, exit_receiver) = futures::channel::mpsc::unbounded();
|
||||
let target_requires_extra = params.target_requires_extra;
|
||||
let target_requires_completion = params.target_requires_completion;
|
||||
let stop_at = params.stop_at;
|
||||
let source = Source::new(
|
||||
params.best_source_header.id(),
|
||||
params.headers_on_source,
|
||||
move |method, _| {
|
||||
if !target_requires_extra {
|
||||
source_reject_extra(&method);
|
||||
}
|
||||
if !target_requires_completion {
|
||||
source_reject_completion(&method);
|
||||
}
|
||||
},
|
||||
);
|
||||
let target = Target::new(
|
||||
params.best_target_header.id(),
|
||||
params.headers_on_target.into_iter().map(|header| header.id()).collect(),
|
||||
move |method, data| {
|
||||
target_accept_all_headers(&method, data, target_requires_extra);
|
||||
if target_requires_completion {
|
||||
target_signal_exit_when_header_completed(&method, stop_at, &exit_sender);
|
||||
} else {
|
||||
target_signal_exit_when_header_submitted(&method, stop_at, &exit_sender);
|
||||
}
|
||||
},
|
||||
);
|
||||
target.data.lock().requires_extra = target_requires_extra;
|
||||
target.data.lock().requires_completion = target_requires_completion;
|
||||
|
||||
run(
|
||||
source,
|
||||
test_tick(),
|
||||
target,
|
||||
test_tick(),
|
||||
(),
|
||||
crate::sync::tests::default_sync_params(),
|
||||
None,
|
||||
exit_receiver.into_future().map(|(_, _)| ()),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_loop_is_able_to_synchronize_single_header() {
|
||||
run_sync_loop_test(SyncLoopTestParams {
|
||||
best_source_header: test_header(1),
|
||||
headers_on_source: vec![(true, test_header(1))],
|
||||
best_target_header: test_header(0),
|
||||
headers_on_target: vec![test_header(0)],
|
||||
target_requires_extra: false,
|
||||
target_requires_completion: false,
|
||||
stop_at: test_id(1),
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_loop_is_able_to_synchronize_single_header_with_extra() {
|
||||
run_sync_loop_test(SyncLoopTestParams {
|
||||
best_source_header: test_header(1),
|
||||
headers_on_source: vec![(true, test_header(1))],
|
||||
best_target_header: test_header(0),
|
||||
headers_on_target: vec![test_header(0)],
|
||||
target_requires_extra: true,
|
||||
target_requires_completion: false,
|
||||
stop_at: test_id(1),
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_loop_is_able_to_synchronize_single_header_with_completion() {
|
||||
run_sync_loop_test(SyncLoopTestParams {
|
||||
best_source_header: test_header(1),
|
||||
headers_on_source: vec![(true, test_header(1))],
|
||||
best_target_header: test_header(0),
|
||||
headers_on_target: vec![test_header(0)],
|
||||
target_requires_extra: false,
|
||||
target_requires_completion: true,
|
||||
stop_at: test_id(1),
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_loop_is_able_to_reorganize_from_shorter_fork() {
|
||||
run_sync_loop_test(SyncLoopTestParams {
|
||||
best_source_header: test_header(3),
|
||||
headers_on_source: vec![
|
||||
(true, test_header(1)),
|
||||
(true, test_header(2)),
|
||||
(true, test_header(3)),
|
||||
(false, test_forked_header(1, 0)),
|
||||
(false, test_forked_header(2, 0)),
|
||||
],
|
||||
best_target_header: test_forked_header(2, 0),
|
||||
headers_on_target: vec![test_header(0), test_forked_header(1, 0), test_forked_header(2, 0)],
|
||||
target_requires_extra: false,
|
||||
target_requires_completion: false,
|
||||
stop_at: test_id(3),
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_loop_is_able_to_reorganize_from_longer_fork() {
|
||||
run_sync_loop_test(SyncLoopTestParams {
|
||||
best_source_header: test_header(3),
|
||||
headers_on_source: vec![
|
||||
(true, test_header(1)),
|
||||
(true, test_header(2)),
|
||||
(true, test_header(3)),
|
||||
(false, test_forked_header(1, 0)),
|
||||
(false, test_forked_header(2, 0)),
|
||||
(false, test_forked_header(3, 0)),
|
||||
(false, test_forked_header(4, 0)),
|
||||
(false, test_forked_header(5, 0)),
|
||||
],
|
||||
best_target_header: test_forked_header(5, 0),
|
||||
headers_on_target: vec![
|
||||
test_header(0),
|
||||
test_forked_header(1, 0),
|
||||
test_forked_header(2, 0),
|
||||
test_forked_header(3, 0),
|
||||
test_forked_header(4, 0),
|
||||
test_forked_header(5, 0),
|
||||
],
|
||||
target_requires_extra: false,
|
||||
target_requires_completion: false,
|
||||
stop_at: test_id(3),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
// Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Types that are used by headers synchronization components.
|
||||
|
||||
use relay_utils::{format_ids, HeaderId};
|
||||
use std::{ops::Deref, sync::Arc};
|
||||
|
||||
/// Ethereum header synchronization status.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum HeaderStatus {
|
||||
/// Header is unknown.
|
||||
Unknown,
|
||||
/// Header is in MaybeOrphan queue.
|
||||
MaybeOrphan,
|
||||
/// Header is in Orphan queue.
|
||||
Orphan,
|
||||
/// Header is in MaybeExtra queue.
|
||||
MaybeExtra,
|
||||
/// Header is in Extra queue.
|
||||
Extra,
|
||||
/// Header is in Ready queue.
|
||||
Ready,
|
||||
/// Header is in Incomplete queue.
|
||||
Incomplete,
|
||||
/// Header has been recently submitted to the target node.
|
||||
Submitted,
|
||||
/// Header is known to the target node.
|
||||
Synced,
|
||||
}
|
||||
|
||||
/// Headers synchronization pipeline.
|
||||
pub trait HeadersSyncPipeline: Clone + Send + Sync {
|
||||
/// Name of the headers source.
|
||||
const SOURCE_NAME: &'static str;
|
||||
/// Name of the headers target.
|
||||
const TARGET_NAME: &'static str;
|
||||
|
||||
/// Headers we're syncing are identified by this hash.
|
||||
type Hash: Eq + Clone + Copy + Send + Sync + std::fmt::Debug + std::fmt::Display + std::hash::Hash;
|
||||
/// Headers we're syncing are identified by this number.
|
||||
type Number: relay_utils::BlockNumberBase;
|
||||
/// Type of header that we're syncing.
|
||||
type Header: SourceHeader<Self::Hash, Self::Number>;
|
||||
/// Type of extra data for the header that we're receiving from the source node:
|
||||
/// 1) extra data is required for some headers;
|
||||
/// 2) target node may answer if it'll require extra data before header is submitted;
|
||||
/// 3) extra data available since the header creation time;
|
||||
/// 4) header and extra data are submitted in single transaction.
|
||||
///
|
||||
/// Example: Ethereum transactions receipts.
|
||||
type Extra: Clone + Send + Sync + PartialEq + std::fmt::Debug;
|
||||
/// Type of data required to 'complete' header that we're receiving from the source node:
|
||||
/// 1) completion data is required for some headers;
|
||||
/// 2) target node can't answer if it'll require completion data before header is accepted;
|
||||
/// 3) completion data may be generated after header generation;
|
||||
/// 4) header and completion data are submitted in separate transactions.
|
||||
///
|
||||
/// Example: Substrate GRANDPA justifications.
|
||||
type Completion: Clone + Send + Sync + std::fmt::Debug;
|
||||
|
||||
/// Function used to estimate size of target-encoded header.
|
||||
fn estimate_size(source: &QueuedHeader<Self>) -> usize;
|
||||
}
|
||||
|
||||
/// A HeaderId for `HeaderSyncPipeline`.
|
||||
pub type HeaderIdOf<P> = HeaderId<<P as HeadersSyncPipeline>::Hash, <P as HeadersSyncPipeline>::Number>;
|
||||
|
||||
/// Header that we're receiving from source node.
|
||||
pub trait SourceHeader<Hash, Number>: Clone + std::fmt::Debug + PartialEq + Send + Sync {
|
||||
/// Returns ID of header.
|
||||
fn id(&self) -> HeaderId<Hash, Number>;
|
||||
/// Returns ID of parent header.
|
||||
///
|
||||
/// Panics if called for genesis header.
|
||||
fn parent_id(&self) -> HeaderId<Hash, Number>;
|
||||
}
|
||||
|
||||
/// Header how it's stored in the synchronization queue.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct QueuedHeader<P: HeadersSyncPipeline>(Arc<QueuedHeaderData<P>>);
|
||||
|
||||
impl<P: HeadersSyncPipeline> QueuedHeader<P> {
|
||||
/// Creates new queued header.
|
||||
pub fn new(header: P::Header) -> Self {
|
||||
QueuedHeader(Arc::new(QueuedHeaderData { header, extra: None }))
|
||||
}
|
||||
|
||||
/// Set associated extra data.
|
||||
pub fn set_extra(self, extra: P::Extra) -> Self {
|
||||
QueuedHeader(Arc::new(QueuedHeaderData {
|
||||
header: Arc::try_unwrap(self.0)
|
||||
.map(|data| data.header)
|
||||
.unwrap_or_else(|data| data.header.clone()),
|
||||
extra: Some(extra),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: HeadersSyncPipeline> Deref for QueuedHeader<P> {
|
||||
type Target = QueuedHeaderData<P>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Header how it's stored in the synchronization queue.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct QueuedHeaderData<P: HeadersSyncPipeline> {
|
||||
header: P::Header,
|
||||
extra: Option<P::Extra>,
|
||||
}
|
||||
|
||||
impl<P: HeadersSyncPipeline> QueuedHeader<P> {
|
||||
/// Returns ID of header.
|
||||
pub fn id(&self) -> HeaderId<P::Hash, P::Number> {
|
||||
self.header.id()
|
||||
}
|
||||
|
||||
/// Returns ID of parent header.
|
||||
pub fn parent_id(&self) -> HeaderId<P::Hash, P::Number> {
|
||||
self.header.parent_id()
|
||||
}
|
||||
|
||||
/// Returns reference to header.
|
||||
pub fn header(&self) -> &P::Header {
|
||||
&self.header
|
||||
}
|
||||
|
||||
/// Returns reference to associated extra data.
|
||||
pub fn extra(&self) -> &Option<P::Extra> {
|
||||
&self.extra
|
||||
}
|
||||
}
|
||||
|
||||
/// Headers submission result.
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
pub struct SubmittedHeaders<Id, Error> {
|
||||
/// IDs of headers that have been submitted to target node.
|
||||
pub submitted: Vec<Id>,
|
||||
/// IDs of incomplete headers. These headers were submitted (so this id is also in `submitted` vec),
|
||||
/// but all descendants are not.
|
||||
pub incomplete: Vec<Id>,
|
||||
/// IDs of ignored headers that we have decided not to submit (they're either rejected by
|
||||
/// target node immediately, or they're descendants of incomplete headers).
|
||||
pub rejected: Vec<Id>,
|
||||
/// Fatal target node error, if it has occured during submission.
|
||||
pub fatal_error: Option<Error>,
|
||||
}
|
||||
|
||||
impl<Id, Error> Default for SubmittedHeaders<Id, Error> {
|
||||
fn default() -> Self {
|
||||
SubmittedHeaders {
|
||||
submitted: Vec::new(),
|
||||
incomplete: Vec::new(),
|
||||
rejected: Vec::new(),
|
||||
fatal_error: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Id: std::fmt::Debug, Error> std::fmt::Display for SubmittedHeaders<Id, Error> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
let submitted = format_ids(self.submitted.iter());
|
||||
let incomplete = format_ids(self.incomplete.iter());
|
||||
let rejected = format_ids(self.rejected.iter());
|
||||
|
||||
write!(
|
||||
f,
|
||||
"Submitted: {}, Incomplete: {}, Rejected: {}",
|
||||
submitted, incomplete, rejected
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "relay-kusama-client"
|
||||
version = "0.1.0"
|
||||
authors = ["Parity Technologies <admin@parity.io>"]
|
||||
edition = "2018"
|
||||
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
|
||||
|
||||
[dependencies]
|
||||
codec = { package = "parity-scale-codec", version = "2.0.0" }
|
||||
headers-relay = { path = "../headers-relay" }
|
||||
relay-substrate-client = { path = "../substrate-client" }
|
||||
relay-utils = { path = "../utils" }
|
||||
|
||||
# Bridge dependencies
|
||||
|
||||
bp-kusama = { path = "../../primitives/kusama" }
|
||||
|
||||
# Substrate Dependencies
|
||||
|
||||
frame-system = { git = "https://github.com/paritytech/substrate.git", branch = "master" }
|
||||
frame-support = { git = "https://github.com/paritytech/substrate.git", branch = "master" }
|
||||
pallet-transaction-payment = { git = "https://github.com/paritytech/substrate.git", branch = "master" }
|
||||
sp-core = { git = "https://github.com/paritytech/substrate.git", branch = "master" }
|
||||
sp-keyring = { git = "https://github.com/paritytech/substrate.git", branch = "master" }
|
||||
sp-runtime = { git = "https://github.com/paritytech/substrate.git", branch = "master" }
|
||||
@@ -0,0 +1,47 @@
|
||||
// Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Types used to connect to the Kusama chain.
|
||||
|
||||
use relay_substrate_client::{Chain, ChainBase};
|
||||
use std::time::Duration;
|
||||
|
||||
/// Kusama header id.
|
||||
pub type HeaderId = relay_utils::HeaderId<bp_kusama::Hash, bp_kusama::BlockNumber>;
|
||||
|
||||
/// Kusama chain definition
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Kusama;
|
||||
|
||||
impl ChainBase for Kusama {
|
||||
type BlockNumber = bp_kusama::BlockNumber;
|
||||
type Hash = bp_kusama::Hash;
|
||||
type Hasher = bp_kusama::Hasher;
|
||||
type Header = bp_kusama::Header;
|
||||
}
|
||||
|
||||
impl Chain for Kusama {
|
||||
const NAME: &'static str = "Kusama";
|
||||
const AVERAGE_BLOCK_INTERVAL: Duration = Duration::from_secs(6);
|
||||
|
||||
type AccountId = bp_kusama::AccountId;
|
||||
type Index = bp_kusama::Nonce;
|
||||
type SignedBlock = bp_kusama::SignedBlock;
|
||||
type Call = ();
|
||||
}
|
||||
|
||||
/// Kusama header type used in headers sync.
|
||||
pub type SyncHeader = relay_substrate_client::SyncHeader<bp_kusama::Header>;
|
||||
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "messages-relay"
|
||||
version = "0.1.0"
|
||||
authors = ["Parity Technologies <admin@parity.io>"]
|
||||
edition = "2018"
|
||||
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
|
||||
|
||||
[dependencies]
|
||||
async-std = "1.6.5"
|
||||
async-trait = "0.1.40"
|
||||
futures = "0.3.5"
|
||||
hex = "0.4"
|
||||
log = "0.4.11"
|
||||
parking_lot = "0.11.0"
|
||||
|
||||
# Bridge Dependencies
|
||||
|
||||
bp-message-lane = { path = "../../primitives/message-lane" }
|
||||
relay-utils = { path = "../utils" }
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Relaying [`message-lane`](../pallet_message_lane/index.html) application specific
|
||||
//! data. Message lane allows sending arbitrary messages between bridged chains. This
|
||||
//! module provides entrypoint that starts reading messages from given message lane
|
||||
//! of source chain and submits proof-of-message-at-source-chain transactions to the
|
||||
//! target chain. Additionaly, proofs-of-messages-delivery are sent back from the
|
||||
//! target chain to the source chain.
|
||||
|
||||
// required for futures::select!
|
||||
#![recursion_limit = "1024"]
|
||||
#![warn(missing_docs)]
|
||||
|
||||
mod metrics;
|
||||
|
||||
pub mod message_lane;
|
||||
pub mod message_lane_loop;
|
||||
|
||||
mod message_race_delivery;
|
||||
mod message_race_loop;
|
||||
mod message_race_receiving;
|
||||
mod message_race_strategy;
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! One-way message lane types. Within single one-way lane we have three 'races' where we try to:
|
||||
//!
|
||||
//! 1) relay new messages from source to target node;
|
||||
//! 2) relay proof-of-delivery from target to source node.
|
||||
|
||||
use relay_utils::{BlockNumberBase, HeaderId};
|
||||
use std::fmt::Debug;
|
||||
|
||||
/// One-way message lane.
|
||||
pub trait MessageLane: Clone + Send + Sync {
|
||||
/// Name of the messages source.
|
||||
const SOURCE_NAME: &'static str;
|
||||
/// Name of the messages target.
|
||||
const TARGET_NAME: &'static str;
|
||||
|
||||
/// Messages proof.
|
||||
type MessagesProof: Clone + Debug + Send + Sync;
|
||||
/// Messages receiving proof.
|
||||
type MessagesReceivingProof: Clone + Debug + Send + Sync;
|
||||
|
||||
/// Number of the source header.
|
||||
type SourceHeaderNumber: BlockNumberBase;
|
||||
/// Hash of the source header.
|
||||
type SourceHeaderHash: Clone + Debug + Default + PartialEq + Send + Sync;
|
||||
|
||||
/// Number of the target header.
|
||||
type TargetHeaderNumber: BlockNumberBase;
|
||||
/// Hash of the target header.
|
||||
type TargetHeaderHash: Clone + Debug + Default + PartialEq + Send + Sync;
|
||||
}
|
||||
|
||||
/// Source header id within given one-way message lane.
|
||||
pub type SourceHeaderIdOf<P> = HeaderId<<P as MessageLane>::SourceHeaderHash, <P as MessageLane>::SourceHeaderNumber>;
|
||||
|
||||
/// Target header id within given one-way message lane.
|
||||
pub type TargetHeaderIdOf<P> = HeaderId<<P as MessageLane>::TargetHeaderHash, <P as MessageLane>::TargetHeaderNumber>;
|
||||
@@ -0,0 +1,841 @@
|
||||
// Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Message delivery loop. Designed to work with message-lane pallet.
|
||||
//!
|
||||
//! Single relay instance delivers messages of single lane in single direction.
|
||||
//! To serve two-way lane, you would need two instances of relay.
|
||||
//! To serve N two-way lanes, you would need N*2 instances of relay.
|
||||
//!
|
||||
//! Please keep in mind that the best header in this file is actually best
|
||||
//! finalized header. I.e. when talking about headers in lane context, we
|
||||
//! only care about finalized headers.
|
||||
|
||||
use crate::message_lane::{MessageLane, SourceHeaderIdOf, TargetHeaderIdOf};
|
||||
use crate::message_race_delivery::run as run_message_delivery_race;
|
||||
use crate::message_race_receiving::run as run_message_receiving_race;
|
||||
use crate::metrics::MessageLaneLoopMetrics;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use bp_message_lane::{LaneId, MessageNonce, UnrewardedRelayersState, Weight};
|
||||
use futures::{channel::mpsc::unbounded, future::FutureExt, stream::StreamExt};
|
||||
use relay_utils::{
|
||||
interval,
|
||||
metrics::{start as metrics_start, GlobalMetrics, MetricsParams},
|
||||
process_future_result,
|
||||
relay_loop::Client as RelayClient,
|
||||
retry_backoff, FailedClient,
|
||||
};
|
||||
use std::{collections::BTreeMap, fmt::Debug, future::Future, ops::RangeInclusive, time::Duration};
|
||||
|
||||
/// Message lane loop configuration params.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Params {
|
||||
/// Id of lane this loop is servicing.
|
||||
pub lane: LaneId,
|
||||
/// Interval at which we ask target node about its updates.
|
||||
pub source_tick: Duration,
|
||||
/// Interval at which we ask target node about its updates.
|
||||
pub target_tick: Duration,
|
||||
/// Delay between moments when connection error happens and our reconnect attempt.
|
||||
pub reconnect_delay: Duration,
|
||||
/// The loop will auto-restart if there has been no updates during this period.
|
||||
pub stall_timeout: Duration,
|
||||
/// Message delivery race parameters.
|
||||
pub delivery_params: MessageDeliveryParams,
|
||||
}
|
||||
|
||||
/// Message delivery race parameters.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MessageDeliveryParams {
|
||||
/// Maximal number of unconfirmed relayer entries at the inbound lane. If there's that number of entries
|
||||
/// in the `InboundLaneData::relayers` set, all new messages will be rejected until reward payment will
|
||||
/// be proved (by including outbound lane state to the message delivery transaction).
|
||||
pub max_unrewarded_relayer_entries_at_target: MessageNonce,
|
||||
/// Message delivery race will stop delivering messages if there are `max_unconfirmed_nonces_at_target`
|
||||
/// unconfirmed nonces on the target node. The race would continue once they're confirmed by the
|
||||
/// receiving race.
|
||||
pub max_unconfirmed_nonces_at_target: MessageNonce,
|
||||
/// Maximal number of relayed messages in single delivery transaction.
|
||||
pub max_messages_in_single_batch: MessageNonce,
|
||||
/// Maximal cumulative dispatch weight of relayed messages in single delivery transaction.
|
||||
pub max_messages_weight_in_single_batch: Weight,
|
||||
/// Maximal cumulative size of relayed messages in single delivery transaction.
|
||||
pub max_messages_size_in_single_batch: usize,
|
||||
}
|
||||
|
||||
/// Message weights.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct MessageWeights {
|
||||
/// Message dispatch weight.
|
||||
pub weight: Weight,
|
||||
/// Message size (number of bytes in encoded payload).
|
||||
pub size: usize,
|
||||
}
|
||||
|
||||
/// Messages weights map.
|
||||
pub type MessageWeightsMap = BTreeMap<MessageNonce, MessageWeights>;
|
||||
|
||||
/// Message delivery race proof parameters.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct MessageProofParameters {
|
||||
/// Include outbound lane state proof?
|
||||
pub outbound_state_proof_required: bool,
|
||||
/// Cumulative dispatch weight of messages that we're building proof for.
|
||||
pub dispatch_weight: Weight,
|
||||
}
|
||||
|
||||
/// Source client trait.
|
||||
#[async_trait]
|
||||
pub trait SourceClient<P: MessageLane>: RelayClient {
|
||||
/// Returns state of the client.
|
||||
async fn state(&self) -> Result<SourceClientState<P>, Self::Error>;
|
||||
|
||||
/// Get nonce of instance of latest generated message.
|
||||
async fn latest_generated_nonce(
|
||||
&self,
|
||||
id: SourceHeaderIdOf<P>,
|
||||
) -> Result<(SourceHeaderIdOf<P>, MessageNonce), Self::Error>;
|
||||
/// Get nonce of the latest message, which receiving has been confirmed by the target chain.
|
||||
async fn latest_confirmed_received_nonce(
|
||||
&self,
|
||||
id: SourceHeaderIdOf<P>,
|
||||
) -> Result<(SourceHeaderIdOf<P>, MessageNonce), Self::Error>;
|
||||
|
||||
/// Returns mapping of message nonces, generated on this client, to their weights.
|
||||
///
|
||||
/// Some weights may be missing from returned map, if corresponding messages were pruned at
|
||||
/// the source chain.
|
||||
async fn generated_messages_weights(
|
||||
&self,
|
||||
id: SourceHeaderIdOf<P>,
|
||||
nonces: RangeInclusive<MessageNonce>,
|
||||
) -> Result<MessageWeightsMap, Self::Error>;
|
||||
|
||||
/// Prove messages in inclusive range [begin; end].
|
||||
async fn prove_messages(
|
||||
&self,
|
||||
id: SourceHeaderIdOf<P>,
|
||||
nonces: RangeInclusive<MessageNonce>,
|
||||
proof_parameters: MessageProofParameters,
|
||||
) -> Result<(SourceHeaderIdOf<P>, RangeInclusive<MessageNonce>, P::MessagesProof), Self::Error>;
|
||||
|
||||
/// Submit messages receiving proof.
|
||||
async fn submit_messages_receiving_proof(
|
||||
&self,
|
||||
generated_at_block: TargetHeaderIdOf<P>,
|
||||
proof: P::MessagesReceivingProof,
|
||||
) -> Result<(), Self::Error>;
|
||||
}
|
||||
|
||||
/// Target client trait.
|
||||
#[async_trait]
|
||||
pub trait TargetClient<P: MessageLane>: RelayClient {
|
||||
/// Returns state of the client.
|
||||
async fn state(&self) -> Result<TargetClientState<P>, Self::Error>;
|
||||
|
||||
/// Get nonce of latest received message.
|
||||
async fn latest_received_nonce(
|
||||
&self,
|
||||
id: TargetHeaderIdOf<P>,
|
||||
) -> Result<(TargetHeaderIdOf<P>, MessageNonce), Self::Error>;
|
||||
|
||||
/// Get nonce of latest confirmed message.
|
||||
async fn latest_confirmed_received_nonce(
|
||||
&self,
|
||||
id: TargetHeaderIdOf<P>,
|
||||
) -> Result<(TargetHeaderIdOf<P>, MessageNonce), Self::Error>;
|
||||
/// Get state of unrewarded relayers set at the inbound lane.
|
||||
async fn unrewarded_relayers_state(
|
||||
&self,
|
||||
id: TargetHeaderIdOf<P>,
|
||||
) -> Result<(TargetHeaderIdOf<P>, UnrewardedRelayersState), Self::Error>;
|
||||
|
||||
/// Prove messages receiving at given block.
|
||||
async fn prove_messages_receiving(
|
||||
&self,
|
||||
id: TargetHeaderIdOf<P>,
|
||||
) -> Result<(TargetHeaderIdOf<P>, P::MessagesReceivingProof), Self::Error>;
|
||||
|
||||
/// Submit messages proof.
|
||||
async fn submit_messages_proof(
|
||||
&self,
|
||||
generated_at_header: SourceHeaderIdOf<P>,
|
||||
nonces: RangeInclusive<MessageNonce>,
|
||||
proof: P::MessagesProof,
|
||||
) -> Result<RangeInclusive<MessageNonce>, Self::Error>;
|
||||
}
|
||||
|
||||
/// State of the client.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct ClientState<SelfHeaderId, PeerHeaderId> {
|
||||
/// Best header id of this chain.
|
||||
pub best_self: SelfHeaderId,
|
||||
/// Best finalized header id of this chain.
|
||||
pub best_finalized_self: SelfHeaderId,
|
||||
/// Best finalized header id of the peer chain read at the best block of this chain (at `best_finalized_self`).
|
||||
pub best_finalized_peer_at_best_self: PeerHeaderId,
|
||||
}
|
||||
|
||||
/// State of source client in one-way message lane.
|
||||
pub type SourceClientState<P> = ClientState<SourceHeaderIdOf<P>, TargetHeaderIdOf<P>>;
|
||||
|
||||
/// State of target client in one-way message lane.
|
||||
pub type TargetClientState<P> = ClientState<TargetHeaderIdOf<P>, SourceHeaderIdOf<P>>;
|
||||
|
||||
/// Both clients state.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ClientsState<P: MessageLane> {
|
||||
/// Source client state.
|
||||
pub source: Option<SourceClientState<P>>,
|
||||
/// Target client state.
|
||||
pub target: Option<TargetClientState<P>>,
|
||||
}
|
||||
|
||||
/// Run message lane service loop.
|
||||
pub fn run<P: MessageLane>(
|
||||
params: Params,
|
||||
source_client: impl SourceClient<P>,
|
||||
target_client: impl TargetClient<P>,
|
||||
metrics_params: Option<MetricsParams>,
|
||||
exit_signal: impl Future<Output = ()>,
|
||||
) {
|
||||
let exit_signal = exit_signal.shared();
|
||||
let metrics_global = GlobalMetrics::default();
|
||||
let metrics_msg = MessageLaneLoopMetrics::default();
|
||||
let metrics_enabled = metrics_params.is_some();
|
||||
metrics_start(
|
||||
format!(
|
||||
"{}_to_{}_MessageLane_{}",
|
||||
P::SOURCE_NAME,
|
||||
P::TARGET_NAME,
|
||||
hex::encode(params.lane)
|
||||
),
|
||||
metrics_params,
|
||||
&metrics_global,
|
||||
&metrics_msg,
|
||||
);
|
||||
|
||||
relay_utils::relay_loop::run(
|
||||
params.reconnect_delay,
|
||||
source_client,
|
||||
target_client,
|
||||
|source_client, target_client| {
|
||||
run_until_connection_lost(
|
||||
params.clone(),
|
||||
source_client,
|
||||
target_client,
|
||||
if metrics_enabled {
|
||||
Some(metrics_global.clone())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
if metrics_enabled {
|
||||
Some(metrics_msg.clone())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
exit_signal.clone(),
|
||||
)
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Run one-way message delivery loop until connection with target or source node is lost, or exit signal is received.
|
||||
async fn run_until_connection_lost<P: MessageLane, SC: SourceClient<P>, TC: TargetClient<P>>(
|
||||
params: Params,
|
||||
source_client: SC,
|
||||
target_client: TC,
|
||||
metrics_global: Option<GlobalMetrics>,
|
||||
metrics_msg: Option<MessageLaneLoopMetrics>,
|
||||
exit_signal: impl Future<Output = ()>,
|
||||
) -> Result<(), FailedClient> {
|
||||
let mut source_retry_backoff = retry_backoff();
|
||||
let mut source_client_is_online = false;
|
||||
let mut source_state_required = true;
|
||||
let source_state = source_client.state().fuse();
|
||||
let source_go_offline_future = futures::future::Fuse::terminated();
|
||||
let source_tick_stream = interval(params.source_tick).fuse();
|
||||
|
||||
let mut target_retry_backoff = retry_backoff();
|
||||
let mut target_client_is_online = false;
|
||||
let mut target_state_required = true;
|
||||
let target_state = target_client.state().fuse();
|
||||
let target_go_offline_future = futures::future::Fuse::terminated();
|
||||
let target_tick_stream = interval(params.target_tick).fuse();
|
||||
|
||||
let (
|
||||
(delivery_source_state_sender, delivery_source_state_receiver),
|
||||
(delivery_target_state_sender, delivery_target_state_receiver),
|
||||
) = (unbounded(), unbounded());
|
||||
let delivery_race_loop = run_message_delivery_race(
|
||||
source_client.clone(),
|
||||
delivery_source_state_receiver,
|
||||
target_client.clone(),
|
||||
delivery_target_state_receiver,
|
||||
params.stall_timeout,
|
||||
metrics_msg.clone(),
|
||||
params.delivery_params,
|
||||
)
|
||||
.fuse();
|
||||
|
||||
let (
|
||||
(receiving_source_state_sender, receiving_source_state_receiver),
|
||||
(receiving_target_state_sender, receiving_target_state_receiver),
|
||||
) = (unbounded(), unbounded());
|
||||
let receiving_race_loop = run_message_receiving_race(
|
||||
source_client.clone(),
|
||||
receiving_source_state_receiver,
|
||||
target_client.clone(),
|
||||
receiving_target_state_receiver,
|
||||
params.stall_timeout,
|
||||
metrics_msg.clone(),
|
||||
)
|
||||
.fuse();
|
||||
|
||||
let exit_signal = exit_signal.fuse();
|
||||
|
||||
futures::pin_mut!(
|
||||
source_state,
|
||||
source_go_offline_future,
|
||||
source_tick_stream,
|
||||
target_state,
|
||||
target_go_offline_future,
|
||||
target_tick_stream,
|
||||
delivery_race_loop,
|
||||
receiving_race_loop,
|
||||
exit_signal
|
||||
);
|
||||
|
||||
loop {
|
||||
futures::select! {
|
||||
new_source_state = source_state => {
|
||||
source_state_required = false;
|
||||
|
||||
source_client_is_online = process_future_result(
|
||||
new_source_state,
|
||||
&mut source_retry_backoff,
|
||||
|new_source_state| {
|
||||
log::debug!(
|
||||
target: "bridge",
|
||||
"Received state from {} node: {:?}",
|
||||
P::SOURCE_NAME,
|
||||
new_source_state,
|
||||
);
|
||||
let _ = delivery_source_state_sender.unbounded_send(new_source_state.clone());
|
||||
let _ = receiving_source_state_sender.unbounded_send(new_source_state.clone());
|
||||
|
||||
if let Some(metrics_msg) = metrics_msg.as_ref() {
|
||||
metrics_msg.update_source_state::<P>(new_source_state);
|
||||
}
|
||||
},
|
||||
&mut source_go_offline_future,
|
||||
async_std::task::sleep,
|
||||
|| format!("Error retrieving state from {} node", P::SOURCE_NAME),
|
||||
).fail_if_connection_error(FailedClient::Source)?;
|
||||
},
|
||||
_ = source_go_offline_future => {
|
||||
source_client_is_online = true;
|
||||
},
|
||||
_ = source_tick_stream.next() => {
|
||||
source_state_required = true;
|
||||
},
|
||||
new_target_state = target_state => {
|
||||
target_state_required = false;
|
||||
|
||||
target_client_is_online = process_future_result(
|
||||
new_target_state,
|
||||
&mut target_retry_backoff,
|
||||
|new_target_state| {
|
||||
log::debug!(
|
||||
target: "bridge",
|
||||
"Received state from {} node: {:?}",
|
||||
P::TARGET_NAME,
|
||||
new_target_state,
|
||||
);
|
||||
let _ = delivery_target_state_sender.unbounded_send(new_target_state.clone());
|
||||
let _ = receiving_target_state_sender.unbounded_send(new_target_state.clone());
|
||||
|
||||
if let Some(metrics_msg) = metrics_msg.as_ref() {
|
||||
metrics_msg.update_target_state::<P>(new_target_state);
|
||||
}
|
||||
},
|
||||
&mut target_go_offline_future,
|
||||
async_std::task::sleep,
|
||||
|| format!("Error retrieving state from {} node", P::TARGET_NAME),
|
||||
).fail_if_connection_error(FailedClient::Target)?;
|
||||
},
|
||||
_ = target_go_offline_future => {
|
||||
target_client_is_online = true;
|
||||
},
|
||||
_ = target_tick_stream.next() => {
|
||||
target_state_required = true;
|
||||
},
|
||||
|
||||
delivery_error = delivery_race_loop => {
|
||||
match delivery_error {
|
||||
Ok(_) => unreachable!("only ends with error; qed"),
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
},
|
||||
receiving_error = receiving_race_loop => {
|
||||
match receiving_error {
|
||||
Ok(_) => unreachable!("only ends with error; qed"),
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
},
|
||||
|
||||
() = exit_signal => {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref metrics_global) = metrics_global {
|
||||
metrics_global.update().await;
|
||||
}
|
||||
|
||||
if source_client_is_online && source_state_required {
|
||||
log::debug!(target: "bridge", "Asking {} node about its state", P::SOURCE_NAME);
|
||||
source_state.set(source_client.state().fuse());
|
||||
source_client_is_online = false;
|
||||
}
|
||||
|
||||
if target_client_is_online && target_state_required {
|
||||
log::debug!(target: "bridge", "Asking {} node about its state", P::TARGET_NAME);
|
||||
target_state.set(target_client.state().fuse());
|
||||
target_client_is_online = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
use futures::stream::StreamExt;
|
||||
use parking_lot::Mutex;
|
||||
use relay_utils::{HeaderId, MaybeConnectionError};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub fn header_id(number: TestSourceHeaderNumber) -> TestSourceHeaderId {
|
||||
HeaderId(number, number)
|
||||
}
|
||||
|
||||
pub type TestSourceHeaderId = HeaderId<TestSourceHeaderNumber, TestSourceHeaderHash>;
|
||||
pub type TestTargetHeaderId = HeaderId<TestTargetHeaderNumber, TestTargetHeaderHash>;
|
||||
|
||||
pub type TestMessagesProof = (RangeInclusive<MessageNonce>, Option<MessageNonce>);
|
||||
pub type TestMessagesReceivingProof = MessageNonce;
|
||||
|
||||
pub type TestSourceHeaderNumber = u64;
|
||||
pub type TestSourceHeaderHash = u64;
|
||||
|
||||
pub type TestTargetHeaderNumber = u64;
|
||||
pub type TestTargetHeaderHash = u64;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TestError;
|
||||
|
||||
impl MaybeConnectionError for TestError {
|
||||
fn is_connection_error(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TestMessageLane;
|
||||
|
||||
impl MessageLane for TestMessageLane {
|
||||
const SOURCE_NAME: &'static str = "TestSource";
|
||||
const TARGET_NAME: &'static str = "TestTarget";
|
||||
|
||||
type MessagesProof = TestMessagesProof;
|
||||
type MessagesReceivingProof = TestMessagesReceivingProof;
|
||||
|
||||
type SourceHeaderNumber = TestSourceHeaderNumber;
|
||||
type SourceHeaderHash = TestSourceHeaderHash;
|
||||
|
||||
type TargetHeaderNumber = TestTargetHeaderNumber;
|
||||
type TargetHeaderHash = TestTargetHeaderHash;
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct TestClientData {
|
||||
is_source_fails: bool,
|
||||
is_source_reconnected: bool,
|
||||
source_state: SourceClientState<TestMessageLane>,
|
||||
source_latest_generated_nonce: MessageNonce,
|
||||
source_latest_confirmed_received_nonce: MessageNonce,
|
||||
submitted_messages_receiving_proofs: Vec<TestMessagesReceivingProof>,
|
||||
is_target_fails: bool,
|
||||
is_target_reconnected: bool,
|
||||
target_state: SourceClientState<TestMessageLane>,
|
||||
target_latest_received_nonce: MessageNonce,
|
||||
target_latest_confirmed_received_nonce: MessageNonce,
|
||||
submitted_messages_proofs: Vec<TestMessagesProof>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TestSourceClient {
|
||||
data: Arc<Mutex<TestClientData>>,
|
||||
tick: Arc<dyn Fn(&mut TestClientData) + Send + Sync>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl RelayClient for TestSourceClient {
|
||||
type Error = TestError;
|
||||
|
||||
async fn reconnect(&mut self) -> Result<(), TestError> {
|
||||
{
|
||||
let mut data = self.data.lock();
|
||||
(self.tick)(&mut *data);
|
||||
data.is_source_reconnected = true;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl SourceClient<TestMessageLane> for TestSourceClient {
|
||||
async fn state(&self) -> Result<SourceClientState<TestMessageLane>, TestError> {
|
||||
let mut data = self.data.lock();
|
||||
(self.tick)(&mut *data);
|
||||
if data.is_source_fails {
|
||||
return Err(TestError);
|
||||
}
|
||||
Ok(data.source_state.clone())
|
||||
}
|
||||
|
||||
async fn latest_generated_nonce(
|
||||
&self,
|
||||
id: SourceHeaderIdOf<TestMessageLane>,
|
||||
) -> Result<(SourceHeaderIdOf<TestMessageLane>, MessageNonce), TestError> {
|
||||
let mut data = self.data.lock();
|
||||
(self.tick)(&mut *data);
|
||||
if data.is_source_fails {
|
||||
return Err(TestError);
|
||||
}
|
||||
Ok((id, data.source_latest_generated_nonce))
|
||||
}
|
||||
|
||||
async fn latest_confirmed_received_nonce(
|
||||
&self,
|
||||
id: SourceHeaderIdOf<TestMessageLane>,
|
||||
) -> Result<(SourceHeaderIdOf<TestMessageLane>, MessageNonce), TestError> {
|
||||
let mut data = self.data.lock();
|
||||
(self.tick)(&mut *data);
|
||||
Ok((id, data.source_latest_confirmed_received_nonce))
|
||||
}
|
||||
|
||||
async fn generated_messages_weights(
|
||||
&self,
|
||||
_id: SourceHeaderIdOf<TestMessageLane>,
|
||||
nonces: RangeInclusive<MessageNonce>,
|
||||
) -> Result<MessageWeightsMap, TestError> {
|
||||
Ok(nonces
|
||||
.map(|nonce| (nonce, MessageWeights { weight: 1, size: 1 }))
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn prove_messages(
|
||||
&self,
|
||||
id: SourceHeaderIdOf<TestMessageLane>,
|
||||
nonces: RangeInclusive<MessageNonce>,
|
||||
proof_parameters: MessageProofParameters,
|
||||
) -> Result<
|
||||
(
|
||||
SourceHeaderIdOf<TestMessageLane>,
|
||||
RangeInclusive<MessageNonce>,
|
||||
TestMessagesProof,
|
||||
),
|
||||
TestError,
|
||||
> {
|
||||
let mut data = self.data.lock();
|
||||
(self.tick)(&mut *data);
|
||||
Ok((
|
||||
id,
|
||||
nonces.clone(),
|
||||
(
|
||||
nonces,
|
||||
if proof_parameters.outbound_state_proof_required {
|
||||
Some(data.source_latest_confirmed_received_nonce)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
async fn submit_messages_receiving_proof(
|
||||
&self,
|
||||
_generated_at_block: TargetHeaderIdOf<TestMessageLane>,
|
||||
proof: TestMessagesReceivingProof,
|
||||
) -> Result<(), TestError> {
|
||||
let mut data = self.data.lock();
|
||||
(self.tick)(&mut *data);
|
||||
data.submitted_messages_receiving_proofs.push(proof);
|
||||
data.source_latest_confirmed_received_nonce = proof;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TestTargetClient {
|
||||
data: Arc<Mutex<TestClientData>>,
|
||||
tick: Arc<dyn Fn(&mut TestClientData) + Send + Sync>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl RelayClient for TestTargetClient {
|
||||
type Error = TestError;
|
||||
|
||||
async fn reconnect(&mut self) -> Result<(), TestError> {
|
||||
{
|
||||
let mut data = self.data.lock();
|
||||
(self.tick)(&mut *data);
|
||||
data.is_target_reconnected = true;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl TargetClient<TestMessageLane> for TestTargetClient {
|
||||
async fn state(&self) -> Result<TargetClientState<TestMessageLane>, TestError> {
|
||||
let mut data = self.data.lock();
|
||||
(self.tick)(&mut *data);
|
||||
if data.is_target_fails {
|
||||
return Err(TestError);
|
||||
}
|
||||
Ok(data.target_state.clone())
|
||||
}
|
||||
|
||||
async fn latest_received_nonce(
|
||||
&self,
|
||||
id: TargetHeaderIdOf<TestMessageLane>,
|
||||
) -> Result<(TargetHeaderIdOf<TestMessageLane>, MessageNonce), TestError> {
|
||||
let mut data = self.data.lock();
|
||||
(self.tick)(&mut *data);
|
||||
if data.is_target_fails {
|
||||
return Err(TestError);
|
||||
}
|
||||
Ok((id, data.target_latest_received_nonce))
|
||||
}
|
||||
|
||||
async fn unrewarded_relayers_state(
|
||||
&self,
|
||||
id: TargetHeaderIdOf<TestMessageLane>,
|
||||
) -> Result<(TargetHeaderIdOf<TestMessageLane>, UnrewardedRelayersState), TestError> {
|
||||
Ok((
|
||||
id,
|
||||
UnrewardedRelayersState {
|
||||
unrewarded_relayer_entries: 0,
|
||||
messages_in_oldest_entry: 0,
|
||||
total_messages: 0,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
async fn latest_confirmed_received_nonce(
|
||||
&self,
|
||||
id: TargetHeaderIdOf<TestMessageLane>,
|
||||
) -> Result<(TargetHeaderIdOf<TestMessageLane>, MessageNonce), TestError> {
|
||||
let mut data = self.data.lock();
|
||||
(self.tick)(&mut *data);
|
||||
if data.is_target_fails {
|
||||
return Err(TestError);
|
||||
}
|
||||
Ok((id, data.target_latest_confirmed_received_nonce))
|
||||
}
|
||||
|
||||
async fn prove_messages_receiving(
|
||||
&self,
|
||||
id: TargetHeaderIdOf<TestMessageLane>,
|
||||
) -> Result<(TargetHeaderIdOf<TestMessageLane>, TestMessagesReceivingProof), TestError> {
|
||||
Ok((id, self.data.lock().target_latest_received_nonce))
|
||||
}
|
||||
|
||||
async fn submit_messages_proof(
|
||||
&self,
|
||||
_generated_at_header: SourceHeaderIdOf<TestMessageLane>,
|
||||
nonces: RangeInclusive<MessageNonce>,
|
||||
proof: TestMessagesProof,
|
||||
) -> Result<RangeInclusive<MessageNonce>, TestError> {
|
||||
let mut data = self.data.lock();
|
||||
(self.tick)(&mut *data);
|
||||
if data.is_target_fails {
|
||||
return Err(TestError);
|
||||
}
|
||||
data.target_state.best_self =
|
||||
HeaderId(data.target_state.best_self.0 + 1, data.target_state.best_self.1 + 1);
|
||||
data.target_latest_received_nonce = *proof.0.end();
|
||||
if let Some(target_latest_confirmed_received_nonce) = proof.1 {
|
||||
data.target_latest_confirmed_received_nonce = target_latest_confirmed_received_nonce;
|
||||
}
|
||||
data.submitted_messages_proofs.push(proof);
|
||||
Ok(nonces)
|
||||
}
|
||||
}
|
||||
|
||||
fn run_loop_test(
|
||||
data: TestClientData,
|
||||
source_tick: Arc<dyn Fn(&mut TestClientData) + Send + Sync>,
|
||||
target_tick: Arc<dyn Fn(&mut TestClientData) + Send + Sync>,
|
||||
exit_signal: impl Future<Output = ()>,
|
||||
) -> TestClientData {
|
||||
async_std::task::block_on(async {
|
||||
let data = Arc::new(Mutex::new(data));
|
||||
|
||||
let source_client = TestSourceClient {
|
||||
data: data.clone(),
|
||||
tick: source_tick,
|
||||
};
|
||||
let target_client = TestTargetClient {
|
||||
data: data.clone(),
|
||||
tick: target_tick,
|
||||
};
|
||||
run(
|
||||
Params {
|
||||
lane: [0, 0, 0, 0],
|
||||
source_tick: Duration::from_millis(100),
|
||||
target_tick: Duration::from_millis(100),
|
||||
reconnect_delay: Duration::from_millis(0),
|
||||
stall_timeout: Duration::from_millis(60 * 1000),
|
||||
delivery_params: MessageDeliveryParams {
|
||||
max_unrewarded_relayer_entries_at_target: 4,
|
||||
max_unconfirmed_nonces_at_target: 4,
|
||||
max_messages_in_single_batch: 4,
|
||||
max_messages_weight_in_single_batch: 4,
|
||||
max_messages_size_in_single_batch: 4,
|
||||
},
|
||||
},
|
||||
source_client,
|
||||
target_client,
|
||||
None,
|
||||
exit_signal,
|
||||
);
|
||||
let result = data.lock().clone();
|
||||
result
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn message_lane_loop_is_able_to_recover_from_connection_errors() {
|
||||
// with this configuration, source client will return Err, making source client
|
||||
// reconnect. Then the target client will fail with Err + reconnect. Then we finally
|
||||
// able to deliver messages.
|
||||
let (exit_sender, exit_receiver) = unbounded();
|
||||
let result = run_loop_test(
|
||||
TestClientData {
|
||||
is_source_fails: true,
|
||||
source_state: ClientState {
|
||||
best_self: HeaderId(0, 0),
|
||||
best_finalized_self: HeaderId(0, 0),
|
||||
best_finalized_peer_at_best_self: HeaderId(0, 0),
|
||||
},
|
||||
source_latest_generated_nonce: 1,
|
||||
target_state: ClientState {
|
||||
best_self: HeaderId(0, 0),
|
||||
best_finalized_self: HeaderId(0, 0),
|
||||
best_finalized_peer_at_best_self: HeaderId(0, 0),
|
||||
},
|
||||
target_latest_received_nonce: 0,
|
||||
..Default::default()
|
||||
},
|
||||
Arc::new(|data: &mut TestClientData| {
|
||||
if data.is_source_reconnected {
|
||||
data.is_source_fails = false;
|
||||
data.is_target_fails = true;
|
||||
}
|
||||
}),
|
||||
Arc::new(move |data: &mut TestClientData| {
|
||||
if data.is_target_reconnected {
|
||||
data.is_target_fails = false;
|
||||
}
|
||||
if data.target_state.best_finalized_peer_at_best_self.0 < 10 {
|
||||
data.target_state.best_finalized_peer_at_best_self = HeaderId(
|
||||
data.target_state.best_finalized_peer_at_best_self.0 + 1,
|
||||
data.target_state.best_finalized_peer_at_best_self.0 + 1,
|
||||
);
|
||||
}
|
||||
if !data.submitted_messages_proofs.is_empty() {
|
||||
exit_sender.unbounded_send(()).unwrap();
|
||||
}
|
||||
}),
|
||||
exit_receiver.into_future().map(|(_, _)| ()),
|
||||
);
|
||||
|
||||
assert_eq!(result.submitted_messages_proofs, vec![(1..=1, None)],);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn message_lane_loop_works() {
|
||||
let (exit_sender, exit_receiver) = unbounded();
|
||||
let result = run_loop_test(
|
||||
TestClientData {
|
||||
source_state: ClientState {
|
||||
best_self: HeaderId(10, 10),
|
||||
best_finalized_self: HeaderId(10, 10),
|
||||
best_finalized_peer_at_best_self: HeaderId(0, 0),
|
||||
},
|
||||
source_latest_generated_nonce: 10,
|
||||
target_state: ClientState {
|
||||
best_self: HeaderId(0, 0),
|
||||
best_finalized_self: HeaderId(0, 0),
|
||||
best_finalized_peer_at_best_self: HeaderId(0, 0),
|
||||
},
|
||||
target_latest_received_nonce: 0,
|
||||
..Default::default()
|
||||
},
|
||||
Arc::new(|_: &mut TestClientData| {}),
|
||||
Arc::new(move |data: &mut TestClientData| {
|
||||
// syncing source headers -> target chain (all at once)
|
||||
if data.target_state.best_finalized_peer_at_best_self.0 < data.source_state.best_finalized_self.0 {
|
||||
data.target_state.best_finalized_peer_at_best_self = data.source_state.best_finalized_self;
|
||||
}
|
||||
// syncing source headers -> target chain (all at once)
|
||||
if data.source_state.best_finalized_peer_at_best_self.0 < data.target_state.best_finalized_self.0 {
|
||||
data.source_state.best_finalized_peer_at_best_self = data.target_state.best_finalized_self;
|
||||
}
|
||||
// if target has received messages batch => increase blocks so that confirmations may be sent
|
||||
if data.target_latest_received_nonce == 4
|
||||
|| data.target_latest_received_nonce == 8
|
||||
|| data.target_latest_received_nonce == 10
|
||||
{
|
||||
data.target_state.best_self =
|
||||
HeaderId(data.target_state.best_self.0 + 1, data.target_state.best_self.0 + 1);
|
||||
data.target_state.best_finalized_self = data.target_state.best_self;
|
||||
data.source_state.best_self =
|
||||
HeaderId(data.source_state.best_self.0 + 1, data.source_state.best_self.0 + 1);
|
||||
data.source_state.best_finalized_self = data.source_state.best_self;
|
||||
}
|
||||
// if source has received all messages receiving confirmations => increase source block so that confirmations may be sent
|
||||
if data.source_latest_confirmed_received_nonce == 10 {
|
||||
exit_sender.unbounded_send(()).unwrap();
|
||||
}
|
||||
}),
|
||||
exit_receiver.into_future().map(|(_, _)| ()),
|
||||
);
|
||||
|
||||
// there are no strict restrictions on when reward confirmation should come
|
||||
// (because `max_unconfirmed_nonces_at_target` is `100` in tests and this confirmation
|
||||
// depends on the state of both clients)
|
||||
// => we do not check it here
|
||||
assert_eq!(result.submitted_messages_proofs[0].0, 1..=4);
|
||||
assert_eq!(result.submitted_messages_proofs[1].0, 5..=8);
|
||||
assert_eq!(result.submitted_messages_proofs[2].0, 9..=10);
|
||||
assert!(!result.submitted_messages_receiving_proofs.is_empty());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,871 @@
|
||||
// Copyright 2019-2020 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.
|
||||
|
||||
//! Message delivery race delivers proof-of-messages from lane.source to lane.target.
|
||||
|
||||
use crate::message_lane::{MessageLane, SourceHeaderIdOf, TargetHeaderIdOf};
|
||||
use crate::message_lane_loop::{
|
||||
MessageDeliveryParams, MessageProofParameters, MessageWeightsMap, SourceClient as MessageLaneSourceClient,
|
||||
SourceClientState, TargetClient as MessageLaneTargetClient, TargetClientState,
|
||||
};
|
||||
use crate::message_race_loop::{
|
||||
MessageRace, NoncesRange, RaceState, RaceStrategy, SourceClient, SourceClientNonces, TargetClient,
|
||||
TargetClientNonces,
|
||||
};
|
||||
use crate::message_race_strategy::BasicStrategy;
|
||||
use crate::metrics::MessageLaneLoopMetrics;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use bp_message_lane::{MessageNonce, UnrewardedRelayersState, Weight};
|
||||
use futures::stream::FusedStream;
|
||||
use relay_utils::FailedClient;
|
||||
use std::{
|
||||
collections::{BTreeMap, VecDeque},
|
||||
marker::PhantomData,
|
||||
ops::RangeInclusive,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
/// Run message delivery race.
|
||||
pub async fn run<P: MessageLane>(
|
||||
source_client: impl MessageLaneSourceClient<P>,
|
||||
source_state_updates: impl FusedStream<Item = SourceClientState<P>>,
|
||||
target_client: impl MessageLaneTargetClient<P>,
|
||||
target_state_updates: impl FusedStream<Item = TargetClientState<P>>,
|
||||
stall_timeout: Duration,
|
||||
metrics_msg: Option<MessageLaneLoopMetrics>,
|
||||
params: MessageDeliveryParams,
|
||||
) -> Result<(), FailedClient> {
|
||||
crate::message_race_loop::run(
|
||||
MessageDeliveryRaceSource {
|
||||
client: source_client,
|
||||
metrics_msg: metrics_msg.clone(),
|
||||
_phantom: Default::default(),
|
||||
},
|
||||
source_state_updates,
|
||||
MessageDeliveryRaceTarget {
|
||||
client: target_client,
|
||||
metrics_msg,
|
||||
_phantom: Default::default(),
|
||||
},
|
||||
target_state_updates,
|
||||
stall_timeout,
|
||||
MessageDeliveryStrategy::<P> {
|
||||
max_unrewarded_relayer_entries_at_target: params.max_unrewarded_relayer_entries_at_target,
|
||||
max_unconfirmed_nonces_at_target: params.max_unconfirmed_nonces_at_target,
|
||||
max_messages_in_single_batch: params.max_messages_in_single_batch,
|
||||
max_messages_weight_in_single_batch: params.max_messages_weight_in_single_batch,
|
||||
max_messages_size_in_single_batch: params.max_messages_size_in_single_batch,
|
||||
latest_confirmed_nonces_at_source: VecDeque::new(),
|
||||
target_nonces: None,
|
||||
strategy: BasicStrategy::new(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Message delivery race.
|
||||
struct MessageDeliveryRace<P>(std::marker::PhantomData<P>);
|
||||
|
||||
impl<P: MessageLane> MessageRace for MessageDeliveryRace<P> {
|
||||
type SourceHeaderId = SourceHeaderIdOf<P>;
|
||||
type TargetHeaderId = TargetHeaderIdOf<P>;
|
||||
|
||||
type MessageNonce = MessageNonce;
|
||||
type Proof = P::MessagesProof;
|
||||
|
||||
fn source_name() -> String {
|
||||
format!("{}::MessagesDelivery", P::SOURCE_NAME)
|
||||
}
|
||||
|
||||
fn target_name() -> String {
|
||||
format!("{}::MessagesDelivery", P::TARGET_NAME)
|
||||
}
|
||||
}
|
||||
|
||||
/// Message delivery race source, which is a source of the lane.
|
||||
struct MessageDeliveryRaceSource<P: MessageLane, C> {
|
||||
client: C,
|
||||
metrics_msg: Option<MessageLaneLoopMetrics>,
|
||||
_phantom: PhantomData<P>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<P, C> SourceClient<MessageDeliveryRace<P>> for MessageDeliveryRaceSource<P, C>
|
||||
where
|
||||
P: MessageLane,
|
||||
C: MessageLaneSourceClient<P>,
|
||||
{
|
||||
type Error = C::Error;
|
||||
type NoncesRange = MessageWeightsMap;
|
||||
type ProofParameters = MessageProofParameters;
|
||||
|
||||
async fn nonces(
|
||||
&self,
|
||||
at_block: SourceHeaderIdOf<P>,
|
||||
prev_latest_nonce: MessageNonce,
|
||||
) -> Result<(SourceHeaderIdOf<P>, SourceClientNonces<Self::NoncesRange>), Self::Error> {
|
||||
let (at_block, latest_generated_nonce) = self.client.latest_generated_nonce(at_block).await?;
|
||||
let (at_block, latest_confirmed_nonce) = self.client.latest_confirmed_received_nonce(at_block).await?;
|
||||
|
||||
if let Some(metrics_msg) = self.metrics_msg.as_ref() {
|
||||
metrics_msg.update_source_latest_generated_nonce::<P>(latest_generated_nonce);
|
||||
metrics_msg.update_source_latest_confirmed_nonce::<P>(latest_confirmed_nonce);
|
||||
}
|
||||
|
||||
let new_nonces = if latest_generated_nonce > prev_latest_nonce {
|
||||
self.client
|
||||
.generated_messages_weights(at_block.clone(), prev_latest_nonce + 1..=latest_generated_nonce)
|
||||
.await?
|
||||
} else {
|
||||
MessageWeightsMap::new()
|
||||
};
|
||||
|
||||
Ok((
|
||||
at_block,
|
||||
SourceClientNonces {
|
||||
new_nonces,
|
||||
confirmed_nonce: Some(latest_confirmed_nonce),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
async fn generate_proof(
|
||||
&self,
|
||||
at_block: SourceHeaderIdOf<P>,
|
||||
nonces: RangeInclusive<MessageNonce>,
|
||||
proof_parameters: Self::ProofParameters,
|
||||
) -> Result<(SourceHeaderIdOf<P>, RangeInclusive<MessageNonce>, P::MessagesProof), Self::Error> {
|
||||
self.client.prove_messages(at_block, nonces, proof_parameters).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Message delivery race target, which is a target of the lane.
|
||||
struct MessageDeliveryRaceTarget<P: MessageLane, C> {
|
||||
client: C,
|
||||
metrics_msg: Option<MessageLaneLoopMetrics>,
|
||||
_phantom: PhantomData<P>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<P, C> TargetClient<MessageDeliveryRace<P>> for MessageDeliveryRaceTarget<P, C>
|
||||
where
|
||||
P: MessageLane,
|
||||
C: MessageLaneTargetClient<P>,
|
||||
{
|
||||
type Error = C::Error;
|
||||
type TargetNoncesData = DeliveryRaceTargetNoncesData;
|
||||
|
||||
async fn nonces(
|
||||
&self,
|
||||
at_block: TargetHeaderIdOf<P>,
|
||||
update_metrics: bool,
|
||||
) -> Result<(TargetHeaderIdOf<P>, TargetClientNonces<DeliveryRaceTargetNoncesData>), Self::Error> {
|
||||
let (at_block, latest_received_nonce) = self.client.latest_received_nonce(at_block).await?;
|
||||
let (at_block, latest_confirmed_nonce) = self.client.latest_confirmed_received_nonce(at_block).await?;
|
||||
let (at_block, unrewarded_relayers) = self.client.unrewarded_relayers_state(at_block).await?;
|
||||
|
||||
if update_metrics {
|
||||
if let Some(metrics_msg) = self.metrics_msg.as_ref() {
|
||||
metrics_msg.update_target_latest_received_nonce::<P>(latest_received_nonce);
|
||||
metrics_msg.update_target_latest_confirmed_nonce::<P>(latest_confirmed_nonce);
|
||||
}
|
||||
}
|
||||
|
||||
Ok((
|
||||
at_block,
|
||||
TargetClientNonces {
|
||||
latest_nonce: latest_received_nonce,
|
||||
nonces_data: DeliveryRaceTargetNoncesData {
|
||||
confirmed_nonce: latest_confirmed_nonce,
|
||||
unrewarded_relayers,
|
||||
},
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
async fn submit_proof(
|
||||
&self,
|
||||
generated_at_block: SourceHeaderIdOf<P>,
|
||||
nonces: RangeInclusive<MessageNonce>,
|
||||
proof: P::MessagesProof,
|
||||
) -> Result<RangeInclusive<MessageNonce>, Self::Error> {
|
||||
self.client
|
||||
.submit_messages_proof(generated_at_block, nonces, proof)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
/// Additional nonces data from the target client used by message delivery race.
|
||||
#[derive(Debug, Clone)]
|
||||
struct DeliveryRaceTargetNoncesData {
|
||||
/// Latest nonce that we know: (1) has been delivered to us (2) has been confirmed
|
||||
/// back to the source node (by confirmations race) and (3) relayer has received
|
||||
/// reward for (and this has been confirmed by the message delivery race).
|
||||
confirmed_nonce: MessageNonce,
|
||||
/// State of the unrewarded relayers set at the target node.
|
||||
unrewarded_relayers: UnrewardedRelayersState,
|
||||
}
|
||||
|
||||
/// Messages delivery strategy.
|
||||
struct MessageDeliveryStrategy<P: MessageLane> {
|
||||
/// Maximal unrewarded relayer entries at target client.
|
||||
max_unrewarded_relayer_entries_at_target: MessageNonce,
|
||||
/// Maximal unconfirmed nonces at target client.
|
||||
max_unconfirmed_nonces_at_target: MessageNonce,
|
||||
/// Maximal number of messages in the single delivery transaction.
|
||||
max_messages_in_single_batch: MessageNonce,
|
||||
/// Maximal cumulative messages weight in the single delivery transaction.
|
||||
max_messages_weight_in_single_batch: Weight,
|
||||
/// Maximal messages size in the single delivery transaction.
|
||||
max_messages_size_in_single_batch: usize,
|
||||
/// Latest confirmed nonces at the source client + the header id where we have first met this nonce.
|
||||
latest_confirmed_nonces_at_source: VecDeque<(SourceHeaderIdOf<P>, MessageNonce)>,
|
||||
/// Target nonces from the source client.
|
||||
target_nonces: Option<TargetClientNonces<DeliveryRaceTargetNoncesData>>,
|
||||
/// Basic delivery strategy.
|
||||
strategy: MessageDeliveryStrategyBase<P>,
|
||||
}
|
||||
|
||||
type MessageDeliveryStrategyBase<P> = BasicStrategy<
|
||||
<P as MessageLane>::SourceHeaderNumber,
|
||||
<P as MessageLane>::SourceHeaderHash,
|
||||
<P as MessageLane>::TargetHeaderNumber,
|
||||
<P as MessageLane>::TargetHeaderHash,
|
||||
MessageWeightsMap,
|
||||
<P as MessageLane>::MessagesProof,
|
||||
>;
|
||||
|
||||
impl<P: MessageLane> std::fmt::Debug for MessageDeliveryStrategy<P> {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
fmt.debug_struct("MessageDeliveryStrategy")
|
||||
.field(
|
||||
"max_unrewarded_relayer_entries_at_target",
|
||||
&self.max_unrewarded_relayer_entries_at_target,
|
||||
)
|
||||
.field(
|
||||
"max_unconfirmed_nonces_at_target",
|
||||
&self.max_unconfirmed_nonces_at_target,
|
||||
)
|
||||
.field("max_messages_in_single_batch", &self.max_messages_in_single_batch)
|
||||
.field(
|
||||
"max_messages_weight_in_single_batch",
|
||||
&self.max_messages_weight_in_single_batch,
|
||||
)
|
||||
.field(
|
||||
"max_messages_size_in_single_batch",
|
||||
&self.max_messages_size_in_single_batch,
|
||||
)
|
||||
.field(
|
||||
"latest_confirmed_nonces_at_source",
|
||||
&self.latest_confirmed_nonces_at_source,
|
||||
)
|
||||
.field("target_nonces", &self.target_nonces)
|
||||
.field("strategy", &self.strategy)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: MessageLane> RaceStrategy<SourceHeaderIdOf<P>, TargetHeaderIdOf<P>, P::MessagesProof>
|
||||
for MessageDeliveryStrategy<P>
|
||||
{
|
||||
type SourceNoncesRange = MessageWeightsMap;
|
||||
type ProofParameters = MessageProofParameters;
|
||||
type TargetNoncesData = DeliveryRaceTargetNoncesData;
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
self.strategy.is_empty()
|
||||
}
|
||||
|
||||
fn best_at_source(&self) -> Option<MessageNonce> {
|
||||
self.strategy.best_at_source()
|
||||
}
|
||||
|
||||
fn best_at_target(&self) -> Option<MessageNonce> {
|
||||
self.strategy.best_at_target()
|
||||
}
|
||||
|
||||
fn source_nonces_updated(
|
||||
&mut self,
|
||||
at_block: SourceHeaderIdOf<P>,
|
||||
nonces: SourceClientNonces<Self::SourceNoncesRange>,
|
||||
) {
|
||||
if let Some(confirmed_nonce) = nonces.confirmed_nonce {
|
||||
let is_confirmed_nonce_updated = self
|
||||
.latest_confirmed_nonces_at_source
|
||||
.back()
|
||||
.map(|(_, prev_nonce)| *prev_nonce != confirmed_nonce)
|
||||
.unwrap_or(true);
|
||||
if is_confirmed_nonce_updated {
|
||||
self.latest_confirmed_nonces_at_source
|
||||
.push_back((at_block.clone(), confirmed_nonce));
|
||||
}
|
||||
}
|
||||
self.strategy.source_nonces_updated(at_block, nonces)
|
||||
}
|
||||
|
||||
fn best_target_nonces_updated(
|
||||
&mut self,
|
||||
nonces: TargetClientNonces<DeliveryRaceTargetNoncesData>,
|
||||
race_state: &mut RaceState<SourceHeaderIdOf<P>, TargetHeaderIdOf<P>, P::MessagesProof>,
|
||||
) {
|
||||
// best target nonces must always be ge than finalized target nonces
|
||||
let mut target_nonces = self.target_nonces.take().unwrap_or_else(|| nonces.clone());
|
||||
target_nonces.nonces_data = nonces.nonces_data.clone();
|
||||
target_nonces.latest_nonce = std::cmp::max(target_nonces.latest_nonce, nonces.latest_nonce);
|
||||
self.target_nonces = Some(target_nonces);
|
||||
|
||||
self.strategy.best_target_nonces_updated(
|
||||
TargetClientNonces {
|
||||
latest_nonce: nonces.latest_nonce,
|
||||
nonces_data: (),
|
||||
},
|
||||
race_state,
|
||||
)
|
||||
}
|
||||
|
||||
fn finalized_target_nonces_updated(
|
||||
&mut self,
|
||||
nonces: TargetClientNonces<DeliveryRaceTargetNoncesData>,
|
||||
race_state: &mut RaceState<SourceHeaderIdOf<P>, TargetHeaderIdOf<P>, P::MessagesProof>,
|
||||
) {
|
||||
if let Some(ref best_finalized_source_header_id_at_best_target) =
|
||||
race_state.best_finalized_source_header_id_at_best_target
|
||||
{
|
||||
let oldest_header_number_to_keep = best_finalized_source_header_id_at_best_target.0;
|
||||
while self
|
||||
.latest_confirmed_nonces_at_source
|
||||
.front()
|
||||
.map(|(id, _)| id.0 < oldest_header_number_to_keep)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
self.latest_confirmed_nonces_at_source.pop_front();
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref mut target_nonces) = self.target_nonces {
|
||||
target_nonces.latest_nonce = std::cmp::max(target_nonces.latest_nonce, nonces.latest_nonce);
|
||||
}
|
||||
|
||||
self.strategy.finalized_target_nonces_updated(
|
||||
TargetClientNonces {
|
||||
latest_nonce: nonces.latest_nonce,
|
||||
nonces_data: (),
|
||||
},
|
||||
race_state,
|
||||
)
|
||||
}
|
||||
|
||||
fn select_nonces_to_deliver(
|
||||
&mut self,
|
||||
race_state: &RaceState<SourceHeaderIdOf<P>, TargetHeaderIdOf<P>, P::MessagesProof>,
|
||||
) -> Option<(RangeInclusive<MessageNonce>, Self::ProofParameters)> {
|
||||
let best_finalized_source_header_id_at_best_target =
|
||||
race_state.best_finalized_source_header_id_at_best_target.clone()?;
|
||||
let latest_confirmed_nonce_at_source = self
|
||||
.latest_confirmed_nonces_at_source
|
||||
.iter()
|
||||
.take_while(|(id, _)| id.0 <= best_finalized_source_header_id_at_best_target.0)
|
||||
.last()
|
||||
.map(|(_, nonce)| *nonce)?;
|
||||
let target_nonces = self.target_nonces.as_ref()?;
|
||||
|
||||
// There's additional condition in the message delivery race: target would reject messages
|
||||
// if there are too much unconfirmed messages at the inbound lane.
|
||||
|
||||
// The receiving race is responsible to deliver confirmations back to the source chain. So if
|
||||
// there's a lot of unconfirmed messages, let's wait until it'll be able to do its job.
|
||||
let latest_received_nonce_at_target = target_nonces.latest_nonce;
|
||||
let confirmations_missing = latest_received_nonce_at_target.checked_sub(latest_confirmed_nonce_at_source);
|
||||
match confirmations_missing {
|
||||
Some(confirmations_missing) if confirmations_missing >= self.max_unconfirmed_nonces_at_target => {
|
||||
log::debug!(
|
||||
target: "bridge",
|
||||
"Cannot deliver any more messages from {} to {}. Too many unconfirmed nonces \
|
||||
at target: target.latest_received={:?}, source.latest_confirmed={:?}, max={:?}",
|
||||
MessageDeliveryRace::<P>::source_name(),
|
||||
MessageDeliveryRace::<P>::target_name(),
|
||||
latest_received_nonce_at_target,
|
||||
latest_confirmed_nonce_at_source,
|
||||
self.max_unconfirmed_nonces_at_target,
|
||||
);
|
||||
|
||||
return None;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
// Ok - we may have new nonces to deliver. But target may still reject new messages, because we haven't
|
||||
// notified it that (some) messages have been confirmed. So we may want to include updated
|
||||
// `source.latest_confirmed` in the proof.
|
||||
//
|
||||
// Important note: we're including outbound state lane proof whenever there are unconfirmed nonces
|
||||
// on the target chain. Other strategy is to include it only if it's absolutely necessary.
|
||||
let latest_confirmed_nonce_at_target = target_nonces.nonces_data.confirmed_nonce;
|
||||
let outbound_state_proof_required = latest_confirmed_nonce_at_target < latest_confirmed_nonce_at_source;
|
||||
|
||||
// The target node would also reject messages if there are too many entries in the
|
||||
// "unrewarded relayers" set. If we are unable to prove new rewards to the target node, then
|
||||
// we should wait for confirmations race.
|
||||
let unrewarded_relayer_entries_limit_reached =
|
||||
target_nonces.nonces_data.unrewarded_relayers.unrewarded_relayer_entries
|
||||
>= self.max_unrewarded_relayer_entries_at_target;
|
||||
if unrewarded_relayer_entries_limit_reached {
|
||||
// so there are already too many unrewarded relayer entries in the set
|
||||
//
|
||||
// => check if we can prove enough rewards. If not, we should wait for more rewards to be paid
|
||||
let number_of_rewards_being_proved =
|
||||
latest_confirmed_nonce_at_source.saturating_sub(latest_confirmed_nonce_at_target);
|
||||
let enough_rewards_being_proved = number_of_rewards_being_proved
|
||||
>= target_nonces.nonces_data.unrewarded_relayers.messages_in_oldest_entry;
|
||||
if !enough_rewards_being_proved {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
// If we're here, then the confirmations race did its job && sending side now knows that messages
|
||||
// have been delivered. Now let's select nonces that we want to deliver.
|
||||
//
|
||||
// We may deliver at most:
|
||||
//
|
||||
// max_unconfirmed_nonces_at_target - (latest_received_nonce_at_target - latest_confirmed_nonce_at_target)
|
||||
//
|
||||
// messages in the batch. But since we're including outbound state proof in the batch, then it
|
||||
// may be increased to:
|
||||
//
|
||||
// max_unconfirmed_nonces_at_target - (latest_received_nonce_at_target - latest_confirmed_nonce_at_source)
|
||||
let future_confirmed_nonce_at_target = if outbound_state_proof_required {
|
||||
latest_confirmed_nonce_at_source
|
||||
} else {
|
||||
latest_confirmed_nonce_at_target
|
||||
};
|
||||
let max_nonces = latest_received_nonce_at_target
|
||||
.checked_sub(future_confirmed_nonce_at_target)
|
||||
.and_then(|diff| self.max_unconfirmed_nonces_at_target.checked_sub(diff))
|
||||
.unwrap_or_default();
|
||||
let max_nonces = std::cmp::min(max_nonces, self.max_messages_in_single_batch);
|
||||
let max_messages_weight_in_single_batch = self.max_messages_weight_in_single_batch;
|
||||
let max_messages_size_in_single_batch = self.max_messages_size_in_single_batch;
|
||||
let mut selected_weight: Weight = 0;
|
||||
let mut selected_size: usize = 0;
|
||||
let mut selected_count: MessageNonce = 0;
|
||||
|
||||
let selected_nonces = self
|
||||
.strategy
|
||||
.select_nonces_to_deliver_with_selector(race_state, |range| {
|
||||
let to_requeue = range
|
||||
.into_iter()
|
||||
.skip_while(|(_, weight)| {
|
||||
// Since we (hopefully) have some reserves in `max_messages_weight_in_single_batch`
|
||||
// and `max_messages_size_in_single_batch`, we may still try to submit transaction
|
||||
// with single message if message overflows these limits. The worst case would be if
|
||||
// transaction will be rejected by the target runtime, but at least we have tried.
|
||||
|
||||
// limit messages in the batch by weight
|
||||
let new_selected_weight = match selected_weight.checked_add(weight.weight) {
|
||||
Some(new_selected_weight) if new_selected_weight <= max_messages_weight_in_single_batch => {
|
||||
new_selected_weight
|
||||
}
|
||||
new_selected_weight if selected_count == 0 => {
|
||||
log::warn!(
|
||||
target: "bridge",
|
||||
"Going to submit message delivery transaction with declared dispatch \
|
||||
weight {:?} that overflows maximal configured weight {}",
|
||||
new_selected_weight,
|
||||
max_messages_weight_in_single_batch,
|
||||
);
|
||||
new_selected_weight.unwrap_or(Weight::MAX)
|
||||
}
|
||||
_ => return false,
|
||||
};
|
||||
|
||||
// limit messages in the batch by size
|
||||
let new_selected_size = match selected_size.checked_add(weight.size) {
|
||||
Some(new_selected_size) if new_selected_size <= max_messages_size_in_single_batch => {
|
||||
new_selected_size
|
||||
}
|
||||
new_selected_size if selected_count == 0 => {
|
||||
log::warn!(
|
||||
target: "bridge",
|
||||
"Going to submit message delivery transaction with message \
|
||||
size {:?} that overflows maximal configured size {}",
|
||||
new_selected_size,
|
||||
max_messages_size_in_single_batch,
|
||||
);
|
||||
new_selected_size.unwrap_or(usize::MAX)
|
||||
}
|
||||
_ => return false,
|
||||
};
|
||||
|
||||
// limit number of messages in the batch
|
||||
let new_selected_count = selected_count + 1;
|
||||
if new_selected_count > max_nonces {
|
||||
return false;
|
||||
}
|
||||
|
||||
selected_weight = new_selected_weight;
|
||||
selected_size = new_selected_size;
|
||||
selected_count = new_selected_count;
|
||||
true
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
if to_requeue.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(to_requeue)
|
||||
}
|
||||
})?;
|
||||
|
||||
Some((
|
||||
selected_nonces,
|
||||
MessageProofParameters {
|
||||
outbound_state_proof_required,
|
||||
dispatch_weight: selected_weight,
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl NoncesRange for MessageWeightsMap {
|
||||
fn begin(&self) -> MessageNonce {
|
||||
self.keys().next().cloned().unwrap_or_default()
|
||||
}
|
||||
|
||||
fn end(&self) -> MessageNonce {
|
||||
self.keys().next_back().cloned().unwrap_or_default()
|
||||
}
|
||||
|
||||
fn greater_than(mut self, nonce: MessageNonce) -> Option<Self> {
|
||||
let gte = self.split_off(&(nonce + 1));
|
||||
if gte.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(gte)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::message_lane_loop::{
|
||||
tests::{header_id, TestMessageLane, TestMessagesProof, TestSourceHeaderId, TestTargetHeaderId},
|
||||
MessageWeights,
|
||||
};
|
||||
|
||||
type TestRaceState = RaceState<TestSourceHeaderId, TestTargetHeaderId, TestMessagesProof>;
|
||||
type TestStrategy = MessageDeliveryStrategy<TestMessageLane>;
|
||||
|
||||
fn prepare_strategy() -> (TestRaceState, TestStrategy) {
|
||||
let mut race_state = RaceState {
|
||||
best_finalized_source_header_id_at_source: Some(header_id(1)),
|
||||
best_finalized_source_header_id_at_best_target: Some(header_id(1)),
|
||||
best_target_header_id: Some(header_id(1)),
|
||||
best_finalized_target_header_id: Some(header_id(1)),
|
||||
nonces_to_submit: None,
|
||||
nonces_submitted: None,
|
||||
};
|
||||
|
||||
let mut race_strategy = TestStrategy {
|
||||
max_unrewarded_relayer_entries_at_target: 4,
|
||||
max_unconfirmed_nonces_at_target: 4,
|
||||
max_messages_in_single_batch: 4,
|
||||
max_messages_weight_in_single_batch: 4,
|
||||
max_messages_size_in_single_batch: 4,
|
||||
latest_confirmed_nonces_at_source: vec![(header_id(1), 19)].into_iter().collect(),
|
||||
target_nonces: Some(TargetClientNonces {
|
||||
latest_nonce: 19,
|
||||
nonces_data: DeliveryRaceTargetNoncesData {
|
||||
confirmed_nonce: 19,
|
||||
unrewarded_relayers: UnrewardedRelayersState {
|
||||
unrewarded_relayer_entries: 0,
|
||||
messages_in_oldest_entry: 0,
|
||||
total_messages: 0,
|
||||
},
|
||||
},
|
||||
}),
|
||||
strategy: BasicStrategy::new(),
|
||||
};
|
||||
|
||||
race_strategy.strategy.source_nonces_updated(
|
||||
header_id(1),
|
||||
SourceClientNonces {
|
||||
new_nonces: vec![
|
||||
(20, MessageWeights { weight: 1, size: 1 }),
|
||||
(21, MessageWeights { weight: 1, size: 1 }),
|
||||
(22, MessageWeights { weight: 1, size: 1 }),
|
||||
(23, MessageWeights { weight: 1, size: 1 }),
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
confirmed_nonce: Some(19),
|
||||
},
|
||||
);
|
||||
|
||||
let target_nonces = TargetClientNonces {
|
||||
latest_nonce: 19,
|
||||
nonces_data: (),
|
||||
};
|
||||
race_strategy
|
||||
.strategy
|
||||
.best_target_nonces_updated(target_nonces.clone(), &mut race_state);
|
||||
race_strategy
|
||||
.strategy
|
||||
.finalized_target_nonces_updated(target_nonces, &mut race_state);
|
||||
|
||||
(race_state, race_strategy)
|
||||
}
|
||||
|
||||
fn proof_parameters(state_required: bool, weight: Weight) -> MessageProofParameters {
|
||||
MessageProofParameters {
|
||||
outbound_state_proof_required: state_required,
|
||||
dispatch_weight: weight,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn weights_map_works_as_nonces_range() {
|
||||
fn build_map(range: RangeInclusive<MessageNonce>) -> MessageWeightsMap {
|
||||
range
|
||||
.map(|idx| {
|
||||
(
|
||||
idx,
|
||||
MessageWeights {
|
||||
weight: idx,
|
||||
size: idx as _,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
let map = build_map(20..=30);
|
||||
|
||||
assert_eq!(map.begin(), 20);
|
||||
assert_eq!(map.end(), 30);
|
||||
assert_eq!(map.clone().greater_than(10), Some(build_map(20..=30)));
|
||||
assert_eq!(map.clone().greater_than(19), Some(build_map(20..=30)));
|
||||
assert_eq!(map.clone().greater_than(20), Some(build_map(21..=30)));
|
||||
assert_eq!(map.clone().greater_than(25), Some(build_map(26..=30)));
|
||||
assert_eq!(map.clone().greater_than(29), Some(build_map(30..=30)));
|
||||
assert_eq!(map.greater_than(30), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn message_delivery_strategy_selects_messages_to_deliver() {
|
||||
let (state, mut strategy) = prepare_strategy();
|
||||
|
||||
// both sides are ready to relay new messages
|
||||
assert_eq!(
|
||||
strategy.select_nonces_to_deliver(&state),
|
||||
Some(((20..=23), proof_parameters(false, 4)))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn message_delivery_strategy_selects_nothing_if_too_many_confirmations_missing() {
|
||||
let (state, mut strategy) = prepare_strategy();
|
||||
|
||||
// if there are already `max_unconfirmed_nonces_at_target` messages on target,
|
||||
// we need to wait until confirmations will be delivered by receiving race
|
||||
strategy.latest_confirmed_nonces_at_source = vec![(
|
||||
header_id(1),
|
||||
strategy.target_nonces.as_ref().unwrap().latest_nonce - strategy.max_unconfirmed_nonces_at_target,
|
||||
)]
|
||||
.into_iter()
|
||||
.collect();
|
||||
assert_eq!(strategy.select_nonces_to_deliver(&state), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn message_delivery_strategy_includes_outbound_state_proof_when_new_nonces_are_available() {
|
||||
let (state, mut strategy) = prepare_strategy();
|
||||
|
||||
// if there are new confirmed nonces on source, we want to relay this information
|
||||
// to target to prune rewards queue
|
||||
let prev_confirmed_nonce_at_source = strategy.latest_confirmed_nonces_at_source.back().unwrap().1;
|
||||
strategy.target_nonces.as_mut().unwrap().nonces_data.confirmed_nonce = prev_confirmed_nonce_at_source - 1;
|
||||
assert_eq!(
|
||||
strategy.select_nonces_to_deliver(&state),
|
||||
Some(((20..=23), proof_parameters(true, 4)))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn message_delivery_strategy_selects_nothing_if_there_are_too_many_unrewarded_relayers() {
|
||||
let (state, mut strategy) = prepare_strategy();
|
||||
|
||||
// if there are already `max_unrewarded_relayer_entries_at_target` entries at target,
|
||||
// we need to wait until rewards will be paid
|
||||
{
|
||||
let mut unrewarded_relayers = &mut strategy.target_nonces.as_mut().unwrap().nonces_data.unrewarded_relayers;
|
||||
unrewarded_relayers.unrewarded_relayer_entries = strategy.max_unrewarded_relayer_entries_at_target;
|
||||
unrewarded_relayers.messages_in_oldest_entry = 4;
|
||||
}
|
||||
assert_eq!(strategy.select_nonces_to_deliver(&state), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn message_delivery_strategy_selects_nothing_if_proved_rewards_is_not_enough_to_remove_oldest_unrewarded_entry() {
|
||||
let (state, mut strategy) = prepare_strategy();
|
||||
|
||||
// if there are already `max_unrewarded_relayer_entries_at_target` entries at target,
|
||||
// we need to prove at least `messages_in_oldest_entry` rewards
|
||||
let prev_confirmed_nonce_at_source = strategy.latest_confirmed_nonces_at_source.back().unwrap().1;
|
||||
{
|
||||
let mut nonces_data = &mut strategy.target_nonces.as_mut().unwrap().nonces_data;
|
||||
nonces_data.confirmed_nonce = prev_confirmed_nonce_at_source - 1;
|
||||
let mut unrewarded_relayers = &mut nonces_data.unrewarded_relayers;
|
||||
unrewarded_relayers.unrewarded_relayer_entries = strategy.max_unrewarded_relayer_entries_at_target;
|
||||
unrewarded_relayers.messages_in_oldest_entry = 4;
|
||||
}
|
||||
assert_eq!(strategy.select_nonces_to_deliver(&state), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn message_delivery_strategy_includes_outbound_state_proof_if_proved_rewards_is_enough() {
|
||||
let (state, mut strategy) = prepare_strategy();
|
||||
|
||||
// if there are already `max_unrewarded_relayer_entries_at_target` entries at target,
|
||||
// we need to prove at least `messages_in_oldest_entry` rewards
|
||||
let prev_confirmed_nonce_at_source = strategy.latest_confirmed_nonces_at_source.back().unwrap().1;
|
||||
{
|
||||
let mut nonces_data = &mut strategy.target_nonces.as_mut().unwrap().nonces_data;
|
||||
nonces_data.confirmed_nonce = prev_confirmed_nonce_at_source - 3;
|
||||
let mut unrewarded_relayers = &mut nonces_data.unrewarded_relayers;
|
||||
unrewarded_relayers.unrewarded_relayer_entries = strategy.max_unrewarded_relayer_entries_at_target;
|
||||
unrewarded_relayers.messages_in_oldest_entry = 3;
|
||||
}
|
||||
assert_eq!(
|
||||
strategy.select_nonces_to_deliver(&state),
|
||||
Some(((20..=23), proof_parameters(true, 4)))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn message_delivery_strategy_limits_batch_by_messages_weight() {
|
||||
let (state, mut strategy) = prepare_strategy();
|
||||
|
||||
// not all queued messages may fit in the batch, because batch has max weight
|
||||
strategy.max_messages_weight_in_single_batch = 3;
|
||||
assert_eq!(
|
||||
strategy.select_nonces_to_deliver(&state),
|
||||
Some(((20..=22), proof_parameters(false, 3)))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn message_delivery_strategy_accepts_single_message_even_if_its_weight_overflows_maximal_weight() {
|
||||
let (state, mut strategy) = prepare_strategy();
|
||||
|
||||
// first message doesn't fit in the batch, because it has weight (10) that overflows max weight (4)
|
||||
strategy.strategy.source_queue_mut()[0].1.get_mut(&20).unwrap().weight = 10;
|
||||
assert_eq!(
|
||||
strategy.select_nonces_to_deliver(&state),
|
||||
Some(((20..=20), proof_parameters(false, 10)))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn message_delivery_strategy_limits_batch_by_messages_size() {
|
||||
let (state, mut strategy) = prepare_strategy();
|
||||
|
||||
// not all queued messages may fit in the batch, because batch has max weight
|
||||
strategy.max_messages_size_in_single_batch = 3;
|
||||
assert_eq!(
|
||||
strategy.select_nonces_to_deliver(&state),
|
||||
Some(((20..=22), proof_parameters(false, 3)))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn message_delivery_strategy_accepts_single_message_even_if_its_weight_overflows_maximal_size() {
|
||||
let (state, mut strategy) = prepare_strategy();
|
||||
|
||||
// first message doesn't fit in the batch, because it has weight (10) that overflows max weight (4)
|
||||
strategy.strategy.source_queue_mut()[0].1.get_mut(&20).unwrap().size = 10;
|
||||
assert_eq!(
|
||||
strategy.select_nonces_to_deliver(&state),
|
||||
Some(((20..=20), proof_parameters(false, 1)))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn message_delivery_strategy_limits_batch_by_messages_count_when_there_is_upper_limit() {
|
||||
let (state, mut strategy) = prepare_strategy();
|
||||
|
||||
// not all queued messages may fit in the batch, because batch has max number of messages limit
|
||||
strategy.max_messages_in_single_batch = 3;
|
||||
assert_eq!(
|
||||
strategy.select_nonces_to_deliver(&state),
|
||||
Some(((20..=22), proof_parameters(false, 3)))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn message_delivery_strategy_limits_batch_by_messages_count_when_there_are_unconfirmed_nonces() {
|
||||
let (state, mut strategy) = prepare_strategy();
|
||||
|
||||
// 1 delivery confirmation from target to source is still missing, so we may only
|
||||
// relay 3 new messages
|
||||
let prev_confirmed_nonce_at_source = strategy.latest_confirmed_nonces_at_source.back().unwrap().1;
|
||||
strategy.latest_confirmed_nonces_at_source = vec![(header_id(1), prev_confirmed_nonce_at_source - 1)]
|
||||
.into_iter()
|
||||
.collect();
|
||||
strategy.target_nonces.as_mut().unwrap().nonces_data.confirmed_nonce = prev_confirmed_nonce_at_source - 1;
|
||||
assert_eq!(
|
||||
strategy.select_nonces_to_deliver(&state),
|
||||
Some(((20..=22), proof_parameters(false, 3)))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn message_delivery_strategy_waits_for_confirmed_nonce_header_to_appear_on_target() {
|
||||
// 1 delivery confirmation from target to source is still missing, so we may deliver
|
||||
// reward confirmation with our message delivery transaction. But the problem is that
|
||||
// the reward has been paid at header 2 && this header is still unknown to target node.
|
||||
//
|
||||
// => so we can't deliver more than 3 messages
|
||||
let (mut state, mut strategy) = prepare_strategy();
|
||||
let prev_confirmed_nonce_at_source = strategy.latest_confirmed_nonces_at_source.back().unwrap().1;
|
||||
strategy.latest_confirmed_nonces_at_source = vec![
|
||||
(header_id(1), prev_confirmed_nonce_at_source - 1),
|
||||
(header_id(2), prev_confirmed_nonce_at_source),
|
||||
]
|
||||
.into_iter()
|
||||
.collect();
|
||||
strategy.target_nonces.as_mut().unwrap().nonces_data.confirmed_nonce = prev_confirmed_nonce_at_source - 1;
|
||||
state.best_finalized_source_header_id_at_best_target = Some(header_id(1));
|
||||
assert_eq!(
|
||||
strategy.select_nonces_to_deliver(&state),
|
||||
Some(((20..=22), proof_parameters(false, 3)))
|
||||
);
|
||||
|
||||
// the same situation, but the header 2 is known to the target node, so we may deliver reward confirmation
|
||||
let (mut state, mut strategy) = prepare_strategy();
|
||||
let prev_confirmed_nonce_at_source = strategy.latest_confirmed_nonces_at_source.back().unwrap().1;
|
||||
strategy.latest_confirmed_nonces_at_source = vec![
|
||||
(header_id(1), prev_confirmed_nonce_at_source - 1),
|
||||
(header_id(2), prev_confirmed_nonce_at_source),
|
||||
]
|
||||
.into_iter()
|
||||
.collect();
|
||||
strategy.target_nonces.as_mut().unwrap().nonces_data.confirmed_nonce = prev_confirmed_nonce_at_source - 1;
|
||||
state.best_finalized_source_header_id_at_source = Some(header_id(2));
|
||||
state.best_finalized_source_header_id_at_best_target = Some(header_id(2));
|
||||
assert_eq!(
|
||||
strategy.select_nonces_to_deliver(&state),
|
||||
Some(((20..=23), proof_parameters(true, 4)))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,612 @@
|
||||
// Copyright 2019-2020 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.
|
||||
|
||||
//! Loop that is serving single race within message lane. This could be
|
||||
//! message delivery race, receiving confirmations race or processing
|
||||
//! confirmations race.
|
||||
//!
|
||||
//! The idea of the race is simple - we have `nonce`-s on source and target
|
||||
//! nodes. We're trying to prove that the source node has this nonce (and
|
||||
//! associated data - like messages, lane state, etc) to the target node by
|
||||
//! generating and submitting proof.
|
||||
|
||||
use crate::message_lane_loop::ClientState;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use bp_message_lane::MessageNonce;
|
||||
use futures::{
|
||||
future::FutureExt,
|
||||
stream::{FusedStream, StreamExt},
|
||||
};
|
||||
use relay_utils::{process_future_result, retry_backoff, FailedClient, MaybeConnectionError};
|
||||
use std::{
|
||||
fmt::Debug,
|
||||
ops::RangeInclusive,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
/// One of races within lane.
|
||||
pub trait MessageRace {
|
||||
/// Header id of the race source.
|
||||
type SourceHeaderId: Debug + Clone + PartialEq;
|
||||
/// Header id of the race source.
|
||||
type TargetHeaderId: Debug + Clone + PartialEq;
|
||||
|
||||
/// Message nonce used in the race.
|
||||
type MessageNonce: Debug + Clone;
|
||||
/// Proof that is generated and delivered in this race.
|
||||
type Proof: Debug + Clone;
|
||||
|
||||
/// Name of the race source.
|
||||
fn source_name() -> String;
|
||||
/// Name of the race target.
|
||||
fn target_name() -> String;
|
||||
}
|
||||
|
||||
/// State of race source client.
|
||||
type SourceClientState<P> = ClientState<<P as MessageRace>::SourceHeaderId, <P as MessageRace>::TargetHeaderId>;
|
||||
|
||||
/// State of race target client.
|
||||
type TargetClientState<P> = ClientState<<P as MessageRace>::TargetHeaderId, <P as MessageRace>::SourceHeaderId>;
|
||||
|
||||
/// Inclusive nonces range.
|
||||
pub trait NoncesRange: Debug + Sized {
|
||||
/// Get begin of the range.
|
||||
fn begin(&self) -> MessageNonce;
|
||||
/// Get end of the range.
|
||||
fn end(&self) -> MessageNonce;
|
||||
/// Returns new range with current range nonces that are greater than the passed `nonce`.
|
||||
/// If there are no such nonces, `None` is returned.
|
||||
fn greater_than(self, nonce: MessageNonce) -> Option<Self>;
|
||||
}
|
||||
|
||||
/// Nonces on the race source client.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SourceClientNonces<NoncesRange> {
|
||||
/// New nonces range known to the client. `New` here means all nonces generated after
|
||||
/// `prev_latest_nonce` passed to the `SourceClient::nonces` method.
|
||||
pub new_nonces: NoncesRange,
|
||||
/// Latest nonce that is confirmed to the bridged client. This nonce only makes
|
||||
/// sense in some races. In other races it is `None`.
|
||||
pub confirmed_nonce: Option<MessageNonce>,
|
||||
}
|
||||
|
||||
/// Nonces on the race target client.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TargetClientNonces<TargetNoncesData> {
|
||||
/// Latest nonce that is known to the target client.
|
||||
pub latest_nonce: MessageNonce,
|
||||
/// Additional data from target node that may be used by the race.
|
||||
pub nonces_data: TargetNoncesData,
|
||||
}
|
||||
|
||||
/// One of message lane clients, which is source client for the race.
|
||||
#[async_trait]
|
||||
pub trait SourceClient<P: MessageRace> {
|
||||
/// Type of error this clients returns.
|
||||
type Error: std::fmt::Debug + MaybeConnectionError;
|
||||
/// Type of nonces range returned by the source client.
|
||||
type NoncesRange: NoncesRange;
|
||||
/// Additional proof parameters required to generate proof.
|
||||
type ProofParameters;
|
||||
|
||||
/// Return nonces that are known to the source client.
|
||||
async fn nonces(
|
||||
&self,
|
||||
at_block: P::SourceHeaderId,
|
||||
prev_latest_nonce: MessageNonce,
|
||||
) -> Result<(P::SourceHeaderId, SourceClientNonces<Self::NoncesRange>), Self::Error>;
|
||||
/// Generate proof for delivering to the target client.
|
||||
async fn generate_proof(
|
||||
&self,
|
||||
at_block: P::SourceHeaderId,
|
||||
nonces: RangeInclusive<MessageNonce>,
|
||||
proof_parameters: Self::ProofParameters,
|
||||
) -> Result<(P::SourceHeaderId, RangeInclusive<MessageNonce>, P::Proof), Self::Error>;
|
||||
}
|
||||
|
||||
/// One of message lane clients, which is target client for the race.
|
||||
#[async_trait]
|
||||
pub trait TargetClient<P: MessageRace> {
|
||||
/// Type of error this clients returns.
|
||||
type Error: std::fmt::Debug + MaybeConnectionError;
|
||||
/// Type of the additional data from the target client, used by the race.
|
||||
type TargetNoncesData: std::fmt::Debug;
|
||||
|
||||
/// Return nonces that are known to the target client.
|
||||
async fn nonces(
|
||||
&self,
|
||||
at_block: P::TargetHeaderId,
|
||||
update_metrics: bool,
|
||||
) -> Result<(P::TargetHeaderId, TargetClientNonces<Self::TargetNoncesData>), Self::Error>;
|
||||
/// Submit proof to the target client.
|
||||
async fn submit_proof(
|
||||
&self,
|
||||
generated_at_block: P::SourceHeaderId,
|
||||
nonces: RangeInclusive<MessageNonce>,
|
||||
proof: P::Proof,
|
||||
) -> Result<RangeInclusive<MessageNonce>, Self::Error>;
|
||||
}
|
||||
|
||||
/// Race strategy.
|
||||
pub trait RaceStrategy<SourceHeaderId, TargetHeaderId, Proof>: Debug {
|
||||
/// Type of nonces range expected from the source client.
|
||||
type SourceNoncesRange: NoncesRange;
|
||||
/// Additional proof parameters required to generate proof.
|
||||
type ProofParameters;
|
||||
/// Additional data expected from the target client.
|
||||
type TargetNoncesData;
|
||||
|
||||
/// Should return true if nothing has to be synced.
|
||||
fn is_empty(&self) -> bool;
|
||||
/// Return best nonce at source node.
|
||||
///
|
||||
/// `Some` is returned only if we are sure that the value is greater or equal
|
||||
/// than the result of `best_at_target`.
|
||||
fn best_at_source(&self) -> Option<MessageNonce>;
|
||||
/// Return best nonce at target node.
|
||||
///
|
||||
/// May return `None` if value is yet unknown.
|
||||
fn best_at_target(&self) -> Option<MessageNonce>;
|
||||
|
||||
/// Called when nonces are updated at source node of the race.
|
||||
fn source_nonces_updated(&mut self, at_block: SourceHeaderId, nonces: SourceClientNonces<Self::SourceNoncesRange>);
|
||||
/// Called when best nonces are updated at target node of the race.
|
||||
fn best_target_nonces_updated(
|
||||
&mut self,
|
||||
nonces: TargetClientNonces<Self::TargetNoncesData>,
|
||||
race_state: &mut RaceState<SourceHeaderId, TargetHeaderId, Proof>,
|
||||
);
|
||||
/// Called when finalized nonces are updated at target node of the race.
|
||||
fn finalized_target_nonces_updated(
|
||||
&mut self,
|
||||
nonces: TargetClientNonces<Self::TargetNoncesData>,
|
||||
race_state: &mut RaceState<SourceHeaderId, TargetHeaderId, Proof>,
|
||||
);
|
||||
/// Should return `Some(nonces)` if we need to deliver proof of `nonces` (and associated
|
||||
/// data) from source to target node.
|
||||
/// Additionally, parameters required to generate proof are returned.
|
||||
fn select_nonces_to_deliver(
|
||||
&mut self,
|
||||
race_state: &RaceState<SourceHeaderId, TargetHeaderId, Proof>,
|
||||
) -> Option<(RangeInclusive<MessageNonce>, Self::ProofParameters)>;
|
||||
}
|
||||
|
||||
/// State of the race.
|
||||
#[derive(Debug)]
|
||||
pub struct RaceState<SourceHeaderId, TargetHeaderId, Proof> {
|
||||
/// Best finalized source header id at the source client.
|
||||
pub best_finalized_source_header_id_at_source: Option<SourceHeaderId>,
|
||||
/// Best finalized source header id at the best block on the target
|
||||
/// client (at the `best_finalized_source_header_id_at_best_target`).
|
||||
pub best_finalized_source_header_id_at_best_target: Option<SourceHeaderId>,
|
||||
/// Best header id at the target client.
|
||||
pub best_target_header_id: Option<TargetHeaderId>,
|
||||
/// Best finalized header id at the target client.
|
||||
pub best_finalized_target_header_id: Option<TargetHeaderId>,
|
||||
/// Range of nonces that we have selected to submit.
|
||||
pub nonces_to_submit: Option<(SourceHeaderId, RangeInclusive<MessageNonce>, Proof)>,
|
||||
/// Range of nonces that is currently submitted.
|
||||
pub nonces_submitted: Option<RangeInclusive<MessageNonce>>,
|
||||
}
|
||||
|
||||
/// Run race loop until connection with target or source node is lost.
|
||||
pub async fn run<P: MessageRace, SC: SourceClient<P>, TC: TargetClient<P>>(
|
||||
race_source: SC,
|
||||
race_source_updated: impl FusedStream<Item = SourceClientState<P>>,
|
||||
race_target: TC,
|
||||
race_target_updated: impl FusedStream<Item = TargetClientState<P>>,
|
||||
stall_timeout: Duration,
|
||||
mut strategy: impl RaceStrategy<
|
||||
P::SourceHeaderId,
|
||||
P::TargetHeaderId,
|
||||
P::Proof,
|
||||
SourceNoncesRange = SC::NoncesRange,
|
||||
ProofParameters = SC::ProofParameters,
|
||||
TargetNoncesData = TC::TargetNoncesData,
|
||||
>,
|
||||
) -> Result<(), FailedClient> {
|
||||
let mut progress_context = Instant::now();
|
||||
let mut race_state = RaceState::default();
|
||||
let mut stall_countdown = Instant::now();
|
||||
|
||||
let mut source_retry_backoff = retry_backoff();
|
||||
let mut source_client_is_online = true;
|
||||
let mut source_nonces_required = false;
|
||||
let source_nonces = futures::future::Fuse::terminated();
|
||||
let source_generate_proof = futures::future::Fuse::terminated();
|
||||
let source_go_offline_future = futures::future::Fuse::terminated();
|
||||
|
||||
let mut target_retry_backoff = retry_backoff();
|
||||
let mut target_client_is_online = true;
|
||||
let mut target_best_nonces_required = false;
|
||||
let mut target_finalized_nonces_required = false;
|
||||
let target_best_nonces = futures::future::Fuse::terminated();
|
||||
let target_finalized_nonces = futures::future::Fuse::terminated();
|
||||
let target_submit_proof = futures::future::Fuse::terminated();
|
||||
let target_go_offline_future = futures::future::Fuse::terminated();
|
||||
|
||||
futures::pin_mut!(
|
||||
race_source_updated,
|
||||
source_nonces,
|
||||
source_generate_proof,
|
||||
source_go_offline_future,
|
||||
race_target_updated,
|
||||
target_best_nonces,
|
||||
target_finalized_nonces,
|
||||
target_submit_proof,
|
||||
target_go_offline_future,
|
||||
);
|
||||
|
||||
loop {
|
||||
futures::select! {
|
||||
// when headers ids are updated
|
||||
source_state = race_source_updated.next() => {
|
||||
if let Some(source_state) = source_state {
|
||||
let is_source_state_updated = race_state.best_finalized_source_header_id_at_source.as_ref()
|
||||
!= Some(&source_state.best_finalized_self);
|
||||
if is_source_state_updated {
|
||||
source_nonces_required = true;
|
||||
race_state.best_finalized_source_header_id_at_source = Some(source_state.best_finalized_self);
|
||||
}
|
||||
}
|
||||
},
|
||||
target_state = race_target_updated.next() => {
|
||||
if let Some(target_state) = target_state {
|
||||
let is_target_best_state_updated = race_state.best_target_header_id.as_ref()
|
||||
!= Some(&target_state.best_self);
|
||||
|
||||
if is_target_best_state_updated {
|
||||
target_best_nonces_required = true;
|
||||
race_state.best_target_header_id = Some(target_state.best_self);
|
||||
race_state.best_finalized_source_header_id_at_best_target
|
||||
= Some(target_state.best_finalized_peer_at_best_self);
|
||||
}
|
||||
|
||||
let is_target_finalized_state_updated = race_state.best_finalized_target_header_id.as_ref()
|
||||
!= Some(&target_state.best_finalized_self);
|
||||
if is_target_finalized_state_updated {
|
||||
target_finalized_nonces_required = true;
|
||||
race_state.best_finalized_target_header_id = Some(target_state.best_finalized_self);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// when nonces are updated
|
||||
nonces = source_nonces => {
|
||||
source_nonces_required = false;
|
||||
|
||||
source_client_is_online = process_future_result(
|
||||
nonces,
|
||||
&mut source_retry_backoff,
|
||||
|(at_block, nonces)| {
|
||||
log::debug!(
|
||||
target: "bridge",
|
||||
"Received nonces from {}: {:?}",
|
||||
P::source_name(),
|
||||
nonces,
|
||||
);
|
||||
|
||||
strategy.source_nonces_updated(at_block, nonces);
|
||||
},
|
||||
&mut source_go_offline_future,
|
||||
async_std::task::sleep,
|
||||
|| format!("Error retrieving nonces from {}", P::source_name()),
|
||||
).fail_if_connection_error(FailedClient::Source)?;
|
||||
},
|
||||
nonces = target_best_nonces => {
|
||||
target_best_nonces_required = false;
|
||||
|
||||
target_client_is_online = process_future_result(
|
||||
nonces,
|
||||
&mut target_retry_backoff,
|
||||
|(_, nonces)| {
|
||||
log::debug!(
|
||||
target: "bridge",
|
||||
"Received best nonces from {}: {:?}",
|
||||
P::target_name(),
|
||||
nonces,
|
||||
);
|
||||
|
||||
let prev_best_at_target = strategy.best_at_target();
|
||||
strategy.best_target_nonces_updated(nonces, &mut race_state);
|
||||
if strategy.best_at_target() != prev_best_at_target {
|
||||
stall_countdown = Instant::now();
|
||||
}
|
||||
},
|
||||
&mut target_go_offline_future,
|
||||
async_std::task::sleep,
|
||||
|| format!("Error retrieving best nonces from {}", P::target_name()),
|
||||
).fail_if_connection_error(FailedClient::Target)?;
|
||||
},
|
||||
nonces = target_finalized_nonces => {
|
||||
target_finalized_nonces_required = false;
|
||||
|
||||
target_client_is_online = process_future_result(
|
||||
nonces,
|
||||
&mut target_retry_backoff,
|
||||
|(_, nonces)| {
|
||||
log::debug!(
|
||||
target: "bridge",
|
||||
"Received finalized nonces from {}: {:?}",
|
||||
P::target_name(),
|
||||
nonces,
|
||||
);
|
||||
|
||||
strategy.finalized_target_nonces_updated(nonces, &mut race_state);
|
||||
},
|
||||
&mut target_go_offline_future,
|
||||
async_std::task::sleep,
|
||||
|| format!("Error retrieving finalized nonces from {}", P::target_name()),
|
||||
).fail_if_connection_error(FailedClient::Target)?;
|
||||
},
|
||||
|
||||
// proof generation and submission
|
||||
proof = source_generate_proof => {
|
||||
source_client_is_online = process_future_result(
|
||||
proof,
|
||||
&mut source_retry_backoff,
|
||||
|(at_block, nonces_range, proof)| {
|
||||
log::debug!(
|
||||
target: "bridge",
|
||||
"Received proof for nonces in range {:?} from {}",
|
||||
nonces_range,
|
||||
P::source_name(),
|
||||
);
|
||||
|
||||
race_state.nonces_to_submit = Some((at_block, nonces_range, proof));
|
||||
},
|
||||
&mut source_go_offline_future,
|
||||
async_std::task::sleep,
|
||||
|| format!("Error generating proof at {}", P::source_name()),
|
||||
).fail_if_connection_error(FailedClient::Source)?;
|
||||
},
|
||||
proof_submit_result = target_submit_proof => {
|
||||
target_client_is_online = process_future_result(
|
||||
proof_submit_result,
|
||||
&mut target_retry_backoff,
|
||||
|nonces_range| {
|
||||
log::debug!(
|
||||
target: "bridge",
|
||||
"Successfully submitted proof of nonces {:?} to {}",
|
||||
nonces_range,
|
||||
P::target_name(),
|
||||
);
|
||||
|
||||
race_state.nonces_to_submit = None;
|
||||
race_state.nonces_submitted = Some(nonces_range);
|
||||
stall_countdown = Instant::now();
|
||||
},
|
||||
&mut target_go_offline_future,
|
||||
async_std::task::sleep,
|
||||
|| format!("Error submitting proof {}", P::target_name()),
|
||||
).fail_if_connection_error(FailedClient::Target)?;
|
||||
},
|
||||
|
||||
// when we're ready to retry request
|
||||
_ = source_go_offline_future => {
|
||||
source_client_is_online = true;
|
||||
},
|
||||
_ = target_go_offline_future => {
|
||||
target_client_is_online = true;
|
||||
},
|
||||
}
|
||||
|
||||
progress_context = print_race_progress::<P, _>(progress_context, &strategy);
|
||||
|
||||
if stall_countdown.elapsed() > stall_timeout {
|
||||
log::warn!(
|
||||
target: "bridge",
|
||||
"{} -> {} race has stalled. State: {:?}. Strategy: {:?}",
|
||||
P::source_name(),
|
||||
P::target_name(),
|
||||
race_state,
|
||||
strategy,
|
||||
);
|
||||
|
||||
return Err(FailedClient::Both);
|
||||
} else if race_state.nonces_to_submit.is_none() && race_state.nonces_submitted.is_none() && strategy.is_empty()
|
||||
{
|
||||
stall_countdown = Instant::now();
|
||||
}
|
||||
|
||||
if source_client_is_online {
|
||||
source_client_is_online = false;
|
||||
|
||||
let nonces_to_deliver = select_nonces_to_deliver(&race_state, &mut strategy);
|
||||
let best_at_source = strategy.best_at_source();
|
||||
|
||||
if let Some((at_block, nonces_range, proof_parameters)) = nonces_to_deliver {
|
||||
log::debug!(
|
||||
target: "bridge",
|
||||
"Asking {} to prove nonces in range {:?} at block {:?}",
|
||||
P::source_name(),
|
||||
nonces_range,
|
||||
at_block,
|
||||
);
|
||||
source_generate_proof.set(
|
||||
race_source
|
||||
.generate_proof(at_block, nonces_range, proof_parameters)
|
||||
.fuse(),
|
||||
);
|
||||
} else if source_nonces_required && best_at_source.is_some() {
|
||||
log::debug!(target: "bridge", "Asking {} about message nonces", P::source_name());
|
||||
let at_block = race_state
|
||||
.best_finalized_source_header_id_at_source
|
||||
.as_ref()
|
||||
.expect(
|
||||
"source_nonces_required is only true when\
|
||||
best_finalized_source_header_id_at_source is Some; qed",
|
||||
)
|
||||
.clone();
|
||||
source_nonces.set(
|
||||
race_source
|
||||
.nonces(at_block, best_at_source.expect("guaranteed by if condition; qed"))
|
||||
.fuse(),
|
||||
);
|
||||
} else {
|
||||
source_client_is_online = true;
|
||||
}
|
||||
}
|
||||
|
||||
if target_client_is_online {
|
||||
target_client_is_online = false;
|
||||
|
||||
if let Some((at_block, nonces_range, proof)) = race_state.nonces_to_submit.as_ref() {
|
||||
log::debug!(
|
||||
target: "bridge",
|
||||
"Going to submit proof of messages in range {:?} to {} node",
|
||||
nonces_range,
|
||||
P::target_name(),
|
||||
);
|
||||
target_submit_proof.set(
|
||||
race_target
|
||||
.submit_proof(at_block.clone(), nonces_range.clone(), proof.clone())
|
||||
.fuse(),
|
||||
);
|
||||
} else if target_best_nonces_required {
|
||||
log::debug!(target: "bridge", "Asking {} about best message nonces", P::target_name());
|
||||
let at_block = race_state
|
||||
.best_target_header_id
|
||||
.as_ref()
|
||||
.expect("target_best_nonces_required is only true when best_target_header_id is Some; qed")
|
||||
.clone();
|
||||
target_best_nonces.set(race_target.nonces(at_block, false).fuse());
|
||||
} else if target_finalized_nonces_required {
|
||||
log::debug!(target: "bridge", "Asking {} about finalized message nonces", P::target_name());
|
||||
let at_block = race_state
|
||||
.best_finalized_target_header_id
|
||||
.as_ref()
|
||||
.expect(
|
||||
"target_finalized_nonces_required is only true when\
|
||||
best_finalized_target_header_id is Some; qed",
|
||||
)
|
||||
.clone();
|
||||
target_finalized_nonces.set(race_target.nonces(at_block, true).fuse());
|
||||
} else {
|
||||
target_client_is_online = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<SourceHeaderId, TargetHeaderId, Proof> Default for RaceState<SourceHeaderId, TargetHeaderId, Proof> {
|
||||
fn default() -> Self {
|
||||
RaceState {
|
||||
best_finalized_source_header_id_at_source: None,
|
||||
best_finalized_source_header_id_at_best_target: None,
|
||||
best_target_header_id: None,
|
||||
best_finalized_target_header_id: None,
|
||||
nonces_to_submit: None,
|
||||
nonces_submitted: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Print race progress.
|
||||
fn print_race_progress<P, S>(prev_time: Instant, strategy: &S) -> Instant
|
||||
where
|
||||
P: MessageRace,
|
||||
S: RaceStrategy<P::SourceHeaderId, P::TargetHeaderId, P::Proof>,
|
||||
{
|
||||
let now_time = Instant::now();
|
||||
|
||||
let need_update = now_time.saturating_duration_since(prev_time) > Duration::from_secs(10);
|
||||
if !need_update {
|
||||
return prev_time;
|
||||
}
|
||||
|
||||
let now_best_nonce_at_source = strategy.best_at_source();
|
||||
let now_best_nonce_at_target = strategy.best_at_target();
|
||||
log::info!(
|
||||
target: "bridge",
|
||||
"Synced {:?} of {:?} nonces in {} -> {} race",
|
||||
now_best_nonce_at_target,
|
||||
now_best_nonce_at_source,
|
||||
P::source_name(),
|
||||
P::target_name(),
|
||||
);
|
||||
now_time
|
||||
}
|
||||
|
||||
fn select_nonces_to_deliver<SourceHeaderId, TargetHeaderId, Proof, Strategy>(
|
||||
race_state: &RaceState<SourceHeaderId, TargetHeaderId, Proof>,
|
||||
strategy: &mut Strategy,
|
||||
) -> Option<(SourceHeaderId, RangeInclusive<MessageNonce>, Strategy::ProofParameters)>
|
||||
where
|
||||
SourceHeaderId: Clone,
|
||||
Strategy: RaceStrategy<SourceHeaderId, TargetHeaderId, Proof>,
|
||||
{
|
||||
race_state
|
||||
.best_finalized_source_header_id_at_best_target
|
||||
.as_ref()
|
||||
.and_then(|best_finalized_source_header_id_at_best_target| {
|
||||
strategy
|
||||
.select_nonces_to_deliver(&race_state)
|
||||
.map(|(nonces_range, proof_parameters)| {
|
||||
(
|
||||
best_finalized_source_header_id_at_best_target.clone(),
|
||||
nonces_range,
|
||||
proof_parameters,
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::message_race_strategy::BasicStrategy;
|
||||
use relay_utils::HeaderId;
|
||||
|
||||
#[test]
|
||||
fn proof_is_generated_at_best_block_known_to_target_node() {
|
||||
const GENERATED_AT: u64 = 6;
|
||||
const BEST_AT_SOURCE: u64 = 10;
|
||||
const BEST_AT_TARGET: u64 = 8;
|
||||
|
||||
// target node only knows about source' BEST_AT_TARGET block
|
||||
// source node has BEST_AT_SOURCE > BEST_AT_TARGET block
|
||||
let mut race_state = RaceState::<_, _, ()> {
|
||||
best_finalized_source_header_id_at_source: Some(HeaderId(BEST_AT_SOURCE, BEST_AT_SOURCE)),
|
||||
best_finalized_source_header_id_at_best_target: Some(HeaderId(BEST_AT_TARGET, BEST_AT_TARGET)),
|
||||
best_target_header_id: Some(HeaderId(0, 0)),
|
||||
best_finalized_target_header_id: Some(HeaderId(0, 0)),
|
||||
nonces_to_submit: None,
|
||||
nonces_submitted: None,
|
||||
};
|
||||
|
||||
// we have some nonces to deliver and they're generated at GENERATED_AT < BEST_AT_SOURCE
|
||||
let mut strategy = BasicStrategy::new();
|
||||
strategy.source_nonces_updated(
|
||||
HeaderId(GENERATED_AT, GENERATED_AT),
|
||||
SourceClientNonces {
|
||||
new_nonces: 0..=10,
|
||||
confirmed_nonce: None,
|
||||
},
|
||||
);
|
||||
strategy.best_target_nonces_updated(
|
||||
TargetClientNonces {
|
||||
latest_nonce: 5u64,
|
||||
nonces_data: (),
|
||||
},
|
||||
&mut race_state,
|
||||
);
|
||||
|
||||
// the proof will be generated on source, but using BEST_AT_TARGET block
|
||||
assert_eq!(
|
||||
select_nonces_to_deliver(&race_state, &mut strategy),
|
||||
Some((HeaderId(BEST_AT_TARGET, BEST_AT_TARGET), 6..=10, (),))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
// Copyright 2019-2020 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.
|
||||
|
||||
//! Message receiving race delivers proof-of-messages-delivery from lane.target to lane.source.
|
||||
|
||||
use crate::message_lane::{MessageLane, SourceHeaderIdOf, TargetHeaderIdOf};
|
||||
use crate::message_lane_loop::{
|
||||
SourceClient as MessageLaneSourceClient, SourceClientState, TargetClient as MessageLaneTargetClient,
|
||||
TargetClientState,
|
||||
};
|
||||
use crate::message_race_loop::{
|
||||
MessageRace, NoncesRange, SourceClient, SourceClientNonces, TargetClient, TargetClientNonces,
|
||||
};
|
||||
use crate::message_race_strategy::BasicStrategy;
|
||||
use crate::metrics::MessageLaneLoopMetrics;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use bp_message_lane::MessageNonce;
|
||||
use futures::stream::FusedStream;
|
||||
use relay_utils::FailedClient;
|
||||
use std::{marker::PhantomData, ops::RangeInclusive, time::Duration};
|
||||
|
||||
/// Message receiving confirmations delivery strategy.
|
||||
type ReceivingConfirmationsBasicStrategy<P> = BasicStrategy<
|
||||
<P as MessageLane>::TargetHeaderNumber,
|
||||
<P as MessageLane>::TargetHeaderHash,
|
||||
<P as MessageLane>::SourceHeaderNumber,
|
||||
<P as MessageLane>::SourceHeaderHash,
|
||||
RangeInclusive<MessageNonce>,
|
||||
<P as MessageLane>::MessagesReceivingProof,
|
||||
>;
|
||||
|
||||
/// Run receiving confirmations race.
|
||||
pub async fn run<P: MessageLane>(
|
||||
source_client: impl MessageLaneSourceClient<P>,
|
||||
source_state_updates: impl FusedStream<Item = SourceClientState<P>>,
|
||||
target_client: impl MessageLaneTargetClient<P>,
|
||||
target_state_updates: impl FusedStream<Item = TargetClientState<P>>,
|
||||
stall_timeout: Duration,
|
||||
metrics_msg: Option<MessageLaneLoopMetrics>,
|
||||
) -> Result<(), FailedClient> {
|
||||
crate::message_race_loop::run(
|
||||
ReceivingConfirmationsRaceSource {
|
||||
client: target_client,
|
||||
metrics_msg: metrics_msg.clone(),
|
||||
_phantom: Default::default(),
|
||||
},
|
||||
target_state_updates,
|
||||
ReceivingConfirmationsRaceTarget {
|
||||
client: source_client,
|
||||
metrics_msg,
|
||||
_phantom: Default::default(),
|
||||
},
|
||||
source_state_updates,
|
||||
stall_timeout,
|
||||
ReceivingConfirmationsBasicStrategy::<P>::new(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Messages receiving confirmations race.
|
||||
struct ReceivingConfirmationsRace<P>(std::marker::PhantomData<P>);
|
||||
|
||||
impl<P: MessageLane> MessageRace for ReceivingConfirmationsRace<P> {
|
||||
type SourceHeaderId = TargetHeaderIdOf<P>;
|
||||
type TargetHeaderId = SourceHeaderIdOf<P>;
|
||||
|
||||
type MessageNonce = MessageNonce;
|
||||
type Proof = P::MessagesReceivingProof;
|
||||
|
||||
fn source_name() -> String {
|
||||
format!("{}::ReceivingConfirmationsDelivery", P::TARGET_NAME)
|
||||
}
|
||||
|
||||
fn target_name() -> String {
|
||||
format!("{}::ReceivingConfirmationsDelivery", P::SOURCE_NAME)
|
||||
}
|
||||
}
|
||||
|
||||
/// Message receiving confirmations race source, which is a target of the lane.
|
||||
struct ReceivingConfirmationsRaceSource<P: MessageLane, C> {
|
||||
client: C,
|
||||
metrics_msg: Option<MessageLaneLoopMetrics>,
|
||||
_phantom: PhantomData<P>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<P, C> SourceClient<ReceivingConfirmationsRace<P>> for ReceivingConfirmationsRaceSource<P, C>
|
||||
where
|
||||
P: MessageLane,
|
||||
C: MessageLaneTargetClient<P>,
|
||||
{
|
||||
type Error = C::Error;
|
||||
type NoncesRange = RangeInclusive<MessageNonce>;
|
||||
type ProofParameters = ();
|
||||
|
||||
async fn nonces(
|
||||
&self,
|
||||
at_block: TargetHeaderIdOf<P>,
|
||||
prev_latest_nonce: MessageNonce,
|
||||
) -> Result<(TargetHeaderIdOf<P>, SourceClientNonces<Self::NoncesRange>), Self::Error> {
|
||||
let (at_block, latest_received_nonce) = self.client.latest_received_nonce(at_block).await?;
|
||||
if let Some(metrics_msg) = self.metrics_msg.as_ref() {
|
||||
metrics_msg.update_target_latest_received_nonce::<P>(latest_received_nonce);
|
||||
}
|
||||
Ok((
|
||||
at_block,
|
||||
SourceClientNonces {
|
||||
new_nonces: prev_latest_nonce + 1..=latest_received_nonce,
|
||||
confirmed_nonce: None,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
#[allow(clippy::unit_arg)]
|
||||
async fn generate_proof(
|
||||
&self,
|
||||
at_block: TargetHeaderIdOf<P>,
|
||||
nonces: RangeInclusive<MessageNonce>,
|
||||
_proof_parameters: Self::ProofParameters,
|
||||
) -> Result<
|
||||
(
|
||||
TargetHeaderIdOf<P>,
|
||||
RangeInclusive<MessageNonce>,
|
||||
P::MessagesReceivingProof,
|
||||
),
|
||||
Self::Error,
|
||||
> {
|
||||
self.client
|
||||
.prove_messages_receiving(at_block)
|
||||
.await
|
||||
.map(|(at_block, proof)| (at_block, nonces, proof))
|
||||
}
|
||||
}
|
||||
|
||||
/// Message receiving confirmations race target, which is a source of the lane.
|
||||
struct ReceivingConfirmationsRaceTarget<P: MessageLane, C> {
|
||||
client: C,
|
||||
metrics_msg: Option<MessageLaneLoopMetrics>,
|
||||
_phantom: PhantomData<P>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<P, C> TargetClient<ReceivingConfirmationsRace<P>> for ReceivingConfirmationsRaceTarget<P, C>
|
||||
where
|
||||
P: MessageLane,
|
||||
C: MessageLaneSourceClient<P>,
|
||||
{
|
||||
type Error = C::Error;
|
||||
type TargetNoncesData = ();
|
||||
|
||||
async fn nonces(
|
||||
&self,
|
||||
at_block: SourceHeaderIdOf<P>,
|
||||
update_metrics: bool,
|
||||
) -> Result<(SourceHeaderIdOf<P>, TargetClientNonces<()>), Self::Error> {
|
||||
let (at_block, latest_confirmed_nonce) = self.client.latest_confirmed_received_nonce(at_block).await?;
|
||||
if update_metrics {
|
||||
if let Some(metrics_msg) = self.metrics_msg.as_ref() {
|
||||
metrics_msg.update_source_latest_confirmed_nonce::<P>(latest_confirmed_nonce);
|
||||
}
|
||||
}
|
||||
Ok((
|
||||
at_block,
|
||||
TargetClientNonces {
|
||||
latest_nonce: latest_confirmed_nonce,
|
||||
nonces_data: (),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
async fn submit_proof(
|
||||
&self,
|
||||
generated_at_block: TargetHeaderIdOf<P>,
|
||||
nonces: RangeInclusive<MessageNonce>,
|
||||
proof: P::MessagesReceivingProof,
|
||||
) -> Result<RangeInclusive<MessageNonce>, Self::Error> {
|
||||
self.client
|
||||
.submit_messages_receiving_proof(generated_at_block, proof)
|
||||
.await?;
|
||||
Ok(nonces)
|
||||
}
|
||||
}
|
||||
|
||||
impl NoncesRange for RangeInclusive<MessageNonce> {
|
||||
fn begin(&self) -> MessageNonce {
|
||||
*RangeInclusive::<MessageNonce>::start(self)
|
||||
}
|
||||
|
||||
fn end(&self) -> MessageNonce {
|
||||
*RangeInclusive::<MessageNonce>::end(self)
|
||||
}
|
||||
|
||||
fn greater_than(self, nonce: MessageNonce) -> Option<Self> {
|
||||
let next_nonce = nonce + 1;
|
||||
let end = *self.end();
|
||||
if next_nonce > end {
|
||||
None
|
||||
} else {
|
||||
Some(std::cmp::max(self.begin(), next_nonce)..=end)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn range_inclusive_works_as_nonces_range() {
|
||||
let range = 20..=30;
|
||||
|
||||
assert_eq!(NoncesRange::begin(&range), 20);
|
||||
assert_eq!(NoncesRange::end(&range), 30);
|
||||
assert_eq!(range.clone().greater_than(10), Some(20..=30));
|
||||
assert_eq!(range.clone().greater_than(19), Some(20..=30));
|
||||
assert_eq!(range.clone().greater_than(20), Some(21..=30));
|
||||
assert_eq!(range.clone().greater_than(25), Some(26..=30));
|
||||
assert_eq!(range.clone().greater_than(29), Some(30..=30));
|
||||
assert_eq!(range.greater_than(30), None);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,479 @@
|
||||
// Copyright 2019-2020 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.
|
||||
|
||||
//! Basic delivery strategy. The strategy selects nonces if:
|
||||
//!
|
||||
//! 1) there are more nonces on the source side than on the target side;
|
||||
//! 2) new nonces may be proved to target node (i.e. they have appeared at the
|
||||
//! block, which is known to the target node).
|
||||
|
||||
use crate::message_race_loop::{NoncesRange, RaceState, RaceStrategy, SourceClientNonces, TargetClientNonces};
|
||||
|
||||
use bp_message_lane::MessageNonce;
|
||||
use relay_utils::HeaderId;
|
||||
use std::{collections::VecDeque, fmt::Debug, marker::PhantomData, ops::RangeInclusive};
|
||||
|
||||
/// Nonces delivery strategy.
|
||||
#[derive(Debug)]
|
||||
pub struct BasicStrategy<
|
||||
SourceHeaderNumber,
|
||||
SourceHeaderHash,
|
||||
TargetHeaderNumber,
|
||||
TargetHeaderHash,
|
||||
SourceNoncesRange,
|
||||
Proof,
|
||||
> {
|
||||
/// All queued nonces.
|
||||
source_queue: VecDeque<(HeaderId<SourceHeaderHash, SourceHeaderNumber>, SourceNoncesRange)>,
|
||||
/// Best nonce known to target node (at its best block). `None` if it has not been received yet.
|
||||
best_target_nonce: Option<MessageNonce>,
|
||||
/// Unused generic types dump.
|
||||
_phantom: PhantomData<(TargetHeaderNumber, TargetHeaderHash, Proof)>,
|
||||
}
|
||||
|
||||
impl<SourceHeaderNumber, SourceHeaderHash, TargetHeaderNumber, TargetHeaderHash, SourceNoncesRange, Proof>
|
||||
BasicStrategy<SourceHeaderNumber, SourceHeaderHash, TargetHeaderNumber, TargetHeaderHash, SourceNoncesRange, Proof>
|
||||
where
|
||||
SourceHeaderHash: Clone,
|
||||
SourceHeaderNumber: Clone + Ord,
|
||||
SourceNoncesRange: NoncesRange,
|
||||
{
|
||||
/// Create new delivery strategy.
|
||||
pub fn new() -> Self {
|
||||
BasicStrategy {
|
||||
source_queue: VecDeque::new(),
|
||||
best_target_nonce: None,
|
||||
_phantom: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Mutable reference to source queue to use in tests.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn source_queue_mut(
|
||||
&mut self,
|
||||
) -> &mut VecDeque<(HeaderId<SourceHeaderHash, SourceHeaderNumber>, SourceNoncesRange)> {
|
||||
&mut self.source_queue
|
||||
}
|
||||
|
||||
/// Should return `Some(nonces)` if we need to deliver proof of `nonces` (and associated
|
||||
/// data) from source to target node.
|
||||
///
|
||||
/// The `selector` function receives range of nonces and should return `None` if the whole
|
||||
/// range needs to be delivered. If there are some nonces in the range that can't be delivered
|
||||
/// right now, it should return `Some` with 'undeliverable' nonces. Please keep in mind that
|
||||
/// this should be the sub-range that the passed range ends with, because nonces are always
|
||||
/// delivered in-order. Otherwise the function will panic.
|
||||
pub fn select_nonces_to_deliver_with_selector(
|
||||
&mut self,
|
||||
race_state: &RaceState<
|
||||
HeaderId<SourceHeaderHash, SourceHeaderNumber>,
|
||||
HeaderId<TargetHeaderHash, TargetHeaderNumber>,
|
||||
Proof,
|
||||
>,
|
||||
mut selector: impl FnMut(SourceNoncesRange) -> Option<SourceNoncesRange>,
|
||||
) -> Option<RangeInclusive<MessageNonce>> {
|
||||
// if we do not know best nonce at target node, we can't select anything
|
||||
let target_nonce = self.best_target_nonce?;
|
||||
|
||||
// if we have already selected nonces that we want to submit, do nothing
|
||||
if race_state.nonces_to_submit.is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// if we already submitted some nonces, do nothing
|
||||
if race_state.nonces_submitted.is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// 1) we want to deliver all nonces, starting from `target_nonce + 1`
|
||||
// 2) we can't deliver new nonce until header, that has emitted this nonce, is finalized
|
||||
// by target client
|
||||
// 3) selector is used for more complicated logic
|
||||
let best_header_at_target = &race_state.best_finalized_source_header_id_at_best_target.as_ref()?;
|
||||
let mut nonces_end = None;
|
||||
while let Some((queued_at, queued_range)) = self.source_queue.pop_front() {
|
||||
// select (sub) range to deliver
|
||||
let queued_range_begin = queued_range.begin();
|
||||
let queued_range_end = queued_range.end();
|
||||
let range_to_requeue = if queued_at.0 > best_header_at_target.0 {
|
||||
// if header that has queued the range is not yet finalized at bridged chain,
|
||||
// we can't prove anything
|
||||
Some(queued_range)
|
||||
} else {
|
||||
// selector returns `Some(range)` if this `range` needs to be requeued
|
||||
selector(queued_range)
|
||||
};
|
||||
|
||||
// requeue (sub) range and update range to deliver
|
||||
match range_to_requeue {
|
||||
Some(range_to_requeue) => {
|
||||
assert!(
|
||||
range_to_requeue.begin() <= range_to_requeue.end()
|
||||
&& range_to_requeue.begin() >= queued_range_begin
|
||||
&& range_to_requeue.end() == queued_range_end,
|
||||
"Incorrect implementation of internal `selector` function. Expected original\
|
||||
range {:?} to end with returned range {:?}",
|
||||
queued_range_begin..=queued_range_end,
|
||||
range_to_requeue,
|
||||
);
|
||||
|
||||
if range_to_requeue.begin() != queued_range_begin {
|
||||
nonces_end = Some(range_to_requeue.begin() - 1);
|
||||
}
|
||||
self.source_queue.push_front((queued_at, range_to_requeue));
|
||||
break;
|
||||
}
|
||||
None => {
|
||||
nonces_end = Some(queued_range_end);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nonces_end.map(|nonces_end| RangeInclusive::new(target_nonce + 1, nonces_end))
|
||||
}
|
||||
}
|
||||
|
||||
impl<SourceHeaderNumber, SourceHeaderHash, TargetHeaderNumber, TargetHeaderHash, SourceNoncesRange, Proof>
|
||||
RaceStrategy<HeaderId<SourceHeaderHash, SourceHeaderNumber>, HeaderId<TargetHeaderHash, TargetHeaderNumber>, Proof>
|
||||
for BasicStrategy<SourceHeaderNumber, SourceHeaderHash, TargetHeaderNumber, TargetHeaderHash, SourceNoncesRange, Proof>
|
||||
where
|
||||
SourceHeaderHash: Clone + Debug,
|
||||
SourceHeaderNumber: Clone + Ord + Debug,
|
||||
SourceNoncesRange: NoncesRange + Debug,
|
||||
TargetHeaderHash: Debug,
|
||||
TargetHeaderNumber: Debug,
|
||||
Proof: Debug,
|
||||
{
|
||||
type SourceNoncesRange = SourceNoncesRange;
|
||||
type ProofParameters = ();
|
||||
type TargetNoncesData = ();
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
self.source_queue.is_empty()
|
||||
}
|
||||
|
||||
fn best_at_source(&self) -> Option<MessageNonce> {
|
||||
let best_in_queue = self.source_queue.back().map(|(_, range)| range.end());
|
||||
match (best_in_queue, self.best_target_nonce) {
|
||||
(Some(best_in_queue), Some(best_target_nonce)) if best_in_queue > best_target_nonce => Some(best_in_queue),
|
||||
(_, Some(best_target_nonce)) => Some(best_target_nonce),
|
||||
(_, None) => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn best_at_target(&self) -> Option<MessageNonce> {
|
||||
self.best_target_nonce
|
||||
}
|
||||
|
||||
fn source_nonces_updated(
|
||||
&mut self,
|
||||
at_block: HeaderId<SourceHeaderHash, SourceHeaderNumber>,
|
||||
nonces: SourceClientNonces<SourceNoncesRange>,
|
||||
) {
|
||||
let best_in_queue = self
|
||||
.source_queue
|
||||
.back()
|
||||
.map(|(_, range)| range.end())
|
||||
.or(self.best_target_nonce)
|
||||
.unwrap_or_default();
|
||||
self.source_queue.extend(
|
||||
nonces
|
||||
.new_nonces
|
||||
.greater_than(best_in_queue)
|
||||
.into_iter()
|
||||
.map(move |range| (at_block.clone(), range)),
|
||||
)
|
||||
}
|
||||
|
||||
fn best_target_nonces_updated(
|
||||
&mut self,
|
||||
nonces: TargetClientNonces<()>,
|
||||
race_state: &mut RaceState<
|
||||
HeaderId<SourceHeaderHash, SourceHeaderNumber>,
|
||||
HeaderId<TargetHeaderHash, TargetHeaderNumber>,
|
||||
Proof,
|
||||
>,
|
||||
) {
|
||||
let nonce = nonces.latest_nonce;
|
||||
|
||||
if let Some(best_target_nonce) = self.best_target_nonce {
|
||||
if nonce < best_target_nonce {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
while let Some(true) = self.source_queue.front().map(|(_, range)| range.begin() <= nonce) {
|
||||
let maybe_subrange = self
|
||||
.source_queue
|
||||
.pop_front()
|
||||
.and_then(|(at_block, range)| range.greater_than(nonce).map(|subrange| (at_block, subrange)));
|
||||
if let Some((at_block, subrange)) = maybe_subrange {
|
||||
self.source_queue.push_front((at_block, subrange));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let need_to_select_new_nonces = race_state
|
||||
.nonces_to_submit
|
||||
.as_ref()
|
||||
.map(|(_, nonces, _)| *nonces.end() <= nonce)
|
||||
.unwrap_or(false);
|
||||
if need_to_select_new_nonces {
|
||||
race_state.nonces_to_submit = None;
|
||||
}
|
||||
|
||||
let need_new_nonces_to_submit = race_state
|
||||
.nonces_submitted
|
||||
.as_ref()
|
||||
.map(|nonces| *nonces.end() <= nonce)
|
||||
.unwrap_or(false);
|
||||
if need_new_nonces_to_submit {
|
||||
race_state.nonces_submitted = None;
|
||||
}
|
||||
|
||||
self.best_target_nonce = Some(std::cmp::max(
|
||||
self.best_target_nonce.unwrap_or(nonces.latest_nonce),
|
||||
nonce,
|
||||
));
|
||||
}
|
||||
|
||||
fn finalized_target_nonces_updated(
|
||||
&mut self,
|
||||
nonces: TargetClientNonces<()>,
|
||||
_race_state: &mut RaceState<
|
||||
HeaderId<SourceHeaderHash, SourceHeaderNumber>,
|
||||
HeaderId<TargetHeaderHash, TargetHeaderNumber>,
|
||||
Proof,
|
||||
>,
|
||||
) {
|
||||
self.best_target_nonce = Some(std::cmp::max(
|
||||
self.best_target_nonce.unwrap_or(nonces.latest_nonce),
|
||||
nonces.latest_nonce,
|
||||
));
|
||||
}
|
||||
|
||||
fn select_nonces_to_deliver(
|
||||
&mut self,
|
||||
race_state: &RaceState<
|
||||
HeaderId<SourceHeaderHash, SourceHeaderNumber>,
|
||||
HeaderId<TargetHeaderHash, TargetHeaderNumber>,
|
||||
Proof,
|
||||
>,
|
||||
) -> Option<(RangeInclusive<MessageNonce>, Self::ProofParameters)> {
|
||||
self.select_nonces_to_deliver_with_selector(race_state, |_| None)
|
||||
.map(|range| (range, ()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::message_lane::MessageLane;
|
||||
use crate::message_lane_loop::tests::{header_id, TestMessageLane, TestMessagesProof};
|
||||
|
||||
type SourceNoncesRange = RangeInclusive<MessageNonce>;
|
||||
|
||||
type BasicStrategy<P> = super::BasicStrategy<
|
||||
<P as MessageLane>::SourceHeaderNumber,
|
||||
<P as MessageLane>::SourceHeaderHash,
|
||||
<P as MessageLane>::TargetHeaderNumber,
|
||||
<P as MessageLane>::TargetHeaderHash,
|
||||
SourceNoncesRange,
|
||||
<P as MessageLane>::MessagesProof,
|
||||
>;
|
||||
|
||||
fn source_nonces(new_nonces: SourceNoncesRange) -> SourceClientNonces<SourceNoncesRange> {
|
||||
SourceClientNonces {
|
||||
new_nonces,
|
||||
confirmed_nonce: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn target_nonces(latest_nonce: MessageNonce) -> TargetClientNonces<()> {
|
||||
TargetClientNonces {
|
||||
latest_nonce,
|
||||
nonces_data: (),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strategy_is_empty_works() {
|
||||
let mut strategy = BasicStrategy::<TestMessageLane>::new();
|
||||
assert_eq!(strategy.is_empty(), true);
|
||||
strategy.source_nonces_updated(header_id(1), source_nonces(1..=1));
|
||||
assert_eq!(strategy.is_empty(), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn best_at_source_is_never_lower_than_target_nonce() {
|
||||
let mut strategy = BasicStrategy::<TestMessageLane>::new();
|
||||
assert_eq!(strategy.best_at_source(), None);
|
||||
strategy.source_nonces_updated(header_id(1), source_nonces(1..=5));
|
||||
assert_eq!(strategy.best_at_source(), None);
|
||||
strategy.best_target_nonces_updated(target_nonces(10), &mut Default::default());
|
||||
assert_eq!(strategy.source_queue, vec![]);
|
||||
assert_eq!(strategy.best_at_source(), Some(10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_nonce_is_never_lower_than_known_target_nonce() {
|
||||
let mut strategy = BasicStrategy::<TestMessageLane>::new();
|
||||
strategy.best_target_nonces_updated(target_nonces(10), &mut Default::default());
|
||||
strategy.source_nonces_updated(header_id(1), source_nonces(1..=5));
|
||||
assert_eq!(strategy.source_queue, vec![]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_nonce_is_never_lower_than_latest_known_source_nonce() {
|
||||
let mut strategy = BasicStrategy::<TestMessageLane>::new();
|
||||
strategy.source_nonces_updated(header_id(1), source_nonces(1..=5));
|
||||
strategy.source_nonces_updated(header_id(2), source_nonces(1..=3));
|
||||
strategy.source_nonces_updated(header_id(2), source_nonces(1..=5));
|
||||
assert_eq!(strategy.source_queue, vec![(header_id(1), 1..=5)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn target_nonce_is_never_lower_than_latest_known_target_nonce() {
|
||||
let mut strategy = BasicStrategy::<TestMessageLane>::new();
|
||||
assert_eq!(strategy.best_target_nonce, None);
|
||||
strategy.best_target_nonces_updated(target_nonces(10), &mut Default::default());
|
||||
assert_eq!(strategy.best_target_nonce, Some(10));
|
||||
strategy.best_target_nonces_updated(target_nonces(5), &mut Default::default());
|
||||
assert_eq!(strategy.best_target_nonce, Some(10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn updated_target_nonce_removes_queued_entries() {
|
||||
let mut strategy = BasicStrategy::<TestMessageLane>::new();
|
||||
strategy.source_nonces_updated(header_id(1), source_nonces(1..=5));
|
||||
strategy.source_nonces_updated(header_id(2), source_nonces(6..=10));
|
||||
strategy.source_nonces_updated(header_id(3), source_nonces(11..=15));
|
||||
strategy.source_nonces_updated(header_id(4), source_nonces(16..=20));
|
||||
strategy.best_target_nonces_updated(target_nonces(15), &mut Default::default());
|
||||
assert_eq!(strategy.source_queue, vec![(header_id(4), 16..=20)]);
|
||||
strategy.best_target_nonces_updated(target_nonces(17), &mut Default::default());
|
||||
assert_eq!(strategy.source_queue, vec![(header_id(4), 18..=20)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selected_nonces_are_dropped_on_target_nonce_update() {
|
||||
let mut state = RaceState::default();
|
||||
let mut strategy = BasicStrategy::<TestMessageLane>::new();
|
||||
state.nonces_to_submit = Some((header_id(1), 5..=10, (5..=10, None)));
|
||||
strategy.best_target_nonces_updated(target_nonces(7), &mut state);
|
||||
assert!(state.nonces_to_submit.is_some());
|
||||
strategy.best_target_nonces_updated(target_nonces(10), &mut state);
|
||||
assert!(state.nonces_to_submit.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn submitted_nonces_are_dropped_on_target_nonce_update() {
|
||||
let mut state = RaceState::default();
|
||||
let mut strategy = BasicStrategy::<TestMessageLane>::new();
|
||||
state.nonces_submitted = Some(5..=10);
|
||||
strategy.best_target_nonces_updated(target_nonces(7), &mut state);
|
||||
assert!(state.nonces_submitted.is_some());
|
||||
strategy.best_target_nonces_updated(target_nonces(10), &mut state);
|
||||
assert!(state.nonces_submitted.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nothing_is_selected_if_something_is_already_selected() {
|
||||
let mut state = RaceState::default();
|
||||
let mut strategy = BasicStrategy::<TestMessageLane>::new();
|
||||
state.nonces_to_submit = Some((header_id(1), 1..=10, (1..=10, None)));
|
||||
strategy.best_target_nonces_updated(target_nonces(0), &mut state);
|
||||
strategy.source_nonces_updated(header_id(1), source_nonces(1..=10));
|
||||
assert_eq!(strategy.select_nonces_to_deliver(&state), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nothing_is_selected_if_something_is_already_submitted() {
|
||||
let mut state = RaceState::default();
|
||||
let mut strategy = BasicStrategy::<TestMessageLane>::new();
|
||||
state.nonces_submitted = Some(1..=10);
|
||||
strategy.best_target_nonces_updated(target_nonces(0), &mut state);
|
||||
strategy.source_nonces_updated(header_id(1), source_nonces(1..=10));
|
||||
assert_eq!(strategy.select_nonces_to_deliver(&state), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_nonces_to_deliver_works() {
|
||||
let mut state = RaceState::<_, _, TestMessagesProof>::default();
|
||||
let mut strategy = BasicStrategy::<TestMessageLane>::new();
|
||||
strategy.best_target_nonces_updated(target_nonces(0), &mut state);
|
||||
strategy.source_nonces_updated(header_id(1), source_nonces(1..=1));
|
||||
strategy.source_nonces_updated(header_id(2), source_nonces(2..=2));
|
||||
strategy.source_nonces_updated(header_id(3), source_nonces(3..=6));
|
||||
strategy.source_nonces_updated(header_id(5), source_nonces(7..=8));
|
||||
|
||||
state.best_finalized_source_header_id_at_best_target = Some(header_id(4));
|
||||
assert_eq!(strategy.select_nonces_to_deliver(&state), Some((1..=6, ())));
|
||||
strategy.best_target_nonces_updated(target_nonces(6), &mut state);
|
||||
assert_eq!(strategy.select_nonces_to_deliver(&state), None);
|
||||
|
||||
state.best_finalized_source_header_id_at_best_target = Some(header_id(5));
|
||||
assert_eq!(strategy.select_nonces_to_deliver(&state), Some((7..=8, ())));
|
||||
strategy.best_target_nonces_updated(target_nonces(8), &mut state);
|
||||
assert_eq!(strategy.select_nonces_to_deliver(&state), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_nonces_to_deliver_able_to_split_ranges_with_selector() {
|
||||
let mut state = RaceState::<_, _, TestMessagesProof>::default();
|
||||
let mut strategy = BasicStrategy::<TestMessageLane>::new();
|
||||
strategy.best_target_nonces_updated(target_nonces(0), &mut state);
|
||||
strategy.source_nonces_updated(header_id(1), source_nonces(1..=100));
|
||||
|
||||
state.best_finalized_source_header_id_at_source = Some(header_id(1));
|
||||
state.best_finalized_source_header_id_at_best_target = Some(header_id(1));
|
||||
state.best_target_header_id = Some(header_id(1));
|
||||
|
||||
assert_eq!(
|
||||
strategy.select_nonces_to_deliver_with_selector(&state, |_| Some(50..=100)),
|
||||
Some(1..=49),
|
||||
);
|
||||
}
|
||||
|
||||
fn run_panic_test_for_incorrect_selector(
|
||||
invalid_selector: impl Fn(SourceNoncesRange) -> Option<SourceNoncesRange>,
|
||||
) {
|
||||
let mut state = RaceState::<_, _, TestMessagesProof>::default();
|
||||
let mut strategy = BasicStrategy::<TestMessageLane>::new();
|
||||
strategy.source_nonces_updated(header_id(1), source_nonces(1..=100));
|
||||
strategy.best_target_nonces_updated(target_nonces(50), &mut state);
|
||||
state.best_finalized_source_header_id_at_source = Some(header_id(1));
|
||||
state.best_finalized_source_header_id_at_best_target = Some(header_id(1));
|
||||
state.best_target_header_id = Some(header_id(1));
|
||||
strategy.select_nonces_to_deliver_with_selector(&state, invalid_selector);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn select_nonces_to_deliver_panics_if_selector_returns_empty_range() {
|
||||
#[allow(clippy::reversed_empty_ranges)]
|
||||
run_panic_test_for_incorrect_selector(|_| Some(2..=1))
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn select_nonces_to_deliver_panics_if_selector_returns_range_that_starts_before_passed_range() {
|
||||
run_panic_test_for_incorrect_selector(|range| Some(range.begin() - 1..=*range.end()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn select_nonces_to_deliver_panics_if_selector_returns_range_with_mismatched_end() {
|
||||
run_panic_test_for_incorrect_selector(|range| Some(range.begin()..=*range.end() + 1))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
// Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Metrics for message lane relay loop.
|
||||
|
||||
use crate::message_lane::MessageLane;
|
||||
use crate::message_lane_loop::{SourceClientState, TargetClientState};
|
||||
|
||||
use bp_message_lane::MessageNonce;
|
||||
use relay_utils::metrics::{register, GaugeVec, Metrics, Opts, Registry, U64};
|
||||
|
||||
/// Message lane relay metrics.
|
||||
///
|
||||
/// Cloning only clones references.
|
||||
#[derive(Clone)]
|
||||
pub struct MessageLaneLoopMetrics {
|
||||
/// Best finalized block numbers - "source", "target", "source_at_target", "target_at_source".
|
||||
best_block_numbers: GaugeVec<U64>,
|
||||
/// Lane state nonces: "source_latest_generated", "source_latest_confirmed",
|
||||
/// "target_latest_received", "target_latest_confirmed".
|
||||
lane_state_nonces: GaugeVec<U64>,
|
||||
}
|
||||
|
||||
impl Metrics for MessageLaneLoopMetrics {
|
||||
fn register(&self, registry: &Registry) -> Result<(), String> {
|
||||
register(self.best_block_numbers.clone(), registry).map_err(|e| e.to_string())?;
|
||||
register(self.lane_state_nonces.clone(), registry).map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MessageLaneLoopMetrics {
|
||||
fn default() -> Self {
|
||||
MessageLaneLoopMetrics {
|
||||
best_block_numbers: GaugeVec::new(
|
||||
Opts::new("best_block_numbers", "Best finalized block numbers"),
|
||||
&["type"],
|
||||
)
|
||||
.expect("metric is static and thus valid; qed"),
|
||||
lane_state_nonces: GaugeVec::new(Opts::new("lane_state_nonces", "Nonces of the lane state"), &["type"])
|
||||
.expect("metric is static and thus valid; qed"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageLaneLoopMetrics {
|
||||
/// Update source client state metrics.
|
||||
pub fn update_source_state<P: MessageLane>(&self, source_client_state: SourceClientState<P>) {
|
||||
self.best_block_numbers
|
||||
.with_label_values(&["source"])
|
||||
.set(source_client_state.best_self.0.into());
|
||||
self.best_block_numbers
|
||||
.with_label_values(&["target_at_source"])
|
||||
.set(source_client_state.best_finalized_peer_at_best_self.0.into());
|
||||
}
|
||||
|
||||
/// Update target client state metrics.
|
||||
pub fn update_target_state<P: MessageLane>(&self, target_client_state: TargetClientState<P>) {
|
||||
self.best_block_numbers
|
||||
.with_label_values(&["target"])
|
||||
.set(target_client_state.best_self.0.into());
|
||||
self.best_block_numbers
|
||||
.with_label_values(&["source_at_target"])
|
||||
.set(target_client_state.best_finalized_peer_at_best_self.0.into());
|
||||
}
|
||||
|
||||
/// Update latest generated nonce at source.
|
||||
pub fn update_source_latest_generated_nonce<P: MessageLane>(&self, source_latest_generated_nonce: MessageNonce) {
|
||||
self.lane_state_nonces
|
||||
.with_label_values(&["source_latest_generated"])
|
||||
.set(source_latest_generated_nonce);
|
||||
}
|
||||
|
||||
/// Update latest confirmed nonce at source.
|
||||
pub fn update_source_latest_confirmed_nonce<P: MessageLane>(&self, source_latest_confirmed_nonce: MessageNonce) {
|
||||
self.lane_state_nonces
|
||||
.with_label_values(&["source_latest_confirmed"])
|
||||
.set(source_latest_confirmed_nonce);
|
||||
}
|
||||
|
||||
/// Update latest received nonce at target.
|
||||
pub fn update_target_latest_received_nonce<P: MessageLane>(&self, target_latest_generated_nonce: MessageNonce) {
|
||||
self.lane_state_nonces
|
||||
.with_label_values(&["target_latest_received"])
|
||||
.set(target_latest_generated_nonce);
|
||||
}
|
||||
|
||||
/// Update latest confirmed nonce at target.
|
||||
pub fn update_target_latest_confirmed_nonce<P: MessageLane>(&self, target_latest_confirmed_nonce: MessageNonce) {
|
||||
self.lane_state_nonces
|
||||
.with_label_values(&["target_latest_confirmed"])
|
||||
.set(target_latest_confirmed_nonce);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "relay-millau-client"
|
||||
version = "0.1.0"
|
||||
authors = ["Parity Technologies <admin@parity.io>"]
|
||||
edition = "2018"
|
||||
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
|
||||
|
||||
[dependencies]
|
||||
codec = { package = "parity-scale-codec", version = "2.0.0" }
|
||||
headers-relay = { path = "../headers-relay" }
|
||||
relay-substrate-client = { path = "../substrate-client" }
|
||||
relay-utils = { path = "../utils" }
|
||||
|
||||
# Supported Chains
|
||||
|
||||
millau-runtime = { path = "../../bin/millau/runtime" }
|
||||
|
||||
# Substrate Dependencies
|
||||
|
||||
frame-support = { git = "https://github.com/paritytech/substrate.git", branch = "master" }
|
||||
frame-system = { git = "https://github.com/paritytech/substrate.git", branch = "master" }
|
||||
pallet-transaction-payment = { git = "https://github.com/paritytech/substrate.git", branch = "master" }
|
||||
sp-core = { git = "https://github.com/paritytech/substrate.git", branch = "master" }
|
||||
sp-keyring = { git = "https://github.com/paritytech/substrate.git", branch = "master" }
|
||||
sp-runtime = { git = "https://github.com/paritytech/substrate.git", branch = "master" }
|
||||
@@ -0,0 +1,125 @@
|
||||
// Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Types used to connect to the Millau-Substrate chain.
|
||||
|
||||
use codec::Encode;
|
||||
use relay_substrate_client::{Chain, ChainBase, ChainWithBalances, Client, TransactionSignScheme};
|
||||
use sp_core::{storage::StorageKey, Pair};
|
||||
use sp_runtime::{generic::SignedPayload, traits::IdentifyAccount};
|
||||
use std::time::Duration;
|
||||
|
||||
pub use millau_runtime::BridgeRialtoCall;
|
||||
|
||||
/// Millau header id.
|
||||
pub type HeaderId = relay_utils::HeaderId<millau_runtime::Hash, millau_runtime::BlockNumber>;
|
||||
|
||||
/// Millau chain definition.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Millau;
|
||||
|
||||
impl ChainBase for Millau {
|
||||
type BlockNumber = millau_runtime::BlockNumber;
|
||||
type Hash = millau_runtime::Hash;
|
||||
type Hasher = millau_runtime::Hashing;
|
||||
type Header = millau_runtime::Header;
|
||||
}
|
||||
|
||||
impl Chain for Millau {
|
||||
const NAME: &'static str = "Millau";
|
||||
const AVERAGE_BLOCK_INTERVAL: Duration = Duration::from_secs(5);
|
||||
|
||||
type AccountId = millau_runtime::AccountId;
|
||||
type Index = millau_runtime::Index;
|
||||
type SignedBlock = millau_runtime::SignedBlock;
|
||||
type Call = millau_runtime::Call;
|
||||
}
|
||||
|
||||
impl ChainWithBalances for Millau {
|
||||
type NativeBalance = millau_runtime::Balance;
|
||||
|
||||
fn account_info_storage_key(account_id: &Self::AccountId) -> StorageKey {
|
||||
use frame_support::storage::generator::StorageMap;
|
||||
StorageKey(frame_system::Account::<millau_runtime::Runtime>::storage_map_final_key(
|
||||
account_id,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl TransactionSignScheme for Millau {
|
||||
type Chain = Millau;
|
||||
type AccountKeyPair = sp_core::sr25519::Pair;
|
||||
type SignedTransaction = millau_runtime::UncheckedExtrinsic;
|
||||
|
||||
fn sign_transaction(
|
||||
client: &Client<Self>,
|
||||
signer: &Self::AccountKeyPair,
|
||||
signer_nonce: <Self::Chain as Chain>::Index,
|
||||
call: <Self::Chain as Chain>::Call,
|
||||
) -> Self::SignedTransaction {
|
||||
let raw_payload = SignedPayload::from_raw(
|
||||
call,
|
||||
(
|
||||
frame_system::CheckSpecVersion::<millau_runtime::Runtime>::new(),
|
||||
frame_system::CheckTxVersion::<millau_runtime::Runtime>::new(),
|
||||
frame_system::CheckGenesis::<millau_runtime::Runtime>::new(),
|
||||
frame_system::CheckEra::<millau_runtime::Runtime>::from(sp_runtime::generic::Era::Immortal),
|
||||
frame_system::CheckNonce::<millau_runtime::Runtime>::from(signer_nonce),
|
||||
frame_system::CheckWeight::<millau_runtime::Runtime>::new(),
|
||||
pallet_transaction_payment::ChargeTransactionPayment::<millau_runtime::Runtime>::from(0),
|
||||
),
|
||||
(
|
||||
millau_runtime::VERSION.spec_version,
|
||||
millau_runtime::VERSION.transaction_version,
|
||||
*client.genesis_hash(),
|
||||
*client.genesis_hash(),
|
||||
(),
|
||||
(),
|
||||
(),
|
||||
),
|
||||
);
|
||||
let signature = raw_payload.using_encoded(|payload| signer.sign(payload));
|
||||
let signer: sp_runtime::MultiSigner = signer.public().into();
|
||||
let (call, extra, _) = raw_payload.deconstruct();
|
||||
|
||||
millau_runtime::UncheckedExtrinsic::new_signed(call, signer.into_account(), signature.into(), extra)
|
||||
}
|
||||
}
|
||||
|
||||
/// Millau signing params.
|
||||
#[derive(Clone)]
|
||||
pub struct SigningParams {
|
||||
/// Substrate transactions signer.
|
||||
pub signer: sp_core::sr25519::Pair,
|
||||
}
|
||||
|
||||
impl SigningParams {
|
||||
/// Create signing params from SURI and password.
|
||||
pub fn from_suri(suri: &str, password: Option<&str>) -> Result<Self, sp_core::crypto::SecretStringError> {
|
||||
Ok(SigningParams {
|
||||
signer: sp_core::sr25519::Pair::from_string(suri, password)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for SigningParams {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(f, "{}", self.signer.public())
|
||||
}
|
||||
}
|
||||
|
||||
/// Millau header type used in headers sync.
|
||||
pub type SyncHeader = relay_substrate_client::SyncHeader<millau_runtime::Header>;
|
||||
@@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "relay-polkadot-client"
|
||||
version = "0.1.0"
|
||||
authors = ["Parity Technologies <admin@parity.io>"]
|
||||
edition = "2018"
|
||||
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
|
||||
|
||||
[dependencies]
|
||||
codec = { package = "parity-scale-codec", version = "2.0.0" }
|
||||
headers-relay = { path = "../headers-relay" }
|
||||
relay-substrate-client = { path = "../substrate-client" }
|
||||
relay-utils = { path = "../utils" }
|
||||
|
||||
# Bridge dependencies
|
||||
|
||||
bp-polkadot = { path = "../../primitives/polkadot" }
|
||||
|
||||
# Substrate Dependencies
|
||||
|
||||
frame-system = { git = "https://github.com/paritytech/substrate.git", branch = "master" }
|
||||
frame-support = { git = "https://github.com/paritytech/substrate.git", branch = "master" }
|
||||
pallet-transaction-payment = { git = "https://github.com/paritytech/substrate.git", branch = "master" }
|
||||
sp-core = { git = "https://github.com/paritytech/substrate.git", branch = "master" }
|
||||
sp-keyring = { git = "https://github.com/paritytech/substrate.git", branch = "master" }
|
||||
sp-runtime = { git = "https://github.com/paritytech/substrate.git", branch = "master" }
|
||||
@@ -0,0 +1,47 @@
|
||||
// Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Types used to connect to the Polkadot chain.
|
||||
|
||||
use relay_substrate_client::{Chain, ChainBase};
|
||||
use std::time::Duration;
|
||||
|
||||
/// Polkadot header id.
|
||||
pub type HeaderId = relay_utils::HeaderId<bp_polkadot::Hash, bp_polkadot::BlockNumber>;
|
||||
|
||||
/// Polkadot chain definition
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Polkadot;
|
||||
|
||||
impl ChainBase for Polkadot {
|
||||
type BlockNumber = bp_polkadot::BlockNumber;
|
||||
type Hash = bp_polkadot::Hash;
|
||||
type Hasher = bp_polkadot::Hasher;
|
||||
type Header = bp_polkadot::Header;
|
||||
}
|
||||
|
||||
impl Chain for Polkadot {
|
||||
const NAME: &'static str = "Polkadot";
|
||||
const AVERAGE_BLOCK_INTERVAL: Duration = Duration::from_secs(6);
|
||||
|
||||
type AccountId = bp_polkadot::AccountId;
|
||||
type Index = bp_polkadot::Nonce;
|
||||
type SignedBlock = bp_polkadot::SignedBlock;
|
||||
type Call = ();
|
||||
}
|
||||
|
||||
/// Polkadot header type used in headers sync.
|
||||
pub type SyncHeader = relay_substrate_client::SyncHeader<bp_polkadot::Header>;
|
||||
@@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "relay-rialto-client"
|
||||
version = "0.1.0"
|
||||
authors = ["Parity Technologies <admin@parity.io>"]
|
||||
edition = "2018"
|
||||
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
|
||||
|
||||
[dependencies]
|
||||
codec = { package = "parity-scale-codec", version = "2.0.0" }
|
||||
headers-relay = { path = "../headers-relay" }
|
||||
relay-substrate-client = { path = "../substrate-client" }
|
||||
relay-utils = { path = "../utils" }
|
||||
|
||||
# Bridge dependencies
|
||||
|
||||
rialto-runtime = { path = "../../bin/rialto/runtime" }
|
||||
|
||||
# Substrate Dependencies
|
||||
|
||||
frame-system = { git = "https://github.com/paritytech/substrate.git", branch = "master" }
|
||||
frame-support = { git = "https://github.com/paritytech/substrate.git", branch = "master" }
|
||||
pallet-transaction-payment = { git = "https://github.com/paritytech/substrate.git", branch = "master" }
|
||||
sp-core = { git = "https://github.com/paritytech/substrate.git", branch = "master" }
|
||||
sp-keyring = { git = "https://github.com/paritytech/substrate.git", branch = "master" }
|
||||
sp-runtime = { git = "https://github.com/paritytech/substrate.git", branch = "master" }
|
||||
@@ -0,0 +1,133 @@
|
||||
// Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Types used to connect to the Rialto-Substrate chain.
|
||||
|
||||
use codec::Encode;
|
||||
use relay_substrate_client::{Chain, ChainBase, ChainWithBalances, Client, TransactionSignScheme};
|
||||
use sp_core::{storage::StorageKey, Pair};
|
||||
use sp_runtime::{generic::SignedPayload, traits::IdentifyAccount};
|
||||
use std::time::Duration;
|
||||
|
||||
pub use rialto_runtime::BridgeMillauCall;
|
||||
|
||||
/// Rialto header id.
|
||||
pub type HeaderId = relay_utils::HeaderId<rialto_runtime::Hash, rialto_runtime::BlockNumber>;
|
||||
|
||||
/// Rialto chain definition
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Rialto;
|
||||
|
||||
impl ChainBase for Rialto {
|
||||
type BlockNumber = rialto_runtime::BlockNumber;
|
||||
type Hash = rialto_runtime::Hash;
|
||||
type Hasher = rialto_runtime::Hashing;
|
||||
type Header = rialto_runtime::Header;
|
||||
}
|
||||
|
||||
impl Chain for Rialto {
|
||||
const NAME: &'static str = "Rialto";
|
||||
const AVERAGE_BLOCK_INTERVAL: Duration = Duration::from_secs(5);
|
||||
|
||||
type AccountId = rialto_runtime::AccountId;
|
||||
type Index = rialto_runtime::Index;
|
||||
type SignedBlock = rialto_runtime::SignedBlock;
|
||||
type Call = rialto_runtime::Call;
|
||||
}
|
||||
|
||||
impl ChainWithBalances for Rialto {
|
||||
type NativeBalance = rialto_runtime::Balance;
|
||||
|
||||
fn account_info_storage_key(account_id: &Self::AccountId) -> StorageKey {
|
||||
use frame_support::storage::generator::StorageMap;
|
||||
StorageKey(frame_system::Account::<rialto_runtime::Runtime>::storage_map_final_key(
|
||||
account_id,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl TransactionSignScheme for Rialto {
|
||||
type Chain = Rialto;
|
||||
type AccountKeyPair = sp_core::sr25519::Pair;
|
||||
type SignedTransaction = rialto_runtime::UncheckedExtrinsic;
|
||||
|
||||
fn sign_transaction(
|
||||
client: &Client<Self>,
|
||||
signer: &Self::AccountKeyPair,
|
||||
signer_nonce: <Self::Chain as Chain>::Index,
|
||||
call: <Self::Chain as Chain>::Call,
|
||||
) -> Self::SignedTransaction {
|
||||
let raw_payload = SignedPayload::from_raw(
|
||||
call,
|
||||
(
|
||||
frame_system::CheckSpecVersion::<rialto_runtime::Runtime>::new(),
|
||||
frame_system::CheckTxVersion::<rialto_runtime::Runtime>::new(),
|
||||
frame_system::CheckGenesis::<rialto_runtime::Runtime>::new(),
|
||||
frame_system::CheckEra::<rialto_runtime::Runtime>::from(sp_runtime::generic::Era::Immortal),
|
||||
frame_system::CheckNonce::<rialto_runtime::Runtime>::from(signer_nonce),
|
||||
frame_system::CheckWeight::<rialto_runtime::Runtime>::new(),
|
||||
pallet_transaction_payment::ChargeTransactionPayment::<rialto_runtime::Runtime>::from(0),
|
||||
),
|
||||
(
|
||||
rialto_runtime::VERSION.spec_version,
|
||||
rialto_runtime::VERSION.transaction_version,
|
||||
*client.genesis_hash(),
|
||||
*client.genesis_hash(),
|
||||
(),
|
||||
(),
|
||||
(),
|
||||
),
|
||||
);
|
||||
let signature = raw_payload.using_encoded(|payload| signer.sign(payload));
|
||||
let signer: sp_runtime::MultiSigner = signer.public().into();
|
||||
let (call, extra, _) = raw_payload.deconstruct();
|
||||
|
||||
rialto_runtime::UncheckedExtrinsic::new_signed(call, signer.into_account(), signature.into(), extra)
|
||||
}
|
||||
}
|
||||
|
||||
/// Rialto signing params.
|
||||
#[derive(Clone)]
|
||||
pub struct SigningParams {
|
||||
/// Substrate transactions signer.
|
||||
pub signer: sp_core::sr25519::Pair,
|
||||
}
|
||||
|
||||
impl SigningParams {
|
||||
/// Create signing params from SURI and password.
|
||||
pub fn from_suri(suri: &str, password: Option<&str>) -> Result<Self, sp_core::crypto::SecretStringError> {
|
||||
Ok(SigningParams {
|
||||
signer: sp_core::sr25519::Pair::from_string(suri, password)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for SigningParams {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(f, "{}", self.signer.public())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SigningParams {
|
||||
fn default() -> Self {
|
||||
SigningParams {
|
||||
signer: sp_keyring::AccountKeyring::Alice.pair(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Rialto header type used in headers sync.
|
||||
pub type SyncHeader = relay_substrate_client::SyncHeader<rialto_runtime::Header>;
|
||||
@@ -0,0 +1,37 @@
|
||||
[package]
|
||||
name = "relay-substrate-client"
|
||||
version = "0.1.0"
|
||||
authors = ["Parity Technologies <admin@parity.io>"]
|
||||
edition = "2018"
|
||||
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
|
||||
|
||||
[dependencies]
|
||||
async-std = "1.6.5"
|
||||
async-trait = "0.1.40"
|
||||
codec = { package = "parity-scale-codec", version = "2.0.0" }
|
||||
jsonrpsee = { git = "https://github.com/svyatonik/jsonrpsee.git", branch = "shared-client-in-rpc-api", default-features = false, features = ["ws"] }
|
||||
log = "0.4.11"
|
||||
num-traits = "0.2"
|
||||
rand = "0.7"
|
||||
|
||||
# Bridge dependencies
|
||||
|
||||
bp-message-lane = { path = "../../primitives/message-lane" }
|
||||
bp-runtime = { path = "../../primitives/runtime" }
|
||||
headers-relay = { path = "../headers-relay" }
|
||||
relay-utils = { path = "../utils" }
|
||||
|
||||
# Substrate Dependencies
|
||||
|
||||
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" }
|
||||
sp-trie = { git = "https://github.com/paritytech/substrate.git", branch = "master" }
|
||||
sp-version = { git = "https://github.com/paritytech/substrate.git", branch = "master" }
|
||||
|
||||
#[dev-dependencies]
|
||||
futures = "0.3.7"
|
||||
@@ -0,0 +1,103 @@
|
||||
// Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::client::Client;
|
||||
|
||||
use bp_runtime::Chain as ChainBase;
|
||||
use frame_support::Parameter;
|
||||
use jsonrpsee::common::{DeserializeOwned, Serialize};
|
||||
use num_traits::{CheckedSub, Zero};
|
||||
use sp_core::{storage::StorageKey, Pair};
|
||||
use sp_runtime::{
|
||||
generic::SignedBlock,
|
||||
traits::{AtLeast32Bit, Dispatchable, MaybeDisplay, MaybeSerialize, MaybeSerializeDeserialize, Member},
|
||||
Justification,
|
||||
};
|
||||
use std::{fmt::Debug, time::Duration};
|
||||
|
||||
/// Substrate-based chain from minimal relay-client point of view.
|
||||
pub trait Chain: ChainBase {
|
||||
/// Chain name.
|
||||
const NAME: &'static str;
|
||||
/// Average block interval.
|
||||
///
|
||||
/// How often blocks are produced on that chain. It's suggested to set this value
|
||||
/// to match the block time of the chain.
|
||||
const AVERAGE_BLOCK_INTERVAL: Duration;
|
||||
|
||||
/// The user account identifier type for the runtime.
|
||||
type AccountId: Parameter + Member + MaybeSerializeDeserialize + Debug + MaybeDisplay + Ord + Default;
|
||||
/// Index of a transaction used by the chain.
|
||||
type Index: Parameter
|
||||
+ Member
|
||||
+ MaybeSerialize
|
||||
+ Debug
|
||||
+ Default
|
||||
+ MaybeDisplay
|
||||
+ DeserializeOwned
|
||||
+ AtLeast32Bit
|
||||
+ Copy;
|
||||
/// Block type.
|
||||
type SignedBlock: Member + Serialize + DeserializeOwned + BlockWithJustification;
|
||||
/// The aggregated `Call` type.
|
||||
type Call: Dispatchable + Debug;
|
||||
}
|
||||
|
||||
/// Substrate-based chain with `frame_system::Config::AccountData` set to
|
||||
/// the `pallet_balances::AccountData<NativeBalance>`.
|
||||
pub trait ChainWithBalances: Chain {
|
||||
/// Balance of an account in native tokens.
|
||||
type NativeBalance: Parameter + Member + DeserializeOwned + Clone + Copy + CheckedSub + PartialOrd + Zero;
|
||||
|
||||
/// Return runtime storage key for getting `frame_system::AccountInfo` of given account.
|
||||
fn account_info_storage_key(account_id: &Self::AccountId) -> StorageKey;
|
||||
}
|
||||
|
||||
/// Block with justification.
|
||||
pub trait BlockWithJustification {
|
||||
/// Return block justification, if known.
|
||||
fn justification(&self) -> Option<&Justification>;
|
||||
}
|
||||
|
||||
/// Substrate-based chain transactions signing scheme.
|
||||
pub trait TransactionSignScheme {
|
||||
/// Chain that this scheme is to be used.
|
||||
type Chain: Chain;
|
||||
/// Type of key pairs used to sign transactions.
|
||||
type AccountKeyPair: Pair;
|
||||
/// Signed transaction.
|
||||
type SignedTransaction;
|
||||
|
||||
/// Create transaction for given runtime call, signed by given account.
|
||||
fn sign_transaction(
|
||||
client: &Client<Self::Chain>,
|
||||
signer: &Self::AccountKeyPair,
|
||||
signer_nonce: <Self::Chain as Chain>::Index,
|
||||
call: <Self::Chain as Chain>::Call,
|
||||
) -> Self::SignedTransaction;
|
||||
}
|
||||
|
||||
impl BlockWithJustification for () {
|
||||
fn justification(&self) -> Option<&Justification> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl<Block> BlockWithJustification for SignedBlock<Block> {
|
||||
fn justification(&self) -> Option<&Justification> {
|
||||
self.justification.as_ref()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
// Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Substrate node client.
|
||||
|
||||
use crate::chain::{Chain, ChainWithBalances};
|
||||
use crate::rpc::{Substrate, SubstrateMessageLane};
|
||||
use crate::{ConnectionParams, Error, Result};
|
||||
|
||||
use bp_message_lane::{LaneId, MessageNonce};
|
||||
use bp_runtime::InstanceId;
|
||||
use codec::Decode;
|
||||
use frame_system::AccountInfo;
|
||||
use jsonrpsee::common::DeserializeOwned;
|
||||
use jsonrpsee::raw::RawClient;
|
||||
use jsonrpsee::transport::ws::WsTransportClient;
|
||||
use jsonrpsee::{client::Subscription, Client as RpcClient};
|
||||
use num_traits::Zero;
|
||||
use pallet_balances::AccountData;
|
||||
use sp_core::Bytes;
|
||||
use sp_trie::StorageProof;
|
||||
use sp_version::RuntimeVersion;
|
||||
use std::ops::RangeInclusive;
|
||||
|
||||
const SUB_API_GRANDPA_AUTHORITIES: &str = "GrandpaApi_grandpa_authorities";
|
||||
|
||||
/// Opaque justifications subscription type.
|
||||
pub type JustificationsSubscription = Subscription<Bytes>;
|
||||
|
||||
/// Opaque GRANDPA authorities set.
|
||||
pub type OpaqueGrandpaAuthoritiesSet = Vec<u8>;
|
||||
|
||||
/// Substrate client type.
|
||||
///
|
||||
/// Cloning `Client` is a cheap operation.
|
||||
pub struct Client<C: Chain> {
|
||||
/// Client connection params.
|
||||
params: ConnectionParams,
|
||||
/// Substrate RPC client.
|
||||
client: RpcClient,
|
||||
/// Genesis block hash.
|
||||
genesis_hash: C::Hash,
|
||||
}
|
||||
|
||||
impl<C: Chain> Clone for Client<C> {
|
||||
fn clone(&self) -> Self {
|
||||
Client {
|
||||
params: self.params.clone(),
|
||||
client: self.client.clone(),
|
||||
genesis_hash: self.genesis_hash,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Chain> std::fmt::Debug for Client<C> {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
fmt.debug_struct("Client")
|
||||
.field("genesis_hash", &self.genesis_hash)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Chain> Client<C> {
|
||||
/// Returns client that is able to call RPCs on Substrate node over websocket connection.
|
||||
pub async fn new(params: ConnectionParams) -> Result<Self> {
|
||||
let client = Self::build_client(params.clone()).await?;
|
||||
|
||||
let number: C::BlockNumber = Zero::zero();
|
||||
let genesis_hash = Substrate::<C, _, _>::chain_get_block_hash(&client, number).await?;
|
||||
|
||||
Ok(Self {
|
||||
params,
|
||||
client,
|
||||
genesis_hash,
|
||||
})
|
||||
}
|
||||
|
||||
/// Reopen client connection.
|
||||
pub async fn reconnect(&mut self) -> Result<()> {
|
||||
self.client = Self::build_client(self.params.clone()).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Build client to use in connection.
|
||||
async fn build_client(params: ConnectionParams) -> Result<RpcClient> {
|
||||
let uri = format!("ws://{}:{}", params.host, params.port);
|
||||
let transport = WsTransportClient::new(&uri).await?;
|
||||
let raw_client = RawClient::new(transport);
|
||||
Ok(raw_client.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Chain> Client<C> {
|
||||
/// 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::<C, _, _>::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
|
||||
}
|
||||
|
||||
/// Return hash of the best finalized block.
|
||||
pub async fn best_finalized_header_hash(&self) -> Result<C::Hash> {
|
||||
Ok(Substrate::<C, _, _>::chain_get_finalized_head(&self.client).await?)
|
||||
}
|
||||
|
||||
/// Returns the best Substrate header.
|
||||
pub async fn best_header(&self) -> Result<C::Header>
|
||||
where
|
||||
C::Header: DeserializeOwned,
|
||||
{
|
||||
Ok(Substrate::<C, _, _>::chain_get_header(&self.client, None).await?)
|
||||
}
|
||||
|
||||
/// Get a Substrate block from its hash.
|
||||
pub async fn get_block(&self, block_hash: Option<C::Hash>) -> Result<C::SignedBlock> {
|
||||
Ok(Substrate::<C, _, _>::chain_get_block(&self.client, block_hash).await?)
|
||||
}
|
||||
|
||||
/// Get a Substrate header by its hash.
|
||||
pub async fn header_by_hash(&self, block_hash: C::Hash) -> Result<C::Header>
|
||||
where
|
||||
C::Header: DeserializeOwned,
|
||||
{
|
||||
Ok(Substrate::<C, _, _>::chain_get_header(&self.client, block_hash).await?)
|
||||
}
|
||||
|
||||
/// Get a Substrate block hash by its number.
|
||||
pub async fn block_hash_by_number(&self, number: C::BlockNumber) -> Result<C::Hash> {
|
||||
Ok(Substrate::<C, _, _>::chain_get_block_hash(&self.client, number).await?)
|
||||
}
|
||||
|
||||
/// Get a Substrate header by its number.
|
||||
pub async fn header_by_number(&self, block_number: C::BlockNumber) -> Result<C::Header>
|
||||
where
|
||||
C::Header: DeserializeOwned,
|
||||
{
|
||||
let block_hash = Self::block_hash_by_number(self, block_number).await?;
|
||||
Ok(Self::header_by_hash(self, block_hash).await?)
|
||||
}
|
||||
|
||||
/// Return runtime version.
|
||||
pub async fn runtime_version(&self) -> Result<RuntimeVersion> {
|
||||
Ok(Substrate::<C, _, _>::runtime_version(&self.client).await?)
|
||||
}
|
||||
|
||||
/// Return native tokens balance of the account.
|
||||
pub async fn free_native_balance(&self, account: C::AccountId) -> Result<C::NativeBalance>
|
||||
where
|
||||
C: ChainWithBalances,
|
||||
{
|
||||
let storage_key = C::account_info_storage_key(&account);
|
||||
let encoded_account_data = Substrate::<C, _, _>::get_storage(&self.client, storage_key)
|
||||
.await?
|
||||
.ok_or(Error::AccountDoesNotExist)?;
|
||||
let decoded_account_data =
|
||||
AccountInfo::<C::Index, AccountData<C::NativeBalance>>::decode(&mut &encoded_account_data.0[..])
|
||||
.map_err(Error::ResponseParseFailed)?;
|
||||
Ok(decoded_account_data.data.free)
|
||||
}
|
||||
|
||||
/// Get the nonce of the given Substrate account.
|
||||
///
|
||||
/// Note: It's the caller's responsibility to make sure `account` is a valid ss58 address.
|
||||
pub async fn next_account_index(&self, account: C::AccountId) -> Result<C::Index> {
|
||||
Ok(Substrate::<C, _, _>::system_account_next_index(&self.client, account).await?)
|
||||
}
|
||||
|
||||
/// Submit an extrinsic for inclusion in a block.
|
||||
///
|
||||
/// Note: The given transaction does not need be SCALE encoded beforehand.
|
||||
pub async fn submit_extrinsic(&self, transaction: Bytes) -> Result<C::Hash> {
|
||||
let tx_hash = Substrate::<C, _, _>::author_submit_extrinsic(&self.client, transaction).await?;
|
||||
log::trace!(target: "bridge", "Sent transaction to Substrate node: {:?}", tx_hash);
|
||||
Ok(tx_hash)
|
||||
}
|
||||
|
||||
/// Get the GRANDPA authority set at given block.
|
||||
pub async fn grandpa_authorities_set(&self, block: C::Hash) -> Result<OpaqueGrandpaAuthoritiesSet> {
|
||||
let call = SUB_API_GRANDPA_AUTHORITIES.to_string();
|
||||
let data = Bytes(Vec::new());
|
||||
|
||||
let encoded_response = Substrate::<C, _, _>::state_call(&self.client, call, data, Some(block)).await?;
|
||||
let authority_list = encoded_response.0;
|
||||
|
||||
Ok(authority_list)
|
||||
}
|
||||
|
||||
/// Execute runtime call at given block.
|
||||
pub async fn state_call(&self, method: String, data: Bytes, at_block: Option<C::Hash>) -> Result<Bytes> {
|
||||
Substrate::<C, _, _>::state_call(&self.client, method, data, at_block)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Returns proof-of-message(s) in given inclusive range.
|
||||
pub async fn prove_messages(
|
||||
&self,
|
||||
instance: InstanceId,
|
||||
lane: LaneId,
|
||||
range: RangeInclusive<MessageNonce>,
|
||||
include_outbound_lane_state: bool,
|
||||
at_block: C::Hash,
|
||||
) -> Result<StorageProof> {
|
||||
let encoded_trie_nodes = SubstrateMessageLane::<C, _, _>::prove_messages(
|
||||
&self.client,
|
||||
instance,
|
||||
lane,
|
||||
*range.start(),
|
||||
*range.end(),
|
||||
include_outbound_lane_state,
|
||||
Some(at_block),
|
||||
)
|
||||
.await
|
||||
.map_err(Error::Request)?;
|
||||
let decoded_trie_nodes: Vec<Vec<u8>> =
|
||||
Decode::decode(&mut &encoded_trie_nodes[..]).map_err(Error::ResponseParseFailed)?;
|
||||
Ok(StorageProof::new(decoded_trie_nodes))
|
||||
}
|
||||
|
||||
/// Returns proof-of-message(s) delivery.
|
||||
pub async fn prove_messages_delivery(
|
||||
&self,
|
||||
instance: InstanceId,
|
||||
lane: LaneId,
|
||||
at_block: C::Hash,
|
||||
) -> Result<Vec<Vec<u8>>> {
|
||||
let encoded_trie_nodes =
|
||||
SubstrateMessageLane::<C, _, _>::prove_messages_delivery(&self.client, instance, lane, Some(at_block))
|
||||
.await
|
||||
.map_err(Error::Request)?;
|
||||
let decoded_trie_nodes: Vec<Vec<u8>> =
|
||||
Decode::decode(&mut &encoded_trie_nodes[..]).map_err(Error::ResponseParseFailed)?;
|
||||
Ok(decoded_trie_nodes)
|
||||
}
|
||||
|
||||
/// Return new justifications stream.
|
||||
pub async fn subscribe_justifications(self) -> Result<JustificationsSubscription> {
|
||||
Ok(self
|
||||
.client
|
||||
.subscribe(
|
||||
"grandpa_subscribeJustifications",
|
||||
jsonrpsee::common::Params::None,
|
||||
"grandpa_unsubscribeJustifications",
|
||||
)
|
||||
.await?)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
// Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Substrate node RPC errors.
|
||||
|
||||
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<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Errors that can occur only when interacting with
|
||||
/// a Substrate node through RPC.
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// Web socket connection error.
|
||||
WsConnectionError(WsNewDnsError),
|
||||
/// An error that can occur when making a request to
|
||||
/// an JSON-RPC server.
|
||||
Request(RequestError),
|
||||
/// The response from the server could not be SCALE decoded.
|
||||
ResponseParseFailed(codec::Error),
|
||||
/// The Substrate bridge pallet has not yet been initialized.
|
||||
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),
|
||||
}
|
||||
|
||||
impl From<WsNewDnsError> for Error {
|
||||
fn from(error: WsNewDnsError) -> Self {
|
||||
Error::WsConnectionError(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RequestError> for Error {
|
||||
fn from(error: RequestError) -> Self {
|
||||
Error::Request(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl MaybeConnectionError for Error {
|
||||
fn is_connection_error(&self) -> bool {
|
||||
matches!(
|
||||
*self,
|
||||
Error::Request(RequestError::TransportError(_)) | Error::ClientNotSynced(_)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Error> for String {
|
||||
fn from(error: Error) -> String {
|
||||
error.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for Error {
|
||||
fn to_string(&self) -> String {
|
||||
match self {
|
||||
Self::WsConnectionError(e) => e.to_string(),
|
||||
Self::Request(e) => e.to_string(),
|
||||
Self::ResponseParseFailed(e) => e.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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,371 @@
|
||||
// Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Module provides a set of guard functions that are running in background threads
|
||||
//! and are aborting process if some condition fails.
|
||||
|
||||
use crate::{Chain, ChainWithBalances, Client};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use num_traits::CheckedSub;
|
||||
use sp_version::RuntimeVersion;
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
/// Guards environment.
|
||||
#[async_trait]
|
||||
pub trait Environment<C: ChainWithBalances>: Send + Sync + 'static {
|
||||
/// Return current runtime version.
|
||||
async fn runtime_version(&mut self) -> Result<RuntimeVersion, String>;
|
||||
/// Return free native balance of the account on the chain.
|
||||
async fn free_native_balance(&mut self, account: C::AccountId) -> Result<C::NativeBalance, String>;
|
||||
|
||||
/// Return current time.
|
||||
fn now(&self) -> Instant {
|
||||
Instant::now()
|
||||
}
|
||||
/// Sleep given amount of time.
|
||||
async fn sleep(&mut self, duration: Duration) {
|
||||
async_std::task::sleep(duration).await
|
||||
}
|
||||
/// Abort current process. Called when guard condition check fails.
|
||||
async fn abort(&mut self) {
|
||||
std::process::abort();
|
||||
}
|
||||
}
|
||||
|
||||
/// Abort when runtime spec version is different from specified.
|
||||
pub fn abort_on_spec_version_change<C: ChainWithBalances>(mut env: impl Environment<C>, expected_spec_version: u32) {
|
||||
async_std::task::spawn(async move {
|
||||
loop {
|
||||
let actual_spec_version = env.runtime_version().await;
|
||||
match actual_spec_version {
|
||||
Ok(version) if version.spec_version == expected_spec_version => (),
|
||||
Ok(version) => {
|
||||
log::error!(
|
||||
target: "bridge-guard",
|
||||
"{} runtime spec version has changed from {} to {}. Aborting relay",
|
||||
C::NAME,
|
||||
expected_spec_version,
|
||||
version.spec_version,
|
||||
);
|
||||
|
||||
env.abort().await;
|
||||
}
|
||||
Err(error) => log::warn!(
|
||||
target: "bridge-guard",
|
||||
"Failed to read {} runtime version: {:?}. Relay may need to be stopped manually",
|
||||
C::NAME,
|
||||
error,
|
||||
),
|
||||
}
|
||||
|
||||
env.sleep(conditions_check_delay::<C>()).await;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Abort if, during a 24 hours, free balance of given account is decreased at least by given value.
|
||||
/// Other components may increase (or decrease) balance of account and it WILL affect logic of the guard.
|
||||
pub fn abort_when_account_balance_decreased<C: ChainWithBalances>(
|
||||
mut env: impl Environment<C>,
|
||||
account_id: C::AccountId,
|
||||
maximal_decrease: C::NativeBalance,
|
||||
) {
|
||||
const DAY: Duration = Duration::from_secs(60 * 60 * 24);
|
||||
|
||||
async_std::task::spawn(async move {
|
||||
let mut balances = VecDeque::new();
|
||||
|
||||
loop {
|
||||
let current_time = env.now();
|
||||
|
||||
// remember balances that are beyound 24h border
|
||||
let time_border = current_time - DAY;
|
||||
while balances.front().map(|(time, _)| *time < time_border).unwrap_or(false) {
|
||||
balances.pop_front();
|
||||
}
|
||||
|
||||
// read balance of the account
|
||||
let current_balance = env.free_native_balance(account_id.clone()).await;
|
||||
|
||||
// remember balance and check difference
|
||||
match current_balance {
|
||||
Ok(current_balance) => {
|
||||
// remember balance
|
||||
balances.push_back((current_time, current_balance));
|
||||
|
||||
// check if difference between current and oldest balance is too large
|
||||
let (oldest_time, oldest_balance) =
|
||||
balances.front().expect("pushed to queue couple of lines above; qed");
|
||||
let balances_difference = oldest_balance.checked_sub(¤t_balance);
|
||||
if balances_difference > Some(maximal_decrease) {
|
||||
log::error!(
|
||||
target: "bridge-guard",
|
||||
"Balance of {} account {:?} has decreased from {:?} to {:?} in {} minutes. Aborting relay",
|
||||
C::NAME,
|
||||
account_id,
|
||||
oldest_balance,
|
||||
current_balance,
|
||||
current_time.duration_since(*oldest_time).as_secs() / 60,
|
||||
);
|
||||
|
||||
env.abort().await;
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
log::warn!(
|
||||
target: "bridge-guard",
|
||||
"Failed to read {} account {:?} balance: {:?}. Relay may need to be stopped manually",
|
||||
C::NAME,
|
||||
account_id,
|
||||
error,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
env.sleep(conditions_check_delay::<C>()).await;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Delay between conditions check.
|
||||
fn conditions_check_delay<C: Chain>() -> Duration {
|
||||
C::AVERAGE_BLOCK_INTERVAL * (10 + rand::random::<u32>() % 10)
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<C: ChainWithBalances> Environment<C> for Client<C> {
|
||||
async fn runtime_version(&mut self) -> Result<RuntimeVersion, String> {
|
||||
Client::<C>::runtime_version(self).await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
async fn free_native_balance(&mut self, account: C::AccountId) -> Result<C::NativeBalance, String> {
|
||||
Client::<C>::free_native_balance(self, account)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use futures::{
|
||||
channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender},
|
||||
future::FutureExt,
|
||||
stream::StreamExt,
|
||||
SinkExt,
|
||||
};
|
||||
|
||||
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<u32, sp_runtime::traits::BlakeTwo256>;
|
||||
}
|
||||
|
||||
impl Chain for TestChain {
|
||||
const NAME: &'static str = "Test";
|
||||
const AVERAGE_BLOCK_INTERVAL: Duration = Duration::from_millis(1);
|
||||
|
||||
type AccountId = u32;
|
||||
type Index = u32;
|
||||
type SignedBlock = ();
|
||||
type Call = ();
|
||||
}
|
||||
|
||||
impl ChainWithBalances for TestChain {
|
||||
type NativeBalance = u32;
|
||||
|
||||
fn account_info_storage_key(_account_id: &u32) -> sp_core::storage::StorageKey {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
struct TestEnvironment {
|
||||
runtime_version_rx: UnboundedReceiver<RuntimeVersion>,
|
||||
free_native_balance_rx: UnboundedReceiver<u32>,
|
||||
slept_tx: UnboundedSender<()>,
|
||||
aborted_tx: UnboundedSender<()>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Environment<TestChain> for TestEnvironment {
|
||||
async fn runtime_version(&mut self) -> Result<RuntimeVersion, String> {
|
||||
Ok(self.runtime_version_rx.next().await.unwrap_or_default())
|
||||
}
|
||||
|
||||
async fn free_native_balance(&mut self, _account: u32) -> Result<u32, String> {
|
||||
Ok(self.free_native_balance_rx.next().await.unwrap_or_default())
|
||||
}
|
||||
|
||||
async fn sleep(&mut self, _duration: Duration) {
|
||||
let _ = self.slept_tx.send(()).await;
|
||||
}
|
||||
|
||||
async fn abort(&mut self) {
|
||||
let _ = self.aborted_tx.send(()).await;
|
||||
// simulate process abort :)
|
||||
async_std::task::sleep(Duration::from_secs(60)).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aborts_when_spec_version_is_changed() {
|
||||
async_std::task::block_on(async {
|
||||
let (
|
||||
(mut runtime_version_tx, runtime_version_rx),
|
||||
(_free_native_balance_tx, free_native_balance_rx),
|
||||
(slept_tx, mut slept_rx),
|
||||
(aborted_tx, mut aborted_rx),
|
||||
) = (unbounded(), unbounded(), unbounded(), unbounded());
|
||||
abort_on_spec_version_change(
|
||||
TestEnvironment {
|
||||
runtime_version_rx,
|
||||
free_native_balance_rx,
|
||||
slept_tx,
|
||||
aborted_tx,
|
||||
},
|
||||
0,
|
||||
);
|
||||
|
||||
// client responds with wrong version
|
||||
runtime_version_tx
|
||||
.send(RuntimeVersion {
|
||||
spec_version: 42,
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// then the `abort` function is called
|
||||
aborted_rx.next().await;
|
||||
// and we do not reach the `sleep` function call
|
||||
assert!(slept_rx.next().now_or_never().is_none());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn does_not_aborts_when_spec_version_is_unchanged() {
|
||||
async_std::task::block_on(async {
|
||||
let (
|
||||
(mut runtime_version_tx, runtime_version_rx),
|
||||
(_free_native_balance_tx, free_native_balance_rx),
|
||||
(slept_tx, mut slept_rx),
|
||||
(aborted_tx, mut aborted_rx),
|
||||
) = (unbounded(), unbounded(), unbounded(), unbounded());
|
||||
abort_on_spec_version_change(
|
||||
TestEnvironment {
|
||||
runtime_version_rx,
|
||||
free_native_balance_rx,
|
||||
slept_tx,
|
||||
aborted_tx,
|
||||
},
|
||||
42,
|
||||
);
|
||||
|
||||
// client responds with the same version
|
||||
runtime_version_tx
|
||||
.send(RuntimeVersion {
|
||||
spec_version: 42,
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// then the `sleep` function is called
|
||||
slept_rx.next().await;
|
||||
// and the `abort` function is not called
|
||||
assert!(aborted_rx.next().now_or_never().is_none());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aborts_when_balance_is_too_low() {
|
||||
async_std::task::block_on(async {
|
||||
let (
|
||||
(_runtime_version_tx, runtime_version_rx),
|
||||
(mut free_native_balance_tx, free_native_balance_rx),
|
||||
(slept_tx, mut slept_rx),
|
||||
(aborted_tx, mut aborted_rx),
|
||||
) = (unbounded(), unbounded(), unbounded(), unbounded());
|
||||
abort_when_account_balance_decreased(
|
||||
TestEnvironment {
|
||||
runtime_version_rx,
|
||||
free_native_balance_rx,
|
||||
slept_tx,
|
||||
aborted_tx,
|
||||
},
|
||||
0,
|
||||
100,
|
||||
);
|
||||
|
||||
// client responds with initial balance
|
||||
free_native_balance_tx.send(1000).await.unwrap();
|
||||
|
||||
// then the guard sleeps
|
||||
slept_rx.next().await;
|
||||
|
||||
// and then client responds with updated balance, which is too low
|
||||
free_native_balance_tx.send(899).await.unwrap();
|
||||
|
||||
// then the `abort` function is called
|
||||
aborted_rx.next().await;
|
||||
// and we do not reach next `sleep` function call
|
||||
assert!(slept_rx.next().now_or_never().is_none());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn does_not_aborts_when_balance_is_enough() {
|
||||
async_std::task::block_on(async {
|
||||
let (
|
||||
(_runtime_version_tx, runtime_version_rx),
|
||||
(mut free_native_balance_tx, free_native_balance_rx),
|
||||
(slept_tx, mut slept_rx),
|
||||
(aborted_tx, mut aborted_rx),
|
||||
) = (unbounded(), unbounded(), unbounded(), unbounded());
|
||||
abort_when_account_balance_decreased(
|
||||
TestEnvironment {
|
||||
runtime_version_rx,
|
||||
free_native_balance_rx,
|
||||
slept_tx,
|
||||
aborted_tx,
|
||||
},
|
||||
0,
|
||||
100,
|
||||
);
|
||||
|
||||
// client responds with initial balance
|
||||
free_native_balance_tx.send(1000).await.unwrap();
|
||||
|
||||
// then the guard sleeps
|
||||
slept_rx.next().await;
|
||||
|
||||
// and then client responds with updated balance, which is enough
|
||||
free_native_balance_tx.send(950).await.unwrap();
|
||||
|
||||
// then the `sleep` function is called
|
||||
slept_rx.next().await;
|
||||
// and `abort` is not called
|
||||
assert!(aborted_rx.next().now_or_never().is_none());
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
// Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Default generic implementation of headers source for basic Substrate client.
|
||||
|
||||
use crate::chain::{BlockWithJustification, Chain};
|
||||
use crate::client::Client;
|
||||
use crate::error::Error;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use headers_relay::{
|
||||
sync_loop::SourceClient,
|
||||
sync_types::{HeaderIdOf, HeadersSyncPipeline, QueuedHeader, SourceHeader},
|
||||
};
|
||||
use relay_utils::relay_loop::Client as RelayClient;
|
||||
use sp_runtime::{traits::Header as HeaderT, Justification};
|
||||
use std::marker::PhantomData;
|
||||
|
||||
/// Substrate node as headers source.
|
||||
pub struct HeadersSource<C: Chain, P> {
|
||||
client: Client<C>,
|
||||
_phantom: PhantomData<P>,
|
||||
}
|
||||
|
||||
impl<C: Chain, P> HeadersSource<C, P> {
|
||||
/// Create new headers source using given client.
|
||||
pub fn new(client: Client<C>) -> Self {
|
||||
HeadersSource {
|
||||
client,
|
||||
_phantom: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Chain, P> Clone for HeadersSource<C, P> {
|
||||
fn clone(&self) -> Self {
|
||||
HeadersSource {
|
||||
client: self.client.clone(),
|
||||
_phantom: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<C: Chain, P: HeadersSyncPipeline> RelayClient for HeadersSource<C, P> {
|
||||
type Error = Error;
|
||||
|
||||
async fn reconnect(&mut self) -> Result<(), Error> {
|
||||
self.client.reconnect().await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<C, P> SourceClient<P> for HeadersSource<C, P>
|
||||
where
|
||||
C: Chain,
|
||||
C::BlockNumber: relay_utils::BlockNumberBase,
|
||||
C::Header: Into<P::Header>,
|
||||
P: HeadersSyncPipeline<Extra = (), Completion = Justification, Hash = C::Hash, Number = C::BlockNumber>,
|
||||
P::Header: SourceHeader<C::Hash, C::BlockNumber>,
|
||||
{
|
||||
async fn best_block_number(&self) -> Result<P::Number, Error> {
|
||||
// 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())
|
||||
}
|
||||
|
||||
async fn header_by_hash(&self, hash: P::Hash) -> Result<P::Header, Error> {
|
||||
self.client
|
||||
.header_by_hash(hash)
|
||||
.await
|
||||
.map(Into::into)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn header_by_number(&self, number: P::Number) -> Result<P::Header, Error> {
|
||||
self.client
|
||||
.header_by_number(number)
|
||||
.await
|
||||
.map(Into::into)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn header_completion(&self, id: HeaderIdOf<P>) -> Result<(HeaderIdOf<P>, Option<P::Completion>), Error> {
|
||||
let hash = id.1;
|
||||
let signed_block = self.client.get_block(Some(hash)).await?;
|
||||
let grandpa_justification = signed_block.justification().cloned();
|
||||
|
||||
Ok((id, grandpa_justification))
|
||||
}
|
||||
|
||||
async fn header_extra(&self, id: HeaderIdOf<P>, _header: QueuedHeader<P>) -> Result<(HeaderIdOf<P>, ()), Error> {
|
||||
Ok((id, ()))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Tools to interact with (Open) Ethereum node using RPC methods.
|
||||
|
||||
#![warn(missing_docs)]
|
||||
|
||||
mod chain;
|
||||
mod client;
|
||||
mod error;
|
||||
mod rpc;
|
||||
mod sync_header;
|
||||
|
||||
pub mod guard;
|
||||
pub mod headers_source;
|
||||
|
||||
pub use crate::chain::{BlockWithJustification, Chain, ChainWithBalances, TransactionSignScheme};
|
||||
pub use crate::client::{Client, JustificationsSubscription, OpaqueGrandpaAuthoritiesSet};
|
||||
pub use crate::error::{Error, Result};
|
||||
pub use crate::sync_header::SyncHeader;
|
||||
pub use bp_runtime::{BlockNumberOf, Chain as ChainBase, HashOf, HeaderOf};
|
||||
|
||||
/// Header id used by the chain.
|
||||
pub type HeaderIdOf<C> = relay_utils::HeaderId<HashOf<C>, BlockNumberOf<C>>;
|
||||
|
||||
/// Substrate-over-websocket connection params.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ConnectionParams {
|
||||
/// Websocket server hostname.
|
||||
pub host: String,
|
||||
/// Websocket server TCP port.
|
||||
pub port: u16,
|
||||
}
|
||||
|
||||
impl Default for ConnectionParams {
|
||||
fn default() -> Self {
|
||||
ConnectionParams {
|
||||
host: "localhost".into(),
|
||||
port: 9944,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! The most generic Substrate node RPC interface.
|
||||
|
||||
// The compiler doesn't think we're using the
|
||||
// code from rpc_api!
|
||||
#![allow(dead_code)]
|
||||
#![allow(unused_variables)]
|
||||
|
||||
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,
|
||||
};
|
||||
use sp_version::RuntimeVersion;
|
||||
|
||||
jsonrpsee::rpc_api! {
|
||||
pub(crate) Substrate<C: Chain> {
|
||||
#[rpc(method = "system_health", positional_params)]
|
||||
fn system_health() -> Health;
|
||||
#[rpc(method = "chain_getHeader", positional_params)]
|
||||
fn chain_get_header(block_hash: Option<C::Hash>) -> C::Header;
|
||||
#[rpc(method = "chain_getFinalizedHead", positional_params)]
|
||||
fn chain_get_finalized_head() -> C::Hash;
|
||||
#[rpc(method = "chain_getBlock", positional_params)]
|
||||
fn chain_get_block(block_hash: Option<C::Hash>) -> C::SignedBlock;
|
||||
#[rpc(method = "chain_getBlockHash", positional_params)]
|
||||
fn chain_get_block_hash(block_number: Option<C::BlockNumber>) -> C::Hash;
|
||||
#[rpc(method = "system_accountNextIndex", positional_params)]
|
||||
fn system_account_next_index(account_id: C::AccountId) -> C::Index;
|
||||
#[rpc(method = "author_submitExtrinsic", positional_params)]
|
||||
fn author_submit_extrinsic(extrinsic: Bytes) -> C::Hash;
|
||||
#[rpc(method = "state_call", positional_params)]
|
||||
fn state_call(method: String, data: Bytes, at_block: Option<C::Hash>) -> Bytes;
|
||||
#[rpc(method = "state_getStorage", positional_params)]
|
||||
fn get_storage(key: StorageKey) -> Option<StorageData>;
|
||||
#[rpc(method = "state_getRuntimeVersion", positional_params)]
|
||||
fn runtime_version() -> RuntimeVersion;
|
||||
}
|
||||
|
||||
pub(crate) SubstrateMessageLane<C: Chain> {
|
||||
#[rpc(method = "messageLane_proveMessages", positional_params)]
|
||||
fn prove_messages(
|
||||
instance: InstanceId,
|
||||
lane: LaneId,
|
||||
begin: MessageNonce,
|
||||
end: MessageNonce,
|
||||
include_outbound_lane_state: bool,
|
||||
block: Option<C::Hash>,
|
||||
) -> Bytes;
|
||||
|
||||
#[rpc(method = "messageLane_proveMessagesDelivery", positional_params)]
|
||||
fn prove_messages_delivery(
|
||||
instance: InstanceId,
|
||||
lane: LaneId,
|
||||
block: Option<C::Hash>,
|
||||
) -> Bytes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
use headers_relay::sync_types::SourceHeader;
|
||||
use num_traits::{CheckedSub, One};
|
||||
use relay_utils::HeaderId;
|
||||
use sp_runtime::traits::Header as HeaderT;
|
||||
|
||||
/// Generic wrapper for `sp_runtime::traits::Header` based headers, that
|
||||
/// implements `headers_relay::sync_types::SourceHeader` and may be used in headers sync directly.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct SyncHeader<Header>(Header);
|
||||
|
||||
impl<Header> SyncHeader<Header> {
|
||||
/// Extracts wrapped header from self.
|
||||
pub fn into_inner(self) -> Header {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<Header> std::ops::Deref for SyncHeader<Header> {
|
||||
type Target = Header;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<Header> From<Header> for SyncHeader<Header> {
|
||||
fn from(header: Header) -> Self {
|
||||
Self(header)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Header: HeaderT> SourceHeader<Header::Hash, Header::Number> for SyncHeader<Header> {
|
||||
fn id(&self) -> HeaderId<Header::Hash, Header::Number> {
|
||||
relay_utils::HeaderId(*self.number(), self.hash())
|
||||
}
|
||||
|
||||
fn parent_id(&self) -> HeaderId<Header::Hash, Header::Number> {
|
||||
relay_utils::HeaderId(
|
||||
self.number()
|
||||
.checked_sub(&One::one())
|
||||
.expect("should never be called for genesis header"),
|
||||
*self.parent_hash(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
[package]
|
||||
name = "substrate-relay"
|
||||
version = "0.1.0"
|
||||
authors = ["Parity Technologies <admin@parity.io>"]
|
||||
edition = "2018"
|
||||
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
|
||||
|
||||
[dependencies]
|
||||
async-std = "1.9.0"
|
||||
async-trait = "0.1.42"
|
||||
codec = { package = "parity-scale-codec", version = "2.0.0" }
|
||||
futures = "0.3.12"
|
||||
hex = "0.4"
|
||||
log = "0.4.14"
|
||||
num-traits = "0.2"
|
||||
paste = "1.0"
|
||||
structopt = "0.3"
|
||||
|
||||
# Bridge dependencies
|
||||
|
||||
bp-header-chain = { path = "../../primitives/header-chain" }
|
||||
bp-kusama = { path = "../../primitives/kusama" }
|
||||
bp-message-lane = { path = "../../primitives/message-lane" }
|
||||
bp-millau = { path = "../../primitives/millau" }
|
||||
bp-polkadot = { path = "../../primitives/polkadot" }
|
||||
bp-runtime = { path = "../../primitives/runtime" }
|
||||
bp-rialto = { path = "../../primitives/rialto" }
|
||||
bridge-runtime-common = { path = "../../bin/runtime-common" }
|
||||
headers-relay = { path = "../headers-relay" }
|
||||
messages-relay = { path = "../messages-relay" }
|
||||
millau-runtime = { path = "../../bin/millau/runtime" }
|
||||
pallet-bridge-call-dispatch = { path = "../../modules/call-dispatch" }
|
||||
pallet-message-lane = { path = "../../modules/message-lane" }
|
||||
pallet-substrate-bridge = { path = "../../modules/substrate" }
|
||||
relay-kusama-client = { path = "../kusama-client" }
|
||||
relay-millau-client = { path = "../millau-client" }
|
||||
relay-polkadot-client = { path = "../polkadot-client" }
|
||||
relay-rialto-client = { path = "../rialto-client" }
|
||||
relay-substrate-client = { path = "../substrate-client" }
|
||||
relay-utils = { path = "../utils" }
|
||||
rialto-runtime = { path = "../../bin/rialto/runtime" }
|
||||
|
||||
# Substrate Dependencies
|
||||
|
||||
frame-support = { git = "https://github.com/paritytech/substrate.git", branch = "master" }
|
||||
sp-core = { git = "https://github.com/paritytech/substrate.git", branch = "master" }
|
||||
sp-finality-grandpa = { git = "https://github.com/paritytech/substrate.git", branch = "master" }
|
||||
sp-runtime = { git = "https://github.com/paritytech/substrate.git", branch = "master" }
|
||||
sp-trie = { git = "https://github.com/paritytech/substrate.git", branch = "master" }
|
||||
@@ -0,0 +1,360 @@
|
||||
// Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Deal with CLI args of substrate-to-substrate relay.
|
||||
|
||||
use bp_message_lane::LaneId;
|
||||
use frame_support::weights::Weight;
|
||||
use sp_core::Bytes;
|
||||
use sp_finality_grandpa::SetId as GrandpaAuthoritiesSetId;
|
||||
use structopt::{clap::arg_enum, StructOpt};
|
||||
|
||||
/// Parse relay CLI args.
|
||||
pub fn parse_args() -> Command {
|
||||
Command::from_args()
|
||||
}
|
||||
|
||||
/// Substrate-to-Substrate bridge utilities.
|
||||
#[derive(StructOpt)]
|
||||
#[structopt(about = "Substrate-to-Substrate relay")]
|
||||
pub enum Command {
|
||||
/// Start headers relay between two chains.
|
||||
///
|
||||
/// The on-chain bridge component should have been already initialized with
|
||||
/// `init-bridge` sub-command.
|
||||
RelayHeaders(RelayHeaders),
|
||||
/// Start messages relay between two chains.
|
||||
///
|
||||
/// Ties up to `MessageLane` pallets on both chains and starts relaying messages.
|
||||
/// Requires the header relay to be already running.
|
||||
RelayMessages(RelayMessages),
|
||||
/// Initialize on-chain bridge pallet with current header data.
|
||||
///
|
||||
/// Sends initialization transaction to bootstrap the bridge with current finalized block data.
|
||||
InitBridge(InitBridge),
|
||||
/// Send custom message over the bridge.
|
||||
///
|
||||
/// Allows interacting with the bridge by sending messages over `MessageLane` component.
|
||||
/// The message is being sent to the source chain, delivered to the target chain and dispatched
|
||||
/// there.
|
||||
SendMessage(SendMessage),
|
||||
}
|
||||
|
||||
#[derive(StructOpt)]
|
||||
pub enum RelayHeaders {
|
||||
/// Relay Millau headers to Rialto.
|
||||
MillauToRialto {
|
||||
#[structopt(flatten)]
|
||||
millau: MillauConnectionParams,
|
||||
#[structopt(flatten)]
|
||||
rialto: RialtoConnectionParams,
|
||||
#[structopt(flatten)]
|
||||
rialto_sign: RialtoSigningParams,
|
||||
#[structopt(flatten)]
|
||||
prometheus_params: PrometheusParams,
|
||||
},
|
||||
/// Relay Rialto headers to Millau.
|
||||
RialtoToMillau {
|
||||
#[structopt(flatten)]
|
||||
rialto: RialtoConnectionParams,
|
||||
#[structopt(flatten)]
|
||||
millau: MillauConnectionParams,
|
||||
#[structopt(flatten)]
|
||||
millau_sign: MillauSigningParams,
|
||||
#[structopt(flatten)]
|
||||
prometheus_params: PrometheusParams,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(StructOpt)]
|
||||
pub enum RelayMessages {
|
||||
/// Serve given lane of Millau -> Rialto messages.
|
||||
MillauToRialto {
|
||||
#[structopt(flatten)]
|
||||
millau: MillauConnectionParams,
|
||||
#[structopt(flatten)]
|
||||
millau_sign: MillauSigningParams,
|
||||
#[structopt(flatten)]
|
||||
rialto: RialtoConnectionParams,
|
||||
#[structopt(flatten)]
|
||||
rialto_sign: RialtoSigningParams,
|
||||
#[structopt(flatten)]
|
||||
prometheus_params: PrometheusParams,
|
||||
/// Hex-encoded id of lane that should be served by relay.
|
||||
#[structopt(long)]
|
||||
lane: HexLaneId,
|
||||
},
|
||||
/// Serve given lane of Rialto -> Millau messages.
|
||||
RialtoToMillau {
|
||||
#[structopt(flatten)]
|
||||
rialto: RialtoConnectionParams,
|
||||
#[structopt(flatten)]
|
||||
rialto_sign: RialtoSigningParams,
|
||||
#[structopt(flatten)]
|
||||
millau: MillauConnectionParams,
|
||||
#[structopt(flatten)]
|
||||
millau_sign: MillauSigningParams,
|
||||
#[structopt(flatten)]
|
||||
prometheus_params: PrometheusParams,
|
||||
/// Hex-encoded id of lane that should be served by relay.
|
||||
#[structopt(long)]
|
||||
lane: HexLaneId,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(StructOpt)]
|
||||
pub enum InitBridge {
|
||||
/// Initialize Millau headers bridge in Rialto.
|
||||
MillauToRialto {
|
||||
#[structopt(flatten)]
|
||||
millau: MillauConnectionParams,
|
||||
#[structopt(flatten)]
|
||||
rialto: RialtoConnectionParams,
|
||||
#[structopt(flatten)]
|
||||
rialto_sign: RialtoSigningParams,
|
||||
#[structopt(flatten)]
|
||||
millau_bridge_params: MillauBridgeInitializationParams,
|
||||
},
|
||||
/// Initialize Rialto headers bridge in Millau.
|
||||
RialtoToMillau {
|
||||
#[structopt(flatten)]
|
||||
rialto: RialtoConnectionParams,
|
||||
#[structopt(flatten)]
|
||||
millau: MillauConnectionParams,
|
||||
#[structopt(flatten)]
|
||||
millau_sign: MillauSigningParams,
|
||||
#[structopt(flatten)]
|
||||
rialto_bridge_params: RialtoBridgeInitializationParams,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(StructOpt)]
|
||||
pub enum SendMessage {
|
||||
/// Submit message to given Millau -> Rialto lane.
|
||||
MillauToRialto {
|
||||
#[structopt(flatten)]
|
||||
millau: MillauConnectionParams,
|
||||
#[structopt(flatten)]
|
||||
millau_sign: MillauSigningParams,
|
||||
#[structopt(flatten)]
|
||||
rialto_sign: RialtoSigningParams,
|
||||
/// Hex-encoded lane id.
|
||||
#[structopt(long)]
|
||||
lane: HexLaneId,
|
||||
/// Dispatch weight of the message. If not passed, determined automatically.
|
||||
#[structopt(long)]
|
||||
dispatch_weight: Option<ExplicitOrMaximal<Weight>>,
|
||||
/// Delivery and dispatch fee. If not passed, determined automatically.
|
||||
#[structopt(long)]
|
||||
fee: Option<bp_millau::Balance>,
|
||||
/// Message type.
|
||||
#[structopt(subcommand)]
|
||||
message: ToRialtoMessage,
|
||||
/// The origin to use when dispatching the message on the target chain.
|
||||
#[structopt(long, possible_values = &Origins::variants())]
|
||||
origin: Origins,
|
||||
},
|
||||
/// Submit message to given Rialto -> Millau lane.
|
||||
RialtoToMillau {
|
||||
#[structopt(flatten)]
|
||||
rialto: RialtoConnectionParams,
|
||||
#[structopt(flatten)]
|
||||
rialto_sign: RialtoSigningParams,
|
||||
#[structopt(flatten)]
|
||||
millau_sign: MillauSigningParams,
|
||||
/// Hex-encoded lane id.
|
||||
#[structopt(long)]
|
||||
lane: HexLaneId,
|
||||
/// Dispatch weight of the message. If not passed, determined automatically.
|
||||
#[structopt(long)]
|
||||
dispatch_weight: Option<ExplicitOrMaximal<Weight>>,
|
||||
/// Delivery and dispatch fee. If not passed, determined automatically.
|
||||
#[structopt(long)]
|
||||
fee: Option<bp_rialto::Balance>,
|
||||
/// Message type.
|
||||
#[structopt(subcommand)]
|
||||
message: ToMillauMessage,
|
||||
/// The origin to use when dispatching the message on the target chain.
|
||||
#[structopt(long, possible_values = &Origins::variants())]
|
||||
origin: Origins,
|
||||
},
|
||||
}
|
||||
|
||||
/// All possible messages that may be delivered to the Rialto chain.
|
||||
#[derive(StructOpt, Debug)]
|
||||
pub enum ToRialtoMessage {
|
||||
/// Make an on-chain remark (comment).
|
||||
Remark {
|
||||
/// Remark size. If not passed, small UTF8-encoded string is generated by relay as remark.
|
||||
#[structopt(long)]
|
||||
remark_size: Option<ExplicitOrMaximal<usize>>,
|
||||
},
|
||||
/// Transfer the specified `amount` of native tokens to a particular `recipient`.
|
||||
Transfer {
|
||||
#[structopt(long)]
|
||||
recipient: bp_rialto::AccountId,
|
||||
#[structopt(long)]
|
||||
amount: bp_rialto::Balance,
|
||||
},
|
||||
}
|
||||
|
||||
/// All possible messages that may be delivered to the Millau chain.
|
||||
#[derive(StructOpt, Debug)]
|
||||
pub enum ToMillauMessage {
|
||||
/// Make an on-chain remark (comment).
|
||||
Remark {
|
||||
/// Size of the remark. If not passed, small UTF8-encoded string is generated by relay as remark.
|
||||
#[structopt(long)]
|
||||
remark_size: Option<ExplicitOrMaximal<usize>>,
|
||||
},
|
||||
/// Transfer the specified `amount` of native tokens to a particular `recipient`.
|
||||
Transfer {
|
||||
#[structopt(long)]
|
||||
recipient: bp_millau::AccountId,
|
||||
#[structopt(long)]
|
||||
amount: bp_millau::Balance,
|
||||
},
|
||||
}
|
||||
|
||||
arg_enum! {
|
||||
#[derive(Debug)]
|
||||
/// The origin to use when dispatching the message on the target chain.
|
||||
///
|
||||
/// - `Target` uses account existing on the target chain (requires target private key).
|
||||
/// - `Origin` uses account derived from the source-chain account.
|
||||
pub enum Origins {
|
||||
Target,
|
||||
Source,
|
||||
}
|
||||
}
|
||||
|
||||
/// Lane id.
|
||||
#[derive(Debug)]
|
||||
pub struct HexLaneId(LaneId);
|
||||
|
||||
impl From<HexLaneId> for LaneId {
|
||||
fn from(lane_id: HexLaneId) -> LaneId {
|
||||
lane_id.0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::str::FromStr for HexLaneId {
|
||||
type Err = hex::FromHexError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let mut lane_id = LaneId::default();
|
||||
hex::decode_to_slice(s, &mut lane_id)?;
|
||||
Ok(HexLaneId(lane_id))
|
||||
}
|
||||
}
|
||||
|
||||
/// Prometheus metrics params.
|
||||
#[derive(StructOpt)]
|
||||
pub struct PrometheusParams {
|
||||
/// Do not expose a Prometheus metric endpoint.
|
||||
#[structopt(long)]
|
||||
pub no_prometheus: bool,
|
||||
/// Expose Prometheus endpoint at given interface.
|
||||
#[structopt(long, default_value = "127.0.0.1")]
|
||||
pub prometheus_host: String,
|
||||
/// Expose Prometheus endpoint at given port.
|
||||
#[structopt(long, default_value = "9616")]
|
||||
pub prometheus_port: u16,
|
||||
}
|
||||
|
||||
impl From<PrometheusParams> for Option<relay_utils::metrics::MetricsParams> {
|
||||
fn from(cli_params: PrometheusParams) -> Option<relay_utils::metrics::MetricsParams> {
|
||||
if !cli_params.no_prometheus {
|
||||
Some(relay_utils::metrics::MetricsParams {
|
||||
host: cli_params.prometheus_host,
|
||||
port: cli_params.prometheus_port,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Either explicit or maximal allowed value.
|
||||
#[derive(Debug)]
|
||||
pub enum ExplicitOrMaximal<V> {
|
||||
/// User has explicitly specified argument value.
|
||||
Explicit(V),
|
||||
/// Maximal allowed value for this argument.
|
||||
Maximal,
|
||||
}
|
||||
|
||||
impl<V: std::str::FromStr> std::str::FromStr for ExplicitOrMaximal<V>
|
||||
where
|
||||
V::Err: std::fmt::Debug,
|
||||
{
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
if s.to_lowercase() == "max" {
|
||||
return Ok(ExplicitOrMaximal::Maximal);
|
||||
}
|
||||
|
||||
V::from_str(s)
|
||||
.map(ExplicitOrMaximal::Explicit)
|
||||
.map_err(|e| format!("Failed to parse '{:?}'. Expected 'max' or explicit value", e))
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! declare_chain_options {
|
||||
($chain:ident, $chain_prefix:ident) => {
|
||||
paste::item! {
|
||||
#[doc = $chain " connection params."]
|
||||
#[derive(StructOpt)]
|
||||
pub struct [<$chain ConnectionParams>] {
|
||||
#[doc = "Connect to " $chain " node at given host."]
|
||||
#[structopt(long)]
|
||||
pub [<$chain_prefix _host>]: String,
|
||||
#[doc = "Connect to " $chain " node websocket server at given port."]
|
||||
#[structopt(long)]
|
||||
pub [<$chain_prefix _port>]: u16,
|
||||
}
|
||||
|
||||
#[doc = $chain " signing params."]
|
||||
#[derive(StructOpt)]
|
||||
pub struct [<$chain SigningParams>] {
|
||||
#[doc = "The SURI of secret key to use when transactions are submitted to the " $chain " node."]
|
||||
#[structopt(long)]
|
||||
pub [<$chain_prefix _signer>]: String,
|
||||
#[doc = "The password for the SURI of secret key to use when transactions are submitted to the " $chain " node."]
|
||||
#[structopt(long)]
|
||||
pub [<$chain_prefix _signer_password>]: Option<String>,
|
||||
}
|
||||
|
||||
#[doc = $chain " headers bridge initialization params."]
|
||||
#[derive(StructOpt)]
|
||||
pub struct [<$chain BridgeInitializationParams>] {
|
||||
#[doc = "Hex-encoded " $chain " header to initialize bridge with. If not specified, genesis header is used."]
|
||||
#[structopt(long)]
|
||||
pub [<$chain_prefix _initial_header>]: Option<Bytes>,
|
||||
#[doc = "Hex-encoded " $chain " GRANDPA authorities set to initialize bridge with. If not specified, set from genesis block is used."]
|
||||
#[structopt(long)]
|
||||
pub [<$chain_prefix _initial_authorities>]: Option<Bytes>,
|
||||
#[doc = "Id of the " $chain " GRANDPA authorities set to initialize bridge with. If not specified, zero is used."]
|
||||
#[structopt(long)]
|
||||
pub [<$chain_prefix _initial_authorities_set_id>]: Option<GrandpaAuthoritiesSetId>,
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
declare_chain_options!(Rialto, rialto);
|
||||
declare_chain_options!(Millau, millau);
|
||||
@@ -0,0 +1,141 @@
|
||||
// Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Initialize Substrate -> Substrate headers bridge.
|
||||
//!
|
||||
//! Initialization is a transaction that calls `initialize()` function of the
|
||||
//! `pallet-substrate-bridge` pallet. This transaction brings initial header
|
||||
//! and authorities set from source to target chain. The headers sync starts
|
||||
//! with this header.
|
||||
|
||||
use codec::Decode;
|
||||
use pallet_substrate_bridge::InitializationData;
|
||||
use relay_substrate_client::{Chain, Client};
|
||||
use sp_core::Bytes;
|
||||
use sp_finality_grandpa::{AuthorityList as GrandpaAuthoritiesSet, SetId as GrandpaAuthoritiesSetId};
|
||||
|
||||
/// Submit headers-bridge initialization transaction.
|
||||
pub async fn initialize<SourceChain: Chain, TargetChain: Chain>(
|
||||
source_client: Client<SourceChain>,
|
||||
target_client: Client<TargetChain>,
|
||||
raw_initial_header: Option<Bytes>,
|
||||
raw_initial_authorities_set: Option<Bytes>,
|
||||
initial_authorities_set_id: Option<GrandpaAuthoritiesSetId>,
|
||||
prepare_initialize_transaction: impl FnOnce(InitializationData<SourceChain::Header>) -> Result<Bytes, String>,
|
||||
) {
|
||||
let result = do_initialize(
|
||||
source_client,
|
||||
target_client,
|
||||
raw_initial_header,
|
||||
raw_initial_authorities_set,
|
||||
initial_authorities_set_id,
|
||||
prepare_initialize_transaction,
|
||||
)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(tx_hash) => log::info!(
|
||||
target: "bridge",
|
||||
"Successfully submitted {}-headers bridge initialization transaction to {}: {:?}",
|
||||
SourceChain::NAME,
|
||||
TargetChain::NAME,
|
||||
tx_hash,
|
||||
),
|
||||
Err(err) => log::error!(
|
||||
target: "bridge",
|
||||
"Failed to submit {}-headers bridge initialization transaction to {}: {:?}",
|
||||
SourceChain::NAME,
|
||||
TargetChain::NAME,
|
||||
err,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Craft and submit initialization transaction, returning any error that may occur.
|
||||
async fn do_initialize<SourceChain: Chain, TargetChain: Chain>(
|
||||
source_client: Client<SourceChain>,
|
||||
target_client: Client<TargetChain>,
|
||||
raw_initial_header: Option<Bytes>,
|
||||
raw_initial_authorities_set: Option<Bytes>,
|
||||
initial_authorities_set_id: Option<GrandpaAuthoritiesSetId>,
|
||||
prepare_initialize_transaction: impl FnOnce(InitializationData<SourceChain::Header>) -> Result<Bytes, String>,
|
||||
) -> Result<TargetChain::Hash, String> {
|
||||
let initialization_data = prepare_initialization_data(
|
||||
source_client,
|
||||
raw_initial_header,
|
||||
raw_initial_authorities_set,
|
||||
initial_authorities_set_id,
|
||||
)
|
||||
.await?;
|
||||
let initialization_tx = prepare_initialize_transaction(initialization_data)?;
|
||||
let initialization_tx_hash = target_client
|
||||
.submit_extrinsic(initialization_tx)
|
||||
.await
|
||||
.map_err(|err| format!("Failed to submit {} transaction: {:?}", TargetChain::NAME, err))?;
|
||||
Ok(initialization_tx_hash)
|
||||
}
|
||||
|
||||
/// Prepare initialization data for the headers-bridge pallet.
|
||||
async fn prepare_initialization_data<SourceChain: Chain>(
|
||||
source_client: Client<SourceChain>,
|
||||
raw_initial_header: Option<Bytes>,
|
||||
raw_initial_authorities_set: Option<Bytes>,
|
||||
initial_authorities_set_id: Option<GrandpaAuthoritiesSetId>,
|
||||
) -> Result<InitializationData<SourceChain::Header>, String> {
|
||||
let source_genesis_hash = *source_client.genesis_hash();
|
||||
|
||||
let initial_header = match raw_initial_header {
|
||||
Some(raw_initial_header) => SourceChain::Header::decode(&mut &raw_initial_header.0[..])
|
||||
.map_err(|err| format!("Failed to decode {} initial header: {:?}", SourceChain::NAME, err))?,
|
||||
None => source_client
|
||||
.header_by_hash(source_genesis_hash)
|
||||
.await
|
||||
.map_err(|err| format!("Failed to retrive {} genesis header: {:?}", SourceChain::NAME, err))?,
|
||||
};
|
||||
|
||||
let raw_initial_authorities_set = match raw_initial_authorities_set {
|
||||
Some(raw_initial_authorities_set) => raw_initial_authorities_set.0,
|
||||
None => source_client
|
||||
.grandpa_authorities_set(source_genesis_hash)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
format!(
|
||||
"Failed to retrive {} authorities set at genesis header: {:?}",
|
||||
SourceChain::NAME,
|
||||
err
|
||||
)
|
||||
})?,
|
||||
};
|
||||
let initial_authorities_set =
|
||||
GrandpaAuthoritiesSet::decode(&mut &raw_initial_authorities_set[..]).map_err(|err| {
|
||||
format!(
|
||||
"Failed to decode {} initial authorities set: {:?}",
|
||||
SourceChain::NAME,
|
||||
err
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(InitializationData {
|
||||
header: initial_header,
|
||||
authority_list: initial_authorities_set,
|
||||
set_id: initial_authorities_set_id.unwrap_or(0),
|
||||
// There may be multiple scheduled changes, so on real chains we should select proper
|
||||
// moment, when there's nothing scheduled. On ephemeral (temporary) chains, it is ok to
|
||||
// start with genesis.
|
||||
scheduled_change: None,
|
||||
is_halted: false,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,400 @@
|
||||
// Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Substrate-to-Substrate headers synchronization maintain procedure.
|
||||
//!
|
||||
//! Regular headers synchronization only depends on persistent justifications
|
||||
//! that are generated when authorities set changes. This happens rarely on
|
||||
//! real-word chains. So some other way to finalize headers is required.
|
||||
//!
|
||||
//! Full nodes are listening to GRANDPA messages, so they may have track authorities
|
||||
//! votes on their own. They're returning both persistent and ephemeral justifications
|
||||
//! (justifications that are not stored in the database and not broadcasted over network)
|
||||
//! throught `grandpa_subscribeJustifications` RPC subscription.
|
||||
//!
|
||||
//! The idea of this maintain procedure is that when we see justification that 'improves'
|
||||
//! best finalized header on the target chain, we submit this justification to the target
|
||||
//! node.
|
||||
|
||||
use crate::headers_pipeline::SubstrateHeadersSyncPipeline;
|
||||
|
||||
use async_std::sync::{Arc, Mutex};
|
||||
use async_trait::async_trait;
|
||||
use codec::{Decode, Encode};
|
||||
use futures::future::{poll_fn, FutureExt, TryFutureExt};
|
||||
use headers_relay::{
|
||||
sync::HeadersSync,
|
||||
sync_loop::SyncMaintain,
|
||||
sync_types::{HeaderIdOf, HeaderStatus},
|
||||
};
|
||||
use relay_substrate_client::{Chain, Client, Error as SubstrateError, JustificationsSubscription};
|
||||
use relay_utils::HeaderId;
|
||||
use sp_core::Bytes;
|
||||
use sp_runtime::{traits::Header as HeaderT, Justification};
|
||||
use std::{collections::VecDeque, marker::PhantomData, task::Poll};
|
||||
|
||||
/// Substrate-to-Substrate headers synchronization maintain procedure.
|
||||
pub struct SubstrateHeadersToSubstrateMaintain<P: SubstrateHeadersSyncPipeline, SourceChain, TargetChain: Chain> {
|
||||
pipeline: P,
|
||||
target_client: Client<TargetChain>,
|
||||
justifications: Arc<Mutex<Justifications<P>>>,
|
||||
_marker: PhantomData<SourceChain>,
|
||||
}
|
||||
|
||||
/// Future and already received justifications from the source chain.
|
||||
struct Justifications<P: SubstrateHeadersSyncPipeline> {
|
||||
/// Justifications stream.
|
||||
stream: JustificationsSubscription,
|
||||
/// Justifications that we have read from the stream but have not sent to the
|
||||
/// target node, because their targets were still not synced.
|
||||
queue: VecDeque<(HeaderIdOf<P>, Justification)>,
|
||||
}
|
||||
|
||||
impl<P: SubstrateHeadersSyncPipeline, SourceChain, TargetChain: Chain>
|
||||
SubstrateHeadersToSubstrateMaintain<P, SourceChain, TargetChain>
|
||||
{
|
||||
/// Create new maintain procedure.
|
||||
pub fn new(pipeline: P, target_client: Client<TargetChain>, justifications: JustificationsSubscription) -> Self {
|
||||
SubstrateHeadersToSubstrateMaintain {
|
||||
pipeline,
|
||||
target_client,
|
||||
justifications: Arc::new(Mutex::new(Justifications {
|
||||
stream: justifications,
|
||||
queue: VecDeque::new(),
|
||||
})),
|
||||
_marker: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<P: SubstrateHeadersSyncPipeline, SourceChain, TargetChain: Chain> Clone
|
||||
for SubstrateHeadersToSubstrateMaintain<P, SourceChain, TargetChain>
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
SubstrateHeadersToSubstrateMaintain {
|
||||
pipeline: self.pipeline.clone(),
|
||||
target_client: self.target_client.clone(),
|
||||
justifications: self.justifications.clone(),
|
||||
_marker: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<P, SourceChain, TargetChain> SyncMaintain<P> for SubstrateHeadersToSubstrateMaintain<P, SourceChain, TargetChain>
|
||||
where
|
||||
SourceChain: Chain,
|
||||
<SourceChain::Header as HeaderT>::Number: Into<P::Number>,
|
||||
<SourceChain::Header as HeaderT>::Hash: Into<P::Hash>,
|
||||
TargetChain: Chain,
|
||||
P::Number: Decode,
|
||||
P::Hash: Decode,
|
||||
P: SubstrateHeadersSyncPipeline<Completion = Justification, Extra = ()>,
|
||||
{
|
||||
async fn maintain(&self, sync: &mut HeadersSync<P>) {
|
||||
// lock justifications before doing anything else
|
||||
let mut justifications = match self.justifications.try_lock() {
|
||||
Some(justifications) => justifications,
|
||||
None => {
|
||||
// this should never happen, as we use single-thread executor
|
||||
log::warn!(target: "bridge", "Failed to acquire {} justifications lock", P::SOURCE_NAME);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// we need to read best finalized header from the target node to be able to
|
||||
// choose justification to submit
|
||||
let best_finalized = match best_finalized_header_id::<P, _>(&self.target_client).await {
|
||||
Ok(best_finalized) => best_finalized,
|
||||
Err(error) => {
|
||||
log::warn!(
|
||||
target: "bridge",
|
||||
"Failed to read best finalized {} block from maintain: {:?}",
|
||||
P::SOURCE_NAME,
|
||||
error,
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
log::debug!(
|
||||
target: "bridge",
|
||||
"Read best finalized {} block from {}: {:?}",
|
||||
P::SOURCE_NAME,
|
||||
P::TARGET_NAME,
|
||||
best_finalized,
|
||||
);
|
||||
|
||||
// Select justification to submit to the target node. We're submitting at most one justification
|
||||
// on every maintain call. So maintain rate directly affects finalization rate.
|
||||
let justification_to_submit = poll_fn(|context| {
|
||||
// read justifications from the stream and push to the queue
|
||||
justifications.read_from_stream::<SourceChain::Header>(context);
|
||||
|
||||
// remove all obsolete justifications from the queue
|
||||
remove_obsolete::<P>(&mut justifications.queue, best_finalized);
|
||||
|
||||
// select justification to submit
|
||||
Poll::Ready(select_justification(&mut justifications.queue, sync))
|
||||
})
|
||||
.await;
|
||||
|
||||
// finally - submit selected justification
|
||||
if let Some((target, justification)) = justification_to_submit {
|
||||
let submit_result = self
|
||||
.pipeline
|
||||
.make_complete_header_transaction(target, justification)
|
||||
.and_then(|tx| self.target_client.submit_extrinsic(Bytes(tx.encode())))
|
||||
.await;
|
||||
|
||||
match submit_result {
|
||||
Ok(_) => log::debug!(
|
||||
target: "bridge",
|
||||
"Submitted justification received over {} subscription. Target: {:?}",
|
||||
P::SOURCE_NAME,
|
||||
target,
|
||||
),
|
||||
Err(error) => log::warn!(
|
||||
target: "bridge",
|
||||
"Failed to submit justification received over {} subscription for {:?}: {:?}",
|
||||
P::SOURCE_NAME,
|
||||
target,
|
||||
error,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<P> Justifications<P>
|
||||
where
|
||||
P::Number: Decode,
|
||||
P::Hash: Decode,
|
||||
P: SubstrateHeadersSyncPipeline<Completion = Justification, Extra = ()>,
|
||||
{
|
||||
/// Read justifications from the subscription stream without blocking.
|
||||
fn read_from_stream<'a, SourceHeader>(&mut self, context: &mut std::task::Context<'a>)
|
||||
where
|
||||
SourceHeader: HeaderT,
|
||||
SourceHeader::Number: Into<P::Number>,
|
||||
SourceHeader::Hash: Into<P::Hash>,
|
||||
{
|
||||
loop {
|
||||
let maybe_next_justification = self.stream.next();
|
||||
futures::pin_mut!(maybe_next_justification);
|
||||
|
||||
let maybe_next_justification = maybe_next_justification.poll_unpin(context);
|
||||
let justification = match maybe_next_justification {
|
||||
Poll::Ready(justification) => justification,
|
||||
Poll::Pending => return,
|
||||
};
|
||||
|
||||
// decode justification target
|
||||
let target = bp_header_chain::justification::decode_justification_target::<SourceHeader>(&justification);
|
||||
let target = match target {
|
||||
Ok((target_hash, target_number)) => HeaderId(target_number.into(), target_hash.into()),
|
||||
Err(error) => {
|
||||
log::warn!(
|
||||
target: "bridge",
|
||||
"Failed to decode justification from {} subscription: {:?}",
|
||||
P::SOURCE_NAME,
|
||||
error,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
log::debug!(
|
||||
target: "bridge",
|
||||
"Received {} justification over subscription. Target: {:?}",
|
||||
P::SOURCE_NAME,
|
||||
target,
|
||||
);
|
||||
|
||||
self.queue.push_back((target, justification.0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Clean queue of all justifications that are justifying already finalized blocks.
|
||||
fn remove_obsolete<P: SubstrateHeadersSyncPipeline>(
|
||||
queue: &mut VecDeque<(HeaderIdOf<P>, Justification)>,
|
||||
best_finalized: HeaderIdOf<P>,
|
||||
) {
|
||||
while queue
|
||||
.front()
|
||||
.map(|(target, _)| target.0 <= best_finalized.0)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
queue.pop_front();
|
||||
}
|
||||
}
|
||||
|
||||
/// Select appropriate justification that would improve best finalized block on target node.
|
||||
///
|
||||
/// It is assumed that the selected justification will be submitted to the target node. The
|
||||
/// justification itself and all preceeding justifications are removed from the queue.
|
||||
fn select_justification<P>(
|
||||
queue: &mut VecDeque<(HeaderIdOf<P>, Justification)>,
|
||||
sync: &mut HeadersSync<P>,
|
||||
) -> Option<(HeaderIdOf<P>, Justification)>
|
||||
where
|
||||
P: SubstrateHeadersSyncPipeline<Completion = Justification>,
|
||||
{
|
||||
let mut selected_justification = None;
|
||||
while let Some((target, justification)) = queue.pop_front() {
|
||||
// if we're waiting for this justification, report it
|
||||
if sync.headers().requires_completion_data(&target) {
|
||||
sync.headers_mut().completion_response(&target, Some(justification));
|
||||
// we won't submit previous justifications as we going to submit justification for
|
||||
// next header
|
||||
selected_justification = None;
|
||||
// we won't submit next justifications as we need to submit previous justifications
|
||||
// first
|
||||
break;
|
||||
}
|
||||
|
||||
// if we know that the header is already synced (it is known to the target node), let's
|
||||
// select it for submission. We still may select better justification on the next iteration.
|
||||
if sync.headers().status(&target) == HeaderStatus::Synced {
|
||||
selected_justification = Some((target, justification));
|
||||
continue;
|
||||
}
|
||||
|
||||
// finally - return justification back to the queue
|
||||
queue.push_back((target, justification));
|
||||
break;
|
||||
}
|
||||
|
||||
selected_justification
|
||||
}
|
||||
|
||||
/// Returns best finalized source header on the target chain.
|
||||
async fn best_finalized_header_id<P, C>(client: &Client<C>) -> Result<HeaderIdOf<P>, SubstrateError>
|
||||
where
|
||||
P: SubstrateHeadersSyncPipeline,
|
||||
P::Number: Decode,
|
||||
P::Hash: Decode,
|
||||
C: Chain,
|
||||
{
|
||||
let call = P::FINALIZED_BLOCK_METHOD.into();
|
||||
let data = Bytes(Vec::new());
|
||||
|
||||
let encoded_response = client.state_call(call, data, None).await?;
|
||||
let decoded_response: (P::Number, P::Hash) =
|
||||
Decode::decode(&mut &encoded_response.0[..]).map_err(SubstrateError::ResponseParseFailed)?;
|
||||
|
||||
let best_header_id = HeaderId(decoded_response.0, decoded_response.1);
|
||||
Ok(best_header_id)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::headers_pipeline::sync_params;
|
||||
use crate::millau_headers_to_rialto::MillauHeadersToRialto;
|
||||
|
||||
fn parent_hash(index: u8) -> bp_millau::Hash {
|
||||
if index == 1 {
|
||||
Default::default()
|
||||
} else {
|
||||
header(index - 1).hash()
|
||||
}
|
||||
}
|
||||
|
||||
fn header_hash(index: u8) -> bp_millau::Hash {
|
||||
header(index).hash()
|
||||
}
|
||||
|
||||
fn header(index: u8) -> bp_millau::Header {
|
||||
bp_millau::Header::new(
|
||||
index as _,
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
parent_hash(index),
|
||||
Default::default(),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn obsolete_justifications_are_removed() {
|
||||
let mut queue = vec![
|
||||
(HeaderId(1, header_hash(1)), vec![1]),
|
||||
(HeaderId(2, header_hash(2)), vec![2]),
|
||||
(HeaderId(3, header_hash(3)), vec![3]),
|
||||
]
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
remove_obsolete::<MillauHeadersToRialto>(&mut queue, HeaderId(2, header_hash(2)));
|
||||
|
||||
assert_eq!(
|
||||
queue,
|
||||
vec![(HeaderId(3, header_hash(3)), vec![3])]
|
||||
.into_iter()
|
||||
.collect::<VecDeque<_>>(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn latest_justification_is_selected() {
|
||||
let mut queue = vec![
|
||||
(HeaderId(1, header_hash(1)), vec![1]),
|
||||
(HeaderId(2, header_hash(2)), vec![2]),
|
||||
(HeaderId(3, header_hash(3)), vec![3]),
|
||||
]
|
||||
.into_iter()
|
||||
.collect();
|
||||
let mut sync = HeadersSync::<MillauHeadersToRialto>::new(sync_params());
|
||||
sync.headers_mut().header_response(header(1).into());
|
||||
sync.headers_mut().header_response(header(2).into());
|
||||
sync.headers_mut().header_response(header(3).into());
|
||||
sync.target_best_header_response(HeaderId(2, header_hash(2)));
|
||||
|
||||
assert_eq!(
|
||||
select_justification(&mut queue, &mut sync),
|
||||
Some((HeaderId(2, header_hash(2)), vec![2])),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn required_justification_is_reported() {
|
||||
let mut queue = vec![
|
||||
(HeaderId(1, header_hash(1)), vec![1]),
|
||||
(HeaderId(2, header_hash(2)), vec![2]),
|
||||
(HeaderId(3, header_hash(3)), vec![3]),
|
||||
]
|
||||
.into_iter()
|
||||
.collect();
|
||||
let mut sync = HeadersSync::<MillauHeadersToRialto>::new(sync_params());
|
||||
sync.headers_mut().header_response(header(1).into());
|
||||
sync.headers_mut().header_response(header(2).into());
|
||||
sync.headers_mut().header_response(header(3).into());
|
||||
sync.headers_mut()
|
||||
.incomplete_headers_response(vec![HeaderId(2, header_hash(2))].into_iter().collect());
|
||||
sync.target_best_header_response(HeaderId(2, header_hash(2)));
|
||||
|
||||
assert_eq!(sync.headers_mut().header_to_complete(), None,);
|
||||
|
||||
assert_eq!(select_justification(&mut queue, &mut sync), None,);
|
||||
|
||||
assert_eq!(
|
||||
sync.headers_mut().header_to_complete(),
|
||||
Some((HeaderId(2, header_hash(2)), &vec![2])),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
// Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Substrate-to-Substrate headers sync entrypoint.
|
||||
|
||||
use crate::{headers_maintain::SubstrateHeadersToSubstrateMaintain, headers_target::SubstrateHeadersTarget};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use codec::Encode;
|
||||
use headers_relay::{
|
||||
sync::{HeadersSyncParams, TargetTransactionMode},
|
||||
sync_types::{HeaderIdOf, HeadersSyncPipeline, QueuedHeader, SourceHeader},
|
||||
};
|
||||
use relay_substrate_client::{
|
||||
headers_source::HeadersSource, BlockNumberOf, Chain, Client, Error as SubstrateError, HashOf,
|
||||
};
|
||||
use relay_utils::BlockNumberBase;
|
||||
use sp_runtime::Justification;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
/// Headers sync pipeline for Substrate <-> Substrate relays.
|
||||
#[async_trait]
|
||||
pub trait SubstrateHeadersSyncPipeline: HeadersSyncPipeline {
|
||||
/// Name of the `best_block` runtime method.
|
||||
const BEST_BLOCK_METHOD: &'static str;
|
||||
/// Name of the `finalized_block` runtime method.
|
||||
const FINALIZED_BLOCK_METHOD: &'static str;
|
||||
/// Name of the `is_known_block` runtime method.
|
||||
const IS_KNOWN_BLOCK_METHOD: &'static str;
|
||||
/// Name of the `incomplete_headers` runtime method.
|
||||
const INCOMPLETE_HEADERS_METHOD: &'static str;
|
||||
|
||||
/// Signed transaction type.
|
||||
type SignedTransaction: Send + Sync + Encode;
|
||||
|
||||
/// Make submit header transaction.
|
||||
async fn make_submit_header_transaction(
|
||||
&self,
|
||||
header: QueuedHeader<Self>,
|
||||
) -> Result<Self::SignedTransaction, SubstrateError>;
|
||||
|
||||
/// Make completion transaction for the header.
|
||||
async fn make_complete_header_transaction(
|
||||
&self,
|
||||
id: HeaderIdOf<Self>,
|
||||
completion: Justification,
|
||||
) -> Result<Self::SignedTransaction, SubstrateError>;
|
||||
}
|
||||
|
||||
/// Substrate-to-Substrate headers pipeline.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SubstrateHeadersToSubstrate<SourceChain, SourceSyncHeader, TargetChain: Chain, TargetSign> {
|
||||
/// Client for the target chain.
|
||||
pub(crate) target_client: Client<TargetChain>,
|
||||
/// Data required to sign target chain transactions.
|
||||
pub(crate) target_sign: TargetSign,
|
||||
/// Unused generic arguments dump.
|
||||
_marker: PhantomData<(SourceChain, SourceSyncHeader)>,
|
||||
}
|
||||
|
||||
impl<SourceChain, SourceSyncHeader, TargetChain: Chain, TargetSign>
|
||||
SubstrateHeadersToSubstrate<SourceChain, SourceSyncHeader, TargetChain, TargetSign>
|
||||
{
|
||||
/// Create new Substrate-to-Substrate headers pipeline.
|
||||
pub fn new(target_client: Client<TargetChain>, target_sign: TargetSign) -> Self {
|
||||
SubstrateHeadersToSubstrate {
|
||||
target_client,
|
||||
target_sign,
|
||||
_marker: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<SourceChain, SourceSyncHeader, TargetChain, TargetSign> HeadersSyncPipeline
|
||||
for SubstrateHeadersToSubstrate<SourceChain, SourceSyncHeader, TargetChain, TargetSign>
|
||||
where
|
||||
SourceChain: Clone + Chain,
|
||||
BlockNumberOf<SourceChain>: BlockNumberBase,
|
||||
SourceSyncHeader:
|
||||
SourceHeader<HashOf<SourceChain>, BlockNumberOf<SourceChain>> + std::ops::Deref<Target = SourceChain::Header>,
|
||||
TargetChain: Clone + Chain,
|
||||
TargetSign: Clone + Send + Sync,
|
||||
{
|
||||
const SOURCE_NAME: &'static str = SourceChain::NAME;
|
||||
const TARGET_NAME: &'static str = TargetChain::NAME;
|
||||
|
||||
type Hash = HashOf<SourceChain>;
|
||||
type Number = BlockNumberOf<SourceChain>;
|
||||
type Header = SourceSyncHeader;
|
||||
type Extra = ();
|
||||
type Completion = Justification;
|
||||
|
||||
fn estimate_size(source: &QueuedHeader<Self>) -> usize {
|
||||
source.header().encode().len()
|
||||
}
|
||||
}
|
||||
|
||||
/// Return sync parameters for Substrate-to-Substrate headers sync.
|
||||
pub fn sync_params() -> HeadersSyncParams {
|
||||
HeadersSyncParams {
|
||||
max_future_headers_to_download: 32,
|
||||
max_headers_in_submitted_status: 8,
|
||||
max_headers_in_single_submit: 1,
|
||||
max_headers_size_in_single_submit: 1024 * 1024,
|
||||
prune_depth: 256,
|
||||
target_tx_mode: TargetTransactionMode::Signed,
|
||||
}
|
||||
}
|
||||
|
||||
/// Run Substrate-to-Substrate headers sync.
|
||||
pub async fn run<SourceChain, TargetChain, P>(
|
||||
pipeline: P,
|
||||
source_client: Client<SourceChain>,
|
||||
target_client: Client<TargetChain>,
|
||||
metrics_params: Option<relay_utils::metrics::MetricsParams>,
|
||||
) where
|
||||
P: SubstrateHeadersSyncPipeline<
|
||||
Hash = HashOf<SourceChain>,
|
||||
Number = BlockNumberOf<SourceChain>,
|
||||
Completion = Justification,
|
||||
Extra = (),
|
||||
>,
|
||||
P::Header: SourceHeader<HashOf<SourceChain>, BlockNumberOf<SourceChain>>,
|
||||
SourceChain: Clone + Chain,
|
||||
SourceChain::Header: Into<P::Header>,
|
||||
BlockNumberOf<SourceChain>: BlockNumberBase,
|
||||
TargetChain: Clone + Chain,
|
||||
{
|
||||
let source_justifications = match source_client.clone().subscribe_justifications().await {
|
||||
Ok(source_justifications) => source_justifications,
|
||||
Err(error) => {
|
||||
log::warn!(
|
||||
target: "bridge",
|
||||
"Failed to subscribe to {} justifications: {:?}",
|
||||
SourceChain::NAME,
|
||||
error,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let sync_maintain = SubstrateHeadersToSubstrateMaintain::<_, SourceChain, _>::new(
|
||||
pipeline.clone(),
|
||||
target_client.clone(),
|
||||
source_justifications,
|
||||
);
|
||||
|
||||
log::info!(
|
||||
target: "bridge",
|
||||
"Starting {} -> {} headers relay",
|
||||
SourceChain::NAME,
|
||||
TargetChain::NAME,
|
||||
);
|
||||
|
||||
headers_relay::sync_loop::run(
|
||||
HeadersSource::new(source_client),
|
||||
SourceChain::AVERAGE_BLOCK_INTERVAL,
|
||||
SubstrateHeadersTarget::new(target_client, pipeline),
|
||||
TargetChain::AVERAGE_BLOCK_INTERVAL,
|
||||
sync_maintain,
|
||||
sync_params(),
|
||||
metrics_params,
|
||||
futures::future::pending(),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
// Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Substrate client as Substrate headers target. The chain we connect to should have
|
||||
//! runtime that implements `<BridgedChainName>HeaderApi` to allow bridging with
|
||||
//! <BridgedName> chain.
|
||||
|
||||
use crate::headers_pipeline::SubstrateHeadersSyncPipeline;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use codec::{Decode, Encode};
|
||||
use futures::TryFutureExt;
|
||||
use headers_relay::{
|
||||
sync_loop::TargetClient,
|
||||
sync_types::{HeaderIdOf, QueuedHeader, SubmittedHeaders},
|
||||
};
|
||||
use relay_substrate_client::{Chain, Client, Error as SubstrateError};
|
||||
use relay_utils::{relay_loop::Client as RelayClient, HeaderId};
|
||||
use sp_core::Bytes;
|
||||
use sp_runtime::Justification;
|
||||
use std::collections::HashSet;
|
||||
|
||||
/// Substrate client as Substrate headers target.
|
||||
pub struct SubstrateHeadersTarget<C: Chain, P> {
|
||||
client: Client<C>,
|
||||
pipeline: P,
|
||||
}
|
||||
|
||||
impl<C: Chain, P> SubstrateHeadersTarget<C, P> {
|
||||
/// Create new Substrate headers target.
|
||||
pub fn new(client: Client<C>, pipeline: P) -> Self {
|
||||
SubstrateHeadersTarget { client, pipeline }
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Chain, P: SubstrateHeadersSyncPipeline> Clone for SubstrateHeadersTarget<C, P> {
|
||||
fn clone(&self) -> Self {
|
||||
SubstrateHeadersTarget {
|
||||
client: self.client.clone(),
|
||||
pipeline: self.pipeline.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<C: Chain, P: SubstrateHeadersSyncPipeline> RelayClient for SubstrateHeadersTarget<C, P> {
|
||||
type Error = SubstrateError;
|
||||
|
||||
async fn reconnect(&mut self) -> Result<(), SubstrateError> {
|
||||
self.client.reconnect().await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<C, P> TargetClient<P> for SubstrateHeadersTarget<C, P>
|
||||
where
|
||||
C: Chain,
|
||||
P::Number: Decode,
|
||||
P::Hash: Decode + Encode,
|
||||
P: SubstrateHeadersSyncPipeline<Completion = Justification, Extra = ()>,
|
||||
{
|
||||
async fn best_header_id(&self) -> Result<HeaderIdOf<P>, 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());
|
||||
|
||||
let encoded_response = self.client.state_call(call, data, None).await?;
|
||||
let decoded_response: Vec<(P::Number, P::Hash)> =
|
||||
Decode::decode(&mut &encoded_response.0[..]).map_err(SubstrateError::ResponseParseFailed)?;
|
||||
|
||||
// If we parse an empty list of headers it means that bridge pallet has not been initalized
|
||||
// yet. Otherwise we expect to always have at least one header.
|
||||
decoded_response
|
||||
.last()
|
||||
.ok_or(SubstrateError::UninitializedBridgePallet)
|
||||
.map(|(num, hash)| HeaderId(*num, *hash))
|
||||
}
|
||||
|
||||
async fn is_known_header(&self, id: HeaderIdOf<P>) -> Result<(HeaderIdOf<P>, bool), SubstrateError> {
|
||||
let call = P::IS_KNOWN_BLOCK_METHOD.into();
|
||||
let data = Bytes(id.1.encode());
|
||||
|
||||
let encoded_response = self.client.state_call(call, data, None).await?;
|
||||
let is_known_block: bool =
|
||||
Decode::decode(&mut &encoded_response.0[..]).map_err(SubstrateError::ResponseParseFailed)?;
|
||||
|
||||
Ok((id, is_known_block))
|
||||
}
|
||||
|
||||
async fn submit_headers(
|
||||
&self,
|
||||
mut headers: Vec<QueuedHeader<P>>,
|
||||
) -> SubmittedHeaders<HeaderIdOf<P>, SubstrateError> {
|
||||
debug_assert_eq!(
|
||||
headers.len(),
|
||||
1,
|
||||
"Substrate pallet only supports single header / transaction"
|
||||
);
|
||||
|
||||
let header = headers.remove(0);
|
||||
let id = header.id();
|
||||
let submit_transaction_result = self
|
||||
.pipeline
|
||||
.make_submit_header_transaction(header)
|
||||
.and_then(|tx| self.client.submit_extrinsic(Bytes(tx.encode())))
|
||||
.await;
|
||||
|
||||
match submit_transaction_result {
|
||||
Ok(_) => SubmittedHeaders {
|
||||
submitted: vec![id],
|
||||
incomplete: Vec::new(),
|
||||
rejected: Vec::new(),
|
||||
fatal_error: None,
|
||||
},
|
||||
Err(error) => SubmittedHeaders {
|
||||
submitted: Vec::new(),
|
||||
incomplete: Vec::new(),
|
||||
rejected: vec![id],
|
||||
fatal_error: Some(error),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async fn incomplete_headers_ids(&self) -> Result<HashSet<HeaderIdOf<P>>, SubstrateError> {
|
||||
let call = P::INCOMPLETE_HEADERS_METHOD.into();
|
||||
let data = Bytes(Vec::new());
|
||||
|
||||
let encoded_response = self.client.state_call(call, data, None).await?;
|
||||
let decoded_response: Vec<(P::Number, P::Hash)> =
|
||||
Decode::decode(&mut &encoded_response.0[..]).map_err(SubstrateError::ResponseParseFailed)?;
|
||||
|
||||
let incomplete_headers = decoded_response
|
||||
.into_iter()
|
||||
.map(|(number, hash)| HeaderId(number, hash))
|
||||
.collect();
|
||||
Ok(incomplete_headers)
|
||||
}
|
||||
|
||||
async fn complete_header(
|
||||
&self,
|
||||
id: HeaderIdOf<P>,
|
||||
completion: Justification,
|
||||
) -> Result<HeaderIdOf<P>, SubstrateError> {
|
||||
let tx = self.pipeline.make_complete_header_transaction(id, completion).await?;
|
||||
self.client.submit_extrinsic(Bytes(tx.encode())).await?;
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
async fn requires_extra(&self, header: QueuedHeader<P>) -> Result<(HeaderIdOf<P>, bool), SubstrateError> {
|
||||
Ok((header.id(), false))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,709 @@
|
||||
// Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Substrate-to-substrate relay entrypoint.
|
||||
|
||||
#![warn(missing_docs)]
|
||||
|
||||
use codec::{Decode, Encode};
|
||||
use frame_support::weights::{GetDispatchInfo, Weight};
|
||||
use pallet_bridge_call_dispatch::{CallOrigin, MessagePayload};
|
||||
use relay_kusama_client::Kusama;
|
||||
use relay_millau_client::{Millau, SigningParams as MillauSigningParams};
|
||||
use relay_rialto_client::{Rialto, SigningParams as RialtoSigningParams};
|
||||
use relay_substrate_client::{Chain, ConnectionParams, TransactionSignScheme};
|
||||
use relay_utils::initialize::initialize_relay;
|
||||
use sp_core::{Bytes, Pair};
|
||||
use sp_runtime::traits::IdentifyAccount;
|
||||
|
||||
/// Kusama node client.
|
||||
pub type KusamaClient = relay_substrate_client::Client<Kusama>;
|
||||
/// Millau node client.
|
||||
pub type MillauClient = relay_substrate_client::Client<Millau>;
|
||||
/// Rialto node client.
|
||||
pub type RialtoClient = relay_substrate_client::Client<Rialto>;
|
||||
|
||||
mod cli;
|
||||
mod headers_initialize;
|
||||
mod headers_maintain;
|
||||
mod headers_pipeline;
|
||||
mod headers_target;
|
||||
mod messages_lane;
|
||||
mod messages_source;
|
||||
mod messages_target;
|
||||
mod millau_headers_to_rialto;
|
||||
mod millau_messages_to_rialto;
|
||||
mod rialto_headers_to_millau;
|
||||
mod rialto_messages_to_millau;
|
||||
|
||||
fn main() {
|
||||
initialize_relay();
|
||||
|
||||
let result = async_std::task::block_on(run_command(cli::parse_args()));
|
||||
if let Err(error) = result {
|
||||
log::error!(target: "bridge", "Failed to start relay: {}", error);
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_command(command: cli::Command) -> Result<(), String> {
|
||||
match command {
|
||||
cli::Command::InitBridge(arg) => run_init_bridge(arg).await,
|
||||
cli::Command::RelayHeaders(arg) => run_relay_headers(arg).await,
|
||||
cli::Command::RelayMessages(arg) => run_relay_messages(arg).await,
|
||||
cli::Command::SendMessage(arg) => run_send_message(arg).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_init_bridge(command: cli::InitBridge) -> Result<(), String> {
|
||||
match command {
|
||||
cli::InitBridge::MillauToRialto {
|
||||
millau,
|
||||
rialto,
|
||||
rialto_sign,
|
||||
millau_bridge_params,
|
||||
} => {
|
||||
let millau_client = millau.into_client().await?;
|
||||
let rialto_client = rialto.into_client().await?;
|
||||
let rialto_sign = rialto_sign.parse()?;
|
||||
|
||||
let rialto_signer_next_index = rialto_client
|
||||
.next_account_index(rialto_sign.signer.public().into())
|
||||
.await?;
|
||||
|
||||
headers_initialize::initialize(
|
||||
millau_client,
|
||||
rialto_client.clone(),
|
||||
millau_bridge_params.millau_initial_header,
|
||||
millau_bridge_params.millau_initial_authorities,
|
||||
millau_bridge_params.millau_initial_authorities_set_id,
|
||||
move |initialization_data| {
|
||||
Ok(Bytes(
|
||||
Rialto::sign_transaction(
|
||||
&rialto_client,
|
||||
&rialto_sign.signer,
|
||||
rialto_signer_next_index,
|
||||
rialto_runtime::SudoCall::sudo(Box::new(
|
||||
rialto_runtime::BridgeMillauCall::initialize(initialization_data).into(),
|
||||
))
|
||||
.into(),
|
||||
)
|
||||
.encode(),
|
||||
))
|
||||
},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
cli::InitBridge::RialtoToMillau {
|
||||
rialto,
|
||||
millau,
|
||||
millau_sign,
|
||||
rialto_bridge_params,
|
||||
} => {
|
||||
let rialto_client = rialto.into_client().await?;
|
||||
let millau_client = millau.into_client().await?;
|
||||
let millau_sign = millau_sign.parse()?;
|
||||
let millau_signer_next_index = millau_client
|
||||
.next_account_index(millau_sign.signer.public().into())
|
||||
.await?;
|
||||
|
||||
headers_initialize::initialize(
|
||||
rialto_client,
|
||||
millau_client.clone(),
|
||||
rialto_bridge_params.rialto_initial_header,
|
||||
rialto_bridge_params.rialto_initial_authorities,
|
||||
rialto_bridge_params.rialto_initial_authorities_set_id,
|
||||
move |initialization_data| {
|
||||
Ok(Bytes(
|
||||
Millau::sign_transaction(
|
||||
&millau_client,
|
||||
&millau_sign.signer,
|
||||
millau_signer_next_index,
|
||||
millau_runtime::SudoCall::sudo(Box::new(
|
||||
millau_runtime::BridgeRialtoCall::initialize(initialization_data).into(),
|
||||
))
|
||||
.into(),
|
||||
)
|
||||
.encode(),
|
||||
))
|
||||
},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run_relay_headers(command: cli::RelayHeaders) -> Result<(), String> {
|
||||
match command {
|
||||
cli::RelayHeaders::MillauToRialto {
|
||||
millau,
|
||||
rialto,
|
||||
rialto_sign,
|
||||
prometheus_params,
|
||||
} => {
|
||||
let millau_client = millau.into_client().await?;
|
||||
let rialto_client = rialto.into_client().await?;
|
||||
let rialto_sign = rialto_sign.parse()?;
|
||||
millau_headers_to_rialto::run(millau_client, rialto_client, rialto_sign, prometheus_params.into()).await;
|
||||
}
|
||||
cli::RelayHeaders::RialtoToMillau {
|
||||
rialto,
|
||||
millau,
|
||||
millau_sign,
|
||||
prometheus_params,
|
||||
} => {
|
||||
let rialto_client = rialto.into_client().await?;
|
||||
let millau_client = millau.into_client().await?;
|
||||
let millau_sign = millau_sign.parse()?;
|
||||
rialto_headers_to_millau::run(rialto_client, millau_client, millau_sign, prometheus_params.into()).await;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run_relay_messages(command: cli::RelayMessages) -> Result<(), String> {
|
||||
match command {
|
||||
cli::RelayMessages::MillauToRialto {
|
||||
millau,
|
||||
millau_sign,
|
||||
rialto,
|
||||
rialto_sign,
|
||||
prometheus_params,
|
||||
lane,
|
||||
} => {
|
||||
let millau_client = millau.into_client().await?;
|
||||
let millau_sign = millau_sign.parse()?;
|
||||
let rialto_client = rialto.into_client().await?;
|
||||
let rialto_sign = rialto_sign.parse()?;
|
||||
|
||||
millau_messages_to_rialto::run(
|
||||
millau_client,
|
||||
millau_sign,
|
||||
rialto_client,
|
||||
rialto_sign,
|
||||
lane.into(),
|
||||
prometheus_params.into(),
|
||||
);
|
||||
}
|
||||
cli::RelayMessages::RialtoToMillau {
|
||||
rialto,
|
||||
rialto_sign,
|
||||
millau,
|
||||
millau_sign,
|
||||
prometheus_params,
|
||||
lane,
|
||||
} => {
|
||||
let rialto_client = rialto.into_client().await?;
|
||||
let rialto_sign = rialto_sign.parse()?;
|
||||
let millau_client = millau.into_client().await?;
|
||||
let millau_sign = millau_sign.parse()?;
|
||||
|
||||
rialto_messages_to_millau::run(
|
||||
rialto_client,
|
||||
rialto_sign,
|
||||
millau_client,
|
||||
millau_sign,
|
||||
lane.into(),
|
||||
prometheus_params.into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run_send_message(command: cli::SendMessage) -> Result<(), String> {
|
||||
match command {
|
||||
cli::SendMessage::MillauToRialto {
|
||||
millau,
|
||||
millau_sign,
|
||||
rialto_sign,
|
||||
lane,
|
||||
message,
|
||||
dispatch_weight,
|
||||
fee,
|
||||
origin,
|
||||
..
|
||||
} => {
|
||||
let millau_client = millau.into_client().await?;
|
||||
let millau_sign = millau_sign.parse()?;
|
||||
let rialto_sign = rialto_sign.parse()?;
|
||||
let rialto_call = message.into_call();
|
||||
|
||||
let payload =
|
||||
millau_to_rialto_message_payload(&millau_sign, &rialto_sign, &rialto_call, origin, dispatch_weight);
|
||||
let dispatch_weight = payload.weight;
|
||||
|
||||
let lane = lane.into();
|
||||
let fee = get_fee(fee, || {
|
||||
estimate_message_delivery_and_dispatch_fee(
|
||||
&millau_client,
|
||||
bp_rialto::TO_RIALTO_ESTIMATE_MESSAGE_FEE_METHOD,
|
||||
lane,
|
||||
payload.clone(),
|
||||
)
|
||||
})
|
||||
.await?;
|
||||
|
||||
let millau_call = millau_runtime::Call::BridgeRialtoMessageLane(
|
||||
millau_runtime::MessageLaneCall::send_message(lane, payload, fee),
|
||||
);
|
||||
|
||||
let signed_millau_call = Millau::sign_transaction(
|
||||
&millau_client,
|
||||
&millau_sign.signer,
|
||||
millau_client
|
||||
.next_account_index(millau_sign.signer.public().clone().into())
|
||||
.await?,
|
||||
millau_call,
|
||||
)
|
||||
.encode();
|
||||
|
||||
log::info!(
|
||||
target: "bridge",
|
||||
"Sending message to Rialto. Size: {}. Dispatch weight: {}. Fee: {}",
|
||||
signed_millau_call.len(),
|
||||
dispatch_weight,
|
||||
fee,
|
||||
);
|
||||
|
||||
millau_client.submit_extrinsic(Bytes(signed_millau_call)).await?;
|
||||
}
|
||||
cli::SendMessage::RialtoToMillau {
|
||||
rialto,
|
||||
rialto_sign,
|
||||
millau_sign,
|
||||
lane,
|
||||
message,
|
||||
dispatch_weight,
|
||||
fee,
|
||||
origin,
|
||||
..
|
||||
} => {
|
||||
let rialto_client = rialto.into_client().await?;
|
||||
let rialto_sign = rialto_sign.parse()?;
|
||||
let millau_sign = millau_sign.parse()?;
|
||||
let millau_call = message.into_call();
|
||||
|
||||
let payload =
|
||||
rialto_to_millau_message_payload(&rialto_sign, &millau_sign, &millau_call, origin, dispatch_weight);
|
||||
let dispatch_weight = payload.weight;
|
||||
|
||||
let lane = lane.into();
|
||||
let fee = get_fee(fee, || {
|
||||
estimate_message_delivery_and_dispatch_fee(
|
||||
&rialto_client,
|
||||
bp_millau::TO_MILLAU_ESTIMATE_MESSAGE_FEE_METHOD,
|
||||
lane,
|
||||
payload.clone(),
|
||||
)
|
||||
})
|
||||
.await?;
|
||||
|
||||
let rialto_call = rialto_runtime::Call::BridgeMillauMessageLane(
|
||||
rialto_runtime::MessageLaneCall::send_message(lane, payload, fee),
|
||||
);
|
||||
|
||||
let signed_rialto_call = Rialto::sign_transaction(
|
||||
&rialto_client,
|
||||
&rialto_sign.signer,
|
||||
rialto_client
|
||||
.next_account_index(rialto_sign.signer.public().clone().into())
|
||||
.await?,
|
||||
rialto_call,
|
||||
)
|
||||
.encode();
|
||||
|
||||
log::info!(
|
||||
target: "bridge",
|
||||
"Sending message to Millau. Size: {}. Dispatch weight: {}. Fee: {}",
|
||||
signed_rialto_call.len(),
|
||||
dispatch_weight,
|
||||
fee,
|
||||
);
|
||||
|
||||
rialto_client.submit_extrinsic(Bytes(signed_rialto_call)).await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn estimate_message_delivery_and_dispatch_fee<Fee: Decode, C: Chain, P: Encode>(
|
||||
client: &relay_substrate_client::Client<C>,
|
||||
estimate_fee_method: &str,
|
||||
lane: bp_message_lane::LaneId,
|
||||
payload: P,
|
||||
) -> Result<Option<Fee>, relay_substrate_client::Error> {
|
||||
let encoded_response = client
|
||||
.state_call(estimate_fee_method.into(), (lane, payload).encode().into(), None)
|
||||
.await?;
|
||||
let decoded_response: Option<Fee> =
|
||||
Decode::decode(&mut &encoded_response.0[..]).map_err(relay_substrate_client::Error::ResponseParseFailed)?;
|
||||
Ok(decoded_response)
|
||||
}
|
||||
|
||||
fn remark_payload(remark_size: Option<cli::ExplicitOrMaximal<usize>>, maximal_allowed_size: u32) -> Vec<u8> {
|
||||
match remark_size {
|
||||
Some(cli::ExplicitOrMaximal::Explicit(remark_size)) => vec![0; remark_size],
|
||||
Some(cli::ExplicitOrMaximal::Maximal) => vec![0; maximal_allowed_size as _],
|
||||
None => format!(
|
||||
"Unix time: {}",
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::SystemTime::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs(),
|
||||
)
|
||||
.as_bytes()
|
||||
.to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
fn rialto_to_millau_message_payload(
|
||||
rialto_sign: &RialtoSigningParams,
|
||||
millau_sign: &MillauSigningParams,
|
||||
millau_call: &millau_runtime::Call,
|
||||
origin: cli::Origins,
|
||||
user_specified_dispatch_weight: Option<cli::ExplicitOrMaximal<Weight>>,
|
||||
) -> rialto_runtime::millau_messages::ToMillauMessagePayload {
|
||||
let millau_call_weight = prepare_call_dispatch_weight(
|
||||
user_specified_dispatch_weight,
|
||||
cli::ExplicitOrMaximal::Explicit(millau_call.get_dispatch_info().weight),
|
||||
compute_maximal_message_dispatch_weight(bp_millau::max_extrinsic_weight()),
|
||||
);
|
||||
let rialto_sender_public: bp_rialto::AccountSigner = rialto_sign.signer.public().clone().into();
|
||||
let rialto_account_id: bp_rialto::AccountId = rialto_sender_public.into_account();
|
||||
let millau_origin_public = millau_sign.signer.public();
|
||||
|
||||
MessagePayload {
|
||||
spec_version: millau_runtime::VERSION.spec_version,
|
||||
weight: millau_call_weight,
|
||||
origin: match origin {
|
||||
cli::Origins::Source => CallOrigin::SourceAccount(rialto_account_id),
|
||||
cli::Origins::Target => {
|
||||
let digest = rialto_runtime::millau_account_ownership_digest(
|
||||
&millau_call,
|
||||
rialto_account_id.clone(),
|
||||
millau_runtime::VERSION.spec_version,
|
||||
);
|
||||
|
||||
let digest_signature = millau_sign.signer.sign(&digest);
|
||||
|
||||
CallOrigin::TargetAccount(rialto_account_id, millau_origin_public.into(), digest_signature.into())
|
||||
}
|
||||
},
|
||||
call: millau_call.encode(),
|
||||
}
|
||||
}
|
||||
|
||||
fn millau_to_rialto_message_payload(
|
||||
millau_sign: &MillauSigningParams,
|
||||
rialto_sign: &RialtoSigningParams,
|
||||
rialto_call: &rialto_runtime::Call,
|
||||
origin: cli::Origins,
|
||||
user_specified_dispatch_weight: Option<cli::ExplicitOrMaximal<Weight>>,
|
||||
) -> millau_runtime::rialto_messages::ToRialtoMessagePayload {
|
||||
let rialto_call_weight = prepare_call_dispatch_weight(
|
||||
user_specified_dispatch_weight,
|
||||
cli::ExplicitOrMaximal::Explicit(rialto_call.get_dispatch_info().weight),
|
||||
compute_maximal_message_dispatch_weight(bp_rialto::max_extrinsic_weight()),
|
||||
);
|
||||
let millau_sender_public: bp_millau::AccountSigner = millau_sign.signer.public().clone().into();
|
||||
let millau_account_id: bp_millau::AccountId = millau_sender_public.into_account();
|
||||
let rialto_origin_public = rialto_sign.signer.public();
|
||||
|
||||
MessagePayload {
|
||||
spec_version: rialto_runtime::VERSION.spec_version,
|
||||
weight: rialto_call_weight,
|
||||
origin: match origin {
|
||||
cli::Origins::Source => CallOrigin::SourceAccount(millau_account_id),
|
||||
cli::Origins::Target => {
|
||||
let digest = millau_runtime::rialto_account_ownership_digest(
|
||||
&rialto_call,
|
||||
millau_account_id.clone(),
|
||||
rialto_runtime::VERSION.spec_version,
|
||||
);
|
||||
|
||||
let digest_signature = rialto_sign.signer.sign(&digest);
|
||||
|
||||
CallOrigin::TargetAccount(millau_account_id, rialto_origin_public.into(), digest_signature.into())
|
||||
}
|
||||
},
|
||||
call: rialto_call.encode(),
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare_call_dispatch_weight(
|
||||
user_specified_dispatch_weight: Option<cli::ExplicitOrMaximal<Weight>>,
|
||||
weight_from_pre_dispatch_call: cli::ExplicitOrMaximal<Weight>,
|
||||
maximal_allowed_weight: Weight,
|
||||
) -> Weight {
|
||||
match user_specified_dispatch_weight.unwrap_or(weight_from_pre_dispatch_call) {
|
||||
cli::ExplicitOrMaximal::Explicit(weight) => weight,
|
||||
cli::ExplicitOrMaximal::Maximal => maximal_allowed_weight,
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_fee<Fee, F, R, E>(fee: Option<Fee>, f: F) -> Result<Fee, String>
|
||||
where
|
||||
Fee: Decode,
|
||||
F: FnOnce() -> R,
|
||||
R: std::future::Future<Output = Result<Option<Fee>, E>>,
|
||||
E: std::fmt::Debug,
|
||||
{
|
||||
match fee {
|
||||
Some(fee) => Ok(fee),
|
||||
None => match f().await {
|
||||
Ok(Some(fee)) => Ok(fee),
|
||||
Ok(None) => Err("Failed to estimate message fee. Message is too heavy?".into()),
|
||||
Err(error) => Err(format!("Failed to estimate message fee: {:?}", error)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_maximal_message_dispatch_weight(maximal_extrinsic_weight: Weight) -> Weight {
|
||||
bridge_runtime_common::messages::target::maximal_incoming_message_dispatch_weight(maximal_extrinsic_weight)
|
||||
}
|
||||
|
||||
fn compute_maximal_message_arguments_size(
|
||||
maximal_source_extrinsic_size: u32,
|
||||
maximal_target_extrinsic_size: u32,
|
||||
) -> u32 {
|
||||
// assume that both signed extensions and other arguments fit 1KB
|
||||
let service_tx_bytes_on_source_chain = 1024;
|
||||
let maximal_source_extrinsic_size = maximal_source_extrinsic_size - service_tx_bytes_on_source_chain;
|
||||
let maximal_call_size =
|
||||
bridge_runtime_common::messages::target::maximal_incoming_message_size(maximal_target_extrinsic_size);
|
||||
let maximal_call_size = if maximal_call_size > maximal_source_extrinsic_size {
|
||||
maximal_source_extrinsic_size
|
||||
} else {
|
||||
maximal_call_size
|
||||
};
|
||||
|
||||
// bytes in Call encoding that are used to encode everything except arguments
|
||||
let service_bytes = 1 + 1 + 4;
|
||||
maximal_call_size - service_bytes
|
||||
}
|
||||
|
||||
impl crate::cli::RialtoSigningParams {
|
||||
/// Parse CLI parameters into typed signing params.
|
||||
pub fn parse(self) -> Result<RialtoSigningParams, String> {
|
||||
RialtoSigningParams::from_suri(&self.rialto_signer, self.rialto_signer_password.as_deref())
|
||||
.map_err(|e| format!("Failed to parse rialto-signer: {:?}", e))
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::cli::MillauSigningParams {
|
||||
/// Parse CLI parameters into typed signing params.
|
||||
pub fn parse(self) -> Result<MillauSigningParams, String> {
|
||||
MillauSigningParams::from_suri(&self.millau_signer, self.millau_signer_password.as_deref())
|
||||
.map_err(|e| format!("Failed to parse millau-signer: {:?}", e))
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::cli::MillauConnectionParams {
|
||||
/// Convert CLI connection parameters into Millau RPC Client.
|
||||
pub async fn into_client(self) -> relay_substrate_client::Result<MillauClient> {
|
||||
MillauClient::new(ConnectionParams {
|
||||
host: self.millau_host,
|
||||
port: self.millau_port,
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
impl crate::cli::RialtoConnectionParams {
|
||||
/// Convert CLI connection parameters into Rialto RPC Client.
|
||||
pub async fn into_client(self) -> relay_substrate_client::Result<RialtoClient> {
|
||||
RialtoClient::new(ConnectionParams {
|
||||
host: self.rialto_host,
|
||||
port: self.rialto_port,
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::cli::ToRialtoMessage {
|
||||
/// Convert CLI call request into runtime `Call` instance.
|
||||
pub fn into_call(self) -> rialto_runtime::Call {
|
||||
match self {
|
||||
cli::ToRialtoMessage::Remark { remark_size } => {
|
||||
rialto_runtime::Call::System(rialto_runtime::SystemCall::remark(remark_payload(
|
||||
remark_size,
|
||||
compute_maximal_message_arguments_size(
|
||||
bp_millau::max_extrinsic_size(),
|
||||
bp_rialto::max_extrinsic_size(),
|
||||
),
|
||||
)))
|
||||
}
|
||||
cli::ToRialtoMessage::Transfer { recipient, amount } => {
|
||||
rialto_runtime::Call::Balances(rialto_runtime::BalancesCall::transfer(recipient, amount))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::cli::ToMillauMessage {
|
||||
/// Convert CLI call request into runtime `Call` instance.
|
||||
pub fn into_call(self) -> millau_runtime::Call {
|
||||
match self {
|
||||
cli::ToMillauMessage::Remark { remark_size } => {
|
||||
millau_runtime::Call::System(millau_runtime::SystemCall::remark(remark_payload(
|
||||
remark_size,
|
||||
compute_maximal_message_arguments_size(
|
||||
bp_rialto::max_extrinsic_size(),
|
||||
bp_millau::max_extrinsic_size(),
|
||||
),
|
||||
)))
|
||||
}
|
||||
cli::ToMillauMessage::Transfer { recipient, amount } => {
|
||||
millau_runtime::Call::Balances(millau_runtime::BalancesCall::transfer(recipient, amount))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use bp_message_lane::source_chain::TargetHeaderChain;
|
||||
use sp_core::Pair;
|
||||
use sp_runtime::traits::{IdentifyAccount, Verify};
|
||||
|
||||
#[test]
|
||||
fn millau_signature_is_valid_on_rialto() {
|
||||
let millau_sign = relay_millau_client::SigningParams::from_suri("//Dave", None).unwrap();
|
||||
|
||||
let call = rialto_runtime::Call::System(rialto_runtime::SystemCall::remark(vec![]));
|
||||
|
||||
let millau_public: bp_millau::AccountSigner = millau_sign.signer.public().clone().into();
|
||||
let millau_account_id: bp_millau::AccountId = millau_public.into_account();
|
||||
|
||||
let digest = millau_runtime::rialto_account_ownership_digest(
|
||||
&call,
|
||||
millau_account_id,
|
||||
rialto_runtime::VERSION.spec_version,
|
||||
);
|
||||
|
||||
let rialto_signer = relay_rialto_client::SigningParams::from_suri("//Dave", None).unwrap();
|
||||
let signature = rialto_signer.signer.sign(&digest);
|
||||
|
||||
assert!(signature.verify(&digest[..], &rialto_signer.signer.public()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rialto_signature_is_valid_on_millau() {
|
||||
let rialto_sign = relay_rialto_client::SigningParams::from_suri("//Dave", None).unwrap();
|
||||
|
||||
let call = millau_runtime::Call::System(millau_runtime::SystemCall::remark(vec![]));
|
||||
|
||||
let rialto_public: bp_rialto::AccountSigner = rialto_sign.signer.public().clone().into();
|
||||
let rialto_account_id: bp_rialto::AccountId = rialto_public.into_account();
|
||||
|
||||
let digest = rialto_runtime::millau_account_ownership_digest(
|
||||
&call,
|
||||
rialto_account_id,
|
||||
millau_runtime::VERSION.spec_version,
|
||||
);
|
||||
|
||||
let millau_signer = relay_millau_client::SigningParams::from_suri("//Dave", None).unwrap();
|
||||
let signature = millau_signer.signer.sign(&digest);
|
||||
|
||||
assert!(signature.verify(&digest[..], &millau_signer.signer.public()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maximal_rialto_to_millau_message_arguments_size_is_computed_correctly() {
|
||||
use rialto_runtime::millau_messages::Millau;
|
||||
|
||||
let maximal_remark_size =
|
||||
compute_maximal_message_arguments_size(bp_rialto::max_extrinsic_size(), bp_millau::max_extrinsic_size());
|
||||
|
||||
let call: millau_runtime::Call = millau_runtime::SystemCall::remark(vec![42; maximal_remark_size as _]).into();
|
||||
let payload = pallet_bridge_call_dispatch::MessagePayload {
|
||||
spec_version: Default::default(),
|
||||
weight: call.get_dispatch_info().weight,
|
||||
origin: pallet_bridge_call_dispatch::CallOrigin::SourceRoot,
|
||||
call: call.encode(),
|
||||
};
|
||||
assert_eq!(Millau::verify_message(&payload), Ok(()));
|
||||
|
||||
let call: millau_runtime::Call =
|
||||
millau_runtime::SystemCall::remark(vec![42; (maximal_remark_size + 1) as _]).into();
|
||||
let payload = pallet_bridge_call_dispatch::MessagePayload {
|
||||
spec_version: Default::default(),
|
||||
weight: call.get_dispatch_info().weight,
|
||||
origin: pallet_bridge_call_dispatch::CallOrigin::SourceRoot,
|
||||
call: call.encode(),
|
||||
};
|
||||
assert!(Millau::verify_message(&payload).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maximal_size_remark_to_rialto_is_generated_correctly() {
|
||||
assert!(
|
||||
bridge_runtime_common::messages::target::maximal_incoming_message_size(
|
||||
bp_rialto::max_extrinsic_size()
|
||||
) > bp_millau::max_extrinsic_size(),
|
||||
"We can't actually send maximal messages to Rialto from Millau, because Millau extrinsics can't be that large",
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maximal_rialto_to_millau_message_dispatch_weight_is_computed_correctly() {
|
||||
use rialto_runtime::millau_messages::Millau;
|
||||
|
||||
let maximal_dispatch_weight = compute_maximal_message_dispatch_weight(bp_millau::max_extrinsic_weight());
|
||||
let call: millau_runtime::Call = rialto_runtime::SystemCall::remark(vec![]).into();
|
||||
|
||||
let payload = pallet_bridge_call_dispatch::MessagePayload {
|
||||
spec_version: Default::default(),
|
||||
weight: maximal_dispatch_weight,
|
||||
origin: pallet_bridge_call_dispatch::CallOrigin::SourceRoot,
|
||||
call: call.encode(),
|
||||
};
|
||||
assert_eq!(Millau::verify_message(&payload), Ok(()));
|
||||
|
||||
let payload = pallet_bridge_call_dispatch::MessagePayload {
|
||||
spec_version: Default::default(),
|
||||
weight: maximal_dispatch_weight + 1,
|
||||
origin: pallet_bridge_call_dispatch::CallOrigin::SourceRoot,
|
||||
call: call.encode(),
|
||||
};
|
||||
assert!(Millau::verify_message(&payload).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maximal_weight_fill_block_to_rialto_is_generated_correctly() {
|
||||
use millau_runtime::rialto_messages::Rialto;
|
||||
|
||||
let maximal_dispatch_weight = compute_maximal_message_dispatch_weight(bp_rialto::max_extrinsic_weight());
|
||||
let call: rialto_runtime::Call = millau_runtime::SystemCall::remark(vec![]).into();
|
||||
|
||||
let payload = pallet_bridge_call_dispatch::MessagePayload {
|
||||
spec_version: Default::default(),
|
||||
weight: maximal_dispatch_weight,
|
||||
origin: pallet_bridge_call_dispatch::CallOrigin::SourceRoot,
|
||||
call: call.encode(),
|
||||
};
|
||||
assert_eq!(Rialto::verify_message(&payload), Ok(()));
|
||||
|
||||
let payload = pallet_bridge_call_dispatch::MessagePayload {
|
||||
spec_version: Default::default(),
|
||||
weight: maximal_dispatch_weight + 1,
|
||||
origin: pallet_bridge_call_dispatch::CallOrigin::SourceRoot,
|
||||
call: call.encode(),
|
||||
};
|
||||
assert!(Rialto::verify_message(&payload).is_err());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
// Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::messages_source::SubstrateMessagesProof;
|
||||
use crate::messages_target::SubstrateMessagesReceivingProof;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use bp_message_lane::MessageNonce;
|
||||
use codec::Encode;
|
||||
use frame_support::weights::Weight;
|
||||
use messages_relay::message_lane::{MessageLane, SourceHeaderIdOf, TargetHeaderIdOf};
|
||||
use relay_substrate_client::{BlockNumberOf, Chain, Client, Error as SubstrateError, HashOf};
|
||||
use relay_utils::BlockNumberBase;
|
||||
use std::ops::RangeInclusive;
|
||||
|
||||
/// Message sync pipeline for Substrate <-> Substrate relays.
|
||||
#[async_trait]
|
||||
pub trait SubstrateMessageLane: MessageLane {
|
||||
/// Name of the runtime method that returns dispatch weight of outbound messages at the source chain.
|
||||
const OUTBOUND_LANE_MESSAGES_DISPATCH_WEIGHT_METHOD: &'static str;
|
||||
/// Name of the runtime method that returns latest generated nonce at the source chain.
|
||||
const OUTBOUND_LANE_LATEST_GENERATED_NONCE_METHOD: &'static str;
|
||||
/// Name of the runtime method that returns latest received (confirmed) nonce at the the source chain.
|
||||
const OUTBOUND_LANE_LATEST_RECEIVED_NONCE_METHOD: &'static str;
|
||||
|
||||
/// Name of the runtime method that returns latest received nonce at the target chain.
|
||||
const INBOUND_LANE_LATEST_RECEIVED_NONCE_METHOD: &'static str;
|
||||
/// Name of the runtime method that returns latest confirmed (reward-paid) nonce at the target chain.
|
||||
const INBOUND_LANE_LATEST_CONFIRMED_NONCE_METHOD: &'static str;
|
||||
/// Numebr of the runtime method that returns state of "unrewarded relayers" set at the target chain.
|
||||
const INBOUND_LANE_UNREWARDED_RELAYERS_STATE: &'static str;
|
||||
|
||||
/// Name of the runtime method that returns id of best finalized source header at target chain.
|
||||
const BEST_FINALIZED_SOURCE_HEADER_ID_AT_TARGET: &'static str;
|
||||
/// Name of the runtime method that returns id of best finalized target header at source chain.
|
||||
const BEST_FINALIZED_TARGET_HEADER_ID_AT_SOURCE: &'static str;
|
||||
|
||||
/// Signed transaction type of the source chain.
|
||||
type SourceSignedTransaction: Send + Sync + Encode;
|
||||
/// Signed transaction type of the target chain.
|
||||
type TargetSignedTransaction: Send + Sync + Encode;
|
||||
|
||||
/// Make messages delivery transaction.
|
||||
async fn make_messages_delivery_transaction(
|
||||
&self,
|
||||
generated_at_header: SourceHeaderIdOf<Self>,
|
||||
nonces: RangeInclusive<MessageNonce>,
|
||||
proof: Self::MessagesProof,
|
||||
) -> Result<Self::TargetSignedTransaction, SubstrateError>;
|
||||
|
||||
/// Make messages receiving proof transaction.
|
||||
async fn make_messages_receiving_proof_transaction(
|
||||
&self,
|
||||
generated_at_header: TargetHeaderIdOf<Self>,
|
||||
proof: Self::MessagesReceivingProof,
|
||||
) -> Result<Self::SourceSignedTransaction, SubstrateError>;
|
||||
}
|
||||
|
||||
/// Substrate-to-Substrate message lane.
|
||||
#[derive(Debug)]
|
||||
pub struct SubstrateMessageLaneToSubstrate<Source: Chain, SourceSignParams, Target: Chain, TargetSignParams> {
|
||||
/// Client for the source Substrate chain.
|
||||
pub(crate) source_client: Client<Source>,
|
||||
/// Parameters required to sign transactions for source chain.
|
||||
pub(crate) source_sign: SourceSignParams,
|
||||
/// Client for the target Substrate chain.
|
||||
pub(crate) target_client: Client<Target>,
|
||||
/// Parameters required to sign transactions for target chain.
|
||||
pub(crate) target_sign: TargetSignParams,
|
||||
/// Account id of relayer at the source chain.
|
||||
pub(crate) relayer_id_at_source: Source::AccountId,
|
||||
}
|
||||
|
||||
impl<Source: Chain, SourceSignParams: Clone, Target: Chain, TargetSignParams: Clone> Clone
|
||||
for SubstrateMessageLaneToSubstrate<Source, SourceSignParams, Target, TargetSignParams>
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
source_client: self.source_client.clone(),
|
||||
source_sign: self.source_sign.clone(),
|
||||
target_client: self.target_client.clone(),
|
||||
target_sign: self.target_sign.clone(),
|
||||
relayer_id_at_source: self.relayer_id_at_source.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Source: Chain, SourceSignParams, Target: Chain, TargetSignParams> MessageLane
|
||||
for SubstrateMessageLaneToSubstrate<Source, SourceSignParams, Target, TargetSignParams>
|
||||
where
|
||||
SourceSignParams: Clone + Send + Sync + 'static,
|
||||
TargetSignParams: Clone + Send + Sync + 'static,
|
||||
BlockNumberOf<Source>: BlockNumberBase,
|
||||
BlockNumberOf<Target>: BlockNumberBase,
|
||||
{
|
||||
const SOURCE_NAME: &'static str = Source::NAME;
|
||||
const TARGET_NAME: &'static str = Target::NAME;
|
||||
|
||||
type MessagesProof = SubstrateMessagesProof<Source>;
|
||||
type MessagesReceivingProof = SubstrateMessagesReceivingProof<Target>;
|
||||
|
||||
type SourceHeaderNumber = BlockNumberOf<Source>;
|
||||
type SourceHeaderHash = HashOf<Source>;
|
||||
|
||||
type TargetHeaderNumber = BlockNumberOf<Target>;
|
||||
type TargetHeaderHash = HashOf<Target>;
|
||||
}
|
||||
|
||||
/// Returns maximal number of messages and their maximal cumulative dispatch weight, based
|
||||
/// on given chain parameters.
|
||||
pub fn select_delivery_transaction_limits<W: pallet_message_lane::WeightInfoExt>(
|
||||
max_extrinsic_weight: Weight,
|
||||
max_unconfirmed_messages_at_inbound_lane: MessageNonce,
|
||||
) -> (MessageNonce, Weight) {
|
||||
// We may try to guess accurate value, based on maximal number of messages and per-message
|
||||
// weight overhead, but the relay loop isn't using this info in a super-accurate way anyway.
|
||||
// So just a rough guess: let's say 1/3 of max tx weight is for tx itself and the rest is
|
||||
// for messages dispatch.
|
||||
|
||||
// Another thing to keep in mind is that our runtimes (when this code was written) accept
|
||||
// messages with dispatch weight <= max_extrinsic_weight/2. So we can't reserve less than
|
||||
// that for dispatch.
|
||||
|
||||
let weight_for_delivery_tx = max_extrinsic_weight / 3;
|
||||
let weight_for_messages_dispatch = max_extrinsic_weight - weight_for_delivery_tx;
|
||||
|
||||
let delivery_tx_base_weight =
|
||||
W::receive_messages_proof_overhead() + W::receive_messages_proof_outbound_lane_state_overhead();
|
||||
let delivery_tx_weight_rest = weight_for_delivery_tx - delivery_tx_base_weight;
|
||||
let max_number_of_messages = std::cmp::min(
|
||||
delivery_tx_weight_rest / W::receive_messages_proof_messages_overhead(1),
|
||||
max_unconfirmed_messages_at_inbound_lane,
|
||||
);
|
||||
|
||||
assert!(
|
||||
max_number_of_messages > 0,
|
||||
"Relay should fit at least one message in every delivery transaction",
|
||||
);
|
||||
assert!(
|
||||
weight_for_messages_dispatch >= max_extrinsic_weight / 2,
|
||||
"Relay shall be able to deliver messages with dispatch weight = max_extrinsic_weight / 2",
|
||||
);
|
||||
|
||||
(max_number_of_messages, weight_for_messages_dispatch)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
type RialtoToMillauMessageLaneWeights = pallet_message_lane::weights::RialtoWeight<rialto_runtime::Runtime>;
|
||||
|
||||
#[test]
|
||||
fn select_delivery_transaction_limits_works() {
|
||||
let (max_count, max_weight) = select_delivery_transaction_limits::<RialtoToMillauMessageLaneWeights>(
|
||||
bp_millau::max_extrinsic_weight(),
|
||||
bp_millau::MAX_UNREWARDED_RELAYER_ENTRIES_AT_INBOUND_LANE,
|
||||
);
|
||||
assert_eq!(
|
||||
(max_count, max_weight),
|
||||
// We don't actually care about these values, so feel free to update them whenever test
|
||||
// fails. The only thing to do before that is to ensure that new values looks sane: i.e. weight
|
||||
// reserved for messages dispatch allows dispatch of non-trivial messages.
|
||||
//
|
||||
// Any significant change in this values should attract additional attention.
|
||||
(955, 216_583_333_334),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,373 @@
|
||||
// Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Substrate client as Substrate messages source. The chain we connect to should have
|
||||
//! runtime that implements `<BridgedChainName>HeaderApi` to allow bridging with
|
||||
//! <BridgedName> chain.
|
||||
|
||||
use crate::messages_lane::SubstrateMessageLane;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use bp_message_lane::{LaneId, MessageNonce};
|
||||
use bp_runtime::InstanceId;
|
||||
use bridge_runtime_common::messages::target::FromBridgedChainMessagesProof;
|
||||
use codec::{Decode, Encode};
|
||||
use frame_support::weights::Weight;
|
||||
use messages_relay::{
|
||||
message_lane::{SourceHeaderIdOf, TargetHeaderIdOf},
|
||||
message_lane_loop::{
|
||||
ClientState, MessageProofParameters, MessageWeights, MessageWeightsMap, SourceClient, SourceClientState,
|
||||
},
|
||||
};
|
||||
use relay_substrate_client::{Chain, Client, Error as SubstrateError, HashOf, HeaderIdOf};
|
||||
use relay_utils::{relay_loop::Client as RelayClient, BlockNumberBase, HeaderId};
|
||||
use sp_core::Bytes;
|
||||
use sp_runtime::{traits::Header as HeaderT, DeserializeOwned};
|
||||
use std::ops::RangeInclusive;
|
||||
|
||||
/// Intermediate message proof returned by the source Substrate node. Includes everything
|
||||
/// required to submit to the target node: cumulative dispatch weight of bundled messages and
|
||||
/// the proof itself.
|
||||
pub type SubstrateMessagesProof<C> = (Weight, FromBridgedChainMessagesProof<HashOf<C>>);
|
||||
|
||||
/// Substrate client as Substrate messages source.
|
||||
pub struct SubstrateMessagesSource<C: Chain, P> {
|
||||
client: Client<C>,
|
||||
lane: P,
|
||||
lane_id: LaneId,
|
||||
instance: InstanceId,
|
||||
}
|
||||
|
||||
impl<C: Chain, P> SubstrateMessagesSource<C, P> {
|
||||
/// Create new Substrate headers source.
|
||||
pub fn new(client: Client<C>, lane: P, lane_id: LaneId, instance: InstanceId) -> Self {
|
||||
SubstrateMessagesSource {
|
||||
client,
|
||||
lane,
|
||||
lane_id,
|
||||
instance,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Chain, P: SubstrateMessageLane> Clone for SubstrateMessagesSource<C, P> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
client: self.client.clone(),
|
||||
lane: self.lane.clone(),
|
||||
lane_id: self.lane_id,
|
||||
instance: self.instance,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<C: Chain, P: SubstrateMessageLane> RelayClient for SubstrateMessagesSource<C, P> {
|
||||
type Error = SubstrateError;
|
||||
|
||||
async fn reconnect(&mut self) -> Result<(), SubstrateError> {
|
||||
self.client.reconnect().await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<C, P> SourceClient<P> for SubstrateMessagesSource<C, P>
|
||||
where
|
||||
C: Chain,
|
||||
C::Header: DeserializeOwned,
|
||||
C::Index: DeserializeOwned,
|
||||
C::BlockNumber: BlockNumberBase,
|
||||
P: SubstrateMessageLane<
|
||||
MessagesProof = SubstrateMessagesProof<C>,
|
||||
SourceHeaderNumber = <C::Header as HeaderT>::Number,
|
||||
SourceHeaderHash = <C::Header as HeaderT>::Hash,
|
||||
>,
|
||||
P::TargetHeaderNumber: Decode,
|
||||
P::TargetHeaderHash: Decode,
|
||||
{
|
||||
async fn state(&self) -> Result<SourceClientState<P>, 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,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn latest_generated_nonce(
|
||||
&self,
|
||||
id: SourceHeaderIdOf<P>,
|
||||
) -> Result<(SourceHeaderIdOf<P>, MessageNonce), SubstrateError> {
|
||||
let encoded_response = self
|
||||
.client
|
||||
.state_call(
|
||||
P::OUTBOUND_LANE_LATEST_GENERATED_NONCE_METHOD.into(),
|
||||
Bytes(self.lane_id.encode()),
|
||||
Some(id.1),
|
||||
)
|
||||
.await?;
|
||||
let latest_generated_nonce: MessageNonce =
|
||||
Decode::decode(&mut &encoded_response.0[..]).map_err(SubstrateError::ResponseParseFailed)?;
|
||||
Ok((id, latest_generated_nonce))
|
||||
}
|
||||
|
||||
async fn latest_confirmed_received_nonce(
|
||||
&self,
|
||||
id: SourceHeaderIdOf<P>,
|
||||
) -> Result<(SourceHeaderIdOf<P>, MessageNonce), SubstrateError> {
|
||||
let encoded_response = self
|
||||
.client
|
||||
.state_call(
|
||||
P::OUTBOUND_LANE_LATEST_RECEIVED_NONCE_METHOD.into(),
|
||||
Bytes(self.lane_id.encode()),
|
||||
Some(id.1),
|
||||
)
|
||||
.await?;
|
||||
let latest_received_nonce: MessageNonce =
|
||||
Decode::decode(&mut &encoded_response.0[..]).map_err(SubstrateError::ResponseParseFailed)?;
|
||||
Ok((id, latest_received_nonce))
|
||||
}
|
||||
|
||||
async fn generated_messages_weights(
|
||||
&self,
|
||||
id: SourceHeaderIdOf<P>,
|
||||
nonces: RangeInclusive<MessageNonce>,
|
||||
) -> Result<MessageWeightsMap, SubstrateError> {
|
||||
let encoded_response = self
|
||||
.client
|
||||
.state_call(
|
||||
P::OUTBOUND_LANE_MESSAGES_DISPATCH_WEIGHT_METHOD.into(),
|
||||
Bytes((self.lane_id, nonces.start(), nonces.end()).encode()),
|
||||
Some(id.1),
|
||||
)
|
||||
.await?;
|
||||
|
||||
make_message_weights_map::<C>(
|
||||
Decode::decode(&mut &encoded_response.0[..]).map_err(SubstrateError::ResponseParseFailed)?,
|
||||
nonces,
|
||||
)
|
||||
}
|
||||
|
||||
async fn prove_messages(
|
||||
&self,
|
||||
id: SourceHeaderIdOf<P>,
|
||||
nonces: RangeInclusive<MessageNonce>,
|
||||
proof_parameters: MessageProofParameters,
|
||||
) -> Result<(SourceHeaderIdOf<P>, RangeInclusive<MessageNonce>, P::MessagesProof), SubstrateError> {
|
||||
let proof = self
|
||||
.client
|
||||
.prove_messages(
|
||||
self.instance,
|
||||
self.lane_id,
|
||||
nonces.clone(),
|
||||
proof_parameters.outbound_state_proof_required,
|
||||
id.1,
|
||||
)
|
||||
.await?
|
||||
.iter_nodes()
|
||||
.collect();
|
||||
let proof = FromBridgedChainMessagesProof {
|
||||
bridged_header_hash: id.1,
|
||||
storage_proof: proof,
|
||||
lane: self.lane_id,
|
||||
nonces_start: *nonces.start(),
|
||||
nonces_end: *nonces.end(),
|
||||
};
|
||||
Ok((id, nonces, (proof_parameters.dispatch_weight, proof)))
|
||||
}
|
||||
|
||||
async fn submit_messages_receiving_proof(
|
||||
&self,
|
||||
generated_at_block: TargetHeaderIdOf<P>,
|
||||
proof: P::MessagesReceivingProof,
|
||||
) -> Result<(), SubstrateError> {
|
||||
let tx = self
|
||||
.lane
|
||||
.make_messages_receiving_proof_transaction(generated_at_block, proof)
|
||||
.await?;
|
||||
self.client.submit_extrinsic(Bytes(tx.encode())).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn read_client_state<SelfChain, BridgedHeaderHash, BridgedHeaderNumber>(
|
||||
self_client: &Client<SelfChain>,
|
||||
best_finalized_header_id_method_name: &str,
|
||||
) -> Result<ClientState<HeaderIdOf<SelfChain>, HeaderId<BridgedHeaderHash, BridgedHeaderNumber>>, SubstrateError>
|
||||
where
|
||||
SelfChain: Chain,
|
||||
SelfChain::Header: DeserializeOwned,
|
||||
SelfChain::Index: DeserializeOwned,
|
||||
BridgedHeaderHash: Decode,
|
||||
BridgedHeaderNumber: Decode,
|
||||
{
|
||||
// let's read our state first: we need best finalized header hash on **this** chain
|
||||
let self_best_finalized_header_hash = self_client.best_finalized_header_hash().await?;
|
||||
let self_best_finalized_header = self_client.header_by_hash(self_best_finalized_header_hash).await?;
|
||||
let self_best_finalized_id = HeaderId(*self_best_finalized_header.number(), self_best_finalized_header_hash);
|
||||
|
||||
// now let's read our best header on **this** chain
|
||||
let self_best_header = self_client.best_header().await?;
|
||||
let self_best_hash = self_best_header.hash();
|
||||
let self_best_id = HeaderId(*self_best_header.number(), self_best_hash);
|
||||
|
||||
// now let's read id of best finalized peer header at our best finalized block
|
||||
let encoded_best_finalized_peer_on_self = self_client
|
||||
.state_call(
|
||||
best_finalized_header_id_method_name.into(),
|
||||
Bytes(Vec::new()),
|
||||
Some(self_best_hash),
|
||||
)
|
||||
.await?;
|
||||
let decoded_best_finalized_peer_on_self: (BridgedHeaderNumber, BridgedHeaderHash) =
|
||||
Decode::decode(&mut &encoded_best_finalized_peer_on_self.0[..]).map_err(SubstrateError::ResponseParseFailed)?;
|
||||
let peer_on_self_best_finalized_id = HeaderId(
|
||||
decoded_best_finalized_peer_on_self.0,
|
||||
decoded_best_finalized_peer_on_self.1,
|
||||
);
|
||||
|
||||
Ok(ClientState {
|
||||
best_self: self_best_id,
|
||||
best_finalized_self: self_best_finalized_id,
|
||||
best_finalized_peer_at_best_self: peer_on_self_best_finalized_id,
|
||||
})
|
||||
}
|
||||
|
||||
fn make_message_weights_map<C: Chain>(
|
||||
weights: Vec<(MessageNonce, Weight, u32)>,
|
||||
nonces: RangeInclusive<MessageNonce>,
|
||||
) -> Result<MessageWeightsMap, SubstrateError> {
|
||||
let make_missing_nonce_error = |expected_nonce| {
|
||||
Err(SubstrateError::Custom(format!(
|
||||
"Missing nonce {} in messages_dispatch_weight call result. Expected all nonces from {:?}",
|
||||
expected_nonce, nonces,
|
||||
)))
|
||||
};
|
||||
|
||||
let mut weights_map = MessageWeightsMap::new();
|
||||
|
||||
// this is actually prevented by external logic
|
||||
if nonces.is_empty() {
|
||||
return Ok(weights_map);
|
||||
}
|
||||
|
||||
// check if last nonce is missing - loop below is not checking this
|
||||
let last_nonce_is_missing = weights
|
||||
.last()
|
||||
.map(|(last_nonce, _, _)| last_nonce != nonces.end())
|
||||
.unwrap_or(true);
|
||||
if last_nonce_is_missing {
|
||||
return make_missing_nonce_error(*nonces.end());
|
||||
}
|
||||
|
||||
let mut expected_nonce = *nonces.start();
|
||||
let mut is_at_head = true;
|
||||
|
||||
for (nonce, weight, size) in weights {
|
||||
match (nonce == expected_nonce, is_at_head) {
|
||||
(true, _) => (),
|
||||
(false, true) => {
|
||||
// this may happen if some messages were already pruned from the source node
|
||||
//
|
||||
// this is not critical error and will be auto-resolved by messages lane (and target node)
|
||||
log::info!(
|
||||
target: "bridge",
|
||||
"Some messages are missing from the {} node: {:?}. Target node may be out of sync?",
|
||||
C::NAME,
|
||||
expected_nonce..nonce,
|
||||
);
|
||||
}
|
||||
(false, false) => {
|
||||
// some nonces are missing from the middle/tail of the range
|
||||
//
|
||||
// this is critical error, because we can't miss any nonces
|
||||
return make_missing_nonce_error(expected_nonce);
|
||||
}
|
||||
}
|
||||
|
||||
weights_map.insert(
|
||||
nonce,
|
||||
MessageWeights {
|
||||
weight,
|
||||
size: size as _,
|
||||
},
|
||||
);
|
||||
expected_nonce = nonce + 1;
|
||||
is_at_head = false;
|
||||
}
|
||||
|
||||
Ok(weights_map)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn make_message_weights_map_succeeds_if_no_messages_are_missing() {
|
||||
assert_eq!(
|
||||
make_message_weights_map::<relay_rialto_client::Rialto>(vec![(1, 0, 0), (2, 0, 0), (3, 0, 0)], 1..=3,)
|
||||
.unwrap(),
|
||||
vec![
|
||||
(1, MessageWeights { weight: 0, size: 0 }),
|
||||
(2, MessageWeights { weight: 0, size: 0 }),
|
||||
(3, MessageWeights { weight: 0, size: 0 }),
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn make_message_weights_map_succeeds_if_head_messages_are_missing() {
|
||||
assert_eq!(
|
||||
make_message_weights_map::<relay_rialto_client::Rialto>(vec![(2, 0, 0), (3, 0, 0)], 1..=3,).unwrap(),
|
||||
vec![
|
||||
(2, MessageWeights { weight: 0, size: 0 }),
|
||||
(3, MessageWeights { weight: 0, size: 0 }),
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn make_message_weights_map_fails_if_mid_messages_are_missing() {
|
||||
assert!(matches!(
|
||||
make_message_weights_map::<relay_rialto_client::Rialto>(vec![(1, 0, 0), (3, 0, 0)], 1..=3,),
|
||||
Err(SubstrateError::Custom(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn make_message_weights_map_fails_if_tail_messages_are_missing() {
|
||||
assert!(matches!(
|
||||
make_message_weights_map::<relay_rialto_client::Rialto>(vec![(1, 0, 0), (2, 0, 0)], 1..=3,),
|
||||
Err(SubstrateError::Custom(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn make_message_weights_map_fails_if_all_messages_are_missing() {
|
||||
assert!(matches!(
|
||||
make_message_weights_map::<relay_rialto_client::Rialto>(vec![], 1..=3),
|
||||
Err(SubstrateError::Custom(_))
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
// Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Substrate client as Substrate messages target. The chain we connect to should have
|
||||
//! runtime that implements `<BridgedChainName>HeaderApi` to allow bridging with
|
||||
//! <BridgedName> chain.
|
||||
|
||||
use crate::messages_lane::SubstrateMessageLane;
|
||||
use crate::messages_source::read_client_state;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use bp_message_lane::{LaneId, MessageNonce, UnrewardedRelayersState};
|
||||
use bp_runtime::InstanceId;
|
||||
use bridge_runtime_common::messages::source::FromBridgedChainMessagesDeliveryProof;
|
||||
use codec::{Decode, Encode};
|
||||
use messages_relay::{
|
||||
message_lane::{SourceHeaderIdOf, TargetHeaderIdOf},
|
||||
message_lane_loop::{TargetClient, TargetClientState},
|
||||
};
|
||||
use relay_substrate_client::{Chain, Client, Error as SubstrateError, HashOf};
|
||||
use relay_utils::{relay_loop::Client as RelayClient, BlockNumberBase};
|
||||
use sp_core::Bytes;
|
||||
use sp_runtime::{traits::Header as HeaderT, DeserializeOwned};
|
||||
use std::ops::RangeInclusive;
|
||||
|
||||
/// Message receiving proof returned by the target Substrate node.
|
||||
pub type SubstrateMessagesReceivingProof<C> = (
|
||||
UnrewardedRelayersState,
|
||||
FromBridgedChainMessagesDeliveryProof<HashOf<C>>,
|
||||
);
|
||||
|
||||
/// Substrate client as Substrate messages target.
|
||||
pub struct SubstrateMessagesTarget<C: Chain, P> {
|
||||
client: Client<C>,
|
||||
lane: P,
|
||||
lane_id: LaneId,
|
||||
instance: InstanceId,
|
||||
}
|
||||
|
||||
impl<C: Chain, P> SubstrateMessagesTarget<C, P> {
|
||||
/// Create new Substrate headers target.
|
||||
pub fn new(client: Client<C>, lane: P, lane_id: LaneId, instance: InstanceId) -> Self {
|
||||
SubstrateMessagesTarget {
|
||||
client,
|
||||
lane,
|
||||
lane_id,
|
||||
instance,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Chain, P: SubstrateMessageLane> Clone for SubstrateMessagesTarget<C, P> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
client: self.client.clone(),
|
||||
lane: self.lane.clone(),
|
||||
lane_id: self.lane_id,
|
||||
instance: self.instance,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<C: Chain, P: SubstrateMessageLane> RelayClient for SubstrateMessagesTarget<C, P> {
|
||||
type Error = SubstrateError;
|
||||
|
||||
async fn reconnect(&mut self) -> Result<(), SubstrateError> {
|
||||
self.client.reconnect().await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<C, P> TargetClient<P> for SubstrateMessagesTarget<C, P>
|
||||
where
|
||||
C: Chain,
|
||||
C::Header: DeserializeOwned,
|
||||
C::Index: DeserializeOwned,
|
||||
<C::Header as HeaderT>::Number: BlockNumberBase,
|
||||
P: SubstrateMessageLane<
|
||||
MessagesReceivingProof = SubstrateMessagesReceivingProof<C>,
|
||||
TargetHeaderNumber = <C::Header as HeaderT>::Number,
|
||||
TargetHeaderHash = <C::Header as HeaderT>::Hash,
|
||||
>,
|
||||
P::SourceHeaderNumber: Decode,
|
||||
P::SourceHeaderHash: Decode,
|
||||
{
|
||||
async fn state(&self) -> Result<TargetClientState<P>, 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,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn latest_received_nonce(
|
||||
&self,
|
||||
id: TargetHeaderIdOf<P>,
|
||||
) -> Result<(TargetHeaderIdOf<P>, MessageNonce), SubstrateError> {
|
||||
let encoded_response = self
|
||||
.client
|
||||
.state_call(
|
||||
P::INBOUND_LANE_LATEST_RECEIVED_NONCE_METHOD.into(),
|
||||
Bytes(self.lane_id.encode()),
|
||||
Some(id.1),
|
||||
)
|
||||
.await?;
|
||||
let latest_received_nonce: MessageNonce =
|
||||
Decode::decode(&mut &encoded_response.0[..]).map_err(SubstrateError::ResponseParseFailed)?;
|
||||
Ok((id, latest_received_nonce))
|
||||
}
|
||||
|
||||
async fn latest_confirmed_received_nonce(
|
||||
&self,
|
||||
id: TargetHeaderIdOf<P>,
|
||||
) -> Result<(TargetHeaderIdOf<P>, MessageNonce), SubstrateError> {
|
||||
let encoded_response = self
|
||||
.client
|
||||
.state_call(
|
||||
P::INBOUND_LANE_LATEST_CONFIRMED_NONCE_METHOD.into(),
|
||||
Bytes(self.lane_id.encode()),
|
||||
Some(id.1),
|
||||
)
|
||||
.await?;
|
||||
let latest_received_nonce: MessageNonce =
|
||||
Decode::decode(&mut &encoded_response.0[..]).map_err(SubstrateError::ResponseParseFailed)?;
|
||||
Ok((id, latest_received_nonce))
|
||||
}
|
||||
|
||||
async fn unrewarded_relayers_state(
|
||||
&self,
|
||||
id: TargetHeaderIdOf<P>,
|
||||
) -> Result<(TargetHeaderIdOf<P>, UnrewardedRelayersState), SubstrateError> {
|
||||
let encoded_response = self
|
||||
.client
|
||||
.state_call(
|
||||
P::INBOUND_LANE_UNREWARDED_RELAYERS_STATE.into(),
|
||||
Bytes(self.lane_id.encode()),
|
||||
Some(id.1),
|
||||
)
|
||||
.await?;
|
||||
let unrewarded_relayers_state: UnrewardedRelayersState =
|
||||
Decode::decode(&mut &encoded_response.0[..]).map_err(SubstrateError::ResponseParseFailed)?;
|
||||
Ok((id, unrewarded_relayers_state))
|
||||
}
|
||||
|
||||
async fn prove_messages_receiving(
|
||||
&self,
|
||||
id: TargetHeaderIdOf<P>,
|
||||
) -> Result<(TargetHeaderIdOf<P>, P::MessagesReceivingProof), SubstrateError> {
|
||||
let (id, relayers_state) = self.unrewarded_relayers_state(id).await?;
|
||||
let proof = self
|
||||
.client
|
||||
.prove_messages_delivery(self.instance, self.lane_id, id.1)
|
||||
.await?;
|
||||
let proof = FromBridgedChainMessagesDeliveryProof {
|
||||
bridged_header_hash: id.1,
|
||||
storage_proof: proof,
|
||||
lane: self.lane_id,
|
||||
};
|
||||
Ok((id, (relayers_state, proof)))
|
||||
}
|
||||
|
||||
async fn submit_messages_proof(
|
||||
&self,
|
||||
generated_at_header: SourceHeaderIdOf<P>,
|
||||
nonces: RangeInclusive<MessageNonce>,
|
||||
proof: P::MessagesProof,
|
||||
) -> Result<RangeInclusive<MessageNonce>, SubstrateError> {
|
||||
let tx = self
|
||||
.lane
|
||||
.make_messages_delivery_transaction(generated_at_header, nonces.clone(), proof)
|
||||
.await?;
|
||||
self.client.submit_extrinsic(Bytes(tx.encode())).await?;
|
||||
Ok(nonces)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
// Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Millau-to-Rialto headers sync entrypoint.
|
||||
|
||||
use crate::{
|
||||
headers_pipeline::{SubstrateHeadersSyncPipeline, SubstrateHeadersToSubstrate},
|
||||
MillauClient, RialtoClient,
|
||||
};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use bp_millau::{
|
||||
BEST_MILLAU_BLOCKS_METHOD, FINALIZED_MILLAU_BLOCK_METHOD, INCOMPLETE_MILLAU_HEADERS_METHOD,
|
||||
IS_KNOWN_MILLAU_BLOCK_METHOD,
|
||||
};
|
||||
use headers_relay::sync_types::QueuedHeader;
|
||||
use relay_millau_client::{HeaderId as MillauHeaderId, Millau, SyncHeader as MillauSyncHeader};
|
||||
use relay_rialto_client::{BridgeMillauCall, Rialto, SigningParams as RialtoSigningParams};
|
||||
use relay_substrate_client::{Error as SubstrateError, TransactionSignScheme};
|
||||
use sp_core::Pair;
|
||||
use sp_runtime::Justification;
|
||||
|
||||
/// Millau-to-Rialto headers sync pipeline.
|
||||
pub(crate) type MillauHeadersToRialto =
|
||||
SubstrateHeadersToSubstrate<Millau, MillauSyncHeader, Rialto, RialtoSigningParams>;
|
||||
/// Millau header in-the-queue.
|
||||
type QueuedMillauHeader = QueuedHeader<MillauHeadersToRialto>;
|
||||
|
||||
#[async_trait]
|
||||
impl SubstrateHeadersSyncPipeline for MillauHeadersToRialto {
|
||||
const BEST_BLOCK_METHOD: &'static str = BEST_MILLAU_BLOCKS_METHOD;
|
||||
const FINALIZED_BLOCK_METHOD: &'static str = FINALIZED_MILLAU_BLOCK_METHOD;
|
||||
const IS_KNOWN_BLOCK_METHOD: &'static str = IS_KNOWN_MILLAU_BLOCK_METHOD;
|
||||
const INCOMPLETE_HEADERS_METHOD: &'static str = INCOMPLETE_MILLAU_HEADERS_METHOD;
|
||||
|
||||
type SignedTransaction = <Rialto as TransactionSignScheme>::SignedTransaction;
|
||||
|
||||
async fn make_submit_header_transaction(
|
||||
&self,
|
||||
header: QueuedMillauHeader,
|
||||
) -> Result<Self::SignedTransaction, SubstrateError> {
|
||||
let account_id = self.target_sign.signer.public().as_array_ref().clone().into();
|
||||
let nonce = self.target_client.next_account_index(account_id).await?;
|
||||
let call = BridgeMillauCall::import_signed_header(header.header().clone().into_inner()).into();
|
||||
let transaction = Rialto::sign_transaction(&self.target_client, &self.target_sign.signer, nonce, call);
|
||||
Ok(transaction)
|
||||
}
|
||||
|
||||
async fn make_complete_header_transaction(
|
||||
&self,
|
||||
id: MillauHeaderId,
|
||||
completion: Justification,
|
||||
) -> Result<Self::SignedTransaction, SubstrateError> {
|
||||
let account_id = self.target_sign.signer.public().as_array_ref().clone().into();
|
||||
let nonce = self.target_client.next_account_index(account_id).await?;
|
||||
let call = BridgeMillauCall::finalize_header(id.1, completion).into();
|
||||
let transaction = Rialto::sign_transaction(&self.target_client, &self.target_sign.signer, nonce, call);
|
||||
Ok(transaction)
|
||||
}
|
||||
}
|
||||
|
||||
/// Run Millau-to-Rialto headers sync.
|
||||
pub async fn run(
|
||||
millau_client: MillauClient,
|
||||
rialto_client: RialtoClient,
|
||||
rialto_sign: RialtoSigningParams,
|
||||
metrics_params: Option<relay_utils::metrics::MetricsParams>,
|
||||
) {
|
||||
crate::headers_pipeline::run(
|
||||
MillauHeadersToRialto::new(rialto_client.clone(), rialto_sign),
|
||||
millau_client,
|
||||
rialto_client,
|
||||
metrics_params,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
// Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Millau-to-Rialto messages sync entrypoint.
|
||||
|
||||
use crate::messages_lane::{select_delivery_transaction_limits, SubstrateMessageLane, SubstrateMessageLaneToSubstrate};
|
||||
use crate::messages_source::SubstrateMessagesSource;
|
||||
use crate::messages_target::SubstrateMessagesTarget;
|
||||
use crate::{MillauClient, RialtoClient};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use bp_message_lane::{LaneId, MessageNonce};
|
||||
use bp_runtime::{MILLAU_BRIDGE_INSTANCE, RIALTO_BRIDGE_INSTANCE};
|
||||
use bridge_runtime_common::messages::target::FromBridgedChainMessagesProof;
|
||||
use codec::Encode;
|
||||
use frame_support::dispatch::GetDispatchInfo;
|
||||
use messages_relay::message_lane::MessageLane;
|
||||
use relay_millau_client::{HeaderId as MillauHeaderId, Millau, SigningParams as MillauSigningParams};
|
||||
use relay_rialto_client::{HeaderId as RialtoHeaderId, Rialto, SigningParams as RialtoSigningParams};
|
||||
use relay_substrate_client::{Chain, Error as SubstrateError, TransactionSignScheme};
|
||||
use relay_utils::metrics::MetricsParams;
|
||||
use sp_core::Pair;
|
||||
use std::{ops::RangeInclusive, time::Duration};
|
||||
|
||||
/// Millau-to-Rialto message lane.
|
||||
type MillauMessagesToRialto = SubstrateMessageLaneToSubstrate<Millau, MillauSigningParams, Rialto, RialtoSigningParams>;
|
||||
|
||||
#[async_trait]
|
||||
impl SubstrateMessageLane for MillauMessagesToRialto {
|
||||
const OUTBOUND_LANE_MESSAGES_DISPATCH_WEIGHT_METHOD: &'static str =
|
||||
bp_rialto::TO_RIALTO_MESSAGES_DISPATCH_WEIGHT_METHOD;
|
||||
const OUTBOUND_LANE_LATEST_GENERATED_NONCE_METHOD: &'static str =
|
||||
bp_rialto::TO_RIALTO_LATEST_GENERATED_NONCE_METHOD;
|
||||
const OUTBOUND_LANE_LATEST_RECEIVED_NONCE_METHOD: &'static str = bp_rialto::TO_RIALTO_LATEST_RECEIVED_NONCE_METHOD;
|
||||
|
||||
const INBOUND_LANE_LATEST_RECEIVED_NONCE_METHOD: &'static str = bp_millau::FROM_MILLAU_LATEST_RECEIVED_NONCE_METHOD;
|
||||
const INBOUND_LANE_LATEST_CONFIRMED_NONCE_METHOD: &'static str =
|
||||
bp_millau::FROM_MILLAU_LATEST_CONFIRMED_NONCE_METHOD;
|
||||
const INBOUND_LANE_UNREWARDED_RELAYERS_STATE: &'static str = bp_millau::FROM_MILLAU_UNREWARDED_RELAYERS_STATE;
|
||||
|
||||
const BEST_FINALIZED_SOURCE_HEADER_ID_AT_TARGET: &'static str = bp_millau::FINALIZED_MILLAU_BLOCK_METHOD;
|
||||
const BEST_FINALIZED_TARGET_HEADER_ID_AT_SOURCE: &'static str = bp_rialto::FINALIZED_RIALTO_BLOCK_METHOD;
|
||||
|
||||
type SourceSignedTransaction = <Millau as TransactionSignScheme>::SignedTransaction;
|
||||
type TargetSignedTransaction = <Rialto as TransactionSignScheme>::SignedTransaction;
|
||||
|
||||
async fn make_messages_receiving_proof_transaction(
|
||||
&self,
|
||||
_generated_at_block: RialtoHeaderId,
|
||||
proof: <Self as MessageLane>::MessagesReceivingProof,
|
||||
) -> Result<Self::SourceSignedTransaction, SubstrateError> {
|
||||
let (relayers_state, proof) = proof;
|
||||
let account_id = self.source_sign.signer.public().as_array_ref().clone().into();
|
||||
let nonce = self.source_client.next_account_index(account_id).await?;
|
||||
let call: millau_runtime::Call =
|
||||
millau_runtime::MessageLaneCall::receive_messages_delivery_proof(proof, relayers_state).into();
|
||||
let call_weight = call.get_dispatch_info().weight;
|
||||
let transaction = Millau::sign_transaction(&self.source_client, &self.source_sign.signer, nonce, call);
|
||||
log::trace!(
|
||||
target: "bridge",
|
||||
"Prepared Rialto -> Millau confirmation transaction. Weight: {}/{}, size: {}/{}",
|
||||
call_weight,
|
||||
bp_millau::max_extrinsic_weight(),
|
||||
transaction.encode().len(),
|
||||
bp_millau::max_extrinsic_size(),
|
||||
);
|
||||
Ok(transaction)
|
||||
}
|
||||
|
||||
async fn make_messages_delivery_transaction(
|
||||
&self,
|
||||
_generated_at_header: MillauHeaderId,
|
||||
_nonces: RangeInclusive<MessageNonce>,
|
||||
proof: <Self as MessageLane>::MessagesProof,
|
||||
) -> Result<Self::TargetSignedTransaction, SubstrateError> {
|
||||
let (dispatch_weight, proof) = proof;
|
||||
let FromBridgedChainMessagesProof {
|
||||
ref nonces_start,
|
||||
ref nonces_end,
|
||||
..
|
||||
} = proof;
|
||||
let messages_count = nonces_end - nonces_start + 1;
|
||||
let account_id = self.target_sign.signer.public().as_array_ref().clone().into();
|
||||
let nonce = self.target_client.next_account_index(account_id).await?;
|
||||
let call: rialto_runtime::Call = rialto_runtime::MessageLaneCall::receive_messages_proof(
|
||||
self.relayer_id_at_source.clone(),
|
||||
proof,
|
||||
messages_count as _,
|
||||
dispatch_weight,
|
||||
)
|
||||
.into();
|
||||
let call_weight = call.get_dispatch_info().weight;
|
||||
let transaction = Rialto::sign_transaction(&self.target_client, &self.target_sign.signer, nonce, call);
|
||||
log::trace!(
|
||||
target: "bridge",
|
||||
"Prepared Millau -> Rialto delivery transaction. Weight: {}/{}, size: {}/{}",
|
||||
call_weight,
|
||||
bp_rialto::max_extrinsic_weight(),
|
||||
transaction.encode().len(),
|
||||
bp_rialto::max_extrinsic_size(),
|
||||
);
|
||||
Ok(transaction)
|
||||
}
|
||||
}
|
||||
|
||||
/// Millau node as messages source.
|
||||
type MillauSourceClient = SubstrateMessagesSource<Millau, MillauMessagesToRialto>;
|
||||
|
||||
/// Rialto node as messages target.
|
||||
type RialtoTargetClient = SubstrateMessagesTarget<Rialto, MillauMessagesToRialto>;
|
||||
|
||||
/// Run Millau-to-Rialto messages sync.
|
||||
pub fn run(
|
||||
millau_client: MillauClient,
|
||||
millau_sign: MillauSigningParams,
|
||||
rialto_client: RialtoClient,
|
||||
rialto_sign: RialtoSigningParams,
|
||||
lane_id: LaneId,
|
||||
metrics_params: Option<MetricsParams>,
|
||||
) {
|
||||
let stall_timeout = Duration::from_secs(5 * 60);
|
||||
let relayer_id_at_millau = millau_sign.signer.public().as_array_ref().clone().into();
|
||||
|
||||
let lane = MillauMessagesToRialto {
|
||||
source_client: millau_client.clone(),
|
||||
source_sign: millau_sign,
|
||||
target_client: rialto_client.clone(),
|
||||
target_sign: rialto_sign,
|
||||
relayer_id_at_source: relayer_id_at_millau,
|
||||
};
|
||||
|
||||
// 2/3 is reserved for proofs and tx overhead
|
||||
let max_messages_size_in_single_batch = bp_rialto::max_extrinsic_size() as usize / 3;
|
||||
// TODO: use Millau weights after https://github.com/paritytech/parity-bridges-common/issues/390
|
||||
let (max_messages_in_single_batch, max_messages_weight_in_single_batch) =
|
||||
select_delivery_transaction_limits::<pallet_message_lane::weights::RialtoWeight<millau_runtime::Runtime>>(
|
||||
bp_rialto::max_extrinsic_weight(),
|
||||
bp_rialto::MAX_UNREWARDED_RELAYER_ENTRIES_AT_INBOUND_LANE,
|
||||
);
|
||||
|
||||
log::info!(
|
||||
target: "bridge",
|
||||
"Starting Millau -> Rialto messages relay.\n\t\
|
||||
Millau relayer account id: {:?}\n\t\
|
||||
Max messages in single transaction: {}\n\t\
|
||||
Max messages size in single transaction: {}\n\t\
|
||||
Max messages weight in single transaction: {}",
|
||||
lane.relayer_id_at_source,
|
||||
max_messages_in_single_batch,
|
||||
max_messages_size_in_single_batch,
|
||||
max_messages_weight_in_single_batch,
|
||||
);
|
||||
|
||||
messages_relay::message_lane_loop::run(
|
||||
messages_relay::message_lane_loop::Params {
|
||||
lane: lane_id,
|
||||
source_tick: Millau::AVERAGE_BLOCK_INTERVAL,
|
||||
target_tick: Rialto::AVERAGE_BLOCK_INTERVAL,
|
||||
reconnect_delay: relay_utils::relay_loop::RECONNECT_DELAY,
|
||||
stall_timeout,
|
||||
delivery_params: messages_relay::message_lane_loop::MessageDeliveryParams {
|
||||
max_unrewarded_relayer_entries_at_target: bp_rialto::MAX_UNREWARDED_RELAYER_ENTRIES_AT_INBOUND_LANE,
|
||||
max_unconfirmed_nonces_at_target: bp_rialto::MAX_UNCONFIRMED_MESSAGES_AT_INBOUND_LANE,
|
||||
max_messages_in_single_batch,
|
||||
max_messages_weight_in_single_batch,
|
||||
max_messages_size_in_single_batch,
|
||||
},
|
||||
},
|
||||
MillauSourceClient::new(millau_client, lane.clone(), lane_id, RIALTO_BRIDGE_INSTANCE),
|
||||
RialtoTargetClient::new(rialto_client, lane, lane_id, MILLAU_BRIDGE_INSTANCE),
|
||||
metrics_params,
|
||||
futures::future::pending(),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
// Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Rialto-to-Millau headers sync entrypoint.
|
||||
|
||||
use crate::{
|
||||
headers_pipeline::{SubstrateHeadersSyncPipeline, SubstrateHeadersToSubstrate},
|
||||
MillauClient, RialtoClient,
|
||||
};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use bp_rialto::{
|
||||
BEST_RIALTO_BLOCKS_METHOD, FINALIZED_RIALTO_BLOCK_METHOD, INCOMPLETE_RIALTO_HEADERS_METHOD,
|
||||
IS_KNOWN_RIALTO_BLOCK_METHOD,
|
||||
};
|
||||
use headers_relay::sync_types::QueuedHeader;
|
||||
use relay_millau_client::{BridgeRialtoCall, Millau, SigningParams as MillauSigningParams};
|
||||
use relay_rialto_client::{HeaderId as RialtoHeaderId, Rialto, SyncHeader as RialtoSyncHeader};
|
||||
use relay_substrate_client::{Error as SubstrateError, TransactionSignScheme};
|
||||
use sp_core::Pair;
|
||||
use sp_runtime::Justification;
|
||||
|
||||
/// Rialto-to-Millau headers sync pipeline.
|
||||
type RialtoHeadersToMillau = SubstrateHeadersToSubstrate<Rialto, RialtoSyncHeader, Millau, MillauSigningParams>;
|
||||
/// Rialto header in-the-queue.
|
||||
type QueuedRialtoHeader = QueuedHeader<RialtoHeadersToMillau>;
|
||||
|
||||
#[async_trait]
|
||||
impl SubstrateHeadersSyncPipeline for RialtoHeadersToMillau {
|
||||
const BEST_BLOCK_METHOD: &'static str = BEST_RIALTO_BLOCKS_METHOD;
|
||||
const FINALIZED_BLOCK_METHOD: &'static str = FINALIZED_RIALTO_BLOCK_METHOD;
|
||||
const IS_KNOWN_BLOCK_METHOD: &'static str = IS_KNOWN_RIALTO_BLOCK_METHOD;
|
||||
const INCOMPLETE_HEADERS_METHOD: &'static str = INCOMPLETE_RIALTO_HEADERS_METHOD;
|
||||
|
||||
type SignedTransaction = <Millau as TransactionSignScheme>::SignedTransaction;
|
||||
|
||||
async fn make_submit_header_transaction(
|
||||
&self,
|
||||
header: QueuedRialtoHeader,
|
||||
) -> Result<Self::SignedTransaction, SubstrateError> {
|
||||
let account_id = self.target_sign.signer.public().as_array_ref().clone().into();
|
||||
let nonce = self.target_client.next_account_index(account_id).await?;
|
||||
let call = BridgeRialtoCall::import_signed_header(header.header().clone().into_inner()).into();
|
||||
let transaction = Millau::sign_transaction(&self.target_client, &self.target_sign.signer, nonce, call);
|
||||
Ok(transaction)
|
||||
}
|
||||
|
||||
async fn make_complete_header_transaction(
|
||||
&self,
|
||||
id: RialtoHeaderId,
|
||||
completion: Justification,
|
||||
) -> Result<Self::SignedTransaction, SubstrateError> {
|
||||
let account_id = self.target_sign.signer.public().as_array_ref().clone().into();
|
||||
let nonce = self.target_client.next_account_index(account_id).await?;
|
||||
let call = BridgeRialtoCall::finalize_header(id.1, completion).into();
|
||||
let transaction = Millau::sign_transaction(&self.target_client, &self.target_sign.signer, nonce, call);
|
||||
Ok(transaction)
|
||||
}
|
||||
}
|
||||
|
||||
/// Run Rialto-to-Millau headers sync.
|
||||
pub async fn run(
|
||||
rialto_client: RialtoClient,
|
||||
millau_client: MillauClient,
|
||||
millau_sign: MillauSigningParams,
|
||||
metrics_params: Option<relay_utils::metrics::MetricsParams>,
|
||||
) {
|
||||
crate::headers_pipeline::run(
|
||||
RialtoHeadersToMillau::new(millau_client.clone(), millau_sign),
|
||||
rialto_client,
|
||||
millau_client,
|
||||
metrics_params,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
// Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Rialto-to-Millau messages sync entrypoint.
|
||||
|
||||
use crate::messages_lane::{select_delivery_transaction_limits, SubstrateMessageLane, SubstrateMessageLaneToSubstrate};
|
||||
use crate::messages_source::SubstrateMessagesSource;
|
||||
use crate::messages_target::SubstrateMessagesTarget;
|
||||
use crate::{MillauClient, RialtoClient};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use bp_message_lane::{LaneId, MessageNonce};
|
||||
use bp_runtime::{MILLAU_BRIDGE_INSTANCE, RIALTO_BRIDGE_INSTANCE};
|
||||
use bridge_runtime_common::messages::target::FromBridgedChainMessagesProof;
|
||||
use codec::Encode;
|
||||
use frame_support::dispatch::GetDispatchInfo;
|
||||
use messages_relay::message_lane::MessageLane;
|
||||
use relay_millau_client::{HeaderId as MillauHeaderId, Millau, SigningParams as MillauSigningParams};
|
||||
use relay_rialto_client::{HeaderId as RialtoHeaderId, Rialto, SigningParams as RialtoSigningParams};
|
||||
use relay_substrate_client::{Chain, Error as SubstrateError, TransactionSignScheme};
|
||||
use relay_utils::metrics::MetricsParams;
|
||||
use sp_core::Pair;
|
||||
use std::{ops::RangeInclusive, time::Duration};
|
||||
|
||||
/// Rialto-to-Millau message lane.
|
||||
type RialtoMessagesToMillau = SubstrateMessageLaneToSubstrate<Rialto, RialtoSigningParams, Millau, MillauSigningParams>;
|
||||
|
||||
#[async_trait]
|
||||
impl SubstrateMessageLane for RialtoMessagesToMillau {
|
||||
const OUTBOUND_LANE_MESSAGES_DISPATCH_WEIGHT_METHOD: &'static str =
|
||||
bp_millau::TO_MILLAU_MESSAGES_DISPATCH_WEIGHT_METHOD;
|
||||
const OUTBOUND_LANE_LATEST_GENERATED_NONCE_METHOD: &'static str =
|
||||
bp_millau::TO_MILLAU_LATEST_GENERATED_NONCE_METHOD;
|
||||
const OUTBOUND_LANE_LATEST_RECEIVED_NONCE_METHOD: &'static str = bp_millau::TO_MILLAU_LATEST_RECEIVED_NONCE_METHOD;
|
||||
|
||||
const INBOUND_LANE_LATEST_RECEIVED_NONCE_METHOD: &'static str = bp_rialto::FROM_RIALTO_LATEST_RECEIVED_NONCE_METHOD;
|
||||
const INBOUND_LANE_LATEST_CONFIRMED_NONCE_METHOD: &'static str =
|
||||
bp_rialto::FROM_RIALTO_LATEST_CONFIRMED_NONCE_METHOD;
|
||||
const INBOUND_LANE_UNREWARDED_RELAYERS_STATE: &'static str = bp_rialto::FROM_RIALTO_UNREWARDED_RELAYERS_STATE;
|
||||
|
||||
const BEST_FINALIZED_SOURCE_HEADER_ID_AT_TARGET: &'static str = bp_rialto::FINALIZED_RIALTO_BLOCK_METHOD;
|
||||
const BEST_FINALIZED_TARGET_HEADER_ID_AT_SOURCE: &'static str = bp_millau::FINALIZED_MILLAU_BLOCK_METHOD;
|
||||
|
||||
type SourceSignedTransaction = <Rialto as TransactionSignScheme>::SignedTransaction;
|
||||
type TargetSignedTransaction = <Millau as TransactionSignScheme>::SignedTransaction;
|
||||
|
||||
async fn make_messages_receiving_proof_transaction(
|
||||
&self,
|
||||
_generated_at_block: MillauHeaderId,
|
||||
proof: <Self as MessageLane>::MessagesReceivingProof,
|
||||
) -> Result<Self::SourceSignedTransaction, SubstrateError> {
|
||||
let (relayers_state, proof) = proof;
|
||||
let account_id = self.source_sign.signer.public().as_array_ref().clone().into();
|
||||
let nonce = self.source_client.next_account_index(account_id).await?;
|
||||
let call: rialto_runtime::Call =
|
||||
rialto_runtime::MessageLaneCall::receive_messages_delivery_proof(proof, relayers_state).into();
|
||||
let call_weight = call.get_dispatch_info().weight;
|
||||
let transaction = Rialto::sign_transaction(&self.source_client, &self.source_sign.signer, nonce, call);
|
||||
log::trace!(
|
||||
target: "bridge",
|
||||
"Prepared Millau -> Rialto confirmation transaction. Weight: {}/{}, size: {}/{}",
|
||||
call_weight,
|
||||
bp_rialto::max_extrinsic_weight(),
|
||||
transaction.encode().len(),
|
||||
bp_rialto::max_extrinsic_size(),
|
||||
);
|
||||
Ok(transaction)
|
||||
}
|
||||
|
||||
async fn make_messages_delivery_transaction(
|
||||
&self,
|
||||
_generated_at_header: RialtoHeaderId,
|
||||
_nonces: RangeInclusive<MessageNonce>,
|
||||
proof: <Self as MessageLane>::MessagesProof,
|
||||
) -> Result<Self::TargetSignedTransaction, SubstrateError> {
|
||||
let (dispatch_weight, proof) = proof;
|
||||
let FromBridgedChainMessagesProof {
|
||||
ref nonces_start,
|
||||
ref nonces_end,
|
||||
..
|
||||
} = proof;
|
||||
let messages_count = nonces_end - nonces_start + 1;
|
||||
let account_id = self.target_sign.signer.public().as_array_ref().clone().into();
|
||||
let nonce = self.target_client.next_account_index(account_id).await?;
|
||||
let call: millau_runtime::Call = millau_runtime::MessageLaneCall::receive_messages_proof(
|
||||
self.relayer_id_at_source.clone(),
|
||||
proof,
|
||||
messages_count as _,
|
||||
dispatch_weight,
|
||||
)
|
||||
.into();
|
||||
let call_weight = call.get_dispatch_info().weight;
|
||||
let transaction = Millau::sign_transaction(&self.target_client, &self.target_sign.signer, nonce, call);
|
||||
log::trace!(
|
||||
target: "bridge",
|
||||
"Prepared Rialto -> Millau delivery transaction. Weight: {}/{}, size: {}/{}",
|
||||
call_weight,
|
||||
bp_millau::max_extrinsic_weight(),
|
||||
transaction.encode().len(),
|
||||
bp_millau::max_extrinsic_size(),
|
||||
);
|
||||
Ok(transaction)
|
||||
}
|
||||
}
|
||||
|
||||
/// Rialto node as messages source.
|
||||
type RialtoSourceClient = SubstrateMessagesSource<Rialto, RialtoMessagesToMillau>;
|
||||
|
||||
/// Millau node as messages target.
|
||||
type MillauTargetClient = SubstrateMessagesTarget<Millau, RialtoMessagesToMillau>;
|
||||
|
||||
/// Run Rialto-to-Millau messages sync.
|
||||
pub fn run(
|
||||
rialto_client: RialtoClient,
|
||||
rialto_sign: RialtoSigningParams,
|
||||
millau_client: MillauClient,
|
||||
millau_sign: MillauSigningParams,
|
||||
lane_id: LaneId,
|
||||
metrics_params: Option<MetricsParams>,
|
||||
) {
|
||||
let stall_timeout = Duration::from_secs(5 * 60);
|
||||
let relayer_id_at_rialto = rialto_sign.signer.public().as_array_ref().clone().into();
|
||||
|
||||
let lane = RialtoMessagesToMillau {
|
||||
source_client: rialto_client.clone(),
|
||||
source_sign: rialto_sign,
|
||||
target_client: millau_client.clone(),
|
||||
target_sign: millau_sign,
|
||||
relayer_id_at_source: relayer_id_at_rialto,
|
||||
};
|
||||
|
||||
// 2/3 is reserved for proofs and tx overhead
|
||||
let max_messages_size_in_single_batch = bp_millau::max_extrinsic_size() as usize / 3;
|
||||
let (max_messages_in_single_batch, max_messages_weight_in_single_batch) =
|
||||
select_delivery_transaction_limits::<pallet_message_lane::weights::RialtoWeight<rialto_runtime::Runtime>>(
|
||||
bp_millau::max_extrinsic_weight(),
|
||||
bp_millau::MAX_UNREWARDED_RELAYER_ENTRIES_AT_INBOUND_LANE,
|
||||
);
|
||||
|
||||
log::info!(
|
||||
target: "bridge",
|
||||
"Starting Rialto -> Millau messages relay.\n\t\
|
||||
Rialto relayer account id: {:?}\n\t\
|
||||
Max messages in single transaction: {}\n\t\
|
||||
Max messages size in single transaction: {}\n\t\
|
||||
Max messages weight in single transaction: {}",
|
||||
lane.relayer_id_at_source,
|
||||
max_messages_in_single_batch,
|
||||
max_messages_size_in_single_batch,
|
||||
max_messages_weight_in_single_batch,
|
||||
);
|
||||
|
||||
messages_relay::message_lane_loop::run(
|
||||
messages_relay::message_lane_loop::Params {
|
||||
lane: lane_id,
|
||||
source_tick: Rialto::AVERAGE_BLOCK_INTERVAL,
|
||||
target_tick: Millau::AVERAGE_BLOCK_INTERVAL,
|
||||
reconnect_delay: relay_utils::relay_loop::RECONNECT_DELAY,
|
||||
stall_timeout,
|
||||
delivery_params: messages_relay::message_lane_loop::MessageDeliveryParams {
|
||||
max_unrewarded_relayer_entries_at_target: bp_millau::MAX_UNREWARDED_RELAYER_ENTRIES_AT_INBOUND_LANE,
|
||||
max_unconfirmed_nonces_at_target: bp_millau::MAX_UNCONFIRMED_MESSAGES_AT_INBOUND_LANE,
|
||||
max_messages_in_single_batch,
|
||||
max_messages_weight_in_single_batch,
|
||||
max_messages_size_in_single_batch,
|
||||
},
|
||||
},
|
||||
RialtoSourceClient::new(rialto_client, lane.clone(), lane_id, MILLAU_BRIDGE_INSTANCE),
|
||||
MillauTargetClient::new(millau_client, lane, lane_id, RIALTO_BRIDGE_INSTANCE),
|
||||
metrics_params,
|
||||
futures::future::pending(),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "relay-utils"
|
||||
version = "0.1.0"
|
||||
authors = ["Parity Technologies <admin@parity.io>"]
|
||||
edition = "2018"
|
||||
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
|
||||
|
||||
[dependencies]
|
||||
ansi_term = "0.12"
|
||||
async-std = "1.6.5"
|
||||
async-trait = "0.1.40"
|
||||
backoff = "0.2"
|
||||
env_logger = "0.8.2"
|
||||
futures = "0.3.5"
|
||||
log = "0.4.11"
|
||||
num-traits = "0.2"
|
||||
sysinfo = "0.15"
|
||||
time = "0.2"
|
||||
|
||||
# Substrate dependencies
|
||||
|
||||
substrate-prometheus-endpoint = { git = "https://github.com/paritytech/substrate.git", branch = "master" }
|
||||
@@ -0,0 +1,59 @@
|
||||
// Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Relayer initialization functions.
|
||||
|
||||
use std::io::Write;
|
||||
|
||||
/// Initialize relay environment.
|
||||
pub fn initialize_relay() {
|
||||
let mut builder = env_logger::Builder::new();
|
||||
|
||||
let filters = match std::env::var("RUST_LOG") {
|
||||
Ok(env_filters) => format!("bridge=info,{}", env_filters),
|
||||
Err(_) => "bridge=info".into(),
|
||||
};
|
||||
|
||||
builder.parse_filters(&filters);
|
||||
builder.format(move |buf, record| {
|
||||
writeln!(buf, "{}", {
|
||||
let timestamp = time::OffsetDateTime::try_now_local()
|
||||
.unwrap_or_else(|_| time::OffsetDateTime::now_utc())
|
||||
.format("%Y-%m-%d %H:%M:%S %z");
|
||||
if cfg!(windows) {
|
||||
format!("{} {} {} {}", timestamp, record.level(), record.target(), record.args())
|
||||
} else {
|
||||
use ansi_term::Colour as Color;
|
||||
let log_level = match record.level() {
|
||||
log::Level::Error => Color::Fixed(9).bold().paint(record.level().to_string()),
|
||||
log::Level::Warn => Color::Fixed(11).bold().paint(record.level().to_string()),
|
||||
log::Level::Info => Color::Fixed(10).paint(record.level().to_string()),
|
||||
log::Level::Debug => Color::Fixed(14).paint(record.level().to_string()),
|
||||
log::Level::Trace => Color::Fixed(12).paint(record.level().to_string()),
|
||||
};
|
||||
format!(
|
||||
"{} {} {} {}",
|
||||
Color::Fixed(8).bold().paint(timestamp),
|
||||
log_level,
|
||||
Color::Fixed(8).paint(record.target()),
|
||||
record.args()
|
||||
)
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
builder.init();
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
// Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Utilities used by different relays.
|
||||
|
||||
use backoff::{backoff::Backoff, ExponentialBackoff};
|
||||
use futures::future::FutureExt;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Max delay after connection-unrelated error happened before we'll try the
|
||||
/// same request again.
|
||||
pub const MAX_BACKOFF_INTERVAL: Duration = Duration::from_secs(60);
|
||||
/// Delay after connection-related error happened before we'll try
|
||||
/// reconnection again.
|
||||
pub const CONNECTION_ERROR_DELAY: Duration = Duration::from_secs(10);
|
||||
|
||||
pub mod initialize;
|
||||
pub mod metrics;
|
||||
pub mod relay_loop;
|
||||
|
||||
/// Block number traits shared by all chains that relay is able to serve.
|
||||
pub trait BlockNumberBase:
|
||||
'static
|
||||
+ From<u32>
|
||||
+ Into<u64>
|
||||
+ Ord
|
||||
+ Clone
|
||||
+ Copy
|
||||
+ Default
|
||||
+ Send
|
||||
+ Sync
|
||||
+ std::fmt::Debug
|
||||
+ std::fmt::Display
|
||||
+ std::hash::Hash
|
||||
+ std::ops::Add<Output = Self>
|
||||
+ std::ops::Sub<Output = Self>
|
||||
+ num_traits::CheckedSub
|
||||
+ num_traits::Saturating
|
||||
+ num_traits::Zero
|
||||
+ num_traits::One
|
||||
{
|
||||
}
|
||||
|
||||
impl<T> BlockNumberBase for T where
|
||||
T: 'static
|
||||
+ From<u32>
|
||||
+ Into<u64>
|
||||
+ Ord
|
||||
+ Clone
|
||||
+ Copy
|
||||
+ Default
|
||||
+ Send
|
||||
+ Sync
|
||||
+ std::fmt::Debug
|
||||
+ std::fmt::Display
|
||||
+ std::hash::Hash
|
||||
+ std::ops::Add<Output = Self>
|
||||
+ std::ops::Sub<Output = Self>
|
||||
+ num_traits::CheckedSub
|
||||
+ num_traits::Saturating
|
||||
+ num_traits::Zero
|
||||
+ num_traits::One
|
||||
{
|
||||
}
|
||||
|
||||
/// Macro that returns (client, Err(error)) tuple from function if result is Err(error).
|
||||
#[macro_export]
|
||||
macro_rules! bail_on_error {
|
||||
($result: expr) => {
|
||||
match $result {
|
||||
(client, Ok(result)) => (client, result),
|
||||
(client, Err(error)) => return (client, Err(error)),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Macro that returns (client, Err(error)) tuple from function if result is Err(error).
|
||||
#[macro_export]
|
||||
macro_rules! bail_on_arg_error {
|
||||
($result: expr, $client: ident) => {
|
||||
match $result {
|
||||
Ok(result) => result,
|
||||
Err(error) => return ($client, Err(error)),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Ethereum header Id.
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, Hash, PartialEq)]
|
||||
pub struct HeaderId<Hash, Number>(pub Number, pub Hash);
|
||||
|
||||
/// Error type that can signal connection errors.
|
||||
pub trait MaybeConnectionError {
|
||||
/// Returns true if error (maybe) represents connection error.
|
||||
fn is_connection_error(&self) -> bool;
|
||||
}
|
||||
|
||||
/// Stringified error that may be either connection-related or not.
|
||||
#[derive(Debug)]
|
||||
pub enum StringifiedMaybeConnectionError {
|
||||
/// The error is connection-related error.
|
||||
Connection(String),
|
||||
/// The error is connection-unrelated error.
|
||||
NonConnection(String),
|
||||
}
|
||||
|
||||
impl StringifiedMaybeConnectionError {
|
||||
/// Create new stringified connection error.
|
||||
pub fn new(is_connection_error: bool, error: String) -> Self {
|
||||
if is_connection_error {
|
||||
StringifiedMaybeConnectionError::Connection(error)
|
||||
} else {
|
||||
StringifiedMaybeConnectionError::NonConnection(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MaybeConnectionError for StringifiedMaybeConnectionError {
|
||||
fn is_connection_error(&self) -> bool {
|
||||
match *self {
|
||||
StringifiedMaybeConnectionError::Connection(_) => true,
|
||||
StringifiedMaybeConnectionError::NonConnection(_) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for StringifiedMaybeConnectionError {
|
||||
fn to_string(&self) -> String {
|
||||
match *self {
|
||||
StringifiedMaybeConnectionError::Connection(ref err) => err.clone(),
|
||||
StringifiedMaybeConnectionError::NonConnection(ref err) => err.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Exponential backoff for connection-unrelated errors retries.
|
||||
pub fn retry_backoff() -> ExponentialBackoff {
|
||||
ExponentialBackoff {
|
||||
// we do not want relayer to stop
|
||||
max_elapsed_time: None,
|
||||
max_interval: MAX_BACKOFF_INTERVAL,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Compact format of IDs vector.
|
||||
pub fn format_ids<Id: std::fmt::Debug>(mut ids: impl ExactSizeIterator<Item = Id>) -> String {
|
||||
const NTH_PROOF: &str = "we have checked len; qed";
|
||||
match ids.len() {
|
||||
0 => "<nothing>".into(),
|
||||
1 => format!("{:?}", ids.next().expect(NTH_PROOF)),
|
||||
2 => {
|
||||
let id0 = ids.next().expect(NTH_PROOF);
|
||||
let id1 = ids.next().expect(NTH_PROOF);
|
||||
format!("[{:?}, {:?}]", id0, id1)
|
||||
}
|
||||
len => {
|
||||
let id0 = ids.next().expect(NTH_PROOF);
|
||||
let id_last = ids.last().expect(NTH_PROOF);
|
||||
format!("{}:[{:?} ... {:?}]", len, id0, id_last)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Stream that emits item every `timeout_ms` milliseconds.
|
||||
pub fn interval(timeout: Duration) -> impl futures::Stream<Item = ()> {
|
||||
futures::stream::unfold((), move |_| async move {
|
||||
async_std::task::sleep(timeout).await;
|
||||
Some(((), ()))
|
||||
})
|
||||
}
|
||||
|
||||
/// Which client has caused error.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum FailedClient {
|
||||
/// It is the source client who has caused error.
|
||||
Source,
|
||||
/// It is the target client who has caused error.
|
||||
Target,
|
||||
/// Both clients are failing, or we just encountered some other error that
|
||||
/// should be treated like that.
|
||||
Both,
|
||||
}
|
||||
|
||||
/// Future process result.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum ProcessFutureResult {
|
||||
/// Future has been processed successfully.
|
||||
Success,
|
||||
/// Future has failed with non-connection error.
|
||||
Failed,
|
||||
/// Future has failed with connection error.
|
||||
ConnectionFailed,
|
||||
}
|
||||
|
||||
impl ProcessFutureResult {
|
||||
/// Returns true if result is Success.
|
||||
pub fn is_ok(self) -> bool {
|
||||
match self {
|
||||
ProcessFutureResult::Success => true,
|
||||
ProcessFutureResult::Failed | ProcessFutureResult::ConnectionFailed => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns Ok(true) if future has succeeded.
|
||||
/// Returns Ok(false) if future has failed with non-connection error.
|
||||
/// Returns Err if future is `ConnectionFailed`.
|
||||
pub fn fail_if_connection_error(self, failed_client: FailedClient) -> Result<bool, FailedClient> {
|
||||
match self {
|
||||
ProcessFutureResult::Success => Ok(true),
|
||||
ProcessFutureResult::Failed => Ok(false),
|
||||
ProcessFutureResult::ConnectionFailed => Err(failed_client),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Process result of the future from a client.
|
||||
pub fn process_future_result<TResult, TError, TGoOfflineFuture>(
|
||||
result: Result<TResult, TError>,
|
||||
retry_backoff: &mut ExponentialBackoff,
|
||||
on_success: impl FnOnce(TResult),
|
||||
go_offline_future: &mut std::pin::Pin<&mut futures::future::Fuse<TGoOfflineFuture>>,
|
||||
go_offline: impl FnOnce(Duration) -> TGoOfflineFuture,
|
||||
error_pattern: impl FnOnce() -> String,
|
||||
) -> ProcessFutureResult
|
||||
where
|
||||
TError: std::fmt::Debug + MaybeConnectionError,
|
||||
TGoOfflineFuture: FutureExt,
|
||||
{
|
||||
match result {
|
||||
Ok(result) => {
|
||||
on_success(result);
|
||||
retry_backoff.reset();
|
||||
ProcessFutureResult::Success
|
||||
}
|
||||
Err(error) if error.is_connection_error() => {
|
||||
log::error!(
|
||||
target: "bridge",
|
||||
"{}: {:?}. Going to restart",
|
||||
error_pattern(),
|
||||
error,
|
||||
);
|
||||
|
||||
retry_backoff.reset();
|
||||
go_offline_future.set(go_offline(CONNECTION_ERROR_DELAY).fuse());
|
||||
ProcessFutureResult::ConnectionFailed
|
||||
}
|
||||
Err(error) => {
|
||||
let retry_delay = retry_backoff.next_backoff().unwrap_or(CONNECTION_ERROR_DELAY);
|
||||
log::error!(
|
||||
target: "bridge",
|
||||
"{}: {:?}. Retrying in {}",
|
||||
error_pattern(),
|
||||
error,
|
||||
retry_delay.as_secs_f64(),
|
||||
);
|
||||
|
||||
go_offline_future.set(go_offline(retry_delay).fuse());
|
||||
ProcessFutureResult::Failed
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
// Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
pub use substrate_prometheus_endpoint::{register, Counter, CounterVec, Gauge, GaugeVec, Opts, Registry, F64, U64};
|
||||
|
||||
use async_std::sync::{Arc, Mutex};
|
||||
use std::net::SocketAddr;
|
||||
use substrate_prometheus_endpoint::init_prometheus;
|
||||
use sysinfo::{ProcessExt, RefreshKind, System, SystemExt};
|
||||
|
||||
/// Prometheus endpoint MetricsParams.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MetricsParams {
|
||||
/// Serve HTTP requests at given host.
|
||||
pub host: String,
|
||||
/// Serve HTTP requests at given port.
|
||||
pub port: u16,
|
||||
}
|
||||
|
||||
/// Metrics API.
|
||||
pub trait Metrics {
|
||||
/// Register metrics in the registry.
|
||||
fn register(&self, registry: &Registry) -> Result<(), String>;
|
||||
}
|
||||
|
||||
/// Global Prometheus metrics.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GlobalMetrics {
|
||||
system: Arc<Mutex<System>>,
|
||||
system_average_load: GaugeVec<F64>,
|
||||
process_cpu_usage_percentage: Gauge<F64>,
|
||||
process_memory_usage_bytes: Gauge<U64>,
|
||||
}
|
||||
|
||||
/// Start Prometheus endpoint with given metrics registry.
|
||||
pub fn start(
|
||||
prefix: String,
|
||||
params: Option<MetricsParams>,
|
||||
global_metrics: &GlobalMetrics,
|
||||
extra_metrics: &impl Metrics,
|
||||
) {
|
||||
let params = match params {
|
||||
Some(params) => params,
|
||||
None => return,
|
||||
};
|
||||
|
||||
assert!(!prefix.is_empty(), "Metrics prefix can not be empty");
|
||||
|
||||
let do_start = move || {
|
||||
let prometheus_socket_addr = SocketAddr::new(
|
||||
params
|
||||
.host
|
||||
.parse()
|
||||
.map_err(|err| format!("Invalid Prometheus host {}: {}", params.host, err))?,
|
||||
params.port,
|
||||
);
|
||||
let metrics_registry =
|
||||
Registry::new_custom(Some(prefix), None).expect("only fails if prefix is empty; prefix is not empty; qed");
|
||||
global_metrics.register(&metrics_registry)?;
|
||||
extra_metrics.register(&metrics_registry)?;
|
||||
async_std::task::spawn(async move {
|
||||
init_prometheus(prometheus_socket_addr, metrics_registry)
|
||||
.await
|
||||
.map_err(|err| format!("Error starting Prometheus endpoint: {}", err))
|
||||
});
|
||||
|
||||
Ok(())
|
||||
};
|
||||
|
||||
let result: Result<(), String> = do_start();
|
||||
if let Err(err) = result {
|
||||
log::warn!(
|
||||
target: "bridge",
|
||||
"Failed to expose metrics: {}",
|
||||
err,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MetricsParams {
|
||||
fn default() -> Self {
|
||||
MetricsParams {
|
||||
host: "127.0.0.1".into(),
|
||||
port: 9616,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Metrics for GlobalMetrics {
|
||||
fn register(&self, registry: &Registry) -> Result<(), String> {
|
||||
register(self.system_average_load.clone(), registry).map_err(|e| e.to_string())?;
|
||||
register(self.process_cpu_usage_percentage.clone(), registry).map_err(|e| e.to_string())?;
|
||||
register(self.process_memory_usage_bytes.clone(), registry).map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for GlobalMetrics {
|
||||
fn default() -> Self {
|
||||
GlobalMetrics {
|
||||
system: Arc::new(Mutex::new(System::new_with_specifics(RefreshKind::everything()))),
|
||||
system_average_load: GaugeVec::new(Opts::new("system_average_load", "System load average"), &["over"])
|
||||
.expect("metric is static and thus valid; qed"),
|
||||
process_cpu_usage_percentage: Gauge::new("process_cpu_usage_percentage", "Process CPU usage")
|
||||
.expect("metric is static and thus valid; qed"),
|
||||
process_memory_usage_bytes: Gauge::new(
|
||||
"process_memory_usage_bytes",
|
||||
"Process memory (resident set size) usage",
|
||||
)
|
||||
.expect("metric is static and thus valid; qed"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GlobalMetrics {
|
||||
/// Update metrics.
|
||||
pub async fn update(&self) {
|
||||
// update system-wide metrics
|
||||
let mut system = self.system.lock().await;
|
||||
let load = system.get_load_average();
|
||||
self.system_average_load.with_label_values(&["1min"]).set(load.one);
|
||||
self.system_average_load.with_label_values(&["5min"]).set(load.five);
|
||||
self.system_average_load.with_label_values(&["15min"]).set(load.fifteen);
|
||||
|
||||
// update process-related metrics
|
||||
let pid = sysinfo::get_current_pid().expect(
|
||||
"only fails where pid is unavailable (os=unknown || arch=wasm32);\
|
||||
relay is not supposed to run in such MetricsParamss;\
|
||||
qed",
|
||||
);
|
||||
let is_process_refreshed = system.refresh_process(pid);
|
||||
match (is_process_refreshed, system.get_process(pid)) {
|
||||
(true, Some(process_info)) => {
|
||||
let cpu_usage = process_info.cpu_usage() as f64;
|
||||
let memory_usage = process_info.memory() * 1024;
|
||||
log::trace!(
|
||||
target: "bridge-metrics",
|
||||
"Refreshed process metrics: CPU={}, memory={}",
|
||||
cpu_usage,
|
||||
memory_usage,
|
||||
);
|
||||
|
||||
self.process_cpu_usage_percentage
|
||||
.set(if cpu_usage.is_finite() { cpu_usage } else { 0f64 });
|
||||
self.process_memory_usage_bytes.set(memory_usage);
|
||||
}
|
||||
_ => {
|
||||
log::warn!(
|
||||
target: "bridge",
|
||||
"Failed to refresh process information. Metrics may show obsolete values",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::{FailedClient, MaybeConnectionError};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use std::{fmt::Debug, future::Future, time::Duration};
|
||||
|
||||
/// Default pause between reconnect attempts.
|
||||
pub const RECONNECT_DELAY: Duration = Duration::from_secs(10);
|
||||
|
||||
/// Basic blockchain client from relay perspective.
|
||||
#[async_trait]
|
||||
pub trait Client: Clone + Send + Sync {
|
||||
/// Type of error this clients returns.
|
||||
type Error: Debug + MaybeConnectionError;
|
||||
|
||||
/// Try to reconnect to source node.
|
||||
async fn reconnect(&mut self) -> Result<(), Self::Error>;
|
||||
}
|
||||
|
||||
/// Run relay loop.
|
||||
///
|
||||
/// This function represents an outer loop, which in turn calls provided `loop_run` function to do
|
||||
/// actual job. When `loop_run` returns, this outer loop reconnects to failed client (source,
|
||||
/// target or both) and calls `loop_run` again.
|
||||
pub fn run<SC: Client, TC: Client, R, F>(
|
||||
reconnect_delay: Duration,
|
||||
mut source_client: SC,
|
||||
mut target_client: TC,
|
||||
loop_run: R,
|
||||
) where
|
||||
R: Fn(SC, TC) -> F,
|
||||
F: Future<Output = Result<(), FailedClient>>,
|
||||
{
|
||||
let mut local_pool = futures::executor::LocalPool::new();
|
||||
|
||||
local_pool.run_until(async move {
|
||||
loop {
|
||||
let result = loop_run(source_client.clone(), target_client.clone()).await;
|
||||
|
||||
match result {
|
||||
Ok(()) => break,
|
||||
Err(failed_client) => loop {
|
||||
async_std::task::sleep(reconnect_delay).await;
|
||||
if failed_client == FailedClient::Both || failed_client == FailedClient::Source {
|
||||
match source_client.reconnect().await {
|
||||
Ok(()) => (),
|
||||
Err(error) => {
|
||||
log::warn!(
|
||||
target: "bridge",
|
||||
"Failed to reconnect to source client. Going to retry in {}s: {:?}",
|
||||
reconnect_delay.as_secs(),
|
||||
error,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
if failed_client == FailedClient::Both || failed_client == FailedClient::Target {
|
||||
match target_client.reconnect().await {
|
||||
Ok(()) => (),
|
||||
Err(error) => {
|
||||
log::warn!(
|
||||
target: "bridge",
|
||||
"Failed to reconnect to target client. Going to retry in {}s: {:?}",
|
||||
reconnect_delay.as_secs(),
|
||||
error,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
},
|
||||
}
|
||||
|
||||
log::debug!(target: "bridge", "Restarting relay loop");
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user