global: fixes

This commit is contained in:
nym21
2026-05-03 12:44:18 +02:00
parent 9cb5f2c880
commit 4663d13194
46 changed files with 1058 additions and 544 deletions
+48 -15
View File
@@ -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);
+4 -6
View File
@@ -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)
}
}
+2 -5
View File
@@ -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
+41 -22
View File
@@ -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())