mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-05-30 23:21:02 +00:00
bed44dec13
* DummyOrdered relay: initial commit * 1 ML file -> 3 files * extracted generic message race loop * uncommented race error procesing * lane loop tests * cargo fmt * moved HeaderId to utils.rs * restart lane loop on stall * message delivery strategy tests * removed obsolete code * clippy * Update relays/ethereum/src/message_lane_loop.rs Co-authored-by: Tomasz Drwięga <tomusdrw@users.noreply.github.com> * Update relays/ethereum/src/message_lane_loop.rs Co-authored-by: Tomasz Drwięga <tomusdrw@users.noreply.github.com> * added more races docs * Update relays/ethereum/src/message_race_delivery.rs Co-authored-by: Tomasz Drwięga <tomusdrw@users.noreply.github.com> * update docs * Update relays/ethereum/src/message_race_loop.rs Co-authored-by: Tomasz Drwięga <tomusdrw@users.noreply.github.com> * deal with TODOs * removed docs * docs * cargo fmt --all * Update relays/ethereum/src/message_race_loop.rs Co-authored-by: Hernando Castano <HCastano@users.noreply.github.com> Co-authored-by: Tomasz Drwięga <tomusdrw@users.noreply.github.com> Co-authored-by: Hernando Castano <HCastano@users.noreply.github.com>
899 lines
26 KiB
Rust
899 lines
26 KiB
Rust
// Copyright 2019-2020 Parity Technologies (UK) Ltd.
|
|
// This file is part of Parity Bridges Common.
|
|
|
|
// Parity Bridges Common is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
|
|
// Parity Bridges Common is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with Parity Bridges Common. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
//! Relaying proofs of exchange transaction.
|
|
|
|
use crate::utils::{MaybeConnectionError, StringifiedMaybeConnectionError};
|
|
|
|
use async_trait::async_trait;
|
|
use std::{
|
|
fmt::{Debug, Display},
|
|
string::ToString,
|
|
};
|
|
|
|
/// Transaction proof pipeline.
|
|
pub trait TransactionProofPipeline {
|
|
/// Name of the transaction proof source.
|
|
const SOURCE_NAME: &'static str;
|
|
/// Name of the transaction proof target.
|
|
const TARGET_NAME: &'static str;
|
|
|
|
/// Block type.
|
|
type Block: SourceBlock;
|
|
/// Transaction inclusion proof type.
|
|
type TransactionProof;
|
|
}
|
|
|
|
/// Block that is participating in exchange.
|
|
pub trait SourceBlock {
|
|
/// Block hash type.
|
|
type Hash: Clone + Debug + Display;
|
|
/// Block number type.
|
|
type Number: Debug
|
|
+ Display
|
|
+ Clone
|
|
+ Copy
|
|
+ Into<u64>
|
|
+ std::cmp::Ord
|
|
+ std::ops::Add<Output = Self::Number>
|
|
+ num_traits::One;
|
|
/// Block transaction.
|
|
type Transaction: SourceTransaction;
|
|
|
|
/// Return hash of the block.
|
|
fn id(&self) -> crate::utils::HeaderId<Self::Hash, Self::Number>;
|
|
/// Return block transactions iterator.
|
|
fn transactions(&self) -> Vec<Self::Transaction>;
|
|
}
|
|
|
|
/// Transaction that is participating in exchange.
|
|
pub trait SourceTransaction {
|
|
/// Transaction hash type.
|
|
type Hash: Debug + Display;
|
|
|
|
/// Return transaction hash.
|
|
fn hash(&self) -> Self::Hash;
|
|
}
|
|
|
|
/// Block hash for given pipeline.
|
|
pub type BlockHashOf<P> = <<P as TransactionProofPipeline>::Block as SourceBlock>::Hash;
|
|
|
|
/// Block number for given pipeline.
|
|
pub type BlockNumberOf<P> = <<P as TransactionProofPipeline>::Block as SourceBlock>::Number;
|
|
|
|
/// Transaction hash for given pipeline.
|
|
pub type TransactionOf<P> = <<P as TransactionProofPipeline>::Block as SourceBlock>::Transaction;
|
|
|
|
/// Transaction hash for given pipeline.
|
|
pub type TransactionHashOf<P> = <TransactionOf<P> as SourceTransaction>::Hash;
|
|
|
|
/// Header id.
|
|
pub type HeaderId<P> = crate::utils::HeaderId<BlockHashOf<P>, BlockNumberOf<P>>;
|
|
|
|
/// Source client API.
|
|
#[async_trait]
|
|
pub trait SourceClient<P: TransactionProofPipeline> {
|
|
/// Error type.
|
|
type Error: Debug + MaybeConnectionError;
|
|
|
|
/// 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> {
|
|
/// Error type.
|
|
type Error: Debug + MaybeConnectionError;
|
|
|
|
/// Sleep until exchange-related data is (probably) updated.
|
|
async fn tick(&self);
|
|
/// Returns `Ok(true)` if header is known to the target node.
|
|
async fn is_header_known(&self, id: &HeaderId<P>) -> Result<bool, Self::Error>;
|
|
/// Returns `Ok(true)` if header is finalized by the target node.
|
|
async fn is_header_finalized(&self, id: &HeaderId<P>) -> Result<bool, Self::Error>;
|
|
/// Returns best finalized header id.
|
|
async fn best_finalized_header_id(&self) -> Result<HeaderId<P>, Self::Error>;
|
|
/// Returns `Ok(true)` if transaction proof is need to be relayed.
|
|
async fn filter_transaction_proof(&self, proof: &P::TransactionProof) -> Result<bool, Self::Error>;
|
|
/// Submits transaction proof to the target node.
|
|
async fn submit_transaction_proof(&self, proof: P::TransactionProof) -> Result<(), Self::Error>;
|
|
}
|
|
|
|
/// Block transaction statistics.
|
|
#[derive(Debug, Default)]
|
|
#[cfg_attr(test, derive(PartialEq))]
|
|
pub struct RelayedBlockTransactions {
|
|
/// Total number of transactions processed (either relayed or ignored) so far.
|
|
pub processed: usize,
|
|
/// Total number of transactions successfully relayed so far.
|
|
pub relayed: usize,
|
|
/// Total number of transactions that we have failed to relay so far.
|
|
pub failed: usize,
|
|
}
|
|
|
|
/// Relay all suitable transactions from single block.
|
|
///
|
|
/// If connection error occurs, returns Err with number of successfully processed transactions.
|
|
/// If some other error occurs, it is ignored and other transactions are processed.
|
|
///
|
|
/// All transaction-level traces are written by this function. This function is not tracing
|
|
/// any information about block.
|
|
pub async fn relay_block_transactions<P: TransactionProofPipeline>(
|
|
source_client: &impl SourceClient<P>,
|
|
target_client: &impl TargetClient<P>,
|
|
source_block: &P::Block,
|
|
mut relayed_transactions: RelayedBlockTransactions,
|
|
) -> Result<RelayedBlockTransactions, 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?;
|
|
|
|
let needs_to_be_relayed =
|
|
target_client
|
|
.filter_transaction_proof(&source_tx_proof)
|
|
.await
|
|
.map_err(|err| {
|
|
StringifiedMaybeConnectionError::new(
|
|
err.is_connection_error(),
|
|
format!("Transaction filtering has failed with {:?}", err),
|
|
)
|
|
})?;
|
|
|
|
if !needs_to_be_relayed {
|
|
return Ok(false);
|
|
}
|
|
|
|
relay_ready_transaction_proof(target_client, &source_tx_id, source_tx_proof)
|
|
.await
|
|
.map(|_| true)
|
|
}
|
|
.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(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(relayed_transactions);
|
|
}
|
|
|
|
relayed_transactions.processed += 1;
|
|
relayed_transactions.failed += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(relayed_transactions)
|
|
}
|
|
|
|
/// Relay single transaction proof.
|
|
pub async fn relay_single_transaction_proof<P: TransactionProofPipeline>(
|
|
source_client: &impl SourceClient<P>,
|
|
target_client: &impl TargetClient<P>,
|
|
source_tx_hash: TransactionHashOf<P>,
|
|
) -> Result<(), String> {
|
|
// wait for transaction and header on source node
|
|
let (source_header_id, source_tx_index) = wait_transaction_mined(source_client, &source_tx_hash).await?;
|
|
let source_block = source_client.block_by_hash(source_header_id.1.clone()).await;
|
|
let source_block = source_block.map_err(|err| {
|
|
format!(
|
|
"Error retrieving block {} from {} node: {:?}",
|
|
source_header_id.1,
|
|
P::SOURCE_NAME,
|
|
err,
|
|
)
|
|
})?;
|
|
|
|
// wait for transaction and header on target node
|
|
wait_header_imported(target_client, &source_header_id).await?;
|
|
wait_header_finalized(target_client, &source_header_id).await?;
|
|
|
|
// and finally - prepare and submit transaction proof to target node
|
|
let source_tx_id = format!("{}", source_tx_hash);
|
|
relay_ready_transaction_proof(
|
|
target_client,
|
|
&source_tx_id,
|
|
prepare_transaction_proof(source_client, &source_tx_id, &source_block, source_tx_index)
|
|
.await
|
|
.map_err(|err| err.to_string())?,
|
|
)
|
|
.await
|
|
.map_err(|err| err.to_string())
|
|
}
|
|
|
|
/// Prepare transaction proof.
|
|
async fn prepare_transaction_proof<P: TransactionProofPipeline>(
|
|
source_client: &impl SourceClient<P>,
|
|
source_tx_id: &str,
|
|
source_block: &P::Block,
|
|
source_tx_index: usize,
|
|
) -> Result<P::TransactionProof, StringifiedMaybeConnectionError> {
|
|
source_client
|
|
.transaction_proof(source_block, source_tx_index)
|
|
.await
|
|
.map_err(|err| {
|
|
StringifiedMaybeConnectionError::new(
|
|
err.is_connection_error(),
|
|
format!(
|
|
"Error building transaction {} proof on {} node: {:?}",
|
|
source_tx_id,
|
|
P::SOURCE_NAME,
|
|
err,
|
|
),
|
|
)
|
|
})
|
|
}
|
|
|
|
/// Relay prepared proof of transaction.
|
|
async fn relay_ready_transaction_proof<P: TransactionProofPipeline>(
|
|
target_client: &impl TargetClient<P>,
|
|
source_tx_id: &str,
|
|
source_tx_proof: P::TransactionProof,
|
|
) -> Result<(), StringifiedMaybeConnectionError> {
|
|
target_client
|
|
.submit_transaction_proof(source_tx_proof)
|
|
.await
|
|
.map_err(|err| {
|
|
StringifiedMaybeConnectionError::new(
|
|
err.is_connection_error(),
|
|
format!(
|
|
"Error submitting transaction {} proof to {} node: {:?}",
|
|
source_tx_id,
|
|
P::TARGET_NAME,
|
|
err,
|
|
),
|
|
)
|
|
})
|
|
}
|
|
|
|
/// Wait until transaction is mined by source node.
|
|
async fn wait_transaction_mined<P: TransactionProofPipeline>(
|
|
source_client: &impl SourceClient<P>,
|
|
source_tx_hash: &TransactionHashOf<P>,
|
|
) -> Result<(HeaderId<P>, usize), String> {
|
|
loop {
|
|
let source_header_and_tx = source_client.transaction_block(&source_tx_hash).await.map_err(|err| {
|
|
format!(
|
|
"Error retrieving transaction {} from {} node: {:?}",
|
|
source_tx_hash,
|
|
P::SOURCE_NAME,
|
|
err,
|
|
)
|
|
})?;
|
|
match source_header_and_tx {
|
|
Some((source_header_id, source_tx)) => {
|
|
log::info!(
|
|
target: "bridge",
|
|
"Transaction {} is retrieved from {} node. Continuing...",
|
|
source_tx_hash,
|
|
P::SOURCE_NAME,
|
|
);
|
|
|
|
return Ok((source_header_id, source_tx));
|
|
}
|
|
None => {
|
|
log::info!(
|
|
target: "bridge",
|
|
"Waiting for transaction {} to be mined by {} node...",
|
|
source_tx_hash,
|
|
P::SOURCE_NAME,
|
|
);
|
|
|
|
source_client.tick().await;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Wait until target node imports required header.
|
|
async fn wait_header_imported<P: TransactionProofPipeline>(
|
|
target_client: &impl TargetClient<P>,
|
|
source_header_id: &HeaderId<P>,
|
|
) -> Result<(), String> {
|
|
loop {
|
|
let is_header_known = target_client.is_header_known(&source_header_id).await.map_err(|err| {
|
|
format!(
|
|
"Failed to check existence of header {}/{} on {} node: {:?}",
|
|
source_header_id.0,
|
|
source_header_id.1,
|
|
P::TARGET_NAME,
|
|
err,
|
|
)
|
|
})?;
|
|
match is_header_known {
|
|
true => {
|
|
log::info!(
|
|
target: "bridge",
|
|
"Header {}/{} is known to {} node. Continuing.",
|
|
source_header_id.0,
|
|
source_header_id.1,
|
|
P::TARGET_NAME,
|
|
);
|
|
|
|
return Ok(());
|
|
}
|
|
false => {
|
|
log::info!(
|
|
target: "bridge",
|
|
"Waiting for header {}/{} to be imported by {} node...",
|
|
source_header_id.0,
|
|
source_header_id.1,
|
|
P::TARGET_NAME,
|
|
);
|
|
|
|
target_client.tick().await;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Wait until target node finalizes required header.
|
|
async fn wait_header_finalized<P: TransactionProofPipeline>(
|
|
target_client: &impl TargetClient<P>,
|
|
source_header_id: &HeaderId<P>,
|
|
) -> Result<(), String> {
|
|
loop {
|
|
let is_header_finalized = target_client
|
|
.is_header_finalized(&source_header_id)
|
|
.await
|
|
.map_err(|err| {
|
|
format!(
|
|
"Failed to check finality of header {}/{} on {} node: {:?}",
|
|
source_header_id.0,
|
|
source_header_id.1,
|
|
P::TARGET_NAME,
|
|
err,
|
|
)
|
|
})?;
|
|
match is_header_finalized {
|
|
true => {
|
|
log::info!(
|
|
target: "bridge",
|
|
"Header {}/{} is finalizd by {} node. Continuing.",
|
|
source_header_id.0,
|
|
source_header_id.1,
|
|
P::TARGET_NAME,
|
|
);
|
|
|
|
return Ok(());
|
|
}
|
|
false => {
|
|
log::info!(
|
|
target: "bridge",
|
|
"Waiting for header {}/{} to be finalized by {} node...",
|
|
source_header_id.0,
|
|
source_header_id.1,
|
|
P::TARGET_NAME,
|
|
);
|
|
|
|
target_client.tick().await;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub(crate) mod tests {
|
|
use super::*;
|
|
use crate::utils::HeaderId;
|
|
|
|
use parking_lot::Mutex;
|
|
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);
|
|
|
|
pub struct TestTransactionsSource {
|
|
pub on_tick: Box<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,
|
|
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 SourceClient<TestTransactionProofPipeline> for TestTransactionsSource {
|
|
type Error = TestError;
|
|
|
|
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))
|
|
}
|
|
}
|
|
|
|
pub struct TestTransactionsTarget {
|
|
pub on_tick: Box<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,
|
|
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 TargetClient<TestTransactionProofPipeline> for TestTransactionsTarget {
|
|
type Error = TestError;
|
|
|
|
async fn tick(&self) {
|
|
(self.on_tick)(&mut *self.data.lock())
|
|
}
|
|
|
|
async fn is_header_known(&self, _: &TestHeaderId) -> Result<bool, TestError> {
|
|
self.data.lock().is_header_known.clone()
|
|
}
|
|
|
|
async fn is_header_finalized(&self, _: &TestHeaderId) -> Result<bool, TestError> {
|
|
self.data.lock().is_header_finalized.clone()
|
|
}
|
|
|
|
async fn best_finalized_header_id(&self) -> Result<TestHeaderId, TestError> {
|
|
self.data.lock().best_finalized_header_id.clone()
|
|
}
|
|
|
|
async fn filter_transaction_proof(&self, proof: &TestTransactionProof) -> Result<bool, TestError> {
|
|
Ok(self.data.lock().transactions_to_accept.contains(&proof.0))
|
|
}
|
|
|
|
async fn submit_transaction_proof(&self, proof: TestTransactionProof) -> Result<(), TestError> {
|
|
self.data.lock().submitted_proofs.push(proof);
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
fn ensure_relay_single_success(source: &TestTransactionsSource, target: &TestTransactionsTarget) {
|
|
assert_eq!(
|
|
async_std::task::block_on(relay_single_transaction_proof(source, target, test_transaction_hash(0),)),
|
|
Ok(()),
|
|
);
|
|
assert_eq!(
|
|
target.data.lock().submitted_proofs,
|
|
vec![TestTransactionProof(test_transaction_hash(0))],
|
|
);
|
|
}
|
|
|
|
fn ensure_relay_single_failure(source: TestTransactionsSource, target: TestTransactionsTarget) {
|
|
assert!(async_std::task::block_on(relay_single_transaction_proof(
|
|
&source,
|
|
&target,
|
|
test_transaction_hash(0),
|
|
))
|
|
.is_err(),);
|
|
assert!(target.data.lock().submitted_proofs.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn ready_transaction_proof_relayed_immediately() {
|
|
let source = TestTransactionsSource::new(Box::new(|_| unreachable!("no ticks allowed")));
|
|
let target = TestTransactionsTarget::new(Box::new(|_| unreachable!("no ticks allowed")));
|
|
ensure_relay_single_success(&source, &target)
|
|
}
|
|
|
|
#[test]
|
|
fn relay_transaction_proof_waits_for_transaction_to_be_mined() {
|
|
let source = TestTransactionsSource::new(Box::new(|source_data| {
|
|
assert_eq!(source_data.transaction_block, Ok(None));
|
|
source_data.transaction_block = Ok(Some((test_block_id(), 0)));
|
|
}));
|
|
let target = TestTransactionsTarget::new(Box::new(|_| unreachable!("no ticks allowed")));
|
|
|
|
// transaction is not yet mined, but will be available after first wait (tick)
|
|
source.data.lock().transaction_block = Ok(None);
|
|
|
|
ensure_relay_single_success(&source, &target)
|
|
}
|
|
|
|
#[test]
|
|
fn relay_transaction_fails_when_transaction_retrieval_fails() {
|
|
let source = TestTransactionsSource::new(Box::new(|_| unreachable!("no ticks allowed")));
|
|
let target = TestTransactionsTarget::new(Box::new(|_| unreachable!("no ticks allowed")));
|
|
|
|
source.data.lock().transaction_block = Err(TestError(false));
|
|
|
|
ensure_relay_single_failure(source, target)
|
|
}
|
|
|
|
#[test]
|
|
fn relay_transaction_fails_when_proof_retrieval_fails() {
|
|
let source = TestTransactionsSource::new(Box::new(|_| unreachable!("no ticks allowed")));
|
|
let target = TestTransactionsTarget::new(Box::new(|_| unreachable!("no ticks allowed")));
|
|
|
|
source
|
|
.data
|
|
.lock()
|
|
.proofs_to_fail
|
|
.insert(test_transaction_hash(0), TestError(false));
|
|
|
|
ensure_relay_single_failure(source, target)
|
|
}
|
|
|
|
#[test]
|
|
fn relay_transaction_proof_waits_for_header_to_be_imported() {
|
|
let source = TestTransactionsSource::new(Box::new(|_| unreachable!("no ticks allowed")));
|
|
let target = TestTransactionsTarget::new(Box::new(|target_data| {
|
|
assert_eq!(target_data.is_header_known, Ok(false));
|
|
target_data.is_header_known = Ok(true);
|
|
}));
|
|
|
|
// header is not yet imported, but will be available after first wait (tick)
|
|
target.data.lock().is_header_known = Ok(false);
|
|
|
|
ensure_relay_single_success(&source, &target)
|
|
}
|
|
|
|
#[test]
|
|
fn relay_transaction_proof_fails_when_is_header_known_fails() {
|
|
let source = TestTransactionsSource::new(Box::new(|_| unreachable!("no ticks allowed")));
|
|
let target = TestTransactionsTarget::new(Box::new(|_| unreachable!("no ticks allowed")));
|
|
|
|
target.data.lock().is_header_known = Err(TestError(false));
|
|
|
|
ensure_relay_single_failure(source, target)
|
|
}
|
|
|
|
#[test]
|
|
fn relay_transaction_proof_waits_for_header_to_be_finalized() {
|
|
let source = TestTransactionsSource::new(Box::new(|_| unreachable!("no ticks allowed")));
|
|
let target = TestTransactionsTarget::new(Box::new(|target_data| {
|
|
assert_eq!(target_data.is_header_finalized, Ok(false));
|
|
target_data.is_header_finalized = Ok(true);
|
|
}));
|
|
|
|
// header is not yet finalized, but will be available after first wait (tick)
|
|
target.data.lock().is_header_finalized = Ok(false);
|
|
|
|
ensure_relay_single_success(&source, &target)
|
|
}
|
|
|
|
#[test]
|
|
fn relay_transaction_proof_fails_when_is_header_finalized_fails() {
|
|
let source = TestTransactionsSource::new(Box::new(|_| unreachable!("no ticks allowed")));
|
|
let target = TestTransactionsTarget::new(Box::new(|_| unreachable!("no ticks allowed")));
|
|
|
|
target.data.lock().is_header_finalized = Err(TestError(false));
|
|
|
|
ensure_relay_single_failure(source, target)
|
|
}
|
|
|
|
#[test]
|
|
fn relay_transaction_proof_fails_when_target_node_rejects_proof() {
|
|
let source = TestTransactionsSource::new(Box::new(|_| unreachable!("no ticks allowed")));
|
|
let target = TestTransactionsTarget::new(Box::new(|_| unreachable!("no ticks allowed")));
|
|
|
|
target
|
|
.data
|
|
.lock()
|
|
.transactions_to_accept
|
|
.remove(&test_transaction_hash(0));
|
|
|
|
ensure_relay_single_success(&source, &target)
|
|
}
|
|
|
|
fn test_relay_block_transactions(
|
|
source: &TestTransactionsSource,
|
|
target: &TestTransactionsTarget,
|
|
pre_relayed: RelayedBlockTransactions,
|
|
) -> Result<RelayedBlockTransactions, RelayedBlockTransactions> {
|
|
async_std::task::block_on(relay_block_transactions(
|
|
source,
|
|
target,
|
|
&TestBlock(
|
|
test_block_id(),
|
|
vec![test_transaction(0), test_transaction(1), test_transaction(2)],
|
|
),
|
|
pre_relayed,
|
|
))
|
|
}
|
|
|
|
#[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))
|
|
],
|
|
);
|
|
}
|
|
}
|