Merge commit '114f487fd9daef4b4cd791446372a9a690c137ac' into update-bridges-subtree-r/w

This commit is contained in:
antonio-dropulic
2021-12-01 16:34:30 +01:00
183 changed files with 1017 additions and 21238 deletions
@@ -1,42 +0,0 @@
[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]
anyhow = "1.0"
async-std = "1.9.0"
async-trait = "0.1.42"
clap = { version = "2.33.3", features = ["yaml"] }
codec = { package = "parity-scale-codec", version = "2.2.0" }
ethabi = { git = "https://github.com/svyatonik/ethabi.git", branch = "bump-deps" }
ethabi-contract = { git = "https://github.com/svyatonik/ethabi.git", branch = "bump-deps" }
ethabi-derive = { git = "https://github.com/svyatonik/ethabi.git", branch = "bump-deps" }
futures = "0.3.12"
hex = "0.4"
hex-literal = "0.3"
libsecp256k1 = { version = "0.7", default-features = false, features = ["hmac"] }
log = "0.4.14"
num-traits = "0.2"
serde_json = "1.0.64"
thiserror = "1.0.26"
# Bridge dependencies
bp-currency-exchange = { path = "../../primitives/currency-exchange" }
bp-eth-poa = { path = "../../primitives/ethereum-poa" }
exchange-relay = { path = "../exchange" }
headers-relay = { path = "../headers" }
relay-ethereum-client = { path = "../client-ethereum" }
relay-rialto-client = { path = "../client-rialto" }
relay-substrate-client = { path = "../client-substrate" }
relay-utils = { path = "../utils" }
rialto-runtime = { path = "../../bin/rialto/runtime" }
# Substrate Dependencies
sp-core = { git = "https://github.com/paritytech/substrate", branch = "master" }
sp-keyring = { git = "https://github.com/paritytech/substrate", branch = "master" }
sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "master" }
@@ -1,7 +0,0 @@
# 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.
@@ -1,167 +0,0 @@
[
{
"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
@@ -1,5 +0,0 @@
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`
@@ -1,166 +0,0 @@
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: &eth-host
long: eth-host
value_name: ETH_HOST
help: Connect to Ethereum node websocket server at given host.
takes_value: true
- eth-port: &eth-port
long: eth-port
value_name: ETH_PORT
help: Connect to Ethereum node websocket server at given port.
takes_value: true
- sub-host: &sub-host
long: sub-host
value_name: SUB_HOST
help: Connect to Substrate node websocket server 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: &eth-chain-id
long: eth-chain-id
value_name: ETH_CHAIN_ID
help: Chain ID to use for signing.
- eth-signer: &eth-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
@@ -1,38 +0,0 @@
// Copyright 2021 Parity Technologies (UK) Ltd.
// This file is part of Parity Bridges Common.
// Parity Bridges Common is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity Bridges Common is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity Bridges Common. If not, see <http://www.gnu.org/licenses/>.
use crate::rpc_errors::RpcError;
use thiserror::Error;
/// Result type used by PoA relay.
pub type Result<T> = std::result::Result<T, Error>;
/// Ethereum PoA relay errors.
#[derive(Error, Debug)]
pub enum Error {
/// Failed to decode initial header.
#[error("Error decoding initial header: {0}")]
DecodeInitialHeader(codec::Error),
/// RPC error.
#[error("{0}")]
Rpc(#[from] RpcError),
/// Failed to read genesis header.
#[error("Error reading Substrate genesis header: {0:?}")]
ReadGenesisHeader(relay_substrate_client::Error),
/// Failed to read initial GRANDPA authorities.
#[error("Error reading GRANDPA authorities set: {0:?}")]
ReadAuthorities(relay_substrate_client::Error),
/// Failed to deploy bridge contract to Ethereum chain.
#[error("Error deploying contract: {0:?}")]
DeployContract(RpcError),
}
@@ -1,631 +0,0 @@
// Copyright 2019-2021 Parity Technologies (UK) Ltd.
// This file is part of Parity Bridges Common.
// Parity Bridges Common is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity Bridges Common is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity Bridges Common. If not, see <http://www.gnu.org/licenses/>.
use crate::{rpc_errors::RpcError, 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::EncodedJustification;
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 the 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: EncodedJustification,
) -> 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(&params.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: EncodedJustification,
) -> RpcResult<RialtoHeaderId> {
let _ = self
.submit_ethereum_transaction(
&params,
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());
}
}
@@ -1,164 +0,0 @@
// Copyright 2019-2021 Parity Technologies (UK) Ltd.
// This file is part of Parity Bridges Common.
// Parity Bridges Common is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity Bridges Common is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity Bridges Common. If not, see <http://www.gnu.org/licenses/>.
use crate::{
error::{Error, Result},
ethereum_client::{bridge_contract, EthereumHighLevelRpc},
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 async fn run(params: EthereumDeployContractParams) {
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 = async move {
let eth_client = EthereumClient::try_connect(eth_params).await.map_err(RpcError::Ethereum)?;
let sub_client = SubstrateClient::<Rialto>::try_connect(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(
&eth_client,
&eth_sign,
eth_contract_code,
initial_header,
initial_set_id,
initial_set,
).await
}.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>)> {
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(Error::DecodeInitialHeader(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::ReadGenesisHeader)
},
}
}
/// 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> {
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::ReadAuthorities)
}
/// 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<()> {
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::DeployContract)
}
@@ -1,402 +0,0 @@
// Copyright 2019-2021 Parity Technologies (UK) Ltd.
// This file is part of Parity Bridges Common.
// Parity Bridges Common is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity Bridges Common is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity Bridges Common. If not, see <http://www.gnu.org/licenses/>.
//! Relaying proofs of PoA -> Substrate exchange transactions.
use crate::{
instances::BridgeInstance,
rialto_client::{SubmitEthereumExchangeTransactionProof, SubstrateHighLevelRpc},
rpc_errors::RpcError,
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,
},
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.
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: MetricsParams,
/// Instance of the bridge pallet being synchronized.
pub instance: Arc<dyn BridgeInstance>,
}
impl std::fmt::Debug for EthereumExchangeParams {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.debug_struct("EthereumExchangeParams")
.field("eth_params", &self.eth_params)
.field("sub_params", &self.sub_params)
.field("sub_sign", &sp_core::Pair::public(&self.sub_sign))
.field("mode", &self.mode)
.field("metrics_params", &self.metrics_params)
.field("instance", &self.instance)
.finish()
}
}
/// 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().await.map_err(Into::into)
}
}
#[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 async fn run(params: EthereumExchangeParams) {
match params.mode {
ExchangeRelayMode::Single(eth_tx_hash) => {
let result = run_single_transaction_relay(params, 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,
),
}
},
ExchangeRelayMode::Auto(eth_start_with_block_number) => {
let result =
run_auto_transactions_relay_loop(params, eth_start_with_block_number).await;
if let Err(err) = result {
log::error!(
target: "bridge",
"Error auto-relaying Ethereum transactions proofs to Substrate node: {}",
err,
);
}
},
}
}
/// Run single transaction proof relay and stop.
async fn run_single_transaction_relay(
params: EthereumExchangeParams,
eth_tx_hash: H256,
) -> anyhow::Result<()> {
let EthereumExchangeParams { eth_params, sub_params, sub_sign, instance, .. } = params;
let eth_client = EthereumClient::try_connect(eth_params).await.map_err(RpcError::Ethereum)?;
let sub_client = SubstrateClient::<Rialto>::try_connect(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
.map_err(Into::into)
}
async fn run_auto_transactions_relay_loop(
params: EthereumExchangeParams,
eth_start_with_block_number: Option<u64>,
) -> anyhow::Result<()> {
let EthereumExchangeParams {
eth_params, sub_params, sub_sign, metrics_params, instance, ..
} = params;
let eth_client = EthereumClient::new(eth_params).await;
let sub_client = SubstrateClient::<Rialto>::new(sub_params).await;
let eth_start_with_block_number = match eth_start_with_block_number {
Some(eth_start_with_block_number) => eth_start_with_block_number,
None =>
sub_client
.best_ethereum_finalized_block()
.await
.map_err(|err| {
anyhow::format_err!(
"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(),
)
.await?;
Ok(())
}
@@ -1,110 +0,0 @@
// Copyright 2019-2021 Parity Technologies (UK) Ltd.
// This file is part of Parity Bridges Common.
// Parity Bridges Common is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity Bridges Common is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity Bridges Common. If not, see <http://www.gnu.org/licenses/>.
//! Submitting Ethereum -> Substrate exchange transactions.
use anyhow::anyhow;
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 async fn run(params: EthereumExchangeSubmitParams) {
let EthereumExchangeSubmitParams { eth_params, eth_sign, eth_nonce, eth_amount, sub_recipient } =
params;
let result: anyhow::Result<_> = async move {
let eth_client = EthereumClient::try_connect(eth_params)
.await
.map_err(|err| anyhow!("error connecting to Ethereum node: {:?}", err))?;
let eth_signer_address = secret_to_address(&eth_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| anyhow!("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| anyhow!("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(&eth_sign.signer, Some(eth_sign.chain_id));
eth_client
.submit_transaction(eth_tx_signed)
.await
.map_err(|err| anyhow!("error submitting transaction: {:?}", err))?;
Ok(eth_tx_unsigned)
}
.await;
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,
);
},
}
}
@@ -1,299 +0,0 @@
// Copyright 2019-2021 Parity Technologies (UK) Ltd.
// This file is part of Parity Bridges Common.
// Parity Bridges Common is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity Bridges Common is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity Bridges Common. If not, see <http://www.gnu.org/licenses/>.
//! Ethereum PoA -> Rialto-Substrate synchronization.
use crate::{
ethereum_client::EthereumHighLevelRpc,
instances::BridgeInstance,
rialto_client::{SubmitEthereumHeaders, SubstrateHighLevelRpc},
rpc_errors::RpcError,
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::{collections::HashSet, fmt::Debug, 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.
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: MetricsParams,
/// Instance of the bridge pallet being synchronized.
pub instance: Arc<dyn BridgeInstance>,
}
impl Debug for EthereumSyncParams {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.debug_struct("EthereumSyncParams")
.field("eth_params", &self.eth_params)
.field("sub_params", &self.sub_params)
.field("sub_sign", &sp_core::Pair::public(&self.sub_sign))
.field("sync_params", &self.sync_params)
.field("metrics_params", &self.metrics_params)
.field("instance", &self.instance)
.finish()
}
}
/// 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().await.map_err(Into::into)
}
}
#[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 async 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).await;
let sub_client = 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(),
)
.await
.map_err(|e| RpcError::SyncLoop(e.to_string()))?;
Ok(())
}
@@ -1,119 +0,0 @@
// Copyright 2020-2021 Parity Technologies (UK) Ltd.
// This file is part of Parity Bridges Common.
// Parity Bridges Common is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity Bridges Common is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity Bridges Common. If not, see <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,
substrate_types::{into_substrate_ethereum_header, into_substrate_ethereum_receipts},
};
use rialto_runtime::{exchange::EthereumTransactionInclusionProof as Proof, 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_with_receipts: 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 {
header: Box::new(into_substrate_ethereum_header(header.header())),
receipts: 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_with_receipts: 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 {
header: Box::new(into_substrate_ethereum_header(header.header())),
receipts: 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)
}
}
@@ -1,424 +0,0 @@
// Copyright 2019-2021 Parity Technologies (UK) Ltd.
// This file is part of Parity Bridges Common.
// Parity Bridges Common is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity Bridges Common is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity Bridges Common. If not, see <http://www.gnu.org/licenses/>.
#![recursion_limit = "1024"]
mod error;
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 anyhow::anyhow;
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 libsecp256k1::SecretKey;
use relay_utils::{
initialize::initialize_relay,
metrics::{MetricsAddress, MetricsParams},
};
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();
async_std::task::block_on(run_command(&matches));
}
async fn run_command(matches: &clap::ArgMatches<'_>) {
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(eth_to_sub_matches) {
Ok(ethereum_sync_params) => ethereum_sync_params,
Err(err) => {
log::error!(target: "bridge", "Error parsing parameters: {}", err);
return
},
})
.await
.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
},
})
.await
.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(eth_deploy_matches) {
Ok(ethereum_deploy_params) => ethereum_deploy_params,
Err(err) => {
log::error!(target: "bridge", "Error during contract deployment: {}", err);
return
},
},
)
.await;
},
("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(eth_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
},
},
)
.await;
},
("eth-exchange-sub", Some(eth_exchange_matches)) => {
log::info!(target: "bridge", "Starting ETH ➡ SUB exchange transactions relay.");
ethereum_exchange::run(match ethereum_exchange_params(eth_exchange_matches) {
Ok(eth_exchange_params) => eth_exchange_params,
Err(err) => {
log::error!(target: "bridge", "Error relaying Ethereum transactions proofs: {}", err);
return
},
})
.await;
},
("", _) => {
log::error!(target: "bridge", "No subcommand specified");
},
_ => unreachable!("all possible subcommands are checked above; qed"),
}
}
fn ethereum_connection_params(
matches: &clap::ArgMatches,
) -> anyhow::Result<EthereumConnectionParams> {
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| anyhow!("Failed to parse eth-port: {}", e))?;
}
Ok(params)
}
fn ethereum_signing_params(matches: &clap::ArgMatches) -> anyhow::Result<EthereumSigningParams> {
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| anyhow!("Failed to parse eth-signer: {}", e))?,
)
.map_err(|e| anyhow!("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| anyhow!("Failed to parse eth-chain-id: {}", e))?;
}
Ok(params)
}
fn substrate_connection_params(
matches: &clap::ArgMatches,
) -> anyhow::Result<SubstrateConnectionParams> {
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| anyhow!("Failed to parse sub-port: {}", e))?;
}
Ok(params)
}
fn rialto_signing_params(matches: &clap::ArgMatches) -> anyhow::Result<RialtoSigningParams> {
let mut params = sp_keyring::AccountKeyring::Alice.pair();
if let Some(sub_signer) = matches.value_of("sub-signer") {
let sub_signer_password = matches.value_of("sub-signer-password");
params = sp_core::sr25519::Pair::from_string(sub_signer, sub_signer_password)
.map_err(|e| anyhow!("Failed to parse sub-signer: {:?}", e))?;
}
Ok(params)
}
fn ethereum_sync_params(matches: &clap::ArgMatches) -> anyhow::Result<EthereumSyncParams> {
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(anyhow!("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) -> anyhow::Result<SubstrateSyncParams> {
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()?
} 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,
) -> anyhow::Result<EthereumDeployContractParams> {
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 = matches
.value_of("sub-authorities-set-id")
.map(|set| {
set.parse()
.map_err(|e| anyhow!("Failed to parse sub-authorities-set-id: {}", e))
})
.transpose()?;
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,
) -> anyhow::Result<EthereumExchangeSubmitParams> {
let eth_nonce = matches
.value_of("eth-nonce")
.map(|eth_nonce| {
relay_ethereum_client::types::U256::from_dec_str(eth_nonce)
.map_err(|e| anyhow!("Failed to parse eth-nonce: {}", e))
})
.transpose()?;
let eth_amount = matches
.value_of("eth-amount")
.map(|eth_amount| {
eth_amount.parse().map_err(|e| anyhow!("Failed to parse eth-amount: {}", e))
})
.transpose()?
.unwrap_or_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| anyhow!("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) -> anyhow::Result<EthereumExchangeParams> {
let mode = match matches.value_of("eth-tx-hash") {
Some(eth_tx_hash) => ethereum_exchange::ExchangeRelayMode::Single(
eth_tx_hash.parse().map_err(|e| anyhow!("Failed to parse eth-tx-hash: {}", e))?,
),
None => ethereum_exchange::ExchangeRelayMode::Auto(
matches
.value_of("eth-start-with-block")
.map(|eth_start_with_block| {
eth_start_with_block
.parse()
.map_err(|e| anyhow!("Failed to parse eth-start-with-block: {}", e))
})
.transpose()?,
),
};
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) -> anyhow::Result<MetricsParams> {
if matches.is_present("no-prometheus") {
return Ok(None.into())
}
let mut metrics_params = MetricsAddress::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| anyhow!("Failed to parse prometheus-port: {}", e))?;
}
Ok(Some(metrics_params).into())
}
fn instance_params(matches: &clap::ArgMatches) -> anyhow::Result<Arc<dyn BridgeInstance>> {
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(anyhow!("Unsupported bridge pallet instance")),
}
} else {
unreachable!("CLI config enforces a default instance, can never be None")
};
Ok(instance)
}
fn parse_hex_argument(matches: &clap::ArgMatches, arg: &str) -> anyhow::Result<Option<Vec<u8>>> {
match matches.value_of(arg) {
Some(value) =>
Ok(Some(hex::decode(value).map_err(|e| anyhow!("Failed to parse {}: {}", arg, e))?)),
None => Ok(None),
}
}
@@ -1,300 +0,0 @@
// Copyright 2019-2021 Parity Technologies (UK) Ltd.
// This file is part of Parity Bridges Common.
// Parity Bridges Common is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity Bridges Common is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity Bridges Common. If not, see <http://www.gnu.org/licenses/>.
use crate::{
ethereum_sync_loop::QueuedEthereumHeader, instances::BridgeInstance, 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, UnsignedTransaction,
};
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 the 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 transactions receipts are required for Ethereum header submission.
async fn ethereum_receipts_required(&self, header: SubstrateEthereumHeader) -> RpcResult<bool>;
/// Returns whether 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 genesis_hash = *self.genesis_hash();
let submission_result = async {
self.submit_signed_extrinsic(
(*params.public().as_array_ref()).into(),
move |_, transaction_nonce| {
Bytes(
Rialto::sign_transaction(
genesis_hash,
&params,
relay_substrate_client::TransactionEra::immortal(),
UnsignedTransaction::new(
instance.build_signed_header_call(headers),
transaction_nonce,
),
)
.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_unsigned_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 genesis_hash = *self.genesis_hash();
self.submit_signed_extrinsic(
(*params.public().as_array_ref()).into(),
move |_, transaction_nonce| {
Bytes(
Rialto::sign_transaction(
genesis_hash,
&params,
relay_substrate_client::TransactionEra::immortal(),
UnsignedTransaction::new(
instance.build_currency_exchange_call(proof),
transaction_nonce,
),
)
.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)
}
@@ -1,67 +0,0 @@
// Copyright 2019-2021 Parity Technologies (UK) Ltd.
// This file is part of Parity Bridges Common.
// Parity Bridges Common is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity Bridges Common is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity Bridges Common. If not, see <http://www.gnu.org/licenses/>.
use relay_ethereum_client::Error as EthereumNodeError;
use relay_substrate_client::Error as SubstrateNodeError;
use relay_utils::MaybeConnectionError;
use thiserror::Error;
/// Contains common errors that can occur when
/// interacting with a Substrate or Ethereum node
/// through RPC.
#[derive(Debug, Error)]
pub enum RpcError {
/// The arguments to the RPC method failed to serialize.
#[error("RPC arguments serialization failed: {0}")]
Serialization(#[from] serde_json::Error),
/// An error occurred when interacting with an Ethereum node.
#[error("Ethereum node error: {0}")]
Ethereum(#[from] EthereumNodeError),
/// An error occurred when interacting with a Substrate node.
#[error("Substrate node error: {0}")]
Substrate(#[from] SubstrateNodeError),
/// Error running relay loop.
#[error("{0}")]
SyncLoop(String),
}
impl From<RpcError> for String {
fn from(err: RpcError) -> Self {
format!("{}", 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))
}
}
@@ -1,204 +0,0 @@
// Copyright 2019-2021 Parity Technologies (UK) Ltd.
// This file is part of Parity Bridges Common.
// Parity Bridges Common is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity Bridges Common is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity Bridges Common. If not, see <http://www.gnu.org/licenses/>.
//! Rialto-Substrate -> Ethereum PoA synchronization.
use crate::{ethereum_client::EthereumHighLevelRpc, 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::EncodedJustification;
use std::{collections::HashSet, fmt::Debug, 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: 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 = EncodedJustification;
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().await.map_err(Into::into)
}
}
#[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: EncodedJustification,
) -> 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 async 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).await;
let sub_client = 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(),
)
.await
.map_err(|e| RpcError::SyncLoop(e.to_string()))?;
Ok(())
}
@@ -1,78 +0,0 @@
// Copyright 2020-2021 Parity Technologies (UK) Ltd.
// This file is part of Parity Bridges Common.
// Parity Bridges Common is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity Bridges Common is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity Bridges Common. If not, see <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,
},
}
}
@@ -17,6 +17,8 @@
use codec::Decode;
use frame_support::weights::{DispatchClass, DispatchInfo, Pays, Weight};
use relay_kusama_client::Kusama;
use sp_core::storage::StorageKey;
use sp_runtime::{FixedPointNumber, FixedU128};
use sp_version::RuntimeVersion;
use crate::cli::{
@@ -101,3 +103,14 @@ impl CliChain for Kusama {
anyhow::bail!("Sending messages from Kusama is not yet supported.")
}
}
/// Storage key and initial value of Polkadot -> Kusama conversion rate.
pub(crate) fn polkadot_to_kusama_conversion_rate_params() -> (StorageKey, FixedU128) {
(
bp_runtime::storage_parameter_key(
bp_kusama::POLKADOT_TO_KUSAMA_CONVERSION_RATE_PARAMETER_NAME,
),
// starting relay before this parameter will be set to some value may cause troubles
FixedU128::from_inner(FixedU128::DIV),
)
}
@@ -64,10 +64,7 @@ impl SubstrateFinalitySyncPipeline for KusamaFinalityToPolkadot {
type TargetChain = Polkadot;
fn customize_metrics(params: MetricsParams) -> anyhow::Result<MetricsParams> {
crate::chains::add_polkadot_kusama_price_metrics::<Self::FinalitySyncPipeline>(
Some(finality_relay::metrics_prefix::<Self::FinalitySyncPipeline>()),
params,
)
crate::chains::add_polkadot_kusama_price_metrics::<Self::FinalitySyncPipeline>(params)
}
fn start_relay_guards(&self) {
@@ -21,7 +21,6 @@ use std::ops::RangeInclusive;
use codec::Encode;
use frame_support::weights::Weight;
use sp_core::{Bytes, Pair};
use sp_runtime::{FixedPointNumber, FixedU128};
use bp_messages::MessageNonce;
use bridge_runtime_common::messages::target::FromBridgedChainMessagesProof;
@@ -33,7 +32,6 @@ use relay_polkadot_client::{
HeaderId as PolkadotHeaderId, Polkadot, SigningParams as PolkadotSigningParams,
};
use relay_substrate_client::{Chain, Client, TransactionSignScheme, UnsignedTransaction};
use relay_utils::metrics::MetricsParams;
use substrate_relay_helper::{
messages_lane::{
select_delivery_transaction_limits, MessagesRelayParams, StandaloneMessagesMetrics,
@@ -196,12 +194,13 @@ pub async fn run(
let lane_id = params.lane_id;
let source_client = params.source_client;
let target_client = params.target_client;
let lane = KusamaMessagesToPolkadot {
message_lane: SubstrateMessageLaneToSubstrate {
source_client: source_client.clone(),
source_sign: params.source_sign,
source_transactions_mortality: params.source_transactions_mortality,
target_client: params.target_client.clone(),
target_client: target_client.clone(),
target_sign: params.target_sign,
target_transactions_mortality: params.target_transactions_mortality,
relayer_id_at_source: relayer_id_at_kusama,
@@ -240,13 +239,10 @@ pub async fn run(
stall_timeout,
);
let (metrics_params, metrics_values) = add_standalone_metrics(
Some(messages_relay::message_lane_loop::metrics_prefix::<
<KusamaMessagesToPolkadot as SubstrateMessageLane>::MessageLane,
>(&lane_id)),
params.metrics_params,
source_client.clone(),
)?;
let standalone_metrics = params
.standalone_metrics
.map(Ok)
.unwrap_or_else(|| standalone_metrics(source_client.clone(), target_client.clone()))?;
messages_relay::message_lane_loop::run(
messages_relay::message_lane_loop::Params {
lane: lane_id,
@@ -272,41 +268,31 @@ pub async fn run(
params.target_to_source_headers_relay,
),
PolkadotTargetClient::new(
params.target_client,
target_client,
lane,
lane_id,
metrics_values,
standalone_metrics.clone(),
params.source_to_target_headers_relay,
),
metrics_params,
standalone_metrics.register_and_spawn(params.metrics_params)?,
futures::future::pending(),
)
.await
.map_err(Into::into)
}
/// Add standalone metrics for the Kusama -> Polkadot messages loop.
pub(crate) fn add_standalone_metrics(
metrics_prefix: Option<String>,
metrics_params: MetricsParams,
/// Create standalone metrics for the Kusama -> Polkadot messages loop.
pub(crate) fn standalone_metrics(
source_client: Client<Kusama>,
) -> anyhow::Result<(MetricsParams, StandaloneMessagesMetrics)> {
let polkadot_to_kusama_conversion_rate_key = bp_runtime::storage_parameter_key(
bp_kusama::POLKADOT_TO_KUSAMA_CONVERSION_RATE_PARAMETER_NAME,
)
.0;
substrate_relay_helper::messages_lane::add_standalone_metrics::<KusamaMessagesToPolkadot>(
metrics_prefix,
metrics_params,
target_client: Client<Polkadot>,
) -> anyhow::Result<StandaloneMessagesMetrics<Kusama, Polkadot>> {
substrate_relay_helper::messages_lane::standalone_metrics(
source_client,
Some(crate::chains::polkadot::TOKEN_ID),
target_client,
Some(crate::chains::kusama::TOKEN_ID),
Some((
sp_core::storage::StorageKey(polkadot_to_kusama_conversion_rate_key),
// starting relay before this parameter will be set to some value may cause troubles
FixedU128::from_inner(FixedU128::DIV),
)),
Some(crate::chains::polkadot::TOKEN_ID),
Some(crate::chains::polkadot::kusama_to_polkadot_conversion_rate_params()),
Some(crate::chains::kusama::polkadot_to_kusama_conversion_rate_params()),
)
}
@@ -28,8 +28,17 @@ use bp_message_dispatch::{CallOrigin, MessagePayload};
use codec::Decode;
use frame_support::weights::{DispatchInfo, GetDispatchInfo, Weight};
use relay_millau_client::Millau;
use sp_core::storage::StorageKey;
use sp_runtime::FixedU128;
use sp_version::RuntimeVersion;
// Millau/Rialto tokens have no any real value, so the conversion rate we use is always 1:1. But we
// want to test our code that is intended to work with real-value chains. So to keep it close to
// 1:1, we'll be treating Rialto as BTC and Millau as wBTC (only in relayer).
/// The identifier of token, which value is associated with Millau token value by relayer.
pub(crate) const ASSOCIATED_TOKEN_ID: &str = crate::chains::kusama::TOKEN_ID;
impl CliEncodeCall for Millau {
fn max_extrinsic_size() -> u32 {
bp_millau::max_extrinsic_size()
@@ -123,3 +132,11 @@ impl CliChain for Millau {
}
}
}
/// Storage key and initial value of Rialto -> Millau conversion rate.
pub(crate) fn rialto_to_millau_conversion_rate_params() -> (StorageKey, FixedU128) {
(
StorageKey(millau_runtime::rialto_messages::RialtoToMillauConversionRate::key().to_vec()),
millau_runtime::rialto_messages::INITIAL_RIALTO_TO_MILLAU_CONVERSION_RATE,
)
}
@@ -33,7 +33,6 @@ use relay_rialto_client::{
HeaderId as RialtoHeaderId, Rialto, SigningParams as RialtoSigningParams,
};
use relay_substrate_client::{Chain, Client, IndexOf, TransactionSignScheme, UnsignedTransaction};
use relay_utils::metrics::MetricsParams;
use substrate_relay_helper::{
messages_lane::{
select_delivery_transaction_limits, MessagesRelayParams, StandaloneMessagesMetrics,
@@ -193,12 +192,13 @@ pub async fn run(
let lane_id = params.lane_id;
let source_client = params.source_client;
let target_client = params.target_client;
let lane = MillauMessagesToRialto {
message_lane: SubstrateMessageLaneToSubstrate {
source_client: source_client.clone(),
source_sign: params.source_sign,
source_transactions_mortality: params.source_transactions_mortality,
target_client: params.target_client.clone(),
target_client: target_client.clone(),
target_sign: params.target_sign,
target_transactions_mortality: params.target_transactions_mortality,
relayer_id_at_source: relayer_id_at_millau,
@@ -234,13 +234,10 @@ pub async fn run(
stall_timeout,
);
let (metrics_params, metrics_values) = add_standalone_metrics(
Some(messages_relay::message_lane_loop::metrics_prefix::<
<MillauMessagesToRialto as SubstrateMessageLane>::MessageLane,
>(&lane_id)),
params.metrics_params,
source_client.clone(),
)?;
let standalone_metrics = params
.standalone_metrics
.map(Ok)
.unwrap_or_else(|| standalone_metrics(source_client.clone(), target_client.clone()))?;
messages_relay::message_lane_loop::run(
messages_relay::message_lane_loop::Params {
lane: lane_id,
@@ -266,37 +263,31 @@ pub async fn run(
params.target_to_source_headers_relay,
),
RialtoTargetClient::new(
params.target_client,
target_client,
lane,
lane_id,
metrics_values,
standalone_metrics.clone(),
params.source_to_target_headers_relay,
),
metrics_params,
standalone_metrics.register_and_spawn(params.metrics_params)?,
futures::future::pending(),
)
.await
.map_err(Into::into)
}
/// Add standalone metrics for the Millau -> Rialto messages loop.
pub(crate) fn add_standalone_metrics(
metrics_prefix: Option<String>,
metrics_params: MetricsParams,
/// Create standalone metrics for the Millau -> Rialto messages loop.
pub(crate) fn standalone_metrics(
source_client: Client<Millau>,
) -> anyhow::Result<(MetricsParams, StandaloneMessagesMetrics)> {
substrate_relay_helper::messages_lane::add_standalone_metrics::<MillauMessagesToRialto>(
metrics_prefix,
metrics_params,
target_client: Client<Rialto>,
) -> anyhow::Result<StandaloneMessagesMetrics<Millau, Rialto>> {
substrate_relay_helper::messages_lane::standalone_metrics(
source_client,
Some(crate::chains::MILLAU_ASSOCIATED_TOKEN_ID),
Some(crate::chains::RIALTO_ASSOCIATED_TOKEN_ID),
Some((
sp_core::storage::StorageKey(
millau_runtime::rialto_messages::RialtoToMillauConversionRate::key().to_vec(),
),
millau_runtime::rialto_messages::INITIAL_RIALTO_TO_MILLAU_CONVERSION_RATE,
)),
target_client,
Some(crate::chains::millau::ASSOCIATED_TOKEN_ID),
Some(crate::chains::rialto::ASSOCIATED_TOKEN_ID),
Some(crate::chains::rialto::millau_to_rialto_conversion_rate_params()),
Some(crate::chains::millau::rialto_to_millau_conversion_rate_params()),
)
}
@@ -39,31 +39,16 @@ mod rococo;
mod westend;
mod wococo;
// Millau/Rialto tokens have no any real value, so the conversion rate we use is always 1:1. But we
// want to test our code that is intended to work with real-value chains. So to keep it close to
// 1:1, we'll be treating Rialto as BTC and Millau as wBTC (only in relayer).
/// The identifier of token, which value is associated with Rialto token value by relayer.
pub(crate) const RIALTO_ASSOCIATED_TOKEN_ID: &str = polkadot::TOKEN_ID;
/// The identifier of token, which value is associated with Millau token value by relayer.
pub(crate) const MILLAU_ASSOCIATED_TOKEN_ID: &str = kusama::TOKEN_ID;
use relay_utils::metrics::MetricsParams;
use relay_utils::metrics::{MetricsParams, StandaloneMetric};
pub(crate) fn add_polkadot_kusama_price_metrics<T: finality_relay::FinalitySyncPipeline>(
prefix: Option<String>,
params: MetricsParams,
) -> anyhow::Result<MetricsParams> {
// Polkadot/Kusama prices are added as metrics here, because atm we don't have Polkadot <->
// Kusama relays, but we want to test metrics/dashboards in advance
Ok(relay_utils::relay_metrics(prefix, params)
.standalone_metric(|registry, prefix| {
substrate_relay_helper::helpers::token_price_metric(registry, prefix, "polkadot")
})?
.standalone_metric(|registry, prefix| {
substrate_relay_helper::helpers::token_price_metric(registry, prefix, "kusama")
})?
.into_params())
substrate_relay_helper::helpers::token_price_metric(polkadot::TOKEN_ID)?
.register_and_spawn(&params.registry)?;
substrate_relay_helper::helpers::token_price_metric(kusama::TOKEN_ID)?
.register_and_spawn(&params.registry)?;
Ok(params)
}
#[cfg(test)]
@@ -17,6 +17,8 @@
use codec::Decode;
use frame_support::weights::{DispatchClass, DispatchInfo, Pays, Weight};
use relay_polkadot_client::Polkadot;
use sp_core::storage::StorageKey;
use sp_runtime::{FixedPointNumber, FixedU128};
use sp_version::RuntimeVersion;
use crate::cli::{
@@ -101,3 +103,14 @@ impl CliChain for Polkadot {
anyhow::bail!("Sending messages from Polkadot is not yet supported.")
}
}
/// Storage key and initial value of Kusama -> Polkadot conversion rate.
pub(crate) fn kusama_to_polkadot_conversion_rate_params() -> (StorageKey, FixedU128) {
(
bp_runtime::storage_parameter_key(
bp_polkadot::KUSAMA_TO_POLKADOT_CONVERSION_RATE_PARAMETER_NAME,
),
// starting relay before this parameter will be set to some value may cause troubles
FixedU128::from_inner(FixedU128::DIV),
)
}
@@ -64,10 +64,7 @@ impl SubstrateFinalitySyncPipeline for PolkadotFinalityToKusama {
type TargetChain = Kusama;
fn customize_metrics(params: MetricsParams) -> anyhow::Result<MetricsParams> {
crate::chains::add_polkadot_kusama_price_metrics::<Self::FinalitySyncPipeline>(
Some(finality_relay::metrics_prefix::<Self::FinalitySyncPipeline>()),
params,
)
crate::chains::add_polkadot_kusama_price_metrics::<Self::FinalitySyncPipeline>(params)
}
fn start_relay_guards(&self) {
@@ -32,8 +32,6 @@ use relay_polkadot_client::{
HeaderId as PolkadotHeaderId, Polkadot, SigningParams as PolkadotSigningParams,
};
use relay_substrate_client::{Chain, Client, TransactionSignScheme, UnsignedTransaction};
use relay_utils::metrics::MetricsParams;
use sp_runtime::{FixedPointNumber, FixedU128};
use substrate_relay_helper::{
messages_lane::{
select_delivery_transaction_limits, MessagesRelayParams, StandaloneMessagesMetrics,
@@ -195,12 +193,13 @@ pub async fn run(
let lane_id = params.lane_id;
let source_client = params.source_client;
let target_client = params.target_client;
let lane = PolkadotMessagesToKusama {
message_lane: SubstrateMessageLaneToSubstrate {
source_client: source_client.clone(),
source_sign: params.source_sign,
source_transactions_mortality: params.source_transactions_mortality,
target_client: params.target_client.clone(),
target_client: target_client.clone(),
target_sign: params.target_sign,
target_transactions_mortality: params.target_transactions_mortality,
relayer_id_at_source: relayer_id_at_polkadot,
@@ -239,13 +238,10 @@ pub async fn run(
stall_timeout,
);
let (metrics_params, metrics_values) = add_standalone_metrics(
Some(messages_relay::message_lane_loop::metrics_prefix::<
<PolkadotMessagesToKusama as SubstrateMessageLane>::MessageLane,
>(&lane_id)),
params.metrics_params,
source_client.clone(),
)?;
let standalone_metrics = params
.standalone_metrics
.map(Ok)
.unwrap_or_else(|| standalone_metrics(source_client.clone(), target_client.clone()))?;
messages_relay::message_lane_loop::run(
messages_relay::message_lane_loop::Params {
lane: lane_id,
@@ -271,41 +267,31 @@ pub async fn run(
params.target_to_source_headers_relay,
),
KusamaTargetClient::new(
params.target_client,
target_client,
lane,
lane_id,
metrics_values,
standalone_metrics.clone(),
params.source_to_target_headers_relay,
),
metrics_params,
standalone_metrics.register_and_spawn(params.metrics_params)?,
futures::future::pending(),
)
.await
.map_err(Into::into)
}
/// Add standalone metrics for the Polkadot -> Kusama messages loop.
pub(crate) fn add_standalone_metrics(
metrics_prefix: Option<String>,
metrics_params: MetricsParams,
/// Create standalone metrics for the Polkadot -> Kusama messages loop.
pub(crate) fn standalone_metrics(
source_client: Client<Polkadot>,
) -> anyhow::Result<(MetricsParams, StandaloneMessagesMetrics)> {
let kusama_to_polkadot_conversion_rate_key = bp_runtime::storage_parameter_key(
bp_polkadot::KUSAMA_TO_POLKADOT_CONVERSION_RATE_PARAMETER_NAME,
)
.0;
substrate_relay_helper::messages_lane::add_standalone_metrics::<PolkadotMessagesToKusama>(
metrics_prefix,
metrics_params,
target_client: Client<Kusama>,
) -> anyhow::Result<StandaloneMessagesMetrics<Polkadot, Kusama>> {
substrate_relay_helper::messages_lane::standalone_metrics(
source_client,
Some(crate::chains::kusama::TOKEN_ID),
target_client,
Some(crate::chains::polkadot::TOKEN_ID),
Some((
sp_core::storage::StorageKey(kusama_to_polkadot_conversion_rate_key),
// starting relay before this parameter will be set to some value may cause troubles
FixedU128::from_inner(FixedU128::DIV),
)),
Some(crate::chains::kusama::TOKEN_ID),
Some(crate::chains::kusama::polkadot_to_kusama_conversion_rate_params()),
Some(crate::chains::polkadot::kusama_to_polkadot_conversion_rate_params()),
)
}
@@ -28,8 +28,17 @@ use bp_message_dispatch::{CallOrigin, MessagePayload};
use codec::Decode;
use frame_support::weights::{DispatchInfo, GetDispatchInfo, Weight};
use relay_rialto_client::Rialto;
use sp_core::storage::StorageKey;
use sp_runtime::FixedU128;
use sp_version::RuntimeVersion;
// Millau/Rialto tokens have no any real value, so the conversion rate we use is always 1:1. But we
// want to test our code that is intended to work with real-value chains. So to keep it close to
// 1:1, we'll be treating Rialto as BTC and Millau as wBTC (only in relayer).
/// The identifier of token, which value is associated with Rialto token value by relayer.
pub(crate) const ASSOCIATED_TOKEN_ID: &str = crate::chains::polkadot::TOKEN_ID;
impl CliEncodeCall for Rialto {
fn max_extrinsic_size() -> u32 {
bp_rialto::max_extrinsic_size()
@@ -122,3 +131,11 @@ impl CliChain for Rialto {
}
}
}
/// Storage key and initial value of Millau -> Rialto conversion rate.
pub(crate) fn millau_to_rialto_conversion_rate_params() -> (StorageKey, FixedU128) {
(
StorageKey(rialto_runtime::millau_messages::MillauToRialtoConversionRate::key().to_vec()),
rialto_runtime::millau_messages::INITIAL_MILLAU_TO_RIALTO_CONVERSION_RATE,
)
}
@@ -33,7 +33,6 @@ use relay_rialto_client::{
HeaderId as RialtoHeaderId, Rialto, SigningParams as RialtoSigningParams,
};
use relay_substrate_client::{Chain, Client, IndexOf, TransactionSignScheme, UnsignedTransaction};
use relay_utils::metrics::MetricsParams;
use substrate_relay_helper::{
messages_lane::{
select_delivery_transaction_limits, MessagesRelayParams, StandaloneMessagesMetrics,
@@ -193,12 +192,13 @@ pub async fn run(
let lane_id = params.lane_id;
let source_client = params.source_client;
let target_client = params.target_client;
let lane = RialtoMessagesToMillau {
message_lane: SubstrateMessageLaneToSubstrate {
source_client: source_client.clone(),
source_sign: params.source_sign,
source_transactions_mortality: params.source_transactions_mortality,
target_client: params.target_client.clone(),
target_client: target_client.clone(),
target_sign: params.target_sign,
target_transactions_mortality: params.target_transactions_mortality,
relayer_id_at_source: relayer_id_at_rialto,
@@ -233,13 +233,10 @@ pub async fn run(
stall_timeout,
);
let (metrics_params, metrics_values) = add_standalone_metrics(
Some(messages_relay::message_lane_loop::metrics_prefix::<
<RialtoMessagesToMillau as SubstrateMessageLane>::MessageLane,
>(&lane_id)),
params.metrics_params,
source_client.clone(),
)?;
let standalone_metrics = params
.standalone_metrics
.map(Ok)
.unwrap_or_else(|| standalone_metrics(source_client.clone(), target_client.clone()))?;
messages_relay::message_lane_loop::run(
messages_relay::message_lane_loop::Params {
lane: lane_id,
@@ -265,37 +262,31 @@ pub async fn run(
params.target_to_source_headers_relay,
),
MillauTargetClient::new(
params.target_client,
target_client,
lane,
lane_id,
metrics_values,
standalone_metrics.clone(),
params.source_to_target_headers_relay,
),
metrics_params,
standalone_metrics.register_and_spawn(params.metrics_params)?,
futures::future::pending(),
)
.await
.map_err(Into::into)
}
/// Add standalone metrics for the Rialto -> Millau messages loop.
pub(crate) fn add_standalone_metrics(
metrics_prefix: Option<String>,
metrics_params: MetricsParams,
/// Create standalone metrics for the Rialto -> Millau messages loop.
pub(crate) fn standalone_metrics(
source_client: Client<Rialto>,
) -> anyhow::Result<(MetricsParams, StandaloneMessagesMetrics)> {
substrate_relay_helper::messages_lane::add_standalone_metrics::<RialtoMessagesToMillau>(
metrics_prefix,
metrics_params,
target_client: Client<Millau>,
) -> anyhow::Result<StandaloneMessagesMetrics<Rialto, Millau>> {
substrate_relay_helper::messages_lane::standalone_metrics(
source_client,
Some(crate::chains::RIALTO_ASSOCIATED_TOKEN_ID),
Some(crate::chains::MILLAU_ASSOCIATED_TOKEN_ID),
Some((
sp_core::storage::StorageKey(
rialto_runtime::millau_messages::MillauToRialtoConversionRate::key().to_vec(),
),
rialto_runtime::millau_messages::INITIAL_MILLAU_TO_RIALTO_CONVERSION_RATE,
)),
target_client,
Some(crate::chains::rialto::ASSOCIATED_TOKEN_ID),
Some(crate::chains::millau::ASSOCIATED_TOKEN_ID),
Some(crate::chains::millau::rialto_to_millau_conversion_rate_params()),
Some(crate::chains::rialto::millau_to_rialto_conversion_rate_params()),
)
}
@@ -59,10 +59,7 @@ impl SubstrateFinalitySyncPipeline for RococoFinalityToWococo {
type TargetChain = Wococo;
fn customize_metrics(params: MetricsParams) -> anyhow::Result<MetricsParams> {
crate::chains::add_polkadot_kusama_price_metrics::<Self::FinalitySyncPipeline>(
Some(finality_relay::metrics_prefix::<Self::FinalitySyncPipeline>()),
params,
)
crate::chains::add_polkadot_kusama_price_metrics::<Self::FinalitySyncPipeline>(params)
}
fn start_relay_guards(&self) {
@@ -29,7 +29,6 @@ use relay_rococo_client::{
HeaderId as RococoHeaderId, Rococo, SigningParams as RococoSigningParams,
};
use relay_substrate_client::{Chain, Client, IndexOf, TransactionSignScheme, UnsignedTransaction};
use relay_utils::metrics::MetricsParams;
use relay_wococo_client::{
HeaderId as WococoHeaderId, SigningParams as WococoSigningParams, Wococo,
};
@@ -193,12 +192,13 @@ pub async fn run(
let lane_id = params.lane_id;
let source_client = params.source_client;
let target_client = params.target_client;
let lane = RococoMessagesToWococo {
message_lane: SubstrateMessageLaneToSubstrate {
source_client: source_client.clone(),
source_sign: params.source_sign,
source_transactions_mortality: params.source_transactions_mortality,
target_client: params.target_client.clone(),
target_client: target_client.clone(),
target_sign: params.target_sign,
target_transactions_mortality: params.target_transactions_mortality,
relayer_id_at_source: relayer_id_at_rococo,
@@ -237,13 +237,10 @@ pub async fn run(
stall_timeout,
);
let (metrics_params, metrics_values) = add_standalone_metrics(
Some(messages_relay::message_lane_loop::metrics_prefix::<
<RococoMessagesToWococo as SubstrateMessageLane>::MessageLane,
>(&lane_id)),
params.metrics_params,
source_client.clone(),
)?;
let standalone_metrics = params
.standalone_metrics
.map(Ok)
.unwrap_or_else(|| standalone_metrics(source_client.clone(), target_client.clone()))?;
messages_relay::message_lane_loop::run(
messages_relay::message_lane_loop::Params {
lane: lane_id,
@@ -269,29 +266,28 @@ pub async fn run(
params.target_to_source_headers_relay,
),
WococoTargetClient::new(
params.target_client,
target_client,
lane,
lane_id,
metrics_values,
standalone_metrics.clone(),
params.source_to_target_headers_relay,
),
metrics_params,
standalone_metrics.register_and_spawn(params.metrics_params)?,
futures::future::pending(),
)
.await
.map_err(Into::into)
}
/// Add standalone metrics for the Rococo -> Wococo messages loop.
pub(crate) fn add_standalone_metrics(
metrics_prefix: Option<String>,
metrics_params: MetricsParams,
/// Create standalone metrics for the Rococo -> Wococo messages loop.
pub(crate) fn standalone_metrics(
source_client: Client<Rococo>,
) -> anyhow::Result<(MetricsParams, StandaloneMessagesMetrics)> {
substrate_relay_helper::messages_lane::add_standalone_metrics::<RococoMessagesToWococo>(
metrics_prefix,
metrics_params,
target_client: Client<Wococo>,
) -> anyhow::Result<StandaloneMessagesMetrics<Rococo, Wococo>> {
substrate_relay_helper::messages_lane::standalone_metrics(
source_client,
target_client,
None,
None,
None,
None,
@@ -57,10 +57,7 @@ impl SubstrateFinalitySyncPipeline for WestendFinalityToMillau {
type TargetChain = Millau;
fn customize_metrics(params: MetricsParams) -> anyhow::Result<MetricsParams> {
crate::chains::add_polkadot_kusama_price_metrics::<Self::FinalitySyncPipeline>(
Some(finality_relay::metrics_prefix::<Self::FinalitySyncPipeline>()),
params,
)
crate::chains::add_polkadot_kusama_price_metrics::<Self::FinalitySyncPipeline>(params)
}
fn transactions_author(&self) -> bp_millau::AccountId {
@@ -64,10 +64,7 @@ impl SubstrateFinalitySyncPipeline for WococoFinalityToRococo {
type TargetChain = Rococo;
fn customize_metrics(params: MetricsParams) -> anyhow::Result<MetricsParams> {
crate::chains::add_polkadot_kusama_price_metrics::<Self::FinalitySyncPipeline>(
Some(finality_relay::metrics_prefix::<Self::FinalitySyncPipeline>()),
params,
)
crate::chains::add_polkadot_kusama_price_metrics::<Self::FinalitySyncPipeline>(params)
}
fn start_relay_guards(&self) {
@@ -29,7 +29,6 @@ use relay_rococo_client::{
HeaderId as RococoHeaderId, Rococo, SigningParams as RococoSigningParams,
};
use relay_substrate_client::{Chain, Client, IndexOf, TransactionSignScheme, UnsignedTransaction};
use relay_utils::metrics::MetricsParams;
use relay_wococo_client::{
HeaderId as WococoHeaderId, SigningParams as WococoSigningParams, Wococo,
};
@@ -192,12 +191,13 @@ pub async fn run(
let lane_id = params.lane_id;
let source_client = params.source_client;
let target_client = params.target_client;
let lane = WococoMessagesToRococo {
message_lane: SubstrateMessageLaneToSubstrate {
source_client: source_client.clone(),
source_sign: params.source_sign,
source_transactions_mortality: params.source_transactions_mortality,
target_client: params.target_client.clone(),
target_client: target_client.clone(),
target_sign: params.target_sign,
target_transactions_mortality: params.target_transactions_mortality,
relayer_id_at_source: relayer_id_at_wococo,
@@ -236,13 +236,10 @@ pub async fn run(
stall_timeout,
);
let (metrics_params, metrics_values) = add_standalone_metrics(
Some(messages_relay::message_lane_loop::metrics_prefix::<
<WococoMessagesToRococo as SubstrateMessageLane>::MessageLane,
>(&lane_id)),
params.metrics_params,
source_client.clone(),
)?;
let standalone_metrics = params
.standalone_metrics
.map(Ok)
.unwrap_or_else(|| standalone_metrics(source_client.clone(), target_client.clone()))?;
messages_relay::message_lane_loop::run(
messages_relay::message_lane_loop::Params {
lane: lane_id,
@@ -268,29 +265,28 @@ pub async fn run(
params.target_to_source_headers_relay,
),
RococoTargetClient::new(
params.target_client,
target_client,
lane,
lane_id,
metrics_values,
standalone_metrics.clone(),
params.source_to_target_headers_relay,
),
metrics_params,
standalone_metrics.register_and_spawn(params.metrics_params)?,
futures::future::pending(),
)
.await
.map_err(Into::into)
}
/// Add standalone metrics for the Wococo -> Rococo messages loop.
pub(crate) fn add_standalone_metrics(
metrics_prefix: Option<String>,
metrics_params: MetricsParams,
/// Create standalone metrics for the Wococo -> Rococo messages loop.
pub(crate) fn standalone_metrics(
source_client: Client<Wococo>,
) -> anyhow::Result<(MetricsParams, StandaloneMessagesMetrics)> {
substrate_relay_helper::messages_lane::add_standalone_metrics::<WococoMessagesToRococo>(
metrics_prefix,
metrics_params,
target_client: Client<Rococo>,
) -> anyhow::Result<StandaloneMessagesMetrics<Wococo, Rococo>> {
substrate_relay_helper::messages_lane::standalone_metrics(
source_client,
target_client,
None,
None,
None,
None,
@@ -345,7 +345,7 @@ mod tests {
// then
assert!(format!("{:?}", call_hex).starts_with(
"0x10030000000001000000381409000000000001d43593c715fdd31c61141abd04a99fd6822c8558854cc\
"0x0f030000000001000000381409000000000001d43593c715fdd31c61141abd04a99fd6822c8558854cc\
de39a5684e7a56da27d01d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d01"
))
}
@@ -17,6 +17,7 @@
use structopt::StructOpt;
use strum::{EnumString, EnumVariantNames, VariantNames};
use relay_utils::metrics::{GlobalMetrics, StandaloneMetric};
use substrate_relay_helper::finality_pipeline::SubstrateFinalitySyncPipeline;
use crate::cli::{
@@ -121,6 +122,8 @@ impl RelayHeaders {
let target_transactions_mortality = self.target_sign.target_transactions_mortality;
let target_sign = self.target_sign.to_keypair::<Target>()?;
let metrics_params = Finality::customize_metrics(self.prometheus_params.into())?;
GlobalMetrics::new()?.register_and_spawn(&metrics_params.registry)?;
let finality = Finality::new(target_client.clone(), target_sign);
finality.start_relay_guards();
@@ -34,8 +34,7 @@ use relay_substrate_client::{
use relay_utils::metrics::MetricsParams;
use sp_core::{Bytes, Pair};
use substrate_relay_helper::{
messages_lane::{MessagesRelayParams, SubstrateMessageLane},
on_demand_headers::OnDemandHeadersRelay,
messages_lane::MessagesRelayParams, on_demand_headers::OnDemandHeadersRelay,
};
use crate::{
@@ -129,11 +128,6 @@ macro_rules! select_bridge {
type RightToLeftFinality =
crate::chains::rialto_headers_to_millau::RialtoFinalityToMillau;
type LeftToRightMessages =
crate::chains::millau_messages_to_rialto::MillauMessagesToRialto;
type RightToLeftMessages =
crate::chains::rialto_messages_to_millau::RialtoMessagesToMillau;
type LeftAccountIdConverter = bp_millau::AccountIdConverter;
type RightAccountIdConverter = bp_rialto::AccountIdConverter;
@@ -144,12 +138,11 @@ macro_rules! select_bridge {
use crate::chains::{
millau_messages_to_rialto::{
add_standalone_metrics as add_left_to_right_standalone_metrics,
standalone_metrics as left_to_right_standalone_metrics,
run as left_to_right_messages,
update_rialto_to_millau_conversion_rate as update_right_to_left_conversion_rate,
},
rialto_messages_to_millau::{
add_standalone_metrics as add_right_to_left_standalone_metrics,
run as right_to_left_messages,
update_millau_to_rialto_conversion_rate as update_left_to_right_conversion_rate,
},
@@ -184,11 +177,6 @@ macro_rules! select_bridge {
type RightToLeftFinality =
crate::chains::wococo_headers_to_rococo::WococoFinalityToRococo;
type LeftToRightMessages =
crate::chains::rococo_messages_to_wococo::RococoMessagesToWococo;
type RightToLeftMessages =
crate::chains::wococo_messages_to_rococo::WococoMessagesToRococo;
type LeftAccountIdConverter = bp_rococo::AccountIdConverter;
type RightAccountIdConverter = bp_wococo::AccountIdConverter;
@@ -199,11 +187,10 @@ macro_rules! select_bridge {
use crate::chains::{
rococo_messages_to_wococo::{
add_standalone_metrics as add_left_to_right_standalone_metrics,
standalone_metrics as left_to_right_standalone_metrics,
run as left_to_right_messages,
},
wococo_messages_to_rococo::{
add_standalone_metrics as add_right_to_left_standalone_metrics,
run as right_to_left_messages,
},
};
@@ -253,11 +240,6 @@ macro_rules! select_bridge {
type RightToLeftFinality =
crate::chains::polkadot_headers_to_kusama::PolkadotFinalityToKusama;
type LeftToRightMessages =
crate::chains::kusama_messages_to_polkadot::KusamaMessagesToPolkadot;
type RightToLeftMessages =
crate::chains::polkadot_messages_to_kusama::PolkadotMessagesToKusama;
type LeftAccountIdConverter = bp_kusama::AccountIdConverter;
type RightAccountIdConverter = bp_polkadot::AccountIdConverter;
@@ -268,12 +250,11 @@ macro_rules! select_bridge {
use crate::chains::{
kusama_messages_to_polkadot::{
add_standalone_metrics as add_left_to_right_standalone_metrics,
standalone_metrics as left_to_right_standalone_metrics,
run as left_to_right_messages,
update_polkadot_to_kusama_conversion_rate as update_right_to_left_conversion_rate,
},
polkadot_messages_to_kusama::{
add_standalone_metrics as add_right_to_left_standalone_metrics,
run as right_to_left_messages,
update_kusama_to_polkadot_conversion_rate as update_left_to_right_conversion_rate,
},
@@ -378,31 +359,39 @@ impl RelayHeadersAndMessages {
let relayer_mode = params.shared.relayer_mode.into();
let relay_strategy = MixStrategy::new(relayer_mode);
const METRIC_IS_SOME_PROOF: &str =
"it is `None` when metric has been already registered; \
this is the command entrypoint, so nothing has been registered yet; \
qed";
// create metrics registry and register standalone metrics
let metrics_params: MetricsParams = params.shared.prometheus_params.into();
let metrics_params = relay_utils::relay_metrics(None, metrics_params).into_params();
let (metrics_params, left_to_right_metrics) =
add_left_to_right_standalone_metrics(None, metrics_params, left_client.clone())?;
let (metrics_params, right_to_left_metrics) =
add_right_to_left_standalone_metrics(None, metrics_params, right_client.clone())?;
let metrics_params = relay_utils::relay_metrics(metrics_params).into_params();
let left_to_right_metrics =
left_to_right_standalone_metrics(left_client.clone(), right_client.clone())?;
let right_to_left_metrics = left_to_right_metrics.clone().reverse();
// start conversion rate update loops for left/right chains
if let Some(left_messages_pallet_owner) = left_messages_pallet_owner {
let left_client = left_client.clone();
let format_err = || {
anyhow::format_err!(
"Cannon run conversion rate updater: {} -> {}",
Right::NAME,
Left::NAME
)
};
substrate_relay_helper::conversion_rate_update::run_conversion_rate_update_loop(
left_to_right_metrics
.target_to_source_conversion_rate
.expect(METRIC_IS_SOME_PROOF),
.as_ref()
.ok_or_else(format_err)?
.shared_value_ref(),
left_to_right_metrics
.target_to_base_conversion_rate
.clone()
.expect(METRIC_IS_SOME_PROOF),
.as_ref()
.ok_or_else(format_err)?
.shared_value_ref(),
left_to_right_metrics
.source_to_base_conversion_rate
.clone()
.expect(METRIC_IS_SOME_PROOF),
.as_ref()
.ok_or_else(format_err)?
.shared_value_ref(),
CONVERSION_RATE_ALLOWED_DIFFERENCE_RATIO,
move |new_rate| {
log::info!(
@@ -423,16 +412,29 @@ impl RelayHeadersAndMessages {
}
if let Some(right_messages_pallet_owner) = right_messages_pallet_owner {
let right_client = right_client.clone();
let format_err = || {
anyhow::format_err!(
"Cannon run conversion rate updater: {} -> {}",
Left::NAME,
Right::NAME
)
};
substrate_relay_helper::conversion_rate_update::run_conversion_rate_update_loop(
right_to_left_metrics
.target_to_source_conversion_rate
.expect(METRIC_IS_SOME_PROOF),
.as_ref()
.ok_or_else(format_err)?
.shared_value_ref(),
left_to_right_metrics
.source_to_base_conversion_rate
.expect(METRIC_IS_SOME_PROOF),
.as_ref()
.ok_or_else(format_err)?
.shared_value_ref(),
left_to_right_metrics
.target_to_base_conversion_rate
.expect(METRIC_IS_SOME_PROOF),
.as_ref()
.ok_or_else(format_err)?
.shared_value_ref(),
CONVERSION_RATE_ALLOWED_DIFFERENCE_RATIO,
move |new_rate| {
log::info!(
@@ -452,6 +454,7 @@ impl RelayHeadersAndMessages {
);
}
// optionally, create relayers fund account
if params.shared.create_relayers_fund_accounts {
let relayer_fund_acount_id = pallet_bridge_messages::relayer_fund_account_id::<
AccountIdOf<Left>,
@@ -490,6 +493,7 @@ impl RelayHeadersAndMessages {
}
}
// start on-demand header relays
let left_to_right_on_demand_headers = OnDemandHeadersRelay::new(
left_client.clone(),
right_client.clone(),
@@ -521,11 +525,8 @@ impl RelayHeadersAndMessages {
source_to_target_headers_relay: Some(left_to_right_on_demand_headers.clone()),
target_to_source_headers_relay: Some(right_to_left_on_demand_headers.clone()),
lane_id: lane,
metrics_params: metrics_params.clone().disable().metrics_prefix(
messages_relay::message_lane_loop::metrics_prefix::<
<LeftToRightMessages as SubstrateMessageLane>::MessageLane,
>(&lane),
),
metrics_params: metrics_params.clone().disable(),
standalone_metrics: Some(left_to_right_metrics.clone()),
relay_strategy: relay_strategy.clone(),
})
.map_err(|e| anyhow::format_err!("{}", e))
@@ -540,11 +541,8 @@ impl RelayHeadersAndMessages {
source_to_target_headers_relay: Some(right_to_left_on_demand_headers.clone()),
target_to_source_headers_relay: Some(left_to_right_on_demand_headers.clone()),
lane_id: lane,
metrics_params: metrics_params.clone().disable().metrics_prefix(
messages_relay::message_lane_loop::metrics_prefix::<
<RightToLeftMessages as SubstrateMessageLane>::MessageLane,
>(&lane),
),
metrics_params: metrics_params.clone().disable(),
standalone_metrics: Some(right_to_left_metrics.clone()),
relay_strategy: relay_strategy.clone(),
})
.map_err(|e| anyhow::format_err!("{}", e))
@@ -554,7 +552,7 @@ impl RelayHeadersAndMessages {
message_relays.push(right_to_left_messages);
}
relay_utils::relay_metrics(None, metrics_params)
relay_utils::relay_metrics(metrics_params)
.expose()
.await
.map_err(|e| anyhow::format_err!("{}", e))?;
@@ -95,6 +95,7 @@ impl RelayMessages {
target_to_source_headers_relay: None,
lane_id: self.lane.into(),
metrics_params: self.prometheus_params.into(),
standalone_metrics: None,
relay_strategy,
})
.await
@@ -401,7 +401,8 @@ impl SwapTokens {
.await?;
if token_swap_state != None {
return Err(anyhow::format_err!(
"Confirmed token swap state has been changed to {:?} unexpectedly"
"Confirmed token swap state has been changed to {:?} unexpectedly",
token_swap_state
))
}
} else {
@@ -1,20 +0,0 @@
[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]
async-std = "1.6.5"
bp-eth-poa = { path = "../../primitives/ethereum-poa" }
headers-relay = { path = "../headers" }
hex-literal = "0.3"
jsonrpsee-proc-macros = "0.3.1"
jsonrpsee-ws-client = "0.3.1"
libsecp256k1 = { version = "0.7", default-features = false, features = ["hmac"] }
log = "0.4.11"
relay-utils = { path = "../utils" }
tokio = "1.8"
web3 = { git = "https://github.com/svyatonik/rust-web3.git", branch = "bump-deps" }
thiserror = "1.0.26"
@@ -1,265 +0,0 @@
// Copyright 2019-2021 Parity Technologies (UK) Ltd.
// This file is part of Parity Bridges Common.
// Parity Bridges Common is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity Bridges Common is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity Bridges Common. If not, see <http://www.gnu.org/licenses/>.
use crate::{
rpc::Ethereum,
types::{
Address, Bytes, CallRequest, Header, HeaderWithTransactions, Receipt, SignedRawTx,
SyncState, Transaction, TransactionHash, H256, U256,
},
ConnectionParams, Error, Result,
};
use jsonrpsee_ws_client::{WsClient as RpcClient, WsClientBuilder as RpcClientBuilder};
use relay_utils::relay_loop::RECONNECT_DELAY;
use std::{future::Future, sync::Arc};
/// 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 {
tokio: Arc<tokio::runtime::Runtime>,
params: ConnectionParams,
client: Arc<RpcClient>,
}
impl Client {
/// Create a new Ethereum RPC Client.
///
/// This function will keep connecting to given Ethereum node until connection is established
/// and is functional. If attempt fail, it will wait for `RECONNECT_DELAY` and retry again.
pub async fn new(params: ConnectionParams) -> Self {
loop {
match Self::try_connect(params.clone()).await {
Ok(client) => return client,
Err(error) => log::error!(
target: "bridge",
"Failed to connect to Ethereum node: {:?}. Going to retry in {}s",
error,
RECONNECT_DELAY.as_secs(),
),
}
async_std::task::sleep(RECONNECT_DELAY).await;
}
}
/// Try to connect to Ethereum node. Returns Ethereum RPC client if connection has been
/// established or error otherwise.
pub async fn try_connect(params: ConnectionParams) -> Result<Self> {
let (tokio, client) = Self::build_client(&params).await?;
Ok(Self { tokio, client, params })
}
/// Build client to use in connection.
async fn build_client(
params: &ConnectionParams,
) -> Result<(Arc<tokio::runtime::Runtime>, Arc<RpcClient>)> {
let tokio = tokio::runtime::Runtime::new()?;
let uri = format!("ws://{}:{}", params.host, params.port);
let client = tokio
.spawn(async move { RpcClientBuilder::default().build(&uri).await })
.await??;
Ok((Arc::new(tokio), Arc::new(client)))
}
/// Reopen client connection.
pub async fn reconnect(&mut self) -> Result<()> {
let (tokio, client) = Self::build_client(&self.params).await?;
self.tokio = tokio;
self.client = client;
Ok(())
}
}
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<()> {
self.jsonrpsee_execute(move |client| async move {
match Ethereum::syncing(&*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(())
},
}
})
.await
}
/// Estimate gas usage for the given call.
pub async fn estimate_gas(&self, call_request: CallRequest) -> Result<U256> {
self.jsonrpsee_execute(move |client| async move {
Ok(Ethereum::estimate_gas(&*client, call_request).await?)
})
.await
}
/// Retrieve number of the best known block from the Ethereum node.
pub async fn best_block_number(&self) -> Result<u64> {
self.jsonrpsee_execute(move |client| async move {
Ok(Ethereum::block_number(&*client).await?.as_u64())
})
.await
}
/// Retrieve number of the best known block from the Ethereum node.
pub async fn header_by_number(&self, block_number: u64) -> Result<Header> {
self.jsonrpsee_execute(move |client| async move {
let get_full_tx_objects = false;
let header =
Ethereum::get_block_by_number(&*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),
}
})
.await
}
/// Retrieve block header by its hash from Ethereum node.
pub async fn header_by_hash(&self, hash: H256) -> Result<Header> {
self.jsonrpsee_execute(move |client| async move {
let get_full_tx_objects = false;
let header = Ethereum::get_block_by_hash(&*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),
}
})
.await
}
/// 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> {
self.jsonrpsee_execute(move |client| async move {
let get_full_tx_objects = true;
let header = Ethereum::get_block_by_number_with_transactions(
&*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)
})
.await
}
/// 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> {
self.jsonrpsee_execute(move |client| async move {
let get_full_tx_objects = true;
let header =
Ethereum::get_block_by_hash_with_transactions(&*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)
})
.await
}
/// Retrieve transaction by its hash from Ethereum node.
pub async fn transaction_by_hash(&self, hash: H256) -> Result<Option<Transaction>> {
self.jsonrpsee_execute(move |client| async move {
Ok(Ethereum::transaction_by_hash(&*client, hash).await?)
})
.await
}
/// Retrieve transaction receipt by transaction hash.
pub async fn transaction_receipt(&self, transaction_hash: H256) -> Result<Receipt> {
self.jsonrpsee_execute(move |client| async move {
Ok(Ethereum::get_transaction_receipt(&*client, transaction_hash).await?)
})
.await
}
/// Get the nonce of the given account.
pub async fn account_nonce(&self, address: Address) -> Result<U256> {
self.jsonrpsee_execute(move |client| async move {
Ok(Ethereum::get_transaction_count(&*client, address).await?)
})
.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> {
self.jsonrpsee_execute(move |client| async move {
let transaction = Bytes(signed_raw_tx);
let tx_hash = Ethereum::submit_transaction(&*client, transaction).await?;
log::trace!(target: "bridge", "Sent transaction to Ethereum node: {:?}", tx_hash);
Ok(tx_hash)
})
.await
}
/// Call Ethereum smart contract.
pub async fn eth_call(&self, call_transaction: CallRequest) -> Result<Bytes> {
self.jsonrpsee_execute(move |client| async move {
Ok(Ethereum::call(&*client, call_transaction).await?)
})
.await
}
/// Execute jsonrpsee future in tokio context.
async fn jsonrpsee_execute<MF, F, T>(&self, make_jsonrpsee_future: MF) -> Result<T>
where
MF: FnOnce(Arc<RpcClient>) -> F + Send + 'static,
F: Future<Output = Result<T>> + Send,
T: Send + 'static,
{
let client = self.client.clone();
self.tokio.spawn(async move { make_jsonrpsee_future(client).await }).await?
}
}
@@ -1,82 +0,0 @@
// Copyright 2019-2021 Parity Technologies (UK) Ltd.
// This file is part of Parity Bridges Common.
// Parity Bridges Common is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity Bridges Common is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity Bridges Common. If not, see <http://www.gnu.org/licenses/>.
//! Ethereum node RPC errors.
use crate::types::U256;
use jsonrpsee_ws_client::types::Error as RpcError;
use relay_utils::MaybeConnectionError;
use thiserror::Error;
/// 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, Error)]
pub enum Error {
/// IO error.
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
/// An error that can occur when making an HTTP request to
/// an JSON-RPC client.
#[error("RPC error: {0}")]
RpcError(#[from] RpcError),
/// Failed to parse response.
#[error("Response parse failed: {0}")]
ResponseParseFailed(String),
/// We have received a header with missing fields.
#[error("Incomplete Ethereum Header Received (missing some of required fields - hash, number, logs_bloom).")]
IncompleteHeader,
/// We have received a transaction missing a `raw` field.
#[error("Incomplete Ethereum Transaction (missing required field - raw).")]
IncompleteTransaction,
/// An invalid Substrate block number was received from
/// an Ethereum node.
#[error("Received an invalid Substrate block from Ethereum Node.")]
InvalidSubstrateBlockNumber,
/// An invalid index has been received from an Ethereum node.
#[error("Received an invalid incomplete index from 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.
#[error("Ethereum client is not synced: syncing {0} headers.")]
ClientNotSynced(U256),
/// Custom logic error.
#[error("{0}")]
Custom(String),
}
impl From<tokio::task::JoinError> for Error {
fn from(error: tokio::task::JoinError) -> Self {
Error::Custom(format!("Failed to wait tokio task: {}", error))
}
}
impl MaybeConnectionError for Error {
fn is_connection_error(&self) -> bool {
matches!(
*self,
Error::RpcError(RpcError::Transport(_))
// right now if connection to the ws server is dropped (after it is already established),
// we're getting this error
| Error::RpcError(RpcError::Internal(_))
| Error::RpcError(RpcError::RestartNeeded(_))
| Error::ClientNotSynced(_),
)
}
}
@@ -1,47 +0,0 @@
// Copyright 2019-2021 Parity Technologies (UK) Ltd.
// This file is part of Parity Bridges Common.
// Parity Bridges Common is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity Bridges Common is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity Bridges Common. If not, see <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,
error::{Error, Result},
sign::{sign_and_submit_transaction, SigningParams},
};
pub mod types;
/// Ethereum-over-websocket connection params.
#[derive(Debug, Clone)]
pub struct ConnectionParams {
/// Websocket server host name.
pub host: String,
/// Websocket server TCP port.
pub port: u16,
}
impl Default for ConnectionParams {
fn default() -> Self {
ConnectionParams { host: "localhost".into(), port: 8546 }
}
}
@@ -1,51 +0,0 @@
// Copyright 2019-2021 Parity Technologies (UK) Ltd.
// This file is part of Parity Bridges Common.
// Parity Bridges Common is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity Bridges Common is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity Bridges Common. If not, see <http://www.gnu.org/licenses/>.
//! Ethereum node RPC interface.
use crate::types::{
Address, Bytes, CallRequest, Header, HeaderWithTransactions, Receipt, SyncState, Transaction,
TransactionHash, H256, U256, U64,
};
jsonrpsee_proc_macros::rpc_client_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;
}
}
@@ -1,87 +0,0 @@
// Copyright 2019-2021 Parity Technologies (UK) Ltd.
// This file is part of Parity Bridges Common.
// Parity Bridges Common is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity Bridges Common is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity Bridges Common. If not, see <http://www.gnu.org/licenses/>.
use crate::{
types::{Address, CallRequest, U256},
Client, Result,
};
use bp_eth_poa::signatures::{secret_to_address, SignTransaction};
use hex_literal::hex;
use libsecp256k1::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 transaction 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(&params.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(&params.signer, Some(params.chain_id));
let _ = client.submit_transaction(raw_transaction).await?;
Ok(())
}
@@ -1,80 +0,0 @@
// Copyright 2019-2021 Parity Technologies (UK) Ltd.
// This file is part of Parity Bridges Common.
// Parity Bridges Common is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity Bridges Common is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity Bridges Common. If not, see <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)
}
}
@@ -22,7 +22,6 @@ thiserror = "1.0.26"
bp-header-chain = { path = "../../primitives/header-chain" }
bp-runtime = { path = "../../primitives/runtime" }
finality-relay = { path = "../finality" }
headers-relay = { path = "../headers" }
relay-utils = { path = "../utils" }
# Substrate Dependencies
@@ -1,108 +0,0 @@
// Copyright 2019-2021 Parity Technologies (UK) Ltd.
// This file is part of Parity Bridges Common.
// Parity Bridges Common is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity Bridges Common is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity Bridges Common. If not, see <http://www.gnu.org/licenses/>.
//! Default generic implementation of headers source for basic Substrate client.
use crate::{
chain::{BlockWithJustification, Chain},
client::Client,
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, EncodedJustification};
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 = EncodedJustification,
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, ()))
}
}
@@ -26,7 +26,6 @@ mod sync_header;
pub mod finality_source;
pub mod guard;
pub mod headers_source;
pub mod metrics;
use std::time::Duration;
@@ -20,7 +20,8 @@ use async_std::sync::{Arc, RwLock};
use async_trait::async_trait;
use codec::Decode;
use relay_utils::metrics::{
metric_name, register, F64SharedRef, Gauge, PrometheusError, Registry, StandaloneMetrics, F64,
metric_name, register, F64SharedRef, Gauge, Metric, PrometheusError, Registry,
StandaloneMetric, F64,
};
use sp_core::storage::StorageKey;
use sp_runtime::{traits::UniqueSaturatedInto, FixedPointNumber};
@@ -42,8 +43,6 @@ pub struct FloatStorageValueMetric<C: Chain, T: Clone> {
impl<C: Chain, T: Decode + FixedPointNumber> FloatStorageValueMetric<C, T> {
/// Create new metric.
pub fn new(
registry: &Registry,
prefix: Option<&str>,
client: Client<C>,
storage_key: StorageKey,
maybe_default_value: Option<T>,
@@ -55,7 +54,7 @@ impl<C: Chain, T: Decode + FixedPointNumber> FloatStorageValueMetric<C, T> {
client,
storage_key,
maybe_default_value,
metric: register(Gauge::new(metric_name(prefix, &name), help)?, registry)?,
metric: Gauge::new(metric_name(None, &name), help)?,
shared_value_ref,
})
}
@@ -66,8 +65,17 @@ impl<C: Chain, T: Decode + FixedPointNumber> FloatStorageValueMetric<C, T> {
}
}
impl<C: Chain, T> Metric for FloatStorageValueMetric<C, T>
where
T: 'static + Decode + Send + Sync + FixedPointNumber,
{
fn register(&self, registry: &Registry) -> Result<(), PrometheusError> {
register(self.metric.clone(), registry).map(drop)
}
}
#[async_trait]
impl<C: Chain, T> StandaloneMetrics for FloatStorageValueMetric<C, T>
impl<C: Chain, T> StandaloneMetric for FloatStorageValueMetric<C, T>
where
T: 'static + Decode + Send + Sync + FixedPointNumber,
{
@@ -18,7 +18,7 @@ use crate::{chain::Chain, client::Client, error::Error};
use async_trait::async_trait;
use relay_utils::metrics::{
metric_name, register, Gauge, PrometheusError, Registry, StandaloneMetrics, U64,
metric_name, register, Gauge, Metric, PrometheusError, Registry, StandaloneMetric, U64,
};
use sp_core::storage::StorageKey;
use sp_runtime::traits::Header as HeaderT;
@@ -46,16 +46,10 @@ impl<C: Chain> Clone for StorageProofOverheadMetric<C> {
impl<C: Chain> StorageProofOverheadMetric<C> {
/// Create new metric instance with given name and help.
pub fn new(
registry: &Registry,
prefix: Option<&str>,
client: Client<C>,
name: String,
help: String,
) -> Result<Self, PrometheusError> {
pub fn new(client: Client<C>, name: String, help: String) -> Result<Self, PrometheusError> {
Ok(StorageProofOverheadMetric {
client,
metric: register(Gauge::new(metric_name(prefix, &name), help)?, registry)?,
metric: Gauge::new(metric_name(None, &name), help)?,
})
}
@@ -84,8 +78,14 @@ impl<C: Chain> StorageProofOverheadMetric<C> {
}
}
impl<C: Chain> Metric for StorageProofOverheadMetric<C> {
fn register(&self, registry: &Registry) -> Result<(), PrometheusError> {
register(self.metric.clone(), registry).map(drop)
}
}
#[async_trait]
impl<C: Chain> StandaloneMetrics for StorageProofOverheadMetric<C> {
impl<C: Chain> StandaloneMetric for StorageProofOverheadMetric<C> {
fn update_interval(&self) -> Duration {
C::AVERAGE_BLOCK_INTERVAL * UPDATE_INTERVAL_IN_BLOCKS
}
@@ -16,13 +16,10 @@
use bp_header_chain::find_grandpa_authorities_scheduled_change;
use finality_relay::SourceHeader as FinalitySourceHeader;
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.
/// implements `finality_relay::SourceHeader` and may be used in headers sync directly.
#[derive(Clone, Debug, PartialEq)]
pub struct SyncHeader<Header>(Header);
@@ -47,21 +44,6 @@ impl<Header> From<Header> for SyncHeader<Header> {
}
}
impl<Header: HeaderT> SourceHeader<Header::Hash, Header::Number> for SyncHeader<Header> {
fn id(&self) -> HeaderId<Header::Hash, Header::Number> {
relay_utils::HeaderId(*self.0.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(),
)
}
}
impl<Header: HeaderT> FinalitySourceHeader<Header::Number> for SyncHeader<Header> {
fn number(&self) -> Header::Number {
*self.0.number()
@@ -1,18 +0,0 @@
[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]
anyhow = "1.0"
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" }
thiserror = "1.0.26"
@@ -1,66 +0,0 @@
// Copyright 2019-2021 Parity Technologies (UK) Ltd.
// This file is part of Parity Bridges Common.
// Parity Bridges Common is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity Bridges Common is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity Bridges Common. If not, see <http://www.gnu.org/licenses/>.
//! Exchange-relay errors.
use crate::exchange::{BlockHashOf, BlockNumberOf, TransactionHashOf};
use relay_utils::MaybeConnectionError;
use std::fmt::{Debug, Display};
use thiserror::Error;
/// Error type given pipeline.
pub type ErrorOf<P> = Error<BlockHashOf<P>, BlockNumberOf<P>, TransactionHashOf<P>>;
/// Exchange-relay error type.
#[derive(Error, Debug)]
pub enum Error<Hash: Display, HeaderNumber: Display, SourceTxHash: Display> {
/// Failed to check finality of the requested header on the target node.
#[error("Failed to check finality of header {0}/{1} on {2} node: {3:?}")]
Finality(HeaderNumber, Hash, &'static str, anyhow::Error),
/// Error retrieving block from the source node.
#[error("Error retrieving block {0} from {1} node: {2:?}")]
RetrievingBlock(Hash, &'static str, anyhow::Error),
/// Error retrieving transaction from the source node.
#[error("Error retrieving transaction {0} from {1} node: {2:?}")]
RetrievingTransaction(SourceTxHash, &'static str, anyhow::Error),
/// Failed to check existence of header from the target node.
#[error("Failed to check existence of header {0}/{1} on {2} node: {3:?}")]
CheckHeaderExistence(HeaderNumber, Hash, &'static str, anyhow::Error),
/// Failed to prepare proof for the transaction from the source node.
#[error("Error building transaction {0} proof on {1} node: {2:?}")]
BuildTransactionProof(String, &'static str, anyhow::Error, bool),
/// Failed to submit the transaction proof to the target node.
#[error("Error submitting transaction {0} proof to {1} node: {2:?}")]
SubmitTransactionProof(String, &'static str, anyhow::Error, bool),
/// Transaction filtering failed.
#[error("Transaction filtering has failed with {0:?}")]
TransactionFiltering(anyhow::Error, bool),
/// Utilities/metrics error.
#[error("{0}")]
Utils(#[from] relay_utils::Error),
}
impl<T: Display, U: Display, V: Display> MaybeConnectionError for Error<T, U, V> {
fn is_connection_error(&self) -> bool {
match *self {
Self::BuildTransactionProof(_, _, _, b) => b,
Self::SubmitTransactionProof(_, _, _, b) => b,
Self::TransactionFiltering(_, b) => b,
_ => false,
}
}
}
@@ -1,904 +0,0 @@
// Copyright 2019-2021 Parity Technologies (UK) Ltd.
// This file is part of Parity Bridges Common.
// Parity Bridges Common is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity Bridges Common is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity Bridges Common. If not, see <http://www.gnu.org/licenses/>.
//! Relaying proofs of exchange transaction.
use crate::error::{Error, ErrorOf};
use anyhow::anyhow;
use async_trait::async_trait;
use relay_utils::{relay_loop::Client as RelayClient, FailedClient, MaybeConnectionError};
use std::{
fmt::{Debug, Display},
string::ToString,
};
/// Transaction proof pipeline.
pub trait TransactionProofPipeline: 'static {
/// 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: 'static + Send + Sync;
}
/// Block that is participating in exchange.
pub trait SourceBlock: 'static + Send + Sync {
/// Block hash type.
type Hash: 'static + Clone + Send + Sync + Debug + Display;
/// Block number type.
type Number: 'static
+ Debug
+ Display
+ Clone
+ Copy
+ Send
+ Sync
+ 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: 'static + Send {
/// Transaction hash type.
type Hash: Debug + Display + Clone;
/// 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>) -> std::result::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,
Error::TransactionFiltering(
anyhow!("{:?}", err),
err.is_connection_error(),
),
)
})?;
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<(), ErrorOf<P>> {
// 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| {
Error::RetrievingBlock(source_header_id.1.clone(), P::SOURCE_NAME, anyhow!("{:?}", 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?,
)
.await
.map_err(Into::into)
}
/// 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, ErrorOf<P>> {
source_client
.transaction_proof(source_block, source_tx_index)
.await
.map_err(|err| {
Error::BuildTransactionProof(
source_tx_id.to_owned(),
P::SOURCE_NAME,
anyhow!("{:?}", err),
err.is_connection_error(),
)
})
}
/// 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<(), ErrorOf<P>> {
target_client.submit_transaction_proof(source_tx_proof).await.map_err(|err| {
Error::SubmitTransactionProof(
source_tx_id.to_owned(),
P::TARGET_NAME,
anyhow!("{:?}", err),
err.is_connection_error(),
)
})
}
/// 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), ErrorOf<P>> {
loop {
let source_header_and_tx =
source_client.transaction_block(source_tx_hash).await.map_err(|err| {
Error::RetrievingTransaction(
source_tx_hash.clone(),
P::SOURCE_NAME,
anyhow!("{:?}", 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<(), ErrorOf<P>> {
loop {
let is_header_known =
target_client.is_header_known(source_header_id).await.map_err(|err| {
Error::CheckHeaderExistence(
source_header_id.0,
source_header_id.1.clone(),
P::TARGET_NAME,
anyhow!("{:?}", 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<(), ErrorOf<P>> {
loop {
let is_header_finalized =
target_client.is_header_finalized(source_header_id).await.map_err(|err| {
Error::Finality(
source_header_id.0,
source_header_id.1.clone(),
P::TARGET_NAME,
anyhow!("{:?}", 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!(async_std::task::block_on(relay_single_transaction_proof(
source,
target,
test_transaction_hash(0)
))
.is_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))
],
);
}
}
@@ -1,322 +0,0 @@
// Copyright 2019-2021 Parity Technologies (UK) Ltd.
// This file is part of Parity Bridges Common.
// Parity Bridges Common is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity Bridges Common is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity Bridges Common. If not, see <http://www.gnu.org/licenses/>.
//! Relaying proofs of exchange transactions.
use crate::{
error::Error,
exchange::{
relay_block_transactions, BlockNumberOf, RelayedBlockTransactions, SourceClient,
TargetClient, TransactionProofPipeline,
},
exchange_loop_metrics::ExchangeLoopMetrics,
};
use crate::error::ErrorOf;
use backoff::backoff::Backoff;
use futures::{future::FutureExt, select};
use num_traits::One;
use relay_utils::{
metrics::{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: 'static + Clone + Send + Sync {
/// Associated block number.
type BlockNumber: 'static + Send + Sync;
/// 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: 'static + Clone + Copy + Send + Sync> 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;
}
}
/// Return prefix that will be used by default to expose Prometheus metrics of the exchange loop.
pub fn metrics_prefix<P: TransactionProofPipeline>() -> String {
format!("{}_to_{}_Exchange", P::SOURCE_NAME, P::TARGET_NAME)
}
/// Run proofs synchronization.
pub async fn run<P: TransactionProofPipeline>(
storage: impl TransactionProofsRelayStorage<BlockNumber = BlockNumberOf<P>>,
source_client: impl SourceClient<P>,
target_client: impl TargetClient<P>,
metrics_params: MetricsParams,
exit_signal: impl Future<Output = ()> + 'static + Send,
) -> Result<(), ErrorOf<P>> {
let exit_signal = exit_signal.shared();
relay_utils::relay_loop(source_client, target_client)
.with_metrics(Some(metrics_prefix::<P>()), metrics_params)
.loop_metric(ExchangeLoopMetrics::new)?
.standalone_metric(GlobalMetrics::new)?
.expose()
.await?
.run(metrics_prefix::<P>(), move |source_client, target_client, metrics| {
run_until_connection_lost(
storage.clone(),
source_client,
target_client,
metrics,
exit_signal.clone(),
)
})
.await
.map_err(Error::Utils)
}
/// 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_exch: Option<ExchangeLoopMetrics>,
exit_signal: impl Future<Output = ()> + Send,
) -> 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 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(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));
},
_ => (),
}
}));
let _ = async_std::task::block_on(run(
storage,
source,
target,
MetricsParams::disabled(),
exit_receiver.into_future().map(|(_, _)| ()),
));
}
}
@@ -1,97 +0,0 @@
// Copyright 2019-2021 Parity Technologies (UK) Ltd.
// This file is part of Parity Bridges Common.
// Parity Bridges Common is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity Bridges Common is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity Bridges Common. If not, see <http://www.gnu.org/licenses/>.
//! Metrics for currency-exchange relay loop.
use crate::exchange::{BlockNumberOf, RelayedBlockTransactions, TransactionProofPipeline};
use relay_utils::metrics::{
metric_name, register, Counter, CounterVec, GaugeVec, Opts, PrometheusError, 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 ExchangeLoopMetrics {
/// Create and register exchange loop metrics.
pub fn new(registry: &Registry, prefix: Option<&str>) -> Result<Self, PrometheusError> {
Ok(ExchangeLoopMetrics {
best_block_numbers: register(
GaugeVec::new(
Opts::new(
metric_name(prefix, "best_block_numbers"),
"Best finalized block numbers",
),
&["type"],
)?,
registry,
)?,
processed_blocks: register(
Counter::new(
metric_name(prefix, "processed_blocks"),
"Total number of processed blocks",
)?,
registry,
)?,
processed_transactions: register(
CounterVec::new(
Opts::new(
metric_name(prefix, "processed_transactions"),
"Total number of processed transactions",
),
&["type"],
)?,
registry,
)?,
})
}
}
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 _);
}
}
@@ -1,27 +0,0 @@
// Copyright 2019-2021 Parity Technologies (UK) Ltd.
// This file is part of Parity Bridges Common.
// Parity Bridges Common is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity Bridges Common is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity Bridges Common. If not, see <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 error;
pub mod exchange;
pub mod exchange_loop;
pub mod exchange_loop_metrics;
@@ -12,7 +12,6 @@ async-trait = "0.1.40"
backoff = "0.2"
bp-header-chain = { path = "../../primitives/header-chain" }
futures = "0.3.5"
headers-relay = { path = "../headers" }
log = "0.4.11"
num-traits = "0.2"
relay-utils = { path = "../utils" }
@@ -19,17 +19,17 @@
//! is the mandatory headers, which we always submit to the target node. For such headers, we
//! assume that the persistent proof either exists, or will eventually become available.
use crate::{FinalityProof, FinalitySyncPipeline, SourceHeader};
use crate::{
sync_loop_metrics::SyncLoopMetrics, FinalityProof, FinalitySyncPipeline, SourceHeader,
};
use async_trait::async_trait;
use backoff::backoff::Backoff;
use futures::{select, Future, FutureExt, Stream, StreamExt};
use headers_relay::sync_loop_metrics::SyncLoopMetrics;
use num_traits::{One, Saturating};
use relay_utils::{
metrics::{GlobalMetrics, MetricsParams},
relay_loop::Client as RelayClient,
retry_backoff, FailedClient, MaybeConnectionError,
metrics::MetricsParams, relay_loop::Client as RelayClient, retry_backoff, FailedClient,
MaybeConnectionError,
};
use std::{
pin::Pin,
@@ -113,9 +113,8 @@ pub async fn run<P: FinalitySyncPipeline>(
) -> Result<(), relay_utils::Error> {
let exit_signal = exit_signal.shared();
relay_utils::relay_loop(source_client, target_client)
.with_metrics(Some(metrics_prefix::<P>()), metrics_params)
.loop_metric(SyncLoopMetrics::new)?
.standalone_metric(GlobalMetrics::new)?
.with_metrics(metrics_params)
.loop_metric(SyncLoopMetrics::new(Some(&metrics_prefix::<P>()))?)?
.expose()
.await?
.run(metrics_prefix::<P>(), move |source_client, target_client, metrics| {
@@ -28,6 +28,7 @@ use std::fmt::Debug;
mod finality_loop;
mod finality_loop_tests;
mod sync_loop_metrics;
/// Finality proofs synchronization pipeline.
pub trait FinalitySyncPipeline: 'static + Clone + Debug + Send + Sync {
@@ -0,0 +1,64 @@
// Copyright 2019-2021 Parity Technologies (UK) Ltd.
// This file is part of Parity Bridges Common.
// Parity Bridges Common is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity Bridges Common is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity Bridges Common. If not, see <http://www.gnu.org/licenses/>.
//! Metrics for headers synchronization relay loop.
use relay_utils::metrics::{
metric_name, register, GaugeVec, Metric, Opts, PrometheusError, Registry, U64,
};
/// Headers sync metrics.
#[derive(Clone)]
pub struct SyncLoopMetrics {
/// Best syncing headers at "source" and "target" nodes.
best_block_numbers: GaugeVec<U64>,
}
impl SyncLoopMetrics {
/// Create and register headers loop metrics.
pub fn new(prefix: Option<&str>) -> Result<Self, PrometheusError> {
Ok(SyncLoopMetrics {
best_block_numbers: GaugeVec::new(
Opts::new(
metric_name(prefix, "best_block_numbers"),
"Best block numbers on source and target nodes",
),
&["node"],
)?,
})
}
/// Update best block number at source.
pub fn update_best_block_at_source<Number: Into<u64>>(&self, source_best_number: Number) {
self.best_block_numbers
.with_label_values(&["source"])
.set(source_best_number.into());
}
/// Update best block number at target.
pub fn update_best_block_at_target<Number: Into<u64>>(&self, target_best_number: Number) {
self.best_block_numbers
.with_label_values(&["target"])
.set(target_best_number.into());
}
}
impl Metric for SyncLoopMetrics {
fn register(&self, registry: &Registry) -> Result<(), PrometheusError> {
register(self.best_block_numbers.clone(), registry)?;
Ok(())
}
}
@@ -1,17 +0,0 @@
[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
@@ -1,33 +0,0 @@
// Copyright 2019-2021 Parity Technologies (UK) Ltd.
// This file is part of Parity Bridges Common.
// Parity Bridges Common is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity Bridges Common is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity Bridges Common. If not, see <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. Pallet/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;
-529
View File
@@ -1,529 +0,0 @@
// Copyright 2019-2021 Parity Technologies (UK) Ltd.
// This file is part of Parity Bridges Common.
// Parity Bridges Common is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity Bridges Common is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity Bridges Common. If not, see <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,
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,
/// The best header number known to source node.
source_best_number: Option<P::Number>,
/// The 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
}
/// The 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 reorganization
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 reorganizations 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())
}
/// Select orphan header to download.
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},
sync_loop_tests::{TestHash, TestHeadersSyncPipeline, TestNumber},
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)]));
}
}
@@ -1,654 +0,0 @@
// Copyright 2019-2021 Parity Technologies (UK) Ltd.
// This file is part of Parity Bridges Common.
// Parity Bridges Common is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity Bridges Common is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity Bridges Common. If not, see <http://www.gnu.org/licenses/>.
//! Entrypoint for running headers synchronization loop.
use crate::{
sync::{HeadersSync, HeadersSyncParams},
sync_loop_metrics::SyncLoopMetrics,
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::{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 reorganization in target chain.
/// This reorganization could invalidate our knowledge about sync process (i.e. we have asked if
/// HeaderA is known to target, but then reorganization 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) reorganization 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 the 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>: 'static + 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 () {}
/// Return prefix that will be used by default to expose Prometheus metrics of the finality proofs
/// sync loop.
pub fn metrics_prefix<P: HeadersSyncPipeline>() -> String {
format!("{}_to_{}_Sync", P::SOURCE_NAME, P::TARGET_NAME)
}
/// Run headers synchronization.
#[allow(clippy::too_many_arguments)]
pub async 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: MetricsParams,
exit_signal: impl Future<Output = ()> + 'static + Send,
) -> Result<(), relay_utils::Error> {
let exit_signal = exit_signal.shared();
relay_utils::relay_loop(source_client, target_client)
.with_metrics(Some(metrics_prefix::<P>()), metrics_params)
.loop_metric(SyncLoopMetrics::new)?
.standalone_metric(GlobalMetrics::new)?
.expose()
.await?
.run(metrics_prefix::<P>(), move |source_client, target_client, metrics| {
run_until_connection_lost(
source_client,
source_tick,
target_client,
target_tick,
sync_maintain.clone(),
sync_params.clone(),
metrics,
exit_signal.clone(),
)
})
.await
}
/// 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_sync: Option<SyncLoopMetrics>,
exit_signal: impl Future<Output = ()> + Send,
) -> 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_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).map(|id| id.0), *now_target_header)
}
@@ -1,111 +0,0 @@
// Copyright 2019-2021 Parity Technologies (UK) Ltd.
// This file is part of Parity Bridges Common.
// Parity Bridges Common is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity Bridges Common is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity Bridges Common. If not, see <http://www.gnu.org/licenses/>.
//! Metrics for headers synchronization relay loop.
use crate::{
sync::HeadersSync,
sync_types::{HeaderStatus, HeadersSyncPipeline},
};
use num_traits::Zero;
use relay_utils::metrics::{metric_name, register, GaugeVec, Opts, PrometheusError, 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 SyncLoopMetrics {
/// Create and register headers loop metrics.
pub fn new(registry: &Registry, prefix: Option<&str>) -> Result<Self, PrometheusError> {
Ok(SyncLoopMetrics {
best_block_numbers: register(
GaugeVec::new(
Opts::new(
metric_name(prefix, "best_block_numbers"),
"Best block numbers on source and target nodes",
),
&["node"],
)?,
registry,
)?,
blocks_in_state: register(
GaugeVec::new(
Opts::new(
metric_name(prefix, "blocks_in_state"),
"Number of blocks in given state",
),
&["state"],
)?,
registry,
)?,
})
}
}
impl SyncLoopMetrics {
/// Update best block number at source.
pub fn update_best_block_at_source<Number: Into<u64>>(&self, source_best_number: Number) {
self.best_block_numbers
.with_label_values(&["source"])
.set(source_best_number.into());
}
/// Update best block number at target.
pub fn update_best_block_at_target<Number: Into<u64>>(&self, target_best_number: Number) {
self.best_block_numbers
.with_label_values(&["target"])
.set(target_best_number.into());
}
/// 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.update_best_block_at_source(source_best_number);
self.update_best_block_at_target(target_best_number);
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 _);
}
}
@@ -1,602 +0,0 @@
// Copyright 2019-2021 Parity Technologies (UK) Ltd.
// This file is part of Parity Bridges Common.
// Parity Bridges Common is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity Bridges Common is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity Bridges Common. If not, see <http://www.gnu.org/licenses/>.
#![cfg(test)]
use crate::{
sync_loop::{run, SourceClient, TargetClient},
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::{
metrics::MetricsParams, 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;
let _ = async_std::task::block_on(run(
source,
test_tick(),
target,
test_tick(),
(),
crate::sync::tests::default_sync_params(),
MetricsParams::disabled(),
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),
});
}
@@ -1,193 +0,0 @@
// Copyright 2019-2021 Parity Technologies (UK) Ltd.
// This file is part of Parity Bridges Common.
// Parity Bridges Common is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity Bridges Common is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity Bridges Common. If not, see <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: 'static + 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 are either rejected by
/// target node immediately, or their descendants of incomplete headers).
pub rejected: Vec<Id>,
/// Fatal target node error, if it has occurred 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)
}
}
@@ -16,17 +16,11 @@
//! Substrate relay helpers
use relay_utils::metrics::{FloatJsonValueMetric, PrometheusError, Registry};
use relay_utils::metrics::{FloatJsonValueMetric, PrometheusError};
/// Creates standalone token price metric.
pub fn token_price_metric(
registry: &Registry,
prefix: Option<&str>,
token_id: &str,
) -> Result<FloatJsonValueMetric, PrometheusError> {
pub fn token_price_metric(token_id: &str) -> Result<FloatJsonValueMetric, PrometheusError> {
FloatJsonValueMetric::new(
registry,
prefix,
format!("https://api.coingecko.com/api/v3/simple/price?ids={}&vs_currencies=btc", token_id),
format!("$.{}.btc", token_id),
format!("{}_to_base_conversion_rate", token_id.replace("-", "_")),
@@ -34,7 +34,9 @@ use relay_substrate_client::{
BlockNumberOf, Chain, Client, HashOf,
};
use relay_utils::{
metrics::{F64SharedRef, MetricsParams},
metrics::{
FloatJsonValueMetric, GlobalMetrics, MetricsParams, PrometheusError, StandaloneMetric,
},
BlockNumberBase,
};
use sp_core::{storage::StorageKey, Bytes};
@@ -63,6 +65,8 @@ pub struct MessagesRelayParams<SC: Chain, SS, TC: Chain, TS, Strategy: RelayStra
pub lane_id: LaneId,
/// Metrics parameters.
pub metrics_params: MetricsParams,
/// Pre-registered standalone metrics.
pub standalone_metrics: Option<StandaloneMessagesMetrics<SC, TC>>,
/// Relay strategy
pub relay_strategy: Strategy,
}
@@ -241,110 +245,155 @@ pub fn select_delivery_transaction_limits<W: pallet_bridge_messages::WeightInfoE
(max_number_of_messages, weight_for_messages_dispatch)
}
/// Shared references to the values of standalone metrics of the message lane relay loop.
/// Shared references to the standalone metrics of the message lane relay loop.
#[derive(Debug, Clone)]
pub struct StandaloneMessagesMetrics {
/// Shared reference to the actual target -> <base> chain token conversion rate.
pub target_to_base_conversion_rate: Option<F64SharedRef>,
/// Shared reference to the actual source -> <base> chain token conversion rate.
pub source_to_base_conversion_rate: Option<F64SharedRef>,
/// Shared reference to the stored (in the source chain runtime storage) target -> source chain
/// conversion rate.
pub target_to_source_conversion_rate: Option<F64SharedRef>,
pub struct StandaloneMessagesMetrics<SC: Chain, TC: Chain> {
/// Global metrics.
pub global: GlobalMetrics,
/// Storage chain proof overhead metric.
pub source_storage_proof_overhead: StorageProofOverheadMetric<SC>,
/// Target chain proof overhead metric.
pub target_storage_proof_overhead: StorageProofOverheadMetric<TC>,
/// Source tokens to base conversion rate metric.
pub source_to_base_conversion_rate: Option<FloatJsonValueMetric>,
/// Target tokens to base conversion rate metric.
pub target_to_base_conversion_rate: Option<FloatJsonValueMetric>,
/// Source tokens to target tokens conversion rate metric. This rate is stored by the target
/// chain.
pub source_to_target_conversion_rate:
Option<FloatStorageValueMetric<TC, sp_runtime::FixedU128>>,
/// Target tokens to source tokens conversion rate metric. This rate is stored by the source
/// chain.
pub target_to_source_conversion_rate:
Option<FloatStorageValueMetric<SC, sp_runtime::FixedU128>>,
}
impl StandaloneMessagesMetrics {
impl<SC: Chain, TC: Chain> StandaloneMessagesMetrics<SC, TC> {
/// Swap source and target sides.
pub fn reverse(self) -> StandaloneMessagesMetrics<TC, SC> {
StandaloneMessagesMetrics {
global: self.global,
source_storage_proof_overhead: self.target_storage_proof_overhead,
target_storage_proof_overhead: self.source_storage_proof_overhead,
source_to_base_conversion_rate: self.target_to_base_conversion_rate,
target_to_base_conversion_rate: self.source_to_base_conversion_rate,
source_to_target_conversion_rate: self.target_to_source_conversion_rate,
target_to_source_conversion_rate: self.source_to_target_conversion_rate,
}
}
/// Register all metrics in the registry.
pub fn register_and_spawn(
self,
metrics: MetricsParams,
) -> Result<MetricsParams, PrometheusError> {
self.global.register_and_spawn(&metrics.registry)?;
self.source_storage_proof_overhead.register_and_spawn(&metrics.registry)?;
self.target_storage_proof_overhead.register_and_spawn(&metrics.registry)?;
if let Some(m) = self.source_to_base_conversion_rate {
m.register_and_spawn(&metrics.registry)?;
}
if let Some(m) = self.target_to_base_conversion_rate {
m.register_and_spawn(&metrics.registry)?;
}
if let Some(m) = self.target_to_source_conversion_rate {
m.register_and_spawn(&metrics.registry)?;
}
Ok(metrics)
}
/// Return conversion rate from target to source tokens.
pub async fn target_to_source_conversion_rate(&self) -> Option<f64> {
let target_to_base_conversion_rate =
(*self.target_to_base_conversion_rate.as_ref()?.read().await)?;
let source_to_base_conversion_rate =
(*self.source_to_base_conversion_rate.as_ref()?.read().await)?;
Some(source_to_base_conversion_rate / target_to_base_conversion_rate)
Self::compute_target_to_source_conversion_rate(
*self.target_to_base_conversion_rate.as_ref()?.shared_value_ref().read().await,
*self.source_to_base_conversion_rate.as_ref()?.shared_value_ref().read().await,
)
}
/// Return conversion rate from target to source tokens, given conversion rates from
/// target/source tokens to some base token.
fn compute_target_to_source_conversion_rate(
target_to_base_conversion_rate: Option<f64>,
source_to_base_conversion_rate: Option<f64>,
) -> Option<f64> {
Some(source_to_base_conversion_rate? / target_to_base_conversion_rate?)
}
}
/// Add general standalone metrics for the message lane relay loop.
pub fn add_standalone_metrics<P: SubstrateMessageLane>(
metrics_prefix: Option<String>,
metrics_params: MetricsParams,
source_client: Client<P::SourceChain>,
/// Create standalone metrics for the message lane relay loop.
///
/// All metrics returned by this function are exposed by loops that are serving given lane (`P`)
/// and by loops that are serving reverse lane (`P` with swapped `TargetChain` and `SourceChain`).
pub fn standalone_metrics<SC: Chain, TC: Chain>(
source_client: Client<SC>,
target_client: Client<TC>,
source_chain_token_id: Option<&str>,
target_chain_token_id: Option<&str>,
source_to_target_conversion_rate_params: Option<(StorageKey, FixedU128)>,
target_to_source_conversion_rate_params: Option<(StorageKey, FixedU128)>,
) -> anyhow::Result<(MetricsParams, StandaloneMessagesMetrics)> {
let mut target_to_source_conversion_rate = None;
let mut source_to_base_conversion_rate = None;
let mut target_to_base_conversion_rate = None;
let mut metrics_params = relay_utils::relay_metrics(metrics_prefix, metrics_params)
.standalone_metric(|registry, prefix| {
StorageProofOverheadMetric::new(
registry,
prefix,
source_client.clone(),
format!("{}_storage_proof_overhead", P::SourceChain::NAME.to_lowercase()),
format!("{} storage proof overhead", P::SourceChain::NAME),
)
})?;
if let Some((
target_to_source_conversion_rate_storage_key,
initial_target_to_source_conversion_rate,
)) = target_to_source_conversion_rate_params
{
metrics_params = metrics_params.standalone_metric(|registry, prefix| {
let metric = FloatStorageValueMetric::<_, sp_runtime::FixedU128>::new(
registry,
prefix,
source_client,
target_to_source_conversion_rate_storage_key,
Some(initial_target_to_source_conversion_rate),
format!(
"{}_{}_to_{}_conversion_rate",
P::SourceChain::NAME,
P::TargetChain::NAME,
P::SourceChain::NAME
),
format!(
"{} to {} tokens conversion rate (used by {})",
P::TargetChain::NAME,
P::SourceChain::NAME,
P::SourceChain::NAME
),
)?;
target_to_source_conversion_rate = Some(metric.shared_value_ref());
Ok(metric)
})?;
}
if let Some(source_chain_token_id) = source_chain_token_id {
metrics_params = metrics_params.standalone_metric(|registry, prefix| {
let metric =
crate::helpers::token_price_metric(registry, prefix, source_chain_token_id)?;
source_to_base_conversion_rate = Some(metric.shared_value_ref());
Ok(metric)
})?;
}
if let Some(target_chain_token_id) = target_chain_token_id {
metrics_params = metrics_params.standalone_metric(|registry, prefix| {
let metric =
crate::helpers::token_price_metric(registry, prefix, target_chain_token_id)?;
target_to_base_conversion_rate = Some(metric.shared_value_ref());
Ok(metric)
})?;
}
Ok((
metrics_params.into_params(),
StandaloneMessagesMetrics {
target_to_base_conversion_rate,
source_to_base_conversion_rate,
target_to_source_conversion_rate,
},
))
) -> anyhow::Result<StandaloneMessagesMetrics<SC, TC>> {
Ok(StandaloneMessagesMetrics {
global: GlobalMetrics::new()?,
source_storage_proof_overhead: StorageProofOverheadMetric::new(
source_client.clone(),
format!("{}_storage_proof_overhead", SC::NAME.to_lowercase()),
format!("{} storage proof overhead", SC::NAME),
)?,
target_storage_proof_overhead: StorageProofOverheadMetric::new(
target_client.clone(),
format!("{}_storage_proof_overhead", TC::NAME.to_lowercase()),
format!("{} storage proof overhead", TC::NAME),
)?,
source_to_base_conversion_rate: source_chain_token_id
.map(|source_chain_token_id| {
crate::helpers::token_price_metric(source_chain_token_id).map(Some)
})
.unwrap_or(Ok(None))?,
target_to_base_conversion_rate: target_chain_token_id
.map(|target_chain_token_id| {
crate::helpers::token_price_metric(target_chain_token_id).map(Some)
})
.unwrap_or(Ok(None))?,
source_to_target_conversion_rate: source_to_target_conversion_rate_params
.map(|(key, rate)| {
FloatStorageValueMetric::<_, sp_runtime::FixedU128>::new(
target_client,
key,
Some(rate),
format!("{}_{}_to_{}_conversion_rate", TC::NAME, SC::NAME, TC::NAME),
format!(
"{} to {} tokens conversion rate (used by {})",
SC::NAME,
TC::NAME,
TC::NAME
),
)
.map(Some)
})
.unwrap_or(Ok(None))?,
target_to_source_conversion_rate: target_to_source_conversion_rate_params
.map(|(key, rate)| {
FloatStorageValueMetric::<_, sp_runtime::FixedU128>::new(
source_client,
key,
Some(rate),
format!("{}_{}_to_{}_conversion_rate", SC::NAME, TC::NAME, SC::NAME),
format!(
"{} to {} tokens conversion rate (used by {})",
TC::NAME,
SC::NAME,
SC::NAME
),
)
.map(Some)
})
.unwrap_or(Ok(None))?,
})
}
#[cfg(test)]
mod tests {
use super::*;
use async_std::sync::{Arc, RwLock};
type RialtoToMillauMessagesWeights =
pallet_bridge_messages::weights::RialtoWeight<rialto_runtime::Runtime>;
@@ -369,12 +418,9 @@ mod tests {
#[async_std::test]
async fn target_to_source_conversion_rate_works() {
let metrics = StandaloneMessagesMetrics {
target_to_base_conversion_rate: Some(Arc::new(RwLock::new(Some(183.15)))),
source_to_base_conversion_rate: Some(Arc::new(RwLock::new(Some(12.32)))),
target_to_source_conversion_rate: None, // we don't care
};
assert_eq!(metrics.target_to_source_conversion_rate().await, Some(12.32 / 183.15),);
assert_eq!(
StandaloneMessagesMetrics::<relay_rococo_client::Rococo, relay_wococo_client::Wococo>::compute_target_to_source_conversion_rate(Some(183.15), Some(12.32)),
Some(12.32 / 183.15),
);
}
}
@@ -55,7 +55,7 @@ pub struct SubstrateMessagesTarget<P: SubstrateMessageLane> {
client: Client<P::TargetChain>,
lane: P,
lane_id: LaneId,
metric_values: StandaloneMessagesMetrics,
metric_values: StandaloneMessagesMetrics<P::SourceChain, P::TargetChain>,
source_to_target_headers_relay: Option<OnDemandHeadersRelay<P::SourceChain>>,
}
@@ -65,7 +65,7 @@ impl<P: SubstrateMessageLane> SubstrateMessagesTarget<P> {
client: Client<P::TargetChain>,
lane: P,
lane_id: LaneId,
metric_values: StandaloneMessagesMetrics,
metric_values: StandaloneMessagesMetrics<P::SourceChain, P::TargetChain>,
source_to_target_headers_relay: Option<OnDemandHeadersRelay<P::SourceChain>>,
) -> Self {
SubstrateMessagesTarget {
@@ -19,3 +19,5 @@ parking_lot = "0.11.0"
bp-messages = { path = "../../primitives/messages" }
bp-runtime = { path = "../../primitives/runtime" }
relay-utils = { path = "../utils" }
sp-arithmetic = { git = "https://github.com/paritytech/substrate", branch = "master" }
@@ -21,6 +21,7 @@
use num_traits::{SaturatingAdd, Zero};
use relay_utils::{BlockNumberBase, HeaderId};
use sp_arithmetic::traits::AtLeast32BitUnsigned;
use std::{fmt::Debug, ops::Sub};
/// One-way message lane.
@@ -40,7 +41,8 @@ pub trait MessageLane: 'static + Clone + Send + Sync {
/// 1) pay transaction fees;
/// 2) pay message delivery and dispatch fee;
/// 3) pay relayer rewards.
type SourceChainBalance: Clone
type SourceChainBalance: AtLeast32BitUnsigned
+ Clone
+ Copy
+ Debug
+ PartialOrd
@@ -32,10 +32,7 @@ use futures::{channel::mpsc::unbounded, future::FutureExt, stream::StreamExt};
use bp_messages::{LaneId, MessageNonce, UnrewardedRelayersState, Weight};
use bp_runtime::messages::DispatchFeePayment;
use relay_utils::{
interval,
metrics::{GlobalMetrics, MetricsParams},
process_future_result,
relay_loop::Client as RelayClient,
interval, metrics::MetricsParams, process_future_result, relay_loop::Client as RelayClient,
retry_backoff, FailedClient,
};
@@ -270,9 +267,8 @@ pub async fn run<P: MessageLane, Strategy: RelayStrategy>(
let exit_signal = exit_signal.shared();
relay_utils::relay_loop(source_client, target_client)
.reconnect_delay(params.reconnect_delay)
.with_metrics(Some(metrics_prefix::<P>(&params.lane)), metrics_params)
.loop_metric(MessageLaneLoopMetrics::new)?
.standalone_metric(GlobalMetrics::new)?
.with_metrics(metrics_params)
.loop_metric(MessageLaneLoopMetrics::new(Some(&metrics_prefix::<P>(&params.lane)))?)?
.expose()
.await?
.run(metrics_prefix::<P>(&params.lane), move |source_client, target_client, metrics| {
@@ -521,7 +521,7 @@ where
nonces_queue_range: 0..maximal_source_queue_index + 1,
};
let strategy = EnforcementStrategy::new(self.relay_strategy.clone());
let mut strategy = EnforcementStrategy::new(self.relay_strategy.clone());
let range_end = strategy.decide(reference).await?;
let range_begin = source_queue[0].1.begin();
+21 -19
View File
@@ -22,7 +22,9 @@ use crate::{
};
use bp_messages::MessageNonce;
use relay_utils::metrics::{metric_name, register, GaugeVec, Opts, PrometheusError, Registry, U64};
use relay_utils::metrics::{
metric_name, register, GaugeVec, Metric, Opts, PrometheusError, Registry, U64,
};
/// Message lane relay metrics.
///
@@ -38,30 +40,22 @@ pub struct MessageLaneLoopMetrics {
impl MessageLaneLoopMetrics {
/// Create and register messages loop metrics.
pub fn new(registry: &Registry, prefix: Option<&str>) -> Result<Self, PrometheusError> {
pub fn new(prefix: Option<&str>) -> Result<Self, PrometheusError> {
Ok(MessageLaneLoopMetrics {
best_block_numbers: register(
GaugeVec::new(
Opts::new(
metric_name(prefix, "best_block_numbers"),
"Best finalized block numbers",
),
&["type"],
)?,
registry,
best_block_numbers: GaugeVec::new(
Opts::new(
metric_name(prefix, "best_block_numbers"),
"Best finalized block numbers",
),
&["type"],
)?,
lane_state_nonces: register(
GaugeVec::new(
Opts::new(metric_name(prefix, "lane_state_nonces"), "Nonces of the lane state"),
&["type"],
)?,
registry,
lane_state_nonces: GaugeVec::new(
Opts::new(metric_name(prefix, "lane_state_nonces"), "Nonces of the lane state"),
&["type"],
)?,
})
}
}
impl MessageLaneLoopMetrics {
/// Update source client state metrics.
pub fn update_source_state<P: MessageLane>(&self, source_client_state: SourceClientState<P>) {
self.best_block_numbers
@@ -122,3 +116,11 @@ impl MessageLaneLoopMetrics {
.set(target_latest_confirmed_nonce);
}
}
impl Metric for MessageLaneLoopMetrics {
fn register(&self, registry: &Registry) -> Result<(), PrometheusError> {
register(self.best_block_numbers.clone(), registry)?;
register(self.lane_state_nonces.clone(), registry)?;
Ok(())
}
}
@@ -37,7 +37,7 @@ impl RelayStrategy for AltruisticStrategy {
SourceClient: MessageLaneSourceClient<P>,
TargetClient: MessageLaneTargetClient<P>,
>(
&self,
&mut self,
_reference: &mut RelayReference<P, SourceClient, TargetClient>,
) -> bool {
true
@@ -49,7 +49,7 @@ impl<Strategy: RelayStrategy> EnforcementStrategy<Strategy> {
SourceClient: MessageLaneSourceClient<P>,
TargetClient: MessageLaneTargetClient<P>,
>(
&self,
&mut self,
reference: RelayMessagesBatchReference<P, SourceClient, TargetClient>,
) -> Option<MessageNonce> {
let mut hard_selected_count = 0;
@@ -47,7 +47,7 @@ impl RelayStrategy for MixStrategy {
SourceClient: MessageLaneSourceClient<P>,
TargetClient: MessageLaneTargetClient<P>,
>(
&self,
&mut self,
reference: &mut RelayReference<P, SourceClient, TargetClient>,
) -> bool {
match self.relayer_mode {
@@ -52,7 +52,7 @@ pub trait RelayStrategy: 'static + Clone + Send + Sync {
SourceClient: MessageLaneSourceClient<P>,
TargetClient: MessageLaneTargetClient<P>,
>(
&self,
&mut self,
reference: &mut RelayReference<P, SourceClient, TargetClient>,
) -> bool;
}
@@ -41,7 +41,7 @@ impl RelayStrategy for RationalStrategy {
SourceClient: MessageLaneSourceClient<P>,
TargetClient: MessageLaneTargetClient<P>,
>(
&self,
&mut self,
reference: &mut RelayReference<P, SourceClient, TargetClient>,
) -> bool {
// technically, multiple confirmations will be delivered in a single transaction,
+21 -17
View File
@@ -46,28 +46,38 @@ pub struct MetricsParams {
/// Interface and TCP port to be used when exposing Prometheus metrics.
pub address: Option<MetricsAddress>,
/// Metrics registry. May be `Some(_)` if several components share the same endpoint.
pub registry: Option<Registry>,
/// Prefix that must be used in metric names.
pub metrics_prefix: Option<String>,
pub registry: Registry,
}
/// Metrics API.
pub trait Metrics: Clone + Send + Sync + 'static {}
/// Metric API.
pub trait Metric: Clone + Send + Sync + 'static {
fn register(&self, registry: &Registry) -> Result<(), PrometheusError>;
}
impl<T: Clone + Send + Sync + 'static> Metrics for T {}
/// Standalone metrics API.
/// Standalone metric API.
///
/// Metrics of this kind know how to update themselves, so we may just spawn and forget the
/// asynchronous self-update task.
#[async_trait]
pub trait StandaloneMetrics: Metrics {
pub trait StandaloneMetric: Metric {
/// Update metric values.
async fn update(&self);
/// Metrics update interval.
fn update_interval(&self) -> Duration;
/// Register and spawn metric. Metric is only spawned if it is registered for the first time.
fn register_and_spawn(self, registry: &Registry) -> Result<(), PrometheusError> {
match self.register(registry) {
Ok(()) => {
self.spawn();
Ok(())
},
Err(PrometheusError::AlreadyReg) => Ok(()),
Err(e) => Err(e),
}
}
/// Spawn the self update task that will keep update metric value at given intervals.
fn spawn(self) {
async_std::task::spawn(async move {
@@ -89,7 +99,7 @@ impl Default for MetricsAddress {
impl MetricsParams {
/// Creates metrics params so that metrics are not exposed.
pub fn disabled() -> Self {
MetricsParams { address: None, registry: None, metrics_prefix: None }
MetricsParams { address: None, registry: Registry::new() }
}
/// Do not expose metrics.
@@ -97,17 +107,11 @@ impl MetricsParams {
self.address = None;
self
}
/// Set prefix to use in metric names.
pub fn metrics_prefix(mut self, prefix: String) -> Self {
self.metrics_prefix = Some(prefix);
self
}
}
impl From<Option<MetricsAddress>> for MetricsParams {
fn from(address: Option<MetricsAddress>) -> Self {
MetricsParams { address, registry: None, metrics_prefix: None }
MetricsParams { address, registry: Registry::new() }
}
}
@@ -17,8 +17,8 @@
use crate::{
error::{self, Error},
metrics::{
metric_name, register, F64SharedRef, Gauge, PrometheusError, Registry, StandaloneMetrics,
F64,
metric_name, register, F64SharedRef, Gauge, Metric, PrometheusError, Registry,
StandaloneMetric, F64,
},
};
@@ -44,8 +44,6 @@ pub struct FloatJsonValueMetric {
impl FloatJsonValueMetric {
/// Create new metric instance with given name and help.
pub fn new(
registry: &Registry,
prefix: Option<&str>,
url: String,
json_path: String,
name: String,
@@ -55,7 +53,7 @@ impl FloatJsonValueMetric {
Ok(FloatJsonValueMetric {
url,
json_path,
metric: register(Gauge::new(metric_name(prefix, &name), help)?, registry)?,
metric: Gauge::new(metric_name(None, &name), help)?,
shared_value_ref,
})
}
@@ -81,8 +79,14 @@ impl FloatJsonValueMetric {
}
}
impl Metric for FloatJsonValueMetric {
fn register(&self, registry: &Registry) -> Result<(), PrometheusError> {
register(self.metric.clone(), registry).map(drop)
}
}
#[async_trait]
impl StandaloneMetrics for FloatJsonValueMetric {
impl StandaloneMetric for FloatJsonValueMetric {
fn update_interval(&self) -> Duration {
UPDATE_INTERVAL
}
@@ -17,8 +17,8 @@
//! Global system-wide Prometheus metrics exposed by relays.
use crate::metrics::{
metric_name, register, Gauge, GaugeVec, Opts, PrometheusError, Registry, StandaloneMetrics,
F64, U64,
metric_name, register, Gauge, GaugeVec, Metric, Opts, PrometheusError, Registry,
StandaloneMetric, F64, U64,
};
use async_std::sync::{Arc, Mutex};
@@ -40,36 +40,36 @@ pub struct GlobalMetrics {
impl GlobalMetrics {
/// Create and register global metrics.
pub fn new(registry: &Registry, prefix: Option<&str>) -> Result<Self, PrometheusError> {
pub fn new() -> Result<Self, PrometheusError> {
Ok(GlobalMetrics {
system: Arc::new(Mutex::new(System::new_with_specifics(RefreshKind::everything()))),
system_average_load: register(
GaugeVec::new(
Opts::new(metric_name(prefix, "system_average_load"), "System load average"),
&["over"],
)?,
registry,
system_average_load: GaugeVec::new(
Opts::new(metric_name(None, "system_average_load"), "System load average"),
&["over"],
)?,
process_cpu_usage_percentage: register(
Gauge::new(
metric_name(prefix, "process_cpu_usage_percentage"),
"Process CPU usage",
)?,
registry,
process_cpu_usage_percentage: Gauge::new(
metric_name(None, "process_cpu_usage_percentage"),
"Process CPU usage",
)?,
process_memory_usage_bytes: register(
Gauge::new(
metric_name(prefix, "process_memory_usage_bytes"),
"Process memory (resident set size) usage",
)?,
registry,
process_memory_usage_bytes: Gauge::new(
metric_name(None, "process_memory_usage_bytes"),
"Process memory (resident set size) usage",
)?,
})
}
}
impl Metric for GlobalMetrics {
fn register(&self, registry: &Registry) -> Result<(), PrometheusError> {
register(self.system_average_load.clone(), registry)?;
register(self.process_cpu_usage_percentage.clone(), registry)?;
register(self.process_memory_usage_bytes.clone(), registry)?;
Ok(())
}
}
#[async_trait]
impl StandaloneMetrics for GlobalMetrics {
impl StandaloneMetric for GlobalMetrics {
async fn update(&self) {
// update system-wide metrics
let mut system = self.system.lock().await;
+10 -50
View File
@@ -16,7 +16,7 @@
use crate::{
error::Error,
metrics::{Metrics, MetricsAddress, MetricsParams, PrometheusError, StandaloneMetrics},
metrics::{Metric, MetricsAddress, MetricsParams},
FailedClient, MaybeConnectionError,
};
@@ -53,7 +53,7 @@ pub fn relay_loop<SC, TC>(source_client: SC, target_client: TC) -> Loop<SC, TC,
/// Returns generic relay loop metrics that may be customized and used in one or several relay
/// loops.
pub fn relay_metrics(prefix: Option<String>, params: MetricsParams) -> LoopMetrics<(), (), ()> {
pub fn relay_metrics(params: MetricsParams) -> LoopMetrics<(), (), ()> {
LoopMetrics {
relay_loop: Loop {
reconnect_delay: RECONNECT_DELAY,
@@ -62,8 +62,7 @@ pub fn relay_metrics(prefix: Option<String>, params: MetricsParams) -> LoopMetri
loop_metric: None,
},
address: params.address,
registry: params.registry.unwrap_or_else(|| create_metrics_registry(prefix)),
metrics_prefix: params.metrics_prefix,
registry: params.registry,
loop_metric: None,
}
}
@@ -81,7 +80,6 @@ pub struct LoopMetrics<SC, TC, LM> {
relay_loop: Loop<SC, TC, ()>,
address: Option<MetricsAddress>,
registry: Registry,
metrics_prefix: Option<String>,
loop_metric: Option<LM>,
}
@@ -93,11 +91,7 @@ impl<SC, TC, LM> Loop<SC, TC, LM> {
}
/// Start building loop metrics using given prefix.
pub fn with_metrics(
self,
prefix: Option<String>,
params: MetricsParams,
) -> LoopMetrics<SC, TC, ()> {
pub fn with_metrics(self, params: MetricsParams) -> LoopMetrics<SC, TC, ()> {
LoopMetrics {
relay_loop: Loop {
reconnect_delay: self.reconnect_delay,
@@ -106,8 +100,7 @@ impl<SC, TC, LM> Loop<SC, TC, LM> {
loop_metric: None,
},
address: params.address,
registry: params.registry.unwrap_or_else(|| create_metrics_registry(prefix)),
metrics_prefix: params.metrics_prefix,
registry: params.registry,
loop_metric: None,
}
}
@@ -160,44 +153,23 @@ impl<SC, TC, LM> LoopMetrics<SC, TC, LM> {
/// Add relay loop metrics.
///
/// Loop metrics will be passed to the loop callback.
pub fn loop_metric<NewLM: Metrics>(
pub fn loop_metric<NewLM: Metric>(
self,
create_metric: impl FnOnce(&Registry, Option<&str>) -> Result<NewLM, PrometheusError>,
metric: NewLM,
) -> Result<LoopMetrics<SC, TC, NewLM>, Error> {
let loop_metric = create_metric(&self.registry, self.metrics_prefix.as_deref())?;
metric.register(&self.registry)?;
Ok(LoopMetrics {
relay_loop: self.relay_loop,
address: self.address,
registry: self.registry,
metrics_prefix: self.metrics_prefix,
loop_metric: Some(loop_metric),
loop_metric: Some(metric),
})
}
/// Add standalone metrics.
pub fn standalone_metric<M: StandaloneMetrics>(
self,
create_metric: impl FnOnce(&Registry, Option<&str>) -> Result<M, PrometheusError>,
) -> Result<Self, Error> {
// since standalone metrics are updating themselves, we may just ignore the fact that the
// same standalone metric is exposed by several loops && only spawn single metric
match create_metric(&self.registry, self.metrics_prefix.as_deref()) {
Ok(standalone_metrics) => standalone_metrics.spawn(),
Err(PrometheusError::AlreadyReg) => (),
Err(e) => return Err(e.into()),
}
Ok(self)
}
/// Convert into `MetricsParams` structure so that metrics registry may be extended later.
pub fn into_params(self) -> MetricsParams {
MetricsParams {
address: self.address,
registry: Some(self.registry),
metrics_prefix: self.metrics_prefix,
}
MetricsParams { address: self.address, registry: self.registry }
}
/// Expose metrics using address passed at creation.
@@ -274,15 +246,3 @@ pub async fn reconnect_failed_client(
break
}
}
/// Create new registry with global metrics.
fn create_metrics_registry(prefix: Option<String>) -> Registry {
match prefix {
Some(prefix) => {
assert!(!prefix.is_empty(), "Metrics prefix can not be empty");
Registry::new_custom(Some(prefix), None)
.expect("only fails if prefix is empty; prefix is not empty; qed")
},
None => Registry::new(),
}
}