mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-07-05 00:03:40 -07:00
server: ms endpoint fixes
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user