brk_mempool 0.3.2

Bitcoin mempool monitor with fee estimation
Documentation
//! # Locking
//!
//! Two locks live on `Rebuilder`: `history` and `snapshot`. Writes always
//! land on `history` first, then `snapshot`, so any `next_block_hash` a
//! reader sees in the published snapshot is already recorded in
//! `historical_block0`. No read path ever holds both, and no path holds
//! a `State` guard together with either Rebuilder lock - the cycle reads
//! `State` once to build the snapshot, then drops it before touching
//! these locks.

use std::{
    collections::VecDeque,
    sync::{
        Arc,
        atomic::{AtomicU64, Ordering},
    },
};

use brk_types::{FeeRate, NextBlockHash, Txid, TxidPrefix};
use parking_lot::RwLock;
use rustc_hash::FxHashSet;

use crate::State;

use super::{Partitioner, Snapshot, TxIndex};

const NUM_BLOCKS: usize = 8;
const HISTORY: usize = 10;

#[derive(Default)]
pub struct Rebuilder {
    snapshot: RwLock<Arc<Snapshot>>,
    /// Past block-0 txid lists keyed by `next_block_hash`, oldest first.
    /// Ordered so `block_template_diff` can emit `Retained(prior_index)`
    /// entries that line up with the client's cached prior template.
    history: RwLock<VecDeque<(NextBlockHash, Vec<Txid>)>>,
    rebuild_count: AtomicU64,
}

impl Rebuilder {
    /// Rebuild every cycle. `min_fee` participates in the result, so a
    /// "skip if no add/remove" gate would freeze served fees when Core's
    /// `mempoolminfee` drifts on a quiet pool.
    ///
    /// History is updated before the snapshot Arc is swapped so a reader
    /// can never observe a `next_block_hash` that hasn't been recorded
    /// yet. `block_template_diff(current_hash)` returning 404 in the
    /// publish gap would force unnecessary client refetches.
    pub fn tick(&self, lock: &RwLock<State>, gbt_txids: &[Txid], min_fee: FeeRate) {
        let snap = Self::build_snapshot(lock, gbt_txids, min_fee);
        let block0: Vec<Txid> = snap.block0_txids().collect();
        let next_hash = snap.next_block_hash;

        let mut hist = self.history.write();
        hist.retain(|(h, _)| *h != next_hash);
        hist.push_back((next_hash, block0));
        while hist.len() > HISTORY {
            hist.pop_front();
        }
        drop(hist);

        *self.snapshot.write() = Arc::new(snap);

        self.rebuild_count.fetch_add(1, Ordering::Relaxed);
    }

    /// Past block-0 ordered txid list for `hash`, or `None` if it has
    /// aged out (or was never seen). Used by `block_template_diff` to
    /// decide 200 vs 404 and to resolve `Retained(prior_index)` entries.
    pub fn historical_block0(&self, hash: NextBlockHash) -> Option<Vec<Txid>> {
        self.history
            .read()
            .iter()
            .find(|(h, _)| *h == hash)
            .map(|(_, block0)| block0.clone())
    }

    pub fn rebuild_count(&self) -> u64 {
        self.rebuild_count.load(Ordering::Relaxed)
    }

    fn build_snapshot(
        lock: &RwLock<State>,
        gbt_txids: &[Txid],
        min_fee: FeeRate,
    ) -> Snapshot {
        let (txs, prefix_to_idx) = {
            let state = lock.read();
            Snapshot::build_txs(&state.txs)
        };

        let block0: Vec<TxIndex> = gbt_txids
            .iter()
            .filter_map(|txid| prefix_to_idx.get(&TxidPrefix::from(txid)).copied())
            .collect();
        let excluded: FxHashSet<TxIndex> = block0.iter().copied().collect();
        let rest = Partitioner::partition(&txs, &excluded, NUM_BLOCKS.saturating_sub(1));

        let mut blocks = Vec::with_capacity(NUM_BLOCKS);
        blocks.push(block0);
        blocks.extend(rest);

        Snapshot::build(txs, blocks, prefix_to_idx, min_fee)
    }

    pub fn snapshot(&self) -> Arc<Snapshot> {
        self.snapshot.read().clone()
    }
}