mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-05-20 06:44:47 -07:00
global: fixes
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user