global: snap

This commit is contained in:
nym21
2026-08-09 14:33:32 +02:00
parent 1118a2958f
commit 26f07f01a3
135 changed files with 4462 additions and 1098 deletions
Generated
+2
View File
@@ -733,6 +733,8 @@ name = "brk_oracle"
version = "0.11.2"
dependencies = [
"brk_indexer",
"brk_reader",
"brk_rpc",
"brk_types",
"serde_json",
"vecdb",
+7 -7
View File
@@ -36,23 +36,23 @@ pub fn main() -> anyhow::Result<()> {
let reader = Reader::new(config.blocksdir(), &client);
let mut indexer = Indexer::forced_import(&config.brkdir())?;
let mut indexer = Indexer::import(&config.brkdir(), &reader)?;
#[cfg(not(debug_assertions))]
{
// Pre-run indexer if too far behind, then drop and reimport to reduce memory
let chain_height = client.get_last_height()?;
let indexed_height = indexer.vecs.next_height();
let indexed_height = indexer.vecs().next_height();
let blocks_behind = chain_height.saturating_sub(*indexed_height);
if blocks_behind > 10_000 {
info!("---");
info!("Indexing {blocks_behind} blocks before starting server...");
info!("---");
sleep(Duration::from_secs(10));
indexer.index(&reader, &client, &exit)?;
indexer.index(&exit)?;
drop(indexer);
Mimalloc::collect();
indexer = Indexer::forced_import(&config.brkdir())?;
indexer = Indexer::import(&config.brkdir(), &reader)?;
}
}
@@ -60,7 +60,7 @@ pub fn main() -> anyhow::Result<()> {
let mempool = Mempool::new(&client);
let query = AsyncQuery::build(&reader, &indexer, &computer, Some(mempool.clone()));
let query = AsyncQuery::build(&indexer, &computer, Some(mempool.clone()));
let mempool_clone = mempool.clone();
let resolver = query.sync(|q| q.indexer_prevout_resolver());
@@ -104,9 +104,9 @@ pub fn main() -> anyhow::Result<()> {
let total_start = Instant::now();
if cfg!(debug_assertions) {
indexer.checked_index(&reader, &client, &exit)?;
indexer.checked_index(&exit)?;
} else {
indexer.index(&reader, &client, &exit)?;
indexer.index(&exit)?;
}
Mimalloc::collect();
+155
View File
@@ -4,6 +4,7 @@ use brk_traversable::Traversable;
use brk_types::Age;
use rayon::iter::{IntoParallelIterator, ParallelIterator};
use serde::Serialize;
use vecdb::{ColumnId, VecValue, Version};
use super::{CohortName, Filter, TimeFilter};
@@ -33,6 +34,135 @@ pub const HOURS_12Y: usize = 24 * 12 * 365;
pub const HOURS_15Y: usize = 24 * 15 * 365;
pub const AGE_RANGE_COUNT: usize = 23;
pub const STH_AGE_RANGE_COUNT: usize = 8;
pub const LTH_AGE_RANGE_COUNT: usize = AGE_RANGE_COUNT - STH_AGE_RANGE_COUNT;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(u8)]
pub enum AgeRangeId {
Under1H,
From1HTo1D,
From1DTo1W,
From1WTo1M,
From1MTo2M,
From2MTo3M,
From3MTo4M,
From4MTo5M,
From5MTo6M,
From6MTo9M,
From9MTo1Y,
From1YTo18M,
From18MTo2Y,
From2YTo3Y,
From3YTo4Y,
From4YTo5Y,
From5YTo6Y,
From6YTo7Y,
From7YTo8Y,
From8YTo10Y,
From10YTo12Y,
From12YTo15Y,
Over15Y,
}
pub const AGE_RANGE_IDS: [AgeRangeId; AGE_RANGE_COUNT] = [
AgeRangeId::Under1H,
AgeRangeId::From1HTo1D,
AgeRangeId::From1DTo1W,
AgeRangeId::From1WTo1M,
AgeRangeId::From1MTo2M,
AgeRangeId::From2MTo3M,
AgeRangeId::From3MTo4M,
AgeRangeId::From4MTo5M,
AgeRangeId::From5MTo6M,
AgeRangeId::From6MTo9M,
AgeRangeId::From9MTo1Y,
AgeRangeId::From1YTo18M,
AgeRangeId::From18MTo2Y,
AgeRangeId::From2YTo3Y,
AgeRangeId::From3YTo4Y,
AgeRangeId::From4YTo5Y,
AgeRangeId::From5YTo6Y,
AgeRangeId::From6YTo7Y,
AgeRangeId::From7YTo8Y,
AgeRangeId::From8YTo10Y,
AgeRangeId::From10YTo12Y,
AgeRangeId::From12YTo15Y,
AgeRangeId::Over15Y,
];
pub const STH_AGE_RANGE_IDS: [AgeRangeId; STH_AGE_RANGE_COUNT] = [
AgeRangeId::Under1H,
AgeRangeId::From1HTo1D,
AgeRangeId::From1DTo1W,
AgeRangeId::From1WTo1M,
AgeRangeId::From1MTo2M,
AgeRangeId::From2MTo3M,
AgeRangeId::From3MTo4M,
AgeRangeId::From4MTo5M,
];
pub const LTH_AGE_RANGE_IDS: [AgeRangeId; LTH_AGE_RANGE_COUNT] = [
AgeRangeId::From5MTo6M,
AgeRangeId::From6MTo9M,
AgeRangeId::From9MTo1Y,
AgeRangeId::From1YTo18M,
AgeRangeId::From18MTo2Y,
AgeRangeId::From2YTo3Y,
AgeRangeId::From3YTo4Y,
AgeRangeId::From4YTo5Y,
AgeRangeId::From5YTo6Y,
AgeRangeId::From6YTo7Y,
AgeRangeId::From7YTo8Y,
AgeRangeId::From8YTo10Y,
AgeRangeId::From10YTo12Y,
AgeRangeId::From12YTo15Y,
AgeRangeId::Over15Y,
];
impl ColumnId for AgeRangeId {
type Row<T>
= [T; AGE_RANGE_COUNT]
where
T: VecValue;
const VERSION: Version = Version::ONE;
const ALL: &'static [Self] = &AGE_RANGE_IDS;
#[inline]
fn index(self) -> usize {
self as usize
}
#[inline]
fn get<T: VecValue>(self, row: &Self::Row<T>) -> &T {
&row[self as usize]
}
#[inline]
fn get_mut<T: VecValue>(self, row: &mut Self::Row<T>) -> &mut T {
&mut row[self as usize]
}
#[inline]
fn from_fn<T, F>(mut f: F) -> Self::Row<T>
where
T: VecValue,
F: FnMut(Self) -> T,
{
std::array::from_fn(|index| f(AGE_RANGE_IDS[index]))
}
#[inline]
fn map<T, U, F>(row: Self::Row<T>, f: F) -> Self::Row<U>
where
T: VecValue,
U: VecValue,
F: FnMut(T) -> U,
{
row.map(f)
}
}
/// Age boundaries in hours. Defines the cohort ranges:
/// [0, 1h), [1h, 1d), [1d, 1w), [1w, 1m), ..., [15y, ∞)
@@ -444,6 +574,31 @@ mod tests {
}
}
#[test]
fn column_ids_match_storage_order_and_term_split() {
assert_eq!(AgeRangeId::ALL, &AGE_RANGE_IDS);
assert_eq!(
STH_AGE_RANGE_IDS.len() + LTH_AGE_RANGE_IDS.len(),
AGE_RANGE_COUNT
);
assert_eq!(
STH_AGE_RANGE_IDS.as_slice(),
&AGE_RANGE_IDS[..STH_AGE_RANGE_COUNT]
);
assert_eq!(
LTH_AGE_RANGE_IDS.as_slice(),
&AGE_RANGE_IDS[STH_AGE_RANGE_COUNT..]
);
assert_eq!(STH_AGE_RANGE_IDS.last(), Some(&AgeRangeId::From4MTo5M));
assert_eq!(LTH_AGE_RANGE_IDS.first(), Some(&AgeRangeId::From5MTo6M));
let row = AgeRangeId::from_fn(|column| column.index());
for (index, &column) in AGE_RANGE_IDS.iter().enumerate() {
assert_eq!(column.index(), index);
assert_eq!(*column.get(&row), index);
}
}
#[test]
fn split_range_names_and_boundaries_match() {
assert_eq!(HOURS_9M, HOURS_6M + HOURS_3M);
+5 -5
View File
@@ -30,26 +30,26 @@ pub fn main() -> color_eyre::Result<()> {
let reader = Reader::new(bitcoin_dir.join("blocks"), &client);
let mut indexer = Indexer::forced_import(&outputs_dir)?;
let mut indexer = Indexer::import(&outputs_dir, &reader)?;
let exit = Exit::new();
exit.set_ctrlc_handler();
// Pre-run indexer if too far behind, then drop and reimport to reduce memory
let chain_height = client.get_last_height()?;
let indexed_height = indexer.vecs.next_height();
let indexed_height = indexer.vecs().next_height();
if u32::from(chain_height).saturating_sub(u32::from(indexed_height)) > 1000 {
indexer.checked_index(&reader, &client, &exit)?;
indexer.checked_index(&exit)?;
drop(indexer);
Mimalloc::collect();
indexer = Indexer::forced_import(&outputs_dir)?;
indexer = Indexer::import(&outputs_dir, &reader)?;
}
let mut computer = Computer::forced_import(&outputs_dir, &indexer)?;
loop {
let i = Instant::now();
indexer.checked_index(&reader, &client, &exit)?;
indexer.checked_index(&exit)?;
Mimalloc::collect();
@@ -27,7 +27,7 @@ pub fn main() -> Result<()> {
let reader = Reader::new(bitcoin_dir.join("blocks"), &client);
let mut indexer = Indexer::forced_import(&outputs_dir)?;
let mut indexer = Indexer::import(&outputs_dir, &reader)?;
let mut computer = Computer::forced_import(&outputs_benches_dir, &indexer)?;
@@ -44,7 +44,7 @@ pub fn main() -> Result<()> {
});
let i = Instant::now();
indexer.index(&reader, &client, &exit)?;
indexer.index(&exit)?;
info!("Done in {:?}", i.elapsed());
Mimalloc::collect();
@@ -3,6 +3,8 @@ use std::{env, path::Path, time::Instant};
use brk_computer::Computer;
use brk_error::Result;
use brk_indexer::Indexer;
use brk_reader::Reader;
use brk_rpc::{Auth, Client};
use vecdb::{AnySerializableVec, AnyVec};
pub fn main() -> Result<()> {
@@ -10,7 +12,13 @@ pub fn main() -> Result<()> {
let outputs_dir = Path::new(&env::var("HOME").unwrap()).join(".brk");
let indexer = Indexer::forced_import(&outputs_dir)?;
let bitcoin_dir = Client::default_bitcoin_path();
let client = Client::new(
Client::default_url(),
Auth::CookieFile(bitcoin_dir.join(".cookie")),
)?;
let reader = Reader::new(bitcoin_dir.join("blocks"), &client);
let indexer = Indexer::import(&outputs_dir, &reader)?;
let computer = Computer::forced_import(&outputs_dir, &indexer)?;
@@ -2,6 +2,8 @@ use std::{env, fs, path::Path};
use brk_computer::Computer;
use brk_indexer::Indexer;
use brk_reader::Reader;
use brk_rpc::{Auth, Client};
use brk_traversable::{Traversable, TreeNode};
pub fn main() -> color_eyre::Result<()> {
@@ -10,12 +12,14 @@ pub fn main() -> color_eyre::Result<()> {
let tmp = env::temp_dir().join("brk_tree_gen");
fs::create_dir_all(&tmp)?;
let indexer = Indexer::forced_import(&tmp)?;
let client = Client::new("http://127.0.0.1:1", Auth::None)?;
let reader = Reader::new_without_rlimit(tmp.join("blocks"), &client);
let indexer = Indexer::import(&tmp, &reader)?;
let computer = Computer::forced_import(&tmp, &indexer)?;
let tree = TreeNode::Branch(
[
("indexed".to_string(), indexer.vecs.to_tree_node()),
("indexed".to_string(), indexer.vecs().to_tree_node()),
("computed".to_string(), computer.to_tree_node()),
]
.into_iter()
+5 -5
View File
@@ -44,23 +44,23 @@ pub fn main() -> color_eyre::Result<()> {
let reader = Reader::new(bitcoin_dir.join("blocks"), &client);
let mut indexer = Indexer::forced_import(&outputs_dir)?;
let mut indexer = Indexer::import(&outputs_dir, &reader)?;
// Pre-run indexer if too far behind, then drop and reimport to reduce memory
let chain_height = client.get_last_height()?;
let indexed_height = indexer.vecs.next_height();
let indexed_height = indexer.vecs().next_height();
if chain_height.saturating_sub(*indexed_height) > 1000 {
indexer.index(&reader, &client, &exit)?;
indexer.index(&exit)?;
drop(indexer);
Mimalloc::collect();
indexer = Indexer::forced_import(&outputs_dir)?;
indexer = Indexer::import(&outputs_dir, &reader)?;
}
let mut computer = Computer::forced_import(&outputs_dir, &indexer)?;
loop {
let i = Instant::now();
indexer.index(&reader, &client, &exit)?;
indexer.index(&exit)?;
info!("Done in {:?}", i.elapsed());
Mimalloc::collect();
@@ -47,7 +47,7 @@ impl Vecs {
total: LazyPerBlockCumulativeRolling::from_indexed_source(
"block_count",
version + Version::ONE,
&indexer.vecs.blocks.weight,
&indexer.vecs().blocks.weight,
cumulative_block_count,
cached_starts,
indexes,
@@ -36,7 +36,7 @@ impl Vecs {
let hashrate = LazyPerBlock::from_height_source::<DifficultyToHashF64, _>(
"difficulty_hashrate",
version,
indexer.vecs.blocks.difficulty.read_only_clone(),
indexer.vecs().blocks.difficulty.read_only_clone(),
indexes,
);
@@ -63,7 +63,7 @@ impl Vecs {
Self {
value: Resolutions::forced_import(
"difficulty",
indexer.vecs.blocks.difficulty.read_only_clone(),
indexer.vecs().blocks.difficulty.read_only_clone(),
version,
indexes,
),
@@ -71,7 +71,7 @@ impl Vecs {
adjustment: LazyPercentPerBlock::from_lookback_source(
"difficulty_adjustment",
version + Version::ONE,
&indexer.vecs.blocks.difficulty,
&indexer.vecs().blocks.difficulty,
DIFFICULTY_ADJUSTMENT_LOOKBACK,
difficulty_adjustment,
indexes,
@@ -11,12 +11,12 @@ impl Vecs {
let mut prev_timestamp = None;
self.0.compute_from(
starting_height,
&indexer.vecs.blocks.timestamp,
&indexer.vecs().blocks.timestamp,
|height, timestamp| {
let interval = if let Some(previous_height) = height.decremented() {
let previous = prev_timestamp.unwrap_or_else(|| {
indexer
.vecs
.vecs()
.blocks
.timestamp
.collect_one(previous_height)
@@ -20,7 +20,7 @@ impl Vecs {
self.size.compute(
starting_height,
&window_starts,
&indexer.vecs.blocks.total,
&indexer.vecs().blocks.total,
exit,
)?;
@@ -26,7 +26,7 @@ impl Vecs {
db,
"block_vbytes",
version,
&indexer.vecs.blocks.weight,
&indexer.vecs().blocks.weight,
block_vbytes,
indexes,
cached_starts,
@@ -34,7 +34,7 @@ impl Vecs {
let fullness = LazyPercentVec::from_indexed_source(
"block_fullness",
version,
&indexer.vecs.blocks.weight,
&indexer.vecs().blocks.weight,
block_fullness,
);
@@ -63,9 +63,9 @@ pub(crate) fn process_blocks(
return Ok(());
}
let height_to_first_tx_index = &indexer.vecs.transactions.first_tx_index;
let height_to_first_txout_index = &indexer.vecs.outputs.first_txout_index;
let height_to_first_txin_index = &indexer.vecs.inputs.first_txin_index;
let height_to_first_tx_index = &indexer.vecs().transactions.first_tx_index;
let height_to_first_txout_index = &indexer.vecs().outputs.first_txout_index;
let height_to_first_txin_index = &indexer.vecs().inputs.first_txin_index;
let height_to_tx_count = &transactions.count.total.block;
let height_to_output_count = &outputs.count.total.sum;
let height_to_input_count = &inputs.count.sum;
@@ -106,7 +106,7 @@ pub(crate) fn process_blocks(
debug!("VecsReaders created");
// Extend tx_index_to_height RangeMap with new entries (incremental, O(new_blocks))
let target_len = indexer.vecs.transactions.first_tx_index.len();
let target_len = indexer.vecs().transactions.first_tx_index.len();
let current_len = tx_index_to_height.len();
if current_len < target_len {
debug!(
@@ -114,7 +114,7 @@ pub(crate) fn process_blocks(
current_len, target_len
);
let new_entries: Vec<TxIndex> = indexer
.vecs
.vecs()
.transactions
.first_tx_index
.collect_range_at(current_len, target_len);
@@ -141,49 +141,49 @@ pub(crate) fn process_blocks(
// Pre-collect first address indexes per type for the block range
let first_p2a_vec = indexer
.vecs
.vecs()
.addrs
.p2a
.first_index
.collect_range_at(start_usize, end_usize);
let first_p2pk33_vec = indexer
.vecs
.vecs()
.addrs
.p2pk33
.first_index
.collect_range_at(start_usize, end_usize);
let first_p2pk65_vec = indexer
.vecs
.vecs()
.addrs
.p2pk65
.first_index
.collect_range_at(start_usize, end_usize);
let first_p2pkh_vec = indexer
.vecs
.vecs()
.addrs
.p2pkh
.first_index
.collect_range_at(start_usize, end_usize);
let first_p2sh_vec = indexer
.vecs
.vecs()
.addrs
.p2sh
.first_index
.collect_range_at(start_usize, end_usize);
let first_p2tr_vec = indexer
.vecs
.vecs()
.addrs
.p2tr
.first_index
.collect_range_at(start_usize, end_usize);
let first_p2wpkh_vec = indexer
.vecs
.vecs()
.addrs
.p2wpkh
.first_index
.collect_range_at(start_usize, end_usize);
let first_p2wsh_vec = indexer
.vecs
.vecs()
.addrs
.p2wsh
.first_index
@@ -46,21 +46,21 @@ impl<'a> TxOutReaders<'a> {
output_count: usize,
) -> &[TxOutData] {
let end = first_txout_index + output_count;
self.indexer.vecs.outputs.value.collect_range_into_at(
self.indexer.vecs().outputs.value.collect_range_into_at(
first_txout_index,
end,
&mut self.values_buf,
);
self.indexer.vecs.outputs.output_type.collect_range_into_at(
first_txout_index,
end,
&mut self.output_types_buf,
);
self.indexer.vecs.outputs.type_index.collect_range_into_at(
first_txout_index,
end,
&mut self.type_indexes_buf,
);
self.indexer
.vecs()
.outputs
.output_type
.collect_range_into_at(first_txout_index, end, &mut self.output_types_buf);
self.indexer
.vecs()
.outputs
.type_index
.collect_range_into_at(first_txout_index, end, &mut self.type_indexes_buf);
self.txout_data_buf.clear();
self.txout_data_buf.extend(
@@ -118,17 +118,17 @@ impl<'a> TxInReaders<'a> {
let end = first_txin_index + input_count;
self.input_values
.collect_range_into_at(first_txin_index, end, &mut self.values_buf);
self.indexer.vecs.inputs.outpoint.collect_range_into_at(
self.indexer.vecs().inputs.outpoint.collect_range_into_at(
first_txin_index,
end,
&mut self.outpoints_buf,
);
self.indexer.vecs.inputs.output_type.collect_range_into_at(
first_txin_index,
end,
&mut self.output_types_buf,
);
self.indexer.vecs.inputs.type_index.collect_range_into_at(
self.indexer
.vecs()
.inputs
.output_type
.collect_range_into_at(first_txin_index, end, &mut self.output_types_buf);
self.indexer.vecs().inputs.type_index.collect_range_into_at(
first_txin_index,
end,
&mut self.type_indexes_buf,
+18 -18
View File
@@ -383,29 +383,29 @@ impl Vecs {
+ [
prices.spot.cents.height.version(),
indexes.timestamp.monotonic.version(),
indexer.vecs.transactions.first_tx_index.version(),
indexer.vecs.outputs.first_txout_index.version(),
indexer.vecs.inputs.first_txin_index.version(),
indexer.vecs().transactions.first_tx_index.version(),
indexer.vecs().outputs.first_txout_index.version(),
indexer.vecs().inputs.first_txin_index.version(),
transactions.count.total.block.version(),
outputs.count.total.sum.version(),
inputs.count.sum.version(),
indexes.tx_index.output_count.version(),
indexes.tx_index.input_count.version(),
indexer.vecs.outputs.value.version(),
indexer.vecs.outputs.output_type.version(),
indexer.vecs.outputs.type_index.version(),
indexer.vecs().outputs.value.version(),
indexer.vecs().outputs.output_type.version(),
indexer.vecs().outputs.type_index.version(),
inputs.value.version(),
indexer.vecs.inputs.outpoint.version(),
indexer.vecs.inputs.output_type.version(),
indexer.vecs.inputs.type_index.version(),
indexer.vecs.addrs.p2pk65.first_index.version(),
indexer.vecs.addrs.p2pk33.first_index.version(),
indexer.vecs.addrs.p2pkh.first_index.version(),
indexer.vecs.addrs.p2sh.first_index.version(),
indexer.vecs.addrs.p2wpkh.first_index.version(),
indexer.vecs.addrs.p2wsh.first_index.version(),
indexer.vecs.addrs.p2tr.first_index.version(),
indexer.vecs.addrs.p2a.first_index.version(),
indexer.vecs().inputs.outpoint.version(),
indexer.vecs().inputs.output_type.version(),
indexer.vecs().inputs.type_index.version(),
indexer.vecs().addrs.p2pk65.first_index.version(),
indexer.vecs().addrs.p2pk33.first_index.version(),
indexer.vecs().addrs.p2pkh.first_index.version(),
indexer.vecs().addrs.p2sh.first_index.version(),
indexer.vecs().addrs.p2wpkh.first_index.version(),
indexer.vecs().addrs.p2wsh.first_index.version(),
indexer.vecs().addrs.p2tr.first_index.version(),
indexer.vecs().addrs.p2a.first_index.version(),
]
.into_iter()
.sum::<Version>();
@@ -571,7 +571,7 @@ impl Vecs {
};
// 3. Get last height from indexer
let last_height = Height::from(indexer.vecs.blocks.blockhash.len().saturating_sub(1));
let last_height = Height::from(indexer.vecs().blocks.blockhash.len().saturating_sub(1));
debug!(
"last_height={}, starting_height={}",
last_height, starting_height
+20 -20
View File
@@ -99,13 +99,13 @@ impl Vecs {
identity: LazyVec::init(
"p2pk33_addr_index",
version,
indexer.vecs.addrs.p2pk33.bytes.read_only_boxed_clone(),
indexer.vecs().addrs.p2pk33.bytes.read_only_boxed_clone(),
|index, _| index,
),
addr: LazyVec::init(
"p2pk33_addr",
version,
indexer.vecs.addrs.p2pk33.bytes.read_only_boxed_clone(),
indexer.vecs().addrs.p2pk33.bytes.read_only_boxed_clone(),
|_, bytes| Addr::try_from(&AddrBytes::from(bytes)).unwrap(),
),
},
@@ -113,13 +113,13 @@ impl Vecs {
identity: LazyVec::init(
"p2pk65_addr_index",
version,
indexer.vecs.addrs.p2pk65.bytes.read_only_boxed_clone(),
indexer.vecs().addrs.p2pk65.bytes.read_only_boxed_clone(),
|index, _| index,
),
addr: LazyVec::init(
"p2pk65_addr",
version,
indexer.vecs.addrs.p2pk65.bytes.read_only_boxed_clone(),
indexer.vecs().addrs.p2pk65.bytes.read_only_boxed_clone(),
|_, bytes| Addr::try_from(&AddrBytes::from(bytes)).unwrap(),
),
},
@@ -127,13 +127,13 @@ impl Vecs {
identity: LazyVec::init(
"p2pkh_addr_index",
version,
indexer.vecs.addrs.p2pkh.bytes.read_only_boxed_clone(),
indexer.vecs().addrs.p2pkh.bytes.read_only_boxed_clone(),
|index, _| index,
),
addr: LazyVec::init(
"p2pkh_addr",
version,
indexer.vecs.addrs.p2pkh.bytes.read_only_boxed_clone(),
indexer.vecs().addrs.p2pkh.bytes.read_only_boxed_clone(),
|_, bytes| Addr::try_from(&AddrBytes::from(bytes)).unwrap(),
),
},
@@ -141,13 +141,13 @@ impl Vecs {
identity: LazyVec::init(
"p2sh_addr_index",
version,
indexer.vecs.addrs.p2sh.bytes.read_only_boxed_clone(),
indexer.vecs().addrs.p2sh.bytes.read_only_boxed_clone(),
|index, _| index,
),
addr: LazyVec::init(
"p2sh_addr",
version,
indexer.vecs.addrs.p2sh.bytes.read_only_boxed_clone(),
indexer.vecs().addrs.p2sh.bytes.read_only_boxed_clone(),
|_, bytes| Addr::try_from(&AddrBytes::from(bytes)).unwrap(),
),
},
@@ -155,13 +155,13 @@ impl Vecs {
identity: LazyVec::init(
"p2tr_addr_index",
version,
indexer.vecs.addrs.p2tr.bytes.read_only_boxed_clone(),
indexer.vecs().addrs.p2tr.bytes.read_only_boxed_clone(),
|index, _| index,
),
addr: LazyVec::init(
"p2tr_addr",
version,
indexer.vecs.addrs.p2tr.bytes.read_only_boxed_clone(),
indexer.vecs().addrs.p2tr.bytes.read_only_boxed_clone(),
|_, bytes| Addr::try_from(&AddrBytes::from(bytes)).unwrap(),
),
},
@@ -169,13 +169,13 @@ impl Vecs {
identity: LazyVec::init(
"p2wpkh_addr_index",
version,
indexer.vecs.addrs.p2wpkh.bytes.read_only_boxed_clone(),
indexer.vecs().addrs.p2wpkh.bytes.read_only_boxed_clone(),
|index, _| index,
),
addr: LazyVec::init(
"p2wpkh_addr",
version,
indexer.vecs.addrs.p2wpkh.bytes.read_only_boxed_clone(),
indexer.vecs().addrs.p2wpkh.bytes.read_only_boxed_clone(),
|_, bytes| Addr::try_from(&AddrBytes::from(bytes)).unwrap(),
),
},
@@ -183,13 +183,13 @@ impl Vecs {
identity: LazyVec::init(
"p2wsh_addr_index",
version,
indexer.vecs.addrs.p2wsh.bytes.read_only_boxed_clone(),
indexer.vecs().addrs.p2wsh.bytes.read_only_boxed_clone(),
|index, _| index,
),
addr: LazyVec::init(
"p2wsh_addr",
version,
indexer.vecs.addrs.p2wsh.bytes.read_only_boxed_clone(),
indexer.vecs().addrs.p2wsh.bytes.read_only_boxed_clone(),
|_, bytes| Addr::try_from(&AddrBytes::from(bytes)).unwrap(),
),
},
@@ -197,13 +197,13 @@ impl Vecs {
identity: LazyVec::init(
"p2a_addr_index",
version,
indexer.vecs.addrs.p2a.bytes.read_only_boxed_clone(),
indexer.vecs().addrs.p2a.bytes.read_only_boxed_clone(),
|index, _| index,
),
addr: LazyVec::init(
"p2a_addr",
version,
indexer.vecs.addrs.p2a.bytes.read_only_boxed_clone(),
indexer.vecs().addrs.p2a.bytes.read_only_boxed_clone(),
|_, bytes| Addr::try_from(&AddrBytes::from(bytes)).unwrap(),
),
},
@@ -212,7 +212,7 @@ impl Vecs {
"p2ms_output_index",
version,
indexer
.vecs
.vecs()
.scripts
.p2ms
.to_tx_index
@@ -225,7 +225,7 @@ impl Vecs {
"empty_output_index",
version,
indexer
.vecs
.vecs()
.scripts
.empty
.to_tx_index
@@ -238,7 +238,7 @@ impl Vecs {
"unknown_output_index",
version,
indexer
.vecs
.vecs()
.scripts
.unknown
.to_tx_index
@@ -250,7 +250,7 @@ impl Vecs {
identity: LazyVec::init(
"op_return_index",
version,
indexer.vecs.op_return.to_tx_index.read_only_boxed_clone(),
indexer.vecs().op_return.to_tx_index.read_only_boxed_clone(),
|index, _| index,
),
},
@@ -20,27 +20,31 @@ impl CachedChainCounts {
"tx_count_cumulative",
version,
indexer
.vecs
.vecs()
.transactions
.first_tx_index
.read_only_boxed_clone(),
indexer.vecs.transactions.txid.read_only_boxed_clone(),
indexer.vecs().transactions.txid.read_only_boxed_clone(),
)),
input: CachedVec::wrap(LazyCumulativeIndexVec::new(
"input_count_cumulative",
version,
indexer.vecs.inputs.first_txin_index.read_only_boxed_clone(),
indexer.vecs.inputs.outpoint.read_only_boxed_clone(),
indexer
.vecs()
.inputs
.first_txin_index
.read_only_boxed_clone(),
indexer.vecs().inputs.outpoint.read_only_boxed_clone(),
)),
output: CachedVec::wrap(LazyCumulativeIndexVec::new(
"output_count_cumulative",
version,
indexer
.vecs
.vecs()
.outputs
.first_txout_index
.read_only_boxed_clone(),
indexer.vecs.outputs.value.read_only_boxed_clone(),
indexer.vecs().outputs.value.read_only_boxed_clone(),
)),
}
}
+2 -2
View File
@@ -139,7 +139,7 @@ impl Vecs {
let timestamp = Timestamps::from_locals(
version,
monotonic,
indexer.vecs.blocks.timestamp.read_only_boxed_clone(),
indexer.vecs().blocks.timestamp.read_only_boxed_clone(),
&minute10,
&minute30,
&hour1,
@@ -192,7 +192,7 @@ impl Vecs {
let starting_height = indexer.safe_lengths().height;
self.tx_heights.update(indexer, starting_height);
if starting_height.to_usize() < indexer.vecs.transactions.first_tx_index.len() {
if starting_height.to_usize() < indexer.vecs().transactions.first_tx_index.len() {
self.chain_counts.clear();
}
+1 -1
View File
@@ -125,7 +125,7 @@ impl Timestamps {
let mut prev = None;
self.monotonic.inner.compute_transform(
starting_height,
&indexer.vecs.blocks.timestamp,
&indexer.vecs().blocks.timestamp,
|(h, timestamp, this)| {
if prev.is_none()
&& let Some(prev_h) = h.decremented()
@@ -19,10 +19,10 @@ pub struct TxHeights(Arc<RwLock<RangeMap<TxIndex, Height>>>);
impl TxHeights {
/// Build from the full `first_tx_index` vec at startup.
pub fn init(indexer: &Indexer) -> Self {
let len = indexer.vecs.transactions.first_tx_index.len();
let len = indexer.vecs().transactions.first_tx_index.len();
let entries: Vec<TxIndex> = if len > 0 {
indexer
.vecs
.vecs()
.transactions
.first_tx_index
.collect_range_at(0, len)
@@ -39,11 +39,11 @@ impl TxHeights {
if inner.len() > reorg_len {
inner.truncate(reorg_len);
}
let target_len = indexer.vecs.transactions.first_tx_index.len();
let target_len = indexer.vecs().transactions.first_tx_index.len();
let current_len = inner.len();
if current_len < target_len {
let new_entries: Vec<TxIndex> = indexer
.vecs
.vecs()
.transactions
.first_tx_index
.collect_range_at(current_len, target_len);
+5 -5
View File
@@ -18,28 +18,28 @@ impl Vecs {
identity: LazyVec::init(
"tx_index",
version,
indexer.vecs.transactions.txid.read_only_boxed_clone(),
indexer.vecs().transactions.txid.read_only_boxed_clone(),
|index, _| index,
),
input_count: LazyIndexCountVec::new(
"input_count",
version,
indexer
.vecs
.vecs()
.transactions
.first_txin_index
.read_only_boxed_clone(),
indexer.vecs.inputs.outpoint.read_only_boxed_clone(),
indexer.vecs().inputs.outpoint.read_only_boxed_clone(),
),
output_count: LazyIndexCountVec::new(
"output_count",
version,
indexer
.vecs
.vecs()
.transactions
.first_txout_index
.read_only_boxed_clone(),
indexer.vecs.outputs.value.read_only_boxed_clone(),
indexer.vecs().outputs.value.read_only_boxed_clone(),
),
}
}
@@ -14,7 +14,7 @@ impl Vecs {
identity: LazyVec::init(
"txin_index",
version,
indexer.vecs.inputs.outpoint.read_only_boxed_clone(),
indexer.vecs().inputs.outpoint.read_only_boxed_clone(),
|index, _| index,
),
}
@@ -14,7 +14,7 @@ impl Vecs {
identity: LazyVec::init(
"txout_index",
version,
indexer.vecs.outputs.value.read_only_boxed_clone(),
indexer.vecs().outputs.value.read_only_boxed_clone(),
|index, _| index,
),
}
@@ -11,10 +11,10 @@ impl Vecs {
pub(crate) fn compute(&mut self, indexer: &Indexer, exit: &Exit) -> Result<()> {
let starting_lengths = indexer.safe_lengths();
let dep_version = indexer.vecs.inputs.output_type.version()
+ indexer.vecs.transactions.first_tx_index.version()
+ indexer.vecs.transactions.first_txin_index.version()
+ indexer.vecs.transactions.txid.version();
let dep_version = indexer.vecs().inputs.output_type.version()
+ indexer.vecs().transactions.first_tx_index.version()
+ indexer.vecs().transactions.first_txin_index.version()
+ indexer.vecs().transactions.txid.version();
self.input_count
.validate_and_truncate(dep_version, starting_lengths.height)?;
@@ -26,18 +26,18 @@ impl Vecs {
.min_stateful_len()
.min(self.tx_count.min_stateful_len());
let first_tx_index = &indexer.vecs.transactions.first_tx_index;
let first_tx_index = &indexer.vecs().transactions.first_tx_index;
let end = first_tx_index.len();
if skip < end {
self.input_count.truncate_if_needed_at(skip)?;
self.tx_count.truncate_if_needed_at(skip)?;
let fi_batch = first_tx_index.collect_range_at(skip, end);
let txid_len = indexer.vecs.transactions.txid.len();
let total_txin_len = indexer.vecs.inputs.output_type.len();
let txid_len = indexer.vecs().transactions.txid.len();
let total_txin_len = indexer.vecs().inputs.output_type.len();
let mut itype_cursor = indexer.vecs.inputs.output_type.cursor();
let mut fi_in_cursor = indexer.vecs.transactions.first_txin_index.cursor();
let mut itype_cursor = indexer.vecs().inputs.output_type.cursor();
let mut fi_in_cursor = indexer.vecs().transactions.first_txin_index.cursor();
let mut height = skip;
walk_blocks(
+3 -3
View File
@@ -13,8 +13,8 @@ const BATCH_SIZE: usize = SORT_MEMORY_BUDGET / (size_of::<Entry>() + size_of::<S
impl Vecs {
pub(super) fn compute_value(&mut self, indexer: &Indexer, exit: &Exit) -> Result<()> {
let starting_lengths = indexer.safe_lengths();
let txout_indexes = &indexer.vecs.inputs.txout_index;
let dep_version = txout_indexes.version() + indexer.vecs.outputs.value.version();
let txout_indexes = &indexer.vecs().inputs.txout_index;
let dep_version = txout_indexes.version() + indexer.vecs().outputs.value.version();
self.value.validate_computed_version_or_reset(dep_version)?;
let target = txout_indexes.len();
@@ -24,7 +24,7 @@ impl Vecs {
return Ok(());
}
let value_reader = indexer.vecs.outputs.value.reader();
let value_reader = indexer.vecs().outputs.value.reader();
let mut entries = Vec::with_capacity((target - min).min(BATCH_SIZE));
let mut values = Vec::with_capacity((target - min).min(BATCH_SIZE));
@@ -118,6 +118,8 @@ mod tests {
use std::time::{SystemTime, UNIX_EPOCH};
use brk_indexer::Indexer;
use brk_reader::Reader;
use brk_rpc::{Auth, Client};
use brk_traversable::Traversable;
use vecdb::{AnyVec, ReadableVec};
@@ -135,7 +137,9 @@ mod tests {
std::process::id()
));
let indexer = Indexer::forced_import(&path).unwrap();
let client = Client::new("http://127.0.0.1:1", Auth::None).unwrap();
let reader = Reader::new_without_rlimit(path.join("blocks"), &client);
let indexer = Indexer::import(&path, &reader).unwrap();
let indexes = indexes::Vecs::forced_import(&path, Version::ONE, &indexer).unwrap();
let lookback = LookbackVecs::new(
Version::ONE,
@@ -96,7 +96,7 @@ where
self.block.compute_with_skip(
starting_lengths.height,
tx_index_source,
&indexer.vecs.transactions.first_tx_index,
&indexer.vecs().transactions.first_tx_index,
&indexes.height.tx_index_count,
exit,
skip_count,
@@ -105,7 +105,7 @@ where
self.distribution._6b.compute_from_nblocks(
starting_lengths.height,
tx_index_source,
&indexer.vecs.transactions.first_tx_index,
&indexer.vecs().transactions.first_tx_index,
&indexes.height.tx_index_count,
6,
exit,
@@ -135,7 +135,7 @@ where
starting_lengths.height,
tx_index_source,
vsize_source,
&indexer.vecs.transactions.first_tx_index,
&indexer.vecs().transactions.first_tx_index,
&indexes.height.tx_index_count,
exit,
skip_count,
@@ -144,7 +144,7 @@ where
self.distribution._6b.compute_from_nblocks(
starting_lengths.height,
tx_index_source,
&indexer.vecs.transactions.first_tx_index,
&indexer.vecs().transactions.first_tx_index,
&indexes.height.tx_index_count,
6,
exit,
@@ -26,10 +26,10 @@ impl Vecs {
self.coinbase.compute_from(
starting_height,
prices,
&indexer.vecs.transactions.first_tx_index,
&indexer.vecs().transactions.first_tx_index,
|_, tx_index| {
let mut txout_cursor = indexer
.vecs
.vecs()
.transactions
.first_txout_index
.reader()
@@ -44,7 +44,7 @@ impl Vecs {
count_cursor.advance(ti - count_cursor.position());
let output_count: usize = count_cursor.next().unwrap().into();
indexer.vecs.outputs.value.fold_range_at(
indexer.vecs().outputs.value.fold_range_at(
first_txout_index,
first_txout_index + output_count,
Sats::ZERO,
@@ -59,7 +59,7 @@ impl Vecs {
starting_height,
&window_starts,
prices,
&indexer.vecs.transactions.first_tx_index,
&indexer.vecs().transactions.first_tx_index,
&indexes.height.tx_index_count,
&transactions.fees.fee.tx_index,
exit,
@@ -22,9 +22,9 @@ impl Vecs {
cached_starts: &Windows<&CachedWindowStartVec>,
) -> Result<Self> {
let coinbase_version = version
+ indexer.vecs.transactions.first_txout_index.version()
+ indexer.vecs().transactions.first_txout_index.version()
+ indexes.tx_index.output_count.version()
+ indexer.vecs.outputs.value.version();
+ indexer.vecs().outputs.value.version();
let coinbase = ValuePerBlockCumulativeRolling::forced_import(
db,
+2 -2
View File
@@ -51,8 +51,8 @@ impl Vecs {
self.db.sync_bg_tasks()?;
let starting_lengths = indexer.safe_lengths();
let raw = &indexer.vecs.op_return;
let txs = &indexer.vecs.transactions;
let raw = &indexer.vecs().op_return;
let txs = &indexer.vecs().transactions;
let version = raw.first_index.version()
+ raw.to_tx_index.version()
+ raw.kind.version()
@@ -11,10 +11,10 @@ impl Vecs {
pub(crate) fn compute(&mut self, indexer: &Indexer, exit: &Exit) -> Result<()> {
let starting_lengths = indexer.safe_lengths();
let dep_version = indexer.vecs.outputs.output_type.version()
+ indexer.vecs.transactions.first_tx_index.version()
+ indexer.vecs.transactions.first_txout_index.version()
+ indexer.vecs.transactions.txid.version();
let dep_version = indexer.vecs().outputs.output_type.version()
+ indexer.vecs().transactions.first_tx_index.version()
+ indexer.vecs().transactions.first_txout_index.version()
+ indexer.vecs().transactions.txid.version();
self.output_count
.validate_and_truncate(dep_version, starting_lengths.height)?;
@@ -26,19 +26,19 @@ impl Vecs {
.min_stateful_len()
.min(self.tx_count.min_stateful_len());
let first_tx_index = &indexer.vecs.transactions.first_tx_index;
let first_tx_index = &indexer.vecs().transactions.first_tx_index;
let end = first_tx_index.len();
if skip < end {
self.output_count.truncate_if_needed_at(skip)?;
self.tx_count.truncate_if_needed_at(skip)?;
let fi_batch = first_tx_index.collect_range_at(skip, end);
let txid_len = indexer.vecs.transactions.txid.len();
let total_txout_len = indexer.vecs.outputs.output_type.len();
let txid_len = indexer.vecs().transactions.txid.len();
let total_txout_len = indexer.vecs().outputs.output_type.len();
let mut otype_cursor = indexer.vecs.outputs.output_type.reader().cursor();
let mut otype_cursor = indexer.vecs().outputs.output_type.reader().cursor();
let fo_cursor = indexer
.vecs
.vecs()
.transactions
.first_txout_index
.reader()
@@ -12,14 +12,14 @@ impl Vecs {
pub(crate) fn compute(&mut self, indexer: &Indexer, exit: &Exit) -> Result<ExitGuard> {
let starting_lengths = indexer.safe_lengths();
let dep_version = indexer.vecs.inputs.txout_index.version()
+ indexer.vecs.outputs.first_txout_index.version()
+ indexer.vecs.inputs.first_txin_index.version()
+ indexer.vecs.outputs.value.version();
let dep_version = indexer.vecs().inputs.txout_index.version()
+ indexer.vecs().outputs.first_txout_index.version()
+ indexer.vecs().inputs.first_txin_index.version()
+ indexer.vecs().outputs.value.version();
self.txin_index
.validate_computed_version_or_reset(dep_version)?;
let target_height = indexer.vecs.blocks.blockhash.len();
let target_height = indexer.vecs().blocks.blockhash.len();
if target_height == 0 {
return Ok(exit.lock());
}
@@ -35,10 +35,9 @@ impl Vecs {
self.txin_index
.truncate_if_needed(TxOutIndex::from(min_txout_index))?;
let txin_index_to_txout_index = &indexer.vecs.inputs.txout_index;
let txin_index_to_txout_index = &indexer.vecs().inputs.txout_index;
// Find min_height via binary search (first_txout_index is monotonically non-decreasing)
let first_txout_index_vec = &indexer.vecs.outputs.first_txout_index;
let first_txout_index_vec = &indexer.vecs().outputs.first_txout_index;
let min_height = if min_txout_index == 0 {
Height::ZERO
} else if min_txout_index >= starting_lengths.txout_index.to_usize() {
@@ -67,7 +66,7 @@ impl Vecs {
let first_txout_index_data =
first_txout_index_vec.collect_range_at(offset, target_height.to_usize() + 1);
let first_txin_index_data = indexer
.vecs
.vecs()
.inputs
.first_txin_index
.collect_range_at(offset, target_height.to_usize() + 2);
@@ -87,7 +86,7 @@ impl Vecs {
// Fill txout_index up to batch_end_height + 1
let batch_txout_index = if batch_end_height >= target_height {
indexer.vecs.outputs.value.len()
indexer.vecs().outputs.value.len()
} else {
first_txout_index_data[batch_end_height.to_usize() + 1 - offset].to_usize()
};
@@ -98,7 +97,7 @@ impl Vecs {
let txin_start =
first_txin_index_data[batch_start_height.to_usize() - offset].to_usize();
let txin_end = if batch_end_height >= target_height {
indexer.vecs.inputs.txout_index.len()
indexer.vecs().inputs.txout_index.len()
} else {
first_txin_index_data[batch_end_height.to_usize() + 1 - offset].to_usize()
};
@@ -17,13 +17,13 @@ impl Vecs {
let height_vec = &mut self.op_return.cumulative.sats.height;
// Validate computed versions against dependencies
let dep_version = indexer.vecs.outputs.first_txout_index.version()
+ indexer.vecs.outputs.output_type.version()
+ indexer.vecs.outputs.value.version();
let dep_version = indexer.vecs().outputs.first_txout_index.version()
+ indexer.vecs().outputs.output_type.version()
+ indexer.vecs().outputs.value.version();
height_vec.validate_computed_version_or_reset(dep_version)?;
// Get target height
let target_len = indexer.vecs.outputs.first_txout_index.len();
let target_len = indexer.vecs().outputs.first_txout_index.len();
if target_len == 0 {
self.op_return
.compute_cents(starting_lengths.height, prices, exit)?;
@@ -38,9 +38,10 @@ impl Vecs {
if starting_height <= target_height {
// Pre-collect height-indexed data
let first_txout_indexes: Vec<TxOutIndex> =
indexer.vecs.outputs.first_txout_index.collect_range_at(
indexer.vecs().outputs.first_txout_index.collect_range_at(
starting_height.to_usize(),
target_height.to_usize() + 2.min(indexer.vecs.outputs.first_txout_index.len()),
target_height.to_usize()
+ 2.min(indexer.vecs().outputs.first_txout_index.len()),
);
let mut output_types_buf: Vec<OutputType> = Vec::new();
@@ -62,19 +63,19 @@ impl Vecs {
if let Some(&next) = first_txout_indexes.get(local_idx + 1) {
next
} else {
TxOutIndex::from(indexer.vecs.outputs.value.len())
TxOutIndex::from(indexer.vecs().outputs.value.len())
};
let out_start = first_txout_index.to_usize();
let out_end = next_first_txout_index.to_usize();
// Pre-collect both vecs into reusable buffers
indexer.vecs.outputs.output_type.collect_range_into_at(
indexer.vecs().outputs.output_type.collect_range_into_at(
out_start,
out_end,
&mut output_types_buf,
);
indexer.vecs.outputs.value.collect_range_into_at(
indexer.vecs().outputs.value.collect_range_into_at(
out_start,
out_end,
&mut values_buf,
+27 -27
View File
@@ -137,20 +137,20 @@ impl Vecs {
let starting_height = indexer.safe_lengths().height;
let dep_version: Version = [
indexer.vecs.blocks.coinbase_tag.version(),
indexer.vecs.transactions.first_tx_index.version(),
indexer.vecs.transactions.first_txout_index.version(),
indexer.vecs().blocks.coinbase_tag.version(),
indexer.vecs().transactions.first_tx_index.version(),
indexer.vecs().transactions.first_txout_index.version(),
indexes.tx_index.output_count.version(),
indexer.vecs.outputs.output_type.version(),
indexer.vecs.outputs.type_index.version(),
indexer.vecs.addrs.p2pk65.bytes.version(),
indexer.vecs.addrs.p2pk33.bytes.version(),
indexer.vecs.addrs.p2pkh.bytes.version(),
indexer.vecs.addrs.p2sh.bytes.version(),
indexer.vecs.addrs.p2wpkh.bytes.version(),
indexer.vecs.addrs.p2wsh.bytes.version(),
indexer.vecs.addrs.p2tr.bytes.version(),
indexer.vecs.addrs.p2a.bytes.version(),
indexer.vecs().outputs.output_type.version(),
indexer.vecs().outputs.type_index.version(),
indexer.vecs().addrs.p2pk65.bytes.version(),
indexer.vecs().addrs.p2pk33.bytes.version(),
indexer.vecs().addrs.p2pkh.bytes.version(),
indexer.vecs().addrs.p2sh.bytes.version(),
indexer.vecs().addrs.p2wpkh.bytes.version(),
indexer.vecs().addrs.p2wsh.bytes.version(),
indexer.vecs().addrs.p2tr.bytes.version(),
indexer.vecs().addrs.p2a.bytes.version(),
]
.into_iter()
.sum();
@@ -165,17 +165,17 @@ impl Vecs {
}
self.pool.validate_computed_version_or_reset(dep_version)?;
let first_txout_index = indexer.vecs.transactions.first_txout_index.reader();
let output_type = indexer.vecs.outputs.output_type.reader();
let type_index = indexer.vecs.outputs.type_index.reader();
let p2pk65 = indexer.vecs.addrs.p2pk65.bytes.reader();
let p2pk33 = indexer.vecs.addrs.p2pk33.bytes.reader();
let p2pkh = indexer.vecs.addrs.p2pkh.bytes.reader();
let p2sh = indexer.vecs.addrs.p2sh.bytes.reader();
let p2wpkh = indexer.vecs.addrs.p2wpkh.bytes.reader();
let p2wsh = indexer.vecs.addrs.p2wsh.bytes.reader();
let p2tr = indexer.vecs.addrs.p2tr.bytes.reader();
let p2a = indexer.vecs.addrs.p2a.bytes.reader();
let first_txout_index = indexer.vecs().transactions.first_txout_index.reader();
let output_type = indexer.vecs().outputs.output_type.reader();
let type_index = indexer.vecs().outputs.type_index.reader();
let p2pk65 = indexer.vecs().addrs.p2pk65.bytes.reader();
let p2pk33 = indexer.vecs().addrs.p2pk33.bytes.reader();
let p2pkh = indexer.vecs().addrs.p2pkh.bytes.reader();
let p2sh = indexer.vecs().addrs.p2sh.bytes.reader();
let p2wpkh = indexer.vecs().addrs.p2wpkh.bytes.reader();
let p2wsh = indexer.vecs().addrs.p2wsh.bytes.reader();
let p2tr = indexer.vecs().addrs.p2tr.bytes.reader();
let p2a = indexer.vecs().addrs.p2a.bytes.reader();
let unknown = self.pools.get_unknown();
@@ -184,17 +184,17 @@ impl Vecs {
// Cursors avoid per-height PcoVec page decompression.
// Heights are sequential, tx_index values derived from them are monotonically
// increasing, so both cursors only advance forward.
let mut first_tx_index_cursor = indexer.vecs.transactions.first_tx_index.cursor();
let mut first_tx_index_cursor = indexer.vecs().transactions.first_tx_index.cursor();
first_tx_index_cursor.advance(min);
let mut output_count_cursor = indexes.tx_index.output_count.cursor();
self.pool.truncate_if_needed_at(min)?;
self.pool_heights.truncate(min);
let len = indexer.vecs.blocks.coinbase_tag.len();
let len = indexer.vecs().blocks.coinbase_tag.len();
let mut next_height = min;
indexer.vecs.blocks.coinbase_tag.try_for_each_range_at(
indexer.vecs().blocks.coinbase_tag.try_for_each_range_at(
min,
len,
|coinbase_tag| -> Result<()> {
+15 -15
View File
@@ -29,12 +29,12 @@ impl Vecs {
let starting_height = indexer.safe_lengths().height;
let source_version = [
indexer.vecs.transactions.txid.version(),
indexer.vecs.transactions.first_tx_index.version(),
indexer.vecs.outputs.first_txout_index.version(),
indexer.vecs.transactions.first_txout_index.version(),
indexer.vecs.outputs.value.version(),
indexer.vecs.outputs.output_type.version(),
indexer.vecs().transactions.txid.version(),
indexer.vecs().transactions.first_tx_index.version(),
indexer.vecs().outputs.first_txout_index.version(),
indexer.vecs().transactions.first_txout_index.version(),
indexer.vecs().outputs.value.version(),
indexer.vecs().outputs.output_type.version(),
]
.into_iter()
.sum();
@@ -44,7 +44,7 @@ impl Vecs {
.inner
.validate_computed_version_or_reset(source_version)?;
let total_heights = indexer.vecs.blocks.timestamp.len();
let total_heights = indexer.vecs().blocks.timestamp.len();
if total_heights <= START_HEIGHT_SLOW {
return Ok(());
@@ -195,22 +195,22 @@ impl Vecs {
c.height.to_usize(),
),
None => (
indexer.vecs.transactions.txid.len(),
indexer.vecs.outputs.value.len(),
indexer.vecs.transactions.first_tx_index.len(),
indexer.vecs().transactions.txid.len(),
indexer.vecs().outputs.value.len(),
indexer.vecs().transactions.first_tx_index.len(),
),
};
// Pre-collect height-indexed data for the range (plus one extra for next-block lookups)
let collect_end = (range.end + 1).min(height_len);
let first_tx_indexes: Vec<TxIndex> = indexer
.vecs
.vecs()
.transactions
.first_tx_index
.collect_range_at(range.start, collect_end);
let out_firsts: Vec<TxOutIndex> = indexer
.vecs
.vecs()
.outputs
.first_txout_index
.collect_range_at(range.start, collect_end);
@@ -218,7 +218,7 @@ impl Vecs {
// Cursor avoids per-block PcoVec page decompression for the
// tx-indexed first_txout_index lookup. Accessed tx_index values
// are strictly increasing across blocks, so it only advances forward.
let mut txout_cursor = indexer.vecs.transactions.first_txout_index.cursor();
let mut txout_cursor = indexer.vecs().transactions.first_txout_index.cursor();
// Reusable buffers: avoid per-block allocation. `tx_starts` holds the
// first txout index of each non-coinbase tx in the current block.
@@ -249,11 +249,11 @@ impl Vecs {
let out_start = tx_starts.first().copied().unwrap_or(out_end);
indexer
.vecs
.vecs()
.outputs
.value
.collect_range_into_at(out_start, out_end, &mut values);
indexer.vecs.outputs.output_type.collect_range_into_at(
indexer.vecs().outputs.output_type.collect_range_into_at(
out_start,
out_end,
&mut output_types,
@@ -7,8 +7,7 @@ use super::Vecs;
impl Vecs {
pub(crate) fn compute(&mut self, indexer: &Indexer, exit: &Exit) -> Result<()> {
let starting_height = indexer.safe_lengths().height;
let source = &indexer.vecs.transaction_features.count;
let source = &indexer.vecs().transaction_features.count;
for (metrics, source) in [
(&mut self.count.inscription, &source.inscription),
(&mut self.count.annex, &source.annex),
@@ -25,16 +25,16 @@ impl Vecs {
self.input_value.compute_sum_from_indexes(
starting_lengths.tx_index,
&indexer.vecs.transactions.first_txin_index,
&indexer.vecs().transactions.first_txin_index,
&indexes.tx_index.input_count,
input_values,
exit,
)?;
self.output_value.compute_sum_from_indexes(
starting_lengths.tx_index,
&indexer.vecs.transactions.first_txout_index,
&indexer.vecs().transactions.first_txout_index,
&indexes.tx_index.output_count,
&indexer.vecs.outputs.value,
&indexer.vecs().outputs.value,
exit,
)?;
@@ -75,9 +75,9 @@ impl Vecs {
let dep_version = self.input_value.version()
+ self.output_value.version()
+ size_vecs.vsize.tx_index.version()
+ indexer.vecs.inputs.outpoint.version()
+ indexer.vecs.transactions.first_tx_index.version()
+ indexer.vecs.transactions.first_txin_index.version()
+ indexer.vecs().inputs.outpoint.version()
+ indexer.vecs().transactions.first_tx_index.version()
+ indexer.vecs().transactions.first_txin_index.version()
+ indexes.height.tx_index_count.version();
self.fee
@@ -114,7 +114,7 @@ impl Vecs {
.min(self.is_cpfp_child.len())
.min(starting_lengths.tx_index.to_usize());
let max_height = indexer
.vecs
.vecs()
.transactions
.first_tx_index
.len()
@@ -142,7 +142,7 @@ impl Vecs {
}
let start_tx = indexer
.vecs
.vecs()
.transactions
.first_tx_index
.collect_one_at(start_height)
@@ -163,7 +163,7 @@ impl Vecs {
self.count.cpfp_child.truncate_if_needed_at(start_height)?;
let mut tx_count = indexes.height.tx_index_count.cursor();
let mut next_block_input = indexer.vecs.inputs.first_txin_index.cursor();
let mut next_block_input = indexer.vecs().inputs.first_txin_index.cursor();
tx_count.advance(start_height);
next_block_input.advance(start_height + 1);
@@ -193,7 +193,7 @@ impl Vecs {
.tx_index
.collect_range_into_at(first_tx, first_tx + n, &mut vsizes);
indexer
.vecs
.vecs()
.transactions
.first_txin_index
.collect_range_into_at(first_tx, first_tx + n, &mut txin_starts);
@@ -201,9 +201,9 @@ impl Vecs {
let input_end = if h + 1 < max_height {
next_block_input.next().unwrap().to_usize()
} else {
indexer.vecs.inputs.outpoint.len()
indexer.vecs().inputs.outpoint.len()
};
indexer.vecs.inputs.outpoint.collect_range_into_at(
indexer.vecs().inputs.outpoint.collect_range_into_at(
input_begin,
input_end,
&mut outpoints,
@@ -16,18 +16,18 @@ impl Vecs {
indexes: &indexes::Vecs,
exit: &Exit,
) -> Result<()> {
let features = &indexer.vecs.transaction_features;
let features = &indexer.vecs().transaction_features;
let version = indexes.tx_index.input_count.version()
+ indexes.tx_index.output_count.version()
+ indexer.vecs.transactions.first_tx_index.version()
+ indexer.vecs.transactions.first_txin_index.version()
+ indexer.vecs.transactions.first_txout_index.version()
+ indexer.vecs().transactions.first_tx_index.version()
+ indexer.vecs().transactions.first_txin_index.version()
+ indexer.vecs().transactions.first_txout_index.version()
+ input_values.version()
+ indexer.vecs.inputs.output_type.version()
+ indexer.vecs.inputs.type_index.version()
+ indexer.vecs.outputs.value.version()
+ indexer.vecs.outputs.output_type.version()
+ indexer.vecs.outputs.type_index.version()
+ indexer.vecs().inputs.output_type.version()
+ indexer.vecs().inputs.type_index.version()
+ indexer.vecs().outputs.value.version()
+ indexer.vecs().outputs.output_type.version()
+ indexer.vecs().outputs.type_index.version()
+ features.has_op_return.version()
+ features.has_inscription.version()
+ indexes.height.tx_index_count.version();
@@ -70,7 +70,7 @@ impl Vecs {
return Ok(());
}
let first_tx = &indexer.vecs.transactions.first_tx_index;
let first_tx = &indexer.vecs().transactions.first_tx_index;
let start_tx = first_tx.collect_one_at(start_height).unwrap().to_usize();
self.is_coinjoin.truncate_if_needed_at(start_tx)?;
self.is_consolidation.truncate_if_needed_at(start_tx)?;
@@ -84,14 +84,14 @@ impl Vecs {
.truncate_if_needed_at(start_height)?;
let first_txin = indexer
.vecs
.vecs()
.transactions
.first_txin_index
.collect_one_at(start_tx)
.unwrap()
.to_usize();
let first_txout = indexer
.vecs
.vecs()
.transactions
.first_txout_index
.collect_one_at(start_tx)
@@ -101,11 +101,11 @@ impl Vecs {
let mut input_count = indexes.tx_index.input_count.cursor();
let mut output_count = indexes.tx_index.output_count.cursor();
let mut input_value = input_values.cursor();
let mut input_type = indexer.vecs.inputs.output_type.cursor();
let mut input_type_index = indexer.vecs.inputs.type_index.cursor();
let mut output_value = indexer.vecs.outputs.value.reader().cursor();
let mut output_type = indexer.vecs.outputs.output_type.reader().cursor();
let mut output_type_index = indexer.vecs.outputs.type_index.reader().cursor();
let mut input_type = indexer.vecs().inputs.output_type.cursor();
let mut input_type_index = indexer.vecs().inputs.type_index.cursor();
let mut output_value = indexer.vecs().outputs.value.reader().cursor();
let mut output_type = indexer.vecs().outputs.output_type.reader().cursor();
let mut output_type_index = indexer.vecs().outputs.type_index.reader().cursor();
let mut has_op_return = features.has_op_return.cursor();
let mut has_inscription = features.has_inscription.cursor();
let mut tx_count = indexes.height.tx_index_count.cursor();
@@ -17,11 +17,11 @@ impl Vecs {
fees: &fees::Vecs,
exit: &Exit,
) -> Result<()> {
let features = &indexer.vecs.transaction_features;
let features = &indexer.vecs().transaction_features;
let version = features.is_unconditionally_nonstandard.version()
+ features.has_dust_output.version()
+ fees.fee.tx_index.version()
+ indexer.vecs.transactions.first_tx_index.version()
+ indexer.vecs().transactions.first_tx_index.version()
+ indexes.height.tx_index_count.version();
self.is_nonstandard
.validate_computed_version_or_reset(version)?;
@@ -57,7 +57,7 @@ impl Vecs {
return Ok(());
}
let first_tx = &indexer.vecs.transactions.first_tx_index;
let first_tx = &indexer.vecs().transactions.first_tx_index;
let start_tx = first_tx.collect_one_at(start_height).unwrap().to_usize();
self.is_nonstandard.truncate_if_needed_at(start_tx)?;
self.count.nonstandard.truncate_if_needed_at(start_height)?;
@@ -15,9 +15,9 @@ impl Vecs {
) -> Result<()> {
self.total.compute_cumulative_sum_from_indexes(
indexer.safe_lengths().height,
&indexer.vecs.transactions.first_tx_index,
&indexer.vecs().transactions.first_tx_index,
&indexes.height.tx_index_count,
&indexer.vecs.transactions.total_sigop_cost,
&indexer.vecs().transactions.total_sigop_cost,
|value| StoredU64::from(u64::from(u32::from(value))),
exit,
)
@@ -18,7 +18,7 @@ impl Vecs {
indexer,
indexes,
&starting_lengths,
&indexer.vecs.transactions.weight,
&indexer.vecs().transactions.weight,
exit,
)?;
@@ -21,7 +21,7 @@ impl Vecs {
let tx_index_to_vsize = LazyVec::transformed::<WeightToVSize>(
"tx_vsize",
version,
indexer.vecs.transactions.weight.read_only_boxed_clone(),
indexer.vecs().transactions.weight.read_only_boxed_clone(),
);
let vsize = LazyPerTxDistributionTransformed::new::<WeightToVSize>(
@@ -8,8 +8,7 @@ impl Vecs {
pub(crate) fn compute(&mut self, indexer: &Indexer, exit: &Exit) -> Result<()> {
let lengths = indexer.safe_lengths();
let starting_height = lengths.height;
let counts = &indexer.vecs.transaction_features.count;
let counts = &indexer.vecs().transaction_features.count;
for (metrics, source) in [
(&mut self.v1, &counts.v1),
(&mut self.v2, &counts.v2),
@@ -20,7 +20,7 @@ impl Vecs {
self.transfer_volume.compute_filtered_from_indexes(
starting_height,
prices,
&indexer.vecs.transactions.first_tx_index,
&indexer.vecs().transactions.first_tx_index,
&indexes.height.tx_index_count,
&fees_vecs.input_value,
|sats| !sats.is_max(),
+30 -3
View File
@@ -178,15 +178,42 @@ impl Error {
/// Lock errors are transient and should not trigger data deletion.
#[cfg(feature = "vecdb")]
pub fn is_lock_error(&self) -> bool {
matches!(self, Error::VecDB(e) if e.is_lock_error())
let is_vecdb_lock = matches!(self, Error::VecDB(e) if e.is_lock_error());
#[cfg(feature = "fjall")]
{
is_vecdb_lock || matches!(self, Error::Fjall(fjall::Error::Locked))
}
#[cfg(not(feature = "fjall"))]
{
is_vecdb_lock
}
}
/// Returns true if this error indicates data corruption or version incompatibility.
/// These errors may require resetting/deleting the data to recover.
#[cfg(feature = "vecdb")]
pub fn is_data_error(&self) -> bool {
matches!(self, Error::VecDB(e) if e.is_data_error())
|| matches!(self, Error::VersionMismatch { .. })
let is_vecdb_data = matches!(self, Error::VecDB(e) if e.is_data_error())
|| matches!(self, Error::VersionMismatch { .. });
#[cfg(feature = "fjall")]
{
is_vecdb_data
|| matches!(
self,
Error::Fjall(
fjall::Error::JournalRecovery(_)
| fjall::Error::InvalidVersion(_)
| fjall::Error::Decompress(_)
| fjall::Error::InvalidTrailer
| fjall::Error::InvalidTag(_)
| fjall::Error::Unrecoverable
)
)
}
#[cfg(not(feature = "fjall"))]
{
is_vecdb_data
}
}
/// Returns true if this network/fetch error indicates a permanent/blocking condition
+2 -2
View File
@@ -32,7 +32,7 @@ fn main() -> color_eyre::Result<()> {
let reader = Reader::new(bitcoin_dir.join("blocks"), &client);
debug!("Reader created.");
let mut indexer = Indexer::forced_import(&outputs_dir)?;
let mut indexer = Indexer::import(&outputs_dir, &reader)?;
debug!("Indexer imported.");
let exit = Exit::new();
@@ -40,7 +40,7 @@ fn main() -> color_eyre::Result<()> {
loop {
let i = Instant::now();
indexer.checked_index(&reader, &client, &exit)?;
indexer.checked_index(&exit)?;
indexer.advance_safe_lengths()?;
info!("Done in {:?}", i.elapsed());
+2 -2
View File
@@ -32,7 +32,7 @@ fn main() -> Result<()> {
let reader = Reader::new(bitcoin_dir.join("blocks"), &client);
let mut indexer = Indexer::forced_import(&outputs_dir)?;
let mut indexer = Indexer::import(&outputs_dir, &reader)?;
let mut bencher =
Bencher::from_cargo_env(env!("CARGO_PKG_NAME"), &outputs_dir.join("indexed"))?;
@@ -47,7 +47,7 @@ fn main() -> Result<()> {
});
let i = Instant::now();
indexer.index(&reader, &client, &exit)?;
indexer.index(&exit)?;
indexer.advance_safe_lengths()?;
info!("Done in {:?}", i.elapsed());
@@ -32,7 +32,7 @@ fn main() -> Result<()> {
let reader = Reader::new(bitcoin_dir.join("blocks"), &client);
let mut indexer = Indexer::forced_import(&outputs_dir)?;
let mut indexer = Indexer::import(&outputs_dir, &reader)?;
let mut bencher =
Bencher::from_cargo_env(env!("CARGO_PKG_NAME"), &outputs_dir.join("indexed"))?;
@@ -48,7 +48,7 @@ fn main() -> Result<()> {
loop {
let i = Instant::now();
indexer.index(&reader, &client, &exit)?;
indexer.index(&exit)?;
indexer.advance_safe_lengths()?;
info!("Done in {:?}", i.elapsed());
+13 -2
View File
@@ -2,6 +2,8 @@ use std::{fs, path::Path};
use brk_error::Result;
use brk_indexer::Indexer;
use brk_reader::Reader;
use brk_rpc::{Auth, Client};
use vecdb::ReadableVec;
fn main() -> Result<()> {
@@ -10,9 +12,18 @@ fn main() -> Result<()> {
let outputs_dir = Path::new(&std::env::var("HOME").unwrap()).join(".brk");
fs::create_dir_all(&outputs_dir)?;
let indexer = Indexer::forced_import(&outputs_dir)?;
let bitcoin_dir = Client::default_bitcoin_path();
let client = Client::new(
Client::default_url(),
Auth::CookieFile(bitcoin_dir.join(".cookie")),
)?;
let reader = Reader::new(bitcoin_dir.join("blocks"), &client);
let indexer = Indexer::import(&outputs_dir, &reader)?;
println!("{:?}", indexer.vecs.outputs.value.collect_range_at(0, 200));
println!(
"{:?}",
indexer.vecs().outputs.value.collect_range_at(0, 200)
);
Ok(())
}
@@ -2,6 +2,8 @@ use std::{fs, path::Path, time::Instant};
use brk_error::Result;
use brk_indexer::Indexer;
use brk_reader::Reader;
use brk_rpc::{Auth, Client};
use brk_types::Sats;
use vecdb::ReadableVec;
@@ -10,7 +12,7 @@ fn run_benchmark(indexer: &Indexer) -> (Sats, std::time::Duration, usize) {
let mut sum = Sats::ZERO;
let mut count = 0;
indexer.vecs.outputs.value.for_each(|value| {
indexer.vecs().outputs.value.for_each(|value| {
sum += value;
count += 1;
});
@@ -30,7 +32,13 @@ fn main() -> Result<()> {
println!("╚════════════════════════════════════════════════════════╝\n");
println!("Loading indexer from: {}", outputs_dir.display());
let indexer = Indexer::forced_import(&outputs_dir)?;
let bitcoin_dir = Client::default_bitcoin_path();
let client = Client::new(
Client::default_url(),
Auth::CookieFile(bitcoin_dir.join(".cookie")),
)?;
let reader = Reader::new(bitcoin_dir.join("blocks"), &client);
let indexer = Indexer::import(&outputs_dir, &reader)?;
println!("Indexer loaded.\n");
// Warmup run
+1 -1
View File
@@ -4,7 +4,7 @@ use brk_types::{TxIndex, Txid, TxidPrefix, Version};
// One version for all data sources
// Increment on **change _OR_ addition**
pub const VERSION: Version = Version::new(30);
pub const VERSION: Version = Version::new(31);
pub const SNAPSHOT_BLOCK_RANGE: usize = 1_000;
/// Known duplicate Bitcoin transactions (BIP30)
+91 -10
View File
@@ -1,3 +1,4 @@
use brk_error::Result;
use brk_types::{
EmptyOutputIndex, Height, OpReturnIndex, OutputType, P2AAddrIndex, P2MSOutputIndex,
P2PK33AddrIndex, P2PK65AddrIndex, P2PKHAddrIndex, P2SHAddrIndex, P2TRAddrIndex,
@@ -6,7 +7,7 @@ use brk_types::{
use tracing::info;
use vecdb::{AnyStoredVec, PcoVec, PcoVecValue, ReadableVec, VecIndex, VecValue, WritableVec};
use crate::{Stores, Vecs};
use crate::{Stores, Vecs, stores::IndexerStores as _};
/// Pipeline-wide length/count snapshot. Lengths semantics:
/// `bound.f = N` means positions `0..N` are fully written; readers
@@ -31,6 +32,11 @@ pub struct Lengths {
pub unknown_output_index: UnknownOutputIndex,
}
pub trait IndexerLengths: Sized {
fn from_local(vecs: &Vecs, stores: &Stores) -> Result<Option<Self>>;
fn resume_at(required_height: Height, vecs: &Vecs, stores: &Stores) -> Result<Option<Self>>;
}
impl Lengths {
pub fn to_type_index(&self, output_type: OutputType) -> TypeIndex {
match output_type {
@@ -134,18 +140,23 @@ impl Lengths {
}
/// Read current local lengths. `None` pre-genesis.
pub fn from_local(vecs: &Vecs, stores: &Stores) -> Option<Self> {
let height = vecs.next_height().min(stores.next_height());
Self::collect_at(height, vecs)
fn read_local(vecs: &Vecs, stores: &Stores) -> Result<Option<Self>> {
let Some(height) = matching_height(vecs.next_height(), stores.next_height()?) else {
return Ok(None);
};
Ok(Self::collect_at(height, vecs))
}
/// Read lengths to resume at `required_height`. Reorg-aware:
/// - if vector and store checkpoints differ, return `None` (full reset);
/// - if local is ahead, clamp down to `required_height`;
/// - if local is behind, return `None` (caller must full-reset).
pub fn resume_at(required_height: Height, vecs: &Vecs, stores: &Stores) -> Option<Self> {
let local = vecs.next_height().min(stores.next_height());
fn read_resume(required_height: Height, vecs: &Vecs, stores: &Stores) -> Result<Option<Self>> {
let Some(local) = matching_height(vecs.next_height(), stores.next_height()?) else {
return Ok(None);
};
if local < required_height {
return None;
return Ok(None);
}
let height = if local > required_height {
info!(
@@ -156,7 +167,7 @@ impl Lengths {
} else {
local
};
Self::collect_at(height, vecs)
Ok(Self::collect_at(height, vecs))
}
fn collect_at(height: Height, vecs: &Vecs) -> Option<Self> {
@@ -229,6 +240,29 @@ impl Lengths {
}
}
impl IndexerLengths for Lengths {
fn from_local(vecs: &Vecs, stores: &Stores) -> Result<Option<Self>> {
Self::read_local(vecs, stores)
}
fn resume_at(required_height: Height, vecs: &Vecs, stores: &Stores) -> Result<Option<Self>> {
Self::read_resume(required_height, vecs, stores)
}
}
fn matching_height(vec_height: Height, store_height: Option<Height>) -> Option<Height> {
let store_height = store_height?;
if vec_height == store_height {
Some(vec_height)
} else {
info!(
"Indexer checkpoint mismatch: vectors at {}, stores at {}; full reset required",
vec_height, store_height
);
None
}
}
/// Per-type next-to-write counter at `next_height`. `None` pre-genesis.
fn next_index<I, T>(
height_to_index: &PcoVec<Height, I>,
@@ -240,11 +274,58 @@ where
T: VecValue,
{
let h = Height::from(height_to_index.stamp());
if h.is_zero() {
if next_height.is_zero() {
None
} else if h + 1_u32 == next_height {
} else if h.incremented() == next_height {
Some(I::from(index_to_else.len()))
} else {
height_to_index.collect_one(next_height)
}
}
#[cfg(test)]
mod checkpoint_tests {
use super::*;
use brk_types::StoredU32;
use vecdb::{Database, ImportableVec, Stamp, Version};
#[test]
fn matching_checkpoint_is_accepted() {
let height = Height::new(42);
assert_eq!(matching_height(height, Some(height)), Some(height));
}
#[test]
fn mismatched_checkpoint_requires_reset() {
assert_eq!(
matching_height(Height::new(42), Some(Height::new(41))),
None
);
assert_eq!(
matching_height(Height::new(41), Some(Height::new(42))),
None
);
assert_eq!(matching_height(Height::ZERO, None), None);
}
#[test]
fn genesis_stamp_uses_current_length() {
let dir = tempfile::tempdir().unwrap();
let db = Database::open(dir.path()).unwrap();
let mut first_index =
PcoVec::<Height, TxIndex>::forced_import(&db, "first_index", Version::ONE).unwrap();
let mut values =
PcoVec::<TxIndex, StoredU32>::forced_import(&db, "values", Version::ONE).unwrap();
first_index.push(TxIndex::ZERO);
values.push(StoredU32::from(1_u32));
values.push(StoredU32::from(2_u32));
first_index.stamped_write(Stamp::from(0_u64)).unwrap();
assert_eq!(
next_index(&first_index, &values, Height::new(1)),
Some(TxIndex::new(2))
);
assert_eq!(next_index(&first_index, &values, Height::ZERO), None);
}
}
+484 -142
View File
@@ -1,21 +1,20 @@
#![doc = include_str!("../README.md")]
use std::{
fs,
path::{Path, PathBuf},
fs::{self, File},
io::ErrorKind,
path::Path,
thread,
time::{Duration, Instant},
};
use brk_error::Result;
use brk_reader::{Reader, XORBytes};
use brk_rpc::Client;
use brk_types::{BlockHash, Height};
use fjall::PersistMode;
use rayon::prelude::*;
use brk_error::{Error, Result};
use brk_reader::{Reader, XOR_LEN, XORBytes};
use brk_types::{BlkPosition, BlockHash, Height};
use tracing::{debug, error, info};
use vecdb::{
Exit, RawDBError, ReadOnlyClone, ReadableVec, Ro, Rw, StorageMode, WritableVec, unlikely,
AnyVec, Exit, RawDBError, ReadOnlyClone, ReadableVec, Ro, Rw, StorageMode, WritableVec,
unlikely,
};
mod constants;
mod lengths;
@@ -26,22 +25,109 @@ mod stores;
mod vecs;
use constants::*;
use lengths::IndexerLengths as _;
use processor::{BlockBuffers, BlockProcessor};
use readers::Readers;
use stores::IndexerStores as _;
use vecs::{IndexerVecs as _, TransactionCounts, TxFeatureFlags};
pub use lengths::Lengths;
pub use safe_lengths::SafeLengths;
pub use stores::Stores;
pub use vecs::*;
pub use vecs::{
AddrTypeVecs, AddrsVecs, BlocksVecs, InputsVecs, OpReturnVecs, OutputsVecs, ScriptTypeVecs,
ScriptTypeWithSigOpsVecs, ScriptsVecs, TransactionCountVecs, TransactionFeaturesVecs,
TransactionsVecs, TxMetadataVecs, Vecs,
};
use safe_lengths::SafeLengths;
pub struct Indexer<M: StorageMode = Rw> {
path: PathBuf,
pub vecs: Vecs<M>,
pub stores: Stores,
inner: IndexerInner<M>,
}
struct IndexerInner<M: StorageMode> {
reader: Reader,
vecs: Vecs<M>,
stores: Stores,
buffers: BlockBuffers,
safe_lengths: SafeLengths,
}
enum ImportValidation {
Valid(Lengths),
Reset(&'static str),
}
enum XorMarker {
Missing,
Invalid(usize),
Valid(XORBytes),
}
fn is_export_height(height: Height) -> bool {
height != 0 && height % SNAPSHOT_BLOCK_RANGE == 0
}
fn final_export_height(completed_height: Option<Height>) -> Option<Height> {
completed_height.filter(|height| !is_export_height(*height))
}
fn read_xor_marker(path: &Path) -> Result<XorMarker> {
let bytes = match fs::read(path.join("xor.dat")) {
Ok(bytes) => bytes,
Err(err) if err.kind() == ErrorKind::NotFound => return Ok(XorMarker::Missing),
Err(err) => return Err(err.into()),
};
Ok(match <[u8; XOR_LEN]>::try_from(bytes) {
Ok(bytes) => XorMarker::Valid(XORBytes::from(bytes)),
Err(bytes) => XorMarker::Invalid(bytes.len()),
})
}
fn validate_reader_source(reader: &Reader) -> Result<()> {
let current = match read_xor_marker(reader.blocks_dir())? {
XorMarker::Missing => XORBytes::from([0; XOR_LEN]),
XorMarker::Invalid(received) => {
return Err(Error::WrongLength {
expected: XOR_LEN,
received,
});
}
XorMarker::Valid(xor) => xor,
};
if current != reader.xor_bytes() {
return Err(Error::Internal(
"Block source changed after the Reader was created",
));
}
Ok(())
}
fn write_xor_marker(path: &Path, source_xor: XORBytes) -> Result<()> {
fs::create_dir_all(path)?;
let pending = path.join("xor.pending");
fs::write(&pending, *source_xor)?;
File::open(&pending)?.sync_all()?;
fs::rename(&pending, path.join("xor.dat"))?;
File::open(path)?.sync_all()?;
Ok(())
}
fn read_block_hash_at(reader: &Reader, position: BlkPosition) -> Result<BlockHash> {
let bytes = reader.read_raw_bytes(position, bitcoin::block::Header::SIZE)?;
let header: bitcoin::block::Header = bitcoin::consensus::deserialize(&bytes)?;
Ok(BlockHash::from(header.block_hash()))
}
fn recreate_indexed_dir(path: &Path, source_xor: XORBytes) -> Result<()> {
match fs::remove_dir_all(path) {
Ok(()) => {}
Err(err) if err.kind() == ErrorKind::NotFound => {}
Err(err) => return Err(err.into()),
}
write_xor_marker(path, source_xor)
}
impl<M: StorageMode> Indexer<M> {
/// Tip block hash at the pipeline-safe ceiling.
///
@@ -56,6 +142,7 @@ impl<M: StorageMode> Indexer<M> {
pub fn tip_blockhash(&self) -> BlockHash {
match self.safe_lengths().height.decremented() {
Some(h) => self
.inner
.vecs
.blocks
.blockhash
@@ -69,24 +156,65 @@ impl<M: StorageMode> Indexer<M> {
/// advance and lower this internally; readers clamp non-series
/// answers against this loaded snapshot.
pub fn safe_lengths(&self) -> Lengths {
self.safe_lengths.load()
self.inner.safe_lengths.load()
}
pub fn reader(&self) -> &Reader {
&self.inner.reader
}
#[inline]
pub fn vecs(&self) -> &Vecs<M> {
&self.inner.vecs
}
#[inline]
pub fn stores(&self) -> &Stores {
&self.inner.stores
}
}
impl Indexer<Ro> {
/// Live indexer stamp for diagnostics. For data reads use
/// [`crate::SafeLengths::load`] (via `Query::height`).
/// `SafeLengths::load` (via `Query::height`).
pub fn indexed_height(&self) -> Height {
Height::from(self.vecs.blocks.blockhash.inner.stamp())
Height::from(self.inner.vecs.blocks.blockhash.inner.stamp())
}
}
impl Indexer {
pub fn forced_import(outputs_dir: &Path) -> Result<Self> {
Self::forced_import_inner(outputs_dir, true)
/// Imports and validates an indexer for writing against `reader`.
///
/// Any reset happens before this function returns, after all handles from
/// the failed import attempt have been dropped.
pub fn import(outputs_dir: &Path, reader: &Reader) -> Result<Self> {
Ok(Self {
inner: IndexerInner::import(outputs_dir, reader)?,
})
}
fn forced_import_inner(outputs_dir: &Path, can_retry: bool) -> Result<Self> {
pub fn index(&mut self, exit: &Exit) -> Result<()> {
self.inner.index(exit, false)
}
pub fn checked_index(&mut self, exit: &Exit) -> Result<()> {
self.inner.index(exit, true)
}
/// Publish disk state as the new safe-lengths snapshot. Drains pending
/// bg ingest first so stores are queryable at the new bound.
pub fn advance_safe_lengths(&mut self) -> Result<()> {
self.inner.advance_safe_lengths()
}
}
impl IndexerInner<Rw> {
fn import(outputs_dir: &Path, reader: &Reader) -> Result<Self> {
validate_reader_source(reader)?;
Self::import_inner(outputs_dir, reader, true)
}
fn import_inner(outputs_dir: &Path, reader: &Reader, can_retry: bool) -> Result<Self> {
info!("Importing indexer...");
let indexed_path = outputs_dir.join("indexed");
@@ -100,74 +228,132 @@ impl Indexer {
let stores = Stores::forced_import(&indexed_path, VERSION)?;
info!("Imported stores in {:?}", i.elapsed());
let safe_lengths = SafeLengths::new();
if let Some(lengths) = Lengths::from_local(&vecs, &stores) {
safe_lengths.advance(lengths);
}
Ok(Self {
path: indexed_path.clone(),
reader: reader.clone(),
vecs,
stores,
buffers: BlockBuffers::default(),
safe_lengths,
safe_lengths: SafeLengths::new(),
})
};
match try_import() {
Ok(result) => Ok(result),
let mut indexer = match try_import() {
Ok(indexer) => indexer,
Err(err) if err.is_lock_error() => {
// Lock errors are transient - another process has the database open.
// Don't delete data, just return the error.
Err(err)
return Err(err);
}
Err(err) if can_retry && err.is_data_error() => {
// Data corruption or version mismatch - safe to delete and retry
// The failed attempt has returned, so all of its local database
// handles have been dropped before the directory is removed.
info!("{err:?}, deleting {indexed_path:?} and retrying");
fs::remove_dir_all(&indexed_path)?;
Self::forced_import_inner(outputs_dir, false)
recreate_indexed_dir(&indexed_path, reader.xor_bytes())?;
return Self::import_inner(outputs_dir, reader, false);
}
Err(err) => Err(err),
Err(err) => return Err(err),
};
match indexer.validate_import(&indexed_path)? {
ImportValidation::Valid(lengths) => {
indexer.rollback_to(&lengths)?;
indexer.safe_lengths.advance(lengths);
Ok(indexer)
}
ImportValidation::Reset(reason) if can_retry => {
info!("{reason}, deleting {indexed_path:?} and retrying");
drop(indexer);
recreate_indexed_dir(&indexed_path, reader.xor_bytes())?;
Self::import_inner(outputs_dir, reader, false)
}
ImportValidation::Reset(reason) => Err(Error::Internal(reason)),
}
}
/// Fully resets the indexer by deleting stores from disk and reimporting.
/// Unlike stores.reset() which uses keyspace.clear() (leaving a journal
/// record that gets replayed on every recovery), this cleanly recreates.
fn full_reset(&mut self) -> Result<()> {
info!("Full reset...");
self.buffers.reset();
self.safe_lengths.reset();
self.vecs.reset()?;
let stores_path = self.path.join("stores");
fs::remove_dir_all(&stores_path).ok();
self.stores = Stores::forced_import(&self.path, VERSION)?;
Ok(())
fn validate_import(&self, indexed_path: &Path) -> Result<ImportValidation> {
let reader = &self.reader;
let vec_height = self.vecs.next_height();
let store_height = self.stores.next_height()?;
let is_empty = vec_height.is_zero() && store_height == Some(Height::ZERO);
let local_lengths = if is_empty {
Lengths::default()
} else if let Some(lengths) = Lengths::from_local(&self.vecs, &self.stores)? {
lengths
} else {
return Ok(ImportValidation::Reset(
"Indexer checkpoints are missing, inconsistent, or incomplete",
));
};
match read_xor_marker(indexed_path)? {
XorMarker::Missing if is_empty => write_xor_marker(indexed_path, reader.xor_bytes())?,
XorMarker::Valid(marker) if marker == reader.xor_bytes() => {}
XorMarker::Missing | XorMarker::Invalid(_) | XorMarker::Valid(_) => {
return Ok(ImportValidation::Reset(
"Indexer block source marker is missing, invalid, or changed",
));
}
}
let Some(hash) = self.vecs.blocks.blockhash.collect_last() else {
return Ok(ImportValidation::Valid(local_lengths));
};
let tip_height = Height::from(self.vecs.blocks.blockhash.len() - 1);
let Some(position) = self.vecs.blocks.position.collect_one(tip_height) else {
return Ok(ImportValidation::Reset(
"Indexer tip block position is missing",
));
};
if read_block_hash_at(reader, position)? != hash {
return Ok(ImportValidation::Reset(
"Indexer block positions belong to a different block source",
));
}
reader.client().wait_for_synced_node()?;
let (height, _) = reader.client().get_closest_valid_height(hash)?;
match Lengths::resume_at(height.incremented(), &self.vecs, &self.stores)? {
Some(lengths) => Ok(ImportValidation::Valid(lengths)),
None => Ok(ImportValidation::Reset(
"Indexer state cannot resume from the active chain",
)),
}
}
pub fn index(&mut self, reader: &Reader, client: &Client, exit: &Exit) -> Result<()> {
self.index_(reader, client, exit, false)
fn rollback_to(&mut self, starting_lengths: &Lengths) -> Result<()> {
let local_height = self.vecs.next_height();
if local_height == starting_lengths.height {
return Ok(());
}
if local_height < starting_lengths.height {
return Err(Error::Internal("Cannot roll back beyond local state"));
}
let completed_height = starting_lengths
.height
.decremented()
.ok_or(Error::Internal("Cannot roll back before genesis"))?;
self.stores
.rollback_if_needed(&self.vecs, starting_lengths)?;
self.vecs.rollback_if_needed(starting_lengths)?;
let checkpoint = self.stores.begin_commit(completed_height)?;
let persisted = self.stores.persist(checkpoint)?;
self.vecs.flush(completed_height)?;
persisted.publish()
}
pub fn checked_index(&mut self, reader: &Reader, client: &Client, exit: &Exit) -> Result<()> {
self.index_(reader, client, exit, true)
}
fn index_(
&mut self,
reader: &Reader,
client: &Client,
exit: &Exit,
check_collisions: bool,
) -> Result<()> {
self.vecs.db.sync_bg_tasks()?;
self.check_xor_bytes(reader)?;
fn index(&mut self, exit: &Exit, check_collisions: bool) -> Result<()> {
let reader = self.reader.clone();
validate_reader_source(&reader)?;
let client = reader.client();
self.vecs.sync_bg_tasks()?;
debug!("Starting indexing...");
let last_blockhash = self.vecs.blocks.blockhash.collect_last();
// Rollback sim
// Rollback sim: do not remove
// let last_blockhash = self
// .vecs
// .blocks
@@ -177,7 +363,7 @@ impl Indexer {
let (starting_lengths, prev_hash) = if let Some(hash) = last_blockhash {
let (height, hash) = client.get_closest_valid_height(hash)?;
match Lengths::resume_at(height.incremented(), &self.vecs, &self.stores) {
match Lengths::resume_at(height.incremented(), &self.vecs, &self.stores)? {
Some(starting_lengths) => {
if starting_lengths.height > client.get_last_height()? {
info!("Up to date, nothing to index.");
@@ -186,9 +372,9 @@ impl Indexer {
(starting_lengths, Some(hash))
}
None => {
info!("Data inconsistency detected, resetting indexer...");
self.full_reset()?;
(Lengths::default(), None)
return Err(Error::Internal(
"Indexer became inconsistent after import; drop and re-import it",
));
}
}
} else {
@@ -198,44 +384,43 @@ impl Indexer {
let lock = exit.lock();
self.safe_lengths.lower_before(&starting_lengths);
self.stores
.rollback_if_needed(&mut self.vecs, &starting_lengths)?;
debug!("Rollback stores done.");
self.vecs.rollback_if_needed(&starting_lengths)?;
debug!("Rollback vecs done.");
self.rollback_to(&starting_lengths)?;
debug!("Rollback done.");
drop(lock);
self.buffers.continue_from(prev_hash);
let mut lengths = starting_lengths;
let mut completed_height = None;
let is_export_height =
|height: Height| -> bool { height != 0 && height % SNAPSHOT_BLOCK_RANGE == 0 };
let export = move |stores: &mut Stores, vecs: &mut Vecs, height: Height| -> Result<()> {
info!("Exporting...");
let i = Instant::now();
let _lock = exit.lock();
thread::scope(|s| -> Result<()> {
let stores_res = s.spawn(|| -> Result<()> {
let i = Instant::now();
stores.commit(height)?;
debug!("Stores exported in {:?}", i.elapsed());
let export =
move |stores: &mut Stores, vecs: &mut Vecs, completed_height: Height| -> Result<()> {
info!("Exporting...");
let i = Instant::now();
let _lock = exit.lock();
let checkpoint = stores.begin_commit(completed_height)?;
thread::scope(|s| -> Result<()> {
let stores_res = s.spawn(|| {
let i = Instant::now();
let persisted = stores.persist(checkpoint)?;
debug!("Stores persisted in {:?}", i.elapsed());
Ok::<_, brk_error::Error>(persisted)
});
let vecs_res = s.spawn(|| -> Result<()> {
let i = Instant::now();
vecs.flush(completed_height)?;
debug!("Vecs exported in {:?}", i.elapsed());
Ok(())
});
let persisted = stores_res.join().unwrap()?;
vecs_res.join().unwrap()?;
// The shared checkpoint is visible only after both databases are durable.
persisted.publish()?;
Ok(())
});
let vecs_res = s.spawn(|| -> Result<()> {
let i = Instant::now();
vecs.flush(height)?;
debug!("Vecs exported in {:?}", i.elapsed());
Ok(())
});
stores_res.join().unwrap()?;
vecs_res.join().unwrap()?;
})?;
info!("Exported in {:?}", i.elapsed());
Ok(())
})?;
info!("Exported in {:?}", i.elapsed());
Ok(())
};
};
let mut readers = Readers::new(&self.vecs);
@@ -303,6 +488,7 @@ impl Indexer {
.lengths
.add_block(tx_count, input_count, output_count);
buffers.finish_block(*block.hash());
completed_height = Some(height);
if is_export_height(height) {
drop(readers);
@@ -313,62 +499,40 @@ impl Indexer {
drop(readers);
let lock = exit.lock();
let tasks = self.stores.take_all_pending_ingests(lengths.height)?;
self.vecs.stamped_write(lengths.height)?;
let fjall_db = self.stores.db.clone();
let Some(completed_height) = final_export_height(completed_height) else {
return Ok(());
};
self.vecs.db.run_bg(move |db| {
let lock = exit.lock();
let deferred_commit = self.stores.take_deferred_commit(completed_height)?;
self.vecs.stamped_write(completed_height)?;
self.vecs.run_bg(move |db| {
let _lock = lock;
db.bg_sleep(Duration::from_secs(3));
info!("Exporting...");
let i = Instant::now();
let total_i = Instant::now();
if !tasks.is_empty() {
let i = Instant::now();
tasks
.into_par_iter()
.try_for_each(|task| task().map_err(vecdb::RawDBError::other))?;
debug!("Stores committed in {:?}", i.elapsed());
let i = Instant::now();
fjall_db
.persist(PersistMode::SyncData)
.map_err(RawDBError::other)?;
debug!("Stores persisted in {:?}", i.elapsed());
}
let commit_i = Instant::now();
let persisted = deferred_commit.persist().map_err(RawDBError::other)?;
debug!("Stores persisted in {:?}", commit_i.elapsed());
db.compact()?;
// Keep the checkpoint invalid until the vector write is durable too.
persisted.publish().map_err(RawDBError::other)?;
info!("Exported in {:?}", i.elapsed());
info!("Exported in {:?}", total_i.elapsed());
Ok(())
});
Ok(())
}
fn check_xor_bytes(&mut self, reader: &Reader) -> Result<()> {
let current = reader.xor_bytes();
let cached = XORBytes::from(self.path.as_path());
if cached == current {
return Ok(());
}
self.full_reset()?;
fs::write(self.path.join("xor.dat"), *current)?;
Ok(())
}
/// Publish disk state as the new safe-lengths snapshot. Drains pending
/// bg ingest first so stores are queryable at the new bound.
pub fn advance_safe_lengths(&mut self) -> Result<()> {
self.vecs.db.sync_bg_tasks()?;
if let Some(lengths) = Lengths::from_local(&self.vecs, &self.stores) {
fn advance_safe_lengths(&mut self) -> Result<()> {
self.vecs.sync_bg_tasks()?;
if let Some(lengths) = Lengths::from_local(&self.vecs, &self.stores)? {
self.safe_lengths.advance(lengths);
}
Ok(())
@@ -380,11 +544,189 @@ impl ReadOnlyClone for Indexer {
fn read_only_clone(&self) -> Indexer<Ro> {
Indexer {
path: self.path.clone(),
vecs: self.vecs.read_only_clone(),
stores: self.stores.clone(),
buffers: BlockBuffers::default(),
safe_lengths: self.safe_lengths.clone(),
inner: IndexerInner {
reader: self.inner.reader.clone(),
vecs: self.inner.vecs.read_only_clone(),
stores: self.inner.stores.clone(),
buffers: BlockBuffers::default(),
safe_lengths: self.inner.safe_lengths.clone(),
},
}
}
}
#[cfg(test)]
mod import_tests {
use super::*;
use brk_rpc::{Auth, Client};
use brk_types::BlockHashPrefix;
fn empty_reader(path: &Path) -> Reader {
let client = Client::new("http://127.0.0.1:1", Auth::None).unwrap();
Reader::new_without_rlimit(path.join("blocks"), &client)
}
#[test]
fn final_export_requires_an_unsnapshotted_completed_block() {
let snapshot_height = Height::from(SNAPSHOT_BLOCK_RANGE);
assert_eq!(final_export_height(None), None);
assert_eq!(final_export_height(Some(Height::ZERO)), Some(Height::ZERO));
assert_eq!(final_export_height(Some(snapshot_height)), None);
assert_eq!(
final_export_height(Some(snapshot_height.incremented())),
Some(snapshot_height.incremented())
);
}
#[test]
fn recreate_drops_old_contents_and_seeds_source_identity() {
let dir = tempfile::tempdir().unwrap();
let indexed = dir.path().join("indexed");
fs::create_dir_all(&indexed).unwrap();
fs::write(indexed.join("stale"), b"stale").unwrap();
let source_xor = XORBytes::from([7_u8; 8]);
recreate_indexed_dir(&indexed, source_xor).unwrap();
assert!(!indexed.join("stale").exists());
assert!(matches!(
read_xor_marker(&indexed).unwrap(),
XorMarker::Valid(marker) if marker == source_xor
));
}
#[test]
fn empty_import_writes_identity_marker() -> Result<()> {
let dir = tempfile::tempdir()?;
let reader = empty_reader(dir.path());
drop(Indexer::import(dir.path(), &reader)?);
assert!(matches!(
read_xor_marker(&dir.path().join("indexed"))?,
XorMarker::Valid(marker) if marker == XORBytes::from([0; XOR_LEN])
));
Ok(())
}
#[test]
fn malformed_xor_marker_recreates_the_index() -> Result<()> {
let dir = tempfile::tempdir()?;
let indexed = dir.path().join("indexed");
let reader = empty_reader(dir.path());
drop(Indexer::import(dir.path(), &reader)?);
fs::write(indexed.join("xor.dat"), [0_u8; 3])?;
fs::write(indexed.join("stale"), b"stale")?;
drop(Indexer::import(dir.path(), &reader)?);
assert!(!indexed.join("stale").exists());
assert!(matches!(
read_xor_marker(&indexed)?,
XorMarker::Valid(marker) if marker == reader.xor_bytes()
));
Ok(())
}
#[test]
fn malformed_source_xor_never_deletes_data() -> Result<()> {
let dir = tempfile::tempdir()?;
let indexed = dir.path().join("indexed");
let reader = empty_reader(dir.path());
drop(Indexer::import(dir.path(), &reader)?);
fs::write(indexed.join("stale"), b"stale")?;
fs::create_dir_all(dir.path().join("blocks"))?;
fs::write(dir.path().join("blocks/xor.dat"), [0_u8; 3])?;
let reader = empty_reader(dir.path());
assert!(Indexer::import(dir.path(), &reader).is_err());
assert!(indexed.join("stale").exists());
Ok(())
}
#[test]
fn xor_marker_io_error_never_deletes_data() -> Result<()> {
let dir = tempfile::tempdir()?;
let indexed = dir.path().join("indexed");
let marker = indexed.join("xor.dat");
let reader = empty_reader(dir.path());
drop(Indexer::import(dir.path(), &reader)?);
fs::remove_file(&marker)?;
fs::create_dir(&marker)?;
fs::write(indexed.join("stale"), b"stale")?;
assert!(Indexer::import(dir.path(), &reader).is_err());
assert!(indexed.join("stale").exists());
Ok(())
}
#[test]
fn checkpoint_io_error_never_deletes_data() -> Result<()> {
let dir = tempfile::tempdir()?;
let indexed = dir.path().join("indexed");
let checkpoint = indexed.join("stores/height");
let reader = empty_reader(dir.path());
drop(Indexer::import(dir.path(), &reader)?);
fs::remove_file(&checkpoint)?;
fs::create_dir(&checkpoint)?;
fs::write(indexed.join("stale"), b"stale")?;
assert!(Indexer::import(dir.path(), &reader).is_err());
assert!(indexed.join("stale").exists());
Ok(())
}
#[test]
fn block_position_is_verified_against_its_header() -> Result<()> {
let dir = tempfile::tempdir()?;
let blocks = dir.path().join("blocks");
fs::create_dir(&blocks)?;
let genesis = bitcoin::blockdata::constants::genesis_block(bitcoin::Network::Bitcoin);
fs::write(
blocks.join("blk00000.dat"),
bitcoin::consensus::serialize(&genesis.header),
)?;
let reader = empty_reader(dir.path());
assert_eq!(
read_block_hash_at(&reader, BlkPosition::new(0, 0))?,
BlockHash::from(genesis.block_hash())
);
Ok(())
}
#[test]
fn fjall_lock_never_triggers_deletion() {
let error = Error::from(fjall::Error::Locked);
assert!(error.is_lock_error());
assert!(!error.is_data_error());
}
#[test]
fn invalid_checkpoint_drops_handles_and_recreates_entire_index() -> Result<()> {
let dir = tempfile::tempdir()?;
let indexed = dir.path().join("indexed");
let reader = empty_reader(dir.path());
{
let mut indexer = Indexer::import(dir.path(), &reader)?;
indexer
.inner
.stores
.insert_block_height(BlockHashPrefix::from(1_u64), Height::ZERO);
let checkpoint = indexer.inner.stores.begin_commit(Height::ZERO)?;
let persisted = indexer.inner.stores.persist(checkpoint)?;
drop(persisted);
}
fs::write(indexed.join("stale"), b"stale")?;
let indexer = Indexer::import(dir.path(), &reader)?;
assert!(!indexed.join("stale").exists());
assert_eq!(indexer.vecs().next_height(), Height::ZERO);
assert_eq!(indexer.stores().next_height()?, Some(Height::ZERO));
Ok(())
}
}
@@ -4,6 +4,7 @@ use tracing::error;
use vecdb::{WritableVec, unlikely};
use super::{BlockProcessor, transaction::ComputedTx};
use crate::stores::IndexerStores as _;
impl BlockProcessor<'_> {
pub(crate) fn process_block_metadata(&mut self) -> Result<()> {
@@ -14,9 +15,8 @@ impl BlockProcessor<'_> {
if unlikely(self.check_collisions)
&& self
.stores
.blockhash_prefix_to_height
.get(&blockhash_prefix)?
.is_some_and(|prev_height| *prev_height != height)
.block_height(&blockhash_prefix)?
.is_some_and(|prev_height| prev_height != height)
{
error!("BlockHash: {blockhash}");
return Err(Error::Internal("BlockHash prefix collision"));
@@ -24,9 +24,7 @@ impl BlockProcessor<'_> {
self.lengths.push(self.vecs);
self.stores
.blockhash_prefix_to_height
.insert(blockhash_prefix, height);
self.stores.insert_block_height(blockhash_prefix, height);
self.vecs
.blocks
@@ -21,9 +21,4 @@ impl BlockBuffers {
pub(crate) fn finish_block(&mut self, blockhash: BlockHash) {
self.tip = Some(blockhash);
}
pub(crate) fn reset(&mut self) {
self.addresses.clear_cache();
self.tip = None;
}
}
@@ -8,8 +8,11 @@ use rayon::prelude::*;
use tracing::error;
use vecdb::{AnyVec, WritableVec, likely, unlikely};
use crate::constants::DUPLICATE_TXIDS;
use crate::{TransactionCounts, TransactionFeaturesVecs, TxMetadataVecs};
use crate::{
TransactionCounts, TransactionFeaturesVecs, TxMetadataVecs,
constants::DUPLICATE_TXIDS,
stores::{IndexerStores as _, TransactionStoresMut},
};
pub(super) use computed::ComputedTx;
@@ -39,11 +42,7 @@ impl<'a> BlockProcessor<'a> {
true
} else {
let txid_prefix = TxidPrefix::from(&txid);
let prev_tx_index = self
.stores
.txid_prefix_to_tx_index
.get(&txid_prefix)?
.map(|value| *value);
let prev_tx_index = self.stores.tx_index(&txid_prefix)?;
if let Some(prev_tx_index) = prev_tx_index {
self.validate_txid_collision(tx_index, prev_tx_index)?;
@@ -125,10 +124,12 @@ impl<'a> BlockProcessor<'a> {
let transaction_features = &mut self.vecs.transaction_features;
let height = self.height;
let addr_hash_stores = &mut self.stores.addr_type_to_addr_hash_to_addr_index;
let addr_tx_index_stores = &mut self.stores.addr_type_to_addr_index_and_tx_index;
let addr_outpoint_stores = &mut self.stores.addr_type_to_addr_index_and_unspent_outpoint;
let txid_prefix_store = &mut self.stores.txid_prefix_to_tx_index;
let TransactionStoresMut {
addr_hashes,
addr_tx_indexes,
addr_unspent_outpoints,
txid_prefixes,
} = self.stores.transaction_stores_mut();
rayon::join(
|| {
@@ -141,9 +142,9 @@ impl<'a> BlockProcessor<'a> {
addrs,
scripts,
op_return,
addr_hash_stores,
addr_tx_index_stores,
addr_outpoint_stores,
addr_hashes,
addr_tx_indexes,
addr_unspent_outpoints,
&mut txouts,
addresses,
);
@@ -153,8 +154,8 @@ impl<'a> BlockProcessor<'a> {
base_txin_index,
first_txin_index,
inputs,
addr_tx_index_stores,
addr_outpoint_stores,
addr_tx_indexes,
addr_unspent_outpoints,
txins,
&txouts,
);
@@ -164,7 +165,7 @@ impl<'a> BlockProcessor<'a> {
height,
txs,
transaction_analyses,
txid_prefix_store,
txid_prefixes,
&mut tx_metadata,
transaction_features,
)
@@ -241,11 +241,7 @@ impl ReadBatch {
.zip(previous_parent_prefixes.par_iter())
.try_for_each(|read| {
let (read, txid_prefix) = read;
let store_result = processor
.stores
.txid_prefix_to_tx_index
.get(txid_prefix)?
.map(|value| *value);
let store_result = processor.stores.tx_index(txid_prefix)?;
let tx_index = match store_result {
Some(tx_index) if tx_index < current_tx_index => tx_index,
@@ -71,16 +71,14 @@ impl BlockAddresses {
self.lookups
.sort_unstable_by_key(|lookup| (lookup.output_type, lookup.hash));
let stores = &processor.stores.addr_type_to_addr_hash_to_addr_index;
let lengths = &*processor.lengths;
self.lookups
.par_iter_mut()
.try_for_each(|lookup| -> Result<()> {
lookup.type_index = stores
.get_unwrap(lookup.output_type)
.get(&lookup.hash)?
.map(|type_index| *type_index)
lookup.type_index = processor
.stores
.addr_index(lookup.output_type, &lookup.hash)?
.filter(|type_index| *type_index < lengths.to_type_index(lookup.output_type));
Ok(())
})?;
-4
View File
@@ -20,10 +20,6 @@ impl SafeLengths {
self.0.read().clone()
}
pub fn reset(&self) {
*self.0.write() = Lengths::default();
}
pub fn advance(&self, next: Lengths) {
let mut g = self.0.write();
debug_assert!(
+272 -87
View File
@@ -1,54 +1,161 @@
use std::{fs, path::Path, time::Instant};
use std::{fs, ops::Range, path::Path, time::Instant};
use rustc_hash::FxHashSet;
use brk_cohort::ByAddrType;
use brk_error::{Error, Result};
use brk_store::{AnyStore, Kind, Mode, Store};
use brk_error::{Error, OptionData, Result};
use brk_store::{AnyStore, Kind, Mode, PendingIngest, Store};
use brk_types::{
AddrHash, AddrIndexOutPoint, AddrIndexTxIndex, BlockHashPrefix, Height, OutPoint, OutputType,
TxIndex, TxOutIndex, TxidPrefix, TypeIndex, Unit, Version, Vout,
};
use fjall::{Database, PersistMode};
use fjall::Database;
use rayon::prelude::*;
use tracing::{debug, info};
use tracing::debug;
use vecdb::{AnyVec, ReadableVec, VecIndex};
use crate::{Lengths, constants::DUPLICATE_TXID_PREFIXES};
use crate::{Lengths, constants::DUPLICATE_TXID_PREFIXES, vecs::IndexerVecs as _};
use super::Vecs;
mod checkpoint;
use checkpoint::{
DeferredStoresCommit, PendingStoresCheckpoint, PersistedStoresCheckpoint, StoresCheckpoint,
};
#[derive(Clone)]
pub struct Stores {
pub db: Database,
inner: StoresInner,
}
pub addr_type_to_addr_hash_to_addr_index: ByAddrType<Store<AddrHash, TypeIndex>>,
pub addr_type_to_addr_index_and_tx_index: ByAddrType<Store<AddrIndexTxIndex, Unit>>,
pub addr_type_to_addr_index_and_unspent_outpoint: ByAddrType<Store<AddrIndexOutPoint, Unit>>,
pub blockhash_prefix_to_height: Store<BlockHashPrefix, Height>,
pub txid_prefix_to_tx_index: Store<TxidPrefix, TxIndex>,
#[derive(Clone)]
struct StoresInner {
db: Database,
checkpoint: StoresCheckpoint,
addr_type_to_addr_hash_to_addr_index: ByAddrType<Store<AddrHash, TypeIndex>>,
addr_type_to_addr_index_and_tx_index: ByAddrType<Store<AddrIndexTxIndex, Unit>>,
addr_type_to_addr_index_and_unspent_outpoint: ByAddrType<Store<AddrIndexOutPoint, Unit>>,
blockhash_prefix_to_height: Store<BlockHashPrefix, Height>,
txid_prefix_to_tx_index: Store<TxidPrefix, TxIndex>,
}
pub struct TransactionStoresMut<'a> {
pub addr_hashes: &'a mut ByAddrType<Store<AddrHash, TypeIndex>>,
pub addr_tx_indexes: &'a mut ByAddrType<Store<AddrIndexTxIndex, Unit>>,
pub addr_unspent_outpoints: &'a mut ByAddrType<Store<AddrIndexOutPoint, Unit>>,
pub txid_prefixes: &'a mut Store<TxidPrefix, TxIndex>,
}
pub trait IndexerStores: Sized {
fn forced_import(parent: &Path, version: Version) -> Result<Self>;
fn next_height(&self) -> Result<Option<Height>>;
fn begin_commit(&self, completed_height: Height) -> Result<PendingStoresCheckpoint>;
fn persist(&mut self, checkpoint: PendingStoresCheckpoint)
-> Result<PersistedStoresCheckpoint>;
fn take_deferred_commit(&mut self, completed_height: Height) -> Result<DeferredStoresCommit>;
fn rollback_if_needed(&mut self, vecs: &Vecs, starting_lengths: &Lengths) -> Result<()>;
fn insert_block_height(&mut self, prefix: BlockHashPrefix, height: Height);
fn transaction_stores_mut(&mut self) -> TransactionStoresMut<'_>;
}
impl Stores {
pub fn forced_import(parent: &Path, version: Version) -> Result<Self> {
Self::forced_import_inner(parent, version, true)
#[inline]
pub fn addr_index(&self, addr_type: OutputType, hash: &AddrHash) -> Result<Option<TypeIndex>> {
Ok(self
.inner
.addr_type_to_addr_hash_to_addr_index
.get(addr_type)
.data()?
.get(hash)?
.map(|index| index.into_owned()))
}
fn forced_import_inner(parent: &Path, version: Version, can_retry: bool) -> Result<Self> {
pub fn addr_hash_range(
&self,
addr_type: OutputType,
range: Range<AddrHash>,
) -> Result<impl DoubleEndedIterator<Item = (AddrHash, TypeIndex)> + '_> {
Ok(self
.inner
.addr_type_to_addr_hash_to_addr_index
.get(addr_type)
.data()?
.range(range))
}
pub fn addr_tx_indexes(
&self,
addr_type: OutputType,
addr_index: TypeIndex,
) -> Result<impl DoubleEndedIterator<Item = TxIndex> + '_> {
Ok(self
.inner
.addr_type_to_addr_index_and_tx_index
.get(addr_type)
.data()?
.prefix(addr_index)
.map(|(key, _)| key.tx_index()))
}
pub fn addr_tx_indexes_before(
&self,
addr_type: OutputType,
addr_index: TypeIndex,
before: TxIndex,
) -> Result<impl DoubleEndedIterator<Item = TxIndex> + '_> {
let min = AddrIndexTxIndex::min_for_addr(addr_index);
let cursor = AddrIndexTxIndex::from((addr_index, before));
Ok(self
.inner
.addr_type_to_addr_index_and_tx_index
.get(addr_type)
.data()?
.range(min..cursor)
.map(|(key, _)| key.tx_index()))
}
pub fn addr_unspent_outpoints(
&self,
addr_type: OutputType,
addr_index: TypeIndex,
) -> Result<impl DoubleEndedIterator<Item = (TxIndex, Vout)> + '_> {
Ok(self
.inner
.addr_type_to_addr_index_and_unspent_outpoint
.get(addr_type)
.data()?
.prefix(addr_index)
.map(|(key, _)| (key.tx_index(), key.vout())))
}
#[inline]
pub fn block_height(&self, prefix: &BlockHashPrefix) -> Result<Option<Height>> {
Ok(self
.inner
.blockhash_prefix_to_height
.get(prefix)?
.map(|height| height.into_owned()))
}
#[inline]
pub fn tx_index(&self, prefix: &TxidPrefix) -> Result<Option<TxIndex>> {
Ok(self
.inner
.txid_prefix_to_tx_index
.get(prefix)?
.map(|index| index.into_owned()))
}
}
impl StoresInner {
fn open(parent: &Path, version: Version) -> Result<Self> {
let pathbuf = parent.join("stores");
let path = pathbuf.as_path();
fs::create_dir_all(&pathbuf)?;
let database = match brk_store::open_database(path) {
Ok(database) => database,
Err(err) if can_retry => {
info!("Failed to open stores at {path:?}: {err:?}, deleting and retrying");
fs::remove_dir_all(path)?;
return Self::forced_import_inner(parent, version, false);
}
Err(err) => return Err(err.into()),
};
let database = brk_store::open_database(path)?;
let database_ref = &database;
@@ -87,6 +194,7 @@ impl Stores {
let stores = Self {
db: database.clone(),
checkpoint: StoresCheckpoint::new(path),
addr_type_to_addr_hash_to_addr_index: ByAddrType::new_with_index(
create_addr_hash_to_addr_index_store,
@@ -116,37 +224,15 @@ impl Stores {
)?,
};
if stores.checkpoint.next_height()?.is_none() && stores.is_empty()? {
stores.checkpoint.initialize_empty()?;
}
Ok(stores)
}
pub fn next_height(&self) -> Height {
self.iter_any()
.map(|store| store.height().map(Height::incremented).unwrap_or_default())
.min()
.unwrap()
}
fn iter_any(&self) -> impl Iterator<Item = &dyn AnyStore> {
[
&self.blockhash_prefix_to_height as &dyn AnyStore,
&self.txid_prefix_to_tx_index,
]
.into_iter()
.chain(
self.addr_type_to_addr_hash_to_addr_index
.values()
.map(|s| s as &dyn AnyStore),
)
.chain(
self.addr_type_to_addr_index_and_tx_index
.values()
.map(|s| s as &dyn AnyStore),
)
.chain(
self.addr_type_to_addr_index_and_unspent_outpoint
.values()
.map(|s| s as &dyn AnyStore),
)
fn checkpoint_height(&self) -> Result<Option<Height>> {
self.checkpoint.next_height()
}
fn par_iter_any_mut(&mut self) -> impl ParallelIterator<Item = &mut dyn AnyStore> {
@@ -172,32 +258,34 @@ impl Stores {
)
}
pub fn commit(&mut self, height: Height) -> Result<()> {
let i = Instant::now();
self.par_iter_any_mut()
.try_for_each(|store| store.commit(height))?;
debug!("Stores committed in {:?}", i.elapsed());
fn prepare_checkpoint(&self, completed_height: Height) -> Result<PendingStoresCheckpoint> {
self.checkpoint.begin(completed_height)
}
fn persist_checkpoint(
&mut self,
checkpoint: PendingStoresCheckpoint,
) -> Result<PersistedStoresCheckpoint> {
let db = self.db.clone();
let i = Instant::now();
self.db.persist(PersistMode::SyncData)?;
let persisted = checkpoint.persist(&db, || {
self.par_iter_any_mut()
.try_for_each(|store| store.ingest_pending())
})?;
debug!("Stores persisted in {:?}", i.elapsed());
Ok(())
Ok(persisted)
}
/// Takes all pending puts/dels from every store and returns closures
/// that can ingest them on a background thread.
#[allow(clippy::type_complexity)]
pub fn take_all_pending_ingests(
&mut self,
height: Height,
) -> Result<Vec<Box<dyn FnOnce() -> Result<()> + Send>>> {
let h = height;
fn take_pending_ingests(&mut self) -> Vec<PendingIngest> {
let mut tasks = Vec::new();
macro_rules! take {
($store:expr) => {
tasks.extend($store.take_pending_ingest(h)?);
tasks.extend($store.take_pending_ingest());
};
}
@@ -217,16 +305,21 @@ impl Stores {
take!(store);
}
Ok(tasks)
tasks
}
/// Rewrites reverse-key entries below the lowered bound. In-flight
/// readers may briefly see torn state.
pub fn rollback_if_needed(
&mut self,
vecs: &mut Vecs,
starting_lengths: &Lengths,
) -> Result<()> {
fn defer_commit(&mut self, completed_height: Height) -> Result<DeferredStoresCommit> {
let checkpoint = self.checkpoint.begin(completed_height)?;
let ingests = self.take_pending_ingests();
Ok(DeferredStoresCommit::new(
self.db.clone(),
ingests,
checkpoint,
))
}
/// Stages reverse-key entries below the lowered bound for persistence.
fn rollback(&mut self, vecs: &Vecs, starting_lengths: &Lengths) -> Result<()> {
if self.is_empty()? {
return Ok(());
}
@@ -239,11 +332,6 @@ impl Stores {
self.rollback_txids(vecs, starting_lengths);
self.rollback_outputs_and_inputs(vecs, starting_lengths)?;
let rollback_height = starting_lengths.height.decremented().unwrap_or_default();
self.par_iter_any_mut()
.try_for_each(|store| store.export_meta(rollback_height))?;
self.commit(rollback_height)?;
Ok(())
}
@@ -264,11 +352,7 @@ impl Stores {
.try_fold(true, |acc, s| s.is_empty().map(|empty| acc && empty))?)
}
fn rollback_block_metadata(
&mut self,
vecs: &mut Vecs,
starting_lengths: &Lengths,
) -> Result<()> {
fn rollback_block_metadata(&mut self, vecs: &Vecs, starting_lengths: &Lengths) -> Result<()> {
vecs.blocks.blockhash.for_each_range_at(
starting_lengths.height.to_usize(),
vecs.blocks.blockhash.len(),
@@ -289,7 +373,7 @@ impl Stores {
Ok(())
}
fn rollback_txids(&mut self, vecs: &mut Vecs, starting_lengths: &Lengths) {
fn rollback_txids(&mut self, vecs: &Vecs, starting_lengths: &Lengths) {
let start = starting_lengths.tx_index.to_usize();
let end = vecs.transactions.txid.len();
let mut current_index = start;
@@ -317,7 +401,7 @@ impl Stores {
fn rollback_outputs_and_inputs(
&mut self,
vecs: &mut Vecs,
vecs: &Vecs,
starting_lengths: &Lengths,
) -> Result<()> {
let tx_index_to_first_txout_index_reader = vecs.transactions.first_txout_index.reader();
@@ -414,6 +498,50 @@ impl Stores {
}
}
impl IndexerStores for Stores {
fn forced_import(parent: &Path, version: Version) -> Result<Self> {
Ok(Self {
inner: StoresInner::open(parent, version)?,
})
}
fn next_height(&self) -> Result<Option<Height>> {
self.inner.checkpoint_height()
}
fn begin_commit(&self, completed_height: Height) -> Result<PendingStoresCheckpoint> {
self.inner.prepare_checkpoint(completed_height)
}
fn persist(
&mut self,
checkpoint: PendingStoresCheckpoint,
) -> Result<PersistedStoresCheckpoint> {
self.inner.persist_checkpoint(checkpoint)
}
fn take_deferred_commit(&mut self, completed_height: Height) -> Result<DeferredStoresCommit> {
self.inner.defer_commit(completed_height)
}
fn rollback_if_needed(&mut self, vecs: &Vecs, starting_lengths: &Lengths) -> Result<()> {
self.inner.rollback(vecs, starting_lengths)
}
fn insert_block_height(&mut self, prefix: BlockHashPrefix, height: Height) {
self.inner.blockhash_prefix_to_height.insert(prefix, height);
}
fn transaction_stores_mut(&mut self) -> TransactionStoresMut<'_> {
TransactionStoresMut {
addr_hashes: &mut self.inner.addr_type_to_addr_hash_to_addr_index,
addr_tx_indexes: &mut self.inner.addr_type_to_addr_index_and_tx_index,
addr_unspent_outpoints: &mut self.inner.addr_type_to_addr_index_and_unspent_outpoint,
txid_prefixes: &mut self.inner.txid_prefix_to_tx_index,
}
}
}
fn valid_rollback_boundaries(
first_txout_indexes: &[TxOutIndex],
rollback_start: usize,
@@ -456,8 +584,65 @@ fn txout_ranges(
#[cfg(test)]
mod tests {
use fjall::PersistMode;
use super::*;
#[test]
fn empty_stores_initialize_zero_checkpoint() -> Result<()> {
let dir = tempfile::tempdir()?;
let stores = Stores::forced_import(dir.path(), Version::ZERO)?;
assert_eq!(stores.next_height()?, Some(Height::ZERO));
Ok(())
}
#[test]
fn missing_checkpoint_with_data_stays_invalid() -> Result<()> {
let dir = tempfile::tempdir()?;
{
let mut stores = Stores::forced_import(dir.path(), Version::ZERO)?;
let inner = &mut stores.inner;
inner
.blockhash_prefix_to_height
.insert(BlockHashPrefix::from(1_u64), Height::ZERO);
inner
.blockhash_prefix_to_height
.take_pending_ingest()
.unwrap()()?;
inner.db.persist(PersistMode::SyncData)?;
let pending_checkpoint = inner.checkpoint.begin(Height::ZERO)?;
drop(pending_checkpoint);
}
let reopened = Stores::forced_import(dir.path(), Version::ZERO)?;
assert_eq!(reopened.next_height()?, None);
Ok(())
}
#[test]
fn synchronous_commit_persists_data_and_checkpoint() -> Result<()> {
let dir = tempfile::tempdir()?;
let prefix = BlockHashPrefix::from(1_u64);
{
let mut stores = Stores::forced_import(dir.path(), Version::ZERO)?;
stores
.inner
.blockhash_prefix_to_height
.insert(prefix, Height::ZERO);
let checkpoint = stores.begin_commit(Height::new(42))?;
stores.persist(checkpoint)?.publish()?;
}
let reopened = Stores::forced_import(dir.path(), Version::ZERO)?;
assert_eq!(reopened.next_height()?, Some(Height::new(43)));
assert_eq!(reopened.block_height(&prefix)?, Some(Height::ZERO));
Ok(())
}
#[test]
fn rollback_output_ranges_reconstruct_tx_indexes_and_vouts() {
let first_txout_indexes = [100_usize, 103, 103, 105].map(TxOutIndex::from);
+296
View File
@@ -0,0 +1,296 @@
use std::{
fs::{self, File},
io::{self, ErrorKind},
path::{Path, PathBuf},
};
use brk_error::Result;
use brk_store::PendingIngest;
use brk_types::Height;
use fjall::{Database, PersistMode};
use rayon::prelude::*;
/// Sole durability marker for the shared stores database.
///
/// The file contains the next block height to index. A missing or malformed
/// file means a commit was interrupted and the stores must not be resumed.
#[derive(Debug, Clone)]
pub struct StoresCheckpoint {
path: PathBuf,
}
impl StoresCheckpoint {
pub fn new(stores_path: &Path) -> Self {
Self {
path: stores_path.join("height"),
}
}
pub fn next_height(&self) -> Result<Option<Height>> {
let bytes = match fs::read(&self.path) {
Ok(bytes) => bytes,
Err(err) if err.kind() == ErrorKind::NotFound => return Ok(None),
Err(err) => return Err(err.into()),
};
let Ok(bytes) = <[u8; size_of::<u32>()]>::try_from(bytes) else {
return Ok(None);
};
Ok(Some(Height::new(u32::from_le_bytes(bytes))))
}
pub fn initialize_empty(&self) -> Result<()> {
let pending_path = self.invalidate()?;
PersistedStoresCheckpoint(PendingStoresCheckpoint {
next_height: Height::ZERO,
path: self.path.clone(),
pending_path,
})
.publish()
}
pub fn begin(&self, completed_height: Height) -> Result<PendingStoresCheckpoint> {
let pending_path = self.invalidate()?;
Ok(PendingStoresCheckpoint {
next_height: completed_height.incremented(),
path: self.path.clone(),
pending_path,
})
}
fn invalidate(&self) -> Result<PathBuf> {
let pending_path = self.path.with_extension("pending");
let removed_checkpoint = remove_if_exists(&self.path)?;
let removed_pending = remove_if_exists(&pending_path)?;
if removed_checkpoint || removed_pending {
sync_parent(&self.path)?;
}
Ok(pending_path)
}
}
#[must_use = "dropping a pending checkpoint leaves the stores checkpoint invalid"]
pub struct PendingStoresCheckpoint {
next_height: Height,
path: PathBuf,
pending_path: PathBuf,
}
impl PendingStoresCheckpoint {
pub fn persist(
self,
db: &Database,
ingest: impl FnOnce() -> Result<()>,
) -> Result<PersistedStoresCheckpoint> {
ingest()?;
db.persist(PersistMode::SyncData)?;
Ok(PersistedStoresCheckpoint(self))
}
}
#[must_use = "publish this checkpoint after every related database is durable"]
pub struct PersistedStoresCheckpoint(PendingStoresCheckpoint);
impl PersistedStoresCheckpoint {
pub fn publish(self) -> Result<()> {
let pending = self.0;
pending.next_height.write(&pending.pending_path)?;
File::open(&pending.pending_path)?.sync_all()?;
fs::rename(&pending.pending_path, &pending.path)?;
sync_parent(&pending.path)?;
Ok(())
}
}
#[must_use = "persist this deferred commit before publishing its checkpoint"]
pub struct DeferredStoresCommit {
checkpoint: PendingStoresCheckpoint,
db: Database,
ingests: Vec<PendingIngest>,
}
impl DeferredStoresCommit {
pub fn new(
db: Database,
ingests: Vec<PendingIngest>,
checkpoint: PendingStoresCheckpoint,
) -> Self {
Self {
checkpoint,
db,
ingests,
}
}
pub fn persist(self) -> Result<PersistedStoresCheckpoint> {
self.checkpoint.persist(&self.db, || {
self.ingests.into_par_iter().try_for_each(|ingest| ingest())
})
}
}
fn remove_if_exists(path: &Path) -> io::Result<bool> {
match fs::remove_file(path) {
Ok(()) => Ok(true),
Err(err) if err.kind() == ErrorKind::NotFound => Ok(false),
Err(err) => Err(err),
}
}
fn sync_parent(path: &Path) -> io::Result<()> {
File::open(path.parent().expect("checkpoint has a parent"))?.sync_all()
}
#[cfg(test)]
mod tests {
use brk_error::Error;
use brk_store::{Kind, Mode, Store};
use brk_types::{AddrIndexTxIndex, TxIndex, TypeIndex, Unit, Version};
use super::*;
fn key(address: u32, transaction: u32) -> AddrIndexTxIndex {
AddrIndexTxIndex::from((TypeIndex::new(address), TxIndex::new(transaction)))
}
fn open_store(db: &Database, path: &Path, name: &str) -> Result<Store<AddrIndexTxIndex, Unit>> {
Store::import(db, path, name, Version::ZERO, Mode::Any, Kind::Vec)
}
#[test]
fn dropped_commit_leaves_checkpoint_invalid() -> Result<()> {
let dir = tempfile::tempdir()?;
let checkpoint = StoresCheckpoint::new(dir.path());
let db = brk_store::open_database(dir.path())?;
checkpoint
.begin(Height::new(41))?
.persist(&db, || Ok(()))?
.publish()?;
assert_eq!(checkpoint.next_height()?, Some(Height::new(42)));
let pending = checkpoint.begin(Height::new(42))?;
assert_eq!(checkpoint.next_height()?, None);
drop(pending);
let reopened = StoresCheckpoint::new(dir.path());
assert_eq!(reopened.next_height()?, None);
Ok(())
}
#[test]
fn failed_ingest_does_not_publish_checkpoint() -> Result<()> {
let dir = tempfile::tempdir()?;
let checkpoint = StoresCheckpoint::new(dir.path());
let pending = checkpoint.begin(Height::new(42))?;
let db = brk_store::open_database(dir.path())?;
let ingests: Vec<PendingIngest> = vec![Box::new(|| {
Err(Error::Internal("simulated ingest failure"))
})];
assert!(
DeferredStoresCommit::new(db, ingests, pending)
.persist()
.is_err()
);
assert_eq!(checkpoint.next_height()?, None);
Ok(())
}
#[test]
fn persisted_commit_is_not_published_early() -> Result<()> {
let dir = tempfile::tempdir()?;
let checkpoint = StoresCheckpoint::new(dir.path());
let pending = checkpoint.begin(Height::new(42))?;
let db = brk_store::open_database(dir.path())?;
let persisted = DeferredStoresCommit::new(db, vec![], pending).persist()?;
assert_eq!(checkpoint.next_height()?, None);
persisted.publish()?;
let reopened = StoresCheckpoint::new(dir.path());
assert_eq!(reopened.next_height()?, Some(Height::new(43)));
Ok(())
}
#[test]
fn dropped_deferred_ingest_reopens_without_value_or_checkpoint() -> Result<()> {
let dir = tempfile::tempdir()?;
let checkpoint = StoresCheckpoint::new(dir.path());
{
let db = brk_store::open_database(dir.path())?;
let mut store = open_store(&db, dir.path(), "dropped_deferred_ingest")?;
checkpoint
.begin(Height::new(41))?
.persist(&db, || Ok(()))?
.publish()?;
store.insert(key(1, 1), Unit);
let pending_checkpoint = checkpoint.begin(Height::new(42))?;
let pending_ingest = store.take_pending_ingest().unwrap();
drop(pending_ingest);
drop(pending_checkpoint);
}
let db = brk_store::open_database(dir.path())?;
let store = open_store(&db, dir.path(), "dropped_deferred_ingest")?;
assert_eq!(checkpoint.next_height()?, None);
assert!(store.get(&key(1, 1))?.is_none());
Ok(())
}
#[test]
fn successful_ingest_reopens_with_value_and_checkpoint() -> Result<()> {
let dir = tempfile::tempdir()?;
let checkpoint = StoresCheckpoint::new(dir.path());
{
let db = brk_store::open_database(dir.path())?;
let mut store = open_store(&db, dir.path(), "successful_ingest")?;
store.insert(key(1, 1), Unit);
let pending_checkpoint = checkpoint.begin(Height::new(42))?;
let pending_ingest = store.take_pending_ingest().unwrap();
DeferredStoresCommit::new(db.clone(), vec![pending_ingest], pending_checkpoint)
.persist()?
.publish()?;
}
let db = brk_store::open_database(dir.path())?;
let store = open_store(&db, dir.path(), "successful_ingest")?;
assert_eq!(checkpoint.next_height()?, Some(Height::new(43)));
assert!(store.get(&key(1, 1))?.is_some());
Ok(())
}
#[test]
fn empty_database_has_an_explicit_zero_checkpoint() -> Result<()> {
let dir = tempfile::tempdir()?;
let checkpoint = StoresCheckpoint::new(dir.path());
assert_eq!(checkpoint.next_height()?, None);
checkpoint.initialize_empty()?;
assert_eq!(checkpoint.next_height()?, Some(Height::ZERO));
Ok(())
}
#[test]
fn malformed_checkpoint_is_invalid_but_io_errors_propagate() -> Result<()> {
let dir = tempfile::tempdir()?;
let checkpoint = StoresCheckpoint::new(dir.path());
fs::write(&checkpoint.path, [0_u8; 3])?;
assert_eq!(checkpoint.next_height()?, None);
fs::remove_file(&checkpoint.path)?;
fs::create_dir(&checkpoint.path)?;
assert!(checkpoint.next_height().is_err());
Ok(())
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
/// Imports multiple items in parallel using thread::scope.
/// Each expression must return Result<T>.
/// Each expression must return `Result<T>`.
///
/// # Example
/// ```ignore
+74 -27
View File
@@ -4,11 +4,11 @@ use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{AddrHash, Height, OutputType, Version};
use rayon::prelude::*;
use vecdb::{AnyStoredVec, Database, Rw, Stamp, StorageMode};
use vecdb::{AnyStoredVec, AnyVec, Database, RawDBError, Rw, Stamp, StorageMode};
const PAGE_SIZE: usize = 4096;
use crate::parallel_import;
use crate::{Lengths, parallel_import};
mod addrs;
mod blocks;
@@ -27,12 +27,10 @@ pub use outputs::*;
pub use scripts::*;
pub use transactions::*;
use crate::Lengths;
#[derive(Traversable)]
pub struct Vecs<M: StorageMode = Rw> {
#[traversable(skip)]
pub db: Database,
db: Database,
pub blocks: BlocksVecs<M>,
#[traversable(wrap = "transactions", rename = "raw")]
pub transactions: TransactionsVecs<M>,
@@ -50,8 +48,25 @@ pub struct Vecs<M: StorageMode = Rw> {
pub op_return: OpReturnVecs<M>,
}
impl Vecs {
pub fn forced_import(parent: &Path, version: Version) -> Result<Self> {
pub trait IndexerVecs: Sized {
fn forced_import(parent: &Path, version: Version) -> Result<Self>;
fn rollback_if_needed(&mut self, starting_lengths: &Lengths) -> Result<()>;
fn flush(&mut self, height: Height) -> Result<()>;
fn stamped_write(&mut self, height: Height) -> Result<()>;
fn sync_bg_tasks(&self) -> Result<()>;
fn run_bg(
&self,
f: impl FnOnce(&Database) -> std::result::Result<(), RawDBError> + Send + 'static,
);
fn iter_addr_hashes_from(
&self,
addr_type: OutputType,
height: Height,
) -> Result<Box<dyn Iterator<Item = AddrHash> + '_>>;
}
impl IndexerVecs for Vecs {
fn forced_import(parent: &Path, version: Version) -> Result<Self> {
tracing::debug!("Opening vecs database...");
let db = Database::open(&parent.join("vecs"))?;
tracing::debug!("Setting min len...");
@@ -99,7 +114,7 @@ impl Vecs {
Ok(this)
}
pub fn rollback_if_needed(&mut self, starting_lengths: &Lengths) -> Result<()> {
fn rollback_if_needed(&mut self, starting_lengths: &Lengths) -> Result<()> {
let saved_height = starting_lengths.height.decremented().unwrap_or_default();
let stamp = Stamp::from(u64::from(saved_height));
@@ -150,46 +165,49 @@ impl Vecs {
Ok(())
}
pub fn flush(&mut self, height: Height) -> Result<()> {
fn flush(&mut self, height: Height) -> Result<()> {
self.stamped_write(height)?;
self.db.flush()?;
Ok(())
}
pub fn next_height(&self) -> Height {
self.iter_any_stored_vec()
.map(|vec| {
let h = Height::from(vec.stamp());
if h > Height::ZERO { h.incremented() } else { h }
})
.min()
.unwrap()
}
pub fn stamped_write(&mut self, height: Height) -> Result<()> {
fn stamped_write(&mut self, height: Height) -> Result<()> {
self.par_iter_mut_any_stored_vec()
.try_for_each(|vec| vec.stamped_write(Stamp::from(height)))?;
Ok(())
}
pub fn compact(&self) -> Result<()> {
self.db.compact()?;
fn sync_bg_tasks(&self) -> Result<()> {
self.db.sync_bg_tasks()?;
Ok(())
}
pub fn reset(&mut self) -> Result<()> {
self.par_iter_mut_any_stored_vec()
.try_for_each(|vec| vec.any_reset())?;
Ok(())
fn run_bg(
&self,
f: impl FnOnce(&Database) -> std::result::Result<(), RawDBError> + Send + 'static,
) {
self.db.run_bg(f);
}
pub fn iter_addr_hashes_from(
fn iter_addr_hashes_from(
&self,
addr_type: OutputType,
height: Height,
) -> Result<Box<dyn Iterator<Item = AddrHash> + '_>> {
self.addrs.iter_hashes_from(addr_type, height)
}
}
impl Vecs {
pub fn next_height(&self) -> Height {
let min_stamp = self
.iter_any_stored_vec()
.map(|vec| vec.stamp())
.min()
.unwrap();
next_height_from_min_stamp(min_stamp, !self.blocks.blockhash.is_empty())
}
fn par_iter_mut_any_stored_vec(
&mut self,
@@ -217,3 +235,32 @@ impl Vecs {
.chain(self.op_return.iter_any())
}
}
fn next_height_from_min_stamp(min_stamp: Stamp, has_blocks: bool) -> Height {
if has_blocks {
Height::from(min_stamp).incremented()
} else {
Height::ZERO
}
}
#[cfg(test)]
mod checkpoint_tests {
use super::*;
#[test]
fn zero_stamp_distinguishes_empty_from_genesis() {
let zero = Stamp::from(0_u64);
assert_eq!(next_height_from_min_stamp(zero, false), Height::ZERO);
assert_eq!(next_height_from_min_stamp(zero, true), Height::new(1));
}
#[test]
fn nonzero_stamp_advances_to_next_height() {
assert_eq!(
next_height_from_min_stamp(Stamp::from(41_u64), true),
Height::new(42)
);
}
}
+2
View File
@@ -13,5 +13,7 @@ brk_types = { workspace = true }
[dev-dependencies]
brk_indexer = { workspace = true }
brk_reader = { workspace = true }
brk_rpc = { workspace = true }
serde_json = { workspace = true }
vecdb = { workspace = true }
+16
View File
@@ -0,0 +1,16 @@
use std::path::Path;
use brk_indexer::Indexer;
use brk_reader::Reader;
use brk_rpc::{Auth, Client};
pub fn import_indexer(data_dir: &Path) -> Indexer {
let bitcoin_dir = Client::default_bitcoin_path();
let client = Client::new(
Client::default_url(),
Auth::CookieFile(bitcoin_dir.join(".cookie")),
)
.expect("Failed to connect to Bitcoin Core");
let reader = Reader::new(bitcoin_dir.join("blocks"), &client);
Indexer::import(data_dir, &reader).expect("Failed to import indexer")
}
+11 -10
View File
@@ -11,7 +11,6 @@
use std::path::PathBuf;
use brk_indexer::Indexer;
use brk_oracle::{
Config, HistogramRaw, Oracle, PaymentFilter, START_HEIGHT_FAST, START_HEIGHT_SLOW,
bin_to_cents, cents_to_bin,
@@ -19,6 +18,8 @@ use brk_oracle::{
use brk_types::{OutputType, Sats, TxIndex, TxOutIndex};
use vecdb::{AnyVec, ReadableVec, VecIndex};
mod common;
struct Block {
height: usize,
values: Vec<Sats>,
@@ -52,8 +53,8 @@ fn main() {
PathBuf::from(home).join(".brk")
});
let indexer = Indexer::forced_import(&data_dir).expect("Failed to load indexer");
let total_heights = indexer.vecs.blocks.timestamp.len();
let indexer = common::import_indexer(&data_dir);
let total_heights = indexer.vecs().blocks.timestamp.len();
let fast_config = Config::default();
let window_size = fast_config.window_size;
@@ -75,12 +76,12 @@ fn main() {
"Loading {} blocks ({load_start}..{end_height})...",
end_height - load_start
);
let total_txs = indexer.vecs.transactions.txid.len();
let total_outputs = indexer.vecs.outputs.value.len();
let first_tx_index: Vec<TxIndex> = indexer.vecs.transactions.first_tx_index.collect();
let out_first: Vec<TxOutIndex> = indexer.vecs.outputs.first_txout_index.collect();
let total_txs = indexer.vecs().transactions.txid.len();
let total_outputs = indexer.vecs().outputs.value.len();
let first_tx_index: Vec<TxIndex> = indexer.vecs().transactions.first_tx_index.collect();
let out_first: Vec<TxOutIndex> = indexer.vecs().outputs.first_txout_index.collect();
let mut txout_cursor = indexer
.vecs
.vecs()
.transactions
.first_txout_index
.reader()
@@ -109,12 +110,12 @@ fn main() {
let out_start = tx_starts.first().copied().unwrap_or(out_end);
let values: Vec<Sats> = indexer
.vecs
.vecs()
.outputs
.value
.collect_range_at(out_start, out_end);
let output_types: Vec<OutputType> = indexer
.vecs
.vecs()
.outputs
.output_type
.collect_range_at(out_start, out_end);
+12 -11
View File
@@ -21,10 +21,11 @@ use std::{
path::PathBuf,
};
use brk_indexer::Indexer;
use brk_types::{OutputType, Sats, TxIndex, TxOutIndex};
use vecdb::{AnyVec, ReadableVec, VecIndex};
mod common;
fn main() {
let data_dir = std::env::var("BRK_DIR")
.map(PathBuf::from)
@@ -39,8 +40,8 @@ fn main() {
.and_then(|s| s.parse().ok())
.unwrap_or(510_000);
let indexer = Indexer::forced_import(&data_dir).expect("Failed to load indexer");
let total_heights = indexer.vecs.blocks.timestamp.len();
let indexer = common::import_indexer(&data_dir);
let total_heights = indexer.vecs().blocks.timestamp.len();
let end = end.min(total_heights);
let manifest_dir = env!("CARGO_MANIFEST_DIR");
@@ -50,13 +51,13 @@ fn main() {
)
.expect("parse height OHLC");
let timestamps: Vec<brk_types::Timestamp> = indexer.vecs.blocks.timestamp.collect();
let total_txs = indexer.vecs.transactions.txid.len();
let total_outputs = indexer.vecs.outputs.value.len();
let first_tx_index: Vec<TxIndex> = indexer.vecs.transactions.first_tx_index.collect();
let out_first: Vec<TxOutIndex> = indexer.vecs.outputs.first_txout_index.collect();
let timestamps: Vec<brk_types::Timestamp> = indexer.vecs().blocks.timestamp.collect();
let total_txs = indexer.vecs().transactions.txid.len();
let total_outputs = indexer.vecs().outputs.value.len();
let first_tx_index: Vec<TxIndex> = indexer.vecs().transactions.first_tx_index.collect();
let out_first: Vec<TxOutIndex> = indexer.vecs().outputs.first_txout_index.collect();
let mut txout_cursor = indexer
.vecs
.vecs()
.transactions
.first_txout_index
.reader()
@@ -99,12 +100,12 @@ fn main() {
let out_start = tx_starts.first().copied().unwrap_or(out_end);
let values: Vec<Sats> = indexer
.vecs
.vecs()
.outputs
.value
.collect_range_at(out_start, out_end);
let output_types: Vec<OutputType> = indexer
.vecs
.vecs()
.outputs
.output_type
.collect_range_at(out_start, out_end);
+12 -11
View File
@@ -9,7 +9,6 @@
use std::{cmp::Ordering, env, path::PathBuf};
use brk_indexer::Indexer;
use brk_oracle::{
BINS_PER_DECADE, Config, NUM_BINS, PaymentFilter, START_HEIGHT_FAST, START_HEIGHT_SLOW,
bin_to_cents, cents_to_bin, seed_bin as oracle_seed_bin,
@@ -17,6 +16,8 @@ use brk_oracle::{
use brk_types::{OutputType, Sats, TxIndex, TxOutIndex};
use vecdb::{AnyVec, ReadableVec, VecIndex};
mod common;
const GENESIS_DAY: u32 = 14252;
const BINS_5PCT: f64 = 4.24;
const BINS_10PCT: f64 = 8.28;
@@ -455,8 +456,8 @@ fn main() {
.unwrap_or(START_HEIGHT_SLOW)
.max(START_HEIGHT_SLOW);
let indexer = Indexer::forced_import(&data_dir).expect("Failed to load indexer");
let total_heights = indexer.vecs.blocks.timestamp.len();
let indexer = common::import_indexer(&data_dir);
let total_heights = indexer.vecs().blocks.timestamp.len();
let end = end_override.unwrap_or(total_heights).min(total_heights);
let manifest_dir = env!("CARGO_MANIFEST_DIR");
@@ -478,7 +479,7 @@ fn main() {
})
.collect();
let timestamps: Vec<brk_types::Timestamp> = indexer.vecs.blocks.timestamp.collect();
let timestamps: Vec<brk_types::Timestamp> = indexer.vecs().blocks.timestamp.collect();
let height_years: Vec<u16> = timestamps
.iter()
.map(|ts| timestamp_to_year(**ts))
@@ -530,12 +531,12 @@ fn main() {
.map(|cfg| Variant::new(cfg, seed_bin))
.collect();
let total_txs = indexer.vecs.transactions.txid.len();
let total_outputs = indexer.vecs.outputs.value.len();
let first_tx_index: Vec<TxIndex> = indexer.vecs.transactions.first_tx_index.collect();
let out_first: Vec<TxOutIndex> = indexer.vecs.outputs.first_txout_index.collect();
let total_txs = indexer.vecs().transactions.txid.len();
let total_outputs = indexer.vecs().outputs.value.len();
let first_tx_index: Vec<TxIndex> = indexer.vecs().transactions.first_tx_index.collect();
let out_first: Vec<TxOutIndex> = indexer.vecs().outputs.first_txout_index.collect();
let mut txout_cursor = indexer
.vecs
.vecs()
.transactions
.first_txout_index
.reader()
@@ -580,11 +581,11 @@ fn main() {
let out_start = tx_starts.first().copied().unwrap_or(out_end);
indexer
.vecs
.vecs()
.outputs
.value
.collect_range_into_at(out_start, out_end, &mut values);
indexer.vecs.outputs.output_type.collect_range_into_at(
indexer.vecs().outputs.output_type.collect_range_into_at(
out_start,
out_end,
&mut output_types,
+12 -11
View File
@@ -4,13 +4,14 @@
use std::path::PathBuf;
use brk_indexer::Indexer;
use brk_oracle::{
Config, Oracle, PaymentFilter, START_HEIGHT_FAST, START_HEIGHT_SLOW, bin_to_cents, cents_to_bin,
};
use brk_types::{OutputType, Sats, TxIndex, TxOutIndex};
use vecdb::{AnyVec, ReadableVec, VecIndex};
mod common;
/// Day1 1 = Jan 9, 2009 (block 1). For dates after genesis week:
/// day1 = floor(timestamp / 86400) - 14252.
const GENESIS_DAY: u32 = 14252;
@@ -131,8 +132,8 @@ fn main() {
PathBuf::from(home).join(".brk")
});
let indexer = Indexer::forced_import(&data_dir).expect("Failed to load indexer");
let total_heights = indexer.vecs.blocks.timestamp.len();
let indexer = common::import_indexer(&data_dir);
let total_heights = indexer.vecs().blocks.timestamp.len();
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let height_ohlc: Vec<[f64; 4]> = serde_json::from_str(
@@ -161,7 +162,7 @@ fn main() {
.collect();
// Read block timestamps for year + day1 mapping.
let timestamps: Vec<brk_types::Timestamp> = indexer.vecs.blocks.timestamp.collect();
let timestamps: Vec<brk_types::Timestamp> = indexer.vecs().blocks.timestamp.collect();
let height_years: Vec<u16> = timestamps
.iter()
.map(|ts| timestamp_to_year(**ts))
@@ -173,15 +174,15 @@ fn main() {
let mut oracle = Oracle::from_seed();
let total_txs = indexer.vecs.transactions.txid.len();
let total_outputs = indexer.vecs.outputs.value.len();
let total_txs = indexer.vecs().transactions.txid.len();
let total_outputs = indexer.vecs().outputs.value.len();
// Pre-collect height-indexed vecs (small). Transaction-indexed vecs are too
// large, so the tx-indexed first_txout_index is read through a forward cursor.
let first_tx_index: Vec<TxIndex> = indexer.vecs.transactions.first_tx_index.collect();
let out_first: Vec<TxOutIndex> = indexer.vecs.outputs.first_txout_index.collect();
let first_tx_index: Vec<TxIndex> = indexer.vecs().transactions.first_tx_index.collect();
let out_first: Vec<TxOutIndex> = indexer.vecs().outputs.first_txout_index.collect();
let mut txout_cursor = indexer
.vecs
.vecs()
.transactions
.first_txout_index
.reader()
@@ -225,12 +226,12 @@ fn main() {
let out_start = tx_starts.first().copied().unwrap_or(out_end);
let values: Vec<Sats> = indexer
.vecs
.vecs()
.outputs
.value
.collect_range_at(out_start, out_end);
let output_types: Vec<OutputType> = indexer
.vecs
.vecs()
.outputs
.output_type
.collect_range_at(out_start, out_end);
+12 -11
View File
@@ -7,7 +7,6 @@
use std::path::PathBuf;
use brk_indexer::Indexer;
use brk_oracle::{
Config, HistogramEma, HistogramRaw, NUM_BINS, PaymentFilter, START_HEIGHT_FAST, bin_to_cents,
cents_to_bin, pre_oracle_price_cents,
@@ -15,6 +14,8 @@ use brk_oracle::{
use brk_types::{OutputType, Sats, TxIndex, TxOutIndex};
use vecdb::{AnyVec, ReadableVec, VecIndex};
mod common;
/// Day1 1 = Jan 9, 2009 (block 1). For dates after genesis week:
/// day1 = floor(timestamp / 86400) - 14252.
const GENESIS_DAY: u32 = 14252;
@@ -542,8 +543,8 @@ fn main() {
.and_then(|s| s.parse().ok())
.unwrap_or(5000);
let indexer = Indexer::forced_import(&data_dir).expect("Failed to load indexer");
let total_heights = indexer.vecs.blocks.timestamp.len();
let indexer = common::import_indexer(&data_dir);
let total_heights = indexer.vecs().blocks.timestamp.len();
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let height_ohlc: Vec<[f64; 4]> = serde_json::from_str(
@@ -572,7 +573,7 @@ fn main() {
.collect();
// Read block timestamps for year + day1 mapping.
let timestamps: Vec<brk_types::Timestamp> = indexer.vecs.blocks.timestamp.collect();
let timestamps: Vec<brk_types::Timestamp> = indexer.vecs().blocks.timestamp.collect();
let height_years: Vec<u16> = timestamps
.iter()
.map(|ts| timestamp_to_year(**ts))
@@ -835,15 +836,15 @@ fn main() {
let mut sharp_ema = HistogramEma::zeros();
eprintln!(" sharp: span={sharp_span:.0} window={sharp_window} alpha={sharp_alpha:.5}");
let total_txs = indexer.vecs.transactions.txid.len();
let total_outputs = indexer.vecs.outputs.value.len();
let total_txs = indexer.vecs().transactions.txid.len();
let total_outputs = indexer.vecs().outputs.value.len();
// Pre-collect height-indexed vecs (small). Transaction-indexed vecs are too
// large, so the tx-indexed first_txout_index is read through a forward cursor.
let first_tx_index: Vec<TxIndex> = indexer.vecs.transactions.first_tx_index.collect();
let out_first: Vec<TxOutIndex> = indexer.vecs.outputs.first_txout_index.collect();
let first_tx_index: Vec<TxIndex> = indexer.vecs().transactions.first_tx_index.collect();
let out_first: Vec<TxOutIndex> = indexer.vecs().outputs.first_txout_index.collect();
let mut txout_cursor = indexer
.vecs
.vecs()
.transactions
.first_txout_index
.reader()
@@ -893,12 +894,12 @@ fn main() {
let out_start = tx_starts.first().copied().unwrap_or(out_end);
let values: Vec<Sats> = indexer
.vecs
.vecs()
.outputs
.value
.collect_range_at(out_start, out_end);
let output_types: Vec<OutputType> = indexer
.vecs
.vecs()
.outputs
.output_type
.collect_range_at(out_start, out_end);
+2 -2
View File
@@ -18,7 +18,7 @@ Query blocks, transactions, addresses, and 1000+ on-chain metrics through a unif
## Core API
```rust,ignore
let query = Query::build(&reader, &indexer, &computer, Some(mempool));
let query = Query::build(&indexer, &computer, Some(mempool));
// Current height
let height = query.height();
@@ -55,7 +55,7 @@ let stats = query.address(address)?;
## Async Usage
```rust,ignore
let async_query = AsyncQuery::build(&reader, &indexer, &computer, mempool);
let async_query = AsyncQuery::build(&indexer, &computer, mempool);
// Run blocking queries in thread pool
let result = async_query.run(|q| q.block_by_height(height)).await;
+5 -1
View File
@@ -3,13 +3,17 @@ use std::{env, fs, path::Path};
use brk_computer::Computer;
use brk_indexer::Indexer;
use brk_query::Vecs;
use brk_reader::Reader;
use brk_rpc::{Auth, Client};
use vecdb::ReadOnlyClone;
pub fn main() -> brk_error::Result<()> {
let tmp = env::temp_dir().join("brk_search_gen");
fs::create_dir_all(&tmp)?;
let indexer = Indexer::forced_import(&tmp)?;
let client = Client::new("http://127.0.0.1:1", Auth::None)?;
let reader = Reader::new_without_rlimit(tmp.join("blocks"), &client);
let indexer = Indexer::import(&tmp, &reader)?;
let computer = Computer::forced_import(&tmp, &indexer)?;
let indexer_ro = indexer.read_only_clone();
+3 -11
View File
@@ -7,7 +7,7 @@ use brk_mempool::Mempool;
use brk_query::Query;
use brk_reader::Reader;
use brk_rpc::{Auth, Client};
use brk_types::{Addr, OutputType};
use brk_types::Addr;
use vecdb::Exit;
pub fn main() -> Result<()> {
@@ -33,7 +33,7 @@ pub fn main() -> Result<()> {
let reader = Reader::new(blocks_dir, &client);
let indexer = Indexer::forced_import(&outputs_dir)?;
let indexer = Indexer::import(&outputs_dir, &reader)?;
let computer = Computer::forced_import(&outputs_dir, &indexer)?;
@@ -43,15 +43,7 @@ pub fn main() -> Result<()> {
mempool_clone.start();
});
let query = Query::build(&reader, &indexer, &computer, Some(mempool));
dbg!(
indexer
.stores
.addr_type_to_addr_hash_to_addr_index
.get_unwrap(OutputType::P2WSH)
.approximate_len()
);
let query = Query::build(&indexer, &computer, Some(mempool));
let _ = dbg!(query.addr(Addr::from(
"bc1qwzrryqr3ja8w7hnja2spmkgfdcgvqwp5swz4af4ngsjecfz0w0pqud7k38".to_string(),
+2 -8
View File
@@ -2,7 +2,6 @@ use brk_computer::Computer;
use brk_error::Result;
use brk_indexer::Indexer;
use brk_mempool::Mempool;
use brk_reader::Reader;
use tokio::task::spawn_blocking;
use crate::Query;
@@ -11,13 +10,8 @@ use crate::Query;
pub struct AsyncQuery(Query);
impl AsyncQuery {
pub fn build(
reader: &Reader,
indexer: &Indexer,
computer: &Computer,
mempool: Option<Mempool>,
) -> Self {
Self(Query::build(reader, indexer, computer, mempool))
pub fn build(indexer: &Indexer, computer: &Computer, mempool: Option<Mempool>) -> Self {
Self(Query::build(indexer, computer, mempool))
}
/// Run a blocking query operation on a spawn_blocking thread.
+7 -16
View File
@@ -1,5 +1,5 @@
use brk_error::{Error, OptionData, Result};
use brk_types::{Addr, AddrIndexTxIndex, Height, Txid, Unit};
use brk_error::{Error, Result};
use brk_types::{Addr, Height, Txid};
use crate::Query;
@@ -14,29 +14,20 @@ impl Query {
before_txid: Option<&Txid>,
) -> Result<Height> {
let (output_type, type_index) = self.resolve_addr(addr)?;
let store = self
.indexer()
.stores
.addr_type_to_addr_index_and_tx_index
.get(output_type)
.data()?;
let stores = self.indexer().stores();
let tx_index_len = self.safe_lengths().tx_index;
let last_tx_index = match before_txid {
Some(txid) => {
let before_tx_index = self.resolve_tx_index(txid)?;
let min = AddrIndexTxIndex::min_for_addr(type_index);
let cursor = AddrIndexTxIndex::from((type_index, before_tx_index));
store
.range(min..cursor)
stores
.addr_tx_indexes_before(output_type, type_index, before_tx_index)?
.rev()
.map(|(key, _): (AddrIndexTxIndex, Unit)| key.tx_index())
.find(|tx_index| *tx_index < tx_index_len)
.ok_or(Error::UnknownAddr)?
}
None => store
.prefix(type_index)
None => stores
.addr_tx_indexes(output_type, type_index)?
.rev()
.map(|(key, _): (AddrIndexTxIndex, Unit)| key.tx_index())
.find(|tx_index| *tx_index < tx_index_len)
.ok_or(Error::UnknownAddr)?,
};
+6 -11
View File
@@ -1,4 +1,4 @@
use brk_error::{Error, OptionData, Result};
use brk_error::{Error, Result};
use brk_types::{Addr, AddrHash, AddrHashPrefixMatches, OutputType};
use crate::Query;
@@ -16,19 +16,14 @@ impl Query {
}
let prefix = AddrHashPrefix::parse(prefix)?;
let store = self
.indexer()
.stores
.addr_type_to_addr_hash_to_addr_index
.get(addr_type)
.data()?;
let stores = self.indexer().stores();
let safe_type_index = self.safe_lengths().to_type_index(addr_type);
let addr_readers = self.indexer().vecs.addrs.addr_readers();
let addr_readers = self.indexer().vecs().addrs.addr_readers();
let mut addresses = Vec::new();
let max_hash = AddrHash::new(u64::MAX);
if let Some(upper) = prefix.upper {
for (_, type_index) in store.range(prefix.lower..upper) {
for (_, type_index) in stores.addr_hash_range(addr_type, prefix.lower..upper)? {
if type_index >= safe_type_index {
continue;
}
@@ -41,7 +36,7 @@ impl Query {
}
}
} else {
for (_, type_index) in store.range(prefix.lower..max_hash) {
for (_, type_index) in stores.addr_hash_range(addr_type, prefix.lower..max_hash)? {
if type_index >= safe_type_index {
continue;
}
@@ -55,7 +50,7 @@ impl Query {
}
if addresses.len() <= ADDR_HASH_PREFIX_MATCH_LIMIT
&& let Some(type_index) = store.get(&max_hash)?.map(|cow| cow.into_owned())
&& let Some(type_index) = stores.addr_index(addr_type, &max_hash)?
&& type_index < safe_type_index
{
let script = addr_readers.script_pubkey(addr_type, type_index);
+3 -7
View File
@@ -1,6 +1,6 @@
use std::str::FromStr;
use brk_error::{Error, OptionData, Result};
use brk_error::{Error, Result};
use brk_types::{Addr, AddrBytes, AddrHash, OutputType, TypeIndex};
use crate::Query;
@@ -22,12 +22,8 @@ impl Query {
hash: &AddrHash,
) -> Result<TypeIndex> {
self.indexer()
.stores
.addr_type_to_addr_hash_to_addr_index
.get(output_type)
.data()?
.get(hash)?
.map(|cow| cow.into_owned())
.stores()
.addr_index(output_type, hash)?
.ok_or(Error::UnknownAddr)
}
}
+8 -18
View File
@@ -1,5 +1,5 @@
use brk_error::{OptionData, Result};
use brk_types::{Addr, AddrIndexTxIndex, Transaction, TxIndex, Txid, Unit};
use brk_error::Result;
use brk_types::{Addr, Transaction, TxIndex, Txid};
use crate::Query;
@@ -21,7 +21,7 @@ impl Query {
limit: usize,
) -> Result<Vec<Txid>> {
let txindices = self.addr_txindices(&addr, after_txid, limit)?;
let txid_reader = self.indexer().vecs.transactions.txid.reader();
let txid_reader = self.indexer().vecs().transactions.txid.reader();
Ok(txindices
.into_iter()
.map(|tx_index| txid_reader.get(tx_index))
@@ -34,33 +34,23 @@ impl Query {
after_txid: Option<Txid>,
limit: usize,
) -> Result<Vec<TxIndex>> {
let stores = &self.indexer().stores;
let stores = self.indexer().stores();
let (output_type, type_index) = self.resolve_addr(addr)?;
let store = stores
.addr_type_to_addr_index_and_tx_index
.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 cursor = AddrIndexTxIndex::from((type_index, after_tx_index));
Ok(store
.range(min..cursor)
Ok(stores
.addr_tx_indexes_before(output_type, type_index, after_tx_index)?
.rev()
.map(|(key, _): (AddrIndexTxIndex, Unit)| key.tx_index())
.filter(|tx_index| *tx_index < tx_index_len)
.take(limit)
.collect())
} else {
Ok(store
.prefix(type_index)
Ok(stores
.addr_tx_indexes(output_type, type_index)?
.rev()
.map(|(key, _): (AddrIndexTxIndex, Unit)| key.tx_index())
.filter(|tx_index| *tx_index < tx_index_len)
.take(limit)
.collect())
+6 -12
View File
@@ -1,25 +1,19 @@
use brk_error::{Error, OptionData, Result};
use brk_types::{Addr, AddrIndexOutPoint, Height, TxIndex, TxStatus, Unit, Utxo, Vout};
use brk_error::{Error, Result};
use brk_types::{Addr, Height, TxIndex, TxStatus, Utxo, Vout};
use crate::Query;
impl Query {
pub fn addr_utxos(&self, addr: Addr, max_utxos: usize) -> Result<Vec<Utxo>> {
let indexer = self.indexer();
let stores = &indexer.stores;
let vecs = &indexer.vecs;
let stores = indexer.stores();
let vecs = indexer.vecs();
let (output_type, type_index) = self.resolve_addr(&addr)?;
let store = stores
.addr_type_to_addr_index_and_unspent_outpoint
.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()))
let outpoints: Vec<(TxIndex, Vout)> = stores
.addr_unspent_outpoints(output_type, type_index)?
.filter(|(tx_index, _)| *tx_index < tx_index_len)
.take(max_utxos + 1)
.collect();
+40 -26
View File
@@ -85,7 +85,7 @@ impl Query {
if height >= self.safe_lengths().height {
return Err(Error::OutOfRange("Block height out of range".into()));
}
self.indexer().vecs.blocks.blockhash.get(height).data()
self.indexer().vecs().blocks.blockhash.get(height).data()
}
/// Most recent `count` blocks ending at `start_height` (default tip),
@@ -122,12 +122,16 @@ impl Query {
// 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);
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);
debug_assert_eq!(blockhashes.len(), count);
debug_assert_eq!(difficulties.len(), count);
debug_assert_eq!(timestamps.len(), count);
@@ -139,7 +143,7 @@ impl Query {
// 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
.vecs()
.transactions
.first_tx_index
.collect_range_at(begin, tx_index_end);
@@ -148,7 +152,7 @@ impl Query {
// Bulk read median time window
let median_start = begin.saturating_sub(10);
let median_timestamps: Vec<Timestamp> = indexer
.vecs
.vecs()
.blocks
.timestamp
.collect_range_at(median_start, end);
@@ -210,12 +214,16 @@ impl Query {
let all_pools = pools();
// Bulk read all indexed data
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);
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);
let pool_slugs = computer.pools.pool.collect_range_at(begin, end);
let pool_block_numbers = computer
.pools
@@ -226,16 +234,24 @@ impl Query {
// 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
.vecs()
.transactions
.first_tx_index
.collect_range_at(begin, tx_index_end);
// Bulk read segwit stats
let segwit_txs = indexer.vecs.blocks.segwit_txs.collect_range_at(begin, end);
let segwit_sizes = indexer.vecs.blocks.segwit_size.collect_range_at(begin, end);
let segwit_txs = indexer
.vecs()
.blocks
.segwit_txs
.collect_range_at(begin, end);
let segwit_sizes = indexer
.vecs()
.blocks
.segwit_size
.collect_range_at(begin, end);
let segwit_weights = indexer
.vecs
.vecs()
.blocks
.segwit_weight
.collect_range_at(begin, end);
@@ -310,7 +326,7 @@ impl Query {
// Bulk read median time window
let median_start = begin.saturating_sub(10);
let median_timestamps = indexer
.vecs
.vecs()
.blocks
.timestamp
.collect_range_at(median_start, end);
@@ -515,15 +531,13 @@ impl Query {
let indexer = self.indexer();
let prefix = BlockHashPrefix::from(hash);
let height = indexer
.stores
.blockhash_prefix_to_height
.get(&prefix)?
.map(|h| *h)
.stores()
.block_height(&prefix)?
.ok_or(Error::NotFound("Block not found".into()))?;
if height >= self.safe_lengths().height {
return Err(Error::NotFound("Block not found".into()));
}
match indexer.vecs.blocks.blockhash.get(height) {
match indexer.vecs().blocks.blockhash.get(height) {
Some(stored) if &stored == hash => Ok(height),
_ => Err(Error::NotFound("Block not found".into())),
}
@@ -536,7 +550,7 @@ impl Query {
pub fn read_block_header(&self, height: Height) -> Result<bitcoin::block::Header> {
let position = self
.indexer()
.vecs
.vecs()
.blocks
.position
.collect_one(height)
+2 -2
View File
@@ -19,8 +19,8 @@ impl Query {
}
let indexer = self.indexer();
let position = indexer.vecs.blocks.position.collect_one(height).data()?;
let size = indexer.vecs.blocks.total.collect_one(height).data()?;
let position = indexer.vecs().blocks.position.collect_one(height).data()?;
let size = indexer.vecs().blocks.total.collect_one(height).data()?;
self.reader().read_raw_bytes(position, *size as usize)
}
+1 -1
View File
@@ -20,7 +20,7 @@ impl Query {
let next_best = if height < tip {
Some(
self.indexer()
.vecs
.vecs()
.blocks
.blockhash
.get(height.incremented())
+2 -2
View File
@@ -41,7 +41,7 @@ impl Query {
let start: usize = usize::from(first_height_of_day).min(tip);
let mut ts_cursor = indexer.vecs.blocks.timestamp.cursor();
let mut ts_cursor = indexer.vecs().blocks.timestamp.cursor();
let mut best: Option<(usize, Timestamp)> = None;
let mut above_streak = 0usize;
@@ -82,7 +82,7 @@ impl Query {
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()?;
let blockhash = indexer.vecs().blocks.blockhash.collect_one(height).data()?;
let ts_secs: i64 = (*best_ts).into();
let iso_timestamp = JiffTimestamp::from_second(ts_secs)
+12 -12
View File
@@ -66,7 +66,7 @@ impl Query {
let (first, tx_count) = self.block_tx_range(height)?;
let txids = self
.indexer()
.vecs
.vecs()
.transactions
.txid
.collect_range_at(first, first + tx_count);
@@ -85,7 +85,7 @@ impl Query {
return Err(Error::OutOfRange("Transaction index out of range".into()));
}
self.indexer()
.vecs
.vecs()
.transactions
.txid
.collect_one_at(first + index)
@@ -124,11 +124,11 @@ impl Query {
// ── Phase 1: Decode all transactions, collect outpoints ─────────
let txid_cursor = indexer.vecs.transactions.txid.reader().cursor();
let mut total_size_cursor = indexer.vecs.transactions.total_size.cursor();
let mut sigops_cursor = indexer.vecs.transactions.total_sigop_cost.cursor();
let mut first_txin_cursor = indexer.vecs.transactions.first_txin_index.cursor();
let mut position_cursor = indexer.vecs.transactions.position.cursor();
let txid_cursor = indexer.vecs().transactions.txid.reader().cursor();
let mut total_size_cursor = indexer.vecs().transactions.total_size.cursor();
let mut sigops_cursor = indexer.vecs().transactions.total_sigop_cost.cursor();
let mut first_txin_cursor = indexer.vecs().transactions.first_txin_index.cursor();
let mut position_cursor = indexer.vecs().transactions.position.cursor();
struct DecodedTx {
pos: usize,
@@ -191,9 +191,9 @@ impl Query {
// sequential cursors avoid re-decompressing the same pages.
// Reading output_type/type_index/value HERE from inputs vecs (sequential)
// avoids random-reading them from outputs vecs in Phase 2.
let mut outpoint_cursor = indexer.vecs.inputs.outpoint.cursor();
let mut input_output_type_cursor = indexer.vecs.inputs.output_type.cursor();
let mut input_type_index_cursor = indexer.vecs.inputs.type_index.cursor();
let mut outpoint_cursor = indexer.vecs().inputs.outpoint.cursor();
let mut input_output_type_cursor = indexer.vecs().inputs.output_type.cursor();
let mut input_type_index_cursor = indexer.vecs().inputs.type_index.cursor();
let mut input_value_cursor = self.computer().inputs.value.cursor();
let mut prevout_input_data: FxHashMap<OutPoint, (OutputType, TypeIndex, Sats)> =
@@ -220,7 +220,7 @@ impl Query {
// Sort by (output_type, type_index) for sequential BytesVec access
// within each address type's file.
let addr_readers = indexer.vecs.addrs.addr_readers();
let addr_readers = indexer.vecs().addrs.addr_readers();
let mut sorted_prevouts: Vec<(OutPoint, OutputType, TypeIndex, Sats)> =
Vec::with_capacity(prevout_input_data.len());
@@ -315,7 +315,7 @@ impl Query {
if height >= safe.height {
return Err(Error::OutOfRange("Block height out of range".into()));
}
let first_tx_index_vec = &self.indexer().vecs.transactions.first_tx_index;
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 {
+10 -10
View File
@@ -35,7 +35,7 @@ impl Query {
let descendants = self.resolve_entries(&walk.descendants)?;
let sigops = self
.indexer()
.vecs
.vecs()
.transactions
.total_sigop_cost
.collect_one(seed)
@@ -56,9 +56,9 @@ impl Query {
) -> Result<Vec<Member>> {
let indexer = self.indexer();
let computer = self.computer();
let mut weight = indexer.vecs.transactions.weight.cursor();
let mut weight = indexer.vecs().transactions.weight.cursor();
let mut fee = computer.transactions.fees.fee.tx_index.cursor();
let txid = indexer.vecs.transactions.txid.reader();
let txid = indexer.vecs().transactions.txid.reader();
members
.iter()
@@ -79,9 +79,9 @@ impl Query {
fn resolve_entries(&self, indexes: &[TxIndex]) -> Result<Vec<CpfpEntry>> {
let indexer = self.indexer();
let computer = self.computer();
let mut weight = indexer.vecs.transactions.weight.cursor();
let mut weight = indexer.vecs().transactions.weight.cursor();
let mut fee = computer.transactions.fees.fee.tx_index.cursor();
let txid = indexer.vecs.transactions.txid.reader();
let txid = indexer.vecs().transactions.txid.reader();
indexes
.iter()
@@ -100,7 +100,7 @@ impl Query {
let indexer = self.indexer();
let computer = self.computer();
let safe = self.safe_lengths();
let first_tx = &indexer.vecs.transactions.first_tx_index;
let first_tx = &indexer.vecs().transactions.first_tx_index;
let block_first = first_tx.collect_one(height).data()?;
let next_height = height.incremented();
let block_end = if next_height < safe.height {
@@ -109,18 +109,18 @@ impl Query {
safe.tx_index
};
let mut first_txin = indexer.vecs.transactions.first_txin_index.cursor();
let mut first_txin = indexer.vecs().transactions.first_txin_index.cursor();
let mut input_count = computer.indexes.tx_index.input_count.cursor();
let mut outpoint = indexer.vecs.inputs.outpoint.cursor();
let mut outpoint = indexer.vecs().inputs.outpoint.cursor();
let first_txout = indexer
.vecs
.vecs()
.transactions
.first_txout_index
.reader()
.cursor();
let mut output_count = computer.indexes.tx_index.output_count.cursor();
let spent = computer.outputs.spent.txin_index.reader().cursor();
let mut spending_tx = indexer.vecs.inputs.tx_index.cursor();
let mut spending_tx = indexer.vecs().inputs.tx_index.cursor();
let mut parents_of = |tx: TxIndex| -> Result<SmallVec<[TxIndex; 2]>> {
let position = tx.to_usize();
+8 -10
View File
@@ -50,20 +50,18 @@ impl Query {
return FxHashMap::default();
}
let safe = indexer.safe_lengths();
let first_txout_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();
let value_reader = indexer.vecs.outputs.value.reader();
let addr_readers = indexer.vecs.addrs.addr_readers();
let first_txout_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();
let value_reader = indexer.vecs().outputs.value.reader();
let addr_readers = indexer.vecs().addrs.addr_readers();
holes
.iter()
.filter_map(|(prev_txid, vout)| {
let prev_tx_index = indexer
.stores
.txid_prefix_to_tx_index
.get(&TxidPrefix::from(prev_txid))
.ok()??
.into_owned();
.stores()
.tx_index(&TxidPrefix::from(prev_txid))
.ok()??;
if prev_tx_index >= safe.tx_index {
return None;
}
@@ -14,7 +14,7 @@ impl Query {
/// corresponding metric. Single bucket-pass: built via `.map(...).unzip()`
/// to avoid re-walking buckets.
pub fn block_sizes_weights(&self, time_period: TimePeriod) -> Result<BlockSizesWeights> {
let blocks = &self.indexer().vecs.blocks;
let blocks = &self.indexer().vecs().blocks;
let bw = BlockWindow::new(self, time_period)?;
let block_sizes: Vec<StoredU64> = bw.read(&blocks.total)?;
@@ -94,7 +94,7 @@ impl BlockWindow {
let timestamps: Vec<Timestamp> = query
.indexer()
.vecs
.vecs()
.blocks
.timestamp
.collect_range(start, end);
@@ -53,7 +53,7 @@ impl Query {
.collect_one(current_epoch)
.data()?;
let current_timestamp = indexer
.vecs
.vecs()
.blocks
.timestamp
.collect_one(current_height)
@@ -105,13 +105,13 @@ impl Query {
.data()?;
let prev_difficulty = indexer
.vecs
.vecs()
.blocks
.difficulty
.collect_one(prev_epoch_start)
.data()?;
let curr_difficulty = indexer
.vecs
.vecs()
.blocks
.difficulty
.collect_one(epoch_start_height)
+1 -1
View File
@@ -40,7 +40,7 @@ impl Query {
let current_height = self.height();
let current_difficulty = *indexer
.vecs
.vecs()
.blocks
.difficulty
.collect_one(current_height)
+2 -3
View File
@@ -230,8 +230,7 @@ impl Query {
let indexer = self.indexer();
let safe_height = safe.height.to_usize();
let total_outputs = safe.txout_index.to_usize();
let first_txout_index = &indexer.vecs.outputs.first_txout_index;
let first_txout_index = &indexer.vecs().outputs.first_txout_index;
let out_start = first_txout_index
.collect_one_at(range.start)
.unwrap()
@@ -245,7 +244,7 @@ impl Query {
let mut hist = HistogramRaw::zeros();
indexer
.vecs
.vecs()
.outputs
.value
.for_each_range_at(out_start, out_end, |sats| {
+1 -1
View File
@@ -225,7 +225,7 @@ impl Query {
}
fn entity_index_at(&self, index: Index, h: Height) -> Option<usize> {
let v = &self.indexer().vecs;
let v = self.indexer().vecs();
match index {
Index::TxIndex => v
.transactions
+15 -17
View File
@@ -22,10 +22,8 @@ impl Query {
#[inline]
pub(crate) fn resolve_tx_index(&self, txid: &Txid) -> Result<TxIndex> {
self.indexer()
.stores
.txid_prefix_to_tx_index
.get(&TxidPrefix::from(txid))?
.map(|cow| cow.into_owned())
.stores()
.tx_index(&TxidPrefix::from(txid))?
.ok_or(Error::UnknownTxid)
}
@@ -47,7 +45,7 @@ impl Query {
return Err(Error::OutOfRange("Transaction index out of range".into()));
}
self.indexer()
.vecs
.vecs()
.transactions
.txid
.collect_one(index)
@@ -90,8 +88,8 @@ impl Query {
#[inline]
pub(crate) fn block_hash_and_time(&self, height: Height) -> Result<(BlockHash, Timestamp)> {
let indexer = self.indexer();
let hash = indexer.vecs.blocks.blockhash.collect_one(height).data()?;
let time = indexer.vecs.blocks.timestamp.collect_one(height).data()?;
let hash = indexer.vecs().blocks.blockhash.collect_one(height).data()?;
let time = indexer.vecs().blocks.timestamp.collect_one(height).data()?;
Ok((hash, time))
}
@@ -224,11 +222,11 @@ impl Query {
) -> Result<Vec<TxOutspend>> {
let indexer = self.indexer();
let txin_index_reader = self.computer().outputs.spent.txin_index.reader();
let txid_reader = indexer.vecs.transactions.txid.reader();
let txid_reader = indexer.vecs().transactions.txid.reader();
let tx_heights = &self.computer().indexes.tx_heights;
let mut input_tx_cursor = indexer.vecs.inputs.tx_index.cursor();
let mut first_txin_cursor = indexer.vecs.transactions.first_txin_index.cursor();
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();
@@ -277,20 +275,20 @@ impl Query {
fn build_outspend(&self, txin_index: TxInIndex) -> Result<TxOutspend> {
let indexer = self.indexer();
let spending_tx_index: TxIndex = indexer
.vecs
.vecs()
.inputs
.tx_index
.collect_one(txin_index)
.data()?;
let spending_first_txin: TxInIndex = indexer
.vecs
.vecs()
.transactions
.first_txin_index
.collect_one(spending_tx_index)
.data()?;
let vin = Vin::from(usize::from(txin_index) - usize::from(spending_first_txin));
let spending_txid = indexer
.vecs
.vecs()
.transactions
.txid
.collect_one(spending_tx_index)
@@ -317,7 +315,7 @@ impl Query {
if tx_index >= safe.tx_index {
return Err(Error::UnknownTxid);
}
let first_txout_vec = &self.indexer().vecs.transactions.first_txout_index;
let first_txout_vec = &self.indexer().vecs().transactions.first_txout_index;
let first_txout_reader = first_txout_vec.reader();
let first = first_txout_reader.try_get(tx_index).ok_or(Error::Internal(
"resolve_tx_outputs: first txout index past data",
@@ -346,13 +344,13 @@ impl Query {
fn transaction_raw_by_index(&self, tx_index: TxIndex) -> Result<Vec<u8>> {
let indexer = self.indexer();
let total_size = indexer
.vecs
.vecs()
.transactions
.total_size
.collect_one(tx_index)
.data()?;
let position = indexer
.vecs
.vecs()
.transactions
.position
.collect_one(tx_index)
@@ -388,7 +386,7 @@ impl Query {
let (tx_index, height) = self.resolve_tx(txid)?;
let first_tx = self
.indexer()
.vecs
.vecs()
.transactions
.first_tx_index
.collect_one(height)
+5 -16
View File
@@ -31,8 +31,6 @@ pub use vecs::Vecs;
pub struct Query(Arc<QueryInner<'static>>);
struct QueryInner<'a> {
vecs: &'a Vecs<'a>,
client: Client,
reader: Reader,
indexer: &'a Indexer<Ro>,
computer: &'a Computer<Ro>,
mempool: Option<Mempool>,
@@ -40,22 +38,13 @@ struct QueryInner<'a> {
}
impl Query {
pub fn build(
reader: &Reader,
indexer: &Indexer,
computer: &Computer,
mempool: Option<Mempool>,
) -> Self {
let client = reader.client().clone();
let reader = reader.clone();
pub fn build(indexer: &Indexer, computer: &Computer, mempool: Option<Mempool>) -> Self {
let indexer = Box::leak(Box::new(indexer.read_only_clone()));
let computer = Box::leak(Box::new(computer.read_only_clone()));
let vecs = Box::leak(Box::new(Vecs::build(indexer, computer)));
Self(Arc::new(QueryInner {
vecs,
client,
reader,
indexer,
computer,
mempool,
@@ -105,7 +94,7 @@ impl Query {
let blocks_behind = Height::from(tip_height.saturating_sub(*indexed_height));
let last_indexed_at_unix = self
.indexer()
.vecs
.vecs()
.blocks
.timestamp
.collect_one(self.height())
@@ -123,17 +112,17 @@ impl Query {
#[inline]
pub fn reader(&self) -> &Reader {
&self.0.reader
self.0.indexer.reader()
}
#[inline]
pub fn client(&self) -> &Client {
&self.0.client
self.reader().client()
}
#[inline]
pub fn blocks_dir(&self) -> &Path {
self.0.reader.blocks_dir()
self.reader().blocks_dir()
}
#[inline]
+4 -4
View File
@@ -26,8 +26,8 @@ pub struct Vecs<'a> {
impl<'a> Vecs<'a> {
pub fn build(indexer: &'a Indexer<Ro>, computer: &'a Computer<Ro>) -> Self {
Self::build_from(
indexer.vecs.iter_any_visible(),
indexer.vecs.to_tree_node(),
indexer.vecs().iter_any_visible(),
indexer.vecs().to_tree_node(),
computer.iter_named_visible(),
computer.to_tree_node(),
)
@@ -35,8 +35,8 @@ impl<'a> Vecs<'a> {
pub fn build_rw(indexer: &'a Indexer, computer: &'a Computer) -> Self {
Self::build_from(
indexer.vecs.iter_any_visible(),
indexer.vecs.to_tree_node(),
indexer.vecs().iter_any_visible(),
indexer.vecs().to_tree_node(),
computer.iter_named_visible(),
computer.to_tree_node(),
)
+5 -1
View File
@@ -4,6 +4,8 @@ use aide::axum::ApiRouter;
use brk_computer::Computer;
use brk_indexer::Indexer;
use brk_query::Vecs;
use brk_reader::Reader;
use brk_rpc::{Auth, Client};
use brk_server::{ApiRoutes, finish_openapi, generate_bindings};
pub fn main() -> color_eyre::Result<()> {
@@ -12,7 +14,9 @@ pub fn main() -> color_eyre::Result<()> {
let tmp = env::temp_dir().join("brk_bindgen");
fs::create_dir_all(&tmp)?;
let indexer = Indexer::forced_import(&tmp)?;
let client = Client::new("http://127.0.0.1:1", Auth::None)?;
let reader = Reader::new_without_rlimit(tmp.join("blocks"), &client);
let indexer = Indexer::import(&tmp, &reader)?;
let computer = Computer::forced_import(&tmp, &indexer)?;
let vecs = Vecs::build_rw(&indexer, &computer);
+2 -2
View File
@@ -23,7 +23,7 @@ pub fn main() -> Result<()> {
)?;
let reader = Reader::new(bitcoin_dir.join("blocks"), &client);
let indexer = Indexer::forced_import(&outputs_dir)?;
let indexer = Indexer::import(&outputs_dir, &reader)?;
let computer = Computer::forced_import(&outputs_dir, &indexer)?;
let mempool = Mempool::new(&client);
@@ -35,7 +35,7 @@ pub fn main() -> Result<()> {
let exit = Exit::new();
exit.set_ctrlc_handler();
let query = AsyncQuery::build(&reader, &indexer, &computer, Some(mempool));
let query = AsyncQuery::build(&indexer, &computer, Some(mempool));
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
+1 -1
View File
@@ -55,7 +55,7 @@ impl AppState {
self.sync(|q| {
let height = q.height();
q.indexer()
.vecs
.vecs()
.blocks
.timestamp
.collect_one(height)

Some files were not shown because too many files have changed in this diff Show More