Files
pezkuwi-sdk/cumulus/client/pov-recovery/src/tests.rs
T

1545 lines
45 KiB
Rust

// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Cumulus.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// Cumulus 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.
// Cumulus 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 Cumulus. If not, see <https://www.gnu.org/licenses/>.
use super::*;
use assert_matches::assert_matches;
use codec::{Decode, Encode};
use cumulus_primitives_core::relay_chain::{
BlockId, CandidateCommitments, CandidateDescriptorV2, CoreIndex, CoreState,
};
use cumulus_relay_chain_interface::{
InboundDownwardMessage, InboundHrmpMessage, OccupiedCoreAssumption, PHash, PHeader,
PersistedValidationData, RelayChainResult, StorageValue, ValidationCodeHash, ValidatorId,
};
use cumulus_test_client::runtime::{Block, Header};
use futures::{channel::mpsc, SinkExt, Stream};
use pezkuwi_node_primitives::AvailableData;
use pezkuwi_node_subsystem::{
messages::{AvailabilityRecoveryMessage, RuntimeApiRequest},
RecoveryError, TimeoutExt,
};
use pezkuwi_primitives::CandidateEvent;
use rstest::rstest;
use sc_client_api::{
BlockImportNotification, ClientInfo, CompactProof, FinalityNotification, FinalityNotifications,
FinalizeSummary, ImportNotifications, StorageEventStream, StorageKey,
};
use sc_consensus::import_queue::RuntimeOrigin;
use sc_utils::mpsc::{TracingUnboundedReceiver, TracingUnboundedSender};
use sp_api::RuntimeApiInfo;
use sp_blockchain::Info;
use sp_runtime::{generic::SignedBlock, Justifications};
use sp_version::RuntimeVersion;
use std::{
borrow::Cow,
collections::{BTreeMap, VecDeque},
ops::Range,
sync::{Arc, Mutex},
};
use tokio::task;
const GENESIS_HASH: PHash = PHash::zero();
const TEST_SESSION_INDEX: SessionIndex = 0;
struct AvailabilityRecoverySubsystemHandle {
tx: mpsc::Sender<AvailabilityRecoveryMessage>,
}
impl AvailabilityRecoverySubsystemHandle {
fn new() -> (Self, mpsc::Receiver<AvailabilityRecoveryMessage>) {
let (tx, rx) = mpsc::channel(10);
(Self { tx }, rx)
}
}
#[async_trait::async_trait]
impl RecoveryHandle for AvailabilityRecoverySubsystemHandle {
async fn send_recovery_msg(
&mut self,
message: AvailabilityRecoveryMessage,
_origin: &'static str,
) {
self.tx.send(message).await.expect("Receiver dropped");
}
}
struct TeyrchainClientInner<Block: BlockT> {
import_notifications_rx: Option<TracingUnboundedReceiver<BlockImportNotification<Block>>>,
finality_notifications_rx: Option<TracingUnboundedReceiver<FinalityNotification<Block>>>,
usage_infos: Vec<ClientInfo<Block>>,
block_statuses: Arc<Mutex<HashMap<Block::Hash, BlockStatus>>>,
}
impl<Block: BlockT> TeyrchainClientInner<Block> {
fn new(
usage_infos: Vec<ClientInfo<Block>>,
block_statuses: Arc<Mutex<HashMap<Block::Hash, BlockStatus>>>,
) -> (
Self,
TracingUnboundedSender<BlockImportNotification<Block>>,
TracingUnboundedSender<FinalityNotification<Block>>,
) {
let (import_notifications_tx, import_notifications_rx) =
sc_utils::mpsc::tracing_unbounded("import_notif", 10);
let (finality_notifications_tx, finality_notifications_rx) =
sc_utils::mpsc::tracing_unbounded("finality_notif", 10);
(
Self {
import_notifications_rx: Some(import_notifications_rx),
finality_notifications_rx: Some(finality_notifications_rx),
usage_infos,
block_statuses,
},
import_notifications_tx,
finality_notifications_tx,
)
}
}
struct TeyrchainClient<Block: BlockT> {
inner: Arc<Mutex<TeyrchainClientInner<Block>>>,
}
impl<Block: BlockT> TeyrchainClient<Block> {
fn new(
usage_infos: Vec<ClientInfo<Block>>,
block_statuses: Arc<Mutex<HashMap<Block::Hash, BlockStatus>>>,
) -> (
Self,
TracingUnboundedSender<BlockImportNotification<Block>>,
TracingUnboundedSender<FinalityNotification<Block>>,
) {
let (inner, import_notifications_tx, finality_notifications_tx) =
TeyrchainClientInner::new(usage_infos, block_statuses);
(
Self { inner: Arc::new(Mutex::new(inner)) },
import_notifications_tx,
finality_notifications_tx,
)
}
}
impl<Block: BlockT> BlockchainEvents<Block> for TeyrchainClient<Block> {
fn import_notification_stream(&self) -> ImportNotifications<Block> {
self.inner
.lock()
.expect("poisoned lock")
.import_notifications_rx
.take()
.expect("Should only be taken once")
}
fn every_import_notification_stream(&self) -> ImportNotifications<Block> {
unimplemented!()
}
fn finality_notification_stream(&self) -> FinalityNotifications<Block> {
self.inner
.lock()
.expect("poisoned lock")
.finality_notifications_rx
.take()
.expect("Should only be taken once")
}
fn storage_changes_notification_stream(
&self,
_filter_keys: Option<&[StorageKey]>,
_child_filter_keys: Option<&[(StorageKey, Option<Vec<StorageKey>>)]>,
) -> sp_blockchain::Result<StorageEventStream<Block::Hash>> {
unimplemented!()
}
}
impl<Block: BlockT> BlockBackend<Block> for TeyrchainClient<Block> {
fn block_body(
&self,
_: Block::Hash,
) -> sp_blockchain::Result<Option<Vec<<Block as BlockT>::Extrinsic>>> {
unimplemented!()
}
fn block(&self, _: Block::Hash) -> sp_blockchain::Result<Option<SignedBlock<Block>>> {
unimplemented!()
}
fn block_status(&self, hash: Block::Hash) -> sp_blockchain::Result<sp_consensus::BlockStatus> {
Ok(self
.inner
.lock()
.expect("Poisoned lock")
.block_statuses
.lock()
.expect("Poisoned lock")
.get(&hash)
.cloned()
.unwrap_or(BlockStatus::Unknown))
}
fn justifications(&self, _: Block::Hash) -> sp_blockchain::Result<Option<Justifications>> {
unimplemented!()
}
fn block_hash(&self, _: NumberFor<Block>) -> sp_blockchain::Result<Option<Block::Hash>> {
unimplemented!()
}
fn indexed_transaction(&self, _: Block::Hash) -> sp_blockchain::Result<Option<Vec<u8>>> {
unimplemented!()
}
fn has_indexed_transaction(&self, _: Block::Hash) -> sp_blockchain::Result<bool> {
unimplemented!()
}
fn block_indexed_body(&self, _: Block::Hash) -> sp_blockchain::Result<Option<Vec<Vec<u8>>>> {
unimplemented!()
}
fn requires_full_sync(&self) -> bool {
unimplemented!()
}
}
impl<Block: BlockT> UsageProvider<Block> for TeyrchainClient<Block> {
fn usage_info(&self) -> ClientInfo<Block> {
let infos = &mut self.inner.lock().expect("Poisoned lock").usage_infos;
assert!(!infos.is_empty());
if infos.len() == 1 {
infos.last().unwrap().clone()
} else {
infos.remove(0)
}
}
}
struct TeyrchainImportQueue<Block: BlockT> {
import_requests_tx: TracingUnboundedSender<Vec<IncomingBlock<Block>>>,
}
impl<Block: BlockT> TeyrchainImportQueue<Block> {
fn new() -> (Self, TracingUnboundedReceiver<Vec<IncomingBlock<Block>>>) {
let (import_requests_tx, import_requests_rx) =
sc_utils::mpsc::tracing_unbounded("test_import_req_forwarding", 10);
(Self { import_requests_tx }, import_requests_rx)
}
}
impl<Block: BlockT> ImportQueueService<Block> for TeyrchainImportQueue<Block> {
fn import_blocks(&mut self, origin: BlockOrigin, blocks: Vec<IncomingBlock<Block>>) {
assert_matches!(origin, BlockOrigin::ConsensusBroadcast);
self.import_requests_tx.unbounded_send(blocks).unwrap();
}
fn import_justifications(
&mut self,
_: RuntimeOrigin,
_: Block::Hash,
_: NumberFor<Block>,
_: Justifications,
) {
unimplemented!()
}
}
#[derive(Default)]
struct DummySyncOracle {
is_major_syncing: bool,
}
impl DummySyncOracle {
fn new(is_major_syncing: bool) -> Self {
Self { is_major_syncing }
}
}
impl SyncOracle for DummySyncOracle {
fn is_major_syncing(&self) -> bool {
self.is_major_syncing
}
fn is_offline(&self) -> bool {
false
}
}
#[derive(Clone)]
struct RelaychainInner {
runtime_version: u32,
import_notifications: Vec<PHeader>,
candidates_pending_availability: HashMap<PHash, Vec<CommittedCandidateReceipt>>,
}
#[derive(Clone)]
struct Relaychain {
inner: Arc<Mutex<RelaychainInner>>,
}
impl Relaychain {
fn new(relay_chain_blocks: Vec<(PHeader, Vec<CommittedCandidateReceipt>)>) -> Self {
let (candidates_pending_availability, import_notifications) = relay_chain_blocks
.into_iter()
.map(|(header, receipt)| ((header.hash(), receipt), header))
.unzip();
Self {
inner: Arc::new(Mutex::new(RelaychainInner {
import_notifications,
candidates_pending_availability,
// The version that introduced candidates_pending_availability
runtime_version:
RuntimeApiRequest::CANDIDATES_PENDING_AVAILABILITY_RUNTIME_REQUIREMENT,
})),
}
}
fn set_runtime_version(&self, version: u32) {
self.inner.lock().expect("Poisoned lock").runtime_version = version;
}
}
#[async_trait::async_trait]
impl RelayChainInterface for Relaychain {
async fn version(&self, _: PHash) -> RelayChainResult<RuntimeVersion> {
let version = self.inner.lock().expect("Poisoned lock").runtime_version;
let apis = sp_version::create_apis_vec!([(
<dyn pezkuwi_primitives::runtime_api::TeyrchainHost<pezkuwi_primitives::Block>>::ID,
version
)])
.into_owned()
.to_vec();
Ok(RuntimeVersion {
spec_name: Cow::Borrowed("test"),
impl_name: Cow::Borrowed("test"),
authoring_version: 1,
spec_version: 1,
impl_version: 0,
apis: Cow::Owned(apis),
transaction_version: 5,
system_version: 1,
})
}
async fn validators(&self, _: PHash) -> RelayChainResult<Vec<ValidatorId>> {
unimplemented!("Not needed for test")
}
async fn best_block_hash(&self) -> RelayChainResult<PHash> {
unimplemented!("Not needed for test")
}
async fn finalized_block_hash(&self) -> RelayChainResult<PHash> {
unimplemented!("Not needed for test")
}
async fn retrieve_dmq_contents(
&self,
_: ParaId,
_: PHash,
) -> RelayChainResult<Vec<InboundDownwardMessage>> {
unimplemented!("Not needed for test")
}
async fn retrieve_all_inbound_hrmp_channel_contents(
&self,
_: ParaId,
_: PHash,
) -> RelayChainResult<BTreeMap<ParaId, Vec<InboundHrmpMessage>>> {
unimplemented!("Not needed for test")
}
async fn persisted_validation_data(
&self,
_: PHash,
_: ParaId,
_: OccupiedCoreAssumption,
) -> RelayChainResult<Option<PersistedValidationData>> {
unimplemented!("Not needed for test")
}
async fn validation_code_hash(
&self,
_: PHash,
_: ParaId,
_: OccupiedCoreAssumption,
) -> RelayChainResult<Option<ValidationCodeHash>> {
unimplemented!("Not needed for test")
}
async fn candidate_pending_availability(
&self,
hash: PHash,
_: ParaId,
) -> RelayChainResult<Option<CommittedCandidateReceipt>> {
if self.inner.lock().expect("Poisoned lock").runtime_version >=
RuntimeApiRequest::CANDIDATES_PENDING_AVAILABILITY_RUNTIME_REQUIREMENT
{
panic!("Should have used candidates_pending_availability instead");
}
Ok(self
.inner
.lock()
.expect("Poisoned lock")
.candidates_pending_availability
.remove(&hash)
.map(|mut c| {
assert_eq!(c.len(), 1);
c.pop().unwrap()
}))
}
async fn candidates_pending_availability(
&self,
hash: PHash,
_: ParaId,
) -> RelayChainResult<Vec<CommittedCandidateReceipt>> {
if self.inner.lock().expect("Poisoned lock").runtime_version <
RuntimeApiRequest::CANDIDATES_PENDING_AVAILABILITY_RUNTIME_REQUIREMENT
{
panic!("Should have used candidate_pending_availability instead");
}
Ok(self
.inner
.lock()
.expect("Poisoned lock")
.candidates_pending_availability
.remove(&hash)
.expect("Not found"))
}
async fn session_index_for_child(&self, _: PHash) -> RelayChainResult<SessionIndex> {
Ok(TEST_SESSION_INDEX)
}
async fn import_notification_stream(
&self,
) -> RelayChainResult<Pin<Box<dyn Stream<Item = PHeader> + Send>>> {
Ok(Box::pin(
futures::stream::iter(std::mem::take(
&mut self.inner.lock().expect("Poisoned lock").import_notifications,
))
.chain(futures::stream::pending()),
))
}
async fn finality_notification_stream(
&self,
) -> RelayChainResult<Pin<Box<dyn Stream<Item = PHeader> + Send>>> {
unimplemented!("Not needed for test")
}
async fn is_major_syncing(&self) -> RelayChainResult<bool> {
unimplemented!("Not needed for test");
}
fn overseer_handle(&self) -> RelayChainResult<OverseerHandle> {
unimplemented!("Not needed for test")
}
async fn get_storage_by_key(
&self,
_: PHash,
_: &[u8],
) -> RelayChainResult<Option<StorageValue>> {
unimplemented!("Not needed for test")
}
async fn prove_read(
&self,
_: PHash,
_: &Vec<Vec<u8>>,
) -> RelayChainResult<sc_client_api::StorageProof> {
unimplemented!("Not needed for test")
}
async fn wait_for_block(&self, _: PHash) -> RelayChainResult<()> {
unimplemented!("Not needed for test");
}
async fn new_best_notification_stream(
&self,
) -> RelayChainResult<Pin<Box<dyn Stream<Item = PHeader> + Send>>> {
unimplemented!("Not needed for test");
}
async fn header(&self, _: BlockId) -> RelayChainResult<Option<PHeader>> {
unimplemented!("Not needed for test");
}
async fn availability_cores(
&self,
_: PHash,
) -> RelayChainResult<Vec<CoreState<PHash, NumberFor<Block>>>> {
unimplemented!("Not needed for test");
}
async fn claim_queue(
&self,
_: PHash,
) -> RelayChainResult<BTreeMap<CoreIndex, VecDeque<ParaId>>> {
unimplemented!("Not needed for test");
}
async fn call_runtime_api(
&self,
_method_name: &'static str,
_hash: PHash,
_payload: &[u8],
) -> RelayChainResult<Vec<u8>> {
unimplemented!("Not needed for test")
}
async fn scheduling_lookahead(&self, _: PHash) -> RelayChainResult<u32> {
unimplemented!("Not needed for test")
}
async fn candidate_events(&self, _: PHash) -> RelayChainResult<Vec<CandidateEvent>> {
unimplemented!("Not needed for test");
}
}
fn make_candidate_chain(candidate_number_range: Range<u32>) -> Vec<CommittedCandidateReceipt> {
let mut latest_parent_hash = GENESIS_HASH;
let mut candidates = vec![];
for number in candidate_number_range {
let head_data = Header {
number,
digest: Default::default(),
extrinsics_root: Default::default(),
parent_hash: latest_parent_hash,
state_root: Default::default(),
};
latest_parent_hash = head_data.hash();
candidates.push(CommittedCandidateReceipt {
descriptor: CandidateDescriptorV2::new(
ParaId::from(1000),
PHash::zero(),
CoreIndex(0),
0,
PHash::zero(),
PHash::zero(),
PHash::zero(),
PHash::zero(),
PHash::zero().into(),
),
commitments: CandidateCommitments {
head_data: head_data.encode().into(),
upward_messages: vec![].try_into().expect("empty vec fits within bounds"),
new_validation_code: None,
horizontal_messages: vec![].try_into().expect("empty vec fits within bounds"),
processed_downward_messages: 0,
hrmp_watermark: 0_u32,
},
});
}
candidates
}
fn dummy_usage_info(finalized_number: u32) -> ClientInfo<Block> {
ClientInfo {
chain: Info {
best_hash: PHash::zero(),
best_number: 0,
genesis_hash: PHash::zero(),
finalized_hash: PHash::zero(),
// Only this field is being used.
finalized_number,
finalized_state: None,
number_leaves: 0,
block_gap: None,
},
usage: None,
}
}
fn dummy_pvd() -> PersistedValidationData {
PersistedValidationData {
parent_head: vec![].into(),
relay_parent_number: 1,
relay_parent_storage_root: PHash::zero(),
max_pov_size: 100,
}
}
#[tokio::test]
async fn pending_candidate_height_lower_than_latest_finalized() {
sp_tracing::init_for_tests();
for finalized_number in [3, 4, 5] {
let (recovery_subsystem_tx, mut recovery_subsystem_rx) =
AvailabilityRecoverySubsystemHandle::new();
let recovery_delay_range =
RecoveryDelayRange { min: Duration::from_millis(0), max: Duration::from_millis(10) };
let (_explicit_recovery_chan_tx, explicit_recovery_chan_rx) = mpsc::channel(10);
let candidates = make_candidate_chain(1..4);
let relay_chain_client = Relaychain::new(vec![(
PHeader {
parent_hash: PHash::from_low_u64_be(0),
number: 1,
state_root: PHash::random(),
extrinsics_root: PHash::random(),
digest: Default::default(),
},
candidates,
)]);
let (teyrchain_client, _import_notifications_tx, _finality_notifications_tx) =
TeyrchainClient::new(vec![dummy_usage_info(finalized_number)], Default::default());
let (teyrchain_import_queue, mut import_requests_rx) = TeyrchainImportQueue::new();
// If the latest finalized block has a larger height compared to the pending candidate, the
// new candidate won't be recovered. Candidates have heights is 1, 2 and 3. Latest finalized
// block is 3, 4 or 5.
let pov_recovery = PoVRecovery::<Block, _, _>::new(
Box::new(recovery_subsystem_tx),
recovery_delay_range,
Arc::new(teyrchain_client),
Box::new(teyrchain_import_queue),
relay_chain_client,
ParaId::new(1000),
explicit_recovery_chan_rx,
Arc::new(DummySyncOracle::default()),
);
task::spawn(pov_recovery.run());
// No recovery message received
assert_matches!(
recovery_subsystem_rx.next().timeout(Duration::from_millis(100)).await,
None
);
// No import request received
assert_matches!(import_requests_rx.next().timeout(Duration::from_millis(100)).await, None);
}
}
#[rstest]
#[case(RuntimeApiRequest::CANDIDATES_PENDING_AVAILABILITY_RUNTIME_REQUIREMENT)]
#[case(10)]
#[tokio::test]
async fn single_pending_candidate_recovery_success(
#[case] runtime_version: u32,
#[values(true, false)] latest_block_data: bool,
) {
sp_tracing::init_for_tests();
let (recovery_subsystem_tx, mut recovery_subsystem_rx) =
AvailabilityRecoverySubsystemHandle::new();
let recovery_delay_range =
RecoveryDelayRange { min: Duration::from_millis(0), max: Duration::from_millis(10) };
let (_explicit_recovery_chan_tx, explicit_recovery_chan_rx) = mpsc::channel(10);
let candidates = make_candidate_chain(1..2);
let header = Header::decode(&mut &candidates[0].commitments.head_data.0[..]).unwrap();
let candidate_hash = candidates[0].hash();
let relay_chain_client = Relaychain::new(vec![(
PHeader {
parent_hash: PHash::from_low_u64_be(0),
number: 1,
state_root: PHash::random(),
extrinsics_root: PHash::random(),
digest: Default::default(),
},
candidates,
)]);
relay_chain_client.set_runtime_version(runtime_version);
let mut known_blocks = HashMap::new();
known_blocks.insert(GENESIS_HASH, BlockStatus::InChainWithState);
let (teyrchain_client, _import_notifications_tx, _finality_notifications_tx) =
TeyrchainClient::new(vec![dummy_usage_info(0)], Arc::new(Mutex::new(known_blocks)));
let (teyrchain_import_queue, mut import_requests_rx) = TeyrchainImportQueue::new();
let pov_recovery = PoVRecovery::<Block, _, _>::new(
Box::new(recovery_subsystem_tx),
recovery_delay_range,
Arc::new(teyrchain_client),
Box::new(teyrchain_import_queue),
relay_chain_client,
ParaId::new(1000),
explicit_recovery_chan_rx,
Arc::new(DummySyncOracle::default()),
);
task::spawn(pov_recovery.run());
assert_matches!(
recovery_subsystem_rx.next().await,
Some(AvailabilityRecoveryMessage::RecoverAvailableData(
receipt,
session_index,
None,
None,
response_tx
)) => {
assert_eq!(receipt.hash(), candidate_hash);
assert_eq!(session_index, TEST_SESSION_INDEX);
let block_data =
TeyrchainBlockData::<Block>::new(
vec![Block::new(header.clone(), vec![])], CompactProof { encoded_nodes: vec![] }
);
response_tx.send(
Ok(
AvailableData {
pov: Arc::new(PoV {
block_data: if latest_block_data {
block_data
} else {
block_data.as_v0().unwrap()
}.encode().into()
}),
validation_data: dummy_pvd(),
}
)
).unwrap()
}
);
// No more recovery messages received.
assert_matches!(recovery_subsystem_rx.next().timeout(Duration::from_millis(100)).await, None);
// Received import request for the recovered candidate
assert_matches!(import_requests_rx.next().await, Some(incoming_blocks) => {
assert_eq!(incoming_blocks.len(), 1);
assert_eq!(incoming_blocks[0].header, Some(header));
});
// No import request received
assert_matches!(import_requests_rx.next().timeout(Duration::from_millis(100)).await, None);
}
#[tokio::test]
async fn single_pending_candidate_recovery_retry_succeeds() {
sp_tracing::init_for_tests();
let (recovery_subsystem_tx, mut recovery_subsystem_rx) =
AvailabilityRecoverySubsystemHandle::new();
let recovery_delay_range =
RecoveryDelayRange { min: Duration::from_millis(0), max: Duration::from_millis(10) };
let (_explicit_recovery_chan_tx, explicit_recovery_chan_rx) = mpsc::channel(10);
let candidates = make_candidate_chain(1..2);
let header = Header::decode(&mut &candidates[0].commitments.head_data.0[..]).unwrap();
let candidate_hash = candidates[0].hash();
let relay_chain_client = Relaychain::new(vec![(
PHeader {
parent_hash: PHash::from_low_u64_be(0),
number: 1,
state_root: PHash::random(),
extrinsics_root: PHash::random(),
digest: Default::default(),
},
candidates,
)]);
let mut known_blocks = HashMap::new();
known_blocks.insert(GENESIS_HASH, BlockStatus::InChainWithState);
let (teyrchain_client, _import_notifications_tx, _finality_notifications_tx) =
TeyrchainClient::new(vec![dummy_usage_info(0)], Arc::new(Mutex::new(known_blocks)));
let (teyrchain_import_queue, mut import_requests_rx) = TeyrchainImportQueue::new();
let pov_recovery = PoVRecovery::<Block, _, _>::new(
Box::new(recovery_subsystem_tx),
recovery_delay_range,
Arc::new(teyrchain_client),
Box::new(teyrchain_import_queue),
relay_chain_client,
ParaId::new(1000),
explicit_recovery_chan_rx,
Arc::new(DummySyncOracle::default()),
);
task::spawn(pov_recovery.run());
// First recovery fails.
assert_matches!(
recovery_subsystem_rx.next().await,
Some(AvailabilityRecoveryMessage::RecoverAvailableData(
receipt,
session_index,
None,
None,
response_tx
)) => {
assert_eq!(receipt.hash(), candidate_hash);
assert_eq!(session_index, TEST_SESSION_INDEX);
response_tx.send(
Err(RecoveryError::Unavailable)
).unwrap()
}
);
// Candidate is not imported.
assert_matches!(import_requests_rx.next().timeout(Duration::from_millis(100)).await, None);
// Recovery is retried and it succeeds now.
assert_matches!(
recovery_subsystem_rx.next().await,
Some(AvailabilityRecoveryMessage::RecoverAvailableData(
receipt,
session_index,
None,
None,
response_tx
)) => {
assert_eq!(receipt.hash(), candidate_hash);
assert_eq!(session_index, TEST_SESSION_INDEX);
response_tx.send(
Ok(
AvailableData {
pov: Arc::new(PoV {
block_data: TeyrchainBlockData::<Block>::new(
vec![Block::new(header.clone(), Vec::new())], CompactProof { encoded_nodes: vec![] }
).encode().into()
}),
validation_data: dummy_pvd(),
}
)
).unwrap()
}
);
// No more recovery messages received.
assert_matches!(recovery_subsystem_rx.next().timeout(Duration::from_millis(100)).await, None);
// Received import request for the recovered candidate
assert_matches!(import_requests_rx.next().await, Some(incoming_blocks) => {
assert_eq!(incoming_blocks.len(), 1);
assert_eq!(incoming_blocks[0].header, Some(header));
});
// No import request received
assert_matches!(import_requests_rx.next().timeout(Duration::from_millis(100)).await, None);
}
#[tokio::test]
async fn single_pending_candidate_recovery_retry_fails() {
sp_tracing::init_for_tests();
let (recovery_subsystem_tx, mut recovery_subsystem_rx) =
AvailabilityRecoverySubsystemHandle::new();
let recovery_delay_range =
RecoveryDelayRange { min: Duration::from_millis(0), max: Duration::from_millis(10) };
let (_explicit_recovery_chan_tx, explicit_recovery_chan_rx) = mpsc::channel(10);
let candidates = make_candidate_chain(1..2);
let candidate_hash = candidates[0].hash();
let relay_chain_client = Relaychain::new(vec![(
PHeader {
parent_hash: PHash::from_low_u64_be(0),
number: 1,
state_root: PHash::random(),
extrinsics_root: PHash::random(),
digest: Default::default(),
},
candidates,
)]);
let mut known_blocks = HashMap::new();
known_blocks.insert(GENESIS_HASH, BlockStatus::InChainWithState);
let (teyrchain_client, _import_notifications_tx, _finality_notifications_tx) =
TeyrchainClient::new(vec![dummy_usage_info(0)], Arc::new(Mutex::new(known_blocks)));
let (teyrchain_import_queue, mut import_requests_rx) = TeyrchainImportQueue::new();
let pov_recovery = PoVRecovery::<Block, _, _>::new(
Box::new(recovery_subsystem_tx),
recovery_delay_range,
Arc::new(teyrchain_client),
Box::new(teyrchain_import_queue),
relay_chain_client,
ParaId::new(1000),
explicit_recovery_chan_rx,
Arc::new(DummySyncOracle::default()),
);
task::spawn(pov_recovery.run());
// First recovery fails.
assert_matches!(
recovery_subsystem_rx.next().await,
Some(AvailabilityRecoveryMessage::RecoverAvailableData(
receipt,
session_index,
None,
None,
response_tx
)) => {
assert_eq!(receipt.hash(), candidate_hash);
assert_eq!(session_index, TEST_SESSION_INDEX);
response_tx.send(
Err(RecoveryError::Unavailable)
).unwrap()
}
);
// Candidate is not imported.
assert_matches!(import_requests_rx.next().timeout(Duration::from_millis(100)).await, None);
// Second retry fails.
assert_matches!(
recovery_subsystem_rx.next().await,
Some(AvailabilityRecoveryMessage::RecoverAvailableData(
receipt,
session_index,
None,
None,
response_tx
)) => {
assert_eq!(receipt.hash(), candidate_hash);
assert_eq!(session_index, TEST_SESSION_INDEX);
response_tx.send(
Err(RecoveryError::Unavailable)
).unwrap()
}
);
// Candidate is not imported.
assert_matches!(import_requests_rx.next().timeout(Duration::from_millis(100)).await, None);
// After the second attempt, give up.
// No more recovery messages received.
assert_matches!(recovery_subsystem_rx.next().timeout(Duration::from_millis(100)).await, None);
}
#[tokio::test]
async fn single_pending_candidate_recovery_irrecoverable_error() {
sp_tracing::init_for_tests();
let (recovery_subsystem_tx, mut recovery_subsystem_rx) =
AvailabilityRecoverySubsystemHandle::new();
let recovery_delay_range =
RecoveryDelayRange { min: Duration::from_millis(0), max: Duration::from_millis(10) };
let (_explicit_recovery_chan_tx, explicit_recovery_chan_rx) = mpsc::channel(10);
let candidates = make_candidate_chain(1..2);
let candidate_hash = candidates[0].hash();
let relay_chain_client = Relaychain::new(vec![(
PHeader {
parent_hash: PHash::from_low_u64_be(0),
number: 1,
state_root: PHash::random(),
extrinsics_root: PHash::random(),
digest: Default::default(),
},
candidates,
)]);
let mut known_blocks = HashMap::new();
known_blocks.insert(GENESIS_HASH, BlockStatus::InChainWithState);
let (teyrchain_client, _import_notifications_tx, _finality_notifications_tx) =
TeyrchainClient::new(vec![dummy_usage_info(0)], Arc::new(Mutex::new(known_blocks)));
let (teyrchain_import_queue, mut import_requests_rx) = TeyrchainImportQueue::new();
let pov_recovery = PoVRecovery::<Block, _, _>::new(
Box::new(recovery_subsystem_tx),
recovery_delay_range,
Arc::new(teyrchain_client),
Box::new(teyrchain_import_queue),
relay_chain_client,
ParaId::new(1000),
explicit_recovery_chan_rx,
Arc::new(DummySyncOracle::default()),
);
task::spawn(pov_recovery.run());
// Recovery succeeds but the block data is wrong. Will not be retried.
assert_matches!(
recovery_subsystem_rx.next().await,
Some(AvailabilityRecoveryMessage::RecoverAvailableData(
receipt,
session_index,
None,
None,
response_tx
)) => {
assert_eq!(receipt.hash(), candidate_hash);
assert_eq!(session_index, TEST_SESSION_INDEX);
response_tx.send(
Ok(
AvailableData {
pov: Arc::new(PoV {
// Empty block data. It will fail to decode.
block_data: vec![].into()
}),
validation_data: dummy_pvd(),
}
)
).unwrap()
}
);
// Candidate is not imported.
assert_matches!(import_requests_rx.next().timeout(Duration::from_millis(100)).await, None);
// No more recovery messages received.
assert_matches!(recovery_subsystem_rx.next().timeout(Duration::from_millis(100)).await, None);
}
#[tokio::test]
async fn pending_candidates_recovery_skipped_while_syncing() {
sp_tracing::init_for_tests();
let (recovery_subsystem_tx, mut recovery_subsystem_rx) =
AvailabilityRecoverySubsystemHandle::new();
let recovery_delay_range =
RecoveryDelayRange { min: Duration::from_millis(0), max: Duration::from_millis(10) };
let (_explicit_recovery_chan_tx, explicit_recovery_chan_rx) = mpsc::channel(10);
let candidates = make_candidate_chain(1..4);
let relay_chain_client = Relaychain::new(vec![(
PHeader {
parent_hash: PHash::from_low_u64_be(0),
number: 1,
state_root: PHash::random(),
extrinsics_root: PHash::random(),
digest: Default::default(),
},
candidates,
)]);
let mut known_blocks = HashMap::new();
known_blocks.insert(GENESIS_HASH, BlockStatus::InChainWithState);
let (teyrchain_client, _import_notifications_tx, _finality_notifications_tx) =
TeyrchainClient::new(vec![dummy_usage_info(0)], Arc::new(Mutex::new(known_blocks)));
let (teyrchain_import_queue, mut import_requests_rx) = TeyrchainImportQueue::new();
let pov_recovery = PoVRecovery::<Block, _, _>::new(
Box::new(recovery_subsystem_tx),
recovery_delay_range,
Arc::new(teyrchain_client),
Box::new(teyrchain_import_queue),
relay_chain_client,
ParaId::new(1000),
explicit_recovery_chan_rx,
Arc::new(DummySyncOracle::new(true)),
);
task::spawn(pov_recovery.run());
// No recovery messages received.
assert_matches!(recovery_subsystem_rx.next().timeout(Duration::from_millis(100)).await, None);
// No candidate is imported.
assert_matches!(import_requests_rx.next().timeout(Duration::from_millis(100)).await, None);
}
#[tokio::test]
async fn candidate_is_imported_while_awaiting_recovery() {
sp_tracing::init_for_tests();
let (recovery_subsystem_tx, mut recovery_subsystem_rx) =
AvailabilityRecoverySubsystemHandle::new();
let recovery_delay_range =
RecoveryDelayRange { min: Duration::from_millis(0), max: Duration::from_millis(10) };
let (_explicit_recovery_chan_tx, explicit_recovery_chan_rx) = mpsc::channel(10);
let candidates = make_candidate_chain(1..2);
let header = Header::decode(&mut &candidates[0].commitments.head_data.0[..]).unwrap();
let candidate_hash = candidates[0].hash();
let relay_chain_client = Relaychain::new(vec![(
PHeader {
parent_hash: PHash::from_low_u64_be(0),
number: 1,
state_root: PHash::random(),
extrinsics_root: PHash::random(),
digest: Default::default(),
},
candidates,
)]);
let mut known_blocks = HashMap::new();
known_blocks.insert(GENESIS_HASH, BlockStatus::InChainWithState);
let (teyrchain_client, import_notifications_tx, _finality_notifications_tx) =
TeyrchainClient::new(vec![dummy_usage_info(0)], Arc::new(Mutex::new(known_blocks)));
let (teyrchain_import_queue, mut import_requests_rx) = TeyrchainImportQueue::new();
let pov_recovery = PoVRecovery::<Block, _, _>::new(
Box::new(recovery_subsystem_tx),
recovery_delay_range,
Arc::new(teyrchain_client),
Box::new(teyrchain_import_queue),
relay_chain_client,
ParaId::new(1000),
explicit_recovery_chan_rx,
Arc::new(DummySyncOracle::default()),
);
task::spawn(pov_recovery.run());
let recovery_response_tx;
assert_matches!(
recovery_subsystem_rx.next().await,
Some(AvailabilityRecoveryMessage::RecoverAvailableData(
receipt,
session_index,
None,
None,
response_tx
)) => {
assert_eq!(receipt.hash(), candidate_hash);
assert_eq!(session_index, TEST_SESSION_INDEX);
recovery_response_tx = response_tx;
}
);
// While candidate is pending recovery, import the candidate from external source.
let (unpin_sender, _unpin_receiver) = sc_utils::mpsc::tracing_unbounded("test_unpin", 10);
import_notifications_tx
.unbounded_send(BlockImportNotification::new(
header.hash(),
BlockOrigin::ConsensusBroadcast,
header.clone(),
false,
None,
unpin_sender,
))
.unwrap();
recovery_response_tx
.send(Ok(AvailableData {
pov: Arc::new(PoV {
block_data: TeyrchainBlockData::<Block>::new(
vec![Block::new(header.clone(), vec![])],
CompactProof { encoded_nodes: vec![] },
)
.encode()
.into(),
}),
validation_data: dummy_pvd(),
}))
.unwrap();
// Received import request for the recovered candidate. This could be optimised to not trigger a
// reimport.
assert_matches!(import_requests_rx.next().await, Some(incoming_blocks) => {
assert_eq!(incoming_blocks.len(), 1);
assert_eq!(incoming_blocks[0].header, Some(header));
});
// No more recovery messages received.
assert_matches!(recovery_subsystem_rx.next().timeout(Duration::from_millis(100)).await, None);
// No more import requests received
assert_matches!(import_requests_rx.next().timeout(Duration::from_millis(100)).await, None);
}
#[tokio::test]
async fn candidate_is_finalized_while_awaiting_recovery() {
sp_tracing::init_for_tests();
let (recovery_subsystem_tx, mut recovery_subsystem_rx) =
AvailabilityRecoverySubsystemHandle::new();
let recovery_delay_range =
RecoveryDelayRange { min: Duration::from_millis(0), max: Duration::from_millis(10) };
let (_explicit_recovery_chan_tx, explicit_recovery_chan_rx) = mpsc::channel(10);
let candidates = make_candidate_chain(1..2);
let header = Header::decode(&mut &candidates[0].commitments.head_data.0[..]).unwrap();
let candidate_hash = candidates[0].hash();
let relay_chain_client = Relaychain::new(vec![(
PHeader {
parent_hash: PHash::from_low_u64_be(0),
number: 1,
state_root: PHash::random(),
extrinsics_root: PHash::random(),
digest: Default::default(),
},
candidates,
)]);
let mut known_blocks = HashMap::new();
known_blocks.insert(GENESIS_HASH, BlockStatus::InChainWithState);
let (teyrchain_client, _import_notifications_tx, finality_notifications_tx) =
TeyrchainClient::new(vec![dummy_usage_info(0)], Arc::new(Mutex::new(known_blocks)));
let (teyrchain_import_queue, mut import_requests_rx) = TeyrchainImportQueue::new();
let pov_recovery = PoVRecovery::<Block, _, _>::new(
Box::new(recovery_subsystem_tx),
recovery_delay_range,
Arc::new(teyrchain_client),
Box::new(teyrchain_import_queue),
relay_chain_client,
ParaId::new(1000),
explicit_recovery_chan_rx,
Arc::new(DummySyncOracle::default()),
);
task::spawn(pov_recovery.run());
let recovery_response_tx;
assert_matches!(
recovery_subsystem_rx.next().await,
Some(AvailabilityRecoveryMessage::RecoverAvailableData(
receipt,
session_index,
None,
None,
response_tx
)) => {
assert_eq!(receipt.hash(), candidate_hash);
assert_eq!(session_index, TEST_SESSION_INDEX);
// save it for later.
recovery_response_tx = response_tx;
}
);
// While candidate is pending recovery, it gets finalized.
let (unpin_sender, _unpin_receiver) = sc_utils::mpsc::tracing_unbounded("test_unpin", 10);
finality_notifications_tx
.unbounded_send(FinalityNotification::from_summary(
FinalizeSummary { header: header.clone(), finalized: vec![], stale_blocks: vec![] },
unpin_sender,
))
.unwrap();
recovery_response_tx
.send(Ok(AvailableData {
pov: Arc::new(PoV {
block_data: TeyrchainBlockData::<Block>::new(
vec![Block::new(header.clone(), vec![])],
CompactProof { encoded_nodes: vec![] },
)
.encode()
.into(),
}),
validation_data: dummy_pvd(),
}))
.unwrap();
// No more recovery messages received.
assert_matches!(recovery_subsystem_rx.next().timeout(Duration::from_millis(100)).await, None);
// candidate is imported
assert_matches!(import_requests_rx.next().await, Some(incoming_blocks) => {
assert_eq!(incoming_blocks.len(), 1);
assert_eq!(incoming_blocks[0].header, Some(header));
});
// No more import requests received
assert_matches!(import_requests_rx.next().timeout(Duration::from_millis(100)).await, None);
}
#[tokio::test]
async fn chained_recovery_success() {
sp_tracing::init_for_tests();
let (recovery_subsystem_tx, mut recovery_subsystem_rx) =
AvailabilityRecoverySubsystemHandle::new();
let recovery_delay_range =
RecoveryDelayRange { min: Duration::from_millis(0), max: Duration::from_millis(0) };
let (_explicit_recovery_chan_tx, explicit_recovery_chan_rx) = mpsc::channel(10);
let candidates = make_candidate_chain(1..4);
let headers = candidates
.iter()
.map(|candidate| Header::decode(&mut &candidate.commitments.head_data.0[..]).unwrap())
.collect::<Vec<_>>();
let candidate_hashes = candidates.iter().map(|candidate| candidate.hash()).collect::<Vec<_>>();
let relay_chain_client = Relaychain::new(vec![(
PHeader {
parent_hash: PHash::from_low_u64_be(0),
number: 1,
state_root: PHash::random(),
extrinsics_root: PHash::random(),
digest: Default::default(),
},
// 3 pending candidates
candidates,
)]);
let mut known_blocks = HashMap::new();
known_blocks.insert(GENESIS_HASH, BlockStatus::InChainWithState);
let known_blocks = Arc::new(Mutex::new(known_blocks));
let (teyrchain_client, import_notifications_tx, _finality_notifications_tx) =
TeyrchainClient::new(vec![dummy_usage_info(0)], known_blocks.clone());
let (teyrchain_import_queue, mut import_requests_rx) = TeyrchainImportQueue::new();
let pov_recovery = PoVRecovery::<Block, _, _>::new(
Box::new(recovery_subsystem_tx),
recovery_delay_range,
Arc::new(teyrchain_client),
Box::new(teyrchain_import_queue),
relay_chain_client,
ParaId::new(1000),
explicit_recovery_chan_rx,
Arc::new(DummySyncOracle::default()),
);
task::spawn(pov_recovery.run());
// Candidates are recovered in the right order.
for (candidate_hash, header) in candidate_hashes.into_iter().zip(headers.into_iter()) {
assert_matches!(
recovery_subsystem_rx.next().await,
Some(AvailabilityRecoveryMessage::RecoverAvailableData(
receipt,
session_index,
None,
None,
response_tx
)) => {
assert_eq!(receipt.hash(), candidate_hash);
assert_eq!(session_index, TEST_SESSION_INDEX);
response_tx
.send(Ok(AvailableData {
pov: Arc::new(PoV {
block_data: TeyrchainBlockData::<Block>::new(
vec![Block::new(header.clone(), vec![])], CompactProof { encoded_nodes: vec![] }
)
.encode()
.into(),
}),
validation_data: dummy_pvd(),
}))
.unwrap();
}
);
assert_matches!(import_requests_rx.next().await, Some(incoming_blocks) => {
assert_eq!(incoming_blocks.len(), 1);
assert_eq!(incoming_blocks[0].header, Some(header.clone()));
});
known_blocks
.lock()
.expect("Poisoned lock")
.insert(header.hash(), BlockStatus::InChainWithState);
let (unpin_sender, _unpin_receiver) = sc_utils::mpsc::tracing_unbounded("test_unpin", 10);
import_notifications_tx
.unbounded_send(BlockImportNotification::new(
header.hash(),
BlockOrigin::ConsensusBroadcast,
header,
false,
None,
unpin_sender,
))
.unwrap();
}
// No more recovery messages received.
assert_matches!(recovery_subsystem_rx.next().timeout(Duration::from_millis(100)).await, None);
// No more import requests received
assert_matches!(import_requests_rx.next().timeout(Duration::from_millis(100)).await, None);
}
#[tokio::test]
async fn chained_recovery_child_succeeds_before_parent() {
sp_tracing::init_for_tests();
let (recovery_subsystem_tx, mut recovery_subsystem_rx) =
AvailabilityRecoverySubsystemHandle::new();
let recovery_delay_range =
RecoveryDelayRange { min: Duration::from_millis(0), max: Duration::from_millis(0) };
let (_explicit_recovery_chan_tx, explicit_recovery_chan_rx) = mpsc::channel(10);
let candidates = make_candidate_chain(1..3);
let headers = candidates
.iter()
.map(|candidate| Header::decode(&mut &candidate.commitments.head_data.0[..]).unwrap())
.collect::<Vec<_>>();
let candidate_hashes = candidates.iter().map(|candidate| candidate.hash()).collect::<Vec<_>>();
let relay_chain_client = Relaychain::new(vec![(
PHeader {
parent_hash: PHash::from_low_u64_be(0),
number: 1,
state_root: PHash::random(),
extrinsics_root: PHash::random(),
digest: Default::default(),
},
// 2 pending candidates
candidates,
)]);
let mut known_blocks = HashMap::new();
known_blocks.insert(GENESIS_HASH, BlockStatus::InChainWithState);
let known_blocks = Arc::new(Mutex::new(known_blocks));
let (teyrchain_client, _import_notifications_tx, _finality_notifications_tx) =
TeyrchainClient::new(vec![dummy_usage_info(0)], known_blocks.clone());
let (teyrchain_import_queue, mut import_requests_rx) = TeyrchainImportQueue::new();
let pov_recovery = PoVRecovery::<Block, _, _>::new(
Box::new(recovery_subsystem_tx),
recovery_delay_range,
Arc::new(teyrchain_client),
Box::new(teyrchain_import_queue),
relay_chain_client,
ParaId::new(1000),
explicit_recovery_chan_rx,
Arc::new(DummySyncOracle::default()),
);
task::spawn(pov_recovery.run());
let mut recovery_responses_senders = vec![];
for candidate_hash in candidate_hashes.iter() {
assert_matches!(
recovery_subsystem_rx.next().await,
Some(AvailabilityRecoveryMessage::RecoverAvailableData(
receipt,
session_index,
None,
None,
response_tx
)) => {
assert_eq!(receipt.hash(), *candidate_hash);
assert_eq!(session_index, TEST_SESSION_INDEX);
recovery_responses_senders.push(response_tx);
}
);
}
// Send out the responses in reverse order.
for (recovery_response_sender, header) in
recovery_responses_senders.into_iter().zip(headers.iter()).rev()
{
recovery_response_sender
.send(Ok(AvailableData {
pov: Arc::new(PoV {
block_data: TeyrchainBlockData::<Block>::new(
vec![Block::new(header.clone(), vec![])],
CompactProof { encoded_nodes: vec![] },
)
.encode()
.into(),
}),
validation_data: dummy_pvd(),
}))
.unwrap();
}
assert_matches!(import_requests_rx.next().await, Some(incoming_blocks) => {
// The two import requests will be batched.
assert_eq!(incoming_blocks.len(), 2);
assert_eq!(incoming_blocks[0].header, Some(headers[0].clone()));
assert_eq!(incoming_blocks[1].header, Some(headers[1].clone()));
});
// No more recovery messages received.
assert_matches!(recovery_subsystem_rx.next().timeout(Duration::from_millis(100)).await, None);
// No more import requests received
assert_matches!(import_requests_rx.next().timeout(Duration::from_millis(100)).await, None);
}
#[tokio::test]
async fn recovery_multiple_blocks_per_candidate() {
sp_tracing::init_for_tests();
let (recovery_subsystem_tx, mut recovery_subsystem_rx) =
AvailabilityRecoverySubsystemHandle::new();
let recovery_delay_range =
RecoveryDelayRange { min: Duration::from_millis(0), max: Duration::from_millis(0) };
let (_explicit_recovery_chan_tx, explicit_recovery_chan_rx) = mpsc::channel(10);
let candidates = make_candidate_chain(1..4);
let candidate = candidates.last().unwrap();
let headers = candidates
.iter()
.map(|c| Header::decode(&mut &c.commitments.head_data.0[..]).unwrap())
.collect::<Vec<_>>();
let header = headers.last().unwrap();
let relay_chain_client = Relaychain::new(vec![(
PHeader {
parent_hash: PHash::from_low_u64_be(0),
number: 1,
state_root: PHash::random(),
extrinsics_root: PHash::random(),
digest: Default::default(),
},
vec![candidate.clone()],
)]);
let mut known_blocks = HashMap::new();
known_blocks.insert(GENESIS_HASH, BlockStatus::InChainWithState);
let known_blocks = Arc::new(Mutex::new(known_blocks));
let (teyrchain_client, import_notifications_tx, _finality_notifications_tx) =
TeyrchainClient::new(vec![dummy_usage_info(0)], known_blocks.clone());
let (teyrchain_import_queue, mut import_requests_rx) = TeyrchainImportQueue::new();
let pov_recovery = PoVRecovery::<Block, _, _>::new(
Box::new(recovery_subsystem_tx),
recovery_delay_range,
Arc::new(teyrchain_client),
Box::new(teyrchain_import_queue),
relay_chain_client,
ParaId::new(1000),
explicit_recovery_chan_rx,
Arc::new(DummySyncOracle::default()),
);
task::spawn(pov_recovery.run());
// Candidates are recovered in the right order.
assert_matches!(
recovery_subsystem_rx.next().await,
Some(AvailabilityRecoveryMessage::RecoverAvailableData(
receipt,
session_index,
None,
None,
response_tx
)) => {
assert_eq!(receipt.hash(), candidate.hash());
assert_eq!(session_index, TEST_SESSION_INDEX);
response_tx
.send(Ok(AvailableData {
pov: Arc::new(PoV {
block_data: TeyrchainBlockData::<Block>::new(
headers.iter().map(|h| Block::new(h.clone(), vec![])).collect(),
CompactProof { encoded_nodes: vec![] },
)
.encode()
.into(),
}),
validation_data: dummy_pvd(),
}))
.unwrap();
}
);
assert_matches!(import_requests_rx.next().await, Some(incoming_blocks) => {
assert_eq!(incoming_blocks.len(), 3);
assert_eq!(incoming_blocks.iter().map(|b| b.header.clone().unwrap()).collect::<Vec<_>>(), headers);
});
known_blocks
.lock()
.expect("Poisoned lock")
.insert(header.hash(), BlockStatus::InChainWithState);
let (unpin_sender, _unpin_receiver) = sc_utils::mpsc::tracing_unbounded("test_unpin", 10);
import_notifications_tx
.unbounded_send(BlockImportNotification::new(
header.hash(),
BlockOrigin::ConsensusBroadcast,
header.clone(),
false,
None,
unpin_sender,
))
.unwrap();
// No more recovery messages received.
assert_matches!(recovery_subsystem_rx.next().timeout(Duration::from_millis(100)).await, None);
// No more import requests received
assert_matches!(import_requests_rx.next().timeout(Duration::from_millis(100)).await, None);
}