use std::{future::Future, time::Duration};
use futures::TryFutureExt;
use thiserror::Error;
use tokio::sync::{mpsc, watch};
use tower::{timeout::Timeout, Service, ServiceExt};
use tracing::Instrument;
use zakura_chain::block;
use zakura_network as zn;
use zakura_state::ChainTipChange;
use crate::{
components::sync::{SyncStatus, PEER_GOSSIP_DELAY, TIPS_RESPONSE_TIMEOUT},
BoxError,
};
use BlockGossipError::*;
const MINED_BLOCK_MARK_CHANNEL_CAPACITY: usize = 16;
#[derive(Debug, Eq, PartialEq)]
enum GossipEvent<T> {
MinedBlockBroadcastCompleted(block::Hash),
MinedBlockSubmitted((block::Hash, block::Height)),
CommittedTip(T),
}
async fn next_gossip_event<T>(
mined_block_receiver: Option<&mut mpsc::Receiver<(block::Hash, block::Height)>>,
mined_block_mark_receiver: &mut mpsc::Receiver<block::Hash>,
committed_tip_fut: impl Future<Output = T>,
) -> GossipEvent<T> {
if let Some(mined_block_receiver) = mined_block_receiver {
tokio::select! {
biased;
Some(mark_hash) = mined_block_mark_receiver.recv() => {
GossipEvent::MinedBlockBroadcastCompleted(mark_hash)
},
Some(tip_change) = mined_block_receiver.recv() => {
GossipEvent::MinedBlockSubmitted(tip_change)
},
committed_tip = committed_tip_fut => {
GossipEvent::CommittedTip(committed_tip)
},
}
} else {
tokio::select! {
biased;
Some(mark_hash) = mined_block_mark_receiver.recv() => {
GossipEvent::MinedBlockBroadcastCompleted(mark_hash)
},
committed_tip = committed_tip_fut => {
GossipEvent::CommittedTip(committed_tip)
},
}
}
}
#[derive(Error, Debug)]
pub enum BlockGossipError {
#[error("chain tip sender was dropped")]
TipChange(watch::error::RecvError),
#[error("sync status sender was dropped")]
SyncStatus(watch::error::RecvError),
#[error("permanent peer set failure")]
PeerSetReadiness(zn::BoxError),
}
pub async fn gossip_best_tip_block_hashes<ZN>(
sync_status: SyncStatus,
mut chain_state: ChainTipChange,
broadcast_network: ZN,
mut mined_block_receiver: Option<mpsc::Receiver<(block::Hash, block::Height)>>,
) -> Result<(), BlockGossipError>
where
ZN: Service<zn::Request, Response = zn::Response, Error = BoxError> + Send + Clone + 'static,
ZN::Future: Send,
{
info!("initializing block gossip task");
let mut broadcast_network = Timeout::new(broadcast_network, TIPS_RESPONSE_TIMEOUT);
let (mined_block_mark_sender, mut mined_block_mark_receiver) =
mpsc::channel(MINED_BLOCK_MARK_CHANNEL_CAPACITY);
loop {
while let Ok(hash) = mined_block_mark_receiver.try_recv() {
chain_state.mark_last_change_hash(hash);
}
let mut sync_status = sync_status.clone();
let mut chain_tip = chain_state.clone_for_task();
let tip_change_close_to_network_tip_fut = async move {
const WAIT_FOR_BLOCK_SUBMISSION_DELAY: Duration = Duration::from_micros(100);
tokio::time::sleep(PEER_GOSSIP_DELAY).await;
let tip_action = chain_tip.wait_for_tip_change().await.map_err(TipChange)?;
tokio::time::sleep(WAIT_FOR_BLOCK_SUBMISSION_DELAY).await;
sync_status
.wait_until_close_to_tip()
.map_err(SyncStatus)
.await?;
let best_tip = chain_tip
.last_tip_change()
.unwrap_or(tip_action)
.best_tip_hash_and_height();
Ok((best_tip, "sending committed block broadcast", chain_tip))
}
.in_current_span();
let (((hash, height), log_msg, updated_chain_state), is_block_submission) =
match next_gossip_event(
mined_block_receiver.as_mut(),
&mut mined_block_mark_receiver,
tip_change_close_to_network_tip_fut,
)
.await
{
GossipEvent::MinedBlockBroadcastCompleted(mark_hash) => {
chain_state.mark_last_change_hash(mark_hash);
continue;
}
GossipEvent::MinedBlockSubmitted(tip_change) => (
(tip_change, "sending mined block broadcast", chain_state),
true,
),
GossipEvent::CommittedTip(tip_change_close_to_network_tip) => {
(tip_change_close_to_network_tip?, false)
}
};
chain_state = updated_chain_state;
let request = if is_block_submission {
zn::Request::AdvertiseBlockToAll(hash)
} else {
zn::Request::AdvertiseBlock(hash, None)
};
info!(?height, ?request, log_msg);
let broadcast_fut = broadcast_network
.ready()
.await
.map_err(PeerSetReadiness)?
.call(request);
if is_block_submission {
let mark_tx = mined_block_mark_sender.clone();
let submission_hash = hash;
tokio::spawn(async move {
if broadcast_fut.await.is_ok() {
let _ = mark_tx.send(submission_hash).await;
}
});
} else {
tokio::spawn(broadcast_fut);
}
}
}
#[cfg(test)]
mod tests {
use super::{next_gossip_event, GossipEvent};
use std::future;
use tokio::sync::mpsc;
use zakura_chain::block;
const READY_EVENT_ATTEMPTS: usize = 64;
#[tokio::test]
async fn ready_gossip_events_are_selected_in_priority_order() {
let submitted_block = (block::Hash([1; 32]), block::Height(1));
for _ in 0..READY_EVENT_ATTEMPTS {
let (mined_block_sender, mut mined_block_receiver) = mpsc::channel(1);
let (mark_sender, mut mark_receiver) = mpsc::channel(1);
mined_block_sender.send(submitted_block).await.unwrap();
mark_sender.send(submitted_block.0).await.unwrap();
let event = next_gossip_event(
Some(&mut mined_block_receiver),
&mut mark_receiver,
future::ready(()),
)
.await;
assert_eq!(
event,
GossipEvent::MinedBlockBroadcastCompleted(submitted_block.0)
);
let event = next_gossip_event(
Some(&mut mined_block_receiver),
&mut mark_receiver,
future::ready(()),
)
.await;
assert_eq!(event, GossipEvent::MinedBlockSubmitted(submitted_block));
let event = next_gossip_event(
Some(&mut mined_block_receiver),
&mut mark_receiver,
future::ready("committed tip"),
)
.await;
assert_eq!(event, GossipEvent::CommittedTip("committed tip"));
}
}
}