use std::collections::VecDeque;
use brk_types::{
CpfpCluster, CpfpClusterChunk, CpfpClusterTx, CpfpClusterTxIndex, CpfpEntry, CpfpInfo, FeeRate,
SigOps, TxidPrefix, VSize,
};
use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
use crate::{
Mempool,
chunking::{ChunkInput, linearize},
steps::{SnapTx, TxIndex},
};
const MAX: usize = 25;
const MAX_CLUSTER: usize = 64;
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)
.map(|tx| tx.total_sigop_cost)
.unwrap_or(SigOps::ZERO);
Some(build_cpfp_info(&snapshot.txs, seed_idx, seed, sigops))
}
}
fn build_cpfp_info(
txs: &[SnapTx],
seed_idx: TxIndex,
seed: &SnapTx,
sigops: SigOps,
) -> CpfpInfo {
let ancestors = collect_entries(txs, seed_idx, |t| &t.parents);
let descendants = collect_entries(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) = build_cluster(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_entries(
txs: &[SnapTx],
seed: TxIndex,
next: impl Fn(&SnapTx) -> &[TxIndex],
) -> Vec<CpfpEntry> {
walk(txs, seed, next)
.iter()
.filter_map(|&i| txs.get(i.as_usize()).map(CpfpEntry::from))
.collect()
}
fn walk(txs: &[SnapTx], seed: TxIndex, next: impl Fn(&SnapTx) -> &[TxIndex]) -> Vec<TxIndex> {
let Some(seed_node) = txs.get(seed.as_usize()) else {
return Vec::new();
};
let mut visited: FxHashSet<TxIndex> =
FxHashSet::with_capacity_and_hasher(MAX + 1, FxBuildHasher);
visited.insert(seed);
let mut out: Vec<TxIndex> = Vec::with_capacity(MAX);
let mut stack: Vec<TxIndex> = next(seed_node).to_vec();
while let Some(idx) = stack.pop() {
if out.len() >= MAX {
break;
}
if !visited.insert(idx) {
continue;
}
out.push(idx);
if let Some(t) = txs.get(idx.as_usize()) {
stack.extend(next(t).iter().copied());
}
}
out
}
fn build_cluster(
txs: &[SnapTx],
seed_idx: TxIndex,
seed: &SnapTx,
) -> (Option<CpfpCluster>, FeeRate) {
let seed_per_tx_rate = FeeRate::from((seed.fee, seed.vsize));
let component = walk_cluster(txs, seed_idx);
if component.len() <= 1 {
return (None, seed_per_tx_rate);
}
let members = topo_sort(txs, &component);
let local_of = build_local_index(&members);
let (cluster_txs, vsizes) = collect_cluster_members(txs, &members, &local_of);
let chunks = linearize_cluster(&cluster_txs, &vsizes);
let (chunk_index, seed_chunk_rate) =
locate_seed_chunk(local_of[&seed_idx], &chunks, seed_per_tx_rate);
(
Some(CpfpCluster {
txs: cluster_txs,
chunks,
chunk_index,
}),
seed_chunk_rate,
)
}
fn build_local_index(members: &[TxIndex]) -> FxHashMap<TxIndex, CpfpClusterTxIndex> {
members
.iter()
.enumerate()
.map(|(i, &idx)| (idx, CpfpClusterTxIndex::from(i as u32)))
.collect()
}
fn collect_cluster_members(
txs: &[SnapTx],
members: &[TxIndex],
local_of: &FxHashMap<TxIndex, CpfpClusterTxIndex>,
) -> (Vec<CpfpClusterTx>, Vec<VSize>) {
let mut cluster_txs: Vec<CpfpClusterTx> = Vec::with_capacity(members.len());
let mut vsizes: Vec<VSize> = Vec::with_capacity(members.len());
for &idx in members {
let Some(t) = txs.get(idx.as_usize()) else {
continue;
};
let parents: Vec<CpfpClusterTxIndex> = t
.parents
.iter()
.filter_map(|p| local_of.get(p).copied())
.collect();
cluster_txs.push(CpfpClusterTx {
txid: t.txid,
weight: t.weight,
fee: t.fee,
parents,
});
vsizes.push(t.vsize);
}
(cluster_txs, vsizes)
}
fn linearize_cluster(cluster_txs: &[CpfpClusterTx], vsizes: &[VSize]) -> Vec<CpfpClusterChunk> {
let inputs: Vec<ChunkInput<'_>> = cluster_txs
.iter()
.zip(vsizes)
.map(|(c, &vsize)| ChunkInput {
fee: c.fee,
vsize,
parents: &c.parents,
})
.collect();
linearize(&inputs)
}
fn locate_seed_chunk(
seed_local: CpfpClusterTxIndex,
chunks: &[CpfpClusterChunk],
seed_per_tx_rate: FeeRate,
) -> (u32, FeeRate) {
chunks
.iter()
.enumerate()
.find(|(_, ch)| ch.txs.contains(&seed_local))
.map(|(i, ch)| (i as u32, ch.feerate))
.unwrap_or((0, seed_per_tx_rate))
}
fn walk_cluster(txs: &[SnapTx], seed: TxIndex) -> Vec<TxIndex> {
if txs.get(seed.as_usize()).is_none() {
return Vec::new();
}
let mut visited: FxHashSet<TxIndex> =
FxHashSet::with_capacity_and_hasher(MAX_CLUSTER, FxBuildHasher);
visited.insert(seed);
let mut out: Vec<TxIndex> = Vec::with_capacity(MAX_CLUSTER);
out.push(seed);
let mut stack: Vec<TxIndex> = vec![seed];
while let Some(idx) = stack.pop() {
let Some(t) = txs.get(idx.as_usize()) else {
continue;
};
for &n in t.parents.iter().chain(t.children.iter()) {
if out.len() >= MAX_CLUSTER {
return out;
}
if visited.insert(n) {
out.push(n);
stack.push(n);
}
}
}
out
}
fn topo_sort(txs: &[SnapTx], component: &[TxIndex]) -> Vec<TxIndex> {
let n = component.len();
let pos: FxHashMap<TxIndex, usize> = component
.iter()
.enumerate()
.map(|(i, &x)| (x, i))
.collect();
let mut indeg: Vec<u32> = vec![0; n];
let mut children: Vec<Vec<usize>> = vec![Vec::new(); n];
for (i, &idx) in component.iter().enumerate() {
let Some(t) = txs.get(idx.as_usize()) else {
continue;
};
indeg[i] = t.parents.iter().filter(|p| pos.contains_key(p)).count() as u32;
for &c in t.children.iter() {
if let Some(&ci) = pos.get(&c) {
children[i].push(ci);
}
}
}
let mut queue: VecDeque<usize> = (0..n).filter(|&i| indeg[i] == 0).collect();
let mut out: Vec<TxIndex> = Vec::with_capacity(n);
while let Some(i) = queue.pop_front() {
out.push(component[i]);
for &c in &children[i] {
indeg[c] -= 1;
if indeg[c] == 0 {
queue.push_back(c);
}
}
}
out
}