use std::{
sync::{Arc, Mutex},
task::{Context, Poll, Waker},
time::Duration,
};
use chrono::Utc;
use futures::future;
use tokio::sync::mpsc;
use tower::Service;
use zebra_chain::{
block::{self, Height},
parameters::Network,
};
use zebra_network as zn;
use zebra_rpc::SubmitBlockChannel;
use zebra_state::{ChainTipBlock, ChainTipSender};
use crate::{
components::sync::{
gossip_best_tip_block_hashes, BlockGossipError, SyncStatus, PEER_GOSSIP_DELAY,
TIPS_RESPONSE_TIMEOUT,
},
BoxError,
};
const MORE_THAN_CHANNEL_CAPACITY: u32 = 10_500;
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
enum Readiness {
Pending,
Ready,
Failed,
}
#[derive(Debug)]
struct GateState {
readiness: Readiness,
wakers: Vec<Waker>,
}
#[derive(Clone, Debug)]
struct GatedPeerSet {
state: Arc<Mutex<GateState>>,
requests: mpsc::UnboundedSender<zn::Request>,
}
impl GatedPeerSet {
fn new() -> (Self, mpsc::UnboundedReceiver<zn::Request>) {
let (requests, request_receiver) = mpsc::unbounded_channel();
let peer_set = GatedPeerSet {
state: Arc::new(Mutex::new(GateState {
readiness: Readiness::Pending,
wakers: Vec::new(),
})),
requests,
};
(peer_set, request_receiver)
}
fn set_readiness(&self, readiness: Readiness) {
let mut state = self.state.lock().expect("lock is not poisoned");
state.readiness = readiness;
state.wakers.drain(..).for_each(Waker::wake);
}
}
impl Service<zn::Request> for GatedPeerSet {
type Response = zn::Response;
type Error = BoxError;
type Future = future::Ready<Result<zn::Response, BoxError>>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), BoxError>> {
let mut state = self.state.lock().expect("lock is not poisoned");
match state.readiness {
Readiness::Pending => {
state.wakers.push(cx.waker().clone());
Poll::Pending
}
Readiness::Ready => Poll::Ready(Ok(())),
Readiness::Failed => Poll::Ready(Err("permanent peer set failure".into())),
}
}
fn call(&mut self, request: zn::Request) -> Self::Future {
let _ = self.requests.send(request);
future::ok(zn::Response::Nil)
}
}
fn mined_block(n: u32) -> (block::Hash, Height) {
let mut hash = [0; 32];
hash[..4].copy_from_slice(&n.to_le_bytes());
(block::Hash(hash), Height(n))
}
#[tokio::test(start_paused = true)]
async fn gossip_keeps_draining_mined_blocks_without_ready_peers() {
let _init_guard = zebra_test::init();
let network = Network::Mainnet;
let (_chain_tip_sender, _latest_chain_tip, chain_tip_change) =
ChainTipSender::new(None, &network);
let (sync_status, _recent_syncs) = SyncStatus::new();
let (peer_set, mut requests) = GatedPeerSet::new();
let channel = SubmitBlockChannel::new();
let mined_block_sender = channel.sender();
let gossip_task = tokio::spawn(gossip_best_tip_block_hashes(
sync_status,
chain_tip_change,
peer_set,
Some(channel.receiver()),
));
for n in 0..MORE_THAN_CHANNEL_CAPACITY {
mined_block_sender
.try_send(mined_block(n))
.unwrap_or_else(|error| {
panic!("mined block {n} could not be queued for gossip: {error:?}")
});
tokio::time::sleep(Duration::from_millis(10)).await;
}
assert!(
!gossip_task.is_finished(),
"gossip task must keep running without ready peers"
);
assert!(
requests.try_recv().is_err(),
"no peers were ready, so nothing should have been broadcast"
);
gossip_task.abort();
}
#[tokio::test(start_paused = true)]
async fn gossip_broadcasts_latest_mined_block_when_peers_become_ready() {
let _init_guard = zebra_test::init();
let network = Network::Mainnet;
let (_chain_tip_sender, _latest_chain_tip, chain_tip_change) =
ChainTipSender::new(None, &network);
let (sync_status, _recent_syncs) = SyncStatus::new();
let (peer_set, mut requests) = GatedPeerSet::new();
let channel = SubmitBlockChannel::new();
let mined_block_sender = channel.sender();
let gossip_task = tokio::spawn(gossip_best_tip_block_hashes(
sync_status,
chain_tip_change,
peer_set.clone(),
Some(channel.receiver()),
));
mined_block_sender
.try_send(mined_block(1))
.expect("channel has capacity");
tokio::time::sleep(TIPS_RESPONSE_TIMEOUT - Duration::from_millis(1)).await;
let (latest_hash, latest_height) = mined_block(2);
mined_block_sender
.try_send((latest_hash, latest_height))
.expect("channel has capacity");
tokio::time::sleep(TIPS_RESPONSE_TIMEOUT * 2).await;
assert!(
requests.try_recv().is_err(),
"no peers were ready, so nothing should have been broadcast"
);
peer_set.set_readiness(Readiness::Ready);
let request = tokio::time::timeout(TIPS_RESPONSE_TIMEOUT, requests.recv())
.await
.expect("latest mined block is broadcast once peers are ready")
.expect("peer set is still alive");
assert_eq!(request, zn::Request::AdvertiseBlockToAll(latest_hash));
tokio::time::sleep(TIPS_RESPONSE_TIMEOUT * 2).await;
assert!(
requests.try_recv().is_err(),
"superseded mined blocks should not be broadcast"
);
assert!(!gossip_task.is_finished());
gossip_task.abort();
}
#[tokio::test(start_paused = true)]
async fn gossip_broadcasts_latest_mined_block_when_an_eligible_peer_connects() {
let _init_guard = zebra_test::init();
for supersede in [false, true] {
let mut peers = zn::test_support::PeerSetHarness::new(&[], None).await;
let (_chain_tip_sender, _latest_chain_tip, chain_tip_change) =
ChainTipSender::new(None, &Network::Mainnet);
let (sync_status, _recent_syncs) = SyncStatus::new();
let channel = SubmitBlockChannel::new();
let mined_block_sender = channel.sender();
let gossip_task = tokio::spawn(gossip_best_tip_block_hashes(
sync_status,
chain_tip_change,
peers.block_advertiser.clone(),
Some(channel.receiver()),
));
let mut latest_block = mined_block(1);
mined_block_sender.try_send(latest_block).unwrap();
tokio::time::sleep(TIPS_RESPONSE_TIMEOUT * 2).await;
assert!(!gossip_task.is_finished());
if supersede {
latest_block = mined_block(2);
mined_block_sender.try_send(latest_block).unwrap();
tokio::time::sleep(Duration::from_millis(1)).await;
}
let peer = peers.connect_peer(zn::types::PeerServices::NODE_NETWORK);
let (hash, write) = peers.next_block_advert(peer).await.unwrap();
assert_eq!(hash, latest_block.0);
write.send(Ok(())).unwrap();
tokio::time::sleep(PEER_GOSSIP_DELAY * 2).await;
assert!(
peers.next_block_advert(peer).await.is_none(),
"only the latest mined block is advertised, exactly once"
);
assert!(!gossip_task.is_finished());
gossip_task.abort();
}
}
#[tokio::test(start_paused = true)]
async fn gossip_broadcasts_tip_change_when_peers_become_ready() {
let _init_guard = zebra_test::init();
let network = Network::Mainnet;
let (mut chain_tip_sender, _latest_chain_tip, chain_tip_change) =
ChainTipSender::new(None, &network);
let (sync_status, mut recent_syncs) = SyncStatus::new();
let (peer_set, mut requests) = GatedPeerSet::new();
recent_syncs.push_extend_tips_length(0);
let gossip_task = tokio::spawn(gossip_best_tip_block_hashes(
sync_status,
chain_tip_change,
peer_set.clone(),
None,
));
let (hash, height) = mined_block(1);
chain_tip_sender.set_finalized_tip(ChainTipBlock {
hash,
height,
time: Utc::now(),
transactions: Vec::new(),
transaction_hashes: Arc::new([]),
previous_block_hash: block::Hash([0xff; 32]),
});
tokio::time::sleep(PEER_GOSSIP_DELAY + TIPS_RESPONSE_TIMEOUT * 2).await;
assert!(
requests.try_recv().is_err(),
"no peers were ready, so nothing should have been broadcast"
);
peer_set.set_readiness(Readiness::Ready);
let request = tokio::time::timeout(TIPS_RESPONSE_TIMEOUT, requests.recv())
.await
.expect("tip change is broadcast once peers are ready")
.expect("peer set is still alive");
assert_eq!(request, zn::Request::AdvertiseBlock(hash, None));
assert!(!gossip_task.is_finished());
gossip_task.abort();
}
#[tokio::test(start_paused = true)]
async fn gossip_keeps_pending_tip_change_when_a_side_chain_block_is_mined() {
let _init_guard = zebra_test::init();
let network = Network::Mainnet;
let (mut chain_tip_sender, _latest_chain_tip, chain_tip_change) =
ChainTipSender::new(None, &network);
let (sync_status, mut recent_syncs) = SyncStatus::new();
let (peer_set, mut requests) = GatedPeerSet::new();
recent_syncs.push_extend_tips_length(0);
let channel = SubmitBlockChannel::new();
let mined_block_sender = channel.sender();
let gossip_task = tokio::spawn(gossip_best_tip_block_hashes(
sync_status,
chain_tip_change,
peer_set.clone(),
Some(channel.receiver()),
));
let (tip_hash, tip_height) = mined_block(1);
chain_tip_sender.set_finalized_tip(ChainTipBlock {
hash: tip_hash,
height: tip_height,
time: Utc::now(),
transactions: Vec::new(),
transaction_hashes: Arc::new([]),
previous_block_hash: block::Hash([0xff; 32]),
});
tokio::time::sleep(PEER_GOSSIP_DELAY + TIPS_RESPONSE_TIMEOUT).await;
let (side_chain_hash, side_chain_height) = mined_block(2);
mined_block_sender
.try_send((side_chain_hash, side_chain_height))
.expect("channel has capacity");
tokio::time::sleep(TIPS_RESPONSE_TIMEOUT).await;
assert!(
requests.try_recv().is_err(),
"no peers were ready, so nothing should have been broadcast"
);
peer_set.set_readiness(Readiness::Ready);
let mut broadcasts = Vec::new();
for _ in 0..2 {
let request = tokio::time::timeout(TIPS_RESPONSE_TIMEOUT, requests.recv())
.await
.expect("both blocks are broadcast once peers are ready")
.expect("peer set is still alive");
broadcasts.push(request);
}
assert!(broadcasts.contains(&zn::Request::AdvertiseBlockToAll(side_chain_hash)));
assert!(broadcasts.contains(&zn::Request::AdvertiseBlock(tip_hash, None)));
assert!(!gossip_task.is_finished());
gossip_task.abort();
}
#[tokio::test(start_paused = true)]
async fn gossip_returns_permanent_peer_set_errors() {
let _init_guard = zebra_test::init();
let network = Network::Mainnet;
let (_chain_tip_sender, _latest_chain_tip, chain_tip_change) =
ChainTipSender::new(None, &network);
let (sync_status, _recent_syncs) = SyncStatus::new();
let (peer_set, _requests) = GatedPeerSet::new();
peer_set.set_readiness(Readiness::Failed);
let channel = SubmitBlockChannel::new();
channel
.sender()
.try_send(mined_block(1))
.expect("channel has capacity");
let result = tokio::time::timeout(
TIPS_RESPONSE_TIMEOUT * 2,
gossip_best_tip_block_hashes(
sync_status,
chain_tip_change,
peer_set,
Some(channel.receiver()),
),
)
.await
.expect("gossip task exits on a permanent peer set error");
assert!(
matches!(result, Err(BlockGossipError::PeerSetReadiness(_))),
"unexpected gossip result: {result:?}"
);
}