use brk_types::{
CPFP_CHAIN_LIMIT, CpfpCluster, CpfpClusterTx, CpfpClusterTxIndex, CpfpEntry, CpfpInfo, FeeRate,
SigOps, TxidPrefix, VSize, find_seed_chunk,
};
use rustc_hash::{FxBuildHasher, FxHashSet};
use crate::Mempool;
use super::{Cluster, SnapTx, Snapshot, TxIndex};
impl Mempool {
pub fn cpfp_info(&self, prefix: &TxidPrefix) -> Option<CpfpInfo> {
let snapshot = self.snapshot();
let seed_idx = snapshot.idx_of(prefix)?;
let seed = snapshot.tx(seed_idx)?;
let sigops = self.read().txs.get(&seed.txid)?.total_sigop_cost;
Some(snapshot.cpfp_info_at(seed_idx, seed, sigops))
}
}
impl Snapshot {
fn cpfp_info_at(&self, seed_idx: TxIndex, seed: &SnapTx, sigops: SigOps) -> CpfpInfo {
let ancestors = Self::collect_cpfp_entries(&self.txs, seed_idx, |t| &t.parents);
let descendants = Self::collect_cpfp_entries(&self.txs, seed_idx, |t| &t.children);
let best_descendant = descendants
.iter()
.max_by_key(|e| FeeRate::from((e.fee, e.weight)))
.cloned();
let (cluster, effective_fee_per_vsize) = Self::build_cpfp_cluster(&self.txs, seed_idx, seed);
let vsize = VSize::from(seed.weight);
CpfpInfo {
ancestors,
best_descendant,
descendants,
effective_fee_per_vsize,
sigops,
fee: seed.fee,
vsize,
adjusted_vsize: sigops.adjust_vsize(vsize),
cluster,
}
}
fn collect_cpfp_entries(
txs: &[SnapTx],
seed: TxIndex,
next: impl Fn(&SnapTx) -> &[TxIndex],
) -> Vec<CpfpEntry> {
let Some(seed_node) = txs.get(seed.as_usize()) else {
return Vec::new();
};
let mut visited: FxHashSet<TxIndex> =
FxHashSet::with_capacity_and_hasher(CPFP_CHAIN_LIMIT + 1, FxBuildHasher);
visited.insert(seed);
let mut out: Vec<CpfpEntry> = Vec::with_capacity(CPFP_CHAIN_LIMIT);
let mut stack: Vec<TxIndex> = next(seed_node).to_vec();
while let Some(idx) = stack.pop() {
if out.len() >= CPFP_CHAIN_LIMIT {
break;
}
if !visited.insert(idx) {
continue;
}
if let Some(t) = txs.get(idx.as_usize()) {
out.push(CpfpEntry::from(t));
stack.extend(next(t).iter().copied());
}
}
out
}
fn build_cpfp_cluster(
txs: &[SnapTx],
seed_idx: TxIndex,
seed: &SnapTx,
) -> (Option<CpfpCluster>, FeeRate) {
let seed_per_tx_rate = FeeRate::from((seed.fee, seed.vsize));
let component = Cluster::walk(txs, seed_idx);
if component.len() <= 1 {
return (None, seed_per_tx_rate);
}
let (members, chunks) = Cluster::linearize(txs, &component);
let cluster_txs = Self::wire_cluster_members(txs, &members);
let seed_local = CpfpClusterTxIndex::from(
members
.iter()
.position(|&i| i == seed_idx)
.map_or(0, |p| p as u32),
);
let (chunk_index, seed_chunk_rate) = find_seed_chunk(&chunks, seed_local, seed_per_tx_rate);
(
Some(CpfpCluster {
txs: cluster_txs,
chunks,
chunk_index,
}),
seed_chunk_rate,
)
}
fn wire_cluster_members(txs: &[SnapTx], members: &[TxIndex]) -> Vec<CpfpClusterTx> {
let local_of = Cluster::local_index(members);
members
.iter()
.map(|&idx| {
let t = &txs[idx.as_usize()];
CpfpClusterTx {
txid: t.txid,
weight: t.weight,
fee: t.fee,
parents: t
.parents
.iter()
.filter_map(|p| local_of.get(p).copied())
.collect(),
}
})
.collect()
}
}
#[cfg(test)]
mod tests {
use brk_types::{FeeRate, Txid};
use super::*;
use crate::{
state::TxEntry,
test_support::{fake_entry_info, fake_tx, p2wpkh_script},
};
fn insert_with_depends(
mempool: &Mempool,
seed: u8,
fee: u64,
vsize: u64,
parents: &[Txid],
) -> Txid {
let tx = fake_tx(seed, &[None], &[(p2wpkh_script(seed + 1), 1_234)]);
let txid = tx.txid;
let mut info = fake_entry_info(txid, fee, vsize);
info.depends = parents.to_vec();
let entry = TxEntry::new(&info, vsize, false);
let mut state = mempool.test_state_lock().write();
state.txs.insert(tx, entry);
txid
}
#[test]
fn singleton_cpfp_info_has_no_cluster() {
let mempool = Mempool::for_test();
let txid = insert_with_depends(&mempool, 0xB0, 10_000, 100, &[]);
mempool.test_tick(&[txid], FeeRate::new(1.0));
let info = mempool
.cpfp_info(&TxidPrefix::from(&txid))
.expect("tx is in mempool");
assert!(info.cluster.is_none(), "singletons emit no cluster");
assert!(info.ancestors.is_empty());
assert!(info.descendants.is_empty());
let isolated = FeeRate::from((info.fee, info.vsize));
assert_eq!(info.effective_fee_per_vsize, isolated);
}
#[test]
fn two_tx_cpfp_cluster_has_both_members_and_lifted_rate() {
let mempool = Mempool::for_test();
let parent = insert_with_depends(&mempool, 0xB1, 100, 100, &[]);
let child = insert_with_depends(&mempool, 0xB2, 1_900, 100, &[parent]);
mempool.test_tick(&[parent, child], FeeRate::new(1.0));
let parent_info = mempool.cpfp_info(&TxidPrefix::from(&parent)).unwrap();
let cluster = parent_info.cluster.expect("two-tx cluster present");
assert_eq!(cluster.txs.len(), 2);
assert_eq!(cluster.txs[0].txid, parent);
assert_eq!(cluster.txs[1].txid, child);
assert_eq!(cluster.txs[1].parents.len(), 1);
let parent_isolated = FeeRate::from((parent_info.fee, parent_info.vsize));
assert!(parent_info.effective_fee_per_vsize > parent_isolated);
let child_info = mempool.cpfp_info(&TxidPrefix::from(&child)).unwrap();
assert_eq!(parent_info.effective_fee_per_vsize, child_info.effective_fee_per_vsize);
}
#[test]
fn cpfp_ancestor_and_descendant_walks_are_directional() {
let mempool = Mempool::for_test();
let a = insert_with_depends(&mempool, 0xB3, 100, 100, &[]);
let b = insert_with_depends(&mempool, 0xB4, 100, 100, &[a]);
let c = insert_with_depends(&mempool, 0xB5, 5_800, 100, &[b]);
mempool.test_tick(&[a, b, c], FeeRate::new(1.0));
let info_b = mempool.cpfp_info(&TxidPrefix::from(&b)).unwrap();
let ancestor_ids: Vec<_> = info_b.ancestors.iter().map(|e| e.txid).collect();
let descendant_ids: Vec<_> = info_b.descendants.iter().map(|e| e.txid).collect();
assert_eq!(ancestor_ids, vec![a]);
assert_eq!(descendant_ids, vec![c]);
assert_eq!(info_b.best_descendant.as_ref().map(|e| e.txid), Some(c));
}
#[test]
fn cpfp_info_returns_none_for_unknown_txid() {
let mempool = Mempool::for_test();
mempool.test_tick(&[], FeeRate::new(1.0));
let bogus = TxidPrefix::from(&Txid::COINBASE);
assert!(mempool.cpfp_info(&bogus).is_none());
}
}