server: ms endpoint fixes

This commit is contained in:
nym21
2026-04-02 22:37:34 +02:00
parent d92cf43c57
commit 8dfc1bc932
62 changed files with 1639 additions and 1698 deletions
+13 -40
View File
@@ -1,49 +1,22 @@
use brk_error::Result;
use brk_types::{BlockFeesEntry, Height, Sats, TimePeriod};
use vecdb::{ReadableVec, VecIndex};
use brk_types::{BlockFeesEntry, TimePeriod};
use super::day1_iter::Day1Iter;
use super::block_window::BlockWindow;
use crate::Query;
impl Query {
pub fn block_fees(&self, time_period: TimePeriod) -> Result<Vec<BlockFeesEntry>> {
let computer = self.computer();
let current_height = self.height();
let start = current_height
.to_usize()
.saturating_sub(time_period.block_count());
let iter = Day1Iter::new(computer, start, current_height.to_usize());
let cumulative = &computer.mining.rewards.fees.cumulative.sats.height;
let first_height = &computer.indexes.day1.first_height;
Ok(iter.collect(|di, ts, h| {
let h_start = first_height.collect_one(di)?;
let h_end = first_height
.collect_one(di + 1_usize)
.unwrap_or(Height::from(current_height.to_usize() + 1));
let block_count = h_end.to_usize() - h_start.to_usize();
if block_count == 0 {
return None;
}
let cumulative_end = cumulative.collect_one_at(h_end.to_usize() - 1)?;
let cumulative_start = if h_start.to_usize() > 0 {
cumulative
.collect_one_at(h_start.to_usize() - 1)
.unwrap_or(Sats::ZERO)
} else {
Sats::ZERO
};
let daily_sum = cumulative_end - cumulative_start;
let avg_fees = Sats::from(*daily_sum / block_count as u64);
Some(BlockFeesEntry {
avg_height: h,
timestamp: ts,
avg_fees,
let bw = BlockWindow::new(self, time_period);
let cumulative = &self.computer().mining.rewards.fees.cumulative.sats.height;
Ok(bw
.cumulative_averages(self, cumulative)
.into_iter()
.map(|w| BlockFeesEntry {
avg_height: w.avg_height,
timestamp: w.timestamp,
avg_fees: w.avg_value,
usd: w.usd,
})
}))
.collect())
}
}
@@ -1,86 +1,22 @@
use brk_error::Result;
use brk_types::{BlockRewardsEntry, Height, Sats, TimePeriod};
use vecdb::{ReadableVec, VecIndex};
use brk_types::{BlockRewardsEntry, TimePeriod};
use super::block_window::BlockWindow;
use crate::Query;
impl Query {
pub fn block_rewards(&self, time_period: TimePeriod) -> Result<Vec<BlockRewardsEntry>> {
let computer = self.computer();
let indexer = self.indexer();
let current_height = self.height().to_usize();
let start = current_height.saturating_sub(time_period.block_count());
let coinbase_vec = &computer.mining.rewards.coinbase.block.sats;
let timestamp_vec = &indexer.vecs.blocks.timestamp;
match time_period {
// Per-block, exact rewards
TimePeriod::Day | TimePeriod::ThreeDays => {
let rewards: Vec<Sats> = coinbase_vec.collect_range_at(start, current_height + 1);
let timestamps: Vec<brk_types::Timestamp> =
timestamp_vec.collect_range_at(start, current_height + 1);
Ok(rewards
.iter()
.zip(timestamps.iter())
.enumerate()
.map(|(i, (reward, ts))| BlockRewardsEntry {
avg_height: (start + i) as u32,
timestamp: **ts,
avg_rewards: **reward,
})
.collect())
}
// Daily averages, sampled to ~200 points
_ => {
let first_height_vec = &computer.indexes.day1.first_height;
let day1_vec = &computer.indexes.height.day1;
let start_di = day1_vec
.collect_one(Height::from(start))
.unwrap_or_default();
let end_di = day1_vec
.collect_one(Height::from(current_height))
.unwrap_or_default();
let total_days = end_di.to_usize().saturating_sub(start_di.to_usize()) + 1;
let step = (total_days / 200).max(1);
let mut entries = Vec::with_capacity(total_days / step + 1);
let mut di = start_di.to_usize();
while di <= end_di.to_usize() {
let day = brk_types::Day1::from(di);
let next_day = brk_types::Day1::from(di + 1);
if let Some(first_h) = first_height_vec.collect_one(day) {
let next_h = first_height_vec
.collect_one(next_day)
.unwrap_or(Height::from(current_height + 1));
let block_count = next_h.to_usize() - first_h.to_usize();
if block_count > 0 {
let sum =
coinbase_vec
.fold_range(first_h, next_h, Sats::ZERO, |acc, v| acc + v);
let avg = *sum / block_count as u64;
if let Some(ts) = timestamp_vec.collect_one(first_h) {
entries.push(BlockRewardsEntry {
avg_height: first_h.to_usize() as u32,
timestamp: *ts,
avg_rewards: avg,
});
}
}
}
di += step;
}
Ok(entries)
}
}
let bw = BlockWindow::new(self, time_period);
let cumulative = &self.computer().mining.rewards.coinbase.cumulative.sats.height;
Ok(bw
.cumulative_averages(self, cumulative)
.into_iter()
.map(|w| BlockRewardsEntry {
avg_height: w.avg_height,
timestamp: w.timestamp,
avg_rewards: w.avg_value,
usd: w.usd,
})
.collect())
}
}
+30 -38
View File
@@ -1,22 +1,18 @@
use brk_error::Result;
use brk_types::{BlockSizeEntry, BlockSizesWeights, BlockWeightEntry, TimePeriod};
use vecdb::{ReadableOptionVec, VecIndex};
use brk_types::{BlockSizeEntry, BlockSizesWeights, BlockWeightEntry, TimePeriod, Weight};
use vecdb::ReadableVec;
use super::day1_iter::Day1Iter;
use super::block_window::BlockWindow;
use crate::Query;
impl Query {
pub fn block_sizes_weights(&self, time_period: TimePeriod) -> Result<BlockSizesWeights> {
let computer = self.computer();
let current_height = self.height();
let start = current_height
.to_usize()
.saturating_sub(time_period.block_count());
let bw = BlockWindow::new(self, time_period);
let timestamps = bw.timestamps(self);
let iter = Day1Iter::new(computer, start, current_height.to_usize());
// Rolling 24h median, sampled at day1 boundaries
let sizes_vec = &computer
// Batch read per-block rolling 24h medians for the range
let all_sizes = computer
.blocks
.size
.size
@@ -24,8 +20,9 @@ impl Query {
.distribution
.median
._24h
.day1;
let weights_vec = &computer
.height
.collect_range_at(bw.start, bw.end);
let all_weights = computer
.blocks
.weight
.weight
@@ -33,35 +30,30 @@ impl Query {
.distribution
.median
._24h
.day1;
.height
.collect_range_at(bw.start, bw.end);
let entries: Vec<_> = iter.collect(|di, ts, h| {
let size: Option<u64> = sizes_vec.collect_one_flat(di).map(|s| *s);
let weight: Option<u64> = weights_vec.collect_one_flat(di).map(|w| *w);
Some((u32::from(h), (*ts), size, weight))
});
// Sample at window midpoints
let mut sizes = Vec::with_capacity(timestamps.len());
let mut weights = Vec::with_capacity(timestamps.len());
let sizes = entries
.iter()
.filter_map(|(h, ts, size, _)| {
size.map(|s| BlockSizeEntry {
avg_height: *h,
for ((avg_height, start, _end), ts) in bw.iter().zip(&timestamps) {
let mid = start - bw.start + (bw.window / 2).min(all_sizes.len().saturating_sub(1));
if let Some(&size) = all_sizes.get(mid) {
sizes.push(BlockSizeEntry {
avg_height,
timestamp: *ts,
avg_size: s,
})
})
.collect();
let weights = entries
.iter()
.filter_map(|(h, ts, _, weight)| {
weight.map(|w| BlockWeightEntry {
avg_height: *h,
avg_size: *size,
});
}
if let Some(&weight) = all_weights.get(mid) {
weights.push(BlockWeightEntry {
avg_height,
timestamp: *ts,
avg_weight: w,
})
})
.collect();
avg_weight: Weight::from(*weight),
});
}
}
Ok(BlockSizesWeights { sizes, weights })
}
@@ -0,0 +1,154 @@
use brk_types::{Cents, Dollars, Height, Sats, Timestamp, TimePeriod};
use vecdb::{ReadableVec, VecIndex};
use crate::Query;
/// Number of blocks per aggregation window, matching mempool.space's granularity.
fn block_window(period: TimePeriod) -> usize {
match period {
TimePeriod::Day | TimePeriod::ThreeDays | TimePeriod::Week => 1,
TimePeriod::Month => 3,
TimePeriod::ThreeMonths => 12,
TimePeriod::SixMonths => 18,
TimePeriod::Year | TimePeriod::TwoYears => 48,
TimePeriod::ThreeYears => 72,
}
}
/// Per-window average with metadata.
pub struct WindowAvg {
pub avg_height: Height,
pub timestamp: Timestamp,
pub avg_value: Sats,
pub usd: Dollars,
}
/// Block range and window size for a time period.
pub struct BlockWindow {
pub start: usize,
pub end: usize,
pub window: usize,
}
impl BlockWindow {
pub fn new(query: &Query, time_period: TimePeriod) -> Self {
let current_height = query.height();
let computer = query.computer();
let lookback = &computer.blocks.lookback;
// Use pre-computed timestamp-based lookback for accurate time boundaries.
// 24h, 1w, 1m, 1y use in-memory CachedVec; others fall back to PcoVec.
let cached = &lookback.cached_window_starts.0;
let start_height = match time_period {
TimePeriod::Day => cached._24h.collect_one(current_height),
TimePeriod::ThreeDays => lookback._3d.collect_one(current_height),
TimePeriod::Week => cached._1w.collect_one(current_height),
TimePeriod::Month => cached._1m.collect_one(current_height),
TimePeriod::ThreeMonths => lookback._3m.collect_one(current_height),
TimePeriod::SixMonths => lookback._6m.collect_one(current_height),
TimePeriod::Year => cached._1y.collect_one(current_height),
TimePeriod::TwoYears => lookback._2y.collect_one(current_height),
TimePeriod::ThreeYears => lookback._3y.collect_one(current_height),
}
.unwrap_or_default();
Self {
start: start_height.to_usize(),
end: current_height.to_usize() + 1,
window: block_window(time_period),
}
}
/// Compute per-window averages from a cumulative sats vec.
/// Batch-reads timestamps, prices, and the cumulative in one pass.
pub fn cumulative_averages(
&self,
query: &Query,
cumulative: &impl ReadableVec<Height, Sats>,
) -> Vec<WindowAvg> {
let indexer = query.indexer();
let computer = query.computer();
// Batch read all needed data for the range
let all_ts = indexer
.vecs
.blocks
.timestamp
.collect_range_at(self.start, self.end);
let all_prices: Vec<Cents> = computer
.prices
.spot
.cents
.height
.collect_range_at(self.start, self.end);
let read_start = self.start.saturating_sub(1).max(0);
let all_cum = cumulative.collect_range_at(read_start, self.end);
let offset = if self.start > 0 { 1 } else { 0 };
let mut results = Vec::with_capacity(self.count());
let mut pos = 0;
let total = all_ts.len();
while pos < total {
let window_end = (pos + self.window).min(total);
let block_count = (window_end - pos) as u64;
if block_count > 0 {
let mid = (pos + window_end) / 2;
let cum_end = all_cum[window_end - 1 + offset];
let cum_start = if pos + offset > 0 {
all_cum[pos + offset - 1]
} else {
Sats::ZERO
};
let total_sats = cum_end - cum_start;
results.push(WindowAvg {
avg_height: Height::from(self.start + mid),
timestamp: all_ts[mid],
avg_value: Sats::from(*total_sats / block_count),
usd: Dollars::from(all_prices[mid]),
});
}
pos = window_end;
}
results
}
/// Batch-read timestamps for the midpoint of each window.
pub fn timestamps(&self, query: &Query) -> Vec<Timestamp> {
let all_ts = query
.indexer()
.vecs
.blocks
.timestamp
.collect_range_at(self.start, self.end);
let mut timestamps = Vec::with_capacity(self.count());
let mut pos = 0;
while pos < all_ts.len() {
let window_end = (pos + self.window).min(all_ts.len());
timestamps.push(all_ts[(pos + window_end) / 2]);
pos = window_end;
}
timestamps
}
/// Number of windows in this range.
fn count(&self) -> usize {
(self.end - self.start + self.window - 1) / self.window
}
/// Iterate windows, yielding (avg_height, window_start, window_end) for each.
pub fn iter(&self) -> impl Iterator<Item = (Height, usize, usize)> + '_ {
let mut pos = self.start;
std::iter::from_fn(move || {
if pos >= self.end {
return None;
}
let window_end = (pos + self.window).min(self.end);
let avg_height = Height::from((pos + window_end) / 2);
let start = pos;
pos = window_end;
Some((avg_height, start, window_end))
})
}
}
@@ -1,67 +0,0 @@
use brk_computer::Computer;
use brk_types::{Day1, Height, Timestamp};
use vecdb::{ReadableVec, Ro, VecIndex};
/// Helper for iterating over day1 ranges with sampling.
pub struct Day1Iter<'a> {
computer: &'a Computer<Ro>,
start_di: Day1,
end_di: Day1,
step: usize,
}
impl<'a> Day1Iter<'a> {
pub fn new(computer: &'a Computer<Ro>, start_height: usize, end_height: usize) -> Self {
let start_di = computer
.indexes
.height
.day1
.collect_one(Height::from(start_height))
.unwrap_or_default();
let end_di = computer
.indexes
.height
.day1
.collect_one(Height::from(end_height))
.unwrap_or_default();
let total = end_di.to_usize().saturating_sub(start_di.to_usize()) + 1;
let step = (total / 200).max(1);
Self {
computer,
start_di,
end_di,
step,
}
}
/// Iterate and collect entries using the provided transform function.
pub fn collect<T, F>(&self, mut transform: F) -> Vec<T>
where
F: FnMut(Day1, Timestamp, Height) -> Option<T>,
{
let total = self
.end_di
.to_usize()
.saturating_sub(self.start_di.to_usize())
+ 1;
let timestamps = &self.computer.indexes.timestamp.day1;
let heights = &self.computer.indexes.day1.first_height;
let mut entries = Vec::with_capacity(total / self.step + 1);
let mut i = self.start_di.to_usize();
while i <= self.end_di.to_usize() {
let di = Day1::from(i);
if let (Some(ts), Some(h)) = (timestamps.collect_one(di), heights.collect_one(di))
&& let Some(entry) = transform(di, ts, h)
{
entries.push(entry);
}
i += self.step;
}
entries
}
}
+15 -8
View File
@@ -85,7 +85,7 @@ impl Query {
let time_offset = expected_time as i64 - elapsed_time as i64;
// Calculate previous retarget using stored difficulty values
let previous_retarget = if current_epoch_usize > 0 {
let (previous_retarget, previous_time) = if current_epoch_usize > 0 {
let prev_epoch = Epoch::from(current_epoch_usize - 1);
let prev_epoch_start = computer
.indexes
@@ -107,26 +107,33 @@ impl Query {
.collect_one(epoch_start_height)
.unwrap();
if *prev_difficulty > 0.0 {
let retarget = if *prev_difficulty > 0.0 {
((*curr_difficulty / *prev_difficulty) - 1.0) * 100.0
} else {
0.0
}
};
(retarget, epoch_start_timestamp)
} else {
0.0
(0.0, epoch_start_timestamp)
};
// Expected blocks based on wall clock time since epoch start
let expected_blocks = elapsed_time as f64 / TARGET_BLOCK_TIME as f64;
Ok(DifficultyAdjustment {
progress_percent,
difficulty_change,
estimated_retarget_date,
estimated_retarget_date: estimated_retarget_date * 1000,
remaining_blocks,
remaining_time,
remaining_time: remaining_time * 1000,
previous_retarget,
previous_time,
next_retarget_height: Height::from(next_retarget_height),
time_avg,
adjusted_time_avg: time_avg,
time_avg: time_avg * 1000,
adjusted_time_avg: time_avg * 1000,
time_offset,
expected_blocks,
})
}
}
+1 -1
View File
@@ -42,7 +42,7 @@ pub fn iter_difficulty_epochs(
let epoch_difficulty = *epoch_to_difficulty.collect_one(epoch).unwrap_or_default();
let change_percent = match prev_difficulty {
Some(prev) if prev > 0.0 => ((epoch_difficulty / prev) - 1.0) * 100.0,
Some(prev) if prev > 0.0 => epoch_difficulty / prev,
_ => 0.0,
};
+3 -2
View File
@@ -79,9 +79,10 @@ impl Query {
let difficulty: Vec<DifficultyEntry> = iter_difficulty_epochs(computer, start, end)
.into_iter()
.map(|e| DifficultyEntry {
timestamp: e.timestamp,
difficulty: e.difficulty,
time: e.timestamp,
height: e.height,
difficulty: e.difficulty,
adjustment: e.change_percent,
})
.collect();
+1 -1
View File
@@ -2,7 +2,7 @@ mod block_fee_rates;
mod block_fees;
mod block_rewards;
mod block_sizes;
mod day1_iter;
mod block_window;
mod difficulty;
mod difficulty_adjustments;
mod epochs;
+203 -74
View File
@@ -1,17 +1,30 @@
use brk_error::{Error, Result};
use brk_types::{
BlockInfoV1, Height, PoolBlockCounts, PoolBlockShares, PoolDetail, PoolDetailInfo,
PoolHashrateEntry, PoolInfo, PoolSlug, PoolStats, PoolsSummary, TimePeriod, pools,
BlockInfoV1, Day1, Height, Pool, PoolBlockCounts, PoolBlockShares, PoolDetail,
PoolDetailInfo, PoolHashrateEntry, PoolInfo, PoolSlug, PoolStats, PoolsSummary, StoredF64,
StoredU64, TimePeriod, pools,
};
use vecdb::{AnyVec, ReadableVec, VecIndex};
use crate::Query;
/// 7-day lookback for share computation (matching mempool.space)
const LOOKBACK_DAYS: usize = 7;
/// Weekly sample interval (matching mempool.space's 604800s interval)
const SAMPLE_WEEKLY: usize = 7;
/// Pre-read shared data for hashrate computation.
struct HashrateSharedData {
start_day: usize,
end_day: usize,
daily_hashrate: Vec<Option<StoredF64>>,
first_heights: Vec<Height>,
}
impl Query {
pub fn mining_pools(&self, time_period: TimePeriod) -> Result<PoolsSummary> {
let computer = self.computer();
let current_height = self.height();
let end = current_height.to_usize();
// No blocks indexed yet
if computer.pools.pool.len() == 0 {
@@ -19,14 +32,29 @@ impl Query {
pools: vec![],
block_count: 0,
last_estimated_hashrate: 0,
last_estimated_hashrate3d: 0,
last_estimated_hashrate1w: 0,
});
}
// Calculate start height based on time period
let start = end.saturating_sub(time_period.block_count());
// Use timestamp-based lookback for accurate time boundaries
let lookback = &computer.blocks.lookback;
let start = match time_period {
TimePeriod::Day => lookback.cached_window_starts.0._24h.collect_one(current_height),
TimePeriod::ThreeDays => lookback._3d.collect_one(current_height),
TimePeriod::Week => lookback.cached_window_starts.0._1w.collect_one(current_height),
TimePeriod::Month => lookback.cached_window_starts.0._1m.collect_one(current_height),
TimePeriod::ThreeMonths => lookback._3m.collect_one(current_height),
TimePeriod::SixMonths => lookback._6m.collect_one(current_height),
TimePeriod::Year => lookback.cached_window_starts.0._1y.collect_one(current_height),
TimePeriod::TwoYears => lookback._2y.collect_one(current_height),
TimePeriod::ThreeYears => lookback._3y.collect_one(current_height),
}
.unwrap_or_default()
.to_usize();
let pools = pools();
let mut pool_data: Vec<(&'static brk_types::Pool, u64)> = Vec::new();
let mut pool_data: Vec<(&'static Pool, u64)> = Vec::new();
// For each pool, get cumulative count at end and start, subtract to get range count
for (pool_id, cumulative) in computer
@@ -78,13 +106,33 @@ impl Query {
})
.collect();
// TODO: Calculate actual hashrate from difficulty
let last_estimated_hashrate = 0u128;
let hashrate_at = |height: Height| -> u128 {
let day = computer.indexes.height.day1.collect_one(height).unwrap_or_default();
computer
.mining
.hashrate
.rate
.base
.day1
.collect_one(day)
.flatten()
.map(|v| *v as u128)
.unwrap_or(0)
};
let lookback = &computer.blocks.lookback;
let last_estimated_hashrate = hashrate_at(current_height);
let last_estimated_hashrate3d =
hashrate_at(lookback._3d.collect_one(current_height).unwrap_or_default());
let last_estimated_hashrate1w =
hashrate_at(lookback._1w.collect_one(current_height).unwrap_or_default());
Ok(PoolsSummary {
pools: pool_stats,
block_count: total_blocks,
last_estimated_hashrate,
last_estimated_hashrate3d,
last_estimated_hashrate1w,
})
}
@@ -118,8 +166,15 @@ impl Query {
// Get total blocks (all time)
let total_all: u64 = *cumulative.collect_one(current_height).unwrap_or_default();
// Get blocks for 24h (144 blocks)
let start_24h = end.saturating_sub(144);
// Use timestamp-based lookback for accurate time boundaries
let lookback = &computer.blocks.lookback;
let start_24h = lookback
.cached_window_starts
.0
._24h
.collect_one(current_height)
.unwrap_or_default()
.to_usize();
let count_before_24h: u64 = if start_24h == 0 {
0
} else {
@@ -129,8 +184,13 @@ impl Query {
};
let total_24h = total_all.saturating_sub(count_before_24h);
// Get blocks for 1w (1008 blocks)
let start_1w = end.saturating_sub(1008);
let start_1w = lookback
.cached_window_starts
.0
._1w
.collect_one(current_height)
.unwrap_or_default()
.to_usize();
let count_before_1w: u64 = if start_1w == 0 {
0
} else {
@@ -191,11 +251,12 @@ impl Query {
let reader = computer.pools.pool.reader();
let end = start.min(reader.len().saturating_sub(1));
let mut heights = Vec::with_capacity(10);
const POOL_BLOCKS_LIMIT: usize = 100;
let mut heights = Vec::with_capacity(POOL_BLOCKS_LIMIT);
for h in (0..=end).rev() {
if reader.get(h) == slug {
heights.push(h);
if heights.len() >= 10 {
if heights.len() >= POOL_BLOCKS_LIMIT {
break;
}
}
@@ -211,98 +272,166 @@ impl Query {
}
pub fn pool_hashrate(&self, slug: PoolSlug) -> Result<Vec<PoolHashrateEntry>> {
let pools_list = pools();
let pool = pools_list.get(slug);
let entries = self.compute_pool_hashrate_entries(slug, 0)?;
Ok(entries
.into_iter()
.map(|(ts, hr, share)| PoolHashrateEntry {
timestamp: ts,
avg_hashrate: hr,
share,
pool_name: pool.name.to_string(),
})
.collect())
let pool_name = pools().get(slug).name.to_string();
let shared = self.hashrate_shared_data(0)?;
let pool_cum = self.pool_daily_cumulative(slug, shared.start_day, shared.end_day)?;
Ok(Self::compute_hashrate_entries(
&shared, &pool_cum, &pool_name, SAMPLE_WEEKLY,
))
}
pub fn pools_hashrate(
&self,
time_period: Option<TimePeriod>,
) -> Result<Vec<PoolHashrateEntry>> {
let current_height = self.height().to_usize();
let start = match time_period {
Some(tp) => current_height.saturating_sub(tp.block_count()),
let start_height = match time_period {
Some(tp) => {
let lookback = &self.computer().blocks.lookback;
let current_height = self.height();
match tp {
TimePeriod::Day => lookback.cached_window_starts.0._24h.collect_one(current_height),
TimePeriod::ThreeDays => lookback._3d.collect_one(current_height),
TimePeriod::Week => lookback.cached_window_starts.0._1w.collect_one(current_height),
TimePeriod::Month => lookback.cached_window_starts.0._1m.collect_one(current_height),
TimePeriod::ThreeMonths => lookback._3m.collect_one(current_height),
TimePeriod::SixMonths => lookback._6m.collect_one(current_height),
TimePeriod::Year => lookback.cached_window_starts.0._1y.collect_one(current_height),
TimePeriod::TwoYears => lookback._2y.collect_one(current_height),
TimePeriod::ThreeYears => lookback._3y.collect_one(current_height),
}
.unwrap_or_default()
.to_usize()
}
None => 0,
};
let shared = self.hashrate_shared_data(start_height)?;
let pools_list = pools();
let mut entries = Vec::new();
for pool in pools_list.iter() {
if let Ok(pool_entries) = self.compute_pool_hashrate_entries(pool.slug, start) {
for (ts, hr, share) in pool_entries {
if share > 0.0 {
entries.push(PoolHashrateEntry {
timestamp: ts,
avg_hashrate: hr,
share,
pool_name: pool.name.to_string(),
});
}
}
}
let Ok(pool_cum) =
self.pool_daily_cumulative(pool.slug, shared.start_day, shared.end_day)
else {
continue;
};
entries.extend(Self::compute_hashrate_entries(
&shared,
&pool_cum,
&pool.name,
SAMPLE_WEEKLY,
));
}
Ok(entries)
}
/// Compute (timestamp, hashrate, share) tuples for a pool from `start_height`.
fn compute_pool_hashrate_entries(
/// Shared data needed for hashrate computation (read once, reuse across pools).
fn hashrate_shared_data(&self, start_height: usize) -> Result<HashrateSharedData> {
let computer = self.computer();
let current_height = self.height();
let start_day = computer
.indexes
.height
.day1
.collect_one_at(start_height)
.unwrap_or_default()
.to_usize();
let end_day = computer
.indexes
.height
.day1
.collect_one(current_height)
.unwrap_or_default()
.to_usize()
+ 1;
let daily_hashrate = computer
.mining
.hashrate
.rate
.base
.day1
.collect_range_at(start_day, end_day);
let first_heights = computer
.indexes
.day1
.first_height
.collect_range_at(start_day, end_day);
Ok(HashrateSharedData {
start_day,
end_day,
daily_hashrate,
first_heights,
})
}
/// Read daily cumulative blocks mined for a pool.
fn pool_daily_cumulative(
&self,
slug: PoolSlug,
start_height: usize,
) -> Result<Vec<(brk_types::Timestamp, u128, f64)>> {
start_day: usize,
end_day: usize,
) -> Result<Vec<Option<StoredU64>>> {
let computer = self.computer();
let indexer = self.indexer();
let end = self.height().to_usize() + 1;
let start = start_height;
let dominance_bps = computer
computer
.pools
.major
.get(&slug)
.map(|v| &v.base.dominance.bps.height)
.map(|v| v.base.blocks_mined.cumulative.day1.collect_range_at(start_day, end_day))
.or_else(|| {
computer
.pools
.minor
.get(&slug)
.map(|v| &v.dominance.bps.height)
.map(|v| v.blocks_mined.cumulative.day1.collect_range_at(start_day, end_day))
})
.ok_or_else(|| Error::NotFound("Pool not found".into()))?;
.ok_or_else(|| Error::NotFound("Pool not found".into()))
}
let total = end - start;
let step = (total / 200).max(1);
/// Compute hashrate entries from daily cumulative blocks + shared data.
/// Uses 7-day windowed share: pool_blocks_in_week / total_blocks_in_week.
fn compute_hashrate_entries(
shared: &HashrateSharedData,
pool_cum: &[Option<StoredU64>],
pool_name: &str,
sample_days: usize,
) -> Vec<PoolHashrateEntry> {
let total = pool_cum.len();
if total <= LOOKBACK_DAYS {
return vec![];
}
// Batch read everything for the range
let timestamps = indexer.vecs.blocks.timestamp.collect_range_at(start, end);
let bps_values = dominance_bps.collect_range_at(start, end);
let day1_values = computer.indexes.height.day1.collect_range_at(start, end);
let hashrate_vec = &computer.mining.hashrate.rate.base.day1;
let mut entries = Vec::new();
let mut i = LOOKBACK_DAYS;
while i < total {
if let (Some(cum_now), Some(cum_prev)) =
(pool_cum[i], pool_cum[i - LOOKBACK_DAYS])
{
let pool_blocks = (*cum_now).saturating_sub(*cum_prev);
if pool_blocks > 0 {
let h_now = shared.first_heights[i].to_usize();
let h_prev = shared.first_heights[i - LOOKBACK_DAYS].to_usize();
let total_blocks = h_now.saturating_sub(h_prev);
// Pre-read all needed hashrates by collecting unique day1 values
let max_day = day1_values.iter().map(|d| d.to_usize()).max().unwrap_or(0);
let min_day = day1_values.iter().map(|d| d.to_usize()).min().unwrap_or(0);
let hashrates = hashrate_vec.collect_range_dyn(min_day, max_day + 1);
if total_blocks > 0 {
if let Some(hr) = shared.daily_hashrate[i].as_ref() {
let network_hr = f64::from(**hr);
let share = pool_blocks as f64 / total_blocks as f64;
let day = Day1::from(shared.start_day + i);
entries.push(PoolHashrateEntry {
timestamp: day.to_timestamp(),
avg_hashrate: (network_hr * share) as u128,
share,
pool_name: pool_name.to_string(),
});
}
}
}
}
i += sample_days;
}
Ok((0..total)
.step_by(step)
.filter_map(|i| {
let bps = *bps_values[i];
let share = bps as f64 / 10000.0;
let day_idx = day1_values[i].to_usize() - min_day;
let network_hr = f64::from(*hashrates.get(day_idx)?.as_ref()?);
Some((timestamps[i], (network_hr * share) as u128, share))
})
.collect())
entries
}
}