mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-07-27 18:58:10 -07:00
global: snapshot
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
//! Verify oracle determinism: oracles started from different heights converge
|
||||
//! to identical ref_bin values after the ring buffer fills.
|
||||
//!
|
||||
//! Creates a reference oracle at height 575k and test oracles every 1000 blocks
|
||||
//! up to 630k. After window_size blocks, each test oracle should produce the
|
||||
//! same ref_bin as the reference, proving the truncated EMA provides
|
||||
//! start-point independence.
|
||||
//!
|
||||
//! Run with: cargo run -p brk_oracle --example determinism --release
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use brk_indexer::Indexer;
|
||||
use brk_oracle::{Config, NUM_BINS, Oracle, PRICES, START_HEIGHT, cents_to_bin, sats_to_bin};
|
||||
use brk_types::{OutputType, Sats, TxIndex, TxOutIndex};
|
||||
use vecdb::{AnyVec, VecIndex, VecIterator};
|
||||
|
||||
fn seed_bin(height: usize) -> f64 {
|
||||
let price: f64 = PRICES
|
||||
.lines()
|
||||
.nth(height - 1)
|
||||
.expect("prices.txt too short")
|
||||
.parse()
|
||||
.expect("Failed to parse seed price");
|
||||
cents_to_bin(price * 100.0)
|
||||
}
|
||||
|
||||
struct TestRun {
|
||||
start_height: usize,
|
||||
oracle: Option<Oracle>,
|
||||
converged_at: Option<usize>,
|
||||
diverged_after: bool,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let data_dir = std::env::var("BRK_DIR")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|_| {
|
||||
let home = std::env::var("HOME").unwrap();
|
||||
PathBuf::from(home).join(".brk")
|
||||
});
|
||||
|
||||
let indexer = Indexer::forced_import(&data_dir).expect("Failed to load indexer");
|
||||
let total_heights = indexer.vecs.blocks.timestamp.len();
|
||||
|
||||
let config = Config::default();
|
||||
let window_size = config.window_size;
|
||||
|
||||
let total_txs = indexer.vecs.transactions.height.len();
|
||||
let total_outputs = indexer.vecs.outputs.value.len();
|
||||
|
||||
let mut first_txindex_iter = indexer.vecs.transactions.first_txindex.into_iter();
|
||||
let mut first_txoutindex_iter = indexer.vecs.transactions.first_txoutindex.into_iter();
|
||||
let mut out_first_iter = indexer.vecs.outputs.first_txoutindex.into_iter();
|
||||
let mut value_iter = indexer.vecs.outputs.value.into_iter();
|
||||
let mut outputtype_iter = indexer.vecs.outputs.outputtype.into_iter();
|
||||
|
||||
let ref_config = Config::default();
|
||||
|
||||
// Reference oracle at 575k.
|
||||
let ref_start = START_HEIGHT;
|
||||
let mut ref_oracle = Oracle::new(seed_bin(ref_start), Config::default());
|
||||
|
||||
// Test oracles every 1000 blocks from 576k to 630k.
|
||||
let mut runs: Vec<TestRun> = (576_000..=630_000)
|
||||
.step_by(1000)
|
||||
.map(|h| TestRun {
|
||||
start_height: h,
|
||||
oracle: None,
|
||||
converged_at: None,
|
||||
diverged_after: false,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let last_start = runs.last().map(|r| r.start_height).unwrap_or(ref_start);
|
||||
// Process enough blocks for all oracles to converge + verification margin.
|
||||
let end_height = (last_start + window_size + 100).min(total_heights);
|
||||
|
||||
for h in START_HEIGHT..end_height {
|
||||
let first_txindex: TxIndex = first_txindex_iter.get_at_unwrap(h);
|
||||
let next_first_txindex = first_txindex_iter
|
||||
.get_at(h + 1)
|
||||
.unwrap_or(TxIndex::from(total_txs));
|
||||
|
||||
let out_start = if first_txindex.to_usize() + 1 < next_first_txindex.to_usize() {
|
||||
first_txoutindex_iter
|
||||
.get_at_unwrap(first_txindex.to_usize() + 1)
|
||||
.to_usize()
|
||||
} else {
|
||||
out_first_iter
|
||||
.get_at(h + 1)
|
||||
.unwrap_or(TxOutIndex::from(total_outputs))
|
||||
.to_usize()
|
||||
};
|
||||
let out_end = out_first_iter
|
||||
.get_at(h + 1)
|
||||
.unwrap_or(TxOutIndex::from(total_outputs))
|
||||
.to_usize();
|
||||
|
||||
let mut hist = [0u32; NUM_BINS];
|
||||
for i in out_start..out_end {
|
||||
let sats: Sats = value_iter.get_at_unwrap(i);
|
||||
let output_type: OutputType = outputtype_iter.get_at_unwrap(i);
|
||||
if ref_config.excluded_output_types.contains(&output_type) {
|
||||
continue;
|
||||
}
|
||||
if *sats < ref_config.min_sats
|
||||
|| (ref_config.exclude_common_round_values && sats.is_common_round_value())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if let Some(bin) = sats_to_bin(sats) {
|
||||
hist[bin] += 1;
|
||||
}
|
||||
}
|
||||
|
||||
let ref_bin = ref_oracle.process_histogram(&hist);
|
||||
|
||||
for run in &mut runs {
|
||||
if h < run.start_height {
|
||||
continue;
|
||||
}
|
||||
if run.oracle.is_none() {
|
||||
run.oracle = Some(Oracle::new(seed_bin(run.start_height), Config::default()));
|
||||
}
|
||||
let test_bin = run.oracle.as_mut().unwrap().process_histogram(&hist);
|
||||
|
||||
if run.converged_at.is_some() {
|
||||
if test_bin != ref_bin {
|
||||
run.diverged_after = true;
|
||||
}
|
||||
} else if test_bin == ref_bin {
|
||||
run.converged_at = Some(h);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Print results.
|
||||
println!();
|
||||
println!(
|
||||
"{:<12} {:>16} {:>8}",
|
||||
"Start", "Converged at", "Blocks"
|
||||
);
|
||||
println!("{}", "-".repeat(40));
|
||||
|
||||
let mut max_blocks = 0usize;
|
||||
let mut failed = Vec::new();
|
||||
let mut diverged = Vec::new();
|
||||
|
||||
for run in &runs {
|
||||
if let Some(converged) = run.converged_at {
|
||||
let blocks = converged - run.start_height;
|
||||
if blocks > max_blocks {
|
||||
max_blocks = blocks;
|
||||
}
|
||||
println!(
|
||||
"{:<12} {:>16} {:>8}",
|
||||
run.start_height, converged, blocks
|
||||
);
|
||||
if run.diverged_after {
|
||||
diverged.push(run.start_height);
|
||||
}
|
||||
} else {
|
||||
println!("{:<12} {:>16} {:>8}", run.start_height, "NEVER", "-");
|
||||
failed.push(run.start_height);
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
println!(
|
||||
"{}/{} converged, max {} blocks to converge (window_size={})",
|
||||
runs.len() - failed.len(),
|
||||
runs.len(),
|
||||
max_blocks,
|
||||
window_size,
|
||||
);
|
||||
|
||||
if !diverged.is_empty() {
|
||||
println!("DIVERGED after convergence: {:?}", diverged);
|
||||
}
|
||||
if !failed.is_empty() {
|
||||
println!("NEVER converged: {:?}", failed);
|
||||
}
|
||||
|
||||
// Assertions.
|
||||
assert!(
|
||||
failed.is_empty(),
|
||||
"{} oracles never converged: {:?}",
|
||||
failed.len(),
|
||||
failed
|
||||
);
|
||||
assert!(
|
||||
diverged.is_empty(),
|
||||
"{} oracles diverged after convergence: {:?}",
|
||||
diverged.len(),
|
||||
diverged
|
||||
);
|
||||
assert!(
|
||||
max_blocks <= window_size * 2,
|
||||
"Convergence took {} blocks, expected <= {} (2 * window_size)",
|
||||
max_blocks,
|
||||
window_size * 2
|
||||
);
|
||||
|
||||
println!();
|
||||
println!("All assertions passed!");
|
||||
}
|
||||
@@ -132,6 +132,7 @@ fn find_best_bin(
|
||||
best_bin as f64 + sub_bin
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Config {
|
||||
/// EMA decay: 2/(N+1) where N is span in blocks. 2/7 = 6-block span.
|
||||
pub alpha: f64,
|
||||
@@ -162,29 +163,39 @@ impl Default for Config {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Oracle {
|
||||
histograms: Vec<[u32; NUM_BINS]>,
|
||||
ema: Box<[f64; NUM_BINS]>,
|
||||
weights: Vec<f64>,
|
||||
cursor: usize,
|
||||
filled: usize,
|
||||
ref_bin: f64,
|
||||
config: Config,
|
||||
weights: Vec<f64>,
|
||||
excluded_mask: u16,
|
||||
warmup: bool,
|
||||
}
|
||||
|
||||
impl Oracle {
|
||||
pub fn new(start_bin: f64, config: Config) -> Self {
|
||||
let weights: Vec<f64> = (0..config.window_size)
|
||||
.map(|age| config.alpha * (1.0 - config.alpha).powi(age as i32))
|
||||
.collect();
|
||||
let window_size = config.window_size;
|
||||
let decay = 1.0 - config.alpha;
|
||||
let weights: Vec<f64> = (0..window_size)
|
||||
.map(|i| config.alpha * decay.powi(i as i32))
|
||||
.collect();
|
||||
let excluded_mask = config
|
||||
.excluded_output_types
|
||||
.iter()
|
||||
.fold(0u16, |mask, ot| mask | (1 << *ot as u8));
|
||||
Self {
|
||||
histograms: vec![[0u32; NUM_BINS]; window_size],
|
||||
ema: Box::new([0.0; NUM_BINS]),
|
||||
weights,
|
||||
cursor: 0,
|
||||
filled: 0,
|
||||
ref_bin: start_bin,
|
||||
weights,
|
||||
excluded_mask,
|
||||
warmup: false,
|
||||
config,
|
||||
}
|
||||
}
|
||||
@@ -215,7 +226,10 @@ impl Oracle {
|
||||
/// ref_bin is anchored to the checkpoint regardless of warmup drift.
|
||||
pub fn from_checkpoint(ref_bin: f64, config: Config, fill: impl FnOnce(&mut Self)) -> Self {
|
||||
let mut oracle = Self::new(ref_bin, config);
|
||||
oracle.warmup = true;
|
||||
fill(&mut oracle);
|
||||
oracle.warmup = false;
|
||||
oracle.recompute_ema();
|
||||
oracle.ref_bin = ref_bin;
|
||||
oracle
|
||||
}
|
||||
@@ -236,12 +250,19 @@ impl Oracle {
|
||||
self.price_cents().into()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn output_to_bin(&self, sats: Sats, output_type: OutputType) -> Option<usize> {
|
||||
self.eligible_bin(sats, output_type)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn eligible_bin(&self, sats: Sats, output_type: OutputType) -> Option<usize> {
|
||||
if self.config.excluded_output_types.contains(&output_type) {
|
||||
if self.excluded_mask & (1 << output_type as u8) != 0 {
|
||||
return None;
|
||||
}
|
||||
if *sats < self.config.min_sats || (self.config.exclude_common_round_values && sats.is_common_round_value()) {
|
||||
if *sats < self.config.min_sats
|
||||
|| (self.config.exclude_common_round_values && sats.is_common_round_value())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
sats_to_bin(sats)
|
||||
@@ -254,24 +275,28 @@ impl Oracle {
|
||||
self.filled += 1;
|
||||
}
|
||||
|
||||
self.recompute_ema();
|
||||
if !self.warmup {
|
||||
self.recompute_ema();
|
||||
|
||||
self.ref_bin = find_best_bin(
|
||||
&self.ema,
|
||||
self.ref_bin,
|
||||
self.config.search_below,
|
||||
self.config.search_above,
|
||||
);
|
||||
self.ref_bin = find_best_bin(
|
||||
&self.ema,
|
||||
self.ref_bin,
|
||||
self.config.search_below,
|
||||
self.config.search_above,
|
||||
);
|
||||
}
|
||||
self.ref_bin
|
||||
}
|
||||
|
||||
fn recompute_ema(&mut self) {
|
||||
self.ema.fill(0.0);
|
||||
for age in 0..self.filled {
|
||||
let idx = (self.cursor + self.config.window_size - 1 - age) % self.config.window_size;
|
||||
let idx =
|
||||
(self.cursor + self.config.window_size - 1 - age) % self.config.window_size;
|
||||
let weight = self.weights[age];
|
||||
let h = &self.histograms[idx];
|
||||
for bin in 0..NUM_BINS {
|
||||
self.ema[bin] += weight * self.histograms[idx][bin] as f64;
|
||||
self.ema[bin] += weight * h[bin] as f64;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user