From a967fe8f35e151af4981a0abf6ef0ed678a1dd04 Mon Sep 17 00:00:00 2001 From: nym21 Date: Thu, 4 Jun 2026 18:35:48 +0200 Subject: [PATCH] oracle: changes + changelog: updated --- crates/brk_computer/src/price/compute.rs | 5 +- .../brk_mempool/src/stores/live_histograms.rs | 4 +- crates/brk_oracle/README.md | 2 +- crates/brk_oracle/examples/determinism.rs | 6 +- crates/brk_oracle/examples/experiment.rs | 8 +- crates/brk_oracle/examples/report.rs | 11 +- crates/brk_oracle/examples/report_from.rs | 4 +- crates/brk_oracle/src/filter.rs | 226 +++++++++--------- crates/brk_oracle/src/lib.rs | 13 +- docs/CHANGELOG.md | 30 +++ website_next/learn/style.css | 5 +- 11 files changed, 170 insertions(+), 144 deletions(-) diff --git a/crates/brk_computer/src/price/compute.rs b/crates/brk_computer/src/price/compute.rs index f743a93e7..a188b26b0 100644 --- a/crates/brk_computer/src/price/compute.rs +++ b/crates/brk_computer/src/price/compute.rs @@ -3,8 +3,7 @@ use std::ops::Range; use brk_error::Result; use brk_indexer::{Indexer, Lengths}; use brk_oracle::{ - Config, Oracle, START_HEIGHT_FAST, START_HEIGHT_SLOW, bin_to_cents, cents_to_bin, - payment_histogram, + bin_to_cents, cents_to_bin, Config, Oracle, PaymentFilter, START_HEIGHT_FAST, START_HEIGHT_SLOW, }; use brk_types::{Cents, OutputType, Sats, TxIndex, TxOutIndex}; use tracing::info; @@ -298,7 +297,7 @@ impl Vecs { .copied() .zip(output_types[lo..hi].iter().copied()) }); - let hist = payment_histogram(range.start + idx, tx_outputs); + let hist = PaymentFilter::for_height(range.start + idx).histogram(tx_outputs); let ref_bin = oracle.process_histogram(&hist); on_block(range.start + idx, oracle, ref_bin); diff --git a/crates/brk_mempool/src/stores/live_histograms.rs b/crates/brk_mempool/src/stores/live_histograms.rs index bebe32877..26fe4670b 100644 --- a/crates/brk_mempool/src/stores/live_histograms.rs +++ b/crates/brk_mempool/src/stores/live_histograms.rs @@ -1,4 +1,4 @@ -use brk_oracle::{for_each_modern_round_dollar_bin, sats_to_bin, HistogramRaw}; +use brk_oracle::{sats_to_bin, HistogramRaw, PaymentFilter}; use brk_types::Transaction; use crate::stores::tx_store::TxRecord; @@ -45,7 +45,7 @@ impl LiveHistograms { /// which are never mutated after insert, so add and remove recompute it /// identically rather than caching. fn eligible_bins(tx: &Transaction, emit: impl FnMut(u16)) { - for_each_modern_round_dollar_bin(tx.output.iter().map(|o| (o.value, o.type_())), emit); + PaymentFilter::MODERN.for_each_bin(tx.output.iter().map(|o| (o.value, o.type_())), emit); } /// Raw bin index per output, dropping only values outside the bin domain diff --git a/crates/brk_oracle/README.md b/crates/brk_oracle/README.md index 328515914..664384e69 100644 --- a/crates/brk_oracle/README.md +++ b/crates/brk_oracle/README.md @@ -122,7 +122,7 @@ Parabolic interpolation between the best bin and its two neighbors refines the e The oracle consumes one pre-built histogram per block via `process_histogram(&hist)`, a `[u32; 2400]` bin-count array, and returns the updated reference bin. -The caller filters as it builds the histogram, applying the [step 1](#1-filter-outputs) rules. `payment_histogram` builds a fresh block histogram from non-coinbase transaction outputs. Incremental callers use `for_each_round_dollar_bin(height, ...)`; live or otherwise guaranteed-modern callers use `for_each_modern_round_dollar_bin(...)`, which applies the modern fan-out cap without requiring a height. `eligible_bin(sats, output_type)` returns an individual output's bin index, or `None` if filtered. The transaction-level rules include the OP_RETURN drop, the >100 transaction-output fan-out cap below height 630,000, and the >250 cap from height 630,000 onward. +The caller filters as it builds the histogram, applying the [step 1](#1-filter-outputs) rules. `PaymentFilter::for_height(height).histogram(txs)` builds a fresh block histogram from non-coinbase transaction outputs. Incremental live callers use `PaymentFilter::MODERN.for_each_bin(outputs, emit)`, which applies the modern fan-out cap without requiring a height. `PaymentFilter::eligible_bin(sats, output_type)` returns an individual output's bin index, or `None` if filtered. The transaction-level rules include the OP_RETURN drop, the >100 transaction-output fan-out cap below height 630,000, and the >250 cap from height 630,000 onward. The initial seed must be close to the real price at the starting height. The crate includes typed pre-oracle helpers for exchange prices at heights 0..340,000. `Oracle::from_seed()` uses the last baked price, height 339,999 (one below `START_HEIGHT_SLOW`), and the slow cold-start config to seed the oracle's first on-chain computation at height 340,000. diff --git a/crates/brk_oracle/examples/determinism.rs b/crates/brk_oracle/examples/determinism.rs index 0a1eeb1ec..148880c65 100644 --- a/crates/brk_oracle/examples/determinism.rs +++ b/crates/brk_oracle/examples/determinism.rs @@ -13,8 +13,8 @@ use std::path::PathBuf; use brk_indexer::Indexer; use brk_oracle::{ - Config, HistogramRaw, Oracle, START_HEIGHT_FAST, START_HEIGHT_SLOW, bin_to_cents, cents_to_bin, - payment_histogram, + bin_to_cents, cents_to_bin, Config, HistogramRaw, Oracle, PaymentFilter, START_HEIGHT_FAST, + START_HEIGHT_SLOW, }; use brk_types::{OutputType, Sats, TxIndex, TxOutIndex}; use vecdb::{AnyVec, ReadableVec, VecIndex}; @@ -41,7 +41,7 @@ fn build_histogram(block: &Block) -> HistogramRaw { .copied() .zip(block.output_types[lo..hi].iter().copied()) }); - payment_histogram(block.height, tx_outputs) + PaymentFilter::for_height(block.height).histogram(tx_outputs) } fn main() { diff --git a/crates/brk_oracle/examples/experiment.rs b/crates/brk_oracle/examples/experiment.rs index 4a1150830..27018df35 100644 --- a/crates/brk_oracle/examples/experiment.rs +++ b/crates/brk_oracle/examples/experiment.rs @@ -11,8 +11,8 @@ use std::{cmp::Ordering, env, path::PathBuf}; use brk_indexer::Indexer; use brk_oracle::{ - bin_to_cents, cents_to_bin, eligible_bin, seed_bin as oracle_seed_bin, Config, BINS_PER_DECADE, - NUM_BINS, START_HEIGHT_FAST, START_HEIGHT_SLOW, + bin_to_cents, cents_to_bin, seed_bin as oracle_seed_bin, Config, PaymentFilter, + BINS_PER_DECADE, NUM_BINS, START_HEIGHT_FAST, START_HEIGHT_SLOW, }; use brk_types::{OutputType, Sats, TxIndex, TxOutIndex}; use vecdb::{AnyVec, ReadableVec, VecIndex}; @@ -511,7 +511,7 @@ fn main() { add_cfg( format!("pre100_post{post}"), Some(100), - brk_oracle::MODERN_TX_OUTPUT_FANOUT_CAP_START_HEIGHT, + PaymentFilter::MODERN_TX_OUTPUT_FANOUT_CAP_START_HEIGHT, Some(post), ); } @@ -596,7 +596,7 @@ fn main() { } bins.clear(); for i in lo..hi { - if let Some(bin) = eligible_bin(values[i], output_types[i]) { + if let Some(bin) = PaymentFilter::eligible_bin(values[i], output_types[i]) { bins.push(bin); } } diff --git a/crates/brk_oracle/examples/report.rs b/crates/brk_oracle/examples/report.rs index 3a75003b5..55519e1c3 100644 --- a/crates/brk_oracle/examples/report.rs +++ b/crates/brk_oracle/examples/report.rs @@ -6,8 +6,7 @@ use std::path::PathBuf; use brk_indexer::Indexer; use brk_oracle::{ - Config, Oracle, START_HEIGHT_FAST, START_HEIGHT_SLOW, bin_to_cents, cents_to_bin, - payment_histogram, + bin_to_cents, cents_to_bin, Config, Oracle, PaymentFilter, START_HEIGHT_FAST, START_HEIGHT_SLOW, }; use brk_types::{OutputType, Sats, TxIndex, TxOutIndex}; use vecdb::{AnyVec, ReadableVec, VecIndex}; @@ -94,7 +93,11 @@ impl YearStats { fn median_pct(&mut self) -> f64 { self.errors.sort_by(|a, b| a.partial_cmp(b).unwrap()); let n = self.errors.len(); - if n == 0 { 0.0 } else { self.errors[n / 2] } + if n == 0 { + 0.0 + } else { + self.errors[n / 2] + } } fn percentile(&self, p: f64) -> f64 { @@ -242,7 +245,7 @@ fn main() { .copied() .zip(output_types[lo..hi].iter().copied()) }); - let hist = payment_histogram(h, tx_outputs); + let hist = PaymentFilter::for_height(h).histogram(tx_outputs); let ref_bin = oracle.process_histogram(&hist); let oracle_price = bin_to_cents(ref_bin) as f64 / 100.0; diff --git a/crates/brk_oracle/examples/report_from.rs b/crates/brk_oracle/examples/report_from.rs index 7d01bb85e..7a38a41fe 100644 --- a/crates/brk_oracle/examples/report_from.rs +++ b/crates/brk_oracle/examples/report_from.rs @@ -10,7 +10,7 @@ use std::path::PathBuf; use brk_indexer::Indexer; use brk_oracle::{ Config, HistogramEma, HistogramRaw, NUM_BINS, START_HEIGHT_FAST, bin_to_cents, cents_to_bin, - eligible_bin, pre_oracle_price_cents, + pre_oracle_price_cents, PaymentFilter, }; use brk_types::{OutputType, Sats, TxIndex, TxOutIndex}; use vecdb::{AnyVec, ReadableVec, VecIndex}; @@ -913,7 +913,7 @@ fn main() { continue; } for i in lo..hi { - if let Some(bin) = eligible_bin(values[i], output_types[i]) { + if let Some(bin) = PaymentFilter::eligible_bin(values[i], output_types[i]) { hist.increment(bin as usize); } } diff --git a/crates/brk_oracle/src/filter.rs b/crates/brk_oracle/src/filter.rs index 0d2e42cd6..89d243dce 100644 --- a/crates/brk_oracle/src/filter.rs +++ b/crates/brk_oracle/src/filter.rs @@ -10,7 +10,7 @@ const MIN_SATS: u64 = 1000; const EXCLUDED_OUTPUT_TYPES: &[OutputType] = &[OutputType::P2TR]; /// Bitmask form of [`EXCLUDED_OUTPUT_TYPES`], folded at compile time so -/// [`eligible_bin`] checks membership with a single AND. +/// [`PaymentFilter::eligible_bin`] checks membership with a single AND. const EXCLUDED_MASK: u16 = { let mut mask = 0u16; let mut i = 0; @@ -21,98 +21,101 @@ const EXCLUDED_MASK: u16 = { mask }; -/// Pre-modern transaction-output fan-out cap. Above this, the transaction is a -/// batch payout (exchange sweep, mixer fan-out), not a round-dollar payment. -pub const PRE_MODERN_TX_OUTPUT_FANOUT_CAP: usize = 100; - -/// Modern-chain transaction-output fan-out cap. Dense post-630k blocks can -/// carry more genuine payment outputs, but very large fan-outs can still -/// dominate one EMA slot and create a false round-dollar ladder. -pub const MODERN_TX_OUTPUT_FANOUT_CAP: usize = 250; - -/// Height where [`PRE_MODERN_TX_OUTPUT_FANOUT_CAP`] relaxes to -/// [`MODERN_TX_OUTPUT_FANOUT_CAP`]. -pub const MODERN_TX_OUTPUT_FANOUT_CAP_START_HEIGHT: usize = 630_000; - -#[inline(always)] -fn tx_output_fanout_cap(height: usize) -> usize { - if height < MODERN_TX_OUTPUT_FANOUT_CAP_START_HEIGHT { - PRE_MODERN_TX_OUTPUT_FANOUT_CAP - } else { - MODERN_TX_OUTPUT_FANOUT_CAP - } -} - -/// Bin index for `(sats, output_type)`, or `None` for an excluded type (P2TR), -/// dust, a round-BTC value, or an out-of-range bin. The per-output half of the -/// round-dollar payment filter. -#[inline(always)] -pub fn eligible_bin(sats: Sats, output_type: OutputType) -> Option { - if EXCLUDED_MASK & (1u16 << output_type as u8) != 0 { - return None; - } - if *sats < MIN_SATS || sats.is_common_round_value() { - return None; - } - sats_to_bin(sats).map(|b| b as u16) -} - -/// The on-chain round-dollar payment filter, shared by the indexer warm-up, -/// per-request reconstruction, and the mempool's live histogram so every path -/// bins identically. Calls `emit(bin)` for each eligible output, in order. +/// Round-dollar payment filter. /// -/// A whole transaction is dropped when it carries any OP_RETURN output (data -/// carriers, not payments) or when it has more than the height-specific fan-out -/// cap: [`PRE_MODERN_TX_OUTPUT_FANOUT_CAP`] below -/// [`MODERN_TX_OUTPUT_FANOUT_CAP_START_HEIGHT`], -/// [`MODERN_TX_OUTPUT_FANOUT_CAP`] at and above it. `height` is the block these -/// outputs belong to. Live or otherwise guaranteed-modern callers should use -/// [`for_each_modern_round_dollar_bin`] instead. -#[inline] -pub fn for_each_round_dollar_bin( - height: usize, - outputs: impl ExactSizeIterator + Clone, - mut emit: impl FnMut(u16), -) { - if outputs.len() > tx_output_fanout_cap(height) { - return; - } - if outputs.clone().any(|(_, ty)| ty == OutputType::OpReturn) { - return; - } - for (sats, ty) in outputs { - if let Some(bin) = eligible_bin(sats, ty) { - emit(bin); +/// Input: transaction outputs. Output: eligible log-scale bins or a fresh block +/// histogram. The only state is the transaction-output fan-out cap selected by +/// block height, or [`MODERN`](Self::MODERN) for live modern transaction streams. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct PaymentFilter { + tx_output_fanout_cap: usize, +} + +impl PaymentFilter { + /// Pre-modern transaction-output fan-out cap. Above this, the transaction is + /// a batch payout (exchange sweep, mixer fan-out), not a round-dollar + /// payment. + pub const PRE_MODERN_TX_OUTPUT_FANOUT_CAP: usize = 100; + + /// Modern-chain transaction-output fan-out cap. Dense post-630k blocks can + /// carry more genuine payment outputs, but very large fan-outs can still + /// dominate one EMA slot and create a false round-dollar ladder. + pub const MODERN_TX_OUTPUT_FANOUT_CAP: usize = 250; + + /// Height where [`Self::PRE_MODERN_TX_OUTPUT_FANOUT_CAP`] relaxes to + /// [`Self::MODERN_TX_OUTPUT_FANOUT_CAP`]. + pub const MODERN_TX_OUTPUT_FANOUT_CAP_START_HEIGHT: usize = 630_000; + + /// Filter for live or otherwise guaranteed-modern transaction streams. + pub const MODERN: Self = Self::with_fanout_cap(Self::MODERN_TX_OUTPUT_FANOUT_CAP); + + const fn with_fanout_cap(tx_output_fanout_cap: usize) -> Self { + Self { + tx_output_fanout_cap, } } -} -/// Heightless form of [`for_each_round_dollar_bin`] for live or otherwise -/// guaranteed-modern transaction streams. Applies -/// [`MODERN_TX_OUTPUT_FANOUT_CAP`]. -#[inline] -pub fn for_each_modern_round_dollar_bin( - outputs: impl ExactSizeIterator + Clone, - emit: impl FnMut(u16), -) { - for_each_round_dollar_bin(MODERN_TX_OUTPUT_FANOUT_CAP_START_HEIGHT, outputs, emit); -} - -/// Build a fresh eligible round-dollar payment histogram for one block's -/// non-coinbase transaction outputs. -#[inline] -pub fn payment_histogram( - height: usize, - txs: impl IntoIterator, -) -> HistogramRaw -where - Outputs: ExactSizeIterator + Clone, -{ - let mut hist = HistogramRaw::zeros(); - for outputs in txs { - for_each_round_dollar_bin(height, outputs, |bin| hist.increment(bin as usize)); + /// Filter for transactions in `height`. + pub const fn for_height(height: usize) -> Self { + if height < Self::MODERN_TX_OUTPUT_FANOUT_CAP_START_HEIGHT { + Self::with_fanout_cap(Self::PRE_MODERN_TX_OUTPUT_FANOUT_CAP) + } else { + Self::MODERN + } + } + + /// Bin index for `(sats, output_type)`, or `None` for an excluded type + /// (P2TR), dust, a round-BTC value, or an out-of-range bin. The per-output + /// half of the round-dollar payment filter. + #[inline(always)] + pub fn eligible_bin(sats: Sats, output_type: OutputType) -> Option { + if EXCLUDED_MASK & (1u16 << output_type as u8) != 0 { + return None; + } + if *sats < MIN_SATS || sats.is_common_round_value() { + return None; + } + sats_to_bin(sats).map(|b| b as u16) + } + + /// Apply the transaction-level payment filter and call `emit(bin)` for each + /// eligible output, in order. + /// + /// A whole transaction is dropped when it carries any OP_RETURN output (data + /// carriers, not payments) or when it has more outputs than this filter's + /// fan-out cap. + #[inline] + pub fn for_each_bin( + self, + outputs: impl ExactSizeIterator + Clone, + mut emit: impl FnMut(u16), + ) { + if outputs.len() > self.tx_output_fanout_cap { + return; + } + if outputs.clone().any(|(_, ty)| ty == OutputType::OpReturn) { + return; + } + for (sats, ty) in outputs { + if let Some(bin) = Self::eligible_bin(sats, ty) { + emit(bin); + } + } + } + + /// Build a fresh eligible round-dollar payment histogram for one block's + /// non-coinbase transaction outputs. + #[inline] + pub fn histogram(self, txs: impl IntoIterator) -> HistogramRaw + where + Outputs: ExactSizeIterator + Clone, + { + let mut hist = HistogramRaw::zeros(); + for outputs in txs { + self.for_each_bin(outputs, |bin| hist.increment(bin as usize)); + } + hist } - hist } #[cfg(test)] @@ -125,7 +128,7 @@ mod tests { fn emitted_count(height: usize, len: usize) -> usize { let mut count = 0; - for_each_round_dollar_bin(height, payment_outputs(len), |_| count += 1); + PaymentFilter::for_height(height).for_each_bin(payment_outputs(len), |_| count += 1); count } @@ -133,15 +136,15 @@ mod tests { fn early_fanout_cap_is_strict() { assert_eq!( emitted_count( - MODERN_TX_OUTPUT_FANOUT_CAP_START_HEIGHT - 1, - PRE_MODERN_TX_OUTPUT_FANOUT_CAP, + PaymentFilter::MODERN_TX_OUTPUT_FANOUT_CAP_START_HEIGHT - 1, + PaymentFilter::PRE_MODERN_TX_OUTPUT_FANOUT_CAP, ), - PRE_MODERN_TX_OUTPUT_FANOUT_CAP + PaymentFilter::PRE_MODERN_TX_OUTPUT_FANOUT_CAP ); assert_eq!( emitted_count( - MODERN_TX_OUTPUT_FANOUT_CAP_START_HEIGHT - 1, - PRE_MODERN_TX_OUTPUT_FANOUT_CAP + 1, + PaymentFilter::MODERN_TX_OUTPUT_FANOUT_CAP_START_HEIGHT - 1, + PaymentFilter::PRE_MODERN_TX_OUTPUT_FANOUT_CAP + 1, ), 0 ); @@ -151,15 +154,15 @@ mod tests { fn modern_fanout_cap_is_relaxed_but_not_lifted() { assert_eq!( emitted_count( - MODERN_TX_OUTPUT_FANOUT_CAP_START_HEIGHT, - MODERN_TX_OUTPUT_FANOUT_CAP, + PaymentFilter::MODERN_TX_OUTPUT_FANOUT_CAP_START_HEIGHT, + PaymentFilter::MODERN_TX_OUTPUT_FANOUT_CAP, ), - MODERN_TX_OUTPUT_FANOUT_CAP + PaymentFilter::MODERN_TX_OUTPUT_FANOUT_CAP ); assert_eq!( emitted_count( - MODERN_TX_OUTPUT_FANOUT_CAP_START_HEIGHT, - MODERN_TX_OUTPUT_FANOUT_CAP + 1, + PaymentFilter::MODERN_TX_OUTPUT_FANOUT_CAP_START_HEIGHT, + PaymentFilter::MODERN_TX_OUTPUT_FANOUT_CAP + 1, ), 0 ); @@ -167,17 +170,20 @@ mod tests { fn emitted_count_modern(len: usize) -> usize { let mut count = 0; - for_each_modern_round_dollar_bin(payment_outputs(len), |_| count += 1); + PaymentFilter::MODERN.for_each_bin(payment_outputs(len), |_| count += 1); count } #[test] fn modern_helper_uses_modern_fanout_cap() { assert_eq!( - emitted_count_modern(MODERN_TX_OUTPUT_FANOUT_CAP), - MODERN_TX_OUTPUT_FANOUT_CAP + emitted_count_modern(PaymentFilter::MODERN_TX_OUTPUT_FANOUT_CAP), + PaymentFilter::MODERN_TX_OUTPUT_FANOUT_CAP + ); + assert_eq!( + emitted_count_modern(PaymentFilter::MODERN_TX_OUTPUT_FANOUT_CAP + 1), + 0 ); - assert_eq!(emitted_count_modern(MODERN_TX_OUTPUT_FANOUT_CAP + 1), 0); } #[test] @@ -190,12 +196,9 @@ mod tests { (sats, OutputType::P2WPKH), ], ]; - let hist = payment_histogram( - MODERN_TX_OUTPUT_FANOUT_CAP_START_HEIGHT, - txs.into_iter().map(|tx| tx.into_iter()), - ); + let hist = PaymentFilter::MODERN.histogram(txs.into_iter().map(|tx| tx.into_iter())); - let bin = eligible_bin(sats, OutputType::P2WPKH).unwrap() as usize; + let bin = PaymentFilter::eligible_bin(sats, OutputType::P2WPKH).unwrap() as usize; assert_eq!(hist[bin], 2); } @@ -207,12 +210,9 @@ mod tests { (Sats::new(100_000_000), OutputType::P2WPKH), ]]; - let hist = payment_histogram( - MODERN_TX_OUTPUT_FANOUT_CAP_START_HEIGHT, - txs.into_iter().map(|tx| tx.into_iter()), - ); + let hist = PaymentFilter::MODERN.histogram(txs.into_iter().map(|tx| tx.into_iter())); - let bin = eligible_bin(sats, OutputType::P2WPKH).unwrap() as usize; + let bin = PaymentFilter::eligible_bin(sats, OutputType::P2WPKH).unwrap() as usize; assert_eq!(hist[bin], 1); } } diff --git a/crates/brk_oracle/src/lib.rs b/crates/brk_oracle/src/lib.rs index 1b31ed04f..7260bef0d 100644 --- a/crates/brk_oracle/src/lib.rs +++ b/crates/brk_oracle/src/lib.rs @@ -9,9 +9,10 @@ //! pre-oracle tape. From there to [`START_HEIGHT_FAST`] a slow cold-start EMA //! runs with a shape-anchoring restoring force. At [`START_HEIGHT_FAST`] it //! switches to a fast EMA that tracks mature-market volatility. -//! - Output filter (`filter`): below [`MODERN_TX_OUTPUT_FANOUT_CAP_START_HEIGHT`] -//! batch-payout transactions are capped strictly. Above it the cap relaxes but -//! still drops very large fan-outs. +//! - Output filter (`filter`): below +//! [`PaymentFilter::MODERN_TX_OUTPUT_FANOUT_CAP_START_HEIGHT`] batch-payout +//! transactions are capped strictly. Above it the cap relaxes but still drops +//! very large fan-outs. //! //! The two boundaries differ on purpose. The EMA must hand off to fast before the //! 2020 crash, while the output cap helps the thin pre-2020 mix for longer and @@ -27,11 +28,7 @@ mod stencil; mod window; pub use config::{Config, START_HEIGHT_FAST, START_HEIGHT_SLOW}; -pub use filter::{ - eligible_bin, for_each_modern_round_dollar_bin, for_each_round_dollar_bin, payment_histogram, - MODERN_TX_OUTPUT_FANOUT_CAP, MODERN_TX_OUTPUT_FANOUT_CAP_START_HEIGHT, - PRE_MODERN_TX_OUTPUT_FANOUT_CAP, -}; +pub use filter::PaymentFilter; pub use scale::{ bin_to_cents, cents_to_bin, sats_to_bin, HistogramEma, HistogramEmaCompact, HistogramRaw, BINS_PER_DECADE, NUM_BINS, diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 3d4f56acb..494f2b191 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -6,6 +6,36 @@ All notable changes to the Bitcoin Research Kit (BRK) project will be documented > *This changelog was generated by Claude Code* +## Unreleased - 2026-06-04 + +### Breaking Changes + +#### `brk_oracle` + +- Bumped the oracle algorithm to version 4 by keeping the strict 100-output transaction fan-out cap below height 630,000 and applying a 250-output cap from height 630,000 onward instead of fully lifting the cap. Stored oracle-derived price data must be recomputed, and historical values from the modern period can differ from v0.3.1 because very large fan-out transactions no longer enter the round-dollar histogram ([source](https://github.com/bitcoinresearchkit/brk/blob/main/crates/brk_oracle/src/filter.rs), [source](https://github.com/bitcoinresearchkit/brk/blob/main/crates/brk_oracle/src/lib.rs)) +- Replaced the public free-function payment filter API with `PaymentFilter`. Library callers must move from `payment_histogram(height, txs)` to `PaymentFilter::for_height(height).histogram(txs)`, from `for_each_round_dollar_bin(height, outputs, emit)` or `for_each_modern_round_dollar_bin(outputs, emit)` to `PaymentFilter::for_height(height).for_each_bin(outputs, emit)` or `PaymentFilter::MODERN.for_each_bin(outputs, emit)`, and from `eligible_bin(sats, output_type)` to `PaymentFilter::eligible_bin(sats, output_type)` ([source](https://github.com/bitcoinresearchkit/brk/blob/main/crates/brk_oracle/src/filter.rs), [source](https://github.com/bitcoinresearchkit/brk/blob/main/crates/brk_oracle/src/lib.rs)) + +### Internal Changes + +#### `brk_oracle` + +- Moved the baked pre-oracle price tape behind the `seed` module and added `Oracle::from_seed()`, so report and determinism tools start from the same height-339,999 seed path as production on-chain computation ([source](https://github.com/bitcoinresearchkit/brk/blob/main/crates/brk_oracle/src/seed.rs), [source](https://github.com/bitcoinresearchkit/brk/blob/main/crates/brk_oracle/src/lib.rs)) +- Contained the round-dollar stencil search behind `Stencil`, with the shape anchor and candidate scoring kept private to the stencil module. `Oracle` now stores a `Stencil` and asks it to pick the next reference bin, keeping the root oracle type focused on window state, config, and price conversion ([source](https://github.com/bitcoinresearchkit/brk/blob/main/crates/brk_oracle/src/stencil.rs), [source](https://github.com/bitcoinresearchkit/brk/blob/main/crates/brk_oracle/src/lib.rs)) +- Added the `experiment` example for replaying filter and EMA variants against historical OHLC data, and changed the report and determinism examples to use `Oracle::from_seed()` and the shared `PaymentFilter` entry points ([source](https://github.com/bitcoinresearchkit/brk/blob/main/crates/brk_oracle/examples/experiment.rs), [source](https://github.com/bitcoinresearchkit/brk/blob/main/crates/brk_oracle/examples/report.rs), [source](https://github.com/bitcoinresearchkit/brk/blob/main/crates/brk_oracle/examples/determinism.rs)) +- Rewrote the oracle README's input-surface and version-history notes for version 4, including the post-630,000 250-output cap and the `PaymentFilter` API that all callers should use when constructing payment histograms ([source](https://github.com/bitcoinresearchkit/brk/blob/main/crates/brk_oracle/README.md)) + +#### `brk_computer` + +- Routed stored oracle price computation through `PaymentFilter::for_height(height).histogram(txs)`, keeping historical price vec generation on the same transaction filter API exposed by `brk_oracle` ([source](https://github.com/bitcoinresearchkit/brk/blob/main/crates/brk_computer/src/price/compute.rs)) + +#### `brk_mempool` + +- Routed live eligible histogram maintenance through `PaymentFilter::MODERN.for_each_bin(...)`, so mempool callers automatically apply the modern 250-output fan-out cap without carrying a synthetic block height ([source](https://github.com/bitcoinresearchkit/brk/blob/main/crates/brk_mempool/src/stores/live_histograms.rs)) + +#### `website` + +- Started work on the next version of the BRK website. This is still in progress and is documented here only as an unreleased development track ([source](https://github.com/bitcoinresearchkit/brk/tree/main/website_next)) + ## [v0.3.1](https://github.com/bitcoinresearchkit/brk/releases/tag/v0.3.1) - 2026-06-01 ### Breaking Changes diff --git a/website_next/learn/style.css b/website_next/learn/style.css index 32a0d492c..11e572970 100644 --- a/website_next/learn/style.css +++ b/website_next/learn/style.css @@ -3,10 +3,7 @@ main.learn { --sidebar-top: 6rem; --sidebar-bottom: 1rem; - display: grid; - grid-template-columns: minmax(0, 1fr) 14rem; - grid-template-rows: minmax(0, 1fr); - gap: 4rem; + display: flex; overflow: hidden; article {