Skip to content

Commit d6ef028

Browse files
committed
feat(index,node): wire bitcoin_rs_index::Indexer into apply_block via IndexerLike trait object
Op: extend
1 parent 5fb04df commit d6ef028

6 files changed

Lines changed: 138 additions & 1 deletion

File tree

crates/index/src/index.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,3 +199,18 @@ impl Visitor for IndexBlockVisitor<'_> {
199199
fn is_null_prevout(prevout: &bsl::OutPoint<'_>) -> bool {
200200
prevout.vout() == u32::MAX && prevout.txid().iter().all(|byte| *byte == 0)
201201
}
202+
203+
/// Storage-agnostic block-ingest interface.
204+
///
205+
/// Use this trait when consumers must hold the indexer behind a trait
206+
/// object (e.g. when the storage backend is selected at runtime).
207+
pub trait IndexerLike: Send + Sync {
208+
/// Walks `block` once and writes index rows. See `Indexer::ingest_block`.
209+
fn ingest_block(&mut self, block: &[u8], height: u32) -> Result<IndexRowCounts, IndexError>;
210+
}
211+
212+
impl<S: KvStore + Send + Sync + 'static> IndexerLike for Indexer<S> {
213+
fn ingest_block(&mut self, block: &[u8], height: u32) -> Result<IndexRowCounts, IndexError> {
214+
Self::ingest_block(self, block, height)
215+
}
216+
}

crates/index/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ pub mod status;
1010
/// Stable electrs-shaped row types.
1111
pub mod types;
1212

13-
pub use index::{IndexError, IndexRowCounts, Indexer};
13+
pub use index::{IndexError, IndexRowCounts, Indexer, IndexerLike};
1414
pub use mempool::{MempoolRowCounts, MempoolRowWriter};
1515
pub use status::{HistoryEntry, HistoryHeight, StatusHash, compute_status_hash};
1616
pub use types::{

crates/node/src/apply.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ pub struct ApplyHandles {
3737
pub utxo: Arc<UtxoSet>,
3838
/// Shared coinstats listener.
3939
pub coin_stats: Arc<bitcoin_rs_coinstats::CoinStatsListener>,
40+
/// Shared best-effort confirmed transaction indexer.
41+
pub tx_index: Arc<parking_lot::Mutex<Box<dyn bitcoin_rs_index::IndexerLike>>>,
4042
/// Shared mempool.
4143
pub mempool: Arc<RwLock<Mempool>>,
4244
/// Shared block records exposed to RPC handlers.
@@ -215,6 +217,31 @@ pub fn apply_block(
215217
let coin_stats_dur = coin_stats_started.elapsed();
216218
metrics::histogram!("node.apply_block.coin_stats_finish_seconds")
217219
.record(coin_stats_dur.as_secs_f64());
220+
let tx_index_ingest_started = quanta::Instant::now();
221+
let block_bytes = bitcoin::consensus::encode::serialize(block);
222+
let tx_index_ingest_result = handles.tx_index.lock().ingest_block(&block_bytes, height);
223+
match tx_index_ingest_result {
224+
Ok(counts) => {
225+
tracing::debug!(
226+
height,
227+
txids = counts.txids,
228+
funding = counts.funding,
229+
spending = counts.spending,
230+
headers = counts.headers,
231+
"tx_index ingested block"
232+
);
233+
}
234+
Err(error) => {
235+
tracing::warn!(
236+
height,
237+
%error,
238+
"tx_index failed to ingest block; best-effort path continues"
239+
);
240+
}
241+
}
242+
let tx_index_ingest_dur = tx_index_ingest_started.elapsed();
243+
metrics::histogram!("node.apply_block.tx_index_ingest_seconds")
244+
.record(tx_index_ingest_dur.as_secs_f64());
218245
let total_dur = total_started.elapsed();
219246
metrics::histogram!("node.apply_block.total_seconds").record(total_dur.as_secs_f64());
220247
metrics::counter!("node.apply_block.txs_applied").increment(tx_count_delta);
@@ -234,6 +261,7 @@ pub fn apply_block(
234261
block_tree_insert_us = block_tree_insert_dur.as_micros(),
235262
mempool_evict_us = mempool_evict_dur.as_micros(),
236263
tx_index_us = tx_index_dur.as_micros(),
264+
tx_index_ingest_us = tx_index_ingest_dur.as_micros(),
237265
coin_stats_us = coin_stats_dur.as_micros(),
238266
total_us = total_dur.as_micros(),
239267
"apply_block: profile"

crates/node/src/state.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ use parking_lot::{Mutex, RwLock};
2323

2424
use crate::Config;
2525

26+
type TxIndexHandle = Arc<Mutex<Box<dyn bitcoin_rs_index::IndexerLike>>>;
27+
2628
/// Errors produced when applying a block to the node state.
2729
#[derive(Debug, thiserror::Error)]
2830
pub enum ApplyError {
@@ -165,13 +167,48 @@ impl fmt::Display for CompiledStorageFeatures {
165167
}
166168
}
167169

170+
fn open_tx_index(config: &Config) -> Result<TxIndexHandle> {
171+
let txindex_dir = config.data_dir.join("txindex");
172+
std::fs::create_dir_all(&txindex_dir)
173+
.with_context(|| format!("create txindex_dir {}", txindex_dir.display()))?;
174+
let tx_index: Box<dyn bitcoin_rs_index::IndexerLike> = match config.storage_backend.as_str() {
175+
#[cfg(feature = "rocksdb")]
176+
"rocksdb" => {
177+
let store =
178+
bitcoin_rs_storage::RocksDbStore::open(&txindex_dir).map_err(anyhow::Error::new)?;
179+
Box::new(bitcoin_rs_index::Indexer::new(Arc::new(store)))
180+
}
181+
#[cfg(feature = "fjall")]
182+
"fjall" => {
183+
let store =
184+
bitcoin_rs_storage::FjallStore::open(&txindex_dir).map_err(anyhow::Error::new)?;
185+
Box::new(bitcoin_rs_index::Indexer::new(Arc::new(store)))
186+
}
187+
#[cfg(feature = "redb")]
188+
"redb" => {
189+
let store =
190+
bitcoin_rs_storage::RedbStore::open(&txindex_dir).map_err(anyhow::Error::new)?;
191+
Box::new(bitcoin_rs_index::Indexer::new(Arc::new(store)))
192+
}
193+
#[cfg(feature = "mdbx")]
194+
"mdbx" => {
195+
let store =
196+
bitcoin_rs_storage::MdbxStore::open(&txindex_dir).map_err(anyhow::Error::new)?;
197+
Box::new(bitcoin_rs_index::Indexer::new(Arc::new(store)))
198+
}
199+
other => bail!("unsupported storage backend for txindex: {other}"),
200+
};
201+
Ok(Arc::new(Mutex::new(tx_index)))
202+
}
203+
168204
/// Aggregate handle to a running node.
169205
pub struct NodeState {
170206
config: Config,
171207
data_dir: PathBuf,
172208
storage: NodeStorage,
173209
utxo: Arc<UtxoSet>,
174210
coin_stats: Arc<bitcoin_rs_coinstats::CoinStatsListener>,
211+
tx_index: TxIndexHandle,
175212
mempool: Arc<RwLock<Mempool>>,
176213
chain_tip: Arc<ArcSwapOption<TipSnapshot>>,
177214
applied_tip: Arc<ArcSwapOption<TipSnapshot>>,
@@ -203,6 +240,7 @@ impl NodeState {
203240
std::fs::create_dir_all(&config.data_dir)
204241
.with_context(|| format!("create data_dir {}", config.data_dir.display()))?;
205242
let storage = NodeStorage::open(&config)?;
243+
let tx_index = open_tx_index(&config)?;
206244
let mut utxo_set = bitcoin_rs_utxo::UtxoSet::new();
207245
let coin_stats_listener = bitcoin_rs_coinstats::CoinStatsListener::new(
208246
bitcoin_rs_coinstats::CoinStats::default(),
@@ -234,6 +272,7 @@ impl NodeState {
234272
block_tree: Arc::clone(&block_tree),
235273
utxo: Arc::clone(&utxo),
236274
coin_stats: Arc::clone(&coin_stats),
275+
tx_index: Arc::clone(&tx_index),
237276
mempool: Arc::clone(&mempool),
238277
blocks: Arc::clone(&blocks),
239278
transactions: Arc::clone(&transactions),
@@ -255,6 +294,7 @@ impl NodeState {
255294
storage,
256295
utxo,
257296
coin_stats,
297+
tx_index,
258298
mempool,
259299
chain_tip,
260300
applied_tip,
@@ -304,6 +344,12 @@ impl NodeState {
304344
Arc::clone(&self.coin_stats)
305345
}
306346

347+
/// Returns the shared block indexer handle.
348+
#[must_use]
349+
pub fn tx_index(&self) -> Arc<Mutex<Box<dyn bitcoin_rs_index::IndexerLike>>> {
350+
Arc::clone(&self.tx_index)
351+
}
352+
307353
/// Returns the shared mempool handle.
308354
#[must_use]
309355
pub fn mempool(&self) -> Arc<RwLock<Mempool>> {
@@ -451,6 +497,7 @@ impl NodeState {
451497
block_tree: Arc::clone(&self.block_tree),
452498
utxo: Arc::clone(&self.utxo),
453499
coin_stats: Arc::clone(&self.coin_stats),
500+
tx_index: Arc::clone(&self.tx_index),
454501
mempool: Arc::clone(&self.mempool),
455502
blocks: Arc::clone(&self.blocks),
456503
transactions: Arc::clone(&self.transactions),
@@ -538,6 +585,19 @@ mod tests {
538585
Ok(())
539586
}
540587

588+
#[test]
589+
fn open_constructs_tx_index() -> anyhow::Result<()> {
590+
let dir = tempfile::tempdir()?;
591+
let mut config = crate::Config::default_for_network(crate::Network::Regtest);
592+
config.data_dir = dir.path().join("node");
593+
config.p2p_listen.clear();
594+
let state = NodeState::open(config)?;
595+
let a = state.tx_index();
596+
let b = state.tx_index();
597+
assert!(Arc::ptr_eq(&a, &b), "tx_index handle stable across calls");
598+
Ok(())
599+
}
600+
541601
#[test]
542602
fn open_constructs_block_sync_orchestrator() -> anyhow::Result<()> {
543603
let dir = tempfile::tempdir()?;

crates/node/src/sync.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,7 @@ mod tests {
305305
pow::CompactTarget,
306306
};
307307
use bitcoin_rs_chain::{BlockTree, ChainWork, NodeStatus, TipSnapshot};
308+
use bitcoin_rs_index::{IndexError, IndexRowCounts, IndexerLike};
308309
use bitcoin_rs_mempool::{Mempool, MempoolLimits};
309310
use bitcoin_rs_p2p::PeerInfo;
310311
use bitcoin_rs_utxo::UtxoSet;
@@ -480,12 +481,30 @@ mod tests {
480481
coin_stats: Arc::new(bitcoin_rs_coinstats::CoinStatsListener::new(
481482
bitcoin_rs_coinstats::CoinStats::default(),
482483
)),
484+
tx_index: noop_tx_index(),
483485
mempool: Arc::new(RwLock::new(Mempool::new(MempoolLimits::default()))),
484486
blocks: Arc::new(RwLock::new(Vec::new())),
485487
transactions: Arc::new(RwLock::new(HashMap::<Txid, Transaction>::new())),
486488
}
487489
}
488490

491+
struct NoopIndexer;
492+
493+
impl IndexerLike for NoopIndexer {
494+
fn ingest_block(
495+
&mut self,
496+
_block: &[u8],
497+
_height: u32,
498+
) -> Result<IndexRowCounts, IndexError> {
499+
Ok(IndexRowCounts::default())
500+
}
501+
}
502+
503+
fn noop_tx_index() -> Arc<Mutex<Box<dyn IndexerLike>>> {
504+
let indexer: Box<dyn IndexerLike> = Box::new(NoopIndexer);
505+
Arc::new(Mutex::new(indexer))
506+
}
507+
489508
fn test_header(prev_blockhash: BlockHash, height: u32) -> BlockHeader {
490509
let mut merkle = [0_u8; 32];
491510
merkle[..4].copy_from_slice(&height.to_le_bytes());

crates/node/tests/sync_smoke.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use bitcoin::hashes::Hash as _;
77
use bitcoin::p2p::message::NetworkMessage;
88
use bitcoin::{BlockHash, Transaction, Txid};
99
use bitcoin_rs_chain::{BlockTree, TipSnapshot};
10+
use bitcoin_rs_index::{IndexError, IndexRowCounts, IndexerLike};
1011
use bitcoin_rs_mempool::{Mempool, MempoolLimits};
1112
use bitcoin_rs_node::{BlockSync, Network, apply::ApplyHandles};
1213
use bitcoin_rs_p2p::{Message, PeerInfo};
@@ -160,12 +161,26 @@ fn apply_handles(
160161
coin_stats: Arc::new(bitcoin_rs_coinstats::CoinStatsListener::new(
161162
bitcoin_rs_coinstats::CoinStats::default(),
162163
)),
164+
tx_index: noop_tx_index(),
163165
mempool: Arc::new(RwLock::new(Mempool::new(MempoolLimits::default()))),
164166
blocks: Arc::new(RwLock::new(Vec::new())),
165167
transactions: Arc::new(RwLock::new(HashMap::<Txid, Transaction>::new())),
166168
}
167169
}
168170

171+
struct NoopIndexer;
172+
173+
impl IndexerLike for NoopIndexer {
174+
fn ingest_block(&mut self, _block: &[u8], _height: u32) -> Result<IndexRowCounts, IndexError> {
175+
Ok(IndexRowCounts::default())
176+
}
177+
}
178+
179+
fn noop_tx_index() -> Arc<Mutex<Box<dyn IndexerLike>>> {
180+
let indexer: Box<dyn IndexerLike> = Box::new(NoopIndexer);
181+
Arc::new(Mutex::new(indexer))
182+
}
183+
169184
fn regtest_genesis_block() -> Result<bitcoin::Block, Box<dyn std::error::Error>> {
170185
use bitcoin::consensus::Decodable as _;
171186

0 commit comments

Comments
 (0)