use std::{collections::HashSet, sync::Arc};
use chrono::Duration;
use indexmap::IndexMap;
use tokio::{
sync::broadcast::{self, error::TryRecvError},
time::Instant,
};
use tower::{Service, ServiceExt};
use zakura_chain::{
block::Height,
chain_tip::ChainTip,
parameters::{Network, NetworkUpgrade},
transaction::{Transaction, UnminedTx, UnminedTxId},
};
use zakura_node_services::{
mempool::{Gossip, Request, Response},
BoxError,
};
use zakura_state::{MinedTx, ReadRequest, ReadResponse};
#[cfg(test)]
mod tests;
const NUMBER_OF_BLOCKS_TO_EXPIRE: i64 = 5;
pub const CHANNEL_AND_QUEUE_CAPACITY: usize = 20;
const NO_CHAIN_TIP_HEIGHT: Height = Height(1);
#[derive(Clone, Debug)]
pub struct Queue {
transactions: IndexMap<UnminedTxId, (Arc<Transaction>, Instant)>,
}
#[derive(Debug)]
pub struct Runner {
queue: Queue,
receiver: broadcast::Receiver<UnminedTx>,
tip_height: Height,
}
impl Queue {
pub fn start() -> (Runner, broadcast::Sender<UnminedTx>) {
let (sender, receiver) = broadcast::channel(CHANNEL_AND_QUEUE_CAPACITY);
let queue = Queue {
transactions: IndexMap::new(),
};
let runner = Runner {
queue,
receiver,
tip_height: Height(0),
};
(runner, sender)
}
pub fn transactions(&self) -> IndexMap<UnminedTxId, (Arc<Transaction>, Instant)> {
self.transactions.clone()
}
pub fn insert(&mut self, unmined_tx: UnminedTx) {
self.transactions
.insert(unmined_tx.id, (unmined_tx.transaction, Instant::now()));
if self.transactions.len() > CHANNEL_AND_QUEUE_CAPACITY {
self.remove_first();
}
}
pub fn remove(&mut self, unmined_id: UnminedTxId) {
self.transactions.swap_remove(&unmined_id);
}
pub fn remove_first(&mut self) {
self.transactions.shift_remove_index(0);
}
}
impl Runner {
fn transactions_as_hash_set(&self) -> HashSet<UnminedTxId> {
let transactions = self.queue.transactions();
transactions.iter().map(|t| *t.0).collect()
}
fn transactions_as_vec(&self) -> Vec<Arc<Transaction>> {
let transactions = self.queue.transactions();
transactions.iter().map(|t| t.1 .0.clone()).collect()
}
pub fn update_tip_height(&mut self, height: Height) {
self.tip_height = height;
}
pub async fn run<Mempool, State, Tip>(
mut self,
mempool: Mempool,
state: State,
tip: Tip,
network: Network,
) where
Mempool: Service<Request, Response = Response, Error = BoxError> + Clone + 'static,
State: Service<ReadRequest, Response = ReadResponse, Error = zakura_state::BoxError>
+ Clone
+ Send
+ Sync
+ 'static,
Tip: ChainTip + Clone + Send + Sync + 'static,
{
loop {
let tip_height = match tip.best_tip_height() {
Some(height) => height,
_ => NO_CHAIN_TIP_HEIGHT,
};
let spacing = NetworkUpgrade::target_spacing_for_height(&network, tip_height);
tokio::time::sleep(spacing.to_std().expect("should never be less than zero")).await;
loop {
let tx = match self.receiver.try_recv() {
Ok(tx) => tx,
Err(TryRecvError::Empty) => break,
Err(TryRecvError::Lagged(skipped_count)) => {
tracing::info!("sendrawtransaction queue was full: skipped {skipped_count} transactions");
continue;
}
Err(TryRecvError::Closed) => {
tracing::info!(
"sendrawtransaction queue was closed: is Zebra shutting down?"
);
return;
}
};
self.queue.insert(tx.clone());
}
if tip_height != self.tip_height {
self.update_tip_height(tip_height);
if !self.queue.transactions().is_empty() {
self.remove_expired(spacing);
let in_mempool =
Self::check_mempool(mempool.clone(), self.transactions_as_hash_set()).await;
self.remove_committed(in_mempool);
let in_state =
Self::check_state(state.clone(), self.transactions_as_hash_set()).await;
self.remove_committed(in_state);
Self::retry(mempool.clone(), self.transactions_as_vec()).await;
}
}
}
}
fn remove_expired(&mut self, spacing: Duration) {
let extra_time = Duration::seconds(5);
let duration_to_expire =
Duration::seconds(NUMBER_OF_BLOCKS_TO_EXPIRE * spacing.num_seconds()) + extra_time;
let transactions = self.queue.transactions();
let now = Instant::now();
for tx in transactions.iter() {
let tx_time =
tx.1 .1
.checked_add(
duration_to_expire
.to_std()
.expect("should never be less than zero"),
)
.expect("this is low numbers, should always be inside bounds");
if now > tx_time {
self.queue.remove(*tx.0);
}
}
}
fn remove_committed(&mut self, to_remove: HashSet<UnminedTxId>) {
for r in to_remove {
self.queue.remove(r);
}
}
async fn check_mempool<Mempool>(
mempool: Mempool,
transactions: HashSet<UnminedTxId>,
) -> HashSet<UnminedTxId>
where
Mempool: Service<Request, Response = Response, Error = BoxError> + Clone + 'static,
{
let mut response = HashSet::new();
if !transactions.is_empty() {
let request = Request::TransactionsById(transactions);
let mempool_response = mempool.oneshot(request).await;
if let Ok(Response::Transactions(txs)) = mempool_response {
for tx in txs {
response.insert(tx.id);
}
}
}
response
}
async fn check_state<State>(
state: State,
transactions: HashSet<UnminedTxId>,
) -> HashSet<UnminedTxId>
where
State: Service<ReadRequest, Response = ReadResponse, Error = zakura_state::BoxError>
+ Clone
+ Send
+ Sync
+ 'static,
{
let mut response = HashSet::new();
for t in transactions {
let request = ReadRequest::Transaction(t.mined_id());
let state_response = state.clone().oneshot(request).await;
if let Ok(ReadResponse::Transaction(Some(MinedTx { tx, .. }))) = state_response {
response.insert(tx.unmined_id());
}
}
response
}
async fn retry<Mempool>(
mempool: Mempool,
transactions: Vec<Arc<Transaction>>,
) -> HashSet<UnminedTxId>
where
Mempool: Service<Request, Response = Response, Error = BoxError> + Clone + 'static,
{
let mut retried = HashSet::new();
for tx in transactions {
let unmined = UnminedTx::from(tx);
let gossip = Gossip::Tx(unmined.clone());
let request = Request::Queue(vec![gossip]);
let _ = mempool.clone().oneshot(request).await;
retried.insert(unmined.id);
}
retried
}
}