mempool: fixes

This commit is contained in:
nym21
2026-05-08 11:51:44 +02:00
parent dd88996f7f
commit 25b2268563
11 changed files with 125 additions and 69 deletions

View File

@@ -8953,7 +8953,7 @@ pub struct BrkClient {
impl BrkClient {
/// Client version.
pub const VERSION: &'static str = "v0.3.0-beta.7";
pub const VERSION: &'static str = "v0.3.0-beta.8";
/// Create a new client with the given base URL.
pub fn new(base_url: impl Into<String>) -> Self {

View File

@@ -41,49 +41,122 @@ pub fn linearize(items: &[ChunkInput<'_>]) -> Vec<CpfpClusterChunk> {
}
let mut remaining: Vec<bool> = vec![true; n];
let mut chunks: Vec<CpfpClusterChunk> = Vec::new();
let empty: FxHashSet<u32> = FxHashSet::default();
while remaining.iter().any(|&r| r) {
let mut best: Option<(FeeRate, FxHashSet<u32>)> = None;
// Pick the top single-anchored ancestor-closed set. On rate
// ties the larger set wins so a uniform-rate chain emits one
// chunk instead of n singletons. The extension loop below
// catches the same case at zero extra cost, but starting big
// shaves iterations.
let mut best: Option<(FeeRate, FxHashSet<u32>, Sats, VSize)> = None;
for i in 0..n {
if !remaining[i] {
continue;
}
let mut anc: FxHashSet<u32> =
FxHashSet::with_capacity_and_hasher(8, FxBuildHasher);
let mut stack: Vec<u32> = vec![i as u32];
while let Some(x) = stack.pop() {
if !anc.insert(x) {
continue;
}
for &p in items[x as usize].parents {
let pu: u32 = u32::from(p);
if remaining[pu as usize] && !anc.contains(&pu) {
stack.push(pu);
}
}
}
let mut fee = Sats::ZERO;
let mut vsize = VSize::from(0u64);
for &x in &anc {
fee += items[x as usize].fee;
vsize += items[x as usize].vsize;
}
let anc = closure(items, &remaining, &empty, i as u32);
let (fee, vsize) = sum_fee_vsize(items, &anc);
let rate = FeeRate::from((fee, vsize));
match &best {
Some((br, _)) if *br >= rate => {}
_ => best = Some((rate, anc)),
let replace = match &best {
None => true,
Some((br, ba, _, _)) => rate > *br || (rate == *br && anc.len() > ba.len()),
};
if replace {
best = Some((rate, anc, fee, vsize));
}
}
let (rate, set) = best.expect("at least one remaining tx");
let mut indices: Vec<u32> = set.into_iter().collect();
let (mut chunk_rate, mut anc, mut chunk_fee, mut chunk_vsize) =
best.expect("at least one remaining tx");
// Extend the chunk with any other remaining ancestor-closed
// subset whose union keeps the chunk rate >= current rate.
// SFL chunks are the *maximum* ancestor-closed set at the top
// rate, but a single anchor only sees one connected component
// up to the cluster root: a parent with one long chain plus
// additional same-rate sibling children leaves the siblings
// stranded as same-rate singleton chunks - which can even
// appear "above" the main chunk under integer-vsize rounding,
// breaking the descending-rate invariant.
loop {
let mut best_ext: Option<(FeeRate, FxHashSet<u32>, Sats, VSize)> = None;
for i in 0..n {
if !remaining[i] || anc.contains(&(i as u32)) {
continue;
}
let extra = closure(items, &remaining, &anc, i as u32);
if extra.is_empty() {
continue;
}
let (ef, ev) = sum_fee_vsize(items, &extra);
let new_fee = chunk_fee + ef;
let new_vsize = chunk_vsize + ev;
let new_rate = FeeRate::from((new_fee, new_vsize));
if new_rate < chunk_rate {
continue;
}
let replace = match &best_ext {
None => true,
Some((br, _, _, _)) => new_rate > *br,
};
if replace {
best_ext = Some((new_rate, extra, new_fee, new_vsize));
}
}
match best_ext {
Some((r, e, f, v)) => {
anc.extend(&e);
chunk_fee = f;
chunk_vsize = v;
chunk_rate = r;
}
None => break,
}
}
let mut indices: Vec<u32> = anc.into_iter().collect();
indices.sort_unstable();
for &x in &indices {
remaining[x as usize] = false;
}
let txs: Vec<CpfpClusterTxIndex> =
indices.into_iter().map(CpfpClusterTxIndex::from).collect();
chunks.push(CpfpClusterChunk { txs, feerate: rate });
chunks.push(CpfpClusterChunk { txs, feerate: chunk_rate });
}
chunks
}
fn closure(
items: &[ChunkInput<'_>],
remaining: &[bool],
excluded: &FxHashSet<u32>,
start: u32,
) -> FxHashSet<u32> {
let mut set: FxHashSet<u32> = FxHashSet::with_capacity_and_hasher(8, FxBuildHasher);
if !remaining[start as usize] || excluded.contains(&start) {
return set;
}
let mut stack: Vec<u32> = vec![start];
while let Some(x) = stack.pop() {
if !set.insert(x) {
continue;
}
for &p in items[x as usize].parents {
let pu: u32 = u32::from(p);
if remaining[pu as usize] && !excluded.contains(&pu) && !set.contains(&pu) {
stack.push(pu);
}
}
}
set
}
fn sum_fee_vsize(items: &[ChunkInput<'_>], set: &FxHashSet<u32>) -> (Sats, VSize) {
let mut fee = Sats::ZERO;
let mut vsize = VSize::from(0u64);
for &x in set {
fee += items[x as usize].fee;
vsize += items[x as usize].vsize;
}
(fee, vsize)
}

View File

@@ -212,12 +212,16 @@ impl Mempool {
.map(|e| e.fee_rate())
}
/// Fee rate snapshotted into a graveyard tomb at burial.
/// Effective fee rate (Core's chunk rate) snapshotted into the
/// tomb's entry at burial - same value `live_effective_fee_rate`
/// returns while the tx is alive, so an evicted RBF predecessor
/// reports the package-effective rate it had in the mempool, not a
/// misleading isolated `fee/vsize`.
pub fn graveyard_fee_rate(&self, txid: &Txid) -> Option<FeeRate> {
self.read()
.graveyard
.get(txid)
.map(|tomb| tomb.entry.fee_rate())
.map(|tomb| tomb.entry.chunk_rate)
}
/// `first_seen` Unix-second timestamps for `txids`, in input order.

View File

@@ -155,7 +155,6 @@ impl Query {
time: first_seen,
rbf: node.rbf,
full_rbf: Some(node.full_rbf),
mined,
},
time: first_seen,
full_rbf: node.full_rbf,

View File

@@ -15,12 +15,6 @@ mod methods;
use client::ClientInner;
pub use methods::MempoolState;
#[derive(Debug, Clone)]
pub struct BlockchainInfo {
pub headers: u64,
pub blocks: u64,
}
#[derive(Debug, Clone)]
pub struct BlockInfo {
pub height: usize,

View File

@@ -7,9 +7,12 @@ use brk_types::{
Weight,
};
use corepc_jsonrpc::error::Error as JsonRpcError;
use corepc_types::v30::{
GetBlockCount, GetBlockHash, GetBlockHeader, GetBlockHeaderVerbose, GetBlockVerboseOne,
GetBlockVerboseZero, GetBlockchainInfo, GetMempoolInfo, GetRawMempool, GetTxOut,
use corepc_types::{
v17::{
GetBlockCount, GetBlockHash, GetBlockHeader, GetBlockHeaderVerbose, GetBlockVerboseOne,
GetBlockVerboseZero, GetRawMempool, GetTxOut,
},
v24::GetMempoolInfo,
};
use rustc_hash::FxHashMap;
use serde::Deserialize;
@@ -21,9 +24,7 @@ use tracing::{debug, info};
/// The mempool fetcher tolerates these per-item failures silently.
const RPC_NOT_FOUND: i32 = -5;
use crate::{
BlockHeaderInfo, BlockInfo, BlockTemplateTx, BlockchainInfo, Client, RawTx, TxOutInfo,
};
use crate::{BlockHeaderInfo, BlockInfo, BlockTemplateTx, Client, RawTx, TxOutInfo};
/// Per-batch request count for `get_block_hashes_range`. Sized so the
/// JSON request body stays well under a megabyte and bitcoind doesn't
@@ -119,14 +120,6 @@ fn build_min_fee(raw: GetMempoolInfo) -> FeeRate {
}
impl Client {
pub fn get_blockchain_info(&self) -> Result<BlockchainInfo> {
let r: GetBlockchainInfo = self.0.call_with_retry("getblockchaininfo", &[])?;
Ok(BlockchainInfo {
headers: r.headers as u64,
blocks: r.blocks as u64,
})
}
/// Returns the numbers of block in the longest chain.
pub fn get_block_count(&self) -> Result<u64> {
let r: GetBlockCount = self.0.call_with_retry("getblockcount", &[])?;
@@ -440,9 +433,14 @@ impl Client {
}
pub fn wait_for_synced_node(&self) -> Result<()> {
#[derive(Deserialize)]
struct SyncProgress {
headers: u64,
blocks: u64,
}
let is_synced = || -> Result<bool> {
let info = self.get_blockchain_info()?;
Ok(info.headers == info.blocks)
let p: SyncProgress = self.0.call_with_retry("getblockchaininfo", &[])?;
Ok(p.headers == p.blocks)
};
if !is_synced()? {

View File

@@ -21,11 +21,6 @@ pub struct RbfTx {
/// this tx displaced at least one non-signaling predecessor.
#[serde(rename = "fullRbf", skip_serializing_if = "Option::is_none", default)]
pub full_rbf: Option<bool>,
/// `Some(true)` iff the tx is currently confirmed in the indexed
/// chain. Absent on serialization when the tx is still pending or
/// has been evicted without confirming.
#[serde(skip_serializing_if = "Option::is_none", default)]
pub mined: Option<bool>,
}
/// One node in an RBF replacement tree. The node's `tx` replaced each