mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-05-19 22:34:46 -07:00
global: adding support for safe lengths
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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(),
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)))
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user