mempool: snap

This commit is contained in:
nym21
2026-05-10 00:24:02 +02:00
parent c52a076bfc
commit fe5f30bca6
36 changed files with 847 additions and 720 deletions
@@ -15,7 +15,8 @@ use addr_entry::AddrEntry;
pub struct AddrTracker(FxHashMap<AddrBytes, AddrEntry>);
impl AddrTracker {
pub fn add_tx(&mut self, tx: &Transaction, txid: &Txid) {
pub fn add_tx(&mut self, tx: &Transaction) {
let txid = &tx.txid;
for txin in &tx.input {
if let Some(prevout) = txin.prevout.as_ref() {
self.add_input(txid, prevout);
@@ -28,7 +29,8 @@ impl AddrTracker {
}
}
pub fn remove_tx(&mut self, tx: &Transaction, txid: &Txid) {
pub fn remove_tx(&mut self, tx: &Transaction) {
let txid = &tx.txid;
for txin in &tx.input {
if let Some(prevout) = txin.prevout.as_ref() {
self.remove_input(txid, prevout);
+10 -9
View File
@@ -1,13 +1,14 @@
//! Stateful in-memory holders. After Phase 3 they're plain owned
//! types (no internal locks) — `State` aggregates them under a
//! single `RwLock` in `crate::state`.
//! In-memory holders for live mempool state. Plain owned types with
//! no internal locks: `crate::state::State` aggregates them under a
//! single `RwLock` so the cycle steps and read-side accessors share
//! one lock-order discipline.
pub mod addr_tracker;
pub(crate) mod addr_tracker;
pub(crate) mod outpoint_spends;
pub mod tx_graveyard;
pub mod tx_store;
pub(crate) mod tx_graveyard;
pub(crate) mod tx_store;
pub use addr_tracker::AddrTracker;
pub(crate) use addr_tracker::AddrTracker;
pub(crate) use outpoint_spends::OutpointSpends;
pub use tx_graveyard::{TxGraveyard, TxTombstone};
pub use tx_store::TxStore;
pub(crate) use tx_graveyard::{TxGraveyard, TxTombstone};
pub(crate) use tx_store::TxStore;
@@ -13,11 +13,7 @@ pub struct OutpointSpends(FxHashMap<OutpointPrefix, TxidPrefix>);
impl OutpointSpends {
pub fn insert_spends(&mut self, tx: &Transaction, spender: TxidPrefix) {
for input in &tx.input {
if input.is_coinbase {
continue;
}
let key = OutpointPrefix::new(TxidPrefix::from(&input.txid), input.vout);
for key in spent_outpoints(tx) {
self.0.insert(key, spender);
}
}
@@ -25,11 +21,7 @@ impl OutpointSpends {
/// Only removes entries whose stored prefix still matches `spender`,
/// so an outpoint already re-claimed by a later spender is left alone.
pub fn remove_spends(&mut self, tx: &Transaction, spender: TxidPrefix) {
for input in &tx.input {
if input.is_coinbase {
continue;
}
let key = OutpointPrefix::new(TxidPrefix::from(&input.txid), input.vout);
for key in spent_outpoints(tx) {
if self.0.get(&key) == Some(&spender) {
self.0.remove(&key);
}
@@ -41,3 +33,10 @@ impl OutpointSpends {
self.0.get(key).copied()
}
}
fn spent_outpoints(tx: &Transaction) -> impl Iterator<Item = OutpointPrefix> + '_ {
tx.input
.iter()
.filter(|i| !i.is_coinbase)
.map(|i| OutpointPrefix::new(TxidPrefix::from(&i.txid), i.vout))
}
@@ -3,12 +3,12 @@ use std::{
time::{Duration, Instant},
};
use brk_types::{Transaction, Txid};
use brk_types::{FeeRate, Transaction, Txid};
use rustc_hash::FxHashMap;
mod tombstone;
pub use tombstone::TxTombstone;
pub(crate) use tombstone::TxTombstone;
use crate::{TxEntry, TxRemoval};
@@ -23,10 +23,6 @@ pub struct TxGraveyard {
}
impl TxGraveyard {
pub fn contains(&self, txid: &Txid) -> bool {
self.tombstones.contains_key(txid)
}
pub fn tombstones_len(&self) -> usize {
self.tombstones.len()
}
@@ -39,6 +35,25 @@ impl TxGraveyard {
self.tombstones.get(txid)
}
/// Tombstone iff the tx vanished from the pool (mined, expired, or
/// dropped). `Replaced` tombstones return `None` because the tx
/// will not confirm.
pub fn get_vanished(&self, txid: &Txid) -> Option<&TxTombstone> {
let tomb = self.tombstones.get(txid)?;
matches!(tomb.removal, TxRemoval::Vanished).then_some(tomb)
}
/// Walk forward through `Replaced { by }` to the terminal replacer.
/// Returns `txid` itself if it isn't replaced (live or `Vanished`).
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
}
/// Tombstones marked as `Replaced { by: replacer }`. Used to walk
/// backward through RBF history: given a tx that's still live (or
/// in the graveyard), find every tx it displaced.
@@ -61,18 +76,33 @@ impl TxGraveyard {
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 {
if ts.removed_at != *t {
return None;
}
Some((txid, ts.replaced_by()?))
})
}
pub fn bury(&mut self, txid: Txid, tx: Transaction, entry: TxEntry, removal: TxRemoval) {
let now = Instant::now();
self.tombstones
.insert(txid, TxTombstone::new(tx, entry, removal, now));
self.order.push_back((now, txid));
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));
}
/// Remove and return the tombstone, e.g. when the tx comes back to life.
@@ -92,7 +122,7 @@ impl TxGraveyard {
}
let (_, txid) = self.order.pop_front().unwrap();
if let Some(ts) = self.tombstones.get(&txid)
&& ts.removed_at() == t
&& ts.removed_at == t
{
self.tombstones.remove(&txid);
}
@@ -1,41 +1,23 @@
use std::time::Instant;
use brk_types::{Transaction, Txid};
use brk_types::{FeeRate, Transaction, Txid};
use crate::{TxEntry, TxRemoval};
/// A buried mempool tx, retained for reappearance detection and
/// post-mine analytics.
/// post-mine analytics. `chunk_rate` is the linearized chunk feerate at
/// burial time - same value `live_effective_fee_rate` reported while
/// the tx was alive, so an evicted RBF predecessor reports the
/// package-effective rate, not a misleading isolated `fee/vsize`.
pub struct TxTombstone {
pub tx: Transaction,
pub entry: TxEntry,
removal: TxRemoval,
removed_at: Instant,
pub chunk_rate: FeeRate,
pub removal: TxRemoval,
pub removed_at: Instant,
}
impl TxTombstone {
pub(crate) fn new(
tx: Transaction,
entry: TxEntry,
removal: TxRemoval,
removed_at: Instant,
) -> Self {
Self {
tx,
entry,
removal,
removed_at,
}
}
pub fn reason(&self) -> &TxRemoval {
&self.removal
}
pub(crate) fn removed_at(&self) -> Instant {
self.removed_at
}
pub(crate) fn replaced_by(&self) -> Option<&Txid> {
match &self.removal {
TxRemoval::Replaced { by } => Some(by),
@@ -34,10 +34,6 @@ impl TxStore {
self.records.len()
}
pub fn is_empty(&self) -> bool {
self.records.is_empty()
}
pub fn get(&self, txid: &Txid) -> Option<&Transaction> {
self.records.get(&TxidPrefix::from(txid)).map(|r| &r.tx)
}