mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-06-08 14:11:56 -07:00
oracle: changes + changelog: updated
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user