global: fixes

This commit is contained in:
nym21
2026-05-04 16:57:21 +02:00
parent 4663d13194
commit dc32bd480f
121 changed files with 2996 additions and 2165 deletions
+346
View File
@@ -0,0 +1,346 @@
//! CPFP queries: dispatches between the live mempool path (handled by
//! `brk_mempool`) and the confirmed-tx path built here from indexer
//! and computer vecs.
//!
//! Confirmed clusters are built on demand by walking the same-block
//! parent/child edges in `TxIndex` space (no `Transaction`
//! reconstruction, no `txid → tx_index` lookup), then handing the
//! resulting `brk_mempool::cluster::Cluster` to `Cluster::to_cpfp_info`
//! — the same wire converter the mempool path uses, so both produce
//! identical `CpfpInfo` shapes.
use std::io::Cursor;
use bitcoin::consensus::Decodable;
use brk_error::{Error, OptionData, Result};
use brk_mempool::cluster::{Cluster, ClusterNode, LocalIdx};
use brk_types::{
CpfpInfo, FeeRate, Height, OutPoint, OutputType, Sats, SigOps, TxIndex, TxInIndex, TypeIndex,
Txid, TxidPrefix, VSize, Weight,
};
use rustc_hash::{FxBuildHasher, FxHashMap};
use smallvec::SmallVec;
use vecdb::{AnyVec, ReadableVec, VecIndex};
use crate::Query;
/// Cap matches Bitcoin Core's default mempool ancestor/descendant
/// chain limits and mempool.space's truncation.
const MAX: usize = 25;
struct WalkResult {
/// Cluster members in build order (`[seed, ancestors..., descendants...]`),
/// each paired with its in-cluster parent edges already resolved to
/// `LocalIdx`. Vec position equals the node's `LocalIdx`.
nodes: Vec<(TxIndex, SmallVec<[LocalIdx; 2]>)>,
/// Pre-permutation `LocalIdx` of the seed. Equals `ancestor_count`
/// because all of seed's in-cluster ancestors topo-sort before it
/// and only ancestors do, so after `Cluster::new` permutes nodes
/// into topological order seed lands at this exact position.
seed_local: LocalIdx,
}
impl Query {
/// CPFP cluster for `txid`. Returns the mempool cluster when the
/// txid is unconfirmed; otherwise reconstructs the confirmed
/// same-block cluster from indexer state. Works even when the
/// mempool feature is off.
pub fn cpfp(&self, txid: &Txid) -> Result<CpfpInfo> {
let prefix = TxidPrefix::from(txid);
if let Some(info) = self.mempool().and_then(|m| m.cpfp_info(&prefix)) {
return Ok(info);
}
self.confirmed_cpfp(txid)
}
/// Effective fee rate for `txid` using the same SFL chunk-rate
/// semantics across paths:
///
/// - Live mempool: snapshot `cluster_of` lookup → seed's chunk rate.
/// If the tx is in the pool but not in the latest snapshot (e.g.
/// just added), falls back to the entry's simple `fee/vsize`.
/// - Confirmed: precomputed `effective_fee_rate.tx_index` (the same
/// SFL chunk rate, computed at index time).
/// - Graveyard-only RBF predecessor: simple `fee/vsize` snapshotted
/// at burial.
///
/// Returns `Error::UnknownTxid` for txids not seen in any of those.
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 Ok(idx) = self.resolve_tx_index(txid)
&& let Some(rate) = self
.computer()
.transactions
.fees
.effective_fee_rate
.tx_index
.collect_one(idx)
{
return Ok(rate);
}
if let Some(mempool) = self.mempool()
&& let Some(tomb) = mempool.graveyard().get(txid)
{
return Ok(tomb.entry.fee_rate());
}
Err(Error::UnknownTxid)
}
/// CPFP cluster for a confirmed tx: the connected component of
/// same-block parent/child edges, walked on demand. SFL runs on
/// the result so `effectiveFeePerVsize` matches the live path's
/// chunk-rate semantics.
fn confirmed_cpfp(&self, txid: &Txid) -> Result<CpfpInfo> {
let seed = self.resolve_tx_index(txid)?;
let height = self.confirmed_status_height(seed)?;
let (cluster, seed_local) = self.build_confirmed_cluster(seed, height)?;
let sigops = self.seed_sigop_cost(seed)?;
Ok(cluster.to_cpfp_info(seed_local, sigops))
}
/// BIP-141 sigop cost for a single confirmed tx, computed on demand:
/// re-decode the raw tx, rebuild its prevout map from `inputs.*` +
/// addr vecs, then defer the actual count to `SigOps::of_bitcoin_tx`.
/// Cost is one BLK read plus `n_inputs` cursor hops, so a few hundred
/// microseconds per CPFP request.
fn seed_sigop_cost(&self, tx_index: TxIndex) -> Result<SigOps> {
let indexer = self.indexer();
let total_size = indexer
.vecs
.transactions
.total_size
.collect_one(tx_index)
.data()?;
let position = indexer
.vecs
.transactions
.position
.collect_one(tx_index)
.data()?;
let buffer = self.reader().read_raw_bytes(position, *total_size as usize)?;
let decoded = bitcoin::Transaction::consensus_decode(&mut Cursor::new(buffer))
.map_err(|_| Error::Parse("Failed to decode transaction".into()))?;
let first_txin = indexer
.vecs
.transactions
.first_txin_index
.collect_one(tx_index)
.data()?;
let start = usize::from(first_txin);
let count = decoded.input.len();
let mut outpoint_cursor = indexer.vecs.inputs.outpoint.cursor();
let mut output_type_cursor = indexer.vecs.inputs.output_type.cursor();
let mut type_index_cursor = indexer.vecs.inputs.type_index.cursor();
let mut value_cursor = self.computer().inputs.spent.value.cursor();
let addr_readers = indexer.vecs.addrs.addr_readers();
let mut prevout_map: FxHashMap<bitcoin::OutPoint, bitcoin::TxOut> =
FxHashMap::with_capacity_and_hasher(count, FxBuildHasher);
for (j, txin) in decoded.input.iter().enumerate() {
let op: OutPoint = outpoint_cursor.get(start + j).data()?;
if op.is_coinbase() {
continue;
}
let ot: OutputType = output_type_cursor.get(start + j).data()?;
let ti: TypeIndex = type_index_cursor.get(start + j).data()?;
let val: Sats = value_cursor.get(start + j).data()?;
let script_pubkey = addr_readers.script_pubkey(ot, ti);
prevout_map.insert(
txin.previous_output,
bitcoin::TxOut {
value: bitcoin::Amount::from_sat(u64::from(val)),
script_pubkey,
},
);
}
Ok(SigOps::of_bitcoin_tx(&decoded, |outpoint| {
prevout_map.get(outpoint).cloned()
}))
}
/// Walk the seed's same-block parent/child edges, materialize each
/// member's `(txid, weight, fee)` from indexer/computer cursors,
/// and build a `Cluster<TxIndex>`. The seed's `LocalIdx` comes
/// straight from the walk (`ancestor_count`), since `Cluster::new`
/// preserves the "ancestors before seed before descendants" ordering
/// that defines that index.
fn build_confirmed_cluster(
&self,
seed: TxIndex,
height: Height,
) -> Result<(Cluster<TxIndex>, LocalIdx)> {
let indexer = self.indexer();
let computer = self.computer();
let block_first = indexer
.vecs
.transactions
.first_tx_index
.collect_one(height)
.data()?;
let block_end = indexer
.vecs
.transactions
.first_tx_index
.collect_one(height.incremented())
.unwrap_or_else(|| TxIndex::from(indexer.vecs.transactions.txid.len()));
let same_block = |idx: TxIndex| idx >= block_first && idx < block_end;
let WalkResult { nodes, seed_local } = self.walk_same_block_edges(seed, same_block);
let mut base_size = indexer.vecs.transactions.base_size.cursor();
let mut total_size = indexer.vecs.transactions.total_size.cursor();
let mut fee_cursor = computer.transactions.fees.fee.tx_index.cursor();
let txid_reader = indexer.vecs.transactions.txid.reader();
let cluster_nodes: Vec<ClusterNode<TxIndex>> = nodes
.into_iter()
.map(|(tx_index, parents)| {
let i = tx_index.to_usize();
let weight = Weight::from_sizes(*base_size.get(i).data()?, *total_size.get(i).data()?);
Ok(ClusterNode {
id: tx_index,
txid: txid_reader.get(i),
fee: fee_cursor.get(i).data()?,
vsize: VSize::from(weight),
weight,
parents,
})
})
.collect::<Result<_>>()?;
Ok((Cluster::new(cluster_nodes), seed_local))
}
/// BFS the seed's same-block ancestors (via `outpoint`) and
/// descendants (via `spent.txin_index` → `spending_tx`), capped
/// at `MAX` each side to match Core/mempool.space. Each node is
/// pushed in build order with its full parent-outpoint list, then
/// at end of walk those lists are filtered against the membership
/// map to keep only in-cluster parents (resolved to `LocalIdx`).
fn walk_same_block_edges(
&self,
seed: TxIndex,
same_block: impl Fn(TxIndex) -> bool,
) -> WalkResult {
let indexer = self.indexer();
let computer = self.computer();
let mut first_txin = indexer.vecs.transactions.first_txin_index.cursor();
let mut first_txout = indexer.vecs.transactions.first_txout_index.cursor();
let mut outpoint = indexer.vecs.inputs.outpoint.cursor();
let mut spent = computer.outputs.spent.txin_index.cursor();
let mut spending_tx = indexer.vecs.inputs.tx_index.cursor();
let mut walk_inputs = |tx: TxIndex| -> SmallVec<[TxIndex; 2]> {
let mut out: SmallVec<[TxIndex; 2]> = SmallVec::new();
let Ok(start) = first_txin.get(tx.to_usize()).data() else { return out };
let Ok(end) = first_txin.get(tx.to_usize() + 1).data() else { return out };
for i in usize::from(start)..usize::from(end) {
let Ok(op) = outpoint.get(i).data() else { continue };
if op.is_coinbase() {
continue;
}
out.push(op.tx_index());
}
out
};
let mut raw: Vec<(TxIndex, SmallVec<[TxIndex; 2]>)> = Vec::with_capacity(2 * MAX + 1);
let mut local_of: FxHashMap<TxIndex, LocalIdx> =
FxHashMap::with_capacity_and_hasher(2 * MAX + 1, FxBuildHasher);
raw.push((seed, walk_inputs(seed)));
local_of.insert(seed, LocalIdx::ZERO);
// Ancestor BFS. Stack holds indices into `raw`; each pop reads
// that node's already-recorded parents and explores any same-block
// ones we haven't visited yet. `walk_inputs` runs at push time so
// parents are ready for the post-walk filter.
let mut stack: Vec<usize> = vec![0];
let mut ancestor_count: usize = 0;
'a: while let Some(idx) = stack.pop() {
let parents = raw[idx].1.clone();
for parent in parents {
if ancestor_count >= MAX {
break 'a;
}
if local_of.contains_key(&parent) || !same_block(parent) {
continue;
}
let new_idx = raw.len();
raw.push((parent, walk_inputs(parent)));
local_of.insert(parent, LocalIdx::from(new_idx));
stack.push(new_idx);
ancestor_count += 1;
}
}
// Descendant BFS. Stack holds tx_indices since we look up each
// tx's txouts via `first_txout`/`spent`/`spending_tx`. `local_of`
// already contains the seed and every ancestor, so they're
// skipped by the membership check.
let mut stack: Vec<TxIndex> = vec![seed];
let mut descendant_count = 0;
'd: while let Some(cur) = stack.pop() {
let Ok(start) = first_txout.get(cur.to_usize()).data() else { continue };
let Ok(end) = first_txout.get(cur.to_usize() + 1).data() else { continue };
for i in usize::from(start)..usize::from(end) {
let Ok(txin_idx) = spent.get(i).data() else { continue };
if txin_idx == TxInIndex::UNSPENT {
continue;
}
let Ok(child) = spending_tx.get(usize::from(txin_idx)).data() else { continue };
if local_of.contains_key(&child) || !same_block(child) {
continue;
}
let new_idx = raw.len();
raw.push((child, walk_inputs(child)));
local_of.insert(child, LocalIdx::from(new_idx));
stack.push(child);
descendant_count += 1;
if descendant_count >= MAX {
break 'd;
}
}
}
// Filter each node's full input list against `local_of` to keep
// only in-cluster parents, resolved to their `LocalIdx`.
let nodes: Vec<(TxIndex, SmallVec<[LocalIdx; 2]>)> = raw
.into_iter()
.map(|(tx_index, full_inputs)| {
let parents: SmallVec<[LocalIdx; 2]> = full_inputs
.iter()
.filter_map(|p| local_of.get(p).copied())
.collect();
(tx_index, parents)
})
.collect();
// Seed's pre-permutation index is 0; after `Cluster::new` topo-sorts
// it lands at `ancestor_count` (all in-cluster ancestors come first,
// and only ancestors do).
WalkResult {
nodes,
seed_local: LocalIdx::from(ancestor_count),
}
}
}