use std::{
collections::VecDeque,
time::{Duration, Instant},
};
use brk_types::{FeeRate, Transaction, Txid};
use rustc_hash::FxHashMap;
mod tombstone;
pub use tombstone::TxTombstone;
use crate::{TxRemoval, state::TxEntry};
const RETENTION: Duration = Duration::from_hours(1);
#[derive(Default)]
pub struct TxGraveyard {
tombstones: FxHashMap<Txid, TxTombstone>,
order: VecDeque<(Instant, Txid)>,
}
impl TxGraveyard {
pub fn tombstones_len(&self) -> usize {
self.tombstones.len()
}
pub fn order_len(&self) -> usize {
self.order.len()
}
pub fn get(&self, txid: &Txid) -> Option<&TxTombstone> {
self.tombstones.get(txid)
}
pub fn get_vanished(&self, txid: &Txid) -> Option<&TxTombstone> {
let tomb = self.tombstones.get(txid)?;
matches!(tomb.removal, TxRemoval::Vanished).then_some(tomb)
}
pub fn replacement_root_of(&self, mut txid: Txid) -> Txid {
while let Some(TxRemoval::Replaced { by }) =
self.tombstones.get(&txid).map(|t| &t.removal)
{
txid = *by;
}
txid
}
pub fn predecessors_of<'a>(
&'a self,
replacer: &'a Txid,
) -> impl Iterator<Item = (&'a Txid, &'a TxTombstone)> {
self.tombstones.iter().filter_map(move |(txid, ts)| {
(ts.replaced_by() == Some(replacer)).then_some((txid, ts))
})
}
pub fn replaced_iter_recent_first(&self) -> impl Iterator<Item = (&Txid, &Txid)> {
self.order.iter().rev().filter_map(|(t, txid)| {
let ts = self.tombstones.get(txid)?;
if ts.removed_at != *t {
return None;
}
Some((txid, ts.replaced_by()?))
})
}
pub fn bury(
&mut self,
tx: Transaction,
entry: TxEntry,
chunk_rate: FeeRate,
removal: TxRemoval,
) {
let txid = entry.txid;
let removed_at = Instant::now();
self.tombstones.insert(
txid,
TxTombstone {
tx,
entry,
chunk_rate,
removal,
removed_at,
},
);
self.order.push_back((removed_at, txid));
}
pub fn exhume(&mut self, txid: &Txid) -> Option<TxTombstone> {
self.tombstones.remove(txid)
}
pub fn evict_old(&mut self) {
while let Some(&(t, _)) = self.order.front() {
if t.elapsed() < RETENTION {
break;
}
let (_, txid) = self.order.pop_front().unwrap();
if let Some(ts) = self.tombstones.get(&txid)
&& ts.removed_at == t
{
self.tombstones.remove(&txid);
}
}
}
#[cfg(test)]
fn shift_oldest_back(&mut self, count: usize) {
let bumped = Instant::now() - (RETENTION + Duration::from_secs(1));
for entry in self.order.iter_mut().take(count) {
let txid = entry.1;
entry.0 = bumped;
if let Some(ts) = self.tombstones.get_mut(&txid) {
ts.removed_at = bumped;
}
}
}
}
#[cfg(test)]
mod tests {
use brk_types::{FeeRate, MempoolEntryInfo, Sats, Timestamp, VSize, Weight};
use super::*;
use crate::test_support::{fake_tx, fake_txid};
fn tomb_inputs(seed: u8) -> (Transaction, TxEntry, FeeRate) {
let tx = fake_tx(seed, &[], &[]);
let info = MempoolEntryInfo {
txid: tx.txid,
vsize: VSize::from(100u64),
weight: Weight::from(400u64),
fee: Sats::from(100u64),
first_seen: Timestamp::from(0u32),
depends: vec![],
};
let entry = TxEntry::new(&info, 100, false);
let rate = FeeRate::from((Sats::from(100u64), VSize::from(100u64)));
(tx, entry, rate)
}
#[test]
fn bury_then_exhume_roundtrips_the_tombstone() {
let mut g = TxGraveyard::default();
let (tx, entry, rate) = tomb_inputs(1);
let txid = entry.txid;
g.bury(tx, entry, rate, TxRemoval::Vanished);
assert_eq!(g.tombstones_len(), 1);
assert!(g.get(&txid).is_some());
let resurrected = g.exhume(&txid).expect("tombstone present");
assert_eq!(resurrected.entry.txid, txid);
assert!(g.get(&txid).is_none());
assert_eq!(g.tombstones_len(), 0);
assert_eq!(g.order_len(), 1);
}
#[test]
fn get_vanished_filters_out_replaced_tombstones() {
let mut g = TxGraveyard::default();
let (tx_a, entry_a, rate) = tomb_inputs(2);
let (tx_b, entry_b, _) = tomb_inputs(3);
let txid_a = entry_a.txid;
let txid_b = entry_b.txid;
g.bury(tx_a, entry_a, rate, TxRemoval::Replaced { by: txid_b });
g.bury(tx_b, entry_b, rate, TxRemoval::Vanished);
assert!(g.get_vanished(&txid_a).is_none());
assert!(g.get_vanished(&txid_b).is_some());
}
#[test]
fn replacement_root_walks_replaced_chain() {
let mut g = TxGraveyard::default();
let (tx_a, entry_a, rate) = tomb_inputs(4);
let (tx_b, entry_b, _) = tomb_inputs(5);
let (tx_c, entry_c, _) = tomb_inputs(6);
let a = entry_a.txid;
let b = entry_b.txid;
let c = entry_c.txid;
g.bury(tx_a, entry_a, rate, TxRemoval::Replaced { by: b });
g.bury(tx_b, entry_b, rate, TxRemoval::Replaced { by: c });
g.bury(tx_c, entry_c, rate, TxRemoval::Vanished);
assert_eq!(g.replacement_root_of(a), c);
assert_eq!(g.replacement_root_of(c), c);
let unknown = fake_txid(99);
assert_eq!(g.replacement_root_of(unknown), unknown);
}
#[test]
fn predecessors_of_returns_direct_replacers() {
let mut g = TxGraveyard::default();
let (tx_a, entry_a, rate) = tomb_inputs(7);
let (tx_b, entry_b, _) = tomb_inputs(8);
let (tx_c, entry_c, _) = tomb_inputs(9);
let replacer = entry_c.txid;
let a = entry_a.txid;
let b = entry_b.txid;
g.bury(tx_a, entry_a, rate, TxRemoval::Replaced { by: replacer });
g.bury(tx_b, entry_b, rate, TxRemoval::Replaced { by: replacer });
g.bury(tx_c, entry_c, rate, TxRemoval::Vanished);
let mut preds: Vec<Txid> = g.predecessors_of(&replacer).map(|(t, _)| *t).collect();
preds.sort_unstable_by_key(|t| t.as_slice()[0]);
let mut expected = vec![a, b];
expected.sort_unstable_by_key(|t| t.as_slice()[0]);
assert_eq!(preds, expected);
assert_eq!(g.predecessors_of(&fake_txid(123)).count(), 0);
}
#[test]
fn replaced_iter_recent_first_skips_stale_order_entries() {
let mut g = TxGraveyard::default();
let (tx_a, entry_a, rate) = tomb_inputs(10);
let (tx_b, entry_b, _) = tomb_inputs(11);
let replacer = entry_b.txid;
let pred = entry_a.txid;
g.bury(tx_a.clone(), entry_a.clone(), rate, TxRemoval::Replaced { by: replacer });
g.bury(tx_b, entry_b, rate, TxRemoval::Vanished);
g.bury(tx_a, entry_a, rate, TxRemoval::Replaced { by: replacer });
let collected: Vec<(Txid, Txid)> = g
.replaced_iter_recent_first()
.map(|(p, by)| (*p, *by))
.collect();
assert_eq!(collected, vec![(pred, replacer)]);
}
#[test]
fn evict_old_drops_aged_tombstones() {
let mut g = TxGraveyard::default();
let (tx_a, entry_a, rate) = tomb_inputs(12);
let (tx_b, entry_b, _) = tomb_inputs(13);
let txid_a = entry_a.txid;
let txid_b = entry_b.txid;
g.bury(tx_a, entry_a, rate, TxRemoval::Vanished);
g.bury(tx_b, entry_b, rate, TxRemoval::Vanished);
g.shift_oldest_back(1);
g.evict_old();
assert!(g.get(&txid_a).is_none(), "aged tombstone evicted");
assert!(g.get(&txid_b).is_some(), "fresh tombstone retained");
}
#[test]
fn re_bury_mid_retention_resets_age() {
let mut g = TxGraveyard::default();
let (tx, entry, rate) = tomb_inputs(14);
let txid = entry.txid;
g.bury(tx.clone(), entry.clone(), rate, TxRemoval::Vanished);
g.shift_oldest_back(1);
g.bury(tx, entry, rate, TxRemoval::Vanished);
g.evict_old();
assert!(g.get(&txid).is_some());
}
}