global: fixes

This commit is contained in:
nym21
2026-04-27 19:19:14 +02:00
parent 6c8afc942c
commit 66494c081c
16 changed files with 1453 additions and 5399 deletions

View File

@@ -10,7 +10,9 @@ use std::{
};
use brk_rpc::Client;
use brk_types::FeeRate;
use parking_lot::RwLock;
use tracing::warn;
#[cfg(debug_assertions)]
use self::projected_blocks::verify::Verifier;
@@ -72,6 +74,11 @@ impl Rebuilder {
self.dirty.store(false, Ordering::Release);
let min_fee = client.get_mempool_min_fee().unwrap_or_else(|e| {
warn!("getmempoolinfo failed, falling back to FeeRate::MIN: {e}");
FeeRate::MIN
});
let built = {
let entries = entries.read();
let entries_slice = entries.entries();
@@ -82,7 +89,7 @@ impl Rebuilder {
#[cfg(not(debug_assertions))]
let _ = client;
Snapshot::build(blocks, entries_slice)
Snapshot::build(blocks, entries_slice, min_fee)
};
*self.snapshot.write() = Arc::new(built);

View File

@@ -2,21 +2,74 @@ use brk_types::{FeeRate, RecommendedFees};
use super::stats::BlockStats;
/// Compute recommended fees from block stats (mempool.space style).
pub fn compute_recommended_fees(stats: &[BlockStats]) -> RecommendedFees {
/// Output rounding granularity in sat/vB. mempool.space's
/// `/api/v1/fees/recommended` uses `1.0`; their `/precise`
/// variant uses `0.001`. bitview always emits precise.
const MIN_INCREMENT: FeeRate = FeeRate::new(0.001);
/// `getPreciseRecommendedFee` adds this to `fastestFee` and
/// half of it to `halfHourFee`, then floors them. Compensates
/// for sub-1-sat/vB fees mined by hashrate that ignores the
/// relay floor.
const PRIORITY_FACTOR: FeeRate = FeeRate::new(0.5);
const MIN_FASTEST_FEE: FeeRate = FeeRate::new(1.0);
const MIN_HALF_HOUR_FEE: FeeRate = FeeRate::new(0.5);
/// Literal port of mempool.space's `getPreciseRecommendedFee`
/// (backend/src/api/fee-api.ts). `min_fee` is bitcoind's live
/// `mempoolminfee` in sat/vB and acts as a floor for every tier
/// while the mempool is purging by fee.
pub fn compute_recommended_fees(stats: &[BlockStats], min_fee: FeeRate) -> RecommendedFees {
let purge_rate = min_fee.ceil_to(MIN_INCREMENT);
let minimum_fee = purge_rate.max(MIN_INCREMENT);
let first = stats.first().map_or(minimum_fee, |b| {
optimize_median_fee(b, stats.get(1), None, minimum_fee)
});
let second = stats.get(1).map_or(minimum_fee, |b| {
optimize_median_fee(b, stats.get(2), Some(first), minimum_fee)
});
let third = stats.get(2).map_or(minimum_fee, |b| {
optimize_median_fee(b, stats.get(3), Some(second), minimum_fee)
});
let mut fastest = minimum_fee.max(first);
let mut half_hour = minimum_fee.max(second);
let mut hour = minimum_fee.max(third);
let economy = third.clamp(minimum_fee, minimum_fee * 2.0);
fastest = fastest.max(half_hour).max(hour).max(economy);
half_hour = half_hour.max(hour).max(economy);
hour = hour.max(economy);
let fastest = (fastest + PRIORITY_FACTOR).max(MIN_FASTEST_FEE);
let half_hour = (half_hour + PRIORITY_FACTOR / 2.0).max(MIN_HALF_HOUR_FEE);
RecommendedFees {
fastest_fee: median_fee_for_block(stats, 0),
half_hour_fee: median_fee_for_block(stats, 2),
hour_fee: median_fee_for_block(stats, 5),
economy_fee: median_fee_for_block(stats, 7),
minimum_fee: FeeRate::MIN,
fastest_fee: fastest.round_milli(),
half_hour_fee: half_hour.round_milli(),
hour_fee: hour.round_milli(),
economy_fee: economy.round_milli(),
minimum_fee: minimum_fee.round_milli(),
}
}
/// Get the median fee rate for block N.
fn median_fee_for_block(stats: &[BlockStats], block_index: usize) -> FeeRate {
stats
.get(block_index)
.map(|s| s.median_fee_rate())
.unwrap_or_else(|| FeeRate::MIN)
/// Pick the fee for one projected block, smoothing toward the
/// previous tier and discounting partially-full final blocks.
fn optimize_median_fee(
block: &BlockStats,
next_block: Option<&BlockStats>,
previous_fee: Option<FeeRate>,
min_fee: FeeRate,
) -> FeeRate {
let median = block.median_fee_rate();
let use_fee = previous_fee.map_or(median, |prev| FeeRate::mean(median, prev));
let vsize = u64::from(block.total_vsize);
if vsize <= 500_000 || median < min_fee {
return min_fee;
}
if vsize <= 950_000 && next_block.is_none() {
let multiplier = (vsize - 500_000) as f64 / 500_000.0;
return (use_fee * multiplier).round_to(MIN_INCREMENT).max(min_fee);
}
use_fee.ceil_to(MIN_INCREMENT).max(min_fee)
}

View File

@@ -1,6 +1,6 @@
use std::hash::{DefaultHasher, Hash, Hasher};
use brk_types::RecommendedFees;
use brk_types::{FeeRate, RecommendedFees};
use super::{
super::block_builder::Package,
@@ -27,13 +27,15 @@ pub struct Snapshot {
impl Snapshot {
/// Build a snapshot from packages grouped by projected block.
pub fn build(blocks: Vec<Vec<Package>>, entries: &[Option<Entry>]) -> Self {
/// `min_fee` is bitcoind's live `mempoolminfee`, used as the floor
/// for every recommended-fee tier.
pub fn build(blocks: Vec<Vec<Package>>, entries: &[Option<Entry>], min_fee: FeeRate) -> Self {
let block_stats: Vec<BlockStats> = blocks
.iter()
.map(|block| stats::compute_block_stats(block, entries))
.collect();
let fees = fees::compute_recommended_fees(&block_stats);
let fees = fees::compute_recommended_fees(&block_stats, min_fee);
let blocks: Vec<Vec<TxIndex>> = blocks
.into_iter()

View File

@@ -1,4 +1,7 @@
use std::hash::{DefaultHasher, Hash, Hasher};
use std::{
collections::hash_map::Entry as MapEntry,
hash::{DefaultHasher, Hash, Hasher},
};
use brk_types::{AddrBytes, AddrMempoolStats, Transaction, TxOut, Txid};
use derive_more::Deref;
@@ -51,7 +54,6 @@ impl AddrTracker {
}
fn update(&mut self, tx: &Transaction, txid: &Txid, is_addition: bool) {
// Inputs: track sending
for txin in &tx.input {
let Some(prevout) = txin.prevout.as_ref() else {
continue;
@@ -59,33 +61,57 @@ impl AddrTracker {
let Some(bytes) = prevout.addr_bytes() else {
continue;
};
let (stats, txids) = self.0.entry(bytes).or_default();
if is_addition {
txids.insert(txid.clone());
stats.sending(prevout);
} else {
txids.remove(txid);
stats.sent(prevout);
}
stats.update_tx_count(txids.len() as u32);
self.apply(bytes, txid, is_addition, |stats| {
if is_addition {
stats.sending(prevout);
} else {
stats.sent(prevout);
}
});
}
// Outputs: track receiving
for txout in &tx.output {
let Some(bytes) = txout.addr_bytes() else {
continue;
};
self.apply(bytes, txid, is_addition, |stats| {
if is_addition {
stats.receiving(txout);
} else {
stats.received(txout);
}
});
}
}
let (stats, txids) = self.0.entry(bytes).or_default();
if is_addition {
txids.insert(txid.clone());
stats.receiving(txout);
} else {
txids.remove(txid);
stats.received(txout);
fn apply(
&mut self,
bytes: AddrBytes,
txid: &Txid,
is_addition: bool,
update_stats: impl FnOnce(&mut AddrMempoolStats),
) {
let mut entry = match self.0.entry(bytes) {
MapEntry::Occupied(e) => e,
MapEntry::Vacant(v) => {
if !is_addition {
return;
}
v.insert_entry(Default::default())
}
stats.update_tx_count(txids.len() as u32);
};
let (stats, txids) = entry.get_mut();
if is_addition {
txids.insert(txid.clone());
} else {
txids.remove(txid);
}
update_stats(stats);
let len = txids.len();
if len == 0 {
entry.remove();
} else {
stats.update_tx_count(len as u32);
}
}
}