use std::{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::BlockDownload {
hash,
advertiser: Some(advertiser),
})
.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::BlockDownload {
hash,
advertiser: Some(advertiser),
})
.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::BlockDownload {
hash,
advertiser: Some(advertiser),
})
.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::BlockDownload {
hash,
advertiser: Some(advertiser),
})
.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::BlockDownload {
hash,
advertiser: Some(advertiser),
})
.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::BlockDownload {
hash,
advertiser: Some(advertiser),
})
.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::BlockDownload {
hash,
advertiser: Some(advertiser),
})
.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::BlockDownload {
hash,
advertiser: Some(advertiser),
})
.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;
}
mod network_checkpoint;
#[tokio::test(start_paused = true)]
async fn download_deadline_includes_readiness_and_response() {
use tokio::time::{sleep, timeout_at, Instant};
use tower::{limit::ConcurrencyLimit, ServiceExt};
let _init_guard = zebra_test::init();
let budget = zn::constants::REQUEST_TIMEOUT + super::BLOCK_DOWNLOAD_TIMEOUT;
for release_capacity in [false, true] {
let mut network: MockNetwork = MockService::build().for_unit_tests();
let limited = ConcurrencyLimit::new(network.clone(), 1);
let mut reservation = limited.clone();
reservation.ready().await.unwrap();
let verifier: MockVerifier = MockService::build().for_unit_tests();
let mut state: MockState = MockService::build().for_unit_tests();
let (tip, _sender) = MockChainTip::new();
let mut downloads = Downloads::new(2, limited, verifier, state.clone(), tip);
let hash = block_1().hash();
assert!(matches!(
downloads.download_and_verify(hash, None),
DownloadAction::AddedToQueue,
));
state
.expect_request(zs::Request::KnownBlock(hash))
.await
.respond(zs::Response::KnownBlock(None));
let started = Instant::now();
tokio::task::yield_now().await;
let _response = if release_capacity {
sleep(budget / 2).await;
drop(reservation);
Some(
network
.expect_request(zn::Request::BlockDownload {
hash,
advertiser: None,
})
.await,
)
} else {
None
};
let (error, advertiser) =
timeout_at(started + budget + Duration::from_secs(1), downloads.next())
.await
.expect("gossip readiness and response must be bounded")
.expect("the queued download has a result")
.expect_err("the unanswered download expires");
assert!(error.is::<tokio::time::error::Elapsed>());
assert_eq!(advertiser, None);
assert_eq!(Instant::now() - started, budget);
}
}