global: fixes

This commit is contained in:
nym21
2026-05-03 12:44:18 +02:00
parent 9cb5f2c880
commit 4663d13194
46 changed files with 1058 additions and 544 deletions

View File

@@ -6,9 +6,17 @@
//! Confirmed-tx CPFP (the same-block connected component on the
//! chain) lives in `brk_query`, since it reads indexer/computer vecs.
use brk_types::{CpfpEntry, CpfpInfo, FeeRate, Sats, TxidPrefix, VSize, Weight};
use rustc_hash::FxHashSet;
use brk_types::{
CpfpCluster, CpfpClusterChunk, CpfpClusterTx, CpfpClusterTxIndex, CpfpEntry, CpfpInfo, FeeRate,
TxidPrefix, VSize, Weight,
};
use rustc_hash::{FxHashMap, FxHashSet};
use smallvec::SmallVec;
use crate::steps::rebuilder::linearize::{
LocalIdx, cluster::Cluster, cluster_node::ClusterNode, sfl::Sfl,
};
use crate::stores::{EntryPool, TxIndex};
use crate::{Mempool, TxEntry};
/// Cap matches Bitcoin Core's default mempool ancestor/descendant
@@ -24,19 +32,16 @@ impl Mempool {
pub fn cpfp_info(&self, prefix: &TxidPrefix) -> Option<CpfpInfo> {
let snapshot = self.snapshot();
let entries = self.entries();
let txs = self.txs();
let seed_idx = entries.idx_of(prefix)?;
let seed = entries.slot(seed_idx)?;
let mut sum_fee = u64::from(seed.fee);
let mut sum_vsize = u64::from(seed.vsize);
let mut ancestor_idxs: Vec<TxIndex> = Vec::new();
let mut descendant_idxs: Vec<TxIndex> = Vec::new();
let mut ancestors: Vec<CpfpEntry> = Vec::new();
let mut descendants: Vec<CpfpEntry> = Vec::new();
if let Some(seed_block) = snapshot.block_of(seed_idx) {
// Ancestor BFS gated to the seed's projected block.
// `visited` dedupes the walk; stale parent prefixes
// (confirmed/evicted between snapshot and now) are skipped
// when `idx_of` returns None.
let mut visited: FxHashSet<TxidPrefix> = FxHashSet::default();
visited.insert(*prefix);
let mut stack: Vec<TxidPrefix> = seed.depends.iter().copied().collect();
@@ -52,17 +57,11 @@ impl Mempool {
continue;
}
let Some(anc) = entries.slot(idx) else { continue };
sum_fee += u64::from(anc.fee);
sum_vsize += u64::from(anc.vsize);
ancestor_idxs.push(idx);
ancestors.push(to_entry(anc));
stack.extend(anc.depends.iter().copied());
}
// Descendant sweep. `desc_set` starts with only the seed
// so siblings (txs sharing an ancestor with seed but not
// downstream of it) are excluded. The topological ordering
// of `Snapshot.blocks` guarantees that all in-block
// ancestors of any tx are visited before it.
let mut desc_set: FxHashSet<TxidPrefix> = FxHashSet::default();
desc_set.insert(*prefix);
for &i in &snapshot.blocks[seed_block.as_usize()] {
@@ -74,8 +73,7 @@ impl Mempool {
continue;
}
desc_set.insert(e.txid_prefix());
sum_fee += u64::from(e.fee);
sum_vsize += u64::from(e.vsize);
descendant_idxs.push(i);
descendants.push(to_entry(e));
}
}
@@ -85,16 +83,39 @@ impl Mempool {
.max_by_key(|e| FeeRate::from((e.fee, e.weight)))
.cloned();
let package_rate = FeeRate::from((Sats::from(sum_fee), VSize::from(sum_vsize)));
let effective = seed.fee_rate().max(package_rate);
let sigops = txs.get(&seed.txid).map(|tx| {
// Bitcoin Core's `total_sigop_cost` is the segwit-weighted sigop
// count (legacy * 4 + segwit * 1), divided by 5 to match
// mempool.space's reported `sigops`. Mempool.space converts
// back to count via `sigopcost / 5`.
u32::try_from(tx.total_sigop_cost / 5).unwrap_or(u32::MAX)
});
// mempool.space's adjustedVsize = max(vsize, sigops * 5).
let adjusted_vsize = match sigops {
Some(s) => VSize::from(u64::from(seed.vsize).max(u64::from(s) * 5)),
None => seed.vsize,
};
let cluster = build_cluster(seed_idx, &ancestor_idxs, &descendant_idxs, &entries);
// mempool.space sets effectiveFeePerVsize to the seed's chunk feerate
// when the cluster is known, falls back to the seed's own rate.
let effective = cluster
.as_ref()
.and_then(|c| c.chunks.get(c.chunk_index as usize))
.map(|chunk| chunk.feerate)
.unwrap_or_else(|| seed.fee_rate());
Some(CpfpInfo {
ancestors,
best_descendant,
descendants,
effective_fee_per_vsize: Some(effective),
sigops,
fee: Some(seed.fee),
adjusted_vsize: Some(seed.vsize),
adjusted_vsize: Some(adjusted_vsize),
cluster,
})
}
}
@@ -106,3 +127,120 @@ fn to_entry(e: &TxEntry) -> CpfpEntry {
fee: e.fee,
}
}
/// Build the cluster output: seed + ancestors + descendants in topological
/// order, with parent indexes inside the cluster, plus SFL-linearized chunks.
fn build_cluster(
seed_idx: TxIndex,
ancestor_idxs: &[TxIndex],
descendant_idxs: &[TxIndex],
entries: &EntryPool,
) -> Option<CpfpCluster> {
let mut ordered: Vec<TxIndex> = Vec::with_capacity(ancestor_idxs.len() + 1 + descendant_idxs.len());
ordered.extend(ancestor_idxs.iter().copied());
ordered.push(seed_idx);
ordered.extend(descendant_idxs.iter().copied());
let pool: Vec<&TxEntry> = ordered.iter().filter_map(|&i| entries.slot(i)).collect();
if pool.len() != ordered.len() {
return None;
}
let prefix_to_local: FxHashMap<TxidPrefix, LocalIdx> = pool
.iter()
.enumerate()
.map(|(i, e)| (e.txid_prefix(), i as LocalIdx))
.collect();
let mut children_of: Vec<SmallVec<[LocalIdx; 2]>> = vec![SmallVec::new(); pool.len()];
let parents_of: Vec<SmallVec<[LocalIdx; 2]>> = pool
.iter()
.enumerate()
.map(|(i, e)| {
let parents: SmallVec<[LocalIdx; 2]> = e
.depends
.iter()
.filter_map(|p| prefix_to_local.get(p).copied())
.collect();
for &p in &parents {
children_of[p as usize].push(i as LocalIdx);
}
parents
})
.collect();
let cluster_nodes: Vec<ClusterNode> = pool
.iter()
.enumerate()
.map(|(i, e)| ClusterNode {
tx_index: ordered[i],
fee: e.fee,
vsize: e.vsize,
parents: parents_of[i].clone(),
children: children_of[i].clone(),
})
.collect();
let cluster = Cluster::new(cluster_nodes);
// Re-order pool so parents come before children (mempool.space convention).
// `topo_rank[i]` gives the position of local index `i` in topological order.
let mut local_to_topo: Vec<usize> = (0..pool.len()).collect();
local_to_topo.sort_unstable_by_key(|&i| cluster.topo_rank[i]);
let topo_to_local: Vec<usize> = {
let mut v = vec![0usize; pool.len()];
for (topo_pos, &local) in local_to_topo.iter().enumerate() {
v[local] = topo_pos;
}
v
};
let topo_idx = |local: usize| CpfpClusterTxIndex::from(topo_to_local[local] as u32);
let txs: Vec<CpfpClusterTx> = local_to_topo
.iter()
.map(|&local| {
let e = pool[local];
let parents: Vec<CpfpClusterTxIndex> = parents_of[local]
.iter()
.map(|&p| topo_idx(p as usize))
.collect();
CpfpClusterTx {
txid: e.txid.clone(),
fee: e.fee,
weight: Weight::from(e.vsize),
parents,
}
})
.collect();
let raw_chunks = Sfl::linearize(&cluster);
let chunks: Vec<CpfpClusterChunk> = raw_chunks
.iter()
.map(|chunk| {
let mut chunk_txs: Vec<CpfpClusterTxIndex> = chunk
.nodes
.iter()
.map(|&local| topo_idx(local as usize))
.collect();
chunk_txs.sort_unstable();
CpfpClusterChunk {
txs: chunk_txs,
feerate: chunk.fee_rate(),
}
})
.collect();
let seed_local = *prefix_to_local.get(&entries.slot(seed_idx)?.txid_prefix())?;
let seed_topo = topo_idx(seed_local as usize);
let chunk_index = chunks
.iter()
.position(|c| c.txs.contains(&seed_topo))
.unwrap_or(0) as u32;
Some(CpfpCluster {
txs,
chunks,
chunk_index,
})
}

View File

@@ -4,13 +4,13 @@
//! prevouts against `known` or `parent_raws`, build a full
//! `Transaction` + `Entry`.
//! - **Revived** - tx in the graveyard. Rebuild the `Entry` only
//! (preserving `first_seen`, `rbf`, `size`). The Applier exhumes
//! the cached tx body. No raw decoding.
//! (preserving `rbf`, `size`). The Applier exhumes the cached tx
//! body. No raw decoding.
use std::mem;
use brk_rpc::RawTx;
use brk_types::{MempoolEntryInfo, Timestamp, Transaction, TxIn, TxOut, TxStatus, Txid, Vout};
use brk_types::{MempoolEntryInfo, Transaction, TxIn, TxOut, TxStatus, Txid, Vout};
use rustc_hash::FxHashMap;
use crate::{TxTombstone, stores::TxStore};
@@ -35,7 +35,7 @@ impl TxAddition {
let total_size = raw.hex.len() / 2;
let rbf = raw.tx.input.iter().any(|i| i.sequence.is_rbf());
let tx = Self::build_tx(info, raw, total_size, mempool_txs, parent_raws);
let entry = TxEntry::new(info, total_size as u64, rbf, Timestamp::now());
let entry = TxEntry::new(info, total_size as u64, rbf);
Self::Fresh { tx, entry }
}
@@ -68,7 +68,7 @@ impl TxAddition {
}
pub(super) fn revived(info: &MempoolEntryInfo, tomb: &TxTombstone) -> Self {
let entry = TxEntry::new(info, tomb.entry.size, tomb.entry.rbf, tomb.entry.first_seen);
let entry = TxEntry::new(info, tomb.entry.size, tomb.entry.rbf);
Self::Revived { entry }
}

View File

@@ -26,14 +26,14 @@ pub struct TxEntry {
}
impl TxEntry {
pub(super) fn new(info: &MempoolEntryInfo, size: u64, rbf: bool, first_seen: Timestamp) -> Self {
pub(super) fn new(info: &MempoolEntryInfo, size: u64, rbf: bool) -> Self {
Self {
txid: info.txid.clone(),
fee: info.fee,
vsize: VSize::from(info.vsize),
size,
depends: info.depends.iter().map(TxidPrefix::from).collect(),
first_seen,
first_seen: info.first_seen,
rbf,
}
}