global: adding support for safe lengths

This commit is contained in:
nym21
2026-05-06 15:33:07 +02:00
parent da7671744f
commit 086bfd9938
177 changed files with 2445 additions and 2049 deletions

View File

@@ -34,6 +34,10 @@ impl Query {
let hash = AddrHash::from(&bytes);
let type_index = self.type_index_for(output_type, &hash)?;
if type_index >= self.safe_lengths().to_type_index(output_type) {
return Err(Error::UnknownAddr);
}
let any_addr_index = computer
.distribution
.any_addr_indexes
@@ -139,22 +143,26 @@ impl Query {
.get(output_type)
.data()?;
let tx_index_len = self.safe_lengths().tx_index;
if let Some(after_txid) = after_txid {
let after_tx_index = self.resolve_tx_index(&after_txid)?;
let min = AddrIndexTxIndex::min_for_addr(type_index);
let bound = AddrIndexTxIndex::from((type_index, after_tx_index));
let cursor = AddrIndexTxIndex::from((type_index, after_tx_index));
Ok(store
.range(min..bound)
.range(min..cursor)
.rev()
.take(limit)
.map(|(key, _): (AddrIndexTxIndex, Unit)| key.tx_index())
.filter(|tx_index| *tx_index < tx_index_len)
.take(limit)
.collect())
} else {
Ok(store
.prefix(type_index)
.rev()
.take(limit)
.map(|(key, _): (AddrIndexTxIndex, Unit)| key.tx_index())
.filter(|tx_index| *tx_index < tx_index_len)
.take(limit)
.collect())
}
}
@@ -171,9 +179,11 @@ impl Query {
.get(output_type)
.data()?;
let tx_index_len = self.safe_lengths().tx_index;
let outpoints: Vec<(TxIndex, Vout)> = store
.prefix(type_index)
.map(|(key, _): (AddrIndexOutPoint, Unit)| (key.tx_index(), key.vout()))
.filter(|(tx_index, _)| *tx_index < tx_index_len)
.take(max_utxos + 1)
.collect();
if outpoints.len() > max_utxos {
@@ -257,10 +267,12 @@ impl Query {
.addr_type_to_addr_index_and_tx_index
.get(output_type)
.data()?;
let tx_index_len = self.safe_lengths().tx_index;
let last_tx_index = store
.prefix(type_index)
.next_back()
.rev()
.map(|(key, _): (AddrIndexTxIndex, Unit)| key.tx_index())
.find(|tx_index| *tx_index < tx_index_len)
.ok_or(Error::UnknownAddr)?;
self.confirmed_status_height(last_tx_index)
}

View File

@@ -7,7 +7,7 @@ use brk_types::{
BlockExtras, BlockHash, BlockHashPrefix, BlockHeader, BlockInfo, BlockInfoV1, BlockPool,
FeeRate, Height, PoolSlug, Sats, Timestamp, TxIndex, VSize, pools,
};
use vecdb::{AnyVec, ReadableVec, VecIndex};
use vecdb::{ReadableVec, VecIndex};
use crate::Query;
@@ -43,9 +43,9 @@ impl Query {
self.block_by_height(height)
}
/// Block by height. Height > tip → `OutOfRange`.
/// Block by height. Height past tip (or pre-genesis) → `OutOfRange`.
pub fn block_by_height(&self, height: Height) -> Result<BlockInfo> {
if height > self.tip_height() {
if height >= self.safe_lengths().height {
return Err(Error::OutOfRange("Block height out of range".into()));
}
let h = height.to_usize();
@@ -58,7 +58,7 @@ impl Query {
/// `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> {
if height > self.height() {
if height >= self.safe_lengths().height {
return Err(Error::OutOfRange("Block height out of range".into()));
}
let h = height.to_usize();
@@ -71,6 +71,9 @@ impl Query {
/// 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)?;
if height >= self.safe_lengths().height {
return Err(Error::OutOfRange("Block height out of range".into()));
}
let header = self.read_block_header(height)?;
Ok(bitcoin::consensus::encode::serialize_hex(&header))
}
@@ -79,7 +82,7 @@ impl Query {
/// 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> {
if height > self.tip_height() {
if height >= self.safe_lengths().height {
return Err(Error::OutOfRange("Block height out of range".into()));
}
self.indexer().vecs.blocks.blockhash.get(height).data()
@@ -88,7 +91,7 @@ impl Query {
/// 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.tip_height());
let (begin, end) = self.resolve_block_range(start_height, count, self.height());
self.blocks_range(begin, end)
}
@@ -102,51 +105,45 @@ impl Query {
// === 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.
/// `end` is re-clamped to `safe.height` (single snapshot) so two-snapshot
/// tearing under a concurrent reorg cannot short-read past the loop guards.
fn blocks_range(&self, begin: usize, end: usize) -> Result<Vec<BlockInfo>> {
let safe = self.safe_lengths();
let height_len = safe.height.to_usize();
let tx_index_len = safe.tx_index.to_usize();
let end = end.min(height_len);
if begin >= end {
return Ok(Vec::new());
}
let indexer = self.indexer();
let computer = self.computer();
let reader = self.reader();
let count = end - begin;
// Bulk read all indexed data
// Bulk read all indexed data. `end <= safe.height` ⇒ these per-block
// vecs are populated for `[begin, end)`, so short reads are impossible.
let blockhashes = indexer.vecs.blocks.blockhash.collect_range_at(begin, end);
let difficulties = indexer.vecs.blocks.difficulty.collect_range_at(begin, end);
let timestamps = indexer.vecs.blocks.timestamp.collect_range_at(begin, end);
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"));
}
debug_assert_eq!(blockhashes.len(), count);
debug_assert_eq!(difficulties.len(), count);
debug_assert_eq!(timestamps.len(), count);
debug_assert_eq!(sizes.len(), count);
debug_assert_eq!(weights.len(), count);
debug_assert_eq!(positions.len(), count);
// Bulk read tx indexes for tx_count
let max_height = self.indexed_height();
let tx_index_end = if end <= max_height.to_usize() {
end + 1
} else {
end
};
// Read one past the last block for its tx-count, capped by the snapshot's
// exclusive height bound. Tip block falls back to `tx_index_len` in the loop.
let tx_index_end = (end + 1).min(height_len);
let first_tx_indexes: Vec<TxIndex> = indexer
.vecs
.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();
debug_assert!(first_tx_indexes.len() >= count);
// Bulk read median time window
let median_start = begin.saturating_sub(10);
@@ -155,9 +152,7 @@ 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"));
}
debug_assert_eq!(median_timestamps.len(), end - median_start);
let mut blocks = Vec::with_capacity(count);
@@ -168,7 +163,7 @@ impl Query {
let tx_count = if i + 1 < first_tx_indexes.len() {
(first_tx_indexes[i + 1].to_usize() - first_tx_indexes[i].to_usize()) as u32
} else {
(total_txs - first_tx_indexes[i].to_usize()) as u32
(tx_index_len - first_tx_indexes[i].to_usize()) as u32
};
let median_time =
@@ -195,9 +190,15 @@ impl Query {
}
/// Build `BlockInfoV1` rows for `[begin, end)` in descending-height order.
/// Caller must bounds-check `end <= min(indexed, computed) + 1`. Returns
/// `Internal` on bulk-read short returns or per-block header read failures.
/// `end` is re-clamped to `bound.height` (single snapshot covering both
/// indexer-stamped and computer-stamped vecs, since `safe_lengths` only
/// advances after compute). Returns `Internal` on per-block header read
/// failures.
pub(crate) fn blocks_v1_range(&self, begin: usize, end: usize) -> Result<Vec<BlockInfoV1>> {
let safe = self.safe_lengths();
let height_len = safe.height.to_usize();
let tx_index_len = safe.tx_index.to_usize();
let end = end.min(height_len);
if begin >= end {
return Ok(Vec::new());
}
@@ -217,19 +218,14 @@ impl Query {
let positions = indexer.vecs.blocks.position.collect_range_at(begin, end);
let pool_slugs = computer.pools.pool.collect_range_at(begin, end);
// Bulk read tx indexes
let max_height = self.indexed_height();
let tx_index_end = if end <= max_height.to_usize() {
end + 1
} else {
end
};
// Read one past the last block for its tx-count, capped by the snapshot's
// exclusive height bound. Tip block falls back to `tx_index_len` in the loop.
let tx_index_end = (end + 1).min(height_len);
let first_tx_indexes: Vec<TxIndex> = indexer
.vecs
.transactions
.first_tx_index
.collect_range_at(begin, tx_index_end);
let total_txs = computer.indexes.tx_index.identity.len();
// Bulk read segwit stats
let segwit_txs = indexer.vecs.blocks.segwit_txs.collect_range_at(begin, end);
@@ -315,49 +311,49 @@ impl Query {
.timestamp
.collect_range_at(median_start, end);
let per_block_lens = [
blockhashes.len(),
difficulties.len(),
timestamps.len(),
sizes.len(),
weights.len(),
positions.len(),
pool_slugs.len(),
segwit_txs.len(),
segwit_sizes.len(),
segwit_weights.len(),
fee_sats.len(),
subsidy_sats.len(),
input_counts.len(),
output_counts.len(),
utxo_set_sizes.len(),
input_volumes.len(),
prices.len(),
output_volumes.len(),
fr_min.len(),
fr_pct10.len(),
fr_pct25.len(),
fr_median.len(),
fr_pct75.len(),
fr_pct90.len(),
fr_max.len(),
fa_min.len(),
fa_pct10.len(),
fa_pct25.len(),
fa_median.len(),
fa_pct75.len(),
fa_pct90.len(),
fa_max.len(),
];
if per_block_lens.iter().any(|&l| l != count) {
return Err(Error::Internal("blocks_v1_range: short read on per-block vecs"));
}
if first_tx_indexes.len() < count {
return Err(Error::Internal("blocks_v1_range: short read on first_tx_index"));
}
if median_timestamps.len() != end - median_start {
return Err(Error::Internal("blocks_v1_range: short read on median window"));
}
// All bulk reads above span `[begin, end)` (or `[median_start, end)`).
// Caller's `end <= bound.height + 1` precondition guarantees populated
// slots, so short reads are impossible.
debug_assert!(
[
blockhashes.len(),
difficulties.len(),
timestamps.len(),
sizes.len(),
weights.len(),
positions.len(),
pool_slugs.len(),
segwit_txs.len(),
segwit_sizes.len(),
segwit_weights.len(),
fee_sats.len(),
subsidy_sats.len(),
input_counts.len(),
output_counts.len(),
utxo_set_sizes.len(),
input_volumes.len(),
prices.len(),
output_volumes.len(),
fr_min.len(),
fr_pct10.len(),
fr_pct25.len(),
fr_median.len(),
fr_pct75.len(),
fr_pct90.len(),
fr_max.len(),
fa_min.len(),
fa_pct10.len(),
fa_pct25.len(),
fa_median.len(),
fa_pct75.len(),
fa_pct90.len(),
fa_max.len(),
]
.iter()
.all(|&l| l == count)
);
debug_assert!(first_tx_indexes.len() >= count);
debug_assert_eq!(median_timestamps.len(), end - median_start);
let mut blocks = Vec::with_capacity(count);
@@ -365,7 +361,7 @@ impl Query {
let tx_count = if i + 1 < first_tx_indexes.len() {
(first_tx_indexes[i + 1].to_usize() - first_tx_indexes[i].to_usize()) as u32
} else {
(total_txs - first_tx_indexes[i].to_usize()) as u32
(tx_index_len - first_tx_indexes[i].to_usize()) as u32
};
// Single reader for header + coinbase (adjacent in blk file).
@@ -503,10 +499,6 @@ impl Query {
// === Helper methods ===
pub fn tip_height(&self) -> Height {
Height::from(self.indexer().vecs.blocks.blockhash.len().saturating_sub(1))
}
/// Hash to height. The prefix store keys on the first 8 bytes of
/// the hash, so the resolved height is verified against the full
/// `blockhash[height]` before being returned. Prefix collisions
@@ -545,9 +537,8 @@ impl Query {
/// `(begin, end)` half-open window of up to `count` blocks ending
/// at `start_height` (default `cap`), clamped to `[0, cap]`. Caller
/// supplies `cap`: `tip_height()` when reading indexer-only series,
/// `height() = min(indexed, computed)` when reading computer-stamped
/// series too.
/// supplies `cap`: typically [`Query::height`] (the highest fully-written
/// height per the safe-lengths snapshot).
fn resolve_block_range(
&self,
start_height: Option<Height>,
@@ -668,10 +659,11 @@ impl Query {
/// but accepts coinbase parse failures (they manifest as missing
/// `extras` rather than a 5xx).
fn parse_coinbase_from_read(reader: impl Read) -> Coinbase {
let tx = match bitcoin::Transaction::consensus_decode(&mut bitcoin::io::FromStd::new(reader)) {
Ok(tx) => tx,
Err(_) => return Coinbase::default(),
};
let tx =
match bitcoin::Transaction::consensus_decode(&mut bitcoin::io::FromStd::new(reader)) {
Ok(tx) => tx,
Err(_) => return Coinbase::default(),
};
let total_size = tx.total_size();

View File

@@ -11,10 +11,10 @@ impl Query {
}
fn block_raw_by_height(&self, height: Height) -> Result<Vec<u8>> {
let max_height = self.tip_height();
if height > max_height {
let bound = self.safe_lengths().height;
if height >= bound {
return Err(Error::OutOfRange(
format!("Block height {height} out of range (tip {max_height})").into(),
format!("Block height {height} out of range (tip {})", self.height()).into(),
));
}

View File

@@ -10,13 +10,14 @@ impl Query {
}
fn block_status_by_height(&self, height: Height) -> Result<BlockStatus> {
let max_height = self.tip_height();
let bound = self.safe_lengths().height;
if height > max_height {
if height >= bound {
return Ok(BlockStatus::not_in_best_chain());
}
let next_best = if height < max_height {
let tip = self.height();
let next_best = if height < tip {
Some(
self.indexer()
.vecs

View File

@@ -1,7 +1,7 @@
use brk_error::{Error, OptionData, Result};
use brk_types::{BlockTimestamp, Date, Day1, Height, Timestamp};
use jiff::Timestamp as JiffTimestamp;
use vecdb::{AnyVec, ReadableVec};
use vecdb::ReadableVec;
use crate::Query;
@@ -23,10 +23,10 @@ impl Query {
let indexer = self.indexer();
let computer = self.computer();
if indexer.vecs.blocks.blockhash.len() == 0 {
if self.safe_lengths().height == Height::ZERO {
return Err(Error::NotFound("No blocks indexed".into()));
}
let tip: usize = self.tip_height().into();
let tip: usize = self.height().into();
let target = timestamp;
let date = Date::from(target);

View File

@@ -8,7 +8,7 @@ use brk_types::{
Weight,
};
use rustc_hash::FxHashMap;
use vecdb::{AnyVec, ReadableVec, VecIndex};
use vecdb::{ReadableVec, VecIndex};
use crate::Query;
@@ -71,9 +71,7 @@ impl Query {
.txid
.collect_range_at(first, first + tx_count);
if txids.len() != tx_count {
return Err(Error::Internal(
"block_txids_by_height: short txid read",
));
return Err(Error::Internal("block_txids_by_height: short txid read"));
}
Ok(txids)
}
@@ -310,28 +308,22 @@ impl Query {
///
/// `OutOfRange` when `height` is past the indexed-tip stamp.
/// `Internal` when `first_tx_index[height]` is missing under the
/// stamp-before-data race. For the tip block (where
/// `first_tx_index[height+1]` is not yet written), `next` falls back
/// to `txid.len()`.
/// stamp-before-data race. The tip-of-safe block falls back to
/// `safe.tx_index` (not live `txid.len()`, which can be ahead of the
/// writer's stamped boundary mid-block).
fn block_tx_range(&self, height: Height) -> Result<(usize, usize)> {
let indexer = self.indexer();
if height > self.indexed_height() {
let safe = self.safe_lengths();
if height >= safe.height {
return Err(Error::OutOfRange("Block height out of range".into()));
}
let first: usize = indexer
.vecs
.transactions
.first_tx_index
.collect_one(height)
.data()?
.into();
let next: usize = indexer
.vecs
.transactions
.first_tx_index
.collect_one(height.incremented())
.unwrap_or_else(|| TxIndex::from(indexer.vecs.transactions.txid.len()))
.into();
let first_tx_index_vec = &self.indexer().vecs.transactions.first_tx_index;
let first: usize = first_tx_index_vec.collect_one(height).data()?.into();
let next_height = height.incremented();
let next: usize = if next_height < safe.height {
first_tx_index_vec.collect_one(next_height).data()?.into()
} else {
safe.tx_index.to_usize()
};
Ok((first, next - first))
}
}

View File

@@ -11,12 +11,10 @@
use brk_error::{Error, OptionData, Result};
use brk_mempool::cluster::{Cluster, ClusterNode, LocalIdx};
use brk_types::{
CpfpInfo, FeeRate, Height, TxIndex, TxInIndex, Txid, TxidPrefix, VSize, Weight,
};
use brk_types::{CpfpInfo, FeeRate, Height, TxInIndex, TxIndex, Txid, TxidPrefix, VSize, Weight};
use rustc_hash::{FxBuildHasher, FxHashMap};
use smallvec::SmallVec;
use vecdb::{AnyVec, ReadableVec, VecIndex};
use vecdb::{ReadableVec, VecIndex};
use crate::Query;
@@ -128,18 +126,15 @@ impl Query {
) -> Result<(Cluster<TxIndex>, LocalIdx)> {
let indexer = self.indexer();
let computer = self.computer();
let block_first = indexer
.vecs
.transactions
.first_tx_index
.collect_one(height)
.data()?;
let block_end = indexer
.vecs
.transactions
.first_tx_index
.collect_one(height.incremented())
.unwrap_or_else(|| TxIndex::from(indexer.vecs.transactions.txid.len()));
let safe = self.safe_lengths();
let first_tx_index_vec = &indexer.vecs.transactions.first_tx_index;
let block_first = first_tx_index_vec.collect_one(height).data()?;
let next_height = height.incremented();
let block_end = if next_height < safe.height {
first_tx_index_vec.collect_one(next_height).data()?
} else {
safe.tx_index
};
let same_block = |idx: TxIndex| idx >= block_first && idx < block_end;
let WalkResult { nodes, seed_local } = self.walk_same_block_edges(seed, same_block);
@@ -153,7 +148,8 @@ impl Query {
.into_iter()
.map(|(tx_index, parents)| {
let i = tx_index.to_usize();
let weight = Weight::from_sizes(*base_size.get(i).data()?, *total_size.get(i).data()?);
let weight =
Weight::from_sizes(*base_size.get(i).data()?, *total_size.get(i).data()?);
Ok(ClusterNode {
id: tx_index,
txid: txid_reader.get(i),
@@ -189,10 +185,16 @@ impl Query {
let mut walk_inputs = |tx: TxIndex| -> SmallVec<[TxIndex; 2]> {
let mut out: SmallVec<[TxIndex; 2]> = SmallVec::new();
let Ok(start) = first_txin.get(tx.to_usize()).data() else { return out };
let Ok(end) = first_txin.get(tx.to_usize() + 1).data() else { return out };
let Ok(start) = first_txin.get(tx.to_usize()).data() else {
return out;
};
let Ok(end) = first_txin.get(tx.to_usize() + 1).data() else {
return out;
};
for i in usize::from(start)..usize::from(end) {
let Ok(op) = outpoint.get(i).data() else { continue };
let Ok(op) = outpoint.get(i).data() else {
continue;
};
if op.is_coinbase() {
continue;
}
@@ -237,14 +239,22 @@ impl Query {
let mut stack: Vec<TxIndex> = vec![seed];
let mut descendant_count = 0;
'd: while let Some(cur) = stack.pop() {
let Ok(start) = first_txout.get(cur.to_usize()).data() else { continue };
let Ok(end) = first_txout.get(cur.to_usize() + 1).data() else { continue };
let Ok(start) = first_txout.get(cur.to_usize()).data() else {
continue;
};
let Ok(end) = first_txout.get(cur.to_usize() + 1).data() else {
continue;
};
for i in usize::from(start)..usize::from(end) {
let Ok(txin_idx) = spent.get(i).data() else { continue };
let Ok(txin_idx) = spent.get(i).data() else {
continue;
};
if txin_idx == TxInIndex::UNSPENT {
continue;
}
let Ok(child) = spending_tx.get(usize::from(txin_idx)).data() else { continue };
let Ok(child) = spending_tx.get(usize::from(txin_idx)).data() else {
continue;
};
if local_of.contains_key(&child) || !same_block(child) {
continue;
}

View File

@@ -67,6 +67,9 @@ impl Query {
let indexer = self.indexer();
let stores = &indexer.stores;
let safe = self.safe_lengths();
let tx_index_len = safe.tx_index;
let txout_index_len = safe.txout_index;
let first_txout_index_reader = indexer.vecs.transactions.first_txout_index.reader();
let output_type_reader = indexer.vecs.outputs.output_type.reader();
let type_index_reader = indexer.vecs.outputs.type_index.reader();
@@ -79,8 +82,15 @@ impl Query {
.get(&TxidPrefix::from(prev_txid))
.ok()??
.into_owned();
if prev_tx_index >= tx_index_len {
return None;
}
let first_txout: TxOutIndex = first_txout_index_reader.get(prev_tx_index.to_usize());
let txout_index = usize::from(first_txout + vout);
let txout = first_txout + vout;
if txout >= txout_index_len {
return None;
}
let txout_index = usize::from(txout);
let output_type: OutputType = output_type_reader.get(txout_index);
let type_index: TypeIndex = type_index_reader.get(txout_index);
let value: Sats = value_reader.get(txout_index);
@@ -106,10 +116,7 @@ impl Query {
let root_txid = Self::walk_to_replacement_root(&graveyard, *txid);
let replaces_vec: Vec<Txid> = graveyard
.predecessors_of(txid)
.map(|(p, _)| *p)
.collect();
let replaces_vec: Vec<Txid> = graveyard.predecessors_of(txid).map(|(p, _)| *p).collect();
let replaces = (!replaces_vec.is_empty()).then_some(replaces_vec);
let replacements = self.build_rbf_node(&root_txid, None, &txs, &entries, &graveyard);
@@ -124,9 +131,7 @@ impl Query {
/// replacer of an RBF chain. Returns `txid` itself if it's already
/// the root.
fn walk_to_replacement_root(graveyard: &TxGraveyard, mut root: Txid) -> Txid {
while let Some(TxRemoval::Replaced { by }) =
graveyard.get(&root).map(TxTombstone::reason)
{
while let Some(TxRemoval::Replaced { by }) = graveyard.get(&root).map(TxTombstone::reason) {
root = *by;
}
root

View File

@@ -62,8 +62,7 @@ impl Query {
// Bitcoin block timestamps can step backward within MTP rules, so
// saturate the subtraction to avoid u32 underflow on a backwards-going
// first block of an epoch.
let elapsed_time =
u64::from((*current_timestamp).saturating_sub(*epoch_start_timestamp));
let elapsed_time = u64::from((*current_timestamp).saturating_sub(*epoch_start_timestamp));
let time_avg = if blocks_into_epoch > 0 {
elapsed_time / blocks_into_epoch as u64
} else {

View File

@@ -49,9 +49,9 @@ pub(super) fn iter_difficulty_epochs(
// Epochs that start before the window are skipped; we still record
// their difficulty so the next in-window entry can compute its ratio.
if epoch_height.to_usize() < start_height {
prev_difficulty = Some(*difficulty_cursor.get(epoch_usize).ok_or(
Error::Internal("iter_difficulty_epochs: missing pre-window epoch difficulty"),
)?);
prev_difficulty = Some(*difficulty_cursor.get(epoch_usize).ok_or(Error::Internal(
"iter_difficulty_epochs: missing pre-window epoch difficulty",
))?);
continue;
}

View File

@@ -194,7 +194,15 @@ impl Query {
let estimated_hashrate = (share_24h * network_hr as f64) as u128;
let total_reward = if let Some(major) = computer.pools.major.get(&slug) {
Some(major.rewards.cumulative.sats.height.collect_one(current_height).data()?)
Some(
major
.rewards
.cumulative
.sats
.height
.collect_one(current_height)
.data()?,
)
} else {
None
};
@@ -232,7 +240,7 @@ impl Query {
let computer = self.computer();
let tip = self.height().to_usize();
let upper = before_height.map(|h| h.to_usize()).unwrap_or(tip);
let end = upper.min(computer.pools.pool.len().saturating_sub(1));
let end = upper.min(tip);
let heights: Vec<usize> = computer
.pools

View File

@@ -102,11 +102,7 @@ impl Query {
Ok(csv)
}
fn get_vec(
&self,
series: &SeriesName,
index: Index,
) -> Result<&'static dyn AnyExportableVec> {
fn get_vec(&self, series: &SeriesName, index: Index) -> Result<&'static dyn AnyExportableVec> {
self.vecs()
.get(series, index)
.ok_or_else(|| self.series_not_found_error(series))
@@ -158,7 +154,11 @@ impl Query {
let resolve_bound = |ri: RangeIndex, fallback: usize| -> Result<usize> {
let i = self.range_index_to_i64(ri, index)?;
Ok(vecs.iter().map(|v| v.i64_to_usize(i)).min().unwrap_or(fallback))
Ok(vecs
.iter()
.map(|v| v.i64_to_usize(i))
.min()
.unwrap_or(fallback))
};
let start = match params.start() {
@@ -186,7 +186,7 @@ impl Query {
// Snapshot tip-derived state together so the historical-branch ETag stays
// self-consistent: stable_count is computed from tip_height, hash_prefix
// is the live tip.
let tip_height = self.indexed_height();
let tip_height = self.height();
let hash_prefix = self.tip_hash_prefix();
let stable_count = self.stable_count(params.index, total, tip_height);
@@ -213,12 +213,7 @@ impl Query {
/// its live tail as stable.
/// - Mutable (Funded/Empty addr): `None`. No immutable region exists, so
/// the caller must use the tip-bound ETag for every range.
pub fn stable_count(
&self,
index: Index,
total: usize,
tip_height: Height,
) -> Option<usize> {
pub fn stable_count(&self, index: Index, total: usize, tip_height: Height) -> Option<usize> {
match index.cache_class() {
CacheClass::Bucket { margin } => Some(total.saturating_sub(margin)),
CacheClass::Entity => {
@@ -232,13 +227,27 @@ impl Query {
fn entity_index_at(&self, index: Index, h: Height) -> Option<usize> {
let v = &self.indexer().vecs;
match index {
Index::TxIndex => v.transactions.first_tx_index.collect_one(h).map(usize::from),
Index::TxIndex => v
.transactions
.first_tx_index
.collect_one(h)
.map(usize::from),
Index::TxInIndex => v.inputs.first_txin_index.collect_one(h).map(usize::from),
Index::TxOutIndex => v.outputs.first_txout_index.collect_one(h).map(usize::from),
Index::EmptyOutputIndex => v.scripts.empty.first_index.collect_one(h).map(usize::from),
Index::OpReturnIndex => v.scripts.op_return.first_index.collect_one(h).map(usize::from),
Index::OpReturnIndex => v
.scripts
.op_return
.first_index
.collect_one(h)
.map(usize::from),
Index::P2MSOutputIndex => v.scripts.p2ms.first_index.collect_one(h).map(usize::from),
Index::UnknownOutputIndex => v.scripts.unknown.first_index.collect_one(h).map(usize::from),
Index::UnknownOutputIndex => v
.scripts
.unknown
.first_index
.collect_one(h)
.map(usize::from),
Index::P2AAddrIndex => v.addrs.p2a.first_index.collect_one(h).map(usize::from),
Index::P2PK33AddrIndex => v.addrs.p2pk33.first_index.collect_one(h).map(usize::from),
Index::P2PK65AddrIndex => v.addrs.p2pk65.first_index.collect_one(h).map(usize::from),

View File

@@ -7,7 +7,7 @@ use brk_types::{
BlockHash, Height, MerkleProof, Timestamp, Transaction, TxInIndex, TxIndex, TxOutIndex,
TxOutspend, TxStatus, Txid, TxidPrefix, Vin, Vout,
};
use vecdb::{AnyVec, ReadableVec, VecIndex};
use vecdb::{ReadableVec, VecIndex};
use crate::Query;
@@ -15,6 +15,10 @@ impl Query {
// ── Txid → TxIndex resolution (single source of truth) ─────────
/// Resolve a txid to its internal TxIndex via prefix lookup.
/// Raw store hit — caller should prefer [`Self::resolve_tx_index_bounded`]
/// when subsequent reads dereference indexer/computer vecs by `tx_index`.
/// Use this raw form only for "is this mined?" probes that don't deref
/// derived data (mempool merge, cpfp fee-rate fall-through).
#[inline]
pub(crate) fn resolve_tx_index(&self, txid: &Txid) -> Result<TxIndex> {
self.indexer()
@@ -25,7 +29,23 @@ impl Query {
.ok_or(Error::UnknownTxid)
}
/// `resolve_tx_index` clamped against the safe-lengths snapshot.
/// Returns `UnknownTxid` for tx_indices the store knows but the snapshot
/// has not yet covered. Use this from any path that will subsequently
/// dereference indexer/computer vecs by `tx_index`.
#[inline]
pub(crate) fn resolve_tx_index_bounded(&self, txid: &Txid) -> Result<TxIndex> {
let tx_index = self.resolve_tx_index(txid)?;
if tx_index >= self.safe_lengths().tx_index {
return Err(Error::UnknownTxid);
}
Ok(tx_index)
}
pub fn txid_by_index(&self, index: TxIndex) -> Result<Txid> {
if index >= self.safe_lengths().tx_index {
return Err(Error::OutOfRange("Transaction index out of range".into()));
}
self.indexer()
.vecs
.transactions
@@ -36,7 +56,7 @@ impl Query {
/// Resolve a txid to (TxIndex, Height).
pub fn resolve_tx(&self, txid: &Txid) -> Result<(TxIndex, Height)> {
let tx_index = self.resolve_tx_index(txid)?;
let tx_index = self.resolve_tx_index_bounded(txid)?;
let height = self.confirmed_status_height(tx_index)?;
Ok((tx_index, height))
}
@@ -44,8 +64,14 @@ impl Query {
// ── TxStatus construction (single source of truth) ─────────────
/// Height for a confirmed tx_index via in-memory TxHeights lookup.
/// Bounded against the safe-lengths snapshot so rejected tx_indices
/// never dereference slots a concurrent writer might be populating.
#[inline]
pub(crate) fn confirmed_status_height(&self, tx_index: TxIndex) -> Result<Height> {
let bound = self.safe_lengths();
if tx_index >= bound.tx_index {
return Err(Error::UnknownTxid);
}
self.computer()
.indexes
.tx_heights
@@ -81,7 +107,7 @@ impl Query {
if let Some(tx) = self.map_mempool_tx(txid, Transaction::clone) {
return Ok(tx);
}
self.transaction_by_index(self.resolve_tx_index(txid)?)
self.transaction_by_index(self.resolve_tx_index_bounded(txid)?)
}
pub fn transaction_status(&self, txid: &Txid) -> Result<TxStatus> {
@@ -96,14 +122,14 @@ impl Query {
if let Some(bytes) = self.map_mempool_tx(txid, Transaction::encode_bytes) {
return Ok(bytes);
}
self.transaction_raw_by_index(self.resolve_tx_index(txid)?)
self.transaction_raw_by_index(self.resolve_tx_index_bounded(txid)?)
}
pub fn transaction_hex(&self, txid: &Txid) -> Result<String> {
if let Some(hex) = self.map_mempool_tx(txid, |tx| tx.encode_bytes().to_lower_hex_string()) {
return Ok(hex);
}
self.transaction_hex_by_index(self.resolve_tx_index(txid)?)
self.transaction_hex_by_index(self.resolve_tx_index_bounded(txid)?)
}
// ── Outspend queries ───────────────────────────────────────────
@@ -142,8 +168,7 @@ impl Query {
}
fn mempool_outspend(&self, txid: &Txid, vout: Vout) -> TxOutspend {
let Some((spender_txid, vin)) =
self.mempool().and_then(|m| m.lookup_spender(txid, vout))
let Some((spender_txid, vin)) = self.mempool().and_then(|m| m.lookup_spender(txid, vout))
else {
return TxOutspend::UNSPENT;
};
@@ -187,6 +212,8 @@ impl Query {
let mut input_tx_cursor = indexer.vecs.inputs.tx_index.cursor();
let mut first_txin_cursor = indexer.vecs.transactions.first_txin_index.cursor();
let bound = self.safe_lengths();
let mut cached_status: Option<(Height, BlockHash, Timestamp)> = None;
let mut outspends = Vec::with_capacity(output_count);
for i in 0..output_count {
@@ -198,6 +225,10 @@ impl Query {
}
let spending_tx_index = input_tx_cursor.get(usize::from(txin_index)).data()?;
if spending_tx_index >= bound.tx_index {
outspends.push(TxOutspend::UNSPENT);
continue;
}
let spending_first_txin = first_txin_cursor.get(spending_tx_index.to_usize()).data()?;
let vin = Vin::from(usize::from(txin_index) - usize::from(spending_first_txin));
let spending_txid = txid_reader.get(spending_tx_index.to_usize());
@@ -258,16 +289,23 @@ impl Query {
}
/// Resolve txid to (tx_index, first_txout_index, output_count).
/// Snapshots `safe_lengths` once and uses `safe.txout_index` as the
/// upper bound for the tip-of-safe tx, so the fallback never reads past
/// the writer's stamped boundary (`vecs.outputs.value.len()` can be
/// ahead of `safe.txout_index` when the writer is mid-block).
fn resolve_tx_outputs(&self, txid: &Txid) -> Result<(TxIndex, TxOutIndex, usize)> {
let safe = self.safe_lengths();
let tx_index = self.resolve_tx_index(txid)?;
let indexer = self.indexer();
let first_txout_vec = &indexer.vecs.transactions.first_txout_index;
if tx_index >= safe.tx_index {
return Err(Error::UnknownTxid);
}
let first_txout_vec = &self.indexer().vecs.transactions.first_txout_index;
let first = first_txout_vec.read_once(tx_index)?;
let next_tx = tx_index.incremented();
let next = if next_tx.to_usize() < first_txout_vec.len() {
let next = if next_tx < safe.tx_index {
first_txout_vec.read_once(next_tx)?
} else {
TxOutIndex::from(indexer.vecs.outputs.value.len())
safe.txout_index
};
Ok((tx_index, first, usize::from(next) - usize::from(first)))
}

View File

@@ -5,7 +5,7 @@ use std::{path::Path, sync::Arc};
use brk_computer::Computer;
use brk_error::{OptionData, Result};
use brk_indexer::Indexer;
use brk_indexer::{Indexer, Lengths};
use brk_mempool::Mempool;
use brk_reader::Reader;
use brk_rpc::Client;
@@ -57,19 +57,24 @@ impl Query {
}))
}
/// Current indexed height
pub fn indexed_height(&self) -> Height {
self.indexer().indexed_height()
}
/// Current computed height (series)
pub fn computed_height(&self) -> Height {
self.computer().computed_height()
}
/// Minimum of indexed and computed heights
/// Pipeline-safe ceiling: the highest height for which both the
/// indexer and computer have committed durable data. Backed by
/// `Indexer::safe_lengths()`, advanced by `main.rs` after each
/// compute pass and lowered before any rollback.
///
/// Returns a height (the last fully-written block), not a length.
/// `safe_lengths().height` is a count: `N` means heights `0..N` are
/// committed, so the highest is `N-1`. Pre-genesis (`N == 0`) falls
/// back to `Height::default()` and clients treat it as "nothing
/// indexed yet".
pub fn height(&self) -> Height {
self.indexed_height().min(self.computed_height())
self.safe_lengths().height.decremented().unwrap_or_default()
}
/// Snapshot of the pipeline-safe `Lengths`. Hot paths that need
/// multiple bound fields should call this once at entry and reuse.
pub(crate) fn safe_lengths(&self) -> Lengths {
self.indexer().safe_lengths()
}
/// Tip block hash, cached in the indexer.
@@ -84,17 +89,20 @@ impl Query {
BlockHashPrefix::from(&self.tip_blockhash())
}
/// Build sync status with the given tip height
/// Build sync status with the given tip height. `indexed_height` and
/// `computed_height` reflect live per-vec stamps (diagnostic) and may be
/// briefly ahead of fully-flushed data; the timestamp data read uses the
/// safe-lengths-derived height so it never outruns committed bytes.
pub fn sync_status(&self, tip_height: Height) -> Result<SyncStatus> {
let indexed_height = self.indexed_height();
let computed_height = self.computed_height();
let indexed_height = self.indexer().indexed_height();
let computed_height = self.computer().computed_height();
let blocks_behind = Height::from(tip_height.saturating_sub(*indexed_height));
let last_indexed_at_unix = self
.indexer()
.vecs
.blocks
.timestamp
.collect_one(indexed_height)
.collect_one(self.height())
.data()?;
Ok(SyncStatus {

View File

@@ -135,7 +135,9 @@ impl<'a> Vecs<'a> {
}
pub fn series_info(&self, series: &SeriesName) -> Option<SeriesInfo> {
let index_to_vec = self.series_to_index_to_vec.get(series.normalize().as_ref())?;
let index_to_vec = self
.series_to_index_to_vec
.get(series.normalize().as_ref())?;
let value_type = index_to_vec.values().next()?.value_type_to_string();
let indexes = index_to_vec.keys().copied().collect();
Some(SeriesInfo {
@@ -191,7 +193,10 @@ impl<'a> Builder<'a> {
.entry(name)
.or_default()
.insert(index, vec);
assert!(prev.is_none(), "Duplicate series: {name} for index {index:?}");
assert!(
prev.is_none(),
"Duplicate series: {name} for index {index:?}"
);
self.index_to_series_to_vec
.entry(index)