Auto relay exchange transactions (#227)

* auto relay exchange transactions

* docker + auto-relay-tx

* clippy

* jsonrpsee in Cargo.lock ???

* fix tests compilation

* Show sccache

* mute clippy

* move

* Update relays/ethereum/src/exchange.rs

Co-authored-by: Hernando Castano <HCastano@users.noreply.github.com>

* finish comment

* (bool, String) -> StringifiedMaybeConnectionError

* Update deployments/rialto/docker-compose.yml

Co-authored-by: Hernando Castano <HCastano@users.noreply.github.com>

Co-authored-by: Denis S. Soldatov aka General-Beck <general.beck@gmail.com>
Co-authored-by: Hernando Castano <HCastano@users.noreply.github.com>
This commit is contained in:
Svyatoslav Nikolsky
2020-07-27 14:58:14 +03:00
committed by Bastian Köcher
parent 112eb86b8f
commit c4424e5b73
17 changed files with 1165 additions and 231 deletions
+562 -129
View File
@@ -14,10 +14,15 @@
// 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.
//! Relaying proofs of exchange transaction.
use crate::sync_types::MaybeConnectionError;
use async_trait::async_trait;
use std::fmt::{Debug, Display};
use std::{
fmt::{Debug, Display},
string::ToString,
};
/// Transaction proof pipeline.
pub trait TransactionProofPipeline {
@@ -26,50 +31,76 @@ pub trait TransactionProofPipeline {
/// Name of the transaction proof target.
const TARGET_NAME: &'static str;
/// Block hash type.
type BlockHash: Display;
/// Block number type.
type BlockNumber: Display;
/// Transaction hash type.
type TransactionHash: Display;
/// Transaction type.
type Transaction;
/// 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 + 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::sync_types::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::sync_types::HeaderId<
<P as TransactionProofPipeline>::BlockHash,
<P as TransactionProofPipeline>::BlockNumber,
>;
pub type HeaderId<P> = crate::sync_types::HeaderId<BlockHashOf<P>, BlockNumberOf<P>>;
/// Source client API.
#[async_trait]
pub trait SourceClient<P: TransactionProofPipeline> {
/// Error type.
type Error: Debug;
type Error: Debug + MaybeConnectionError;
/// Sleep until exchange-related data is (probably) updated.
async fn tick(&self);
/// Return **mined** transaction by its hash. May return `Ok(None)` if transaction is unknown to the source node.
async fn transaction(
&self,
hash: &P::TransactionHash,
) -> Result<Option<(HeaderId<P>, P::Transaction)>, Self::Error>;
/// 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,
header: &HeaderId<P>,
transaction: P::Transaction,
) -> Result<P::TransactionProof, Self::Error>;
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;
type Error: Debug + MaybeConnectionError;
/// Sleep until exchange-related data is (probably) updated.
async fn tick(&self);
@@ -77,44 +108,209 @@ pub trait TargetClient<P: TransactionProofPipeline> {
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,
}
/// Stringified error that may be either connection-related or not.
enum StringifiedMaybeConnectionError {
/// The error is connection-related error.
Connection(String),
/// The error is connection-unrelated error.
NonConnection(String),
}
/// 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: P::TransactionHash,
source_tx_hash: TransactionHashOf<P>,
) -> Result<(), String> {
// wait for transaction and header on source node
let (source_header_id, source_tx) = wait_transaction_mined(source_client, &source_tx_hash).await?;
let transaction_proof = source_client
.transaction_proof(&source_header_id, source_tx)
.await
.map_err(|err| {
format!(
"Error building transaction {} proof on {} node: {:?}",
source_tx_hash,
P::SOURCE_NAME,
err,
)
})?;
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 - submit transaction proof to target node
target_client
.submit_transaction_proof(transaction_proof)
// 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| {
format!(
"Error submitting transaction {} proof to {} node: {:?}",
source_tx_hash,
P::TARGET_NAME,
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,
),
)
})
}
@@ -122,10 +318,10 @@ pub async fn relay_single_transaction_proof<P: TransactionProofPipeline>(
/// Wait until transaction is mined by source node.
async fn wait_transaction_mined<P: TransactionProofPipeline>(
source_client: &impl SourceClient<P>,
source_tx_hash: &P::TransactionHash,
) -> Result<(HeaderId<P>, P::Transaction), String> {
source_tx_hash: &TransactionHashOf<P>,
) -> Result<(HeaderId<P>, usize), String> {
loop {
let source_header_and_tx = source_client.transaction(&source_tx_hash).await.map_err(|err| {
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,
@@ -245,68 +441,144 @@ async fn wait_header_finalized<P: TransactionProofPipeline>(
}
}
impl StringifiedMaybeConnectionError {
fn new(is_connection_error: bool, error: String) -> Self {
if is_connection_error {
StringifiedMaybeConnectionError::Connection(error)
} else {
StringifiedMaybeConnectionError::NonConnection(error)
}
}
}
impl MaybeConnectionError for StringifiedMaybeConnectionError {
fn is_connection_error(&self) -> bool {
match *self {
StringifiedMaybeConnectionError::Connection(_) => true,
StringifiedMaybeConnectionError::NonConnection(_) => false,
}
}
}
impl ToString for StringifiedMaybeConnectionError {
fn to_string(&self) -> String {
match *self {
StringifiedMaybeConnectionError::Connection(ref err) => err.clone(),
StringifiedMaybeConnectionError::NonConnection(ref err) => err.clone(),
}
}
}
#[cfg(test)]
mod tests {
pub(crate) mod tests {
use super::*;
use crate::sync_types::HeaderId;
use parking_lot::Mutex;
use std::{
collections::{HashMap, HashSet},
sync::Arc,
};
fn test_header_id() -> TestHeaderId {
HeaderId(100, 100)
pub fn test_block_id() -> TestHeaderId {
HeaderId(1, 1)
}
fn test_transaction_hash() -> TestTransactionHash {
200
pub fn test_next_block_id() -> TestHeaderId {
HeaderId(2, 2)
}
fn test_transaction() -> TestTransaction {
300
pub fn test_transaction_hash(tx_index: u64) -> TestTransactionHash {
200 + tx_index
}
fn test_transaction_proof() -> TestTransactionProof {
400
pub fn test_transaction(tx_index: u64) -> TestTransaction {
TestTransaction(test_transaction_hash(tx_index))
}
type TestError = u64;
type TestBlockNumber = u64;
type TestBlockHash = u64;
type TestTransactionHash = u64;
type TestTransaction = u64;
type TestTransactionProof = u64;
type TestHeaderId = HeaderId<TestBlockHash, TestBlockNumber>;
pub fn test_block() -> TestBlock {
TestBlock(test_block_id(), vec![test_transaction(0)])
}
struct TestTransactionProofPipeline;
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 BlockHash = TestBlockHash;
type BlockNumber = TestBlockNumber;
type TransactionHash = TestTransactionHash;
type Transaction = TestTransaction;
type Block = TestBlock;
type TransactionProof = TestTransactionProof;
}
struct TestTransactionsSource {
on_tick: Box<dyn Fn(&mut TestTransactionsSourceData) + Send + Sync>,
data: Mutex<TestTransactionsSourceData>,
#[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()
}
}
struct TestTransactionsSourceData {
transaction: Result<Option<(TestHeaderId, TestTransaction)>, TestError>,
transaction_proof: Result<TestTransactionProof, TestError>,
#[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 {
fn new(on_tick: Box<dyn Fn(&mut TestTransactionsSourceData) + Send + Sync>) -> Self {
pub fn new(on_tick: Box<dyn Fn(&mut TestTransactionsSourceData) + Send + Sync>) -> Self {
Self {
on_tick,
data: Mutex::new(TestTransactionsSourceData {
transaction: Ok(Some((test_header_id(), test_transaction()))),
transaction_proof: Ok(test_transaction_proof()),
}),
data: Arc::new(Mutex::new(TestTransactionsSourceData {
block: Ok(test_block()),
transaction_block: Ok(Some((test_block_id(), 0))),
proofs_to_fail: HashMap::new(),
})),
}
}
}
@@ -319,42 +591,53 @@ mod tests {
(self.on_tick)(&mut *self.data.lock())
}
async fn transaction(
&self,
_: &TestTransactionHash,
) -> Result<Option<(TestHeaderId, TestTransaction)>, TestError> {
self.data.lock().transaction
async fn block_by_hash(&self, _: TestBlockHash) -> Result<TestBlock, TestError> {
self.data.lock().block.clone()
}
async fn transaction_proof(
&self,
_: &TestHeaderId,
_: TestTransaction,
) -> Result<TestTransactionProof, TestError> {
self.data.lock().transaction_proof
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))
}
}
struct TestTransactionsTarget {
on_tick: Box<dyn Fn(&mut TestTransactionsTargetData) + Send + Sync>,
data: Mutex<TestTransactionsTargetData>,
pub struct TestTransactionsTarget {
pub on_tick: Box<dyn Fn(&mut TestTransactionsTargetData) + Send + Sync>,
pub data: Arc<Mutex<TestTransactionsTargetData>>,
}
struct TestTransactionsTargetData {
is_header_known: Result<bool, TestError>,
is_header_finalized: Result<bool, TestError>,
submitted_proofs: Vec<TestTransactionProof>,
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 {
fn new(on_tick: Box<dyn Fn(&mut TestTransactionsTargetData) + Send + Sync>) -> Self {
pub fn new(on_tick: Box<dyn Fn(&mut TestTransactionsTargetData) + Send + Sync>) -> Self {
Self {
on_tick,
data: Mutex::new(TestTransactionsTargetData {
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(),
}),
})),
}
}
}
@@ -368,11 +651,19 @@ mod tests {
}
async fn is_header_known(&self, _: &TestHeaderId) -> Result<bool, TestError> {
self.data.lock().is_header_known
self.data.lock().is_header_known.clone()
}
async fn is_header_finalized(&self, _: &TestHeaderId) -> Result<bool, TestError> {
self.data.lock().is_header_finalized
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> {
@@ -381,23 +672,22 @@ mod tests {
}
}
fn ensure_success(source: TestTransactionsSource, target: TestTransactionsTarget) {
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(),
)),
async_std::task::block_on(relay_single_transaction_proof(source, target, test_transaction_hash(0),)),
Ok(()),
);
assert_eq!(target.data.lock().submitted_proofs, vec![test_transaction_proof()],);
assert_eq!(
target.data.lock().submitted_proofs,
vec![TestTransactionProof(test_transaction_hash(0))],
);
}
fn ensure_failure(source: TestTransactionsSource, target: TestTransactionsTarget) {
fn ensure_relay_single_failure(source: TestTransactionsSource, target: TestTransactionsTarget) {
assert!(async_std::task::block_on(relay_single_transaction_proof(
&source,
&target,
test_transaction_hash(),
test_transaction_hash(0),
))
.is_err(),);
assert!(target.data.lock().submitted_proofs.is_empty());
@@ -407,21 +697,21 @@ mod tests {
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_success(source, target)
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, Ok(None));
source_data.transaction = Ok(Some((test_header_id(), test_transaction())));
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 = Ok(None);
source.data.lock().transaction_block = Ok(None);
ensure_success(source, target)
ensure_relay_single_success(&source, &target)
}
#[test]
@@ -429,9 +719,9 @@ mod tests {
let source = TestTransactionsSource::new(Box::new(|_| unreachable!("no ticks allowed")));
let target = TestTransactionsTarget::new(Box::new(|_| unreachable!("no ticks allowed")));
source.data.lock().transaction = Err(0);
source.data.lock().transaction_block = Err(TestError(false));
ensure_failure(source, target)
ensure_relay_single_failure(source, target)
}
#[test]
@@ -439,9 +729,13 @@ mod tests {
let source = TestTransactionsSource::new(Box::new(|_| unreachable!("no ticks allowed")));
let target = TestTransactionsTarget::new(Box::new(|_| unreachable!("no ticks allowed")));
source.data.lock().transaction_proof = Err(0);
source
.data
.lock()
.proofs_to_fail
.insert(test_transaction_hash(0), TestError(false));
ensure_failure(source, target)
ensure_relay_single_failure(source, target)
}
#[test]
@@ -455,7 +749,7 @@ mod tests {
// header is not yet imported, but will be available after first wait (tick)
target.data.lock().is_header_known = Ok(false);
ensure_success(source, target)
ensure_relay_single_success(&source, &target)
}
#[test]
@@ -463,9 +757,9 @@ mod tests {
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(0);
target.data.lock().is_header_known = Err(TestError(false));
ensure_failure(source, target)
ensure_relay_single_failure(source, target)
}
#[test]
@@ -479,7 +773,7 @@ mod tests {
// header is not yet finalized, but will be available after first wait (tick)
target.data.lock().is_header_finalized = Ok(false);
ensure_success(source, target)
ensure_relay_single_success(&source, &target)
}
#[test]
@@ -487,8 +781,147 @@ mod tests {
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(0);
target.data.lock().is_header_finalized = Err(TestError(false));
ensure_failure(source, target)
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))
],
);
}
}