mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-05-20 06:44:47 -07:00
global: fixes
This commit is contained in:
@@ -250,7 +250,7 @@ impl Query {
|
||||
(first_seen, txid)
|
||||
})
|
||||
.collect();
|
||||
ordered.sort_unstable_by(|a, b| b.0.cmp(&a.0));
|
||||
ordered.sort_unstable_by_key(|b| std::cmp::Reverse(b.0));
|
||||
let txs = mempool.txs();
|
||||
Ok(ordered
|
||||
.into_iter()
|
||||
|
||||
@@ -11,62 +11,76 @@ use vecdb::{AnyVec, ReadableVec, VecIndex};
|
||||
|
||||
use crate::Query;
|
||||
|
||||
const DEFAULT_BLOCK_COUNT: u32 = 10;
|
||||
const DEFAULT_V1_BLOCK_COUNT: u32 = 15;
|
||||
const HEADER_SIZE: usize = 80;
|
||||
|
||||
impl Query {
|
||||
/// Block by hash. Unknown hash → 404 via `height_by_hash`.
|
||||
pub fn block(&self, hash: &BlockHash) -> Result<BlockInfo> {
|
||||
let height = self.height_by_hash(hash)?;
|
||||
self.block_by_height(height)
|
||||
}
|
||||
|
||||
/// Block by height. Height > tip → `OutOfRange`.
|
||||
pub fn block_by_height(&self, height: Height) -> Result<BlockInfo> {
|
||||
let max_height = self.indexed_height();
|
||||
if height > max_height {
|
||||
if height > self.tip_height() {
|
||||
return Err(Error::OutOfRange("Block height out of range".into()));
|
||||
}
|
||||
self.blocks_range(height.to_usize(), height.to_usize() + 1)?
|
||||
let h = height.to_usize();
|
||||
self.blocks_range(h, h + 1)?
|
||||
.pop()
|
||||
.ok_or(Error::NotFound("Block not found".into()))
|
||||
}
|
||||
|
||||
/// V1 block by height. Ceiling is `min(indexed, computed)` because
|
||||
/// `blocks_v1_range` reads computer-stamped series (pools, fees,
|
||||
/// supply state). Anything past `computed_height` would short-read.
|
||||
pub fn block_by_height_v1(&self, height: Height) -> Result<BlockInfoV1> {
|
||||
let max_height = self.height();
|
||||
if height > max_height {
|
||||
if height > self.height() {
|
||||
return Err(Error::OutOfRange("Block height out of range".into()));
|
||||
}
|
||||
self.blocks_v1_range(height.to_usize(), height.to_usize() + 1)?
|
||||
let h = height.to_usize();
|
||||
self.blocks_v1_range(h, h + 1)?
|
||||
.pop()
|
||||
.ok_or(Error::NotFound("Block not found".into()))
|
||||
}
|
||||
|
||||
/// Hex-encoded 80-byte block header. Decode-then-encode roundtrip
|
||||
/// doubles as a corruption check on the on-disk bytes.
|
||||
pub fn block_header_hex(&self, hash: &BlockHash) -> Result<String> {
|
||||
let height = self.height_by_hash(hash)?;
|
||||
let header = self.read_block_header(height)?;
|
||||
Ok(bitcoin::consensus::encode::serialize_hex(&header))
|
||||
}
|
||||
|
||||
/// Block hash by height. Cheap typed-index read with a semantic
|
||||
/// bounds gate (`OutOfRange` for past-tip, `Internal` if the data
|
||||
/// is unexpectedly missing inside the gate).
|
||||
pub fn block_hash_by_height(&self, height: Height) -> Result<BlockHash> {
|
||||
let max_height = self.indexed_height();
|
||||
if height > max_height {
|
||||
if height > self.tip_height() {
|
||||
return Err(Error::OutOfRange("Block height out of range".into()));
|
||||
}
|
||||
self.indexer().vecs.blocks.blockhash.get(height).data()
|
||||
}
|
||||
|
||||
pub fn blocks(&self, start_height: Option<Height>) -> Result<Vec<BlockInfo>> {
|
||||
let (begin, end) = self.resolve_block_range(start_height, DEFAULT_BLOCK_COUNT);
|
||||
/// Most recent `count` blocks ending at `start_height` (default tip),
|
||||
/// returned in descending-height order.
|
||||
pub fn blocks(&self, start_height: Option<Height>, count: u32) -> Result<Vec<BlockInfo>> {
|
||||
let (begin, end) = self.resolve_block_range(start_height, count);
|
||||
self.blocks_range(begin, end)
|
||||
}
|
||||
|
||||
pub fn blocks_v1(&self, start_height: Option<Height>) -> Result<Vec<BlockInfoV1>> {
|
||||
let (begin, end) = self.resolve_block_range(start_height, DEFAULT_V1_BLOCK_COUNT);
|
||||
/// V1 most recent `count` blocks with extras ending at `start_height`
|
||||
/// (default tip), returned in descending-height order.
|
||||
pub fn blocks_v1(&self, start_height: Option<Height>, count: u32) -> Result<Vec<BlockInfoV1>> {
|
||||
let (begin, end) = self.resolve_block_range(start_height, count);
|
||||
self.blocks_v1_range(begin, end)
|
||||
}
|
||||
|
||||
// === Range queries (bulk reads) ===
|
||||
|
||||
/// Build `BlockInfo` rows for `[begin, end)` in descending-height order.
|
||||
/// Caller must bounds-check `end <= tip + 1`. Returns `Internal` if any
|
||||
/// bulk read short-returns under per-vec stamp races.
|
||||
fn blocks_range(&self, begin: usize, end: usize) -> Result<Vec<BlockInfo>> {
|
||||
if begin >= end {
|
||||
return Ok(Vec::new());
|
||||
@@ -75,6 +89,7 @@ impl Query {
|
||||
let indexer = self.indexer();
|
||||
let computer = self.computer();
|
||||
let reader = self.reader();
|
||||
let count = end - begin;
|
||||
|
||||
// Bulk read all indexed data
|
||||
let blockhashes = indexer.vecs.blocks.blockhash.collect_range_at(begin, end);
|
||||
@@ -83,6 +98,15 @@ impl Query {
|
||||
let sizes = indexer.vecs.blocks.total.collect_range_at(begin, end);
|
||||
let weights = indexer.vecs.blocks.weight.collect_range_at(begin, end);
|
||||
let positions = indexer.vecs.blocks.position.collect_range_at(begin, end);
|
||||
if blockhashes.len() != count
|
||||
|| difficulties.len() != count
|
||||
|| timestamps.len() != count
|
||||
|| sizes.len() != count
|
||||
|| weights.len() != count
|
||||
|| positions.len() != count
|
||||
{
|
||||
return Err(Error::Internal("blocks_range: short read on per-block vecs"));
|
||||
}
|
||||
|
||||
// Bulk read tx indexes for tx_count
|
||||
let max_height = self.indexed_height();
|
||||
@@ -96,6 +120,9 @@ impl Query {
|
||||
.transactions
|
||||
.first_tx_index
|
||||
.collect_range_at(begin, tx_index_end);
|
||||
if first_tx_indexes.len() < count {
|
||||
return Err(Error::Internal("blocks_range: short read on first_tx_index"));
|
||||
}
|
||||
let total_txs = computer.indexes.tx_index.identity.len();
|
||||
|
||||
// Bulk read median time window
|
||||
@@ -105,8 +132,10 @@ impl Query {
|
||||
.blocks
|
||||
.timestamp
|
||||
.collect_range_at(median_start, end);
|
||||
if median_timestamps.len() != end - median_start {
|
||||
return Err(Error::Internal("blocks_range: short read on median window"));
|
||||
}
|
||||
|
||||
let count = end - begin;
|
||||
let mut blocks = Vec::with_capacity(count);
|
||||
|
||||
for i in (0..count).rev() {
|
||||
@@ -431,6 +460,10 @@ impl Query {
|
||||
|
||||
// === Helper methods ===
|
||||
|
||||
pub fn tip_height(&self) -> Height {
|
||||
Height::from(self.indexer().vecs.blocks.blockhash.len().saturating_sub(1))
|
||||
}
|
||||
|
||||
pub fn height_by_hash(&self, hash: &BlockHash) -> Result<Height> {
|
||||
let indexer = self.indexer();
|
||||
let prefix = BlockHashPrefix::from(hash);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use brk_error::{Error, OptionData, Result};
|
||||
use brk_types::{BlockHash, Height};
|
||||
use vecdb::{AnyVec, ReadableVec};
|
||||
use vecdb::ReadableVec;
|
||||
|
||||
use crate::Query;
|
||||
|
||||
@@ -11,19 +11,17 @@ impl Query {
|
||||
}
|
||||
|
||||
fn block_raw_by_height(&self, height: Height) -> Result<Vec<u8>> {
|
||||
let indexer = self.indexer();
|
||||
let reader = self.reader();
|
||||
|
||||
let max_height = Height::from(indexer.vecs.blocks.blockhash.len().saturating_sub(1));
|
||||
let max_height = self.tip_height();
|
||||
if height > max_height {
|
||||
return Err(Error::OutOfRange(format!(
|
||||
"Block height {height} out of range (tip {max_height})"
|
||||
)));
|
||||
}
|
||||
|
||||
let indexer = self.indexer();
|
||||
let position = indexer.vecs.blocks.position.collect_one(height).data()?;
|
||||
let size = indexer.vecs.blocks.total.collect_one(height).data()?;
|
||||
|
||||
reader.read_raw_bytes(position, *size as usize)
|
||||
self.reader().read_raw_bytes(position, *size as usize)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use brk_error::{OptionData, Result};
|
||||
use brk_types::{BlockHash, BlockStatus, Height};
|
||||
use vecdb::AnyVec;
|
||||
|
||||
use crate::Query;
|
||||
|
||||
@@ -11,9 +10,7 @@ impl Query {
|
||||
}
|
||||
|
||||
fn block_status_by_height(&self, height: Height) -> Result<BlockStatus> {
|
||||
let indexer = self.indexer();
|
||||
|
||||
let max_height = Height::from(indexer.vecs.blocks.blockhash.len().saturating_sub(1));
|
||||
let max_height = self.tip_height();
|
||||
|
||||
if height > max_height {
|
||||
return Ok(BlockStatus::not_in_best_chain());
|
||||
@@ -21,7 +18,7 @@ impl Query {
|
||||
|
||||
let next_best = if height < max_height {
|
||||
Some(
|
||||
indexer
|
||||
self.indexer()
|
||||
.vecs
|
||||
.blocks
|
||||
.blockhash
|
||||
|
||||
@@ -1,27 +1,37 @@
|
||||
use brk_error::{Error, OptionData, Result};
|
||||
use brk_types::{BlockTimestamp, Date, Day1, Height, Timestamp};
|
||||
use jiff::Timestamp as JiffTimestamp;
|
||||
use vecdb::ReadableVec;
|
||||
use vecdb::{AnyVec, ReadableVec};
|
||||
|
||||
use crate::Query;
|
||||
|
||||
/// Per BIP113, a block's timestamp must exceed the median of the previous 11
|
||||
/// blocks. Eleven consecutive `ts > target` therefore prove no later block can
|
||||
/// have `ts ≤ target` (its median floor would already exceed `target`).
|
||||
const MTP_TERMINAL_STREAK: usize = 11;
|
||||
|
||||
impl Query {
|
||||
/// Most recent block with `timestamp ≤ ts`. Backs mempool.space's
|
||||
/// `GET /api/v1/mining/blocks/timestamp/{ts}`. Future timestamps return
|
||||
/// the chain tip; pre-genesis timestamps return 404.
|
||||
///
|
||||
/// Uses `day1.first_height` for an O(1) seek to the target date, then a
|
||||
/// linear scan bounded by the BIP113 MTP rule (see `MTP_TERMINAL_STREAK`).
|
||||
/// Symmetric backward scan handles targets earlier than the seeded day's
|
||||
/// first block.
|
||||
pub fn block_by_timestamp(&self, timestamp: Timestamp) -> Result<BlockTimestamp> {
|
||||
let indexer = self.indexer();
|
||||
let computer = self.computer();
|
||||
|
||||
let max_height = self.indexed_height();
|
||||
let max_height_usize: usize = max_height.into();
|
||||
|
||||
if max_height_usize == 0 {
|
||||
if indexer.vecs.blocks.blockhash.len() == 0 {
|
||||
return Err(Error::NotFound("No blocks indexed".into()));
|
||||
}
|
||||
let tip: usize = self.tip_height().into();
|
||||
|
||||
let target = timestamp;
|
||||
let date = Date::from(target);
|
||||
let day1 = Day1::try_from(date).unwrap_or_default();
|
||||
|
||||
// Get first height of the target date
|
||||
let first_height_of_day = computer
|
||||
.indexes
|
||||
.day1
|
||||
@@ -29,37 +39,46 @@ impl Query {
|
||||
.collect_one(day1)
|
||||
.unwrap_or(Height::from(0usize));
|
||||
|
||||
let start: usize = usize::from(first_height_of_day).min(max_height_usize);
|
||||
let start: usize = usize::from(first_height_of_day).min(tip);
|
||||
|
||||
let mut ts_cursor = indexer.vecs.blocks.timestamp.cursor();
|
||||
let mut best: Option<(usize, Timestamp)> = None;
|
||||
|
||||
// Search forward from start to find the last block <= target timestamp
|
||||
let mut best_height = start;
|
||||
let mut best_ts = ts_cursor.get(start).data()?;
|
||||
|
||||
for h in (start + 1)..=max_height_usize {
|
||||
let mut above_streak = 0usize;
|
||||
for h in start..=tip {
|
||||
let block_ts = ts_cursor.get(h).data()?;
|
||||
if block_ts <= target {
|
||||
best_height = h;
|
||||
best_ts = block_ts;
|
||||
best = Some((h, block_ts));
|
||||
above_streak = 0;
|
||||
} else {
|
||||
break;
|
||||
above_streak += 1;
|
||||
if above_streak >= MTP_TERMINAL_STREAK {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check one block before start in case we need to go backward
|
||||
if start > 0 && best_ts > target {
|
||||
let prev_ts = ts_cursor.get(start - 1).data()?;
|
||||
if prev_ts <= target {
|
||||
best_height = start - 1;
|
||||
best_ts = prev_ts;
|
||||
if best.is_none() && start > 0 {
|
||||
let mut above_streak = 0usize;
|
||||
for h in (0..start).rev() {
|
||||
let block_ts = ts_cursor.get(h).data()?;
|
||||
if block_ts <= target {
|
||||
best = Some((h, block_ts));
|
||||
break;
|
||||
}
|
||||
above_streak += 1;
|
||||
if above_streak >= MTP_TERMINAL_STREAK {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (best_height, best_ts) =
|
||||
best.ok_or_else(|| Error::NotFound("No block at or before timestamp".into()))?;
|
||||
|
||||
let height = Height::from(best_height);
|
||||
let blockhash = indexer.vecs.blocks.blockhash.collect_one(height).data()?;
|
||||
|
||||
// Convert timestamp to ISO 8601 format
|
||||
let ts_secs: i64 = (*best_ts).into();
|
||||
let iso_timestamp = JiffTimestamp::from_second(ts_secs)
|
||||
.map(|t| t.strftime("%Y-%m-%dT%H:%M:%S%.3fZ").to_string())
|
||||
|
||||
@@ -250,8 +250,10 @@ impl Query {
|
||||
best_descendant,
|
||||
descendants,
|
||||
effective_fee_per_vsize: Some(effective),
|
||||
sigops: None,
|
||||
fee: Some(seed_fee),
|
||||
adjusted_vsize: Some(seed_vsize),
|
||||
cluster: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use brk_error::Result;
|
||||
use brk_types::{BlockFeeRatesEntry, FeeRate, FeeRatePercentiles, TimePeriod};
|
||||
use vecdb::ReadableVec;
|
||||
use brk_types::{BlockFeeRatesEntry, FeeRatePercentiles, TimePeriod};
|
||||
|
||||
use super::block_window::BlockWindow;
|
||||
use crate::Query;
|
||||
@@ -8,55 +7,38 @@ use crate::Query;
|
||||
impl Query {
|
||||
pub fn block_fee_rates(&self, time_period: TimePeriod) -> Result<Vec<BlockFeeRatesEntry>> {
|
||||
let bw = BlockWindow::new(self, time_period);
|
||||
let computer = self.computer();
|
||||
let frd = &computer
|
||||
let frd = &self
|
||||
.computer()
|
||||
.transactions
|
||||
.fees
|
||||
.effective_fee_rate
|
||||
.distribution
|
||||
.block;
|
||||
|
||||
let min = frd.min.height.collect_range_at(bw.start, bw.end);
|
||||
let pct10 = frd.pct10.height.collect_range_at(bw.start, bw.end);
|
||||
let pct25 = frd.pct25.height.collect_range_at(bw.start, bw.end);
|
||||
let median = frd.median.height.collect_range_at(bw.start, bw.end);
|
||||
let pct75 = frd.pct75.height.collect_range_at(bw.start, bw.end);
|
||||
let pct90 = frd.pct90.height.collect_range_at(bw.start, bw.end);
|
||||
let max = frd.max.height.collect_range_at(bw.start, bw.end);
|
||||
let min = bw.read(&frd.min.height);
|
||||
let pct10 = bw.read(&frd.pct10.height);
|
||||
let pct25 = bw.read(&frd.pct25.height);
|
||||
let median = bw.read(&frd.median.height);
|
||||
let pct75 = bw.read(&frd.pct75.height);
|
||||
let pct90 = bw.read(&frd.pct90.height);
|
||||
let max = bw.read(&frd.max.height);
|
||||
|
||||
let timestamps = bw.timestamps(self);
|
||||
|
||||
let mut results = Vec::with_capacity(timestamps.len());
|
||||
let mut pos = 0;
|
||||
let total = min.len();
|
||||
|
||||
for ts in ×tamps {
|
||||
let window_end = (pos + bw.window).min(total);
|
||||
let count = window_end - pos;
|
||||
if count > 0 {
|
||||
let mid = (pos + window_end) / 2;
|
||||
let avg = |vals: &[FeeRate]| -> FeeRate {
|
||||
let sum: f64 = vals[pos..window_end].iter().map(|f| f64::from(*f)).sum();
|
||||
FeeRate::new(sum / count as f64)
|
||||
};
|
||||
|
||||
results.push(BlockFeeRatesEntry {
|
||||
avg_height: brk_types::Height::from(bw.start + mid),
|
||||
timestamp: *ts,
|
||||
percentiles: FeeRatePercentiles::new(
|
||||
avg(&min),
|
||||
avg(&pct10),
|
||||
avg(&pct25),
|
||||
avg(&median),
|
||||
avg(&pct75),
|
||||
avg(&pct90),
|
||||
avg(&max),
|
||||
),
|
||||
});
|
||||
}
|
||||
pos = window_end;
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
Ok(bw
|
||||
.buckets
|
||||
.iter()
|
||||
.map(|b| BlockFeeRatesEntry {
|
||||
avg_height: b.avg_height,
|
||||
timestamp: b.avg_timestamp,
|
||||
percentiles: FeeRatePercentiles::new(
|
||||
b.mean(&min),
|
||||
b.mean(&pct10),
|
||||
b.mean(&pct25),
|
||||
b.mean(&median),
|
||||
b.mean(&pct75),
|
||||
b.mean(&pct90),
|
||||
b.mean(&max),
|
||||
),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use brk_error::Result;
|
||||
use brk_types::{BlockFeesEntry, TimePeriod};
|
||||
use brk_types::{BlockFeesEntry, Cents, Dollars, Sats, TimePeriod};
|
||||
|
||||
use super::block_window::BlockWindow;
|
||||
use crate::Query;
|
||||
@@ -7,15 +7,17 @@ use crate::Query;
|
||||
impl Query {
|
||||
pub fn block_fees(&self, time_period: TimePeriod) -> Result<Vec<BlockFeesEntry>> {
|
||||
let bw = BlockWindow::new(self, time_period);
|
||||
let cumulative = &self.computer().mining.rewards.fees.cumulative.sats.height;
|
||||
let fees: Vec<Sats> = bw.read(&self.computer().mining.rewards.fees.block.sats);
|
||||
let prices: Vec<Cents> = bw.read(&self.computer().prices.spot.cents.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,
|
||||
.buckets
|
||||
.iter()
|
||||
.map(|b| BlockFeesEntry {
|
||||
avg_height: b.avg_height,
|
||||
timestamp: b.avg_timestamp,
|
||||
avg_fees: b.mean_rounded(&fees),
|
||||
usd: Dollars::from(b.mean_rounded(&prices)),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use brk_error::Result;
|
||||
use brk_types::{BlockRewardsEntry, TimePeriod};
|
||||
use brk_types::{BlockRewardsEntry, Cents, Dollars, Sats, TimePeriod};
|
||||
|
||||
use super::block_window::BlockWindow;
|
||||
use crate::Query;
|
||||
@@ -7,22 +7,17 @@ use crate::Query;
|
||||
impl Query {
|
||||
pub fn block_rewards(&self, time_period: TimePeriod) -> Result<Vec<BlockRewardsEntry>> {
|
||||
let bw = BlockWindow::new(self, time_period);
|
||||
let cumulative = &self
|
||||
.computer()
|
||||
.mining
|
||||
.rewards
|
||||
.coinbase
|
||||
.cumulative
|
||||
.sats
|
||||
.height;
|
||||
let rewards: Vec<Sats> = bw.read(&self.computer().mining.rewards.coinbase.block.sats);
|
||||
let prices: Vec<Cents> = bw.read(&self.computer().prices.spot.cents.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,
|
||||
.buckets
|
||||
.iter()
|
||||
.map(|b| BlockRewardsEntry {
|
||||
avg_height: b.avg_height,
|
||||
timestamp: b.avg_timestamp,
|
||||
avg_rewards: b.mean_rounded(&rewards),
|
||||
usd: Dollars::from(b.mean_rounded(&prices)),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
@@ -1,59 +1,37 @@
|
||||
use brk_error::Result;
|
||||
use brk_types::{BlockSizeEntry, BlockSizesWeights, BlockWeightEntry, TimePeriod, Weight};
|
||||
use vecdb::ReadableVec;
|
||||
use brk_types::{
|
||||
BlockSizeEntry, BlockSizesWeights, BlockWeightEntry, StoredU64, TimePeriod, Weight,
|
||||
};
|
||||
|
||||
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 blocks = &self.indexer().vecs.blocks;
|
||||
let bw = BlockWindow::new(self, time_period);
|
||||
let timestamps = bw.timestamps(self);
|
||||
|
||||
// Batch read per-block rolling 24h medians for the range
|
||||
let all_sizes = computer
|
||||
.blocks
|
||||
.size
|
||||
.size
|
||||
.rolling
|
||||
.distribution
|
||||
.median
|
||||
._24h
|
||||
.height
|
||||
.collect_range_at(bw.start, bw.end);
|
||||
let all_weights = computer
|
||||
.blocks
|
||||
.weight
|
||||
.weight
|
||||
.rolling
|
||||
.distribution
|
||||
.median
|
||||
._24h
|
||||
.height
|
||||
.collect_range_at(bw.start, bw.end);
|
||||
let block_sizes: Vec<StoredU64> = bw.read(&blocks.total);
|
||||
let block_weights: Vec<Weight> = bw.read(&blocks.weight);
|
||||
|
||||
// Sample at window midpoints
|
||||
let mut sizes = Vec::with_capacity(timestamps.len());
|
||||
let mut weights = Vec::with_capacity(timestamps.len());
|
||||
|
||||
for ((avg_height, start, _end), ts) in bw.iter().zip(×tamps) {
|
||||
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: *size,
|
||||
});
|
||||
}
|
||||
if let Some(&weight) = all_weights.get(mid) {
|
||||
weights.push(BlockWeightEntry {
|
||||
avg_height,
|
||||
timestamp: *ts,
|
||||
avg_weight: Weight::from(*weight),
|
||||
});
|
||||
}
|
||||
}
|
||||
let (sizes, weights) = bw
|
||||
.buckets
|
||||
.iter()
|
||||
.map(|b| {
|
||||
(
|
||||
BlockSizeEntry {
|
||||
avg_height: b.avg_height,
|
||||
timestamp: b.avg_timestamp,
|
||||
avg_size: u64::from(b.mean_rounded(&block_sizes)),
|
||||
},
|
||||
BlockWeightEntry {
|
||||
avg_height: b.avg_height,
|
||||
timestamp: b.avg_timestamp,
|
||||
avg_weight: b.mean_rounded(&block_weights),
|
||||
},
|
||||
)
|
||||
})
|
||||
.unzip();
|
||||
|
||||
Ok(BlockSizesWeights { sizes, weights })
|
||||
}
|
||||
|
||||
@@ -1,155 +1,117 @@
|
||||
use brk_types::{Cents, Dollars, Height, Sats, TimePeriod, Timestamp};
|
||||
use vecdb::{ReadableVec, VecIndex};
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
iter::Sum,
|
||||
ops::{Deref, Div},
|
||||
};
|
||||
|
||||
use brk_types::{Height, TimePeriod, Timestamp};
|
||||
use vecdb::{ReadableVec, VecValue};
|
||||
|
||||
use crate::Query;
|
||||
|
||||
/// Number of blocks per aggregation window, matching mempool.space's granularity.
|
||||
fn block_window(period: TimePeriod) -> usize {
|
||||
/// Mempool.space's `GROUP BY UNIX_TIMESTAMP(blockTimestamp) DIV ${div}` divisor in seconds.
|
||||
/// `div = 1` puts each block in its own bucket.
|
||||
fn time_div(period: TimePeriod) -> u32 {
|
||||
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,
|
||||
TimePeriod::All => 144,
|
||||
TimePeriod::Day | TimePeriod::ThreeDays => 1,
|
||||
TimePeriod::Week => 300,
|
||||
TimePeriod::Month => 1800,
|
||||
TimePeriod::ThreeMonths => 7200,
|
||||
TimePeriod::SixMonths => 10800,
|
||||
TimePeriod::Year | TimePeriod::TwoYears => 28800,
|
||||
TimePeriod::ThreeYears => 43200,
|
||||
TimePeriod::All => 86400,
|
||||
}
|
||||
}
|
||||
|
||||
/// Per-window average with metadata.
|
||||
pub struct WindowAvg {
|
||||
pub avg_height: Height,
|
||||
pub timestamp: Timestamp,
|
||||
pub avg_value: Sats,
|
||||
pub usd: Dollars,
|
||||
/// Round-half-up integer division, matching MySQL's `CAST(AVG(...) AS INT)`.
|
||||
const fn round_half_up(sum: u64, n: u64) -> u64 {
|
||||
(sum + n / 2) / n
|
||||
}
|
||||
|
||||
/// Block range and window size for a time period.
|
||||
/// One time-bucket of blocks in a `BlockWindow`.
|
||||
pub struct BlockBucket {
|
||||
pub avg_height: Height,
|
||||
pub avg_timestamp: Timestamp,
|
||||
/// Offsets into the parent `BlockWindow`'s prefetched `[start, end)` slice.
|
||||
offsets: Vec<usize>,
|
||||
}
|
||||
|
||||
impl BlockBucket {
|
||||
/// Float arithmetic mean of `values[offset]` across this bucket's blocks.
|
||||
/// Use for float-backed types like `FeeRate`.
|
||||
pub fn mean<T>(&self, values: &[T]) -> T
|
||||
where
|
||||
T: Copy + Sum + Div<usize, Output = T>,
|
||||
{
|
||||
self.offsets.iter().map(|&i| values[i]).sum::<T>() / self.offsets.len()
|
||||
}
|
||||
|
||||
/// Round-half-up arithmetic mean for u64-backed integer types, matching
|
||||
/// mempool.space's `CAST(AVG(...) AS INT)`.
|
||||
pub fn mean_rounded<T>(&self, values: &[T]) -> T
|
||||
where
|
||||
T: Copy + Deref<Target = u64> + From<u64>,
|
||||
{
|
||||
let n = self.offsets.len() as u64;
|
||||
let sum: u64 = self.offsets.iter().map(|&i| *values[i]).sum();
|
||||
T::from(round_half_up(sum, n))
|
||||
}
|
||||
}
|
||||
|
||||
/// Mempool-compatible time-bucketed block window. Groups blocks by
|
||||
/// `block.timestamp / div` and exposes arithmetic means per bucket.
|
||||
pub struct BlockWindow {
|
||||
pub start: usize,
|
||||
pub end: usize,
|
||||
pub window: usize,
|
||||
pub start: Height,
|
||||
pub end: Height,
|
||||
pub buckets: Vec<BlockBucket>,
|
||||
}
|
||||
|
||||
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;
|
||||
pub fn new(query: &Query, period: TimePeriod) -> Self {
|
||||
let start = query.start_height(period);
|
||||
let end = query.height() + 1usize;
|
||||
let div = time_div(period);
|
||||
|
||||
// Use pre-computed timestamp-based lookback for accurate time boundaries.
|
||||
// 24h, 1w, 1m, 1y use in-memory CachedVec; others fall back to PcoVec.
|
||||
let start_height = match time_period {
|
||||
TimePeriod::Day => lookback._24h.collect_one(current_height),
|
||||
TimePeriod::ThreeDays => lookback._3d.collect_one(current_height),
|
||||
TimePeriod::Week => lookback._1w.collect_one(current_height),
|
||||
TimePeriod::Month => lookback._1m.collect_one(current_height),
|
||||
TimePeriod::ThreeMonths => lookback._3m.collect_one(current_height),
|
||||
TimePeriod::SixMonths => lookback._6m.collect_one(current_height),
|
||||
TimePeriod::Year => lookback._1y.collect_one(current_height),
|
||||
TimePeriod::TwoYears => lookback._2y.collect_one(current_height),
|
||||
TimePeriod::ThreeYears => lookback._3y.collect_one(current_height),
|
||||
TimePeriod::All => None,
|
||||
}
|
||||
.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);
|
||||
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;
|
||||
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;
|
||||
if let Some(avg) = (*total_sats).checked_div(block_count) {
|
||||
results.push(WindowAvg {
|
||||
avg_height: Height::from(self.start + mid),
|
||||
timestamp: all_ts[mid],
|
||||
avg_value: Sats::from(avg),
|
||||
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
|
||||
let timestamps: Vec<Timestamp> = 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;
|
||||
.collect_range(start, end);
|
||||
|
||||
let mut groups: BTreeMap<u32, Vec<usize>> = BTreeMap::new();
|
||||
for (i, ts) in timestamps.iter().enumerate() {
|
||||
groups.entry(**ts / div).or_default().push(i);
|
||||
}
|
||||
|
||||
let buckets = groups
|
||||
.into_values()
|
||||
.map(|offsets| {
|
||||
let n = offsets.len() as u64;
|
||||
let sum_h: u64 = offsets.iter().map(|&i| u64::from(start + i)).sum();
|
||||
let sum_ts: u64 = offsets.iter().map(|&i| u64::from(timestamps[i])).sum();
|
||||
BlockBucket {
|
||||
avg_height: Height::from(round_half_up(sum_h, n)),
|
||||
avg_timestamp: Timestamp::from(round_half_up(sum_ts, n) as u32),
|
||||
offsets,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
start,
|
||||
end,
|
||||
buckets,
|
||||
}
|
||||
timestamps
|
||||
}
|
||||
|
||||
/// Number of windows in this range.
|
||||
fn count(&self) -> usize {
|
||||
(self.end - self.start).div_ceil(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))
|
||||
})
|
||||
/// Read a height-keyed vec over this window's `[start, end)` range.
|
||||
pub fn read<V, T>(&self, vec: &V) -> Vec<T>
|
||||
where
|
||||
V: ReadableVec<Height, T>,
|
||||
T: VecValue,
|
||||
{
|
||||
vec.collect_range(self.start, self.end)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,10 +10,11 @@ impl Query {
|
||||
&self,
|
||||
time_period: Option<TimePeriod>,
|
||||
) -> Result<Vec<DifficultyAdjustmentEntry>> {
|
||||
let current_height = self.height();
|
||||
let end = current_height.to_usize();
|
||||
let end = self.height().to_usize();
|
||||
// Match mempool.space's wall-clock `time > NOW() - INTERVAL ${period}` cutoff
|
||||
// by walking back through real block timestamps, not estimating via block count.
|
||||
let start = match time_period {
|
||||
Some(tp) => end.saturating_sub(tp.block_count()),
|
||||
Some(tp) => self.start_height(tp).to_usize(),
|
||||
None => 0,
|
||||
};
|
||||
|
||||
|
||||
@@ -7,5 +7,6 @@ mod difficulty;
|
||||
mod difficulty_adjustments;
|
||||
mod epochs;
|
||||
mod hashrate;
|
||||
mod period_start;
|
||||
mod pools;
|
||||
mod reward_stats;
|
||||
|
||||
14
crates/brk_query/src/impl/mining/period_start.rs
Normal file
14
crates/brk_query/src/impl/mining/period_start.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
use brk_types::{Height, TimePeriod};
|
||||
|
||||
use crate::Query;
|
||||
|
||||
impl Query {
|
||||
/// First block height inside `period` looking back from the tip; genesis (0) for `All`.
|
||||
pub(super) fn start_height(&self, period: TimePeriod) -> Height {
|
||||
self.computer()
|
||||
.blocks
|
||||
.lookback
|
||||
.start_height(period, self.height())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user