global + blk

This commit is contained in:
nym21
2026-05-07 14:02:53 +02:00
parent 9347b42c9a
commit cc9ebfaf42
30 changed files with 1044 additions and 140 deletions

2
crates/brk_mempool/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
*.md
!README.md

View File

@@ -96,14 +96,12 @@ impl Mempool {
self.0.state.addrs.read().stats_hash(addr)
}
/// Look up the mempool tx that spends `(txid, vout)`. Returns
/// `(spender_txid, vin)` if the outpoint is spent in the mempool,
/// `None` otherwise. The spender's input list is walked to rule
/// out a `TxidPrefix` collision before returning a match.
/// Mempool tx spending `(txid, vout)`, or `None`. The spender's
/// input list is walked to rule out `TxidPrefix` collisions.
pub fn lookup_spender(&self, txid: &Txid, vout: Vout) -> Option<(Txid, Vin)> {
let key = OutpointPrefix::new(TxidPrefix::from(txid), vout);
let txs = self.0.state.txs.read();
let entries = self.0.state.entries.read();
let txs = self.txs();
let entries = self.entries();
let outpoint_spends = self.0.state.outpoint_spends.read();
let idx = outpoint_spends.get(&key)?;
let spender_txid = entries.slot(idx)?.txid;
@@ -168,9 +166,7 @@ impl Mempool {
}
/// Live mempool txs touching `addr`, newest first by `first_seen`,
/// capped at `limit`. Acquires `txs`, `addrs`, `entries` in canonical
/// order; returns owned `Transaction`s so the lock is released
/// before the caller does anything else with them.
/// capped at `limit`. Returns owned `Transaction`s.
pub fn addr_txs(&self, addr: &AddrBytes, limit: usize) -> Vec<Transaction> {
let txs = self.txs();
let addrs = self.addrs();
@@ -211,9 +207,8 @@ impl Mempool {
f(&mut iter)
}
/// Effective fee rate for a live mempool tx: the seed's chunk rate from
/// the latest snapshot, with fall-back to the entry's simple `fee/vsize`
/// when the snapshot doesn't yet contain it.
/// Effective fee rate for a live tx: seed's snapshot chunk rate,
/// falling back to the entry's `fee/vsize` if not yet in the snapshot.
pub fn live_effective_fee_rate(&self, prefix: &TxidPrefix) -> Option<FeeRate> {
let entries = self.entries();
if let Some(seed_idx) = entries.idx_of(prefix)
@@ -232,9 +227,9 @@ impl Mempool {
}
/// `first_seen` Unix-second timestamps for `txids`, in input order.
/// Returns 0 for unknown txids. `Vanished` graveyard tombstones fall
/// back to the buried entry's `first_seen` so a tx doesn't flicker
/// to 0 in the brief window between mempool drop and indexer catch-up.
/// Returns 0 for unknown txids. `Vanished` tombstones fall back to
/// the buried entry's `first_seen` to avoid flicker between drop
/// and indexer catch-up.
pub fn transaction_times(&self, txids: &[Txid]) -> Vec<u64> {
let entries = self.entries();
let graveyard = self.graveyard();
@@ -260,11 +255,8 @@ impl Mempool {
}
/// Variant of `start` that runs `after_update` after every cycle.
///
/// `update` and `after_update` are wrapped in `catch_unwind` so an
/// unexpected panic in either step doesn't kill the loop and freeze
/// the mempool snapshot. `parking_lot` locks don't poison, so state
/// remains usable after a panic.
/// Both steps are wrapped in `catch_unwind` so a panic doesn't
/// freeze the snapshot; `parking_lot` locks don't poison.
pub fn start_with(&self, mut after_update: impl FnMut()) {
loop {
let outcome = catch_unwind(AssertUnwindSafe(|| {
@@ -288,9 +280,8 @@ impl Mempool {
}
/// Fill remaining `prevout == None` inputs via an external
/// resolver (typically the brk indexer for confirmed parents).
/// Same-cycle in-mempool parents are filled automatically by
/// `Resolver::resolve_in_mempool` after each `Applier::apply`.
/// resolver (typically the indexer for confirmed parents).
/// In-mempool parents are filled automatically each cycle.
pub fn fill_prevouts<F>(&self, resolver: F) -> bool
where
F: Fn(&Txid, Vout) -> Option<TxOut>,

View File

@@ -1,13 +1,7 @@
//! RBF (Replace-By-Fee) tree extraction from the live mempool +
//! graveyard.
//!
//! Both methods return owned, lock-free `RbfNode` trees so the caller
//! (typically `brk_query`) can enrich each node with indexer-resident
//! data (`mined`, effective fee rate) after the mempool lock window
//! closes. Doing the enrichment under the lock would re-enter
//! `Mempool` indirectly via `effective_fee_rate` and recursively
//! acquire the same `entries`/`graveyard` read locks, which can
//! deadlock against a queued writer in `parking_lot`.
//! RBF tree extraction. Returns owned trees so the caller can enrich
//! with indexer data (`mined`, effective fee rate) after the lock
//! drops: enriching under the lock re-enters `Mempool` and would
//! recursively acquire the same read locks.
use brk_types::{Sats, Timestamp, Transaction, Txid, TxidPrefix, VSize};
use rustc_hash::FxHashSet;
@@ -17,15 +11,11 @@ use crate::{
stores::{EntryPool, TxGraveyard},
};
/// One node in an RBF replacement tree, populated entirely from
/// mempool state. The caller layers on `mined` and effective fee rate
/// after the lock has been released.
#[derive(Debug, Clone)]
pub struct RbfNode {
pub txid: Txid,
pub fee: Sats,
pub vsize: VSize,
/// Sum of the tx's output amounts.
pub value: Sats,
pub first_seen: Timestamp,
/// BIP-125 signaling: at least one input has sequence < 0xffffffff-1.
@@ -35,21 +25,18 @@ pub struct RbfNode {
pub replaces: Vec<RbfNode>,
}
/// Result of [`Mempool::rbf_for_tx`].
#[derive(Debug, Clone, Default)]
pub struct RbfForTx {
/// Tree rooted at the requested tx's terminal replacer. `None` if
/// the tx is unknown to both the live pool and the graveyard.
/// Tree rooted at the terminal replacer. `None` if `txid` is unknown.
pub root: Option<RbfNode>,
/// Direct predecessors of the requested tx (txids only).
pub replaces: Vec<Txid>,
}
impl Mempool {
/// Build the RBF tree relevant to `txid`: walk forward through
/// `Replaced { by }` links to the terminal replacer, return its
/// full predecessor tree, plus the requested tx's own direct
/// predecessors. Single read-lock window in canonical order.
/// Walk forward through `Replaced { by }` to the terminal replacer
/// and return its full predecessor tree, plus the requested tx's
/// direct predecessors. Single read-lock window in canonical order.
pub fn rbf_for_tx(&self, txid: &Txid) -> RbfForTx {
let txs = self.txs();
let entries = self.entries();
@@ -61,10 +48,9 @@ impl Mempool {
RbfForTx { root, replaces }
}
/// Recent terminal-replacer trees, most-recent replacement first,
/// deduplicated by tree root, capped at `limit`. When
/// `full_rbf_only` is true, drops trees with no non-signaling
/// predecessor anywhere.
/// Recent terminal-replacer trees, most-recent first, deduplicated
/// by root, capped at `limit`. `full_rbf_only` drops trees with no
/// non-signaling predecessor.
pub fn recent_rbf_trees(&self, full_rbf_only: bool, limit: usize) -> Vec<RbfNode> {
let txs = self.txs();
let entries = self.entries();

View File

@@ -17,8 +17,6 @@ pub struct MempoolStats {
}
impl From<&Mempool> for MempoolStats {
/// Acquires every sub-lock in canonical order to build a coherent
/// snapshot. Cheap; locks are released as soon as the counts are read.
fn from(mempool: &Mempool) -> Self {
let state = mempool.state();
let info = state.info.read();

View File

@@ -37,17 +37,10 @@ impl Applier {
return;
};
if !s.txs.contains(&txid) {
// entries had this prefix but txs didn't — a state divergence
// that should be impossible: publish/bury both touch them
// together under one write_all guard. Reaching this branch
// means a prior cycle left the two stores out of sync (e.g.
// panic mid-publish before `txs.extend` ran). Skip the bury
// entirely: freeing the entries slot here would let
// outpoint_spends point at a slot the next insert recycles
// for an unrelated tx.
warn!(
"mempool bury: entry present but tx missing for txid={txid} - skipping bury to preserve outpoint_spends integrity"
);
// Skip bury on entries/txs divergence: freeing the slot here
// would let outpoint_spends point at a slot the next insert
// recycles for an unrelated tx.
warn!("mempool bury: entry present but tx missing for txid={txid}");
return;
}
let (idx, entry) = s.entries.remove(prefix).expect("entry present");

View File

@@ -89,10 +89,9 @@ impl Rebuilder {
}
}
/// Returns true iff dirty and the throttle window has elapsed. On
/// success, starts a new throttle window. The dirty bit is cleared
/// by `tick` only after `publish` returns, so a panic in
/// `build_snapshot` leaves dirty set and the next cycle retries.
/// True iff dirty and the throttle window has elapsed. The dirty
/// bit is cleared in `tick` only after `publish` returns, so a
/// panic in `build_snapshot` retries on the next cycle.
fn try_claim_rebuild(&self) -> bool {
if !self.dirty.load(Ordering::Acquire) {
self.skip_clean.fetch_add(1, Ordering::Relaxed);

View File

@@ -3,23 +3,11 @@ use parking_lot::{RwLock, RwLockWriteGuard};
use super::{AddrTracker, EntryPool, OutpointSpends, TxGraveyard, TxStore};
/// The six buckets making up live mempool state.
///
/// Each bucket has its own `RwLock` so readers of different buckets
/// don't contend with each other. Any code that takes more than one
/// lock must follow the canonical partial order
/// `info → txs → addrs → entries → outpoint_spends → graveyard`,
/// otherwise a reader-holds-A-wants-B / writer-holds-B-wants-A
/// circular wait can deadlock. The Applier takes all six write locks
/// in this order for a brief window once per cycle via
/// [`MempoolState::write_all`]; multi-lock readers inside the crate
/// take a (canonical-order) subset inline.
///
/// This discipline is *internal* to `brk_mempool`: external crates
/// only see `Mempool` methods that bundle each multi-lock operation
/// behind a single call (e.g. `Mempool::lookup_spender`,
/// `Mempool::addr_txs`, `Mempool::rbf_for_tx`), so callers can never
/// take the order wrong because they don't get to choose.
/// The six buckets making up live mempool state. Each has its own
/// `RwLock`. Multi-lock code must follow the canonical order
/// `info → txs → addrs → entries → outpoint_spends → graveyard` to
/// avoid circular waits. External callers go through bundled
/// `Mempool` methods so they can't take the order wrong.
#[derive(Default)]
pub struct MempoolState {
pub(crate) info: RwLock<MempoolInfo>,