mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-07-11 19:18:14 -07:00
global: added support for oracle histograms
This commit is contained in:
@@ -11,13 +11,21 @@ impl Mempool {
|
||||
self.read().info.clone()
|
||||
}
|
||||
|
||||
/// Snapshot of pre-bucketed oracle bins across all live mempool tx
|
||||
/// outputs. The total is maintained incrementally by `TxStore` on
|
||||
/// every insert/remove, so this hot path is `O(NUM_BINS)` regardless
|
||||
/// of pool size. Used by `live_price` to blend the mempool into the
|
||||
/// committed oracle without re-parsing scripts per request.
|
||||
/// Snapshot of pre-bucketed round-dollar-eligible bins across all live
|
||||
/// mempool tx outputs. Maintained incrementally by `TxStore` on every
|
||||
/// insert/remove, so this hot path is `O(NUM_BINS)` regardless of pool
|
||||
/// size. Used by `live_price` to blend the mempool into the committed
|
||||
/// oracle without re-parsing scripts per request.
|
||||
#[must_use]
|
||||
pub fn live_histogram(&self) -> HistogramRaw {
|
||||
self.read().txs.live_histogram()
|
||||
pub fn live_eligible_histogram(&self) -> HistogramRaw {
|
||||
self.read().txs.live_eligible_histogram()
|
||||
}
|
||||
|
||||
/// Snapshot of the raw histogram: every live mempool output binned by
|
||||
/// value with no payment filtering. Backs the `histogram/raw/live`
|
||||
/// endpoint.
|
||||
#[must_use]
|
||||
pub fn live_raw_histogram(&self) -> HistogramRaw {
|
||||
self.read().txs.live_raw_histogram()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
use brk_oracle::{HistogramRaw, for_each_round_dollar_bin, sats_to_bin};
|
||||
use brk_types::Transaction;
|
||||
|
||||
use crate::stores::tx_store::TxRecord;
|
||||
|
||||
/// The two live per-bin histograms the pool maintains incrementally as txs
|
||||
/// enter and leave: `eligible` applies the round-dollar payment filter (it
|
||||
/// feeds the oracle blend), `raw` bins every output by value with no filtering.
|
||||
/// Add and remove run through the same code so the two stay symmetric.
|
||||
#[derive(Default)]
|
||||
pub struct LiveHistograms {
|
||||
eligible: HistogramRaw,
|
||||
raw: HistogramRaw,
|
||||
}
|
||||
|
||||
impl LiveHistograms {
|
||||
/// Fold a record's outputs into both histograms.
|
||||
pub fn add(&mut self, record: &TxRecord) {
|
||||
Self::eligible_bins(&record.tx, |bin| self.eligible[bin as usize] += 1);
|
||||
for bin in Self::raw_bins(&record.tx) {
|
||||
self.raw[bin] += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Reverse a previous `add` for the same record.
|
||||
pub fn remove(&mut self, record: &TxRecord) {
|
||||
Self::eligible_bins(&record.tx, |bin| self.eligible[bin as usize] -= 1);
|
||||
for bin in Self::raw_bins(&record.tx) {
|
||||
self.raw[bin] -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Round-dollar-eligible bins, blended into the oracle by `live_price`.
|
||||
pub fn eligible(&self) -> HistogramRaw {
|
||||
self.eligible.clone()
|
||||
}
|
||||
|
||||
/// Every live output binned by value, no payment filtering.
|
||||
pub fn raw(&self) -> HistogramRaw {
|
||||
self.raw.clone()
|
||||
}
|
||||
|
||||
/// Round-dollar-eligible bins, applying the oracle payment filter. Calls
|
||||
/// `emit(bin)` per eligible output. Deterministic over a tx's outputs,
|
||||
/// which are never mutated after insert, so add and remove recompute it
|
||||
/// identically rather than caching. Live mempool txs are post-tip, always
|
||||
/// above the historical max-outputs cap window, so the cap never applies.
|
||||
fn eligible_bins(tx: &Transaction, emit: impl FnMut(u16)) {
|
||||
for_each_round_dollar_bin(
|
||||
usize::MAX,
|
||||
tx.output.iter().map(|o| (o.value, o.type_())),
|
||||
emit,
|
||||
);
|
||||
}
|
||||
|
||||
/// Raw bin index per output, dropping only values outside the bin domain
|
||||
/// (zero / out-of-range).
|
||||
fn raw_bins(tx: &Transaction) -> impl Iterator<Item = usize> + '_ {
|
||||
tx.output.iter().filter_map(|o| sats_to_bin(o.value))
|
||||
}
|
||||
}
|
||||
@@ -4,13 +4,13 @@
|
||||
//! one lock-order discipline.
|
||||
|
||||
mod addr_tracker;
|
||||
mod live_histograms;
|
||||
mod outpoint_spends;
|
||||
mod output_bins;
|
||||
mod tx_graveyard;
|
||||
mod tx_store;
|
||||
|
||||
pub use addr_tracker::AddrTracker;
|
||||
pub use live_histograms::LiveHistograms;
|
||||
pub use outpoint_spends::OutpointSpends;
|
||||
pub use output_bins::OutputBins;
|
||||
pub use tx_graveyard::{TxGraveyard, TxTombstone};
|
||||
pub use tx_store::TxStore;
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
use brk_oracle::for_each_round_dollar_bin;
|
||||
use brk_types::Transaction;
|
||||
use smallvec::SmallVec;
|
||||
|
||||
/// Pre-bucketed oracle bins for a tx's eligible outputs. Computed once on
|
||||
/// insert so `Mempool::live_histogram` can bin all live outputs without
|
||||
/// re-parsing scripts or recomputing eligibility per request.
|
||||
pub struct OutputBins(SmallVec<[u16; 4]>);
|
||||
|
||||
impl OutputBins {
|
||||
pub fn from_tx(tx: &Transaction) -> Self {
|
||||
let mut bins = SmallVec::new();
|
||||
// Live mempool txs are post-tip, always above the historical max-outputs
|
||||
// cap window, so the cap never applies here.
|
||||
for_each_round_dollar_bin(
|
||||
usize::MAX,
|
||||
tx.output.iter().map(|o| (o.value, o.type_())),
|
||||
|bin| bins.push(bin),
|
||||
);
|
||||
Self(bins)
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = u16> + '_ {
|
||||
self.0.iter().copied()
|
||||
}
|
||||
}
|
||||
@@ -2,27 +2,20 @@ use brk_oracle::HistogramRaw;
|
||||
use brk_types::{MempoolRecentTx, Transaction, TxOut, Txid, TxidPrefix, Vin};
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
|
||||
use crate::{state::TxEntry, stores::OutputBins};
|
||||
use crate::{state::TxEntry, stores::LiveHistograms};
|
||||
|
||||
const RECENT_CAP: usize = 10;
|
||||
|
||||
/// Per-tx record: live tx body, its mempool entry, and the pre-bucketed
|
||||
/// oracle bins for its outputs. Kept under one key so a single map probe
|
||||
/// returns everything readers need.
|
||||
/// Per-tx record: live tx body and its mempool entry, kept under one key
|
||||
/// so a single map probe returns everything readers need.
|
||||
pub struct TxRecord {
|
||||
pub tx: Transaction,
|
||||
pub entry: TxEntry,
|
||||
pub output_bins: OutputBins,
|
||||
}
|
||||
|
||||
impl TxRecord {
|
||||
pub fn new(tx: Transaction, entry: TxEntry) -> Self {
|
||||
let output_bins = OutputBins::from_tx(&tx);
|
||||
Self {
|
||||
tx,
|
||||
entry,
|
||||
output_bins,
|
||||
}
|
||||
Self { tx, entry }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,15 +25,15 @@ impl TxRecord {
|
||||
/// set of prefixes whose tx still has at least one `prevout: None`,
|
||||
/// maintained on every `insert` / `remove_by_prefix` / `apply_fills`
|
||||
/// so the post-update prevout filler can early-exit when empty.
|
||||
/// `live_histogram` mirrors the union of each record's `OutputBins`,
|
||||
/// kept in sync on `insert` / `remove_by_prefix` so the oracle-blend
|
||||
/// read path is a single array clone, not a full pool walk.
|
||||
/// `histograms` holds the eligible (oracle-blend) and raw per-bin output
|
||||
/// histograms, kept in sync on `insert` / `remove_by_prefix` so each read
|
||||
/// path is a single array clone, not a full pool walk.
|
||||
#[derive(Default)]
|
||||
pub struct TxStore {
|
||||
records: FxHashMap<TxidPrefix, TxRecord>,
|
||||
recent: Vec<MempoolRecentTx>,
|
||||
unresolved: FxHashSet<TxidPrefix>,
|
||||
live_histogram: HistogramRaw,
|
||||
histograms: LiveHistograms,
|
||||
}
|
||||
|
||||
impl TxStore {
|
||||
@@ -92,9 +85,7 @@ impl TxStore {
|
||||
self.unresolved.insert(prefix);
|
||||
}
|
||||
let record = TxRecord::new(tx, entry);
|
||||
for bin in record.output_bins.iter() {
|
||||
self.live_histogram[bin as usize] += 1;
|
||||
}
|
||||
self.histograms.add(&record);
|
||||
self.records.insert(prefix, record);
|
||||
}
|
||||
|
||||
@@ -112,16 +103,21 @@ impl TxStore {
|
||||
pub fn remove_by_prefix(&mut self, prefix: &TxidPrefix) -> Option<TxRecord> {
|
||||
let record = self.records.remove(prefix)?;
|
||||
self.unresolved.remove(prefix);
|
||||
for bin in record.output_bins.iter() {
|
||||
self.live_histogram[bin as usize] -= 1;
|
||||
}
|
||||
self.histograms.remove(&record);
|
||||
Some(record)
|
||||
}
|
||||
|
||||
/// Snapshot the live oracle-bin histogram. Maintained incrementally
|
||||
/// on insert/remove, so this is `O(NUM_BINS)`, not `O(live_outputs)`.
|
||||
pub fn live_histogram(&self) -> HistogramRaw {
|
||||
self.live_histogram.clone()
|
||||
/// Snapshot the round-dollar-eligible histogram that feeds the oracle
|
||||
/// blend. Maintained incrementally, so this is `O(NUM_BINS)`, not
|
||||
/// `O(live_outputs)`.
|
||||
pub fn live_eligible_histogram(&self) -> HistogramRaw {
|
||||
self.histograms.eligible()
|
||||
}
|
||||
|
||||
/// Snapshot the raw histogram: every live output binned by value with no
|
||||
/// payment filtering. Maintained incrementally alongside the eligible one.
|
||||
pub fn live_raw_histogram(&self) -> HistogramRaw {
|
||||
self.histograms.raw()
|
||||
}
|
||||
|
||||
/// Set of prefixes with at least one unfilled prevout. Used by the
|
||||
@@ -338,11 +334,41 @@ mod tests {
|
||||
store.insert(tx_a, entry_a);
|
||||
store.insert(tx_b, entry_b);
|
||||
|
||||
let total_after_both: u32 = store.live_histogram().iter().sum();
|
||||
let total_after_both: u32 = store.live_eligible_histogram().iter().sum();
|
||||
assert_eq!(total_after_both, 3, "two outputs + one output");
|
||||
|
||||
store.remove_by_prefix(&prefix_a);
|
||||
let total_after_remove: u32 = store.live_histogram().iter().sum();
|
||||
let total_after_remove: u32 = store.live_eligible_histogram().iter().sum();
|
||||
assert_eq!(total_after_remove, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_histogram_bins_outputs_the_eligible_filter_drops() {
|
||||
let mut store = TxStore::default();
|
||||
// 2_345 sats is a round-dollar-eligible payment; 100_000_000 sats (1 BTC)
|
||||
// is a round-BTC value the eligible filter drops but raw still bins.
|
||||
let tx = fake_tx(
|
||||
30,
|
||||
&[Some(TxOut::from((p2wpkh_script(1), Sats::from(50_000u64))))],
|
||||
&[(p2wpkh_script(2), 2_345), (p2wpkh_script(3), 100_000_000)],
|
||||
);
|
||||
let entry = entry_for(&tx, 100, 100);
|
||||
let prefix = entry.txid_prefix();
|
||||
store.insert(tx, entry);
|
||||
|
||||
assert_eq!(
|
||||
store.live_eligible_histogram().iter().sum::<u32>(),
|
||||
1,
|
||||
"round-BTC output filtered out of the eligible histogram"
|
||||
);
|
||||
assert_eq!(
|
||||
store.live_raw_histogram().iter().sum::<u32>(),
|
||||
2,
|
||||
"raw histogram bins every output"
|
||||
);
|
||||
|
||||
store.remove_by_prefix(&prefix);
|
||||
assert_eq!(store.live_eligible_histogram().iter().sum::<u32>(), 0);
|
||||
assert_eq!(store.live_raw_histogram().iter().sum::<u32>(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user