mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-05-31 05:11:02 +00:00
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:
committed by
Bastian Köcher
parent
112eb86b8f
commit
c4424e5b73
@@ -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))
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user