use std::{iter, sync::Arc, time::Duration};
use futures::stream::StreamExt;
use zebra_chain::{
block::{Block, Height},
chain_tip::mock::MockChainTip,
serialization::ZcashDeserializeInto,
};
use zebra_network::{InventoryResponse, PeerSocketAddr};
use zebra_state::MAX_BLOCK_REORG_HEIGHT;
use zebra_test::mock_service::{MockService, PanicAssertion};
use zebra_network as zn;
use zebra_state as zs;
use super::{DownloadAction, Downloads, HeightLimitError, MIN_CONCURRENCY_LIMIT};
use InventoryResponse::*;
const MAX_SERVICE_REQUEST_DELAY: Duration = Duration::from_millis(1000);
const ADVERTISER: &str = "127.0.0.1:8233";
type MockNetwork = MockService<zn::Request, zn::Response, PanicAssertion>;
type MockVerifier = MockService<zebra_consensus::Request, zebra_chain::block::Hash, PanicAssertion>;
type MockState = MockService<zs::Request, zs::Response, PanicAssertion>;
#[allow(clippy::type_complexity)]
fn mock_downloads(
tip_height: Height,
) -> (
Downloads<MockNetwork, MockVerifier, MockState, MockChainTip>,
MockNetwork,
MockVerifier,
MockState,
) {
let network: MockNetwork = MockService::build()
.with_max_request_delay(MAX_SERVICE_REQUEST_DELAY)
.for_unit_tests();
let verifier: MockVerifier = MockService::build()
.with_max_request_delay(MAX_SERVICE_REQUEST_DELAY)
.for_unit_tests();
let state: MockState = MockService::build()
.with_max_request_delay(MAX_SERVICE_REQUEST_DELAY)
.for_unit_tests();
let (latest_chain_tip, chain_tip_sender) = MockChainTip::new();
chain_tip_sender.send_best_tip_height(tip_height);
let downloads = Downloads::new(
MIN_CONCURRENCY_LIMIT,
network.clone(),
verifier.clone(),
state.clone(),
latest_chain_tip,
);
(downloads, network, verifier, state)
}
fn block_1() -> Arc<Block> {
zebra_test::vectors::BLOCK_MAINNET_1_BYTES
.zcash_deserialize_into()
.expect("hard-coded block vector deserializes")
}
fn block_2() -> Arc<Block> {
zebra_test::vectors::BLOCK_MAINNET_2_BYTES
.zcash_deserialize_into()
.expect("hard-coded block vector deserializes")
}
fn genesis() -> Arc<Block> {
zebra_test::vectors::BLOCK_MAINNET_GENESIS_BYTES
.zcash_deserialize_into()
.expect("hard-coded block vector deserializes")
}
fn block_10() -> Arc<Block> {
zebra_test::vectors::BLOCK_MAINNET_10_BYTES
.zcash_deserialize_into()
.expect("hard-coded block vector deserializes")
}
fn lookahead_boundary_tip() -> Height {
Height(2 - u32::try_from(MIN_CONCURRENCY_LIMIT).expect("small constant fits in u32"))
}
#[tokio::test]
async fn contradicted_behind_tip_height_is_attributed_and_never_verified() {
let _init_guard = zebra_test::init();
let (mut downloads, mut network, mut verifier, mut state) =
mock_downloads(Height(2 * MAX_BLOCK_REORG_HEIGHT));
let block_1 = block_1();
let block_2 = block_2();
let block = Arc::new(Block {
header: block_2.header.clone(),
transactions: block_1.transactions.clone(),
});
let hash = block.hash();
assert_eq!(
hash,
block_2.hash(),
"the block hash covers only the header"
);
assert_eq!(
block.coinbase_height(),
Some(Height(1)),
"the body must claim the height the downloader reads"
);
let advertiser: PeerSocketAddr = ADVERTISER.parse().expect("hard-coded address is valid");
assert!(
matches!(
downloads.download_and_verify(hash, Some(advertiser)),
DownloadAction::AddedToQueue
),
"download is queued"
);
state
.expect_request(zs::Request::KnownBlock(hash))
.await
.respond(zs::Response::KnownBlock(None));
network
.expect_request(zn::Request::BlocksByHash(iter::once(hash).collect()))
.await
.respond(zn::Response::Blocks(vec![Available((
block.clone(),
Some(advertiser),
))]));
state
.expect_request(zs::Request::BlockHeader(
block.header.previous_block_hash.into(),
))
.await
.respond(zs::Response::BlockHeader {
header: block_1.header.clone(),
hash: block_1.hash(),
height: Height(1),
next_block_hash: Some(hash),
});
let (error, advertiser_addr) = downloads
.next()
.await
.expect("downloads is non-empty")
.expect_err("block behind the reorg limit is dropped");
assert!(
matches!(
error.downcast_ref::<HeightLimitError>(),
Some(HeightLimitError::BehindTip { .. })
),
"a contradicted behind-tip height must be a scoreable typed error, but was: {error:?}"
);
assert_eq!(
advertiser_addr,
Some(advertiser),
"a contradicted behind-tip height must attribute the drop to the supplying peer"
);
verifier.expect_no_requests().await;
}
#[tokio::test]
async fn genuinely_old_block_is_dropped_without_attribution() {
let _init_guard = zebra_test::init();
let (mut downloads, mut network, mut verifier, mut state) =
mock_downloads(Height(2 * MAX_BLOCK_REORG_HEIGHT));
let block = block_1();
let hash = block.hash();
let advertiser: PeerSocketAddr = ADVERTISER.parse().expect("hard-coded address is valid");
assert!(
matches!(
downloads.download_and_verify(hash, Some(advertiser)),
DownloadAction::AddedToQueue
),
"download is queued"
);
state
.expect_request(zs::Request::KnownBlock(hash))
.await
.respond(zs::Response::KnownBlock(None));
network
.expect_request(zn::Request::BlocksByHash(iter::once(hash).collect()))
.await
.respond(zn::Response::Blocks(vec![Available((
block.clone(),
Some(advertiser),
))]));
let genesis = genesis();
assert_eq!(
block.header.previous_block_hash,
genesis.hash(),
"block 1's parent is genesis"
);
state
.expect_request(zs::Request::BlockHeader(
block.header.previous_block_hash.into(),
))
.await
.respond(zs::Response::BlockHeader {
header: genesis.header.clone(),
hash: genesis.hash(),
height: Height(0),
next_block_hash: Some(hash),
});
let (error, advertiser_addr) = downloads
.next()
.await
.expect("downloads is non-empty")
.expect_err("block behind the reorg limit is dropped");
assert!(
matches!(
error.downcast_ref::<HeightLimitError>(),
Some(HeightLimitError::BehindTip { .. })
),
"an old block must still be a typed behind-tip error, but was: {error:?}"
);
assert_eq!(
advertiser_addr, None,
"an authentic old block must not be attributed to its peer"
);
verifier.expect_no_requests().await;
}
#[tokio::test]
async fn behind_tip_block_with_unknown_parent_is_not_attributed() {
let _init_guard = zebra_test::init();
let (mut downloads, mut network, mut verifier, mut state) =
mock_downloads(Height(2 * MAX_BLOCK_REORG_HEIGHT));
let block = block_1();
let hash = block.hash();
let advertiser: PeerSocketAddr = ADVERTISER.parse().expect("hard-coded address is valid");
assert!(
matches!(
downloads.download_and_verify(hash, Some(advertiser)),
DownloadAction::AddedToQueue
),
"download is queued"
);
state
.expect_request(zs::Request::KnownBlock(hash))
.await
.respond(zs::Response::KnownBlock(None));
network
.expect_request(zn::Request::BlocksByHash(iter::once(hash).collect()))
.await
.respond(zn::Response::Blocks(vec![Available((
block.clone(),
Some(advertiser),
))]));
state
.expect_request(zs::Request::BlockHeader(
block.header.previous_block_hash.into(),
))
.await
.respond(Err(zn::BoxError::from("block not found in any chain")));
let (error, advertiser_addr) = downloads
.next()
.await
.expect("downloads is non-empty")
.expect_err("block behind the reorg limit is dropped");
assert!(
matches!(
error.downcast_ref::<HeightLimitError>(),
Some(HeightLimitError::BehindTip { .. })
),
"a behind-tip drop must be a typed error, but was: {error:?}"
);
assert_eq!(
advertiser_addr, None,
"a block whose parent we do not hold must not be attributed"
);
verifier.expect_no_requests().await;
}
#[tokio::test(start_paused = true)]
async fn behind_tip_parent_lookup_timeout_is_not_attributed() {
let _init_guard = zebra_test::init();
let (mut downloads, mut network, mut verifier, mut state) =
mock_downloads(Height(2 * MAX_BLOCK_REORG_HEIGHT));
let block = block_1();
let hash = block.hash();
let advertiser: PeerSocketAddr = ADVERTISER.parse().expect("hard-coded address is valid");
assert!(
matches!(
downloads.download_and_verify(hash, Some(advertiser)),
DownloadAction::AddedToQueue
),
"download is queued"
);
state
.expect_request(zs::Request::KnownBlock(hash))
.await
.respond(zs::Response::KnownBlock(None));
network
.expect_request(zn::Request::BlocksByHash(iter::once(hash).collect()))
.await
.respond(zn::Response::Blocks(vec![Available((
block.clone(),
Some(advertiser),
))]));
let (error, advertiser_addr) = downloads
.next()
.await
.expect("downloads is non-empty")
.expect_err("block behind the reorg limit is dropped");
assert!(
matches!(
error.downcast_ref::<HeightLimitError>(),
Some(HeightLimitError::BehindTip { .. })
),
"a timed-out lookup must still drop with a typed error, but was: {error:?}"
);
assert_eq!(
advertiser_addr, None,
"a timed-out parent lookup must not attribute the drop"
);
verifier.expect_no_requests().await;
}
#[tokio::test]
async fn block_at_reorg_boundary_is_verified_not_dropped() {
let _init_guard = zebra_test::init();
let (mut downloads, mut network, mut verifier, mut state) =
mock_downloads(Height(MAX_BLOCK_REORG_HEIGHT + 1));
let block = block_1();
let hash = block.hash();
let advertiser: PeerSocketAddr = ADVERTISER.parse().expect("hard-coded address is valid");
assert!(
matches!(
downloads.download_and_verify(hash, Some(advertiser)),
DownloadAction::AddedToQueue
),
"download is queued"
);
state
.expect_request(zs::Request::KnownBlock(hash))
.await
.respond(zs::Response::KnownBlock(None));
network
.expect_request(zn::Request::BlocksByHash(iter::once(hash).collect()))
.await
.respond(zn::Response::Blocks(vec![Available((
block.clone(),
Some(advertiser),
))]));
verifier
.expect_request(zebra_consensus::Request::Commit(block))
.await
.respond(hash);
assert_eq!(
downloads
.next()
.await
.expect("downloads is non-empty")
.expect("block on the reorg boundary is verified"),
hash,
"a block at min_accepted_height must be verified, not dropped as behind the tip"
);
state.expect_no_requests().await;
}
#[tokio::test]
async fn contradicted_far_ahead_height_is_attributed_and_never_verified() {
let _init_guard = zebra_test::init();
let (mut downloads, mut network, mut verifier, mut state) =
mock_downloads(lookahead_boundary_tip());
let block_2 = block_2();
let block_10 = block_10();
let block = Arc::new(Block {
header: block_2.header.clone(),
transactions: block_10.transactions.clone(),
});
let hash = block.hash();
assert_eq!(
hash,
block_2.hash(),
"the block hash covers only the header"
);
assert_eq!(
block.coinbase_height(),
Some(Height(10)),
"the body must claim the height the downloader reads"
);
let advertiser: PeerSocketAddr = ADVERTISER.parse().expect("hard-coded address is valid");
assert!(
matches!(
downloads.download_and_verify(hash, Some(advertiser)),
DownloadAction::AddedToQueue
),
"download is queued"
);
state
.expect_request(zs::Request::KnownBlock(hash))
.await
.respond(zs::Response::KnownBlock(None));
network
.expect_request(zn::Request::BlocksByHash(iter::once(hash).collect()))
.await
.respond(zn::Response::Blocks(vec![Available((
block.clone(),
Some(advertiser),
))]));
let block_1 = block_1();
state
.expect_request(zs::Request::BlockHeader(
block.header.previous_block_hash.into(),
))
.await
.respond(zs::Response::BlockHeader {
header: block_1.header.clone(),
hash: block_1.hash(),
height: Height(1),
next_block_hash: Some(hash),
});
let (error, advertiser_addr) = downloads
.next()
.await
.expect("downloads is non-empty")
.expect_err("block above the lookahead limit is dropped");
assert!(
matches!(
error.downcast_ref::<HeightLimitError>(),
Some(HeightLimitError::AboveLookahead { .. })
),
"a contradicted far-ahead height must be a scoreable typed error, but was: {error:?}"
);
assert_eq!(
advertiser_addr,
Some(advertiser),
"a contradicted far-ahead height must attribute the drop to the supplying peer"
);
verifier.expect_no_requests().await;
}
#[tokio::test]
async fn genuinely_far_ahead_block_is_dropped_without_attribution() {
let _init_guard = zebra_test::init();
let (mut downloads, mut network, mut verifier, mut state) =
mock_downloads(lookahead_boundary_tip());
let block = block_10();
let hash = block.hash();
let advertiser: PeerSocketAddr = ADVERTISER.parse().expect("hard-coded address is valid");
assert!(
matches!(
downloads.download_and_verify(hash, Some(advertiser)),
DownloadAction::AddedToQueue
),
"download is queued"
);
state
.expect_request(zs::Request::KnownBlock(hash))
.await
.respond(zs::Response::KnownBlock(None));
network
.expect_request(zn::Request::BlocksByHash(iter::once(hash).collect()))
.await
.respond(zn::Response::Blocks(vec![Available((
block.clone(),
Some(advertiser),
))]));
state
.expect_request(zs::Request::BlockHeader(
block.header.previous_block_hash.into(),
))
.await
.respond(Err(zn::BoxError::from("block hash or height not found")));
let (error, advertiser_addr) = downloads
.next()
.await
.expect("downloads is non-empty")
.expect_err("block above the lookahead limit is dropped");
assert!(
matches!(
error.downcast_ref::<HeightLimitError>(),
Some(HeightLimitError::AboveLookahead { .. })
),
"a far-ahead drop must be a typed error, but was: {error:?}"
);
assert_eq!(
advertiser_addr, None,
"a genuinely far-ahead block must not be attributed to its peer"
);
verifier.expect_no_requests().await;
}
#[tokio::test]
async fn block_at_lookahead_boundary_is_verified_not_dropped() {
let _init_guard = zebra_test::init();
let (mut downloads, mut network, mut verifier, mut state) =
mock_downloads(lookahead_boundary_tip());
let block = block_2();
let hash = block.hash();
let advertiser: PeerSocketAddr = ADVERTISER.parse().expect("hard-coded address is valid");
assert!(
matches!(
downloads.download_and_verify(hash, Some(advertiser)),
DownloadAction::AddedToQueue
),
"download is queued"
);
state
.expect_request(zs::Request::KnownBlock(hash))
.await
.respond(zs::Response::KnownBlock(None));
network
.expect_request(zn::Request::BlocksByHash(iter::once(hash).collect()))
.await
.respond(zn::Response::Blocks(vec![Available((
block.clone(),
Some(advertiser),
))]));
verifier
.expect_request(zebra_consensus::Request::Commit(block))
.await
.respond(hash);
assert_eq!(
downloads
.next()
.await
.expect("downloads is non-empty")
.expect("block on the lookahead boundary is verified"),
hash,
"a block at max_lookahead_height must be verified, not dropped as far ahead"
);
state.expect_no_requests().await;
}