mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-05-19 22:34:46 -07:00
mempool: fixes
This commit is contained in:
@@ -4,8 +4,8 @@ use bitcoin::{Network, PublicKey, ScriptBuf};
|
||||
use brk_error::{Error, OptionData, Result};
|
||||
use brk_types::{
|
||||
Addr, AddrBytes, AddrChainStats, AddrHash, AddrIndexOutPoint, AddrIndexTxIndex, AddrStats,
|
||||
AnyAddrDataIndexEnum, Dollars, Height, OutputType, Timestamp, Transaction, TxIndex, TxStatus,
|
||||
Txid, TxidPrefix, TypeIndex, Unit, Utxo, Vout,
|
||||
AnyAddrDataIndexEnum, Dollars, Height, OutputType, Transaction, TxIndex, TxStatus, Txid,
|
||||
TypeIndex, Unit, Utxo, Vout,
|
||||
};
|
||||
use vecdb::VecIndex;
|
||||
|
||||
@@ -80,7 +80,7 @@ impl Query {
|
||||
},
|
||||
mempool_stats: self
|
||||
.mempool()
|
||||
.and_then(|m| m.addrs().get(&bytes).map(|e| e.stats.clone()))
|
||||
.and_then(|m| m.addr_stats(&bytes))
|
||||
.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
@@ -233,29 +233,7 @@ impl Query {
|
||||
pub fn addr_mempool_txs(&self, addr: &Addr, limit: usize) -> Result<Vec<Transaction>> {
|
||||
let bytes = AddrBytes::from_str(addr)?;
|
||||
let mempool = self.mempool().ok_or(Error::MempoolNotAvailable)?;
|
||||
let addrs = mempool.addrs();
|
||||
let Some(entry) = addrs.get(&bytes) else {
|
||||
return Ok(vec![]);
|
||||
};
|
||||
let entries = mempool.entries();
|
||||
let mut ordered: Vec<(Timestamp, &Txid)> = entry
|
||||
.txids
|
||||
.iter()
|
||||
.map(|txid| {
|
||||
let first_seen = entries
|
||||
.get(&TxidPrefix::from(txid))
|
||||
.map(|e| e.first_seen)
|
||||
.unwrap_or_default();
|
||||
(first_seen, txid)
|
||||
})
|
||||
.collect();
|
||||
ordered.sort_unstable_by_key(|b| std::cmp::Reverse(b.0));
|
||||
let txs = mempool.txs();
|
||||
Ok(ordered
|
||||
.into_iter()
|
||||
.filter_map(|(_, txid)| txs.get(txid).cloned())
|
||||
.take(limit)
|
||||
.collect())
|
||||
Ok(mempool.addr_txs(&bytes, limit))
|
||||
}
|
||||
|
||||
/// Height of the last on-chain activity for an address (last tx_index → height).
|
||||
|
||||
@@ -62,16 +62,10 @@ impl Query {
|
||||
pub fn effective_fee_rate(&self, txid: &Txid) -> Result<FeeRate> {
|
||||
let prefix = TxidPrefix::from(txid);
|
||||
|
||||
if let Some(mempool) = self.mempool() {
|
||||
let entries = mempool.entries();
|
||||
if let Some(seed_idx) = entries.idx_of(&prefix)
|
||||
&& let Some(rate) = mempool.snapshot().chunk_rate_of(seed_idx)
|
||||
{
|
||||
return Ok(rate);
|
||||
}
|
||||
if let Some(entry) = entries.get(&prefix) {
|
||||
return Ok(entry.fee_rate());
|
||||
}
|
||||
if let Some(mempool) = self.mempool()
|
||||
&& let Some(rate) = mempool.live_effective_fee_rate(&prefix)
|
||||
{
|
||||
return Ok(rate);
|
||||
}
|
||||
|
||||
if let Ok(idx) = self.resolve_tx_index(txid)
|
||||
@@ -87,9 +81,9 @@ impl Query {
|
||||
}
|
||||
|
||||
if let Some(mempool) = self.mempool()
|
||||
&& let Some(tomb) = mempool.graveyard().get(txid)
|
||||
&& let Some(rate) = mempool.graveyard_fee_rate(txid)
|
||||
{
|
||||
return Ok(tomb.entry.fee_rate());
|
||||
return Ok(rate);
|
||||
}
|
||||
|
||||
Err(Error::UnknownTxid)
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
use brk_error::{Error, Result};
|
||||
use brk_mempool::{EntryPool, Mempool, TxEntry, TxGraveyard, TxRemoval, TxStore, TxTombstone};
|
||||
use brk_mempool::{Mempool, RbfForTx, RbfNode};
|
||||
use brk_types::{
|
||||
CheckedSub, MempoolBlock, MempoolInfo, MempoolRecentTx, OutputType, RbfResponse, RbfTx,
|
||||
RecommendedFees, ReplacementNode, Sats, Timestamp, Transaction, TxOut, TxOutIndex, Txid,
|
||||
TxidPrefix, TypeIndex,
|
||||
CheckedSub, FeeRate, MempoolBlock, MempoolInfo, MempoolRecentTx, OutputType, RbfResponse, RbfTx,
|
||||
RecommendedFees, ReplacementNode, Sats, Timestamp, TxOut, TxOutIndex, Txid, TxidPrefix,
|
||||
TypeIndex,
|
||||
};
|
||||
use rustc_hash::FxHashSet;
|
||||
use vecdb::VecIndex;
|
||||
|
||||
use crate::Query;
|
||||
@@ -22,8 +21,7 @@ impl Query {
|
||||
}
|
||||
|
||||
pub fn mempool_txids(&self) -> Result<Vec<Txid>> {
|
||||
let txs = self.require_mempool()?.txs();
|
||||
Ok(txs.keys().cloned().collect())
|
||||
Ok(self.require_mempool()?.txids())
|
||||
}
|
||||
|
||||
pub fn recommended_fees(&self) -> Result<RecommendedFees> {
|
||||
@@ -100,159 +98,89 @@ impl Query {
|
||||
}
|
||||
|
||||
pub fn mempool_recent(&self) -> Result<Vec<MempoolRecentTx>> {
|
||||
Ok(self.require_mempool()?.txs().recent().to_vec())
|
||||
Ok(self.require_mempool()?.recent_txs())
|
||||
}
|
||||
|
||||
/// RBF history for a tx, matching mempool.space's
|
||||
/// `GET /api/v1/tx/:txid/rbf`. Walks forward through the graveyard
|
||||
/// to find the latest known replacer (tree root), then recursively
|
||||
/// walks `predecessors_of` backward to build the tree. `replaces`
|
||||
/// is the requested tx's own direct predecessors.
|
||||
/// `GET /api/v1/tx/:txid/rbf`. The mempool builds the owned
|
||||
/// replacement tree (terminal replacer + recursive predecessors)
|
||||
/// under one read-lock window; this method then enriches each node
|
||||
/// with `mined` + effective fee rate, both of which need the
|
||||
/// indexer/computer.
|
||||
pub fn tx_rbf(&self, txid: &Txid) -> Result<RbfResponse> {
|
||||
let mempool = self.require_mempool()?;
|
||||
let txs = mempool.txs();
|
||||
let entries = mempool.entries();
|
||||
let graveyard = mempool.graveyard();
|
||||
|
||||
let root_txid = Self::walk_to_replacement_root(&graveyard, *txid);
|
||||
|
||||
let replaces_vec: Vec<Txid> = graveyard.predecessors_of(txid).map(|(p, _)| *p).collect();
|
||||
let replaces = (!replaces_vec.is_empty()).then_some(replaces_vec);
|
||||
|
||||
let replacements = self.build_rbf_node(&root_txid, None, &txs, &entries, &graveyard);
|
||||
|
||||
let RbfForTx { root, replaces } = self.require_mempool()?.rbf_for_tx(txid);
|
||||
let replacements = root.map(|n| self.enrich_rbf_node(n, None));
|
||||
let replaces = (!replaces.is_empty()).then_some(replaces);
|
||||
Ok(RbfResponse {
|
||||
replacements,
|
||||
replaces,
|
||||
})
|
||||
}
|
||||
|
||||
/// Walk forward through `Replaced { by }` links to the terminal
|
||||
/// replacer of an RBF chain. Returns `txid` itself if it's already
|
||||
/// the root.
|
||||
fn walk_to_replacement_root(graveyard: &TxGraveyard, mut root: Txid) -> Txid {
|
||||
while let Some(TxRemoval::Replaced { by }) = graveyard.get(&root).map(TxTombstone::reason) {
|
||||
root = *by;
|
||||
}
|
||||
root
|
||||
}
|
||||
|
||||
/// Resolve a txid to the data we need for an `RbfTx`. The live
|
||||
/// pool takes priority; the graveyard is the fallback. Returns
|
||||
/// `None` if the tx has no known data in either.
|
||||
fn resolve_rbf_node_data<'a>(
|
||||
txid: &Txid,
|
||||
txs: &'a TxStore,
|
||||
entries: &'a EntryPool,
|
||||
graveyard: &'a TxGraveyard,
|
||||
) -> Option<(&'a Transaction, &'a TxEntry)> {
|
||||
if let (Some(tx), Some(entry)) = (txs.get(txid), entries.get(&TxidPrefix::from(txid))) {
|
||||
return Some((tx, entry));
|
||||
}
|
||||
graveyard.get(txid).map(|tomb| (&tomb.tx, &tomb.entry))
|
||||
}
|
||||
|
||||
/// Recursively build an RBF tree node rooted at `txid`.
|
||||
/// Predecessors are always in the graveyard (that's where
|
||||
/// `Removal::Replaced` lives), so the recursion only needs the
|
||||
/// graveyard; the live pool is consulted for the root.
|
||||
///
|
||||
/// `rate` matches mempool.space's `tx.effectiveFeePerVsize` via
|
||||
/// `Query::effective_fee_rate`, with a fall-back to the entry's
|
||||
/// simple `fee/vsize` when the rate lookup fails.
|
||||
fn build_rbf_node(
|
||||
&self,
|
||||
txid: &Txid,
|
||||
successor_time: Option<Timestamp>,
|
||||
txs: &TxStore,
|
||||
entries: &EntryPool,
|
||||
graveyard: &TxGraveyard,
|
||||
) -> Option<ReplacementNode> {
|
||||
let (tx, entry) = Self::resolve_rbf_node_data(txid, txs, entries, graveyard)?;
|
||||
|
||||
let replaces: Vec<ReplacementNode> = graveyard
|
||||
.predecessors_of(txid)
|
||||
.filter_map(|(pred_txid, _)| {
|
||||
self.build_rbf_node(pred_txid, Some(entry.first_seen), txs, entries, graveyard)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let full_rbf = replaces.iter().any(|c| !c.tx.rbf || c.full_rbf);
|
||||
|
||||
let interval = successor_time
|
||||
.and_then(|st| st.checked_sub(entry.first_seen))
|
||||
.map(|d| *d);
|
||||
|
||||
let value: Sats = tx.output.iter().map(|o| o.value).sum();
|
||||
let mined = self.resolve_tx_index(txid).is_ok().then_some(true);
|
||||
let rate = self
|
||||
.effective_fee_rate(txid)
|
||||
.unwrap_or_else(|_| entry.fee_rate());
|
||||
|
||||
Some(ReplacementNode {
|
||||
tx: RbfTx {
|
||||
txid: *txid,
|
||||
fee: entry.fee,
|
||||
vsize: entry.vsize,
|
||||
value,
|
||||
rate,
|
||||
time: entry.first_seen,
|
||||
rbf: entry.rbf,
|
||||
full_rbf: Some(full_rbf),
|
||||
mined,
|
||||
},
|
||||
time: entry.first_seen,
|
||||
full_rbf,
|
||||
interval,
|
||||
mined,
|
||||
replaces,
|
||||
})
|
||||
}
|
||||
|
||||
/// Recent RBF replacements across the whole mempool, matching
|
||||
/// mempool.space's `GET /api/v1/replacements` and
|
||||
/// `GET /api/v1/fullrbf/replacements`. Each entry is a complete
|
||||
/// replacement tree rooted at the terminal replacer; same shape as
|
||||
/// `tx_rbf().replacements`. Ordered by most-recent replacement
|
||||
/// event first (matches mempool.space's reversed-`replacedBy`
|
||||
/// iteration) and capped at 25 entries. When `full_rbf_only` is
|
||||
/// event first and capped at 25 entries. When `full_rbf_only` is
|
||||
/// true, only trees with at least one non-signaling predecessor
|
||||
/// are returned.
|
||||
pub fn recent_replacements(&self, full_rbf_only: bool) -> Result<Vec<ReplacementNode>> {
|
||||
let mempool = self.require_mempool()?;
|
||||
let txs = mempool.txs();
|
||||
let entries = mempool.entries();
|
||||
let graveyard = mempool.graveyard();
|
||||
|
||||
// A predecessor's `by` may itself be replaced; walk the chain
|
||||
// forward to the terminal replacer for each tree, dedup so each
|
||||
// tree is emitted once at its first (most recent) sighting.
|
||||
let mut seen: FxHashSet<Txid> = FxHashSet::default();
|
||||
Ok(graveyard
|
||||
.replaced_iter_recent_first()
|
||||
.filter_map(|(_, by)| {
|
||||
let root = Self::walk_to_replacement_root(&graveyard, *by);
|
||||
seen.insert(root).then_some(root)
|
||||
})
|
||||
.filter_map(|root| self.build_rbf_node(&root, None, &txs, &entries, &graveyard))
|
||||
.filter(|node| !full_rbf_only || node.full_rbf)
|
||||
.take(RECENT_REPLACEMENTS_LIMIT)
|
||||
Ok(self
|
||||
.require_mempool()?
|
||||
.recent_rbf_trees(full_rbf_only, RECENT_REPLACEMENTS_LIMIT)
|
||||
.into_iter()
|
||||
.map(|n| self.enrich_rbf_node(n, None))
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Layer indexer-resident data (`mined`, effective fee rate) onto
|
||||
/// a `RbfNode` tree. Runs after the mempool lock window has closed
|
||||
/// because `effective_fee_rate` re-enters `Mempool` and would
|
||||
/// recursively acquire the same read locks otherwise.
|
||||
fn enrich_rbf_node(
|
||||
&self,
|
||||
node: RbfNode,
|
||||
successor_time: Option<Timestamp>,
|
||||
) -> ReplacementNode {
|
||||
let interval = successor_time
|
||||
.and_then(|st| st.checked_sub(node.first_seen))
|
||||
.map(|d| *d);
|
||||
let mined = self.resolve_tx_index(&node.txid).is_ok().then_some(true);
|
||||
let rate = self
|
||||
.effective_fee_rate(&node.txid)
|
||||
.unwrap_or_else(|_| FeeRate::from((node.fee, node.vsize)));
|
||||
let first_seen = node.first_seen;
|
||||
let replaces = node
|
||||
.replaces
|
||||
.into_iter()
|
||||
.map(|child| self.enrich_rbf_node(child, Some(first_seen)))
|
||||
.collect();
|
||||
ReplacementNode {
|
||||
tx: RbfTx {
|
||||
txid: node.txid,
|
||||
fee: node.fee,
|
||||
vsize: node.vsize,
|
||||
value: node.value,
|
||||
rate,
|
||||
time: first_seen,
|
||||
rbf: node.rbf,
|
||||
full_rbf: Some(node.full_rbf),
|
||||
mined,
|
||||
},
|
||||
time: first_seen,
|
||||
full_rbf: node.full_rbf,
|
||||
interval,
|
||||
mined,
|
||||
replaces,
|
||||
}
|
||||
}
|
||||
|
||||
/// `first_seen` Unix-second timestamps for each txid, matching
|
||||
/// mempool.space's `POST /api/v1/transaction-times`. Returns 0 for
|
||||
/// unknown txids, in input order.
|
||||
pub fn transaction_times(&self, txids: &[Txid]) -> Result<Vec<u64>> {
|
||||
let entries = self.require_mempool()?.entries();
|
||||
Ok(txids
|
||||
.iter()
|
||||
.map(|txid| {
|
||||
entries
|
||||
.get(&TxidPrefix::from(txid))
|
||||
.map_or(0, |e| u64::from(e.first_seen))
|
||||
})
|
||||
.collect())
|
||||
Ok(self.require_mempool()?.transaction_times(txids))
|
||||
}
|
||||
|
||||
/// Opaque content hash that changes whenever the projected next
|
||||
|
||||
@@ -11,12 +11,7 @@ impl Query {
|
||||
let mut oracle = self.computer().prices.live_oracle(self.indexer())?;
|
||||
|
||||
if let Some(mempool) = self.mempool() {
|
||||
let txs = mempool.txs();
|
||||
oracle.process_outputs(
|
||||
txs.values()
|
||||
.flat_map(|tx| &tx.output)
|
||||
.map(|txout| (txout.value, txout.type_())),
|
||||
);
|
||||
mempool.process_live_outputs(|iter| oracle.process_outputs(iter));
|
||||
}
|
||||
|
||||
Ok(oracle.price_dollars())
|
||||
|
||||
@@ -97,21 +97,40 @@ impl Query {
|
||||
|
||||
// ── Transaction queries ────────────────────────────────────────
|
||||
|
||||
/// Map a mempool transaction by txid through `f`, returning `None`
|
||||
/// if no mempool is attached or the txid is not in mempool.
|
||||
fn map_mempool_tx<R>(&self, txid: &Txid, f: impl FnOnce(&Transaction) -> R) -> Option<R> {
|
||||
self.mempool()?.txs().get(txid).map(f)
|
||||
/// Resolve a tx body across the three sources in order: live mempool,
|
||||
/// indexer (via `indexed`), then `Vanished` graveyard tombstone.
|
||||
/// The graveyard fallback only fires when the indexer reports
|
||||
/// `UnknownTxid`, covering the brief race where a mined tx has been
|
||||
/// buried by `Applier` but `safe_lengths.tx_index` has not yet
|
||||
/// advanced to cover it. `Replaced` tombstones are excluded — those
|
||||
/// txs will never confirm.
|
||||
fn lookup_tx<R>(
|
||||
&self,
|
||||
txid: &Txid,
|
||||
f: impl Fn(&Transaction) -> R,
|
||||
indexed: impl FnOnce(TxIndex) -> Result<R>,
|
||||
) -> Result<R> {
|
||||
if let Some(mempool) = self.mempool()
|
||||
&& let Some(r) = mempool.with_tx(txid, &f)
|
||||
{
|
||||
return Ok(r);
|
||||
}
|
||||
match self.resolve_tx_index_bounded(txid) {
|
||||
Ok(idx) => indexed(idx),
|
||||
Err(Error::UnknownTxid) => self
|
||||
.mempool()
|
||||
.and_then(|m| m.with_vanished_tx(txid, &f))
|
||||
.ok_or(Error::UnknownTxid),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn transaction(&self, txid: &Txid) -> Result<Transaction> {
|
||||
if let Some(tx) = self.map_mempool_tx(txid, Transaction::clone) {
|
||||
return Ok(tx);
|
||||
}
|
||||
self.transaction_by_index(self.resolve_tx_index_bounded(txid)?)
|
||||
self.lookup_tx(txid, Transaction::clone, |idx| self.transaction_by_index(idx))
|
||||
}
|
||||
|
||||
pub fn transaction_status(&self, txid: &Txid) -> Result<TxStatus> {
|
||||
if self.mempool().is_some_and(|m| m.txs().contains_key(txid)) {
|
||||
if self.mempool().is_some_and(|m| m.contains_txid(txid)) {
|
||||
return Ok(TxStatus::UNCONFIRMED);
|
||||
}
|
||||
let (_, height) = self.resolve_tx(txid)?;
|
||||
@@ -119,23 +138,23 @@ impl Query {
|
||||
}
|
||||
|
||||
pub fn transaction_raw(&self, txid: &Txid) -> Result<Vec<u8>> {
|
||||
if let Some(bytes) = self.map_mempool_tx(txid, Transaction::encode_bytes) {
|
||||
return Ok(bytes);
|
||||
}
|
||||
self.transaction_raw_by_index(self.resolve_tx_index_bounded(txid)?)
|
||||
self.lookup_tx(txid, Transaction::encode_bytes, |idx| {
|
||||
self.transaction_raw_by_index(idx)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn transaction_hex(&self, txid: &Txid) -> Result<String> {
|
||||
if let Some(hex) = self.map_mempool_tx(txid, |tx| tx.encode_bytes().to_lower_hex_string()) {
|
||||
return Ok(hex);
|
||||
}
|
||||
self.transaction_hex_by_index(self.resolve_tx_index_bounded(txid)?)
|
||||
self.lookup_tx(
|
||||
txid,
|
||||
|tx| tx.encode_bytes().to_lower_hex_string(),
|
||||
|idx| self.transaction_hex_by_index(idx),
|
||||
)
|
||||
}
|
||||
|
||||
// ── Outspend queries ───────────────────────────────────────────
|
||||
|
||||
pub fn outspend(&self, txid: &Txid, vout: Vout) -> Result<TxOutspend> {
|
||||
if self.mempool().is_some_and(|m| m.txs().contains_key(txid)) {
|
||||
if self.mempool().is_some_and(|m| m.contains_txid(txid)) {
|
||||
return Ok(self.mempool_outspend(txid, vout));
|
||||
}
|
||||
let (_, first_txout, output_count) = self.resolve_tx_outputs(txid)?;
|
||||
@@ -151,7 +170,7 @@ impl Query {
|
||||
|
||||
pub fn outspends(&self, txid: &Txid) -> Result<Vec<TxOutspend>> {
|
||||
if let Some(mempool) = self.mempool()
|
||||
&& let Some(output_count) = mempool.txs().get(txid).map(|tx| tx.output.len())
|
||||
&& let Some(output_count) = mempool.with_tx(txid, |tx| tx.output.len())
|
||||
{
|
||||
return Ok((0..output_count)
|
||||
.map(|i| self.mempool_outspend(txid, Vout::from(i)))
|
||||
|
||||
Reference in New Issue
Block a user