From 27c6a87041090c67151814bf218c6affaca36490 Mon Sep 17 00:00:00 2001 From: Serban Iorga Date: Mon, 18 Sep 2023 11:51:54 +0300 Subject: [PATCH] Add unit tests for the equivocation detection loop (#2571) * Add unit tests for the equivocation detection loop * clippy * use std::future::pending() --- .../src/justification/verification/mod.rs | 1 + bridges/relays/equivocation/Cargo.toml | 2 +- .../relays/equivocation/src/block_checker.rs | 229 +++++++++++++- .../equivocation/src/equivocation_loop.rs | 144 ++++++++- bridges/relays/equivocation/src/lib.rs | 16 +- bridges/relays/equivocation/src/mock.rs | 285 ++++++++++++++++++ bridges/relays/equivocation/src/reporter.rs | 52 +++- .../relays/finality/src/finality_proofs.rs | 10 +- .../src/finality_base/engine.rs | 2 +- bridges/relays/utils/src/lib.rs | 4 +- 10 files changed, 716 insertions(+), 29 deletions(-) create mode 100644 bridges/relays/equivocation/src/mock.rs diff --git a/bridges/primitives/header-chain/src/justification/verification/mod.rs b/bridges/primitives/header-chain/src/justification/verification/mod.rs index bb8aaadf327ec..a66fc1e0d91d1 100644 --- a/bridges/primitives/header-chain/src/justification/verification/mod.rs +++ b/bridges/primitives/header-chain/src/justification/verification/mod.rs @@ -143,6 +143,7 @@ pub enum PrecommitError { } /// The context needed for validating GRANDPA finality proofs. +#[derive(RuntimeDebug)] pub struct JustificationVerificationContext { /// The authority set used to verify the justification. pub voter_set: VoterSet, diff --git a/bridges/relays/equivocation/Cargo.toml b/bridges/relays/equivocation/Cargo.toml index 4df0f0d11709e..ff94e73709114 100644 --- a/bridges/relays/equivocation/Cargo.toml +++ b/bridges/relays/equivocation/Cargo.toml @@ -7,7 +7,7 @@ license = "GPL-3.0-or-later WITH Classpath-exception-2.0" description = "Equivocation detector" [dependencies] -async-std = "1.6.5" +async-std = { version = "1.6.5", features = ["attributes"] } async-trait = "0.1" bp-header-chain = { path = "../../primitives/header-chain" } finality-relay = { path = "../finality" } diff --git a/bridges/relays/equivocation/src/block_checker.rs b/bridges/relays/equivocation/src/block_checker.rs index 358d61fcf8e56..c8131e5b9796f 100644 --- a/bridges/relays/equivocation/src/block_checker.rs +++ b/bridges/relays/equivocation/src/block_checker.rs @@ -28,6 +28,7 @@ use num_traits::Saturating; /// /// Getting the finality info associated to the source headers synced with the target chain /// at the specified block. +#[cfg_attr(test, derive(Debug, PartialEq))] pub struct ReadSyncedHeaders { pub target_block_num: P::TargetNumber, } @@ -61,6 +62,7 @@ impl ReadSyncedHeaders

{ /// Second step in the block checking state machine. /// /// Reading the equivocation reporting context from the target chain. +#[cfg_attr(test, derive(Debug))] pub struct ReadContext { target_block_num: P::TargetNumber, synced_headers: Vec>, @@ -104,6 +106,7 @@ impl ReadContext

{ /// Third step in the block checking state machine. /// /// Searching for equivocations in the source headers synced with the target chain. +#[cfg_attr(test, derive(Debug))] pub struct FindEquivocations { target_block_num: P::TargetNumber, synced_headers: Vec>, @@ -122,10 +125,13 @@ impl FindEquivocations

{ &synced_header.finality_proof, finality_proofs_buf.buf().as_slice(), ) { - Ok(equivocations) => result.push(ReportEquivocations { - source_block_hash: self.context.synced_header_hash, - equivocations, - }), + Ok(equivocations) => + if !equivocations.is_empty() { + result.push(ReportEquivocations { + source_block_hash: self.context.synced_header_hash, + equivocations, + }) + }, Err(e) => { log::error!( target: "bridge", @@ -148,6 +154,7 @@ impl FindEquivocations

{ /// Fourth step in the block checking state machine. /// /// Reporting the detected equivocations (if any). +#[cfg_attr(test, derive(Debug))] pub struct ReportEquivocations { source_block_hash: P::Hash, equivocations: Vec, @@ -157,7 +164,7 @@ impl ReportEquivocations

{ pub async fn next>( mut self, source_client: &mut SC, - reporter: &mut EquivocationsReporter, + reporter: &mut EquivocationsReporter<'_, P, SC>, ) -> Result<(), Self> { let mut unprocessed_equivocations = vec![]; for equivocation in self.equivocations { @@ -191,6 +198,7 @@ impl ReportEquivocations

{ } /// Block checking state machine. +#[cfg_attr(test, derive(Debug))] pub enum BlockChecker { ReadSyncedHeaders(ReadSyncedHeaders

), ReadContext(ReadContext

), @@ -250,3 +258,214 @@ impl BlockChecker

{ .boxed() } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::mock::*; + use std::collections::HashMap; + + impl PartialEq for ReadContext { + fn eq(&self, other: &Self) -> bool { + self.target_block_num == other.target_block_num && + self.synced_headers == other.synced_headers + } + } + + impl PartialEq for FindEquivocations { + fn eq(&self, other: &Self) -> bool { + self.target_block_num == other.target_block_num && + self.synced_headers == other.synced_headers && + self.context == other.context + } + } + + impl PartialEq for ReportEquivocations { + fn eq(&self, other: &Self) -> bool { + self.source_block_hash == other.source_block_hash && + self.equivocations == other.equivocations + } + } + + impl PartialEq for BlockChecker { + fn eq(&self, _other: &Self) -> bool { + matches!(self, _other) + } + } + + #[async_std::test] + async fn block_checker_works() { + let mut source_client = TestSourceClient { ..Default::default() }; + let mut target_client = TestTargetClient { + best_synced_header_hash: HashMap::from([(9, Ok(Some(5)))]), + finality_verification_context: HashMap::from([( + 9, + Ok(TestFinalityVerificationContext { check_equivocations: true }), + )]), + synced_headers_finality_info: HashMap::from([( + 10, + Ok(vec![ + new_header_finality_info(6, None), + new_header_finality_info(7, Some(false)), + new_header_finality_info(8, None), + new_header_finality_info(9, Some(true)), + new_header_finality_info(10, None), + new_header_finality_info(11, None), + new_header_finality_info(12, None), + ]), + )]), + ..Default::default() + }; + let mut reporter = + EquivocationsReporter::::new(); + + let block_checker = BlockChecker::new(10); + assert!(block_checker + .run( + &mut source_client, + &mut target_client, + &mut FinalityProofsBuf::new(vec![ + TestFinalityProof(6, vec!["6-1"]), + TestFinalityProof(7, vec![]), + TestFinalityProof(8, vec!["8-1"]), + TestFinalityProof(9, vec!["9-1"]), + TestFinalityProof(10, vec![]), + TestFinalityProof(11, vec!["11-1", "11-2"]), + TestFinalityProof(12, vec!["12-1"]) + ]), + &mut reporter + ) + .await + .is_ok()); + assert_eq!( + *source_client.reported_equivocations.lock().unwrap(), + HashMap::from([(5, vec!["6-1"]), (9, vec!["11-1", "11-2", "12-1"])]) + ); + } + + #[async_std::test] + async fn block_checker_works_with_empty_context() { + let mut target_client = TestTargetClient { + best_synced_header_hash: HashMap::from([(9, Ok(None))]), + finality_verification_context: HashMap::from([( + 9, + Ok(TestFinalityVerificationContext { check_equivocations: true }), + )]), + synced_headers_finality_info: HashMap::from([( + 10, + Ok(vec![new_header_finality_info(6, None)]), + )]), + ..Default::default() + }; + let mut source_client = TestSourceClient { ..Default::default() }; + let mut reporter = + EquivocationsReporter::::new(); + + let block_checker = BlockChecker::new(10); + assert!(block_checker + .run( + &mut source_client, + &mut target_client, + &mut FinalityProofsBuf::new(vec![TestFinalityProof(6, vec!["6-1"])]), + &mut reporter + ) + .await + .is_ok()); + assert_eq!(*source_client.reported_equivocations.lock().unwrap(), HashMap::default()); + } + + #[async_std::test] + async fn read_synced_headers_handles_errors() { + let mut target_client = TestTargetClient { + synced_headers_finality_info: HashMap::from([ + (10, Err(TestClientError::NonConnection)), + (11, Err(TestClientError::Connection)), + ]), + ..Default::default() + }; + let mut source_client = TestSourceClient { ..Default::default() }; + let mut reporter = + EquivocationsReporter::::new(); + + // NonConnection error + let block_checker = BlockChecker::new(10); + assert_eq!( + block_checker + .run( + &mut source_client, + &mut target_client, + &mut FinalityProofsBuf::new(vec![]), + &mut reporter + ) + .await, + Err(BlockChecker::ReadSyncedHeaders(ReadSyncedHeaders { target_block_num: 10 })) + ); + assert_eq!(target_client.num_reconnects, 0); + + // Connection error + let block_checker = BlockChecker::new(11); + assert_eq!( + block_checker + .run( + &mut source_client, + &mut target_client, + &mut FinalityProofsBuf::new(vec![]), + &mut reporter + ) + .await, + Err(BlockChecker::ReadSyncedHeaders(ReadSyncedHeaders { target_block_num: 11 })) + ); + assert_eq!(target_client.num_reconnects, 1); + } + + #[async_std::test] + async fn read_context_handles_errors() { + let mut target_client = TestTargetClient { + synced_headers_finality_info: HashMap::from([(10, Ok(vec![])), (11, Ok(vec![]))]), + best_synced_header_hash: HashMap::from([ + (9, Err(TestClientError::NonConnection)), + (10, Err(TestClientError::Connection)), + ]), + ..Default::default() + }; + let mut source_client = TestSourceClient { ..Default::default() }; + let mut reporter = + EquivocationsReporter::::new(); + + // NonConnection error + let block_checker = BlockChecker::new(10); + assert_eq!( + block_checker + .run( + &mut source_client, + &mut target_client, + &mut FinalityProofsBuf::new(vec![]), + &mut reporter + ) + .await, + Err(BlockChecker::ReadContext(ReadContext { + target_block_num: 10, + synced_headers: vec![] + })) + ); + assert_eq!(target_client.num_reconnects, 0); + + // Connection error + let block_checker = BlockChecker::new(11); + assert_eq!( + block_checker + .run( + &mut source_client, + &mut target_client, + &mut FinalityProofsBuf::new(vec![]), + &mut reporter + ) + .await, + Err(BlockChecker::ReadContext(ReadContext { + target_block_num: 11, + synced_headers: vec![] + })) + ); + assert_eq!(target_client.num_reconnects, 1); + } +} diff --git a/bridges/relays/equivocation/src/equivocation_loop.rs b/bridges/relays/equivocation/src/equivocation_loop.rs index da3f72b94660d..dfc4af0d4f62b 100644 --- a/bridges/relays/equivocation/src/equivocation_loop.rs +++ b/bridges/relays/equivocation/src/equivocation_loop.rs @@ -21,7 +21,7 @@ use crate::{ use crate::block_checker::BlockChecker; use finality_relay::{FinalityProofsBuf, FinalityProofsStream}; -use futures::{select, FutureExt}; +use futures::{select_biased, FutureExt}; use num_traits::Saturating; use relay_utils::{metrics::MetricsParams, FailedClient}; use std::{future::Future, time::Duration}; @@ -38,7 +38,7 @@ struct EquivocationDetectionLoop< from_block_num: Option, until_block_num: Option, - reporter: EquivocationsReporter, + reporter: EquivocationsReporter<'static, P, SC>, finality_proofs_stream: FinalityProofsStream, finality_proofs_buf: FinalityProofsBuf

, @@ -116,11 +116,11 @@ impl, TC: TargetClient

> .await; current_block_number = current_block_number.saturating_add(1.into()); } - self.until_block_num = Some(current_block_number); + self.from_block_num = Some(current_block_number); - select! { - _ = async_std::task::sleep(tick).fuse() => {}, + select_biased! { _ = exit_signal => return, + _ = async_std::task::sleep(tick).fuse() => {}, } } } @@ -172,3 +172,137 @@ pub async fn run( ) .await } + +#[cfg(test)] +mod tests { + use super::*; + use crate::mock::*; + use futures::{channel::mpsc::UnboundedSender, StreamExt}; + use std::{ + collections::{HashMap, VecDeque}, + sync::{Arc, Mutex}, + }; + + fn best_finalized_header_number( + best_finalized_headers: &Mutex>>, + exit_sender: &UnboundedSender<()>, + ) -> Result { + let mut best_finalized_headers = best_finalized_headers.lock().unwrap(); + let result = best_finalized_headers.pop_front().unwrap(); + if best_finalized_headers.is_empty() { + exit_sender.unbounded_send(()).unwrap(); + } + result + } + + #[async_std::test] + async fn multiple_blocks_are_checked_correctly() { + let best_finalized_headers = Arc::new(Mutex::new(VecDeque::from([Ok(10), Ok(12), Ok(13)]))); + let (exit_sender, exit_receiver) = futures::channel::mpsc::unbounded(); + + let source_client = TestSourceClient { + finality_proofs: Arc::new(Mutex::new(vec![ + TestFinalityProof(2, vec!["2-1"]), + TestFinalityProof(3, vec!["3-1", "3-2"]), + TestFinalityProof(4, vec!["4-1"]), + TestFinalityProof(5, vec!["5-1"]), + TestFinalityProof(6, vec!["6-1", "6-2"]), + TestFinalityProof(7, vec!["7-1", "7-2"]), + ])), + ..Default::default() + }; + let reported_equivocations = source_client.reported_equivocations.clone(); + let target_client = TestTargetClient { + best_finalized_header_number: Arc::new(move || { + best_finalized_header_number(&best_finalized_headers, &exit_sender) + }), + best_synced_header_hash: HashMap::from([ + (9, Ok(Some(1))), + (10, Ok(Some(3))), + (11, Ok(Some(5))), + (12, Ok(Some(6))), + ]), + finality_verification_context: HashMap::from([ + (9, Ok(TestFinalityVerificationContext { check_equivocations: true })), + (10, Ok(TestFinalityVerificationContext { check_equivocations: true })), + (11, Ok(TestFinalityVerificationContext { check_equivocations: false })), + (12, Ok(TestFinalityVerificationContext { check_equivocations: true })), + ]), + synced_headers_finality_info: HashMap::from([ + ( + 10, + Ok(vec![new_header_finality_info(2, None), new_header_finality_info(3, None)]), + ), + ( + 11, + Ok(vec![ + new_header_finality_info(4, None), + new_header_finality_info(5, Some(false)), + ]), + ), + (12, Ok(vec![new_header_finality_info(6, None)])), + (13, Ok(vec![new_header_finality_info(7, None)])), + ]), + ..Default::default() + }; + + assert!(run::( + source_client, + target_client, + Duration::from_secs(0), + MetricsParams { address: None, registry: Default::default() }, + exit_receiver.into_future().map(|(_, _)| ()), + ) + .await + .is_ok()); + assert_eq!( + *reported_equivocations.lock().unwrap(), + HashMap::from([ + (1, vec!["2-1", "3-1", "3-2"]), + (3, vec!["4-1", "5-1"]), + (6, vec!["7-1", "7-2"]) + ]) + ); + } + + #[async_std::test] + async fn blocks_following_error_are_checked_correctly() { + let best_finalized_headers = Mutex::new(VecDeque::from([Ok(10), Ok(11)])); + let (exit_sender, exit_receiver) = futures::channel::mpsc::unbounded(); + + let source_client = TestSourceClient { + finality_proofs: Arc::new(Mutex::new(vec![ + TestFinalityProof(2, vec!["2-1"]), + TestFinalityProof(3, vec!["3-1"]), + ])), + ..Default::default() + }; + let reported_equivocations = source_client.reported_equivocations.clone(); + let target_client = TestTargetClient { + best_finalized_header_number: Arc::new(move || { + best_finalized_header_number(&best_finalized_headers, &exit_sender) + }), + best_synced_header_hash: HashMap::from([(9, Ok(Some(1))), (10, Ok(Some(2)))]), + finality_verification_context: HashMap::from([ + (9, Ok(TestFinalityVerificationContext { check_equivocations: true })), + (10, Ok(TestFinalityVerificationContext { check_equivocations: true })), + ]), + synced_headers_finality_info: HashMap::from([ + (10, Err(TestClientError::NonConnection)), + (11, Ok(vec![new_header_finality_info(3, None)])), + ]), + ..Default::default() + }; + + assert!(run::( + source_client, + target_client, + Duration::from_secs(0), + MetricsParams { address: None, registry: Default::default() }, + exit_receiver.into_future().map(|(_, _)| ()), + ) + .await + .is_ok()); + assert_eq!(*reported_equivocations.lock().unwrap(), HashMap::from([(2, vec!["3-1"]),])); + } +} diff --git a/bridges/relays/equivocation/src/lib.rs b/bridges/relays/equivocation/src/lib.rs index bb1f40c13e6d4..56a71ef3bc63c 100644 --- a/bridges/relays/equivocation/src/lib.rs +++ b/bridges/relays/equivocation/src/lib.rs @@ -16,24 +16,27 @@ mod block_checker; mod equivocation_loop; +mod mock; mod reporter; use async_trait::async_trait; use bp_header_chain::{FinalityProof, FindEquivocations}; use finality_relay::{FinalityPipeline, SourceClientBase}; -use relay_utils::{ - relay_loop::{Client as RelayClient, RECONNECT_DELAY}, - MaybeConnectionError, TransactionTracker, -}; -use std::fmt::Debug; +use relay_utils::{relay_loop::Client as RelayClient, MaybeConnectionError, TransactionTracker}; +use std::{fmt::Debug, time::Duration}; pub use equivocation_loop::run; +#[cfg(not(test))] +const RECONNECT_DELAY: Duration = relay_utils::relay_loop::RECONNECT_DELAY; +#[cfg(test)] +const RECONNECT_DELAY: Duration = mock::TEST_RECONNECT_DELAY; + pub trait EquivocationDetectionPipeline: FinalityPipeline { /// Block number of the target chain. type TargetNumber: relay_utils::BlockNumberBase; /// The context needed for validating finality proofs. - type FinalityVerificationContext: Send; + type FinalityVerificationContext: Debug + Send; /// The type of the equivocation proof. type EquivocationProof: Clone + Debug + Send + Sync; /// The equivocations finder. @@ -91,6 +94,7 @@ pub trait TargetClient: RelayClient { } /// The context needed for finding equivocations inside finality proofs and reporting them. +#[derive(Debug, PartialEq)] struct EquivocationReportingContext { pub synced_header_hash: P::Hash, pub synced_verification_context: P::FinalityVerificationContext, diff --git a/bridges/relays/equivocation/src/mock.rs b/bridges/relays/equivocation/src/mock.rs new file mode 100644 index 0000000000000..ced5c6f358065 --- /dev/null +++ b/bridges/relays/equivocation/src/mock.rs @@ -0,0 +1,285 @@ +// Copyright (C) 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 . + +#![cfg(test)] + +use crate::{EquivocationDetectionPipeline, HeaderFinalityInfo, SourceClient, TargetClient}; +use async_trait::async_trait; +use bp_header_chain::{FinalityProof, FindEquivocations}; +use finality_relay::{FinalityPipeline, SourceClientBase}; +use futures::{Stream, StreamExt}; +use relay_utils::{ + relay_loop::Client as RelayClient, HeaderId, MaybeConnectionError, TrackedTransactionStatus, + TransactionTracker, +}; +use std::{ + collections::HashMap, + pin::Pin, + sync::{Arc, Mutex}, + time::Duration, +}; + +pub type TestSourceHashAndNumber = u64; +pub type TestTargetNumber = u64; +pub type TestEquivocationProof = &'static str; + +pub const TEST_RECONNECT_DELAY: Duration = Duration::from_secs(0); + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TestFinalityProof(pub TestSourceHashAndNumber, pub Vec); + +impl FinalityProof for TestFinalityProof { + fn target_header_hash(&self) -> TestSourceHashAndNumber { + self.0 + } + + fn target_header_number(&self) -> TestSourceHashAndNumber { + self.0 + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct TestEquivocationDetectionPipeline; + +impl FinalityPipeline for TestEquivocationDetectionPipeline { + const SOURCE_NAME: &'static str = "TestSource"; + const TARGET_NAME: &'static str = "TestTarget"; + + type Hash = TestSourceHashAndNumber; + type Number = TestSourceHashAndNumber; + type FinalityProof = TestFinalityProof; +} + +#[derive(Clone, Debug, PartialEq)] +pub struct TestFinalityVerificationContext { + pub check_equivocations: bool, +} + +pub struct TestEquivocationsFinder; + +impl FindEquivocations + for TestEquivocationsFinder +{ + type Error = (); + + fn find_equivocations( + verification_context: &TestFinalityVerificationContext, + synced_proof: &TestFinalityProof, + source_proofs: &[TestFinalityProof], + ) -> Result, Self::Error> { + if verification_context.check_equivocations { + // Get the equivocations from the source proofs, in order to make sure + // that they are correctly provided. + if let Some(proof) = source_proofs.iter().find(|proof| proof.0 == synced_proof.0) { + return Ok(proof.1.clone()) + } + } + + Ok(vec![]) + } +} + +impl EquivocationDetectionPipeline for TestEquivocationDetectionPipeline { + type TargetNumber = TestTargetNumber; + type FinalityVerificationContext = TestFinalityVerificationContext; + type EquivocationProof = TestEquivocationProof; + type EquivocationsFinder = TestEquivocationsFinder; +} + +#[derive(Debug, Clone)] +pub enum TestClientError { + Connection, + NonConnection, +} + +impl MaybeConnectionError for TestClientError { + fn is_connection_error(&self) -> bool { + match self { + TestClientError::Connection => true, + TestClientError::NonConnection => false, + } + } +} + +#[derive(Clone)] +pub struct TestSourceClient { + pub num_reconnects: u32, + pub finality_proofs: Arc>>, + pub reported_equivocations: + Arc>>>, +} + +impl Default for TestSourceClient { + fn default() -> Self { + Self { + num_reconnects: 0, + finality_proofs: Arc::new(Mutex::new(vec![])), + reported_equivocations: Arc::new(Mutex::new(Default::default())), + } + } +} + +#[async_trait] +impl RelayClient for TestSourceClient { + type Error = TestClientError; + + async fn reconnect(&mut self) -> Result<(), Self::Error> { + self.num_reconnects += 1; + + Ok(()) + } +} + +#[async_trait] +impl SourceClientBase for TestSourceClient { + type FinalityProofsStream = Pin + 'static + Send>>; + + async fn finality_proofs(&self) -> Result { + let finality_proofs = std::mem::take(&mut *self.finality_proofs.lock().unwrap()); + Ok(futures::stream::iter(finality_proofs).boxed()) + } +} + +#[derive(Clone, Debug)] +pub struct TestTransactionTracker( + pub TrackedTransactionStatus>, +); + +impl Default for TestTransactionTracker { + fn default() -> TestTransactionTracker { + TestTransactionTracker(TrackedTransactionStatus::Finalized(Default::default())) + } +} + +#[async_trait] +impl TransactionTracker for TestTransactionTracker { + type HeaderId = HeaderId; + + async fn wait( + self, + ) -> TrackedTransactionStatus> { + self.0 + } +} + +#[async_trait] +impl SourceClient for TestSourceClient { + type TransactionTracker = TestTransactionTracker; + + async fn report_equivocation( + &self, + at: TestSourceHashAndNumber, + equivocation: TestEquivocationProof, + ) -> Result { + self.reported_equivocations + .lock() + .unwrap() + .entry(at) + .or_default() + .push(equivocation); + + Ok(TestTransactionTracker::default()) + } +} + +#[derive(Clone)] +pub struct TestTargetClient { + pub num_reconnects: u32, + pub best_finalized_header_number: + Arc Result + Send + Sync>, + pub best_synced_header_hash: + HashMap, TestClientError>>, + pub finality_verification_context: + HashMap>, + pub synced_headers_finality_info: HashMap< + TestTargetNumber, + Result>, TestClientError>, + >, +} + +impl Default for TestTargetClient { + fn default() -> Self { + Self { + num_reconnects: 0, + best_finalized_header_number: Arc::new(|| Ok(0)), + best_synced_header_hash: Default::default(), + finality_verification_context: Default::default(), + synced_headers_finality_info: Default::default(), + } + } +} + +#[async_trait] +impl RelayClient for TestTargetClient { + type Error = TestClientError; + + async fn reconnect(&mut self) -> Result<(), Self::Error> { + self.num_reconnects += 1; + + Ok(()) + } +} + +#[async_trait] +impl TargetClient for TestTargetClient { + async fn best_finalized_header_number(&self) -> Result { + (self.best_finalized_header_number)() + } + + async fn best_synced_header_hash( + &self, + at: TestTargetNumber, + ) -> Result, Self::Error> { + self.best_synced_header_hash + .get(&at) + .unwrap_or(&Err(TestClientError::NonConnection)) + .clone() + } + + async fn finality_verification_context( + &self, + at: TestTargetNumber, + ) -> Result { + self.finality_verification_context + .get(&at) + .unwrap_or(&Err(TestClientError::NonConnection)) + .clone() + } + + async fn synced_headers_finality_info( + &self, + at: TestTargetNumber, + ) -> Result>, Self::Error> { + self.synced_headers_finality_info + .get(&at) + .unwrap_or(&Err(TestClientError::NonConnection)) + .clone() + } +} + +pub fn new_header_finality_info( + source_hdr: TestSourceHashAndNumber, + check_following_equivocations: Option, +) -> HeaderFinalityInfo { + HeaderFinalityInfo:: { + finality_proof: TestFinalityProof(source_hdr, vec![]), + new_verification_context: check_following_equivocations.map( + |check_following_equivocations| TestFinalityVerificationContext { + check_equivocations: check_following_equivocations, + }, + ), + } +} diff --git a/bridges/relays/equivocation/src/reporter.rs b/bridges/relays/equivocation/src/reporter.rs index 27b4d71beb01b..9c4642383d164 100644 --- a/bridges/relays/equivocation/src/reporter.rs +++ b/bridges/relays/equivocation/src/reporter.rs @@ -25,11 +25,11 @@ use std::{ task::{Context, Poll}, }; -pub struct EquivocationsReporter> { - pending_reports: Vec>, +pub struct EquivocationsReporter<'a, P: EquivocationDetectionPipeline, SC: SourceClient

> { + pending_reports: Vec>, } -impl> EquivocationsReporter { +impl<'a, P: EquivocationDetectionPipeline, SC: SourceClient

> EquivocationsReporter<'a, P, SC> { pub fn new() -> Self { Self { pending_reports: vec![] } } @@ -81,3 +81,49 @@ impl> EquivocationsReporte poll_fn(|cx| self.do_process_pending_reports(cx)).await } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::mock::*; + use relay_utils::HeaderId; + use std::sync::Mutex; + + #[async_std::test] + async fn process_pending_reports_works() { + let polled_reports = Mutex::new(vec![]); + let finished_reports = Mutex::new(vec![]); + + let mut reporter = + EquivocationsReporter:: { + pending_reports: vec![ + Box::pin(async { + polled_reports.lock().unwrap().push(1); + finished_reports.lock().unwrap().push(1); + TrackedTransactionStatus::Finalized(HeaderId(1, 1)) + }), + Box::pin(async { + polled_reports.lock().unwrap().push(2); + finished_reports.lock().unwrap().push(2); + TrackedTransactionStatus::Finalized(HeaderId(2, 2)) + }), + Box::pin(async { + polled_reports.lock().unwrap().push(3); + std::future::pending::<()>().await; + finished_reports.lock().unwrap().push(3); + TrackedTransactionStatus::Finalized(HeaderId(3, 3)) + }), + Box::pin(async { + polled_reports.lock().unwrap().push(4); + finished_reports.lock().unwrap().push(4); + TrackedTransactionStatus::Finalized(HeaderId(4, 4)) + }), + ], + }; + + reporter.process_pending_reports().await; + assert_eq!(*polled_reports.lock().unwrap(), vec![1, 2, 3, 4]); + assert_eq!(*finished_reports.lock().unwrap(), vec![1, 2, 4]); + assert_eq!(reporter.pending_reports.len(), 1); + } +} diff --git a/bridges/relays/finality/src/finality_proofs.rs b/bridges/relays/finality/src/finality_proofs.rs index cd6d12938ce42..e78cf8d62790d 100644 --- a/bridges/relays/finality/src/finality_proofs.rs +++ b/bridges/relays/finality/src/finality_proofs.rs @@ -32,6 +32,10 @@ impl> FinalityProofsStream { Self { stream: None } } + pub fn from_stream(stream: SC::FinalityProofsStream) -> Self { + Self { stream: Some(Box::pin(stream)) } + } + fn next(&mut self) -> Option<::Item> { let stream = match &mut self.stream { Some(stream) => stream, @@ -131,12 +135,6 @@ mod tests { use super::*; use crate::mock::*; - impl> FinalityProofsStream { - fn from_stream(stream: SC::FinalityProofsStream) -> Self { - Self { stream: Some(Box::pin(stream)) } - } - } - #[test] fn finality_proofs_buf_fill_works() { // when stream is currently empty, nothing is changed diff --git a/bridges/relays/lib-substrate-relay/src/finality_base/engine.rs b/bridges/relays/lib-substrate-relay/src/finality_base/engine.rs index afb2229fc4cf3..fb4515beeaaca 100644 --- a/bridges/relays/lib-substrate-relay/src/finality_base/engine.rs +++ b/bridges/relays/lib-substrate-relay/src/finality_base/engine.rs @@ -50,7 +50,7 @@ pub trait Engine: Send { /// Type of finality proofs, used by consensus engine. type FinalityProof: FinalityProof, BlockNumberOf> + Decode + Encode; /// The context needed for verifying finality proofs. - type FinalityVerificationContext: Send; + type FinalityVerificationContext: Debug + Send; /// The type of the equivocation proof used by the consensus engine. type EquivocationProof: Clone + Debug + Send + Sync; /// The equivocations finder. diff --git a/bridges/relays/utils/src/lib.rs b/bridges/relays/utils/src/lib.rs index f23357bfed709..2776620be3594 100644 --- a/bridges/relays/utils/src/lib.rs +++ b/bridges/relays/utils/src/lib.rs @@ -142,8 +142,8 @@ pub trait TransactionTracker: Send { } /// Future associated with `TransactionTracker`, monitoring the transaction status. -pub type TrackedTransactionFuture = - BoxFuture<'static, TrackedTransactionStatus<::HeaderId>>; +pub type TrackedTransactionFuture<'a, T> = + BoxFuture<'a, TrackedTransactionStatus<::HeaderId>>; /// Stringified error that may be either connection-related or not. #[derive(Error, Debug)]