mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-05-19 22:34:46 -07:00
global + blk
This commit is contained in:
2
crates/brk_mempool/.gitignore
vendored
Normal file
2
crates/brk_mempool/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*.md
|
||||
!README.md
|
||||
@@ -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>,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>,
|
||||
|
||||
Reference in New Issue
Block a user