oracle: changes + changelog: updated

This commit is contained in:
nym21
2026-06-04 18:35:48 +02:00
parent a3f3c54675
commit a967fe8f35
11 changed files with 170 additions and 144 deletions
+2 -3
View File
@@ -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);
@@ -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
+1 -1
View File
@@ -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.
+3 -3
View File
@@ -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() {
+4 -4
View File
@@ -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);
}
}
+7 -4
View File
@@ -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;
+2 -2
View File
@@ -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);
}
}
+113 -113
View File
@@ -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<u16> {
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<Item = (Sats, OutputType)> + 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<Item = (Sats, OutputType)> + 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<Outputs>(
height: usize,
txs: impl IntoIterator<Item = Outputs>,
) -> HistogramRaw
where
Outputs: ExactSizeIterator<Item = (Sats, OutputType)> + 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<u16> {
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<Item = (Sats, OutputType)> + 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<Outputs>(self, txs: impl IntoIterator<Item = Outputs>) -> HistogramRaw
where
Outputs: ExactSizeIterator<Item = (Sats, OutputType)> + 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);
}
}
+5 -8
View File
@@ -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,
+30
View File
@@ -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
+1 -4
View File
@@ -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 {