Skip to content

Commit 4aed291

Browse files
committed
feat(filters,node): wire FilterIndex into apply_block via FilterIndexLike trait object
Op: extend
1 parent d6ef028 commit 4aed291

6 files changed

Lines changed: 197 additions & 1 deletion

File tree

crates/filters/src/filter_index.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,38 @@ impl<S: KvStore> FilterIndex<S> {
8787
Ok(Some(Hash256::from_le_bytes(&header)))
8888
}
8989
}
90+
91+
/// Storage-agnostic compact-filter ingest interface.
92+
pub trait FilterIndexLike: Send + Sync {
93+
/// Stores a block filter and returns its chained BIP157 filter header.
94+
fn put_filter(
95+
&self,
96+
block_hash: bitcoin_rs_primitives::Hash256,
97+
prev_header: bitcoin_rs_primitives::Hash256,
98+
filter_bytes: &[u8],
99+
) -> Result<bitcoin_rs_primitives::Hash256, FilterIndexError>;
100+
101+
/// Loads the BIP157 filter header for a block, if indexed.
102+
fn filter_header(
103+
&self,
104+
block_hash: bitcoin_rs_primitives::Hash256,
105+
) -> Result<Option<bitcoin_rs_primitives::Hash256>, FilterIndexError>;
106+
}
107+
108+
impl<S: KvStore + Send + Sync + 'static> FilterIndexLike for FilterIndex<S> {
109+
fn put_filter(
110+
&self,
111+
block_hash: bitcoin_rs_primitives::Hash256,
112+
prev_header: bitcoin_rs_primitives::Hash256,
113+
filter_bytes: &[u8],
114+
) -> Result<bitcoin_rs_primitives::Hash256, FilterIndexError> {
115+
Self::put_filter(self, block_hash, prev_header, filter_bytes)
116+
}
117+
118+
fn filter_header(
119+
&self,
120+
block_hash: bitcoin_rs_primitives::Hash256,
121+
) -> Result<Option<bitcoin_rs_primitives::Hash256>, FilterIndexError> {
122+
Self::filter_header(self, block_hash)
123+
}
124+
}

crates/filters/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@ pub mod filter_index;
88
/// BIP158 Golomb-coded set codec.
99
pub mod gcs;
1010

11-
pub use filter_index::{FilterIndex, FilterIndexError};
11+
pub use filter_index::{FilterIndex, FilterIndexError, FilterIndexLike};

crates/node/src/apply.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ pub struct ApplyHandles {
3939
pub coin_stats: Arc<bitcoin_rs_coinstats::CoinStatsListener>,
4040
/// Shared best-effort confirmed transaction indexer.
4141
pub tx_index: Arc<parking_lot::Mutex<Box<dyn bitcoin_rs_index::IndexerLike>>>,
42+
/// Shared best-effort compact-filter indexer.
43+
pub filter_index: Arc<Box<dyn bitcoin_rs_filters::FilterIndexLike>>,
4244
/// Shared mempool.
4345
pub mempool: Arc<RwLock<Mempool>>,
4446
/// Shared block records exposed to RPC handlers.
@@ -159,6 +161,13 @@ pub fn apply_block(
159161
metrics::histogram!("node.apply_block.bip68_seconds").record(bip68_dur.as_secs_f64());
160162
bip68_result?;
161163

164+
let filter_bytes = compute_basic_filter(block, handles).unwrap_or_else(|| {
165+
tracing::trace!(
166+
"BIP158 filter generation unavailable; storing empty filter as placeholder"
167+
);
168+
Vec::new()
169+
});
170+
162171
let changes = build_utxo_changes(block, height)?;
163172
let utxo_commit_started = quanta::Instant::now();
164173
let utxo_commit_result = handles.utxo.commit_block(&changes, &block_hash);
@@ -242,6 +251,30 @@ pub fn apply_block(
242251
let tx_index_ingest_dur = tx_index_ingest_started.elapsed();
243252
metrics::histogram!("node.apply_block.tx_index_ingest_seconds")
244253
.record(tx_index_ingest_dur.as_secs_f64());
254+
let filter_started = quanta::Instant::now();
255+
let prev_filter_header = handles
256+
.applied_tip
257+
.load_full()
258+
.and_then(|tip| handles.filter_index.filter_header(tip.hash).ok().flatten())
259+
.unwrap_or_default();
260+
match handles
261+
.filter_index
262+
.put_filter(block_hash, prev_filter_header, &filter_bytes)
263+
{
264+
Ok(filter_header) => {
265+
tracing::debug!(
266+
height,
267+
%filter_header,
268+
bytes = filter_bytes.len(),
269+
"filter_index stored block filter"
270+
);
271+
}
272+
Err(error) => {
273+
tracing::warn!(height, %error, "filter_index failed to store block filter");
274+
}
275+
}
276+
let filter_dur = filter_started.elapsed();
277+
metrics::histogram!("node.apply_block.filter_index_seconds").record(filter_dur.as_secs_f64());
245278
let total_dur = total_started.elapsed();
246279
metrics::histogram!("node.apply_block.total_seconds").record(total_dur.as_secs_f64());
247280
metrics::counter!("node.apply_block.txs_applied").increment(tx_count_delta);
@@ -262,6 +295,7 @@ pub fn apply_block(
262295
mempool_evict_us = mempool_evict_dur.as_micros(),
263296
tx_index_us = tx_index_dur.as_micros(),
264297
tx_index_ingest_us = tx_index_ingest_dur.as_micros(),
298+
filter_index_us = filter_dur.as_micros(),
265299
coin_stats_us = coin_stats_dur.as_micros(),
266300
total_us = total_dur.as_micros(),
267301
"apply_block: profile"
@@ -281,6 +315,24 @@ fn insert_active_header(
281315
Ok(())
282316
}
283317

318+
fn compute_basic_filter(block: &bitcoin::Block, handles: &ApplyHandles) -> Option<Vec<u8>> {
319+
use bitcoin::hashes::Hash as _;
320+
321+
let filter = bitcoin::bip158::BlockFilter::new_script_filter(block, |outpoint| {
322+
let prev_outpoint = OutPoint::new(
323+
bitcoin_rs_primitives::Hash256::from_le_bytes(outpoint.txid.as_byte_array()),
324+
outpoint.vout,
325+
);
326+
handles
327+
.utxo
328+
.get(&prev_outpoint)
329+
.map(|txout| txout.script_pubkey)
330+
.ok_or(bitcoin::bip158::Error::UtxoMissing(*outpoint))
331+
})
332+
.ok()?;
333+
Some(filter.content)
334+
}
335+
284336
fn verify_block_transactions(
285337
handles: &ApplyHandles,
286338
block: &bitcoin::Block,

crates/node/src/state.rs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ use parking_lot::{Mutex, RwLock};
2424
use crate::Config;
2525

2626
type TxIndexHandle = Arc<Mutex<Box<dyn bitcoin_rs_index::IndexerLike>>>;
27+
type FilterIndexHandle = Arc<Box<dyn bitcoin_rs_filters::FilterIndexLike>>;
2728

2829
/// Errors produced when applying a block to the node state.
2930
#[derive(Debug, thiserror::Error)]
@@ -201,6 +202,33 @@ fn open_tx_index(config: &Config) -> Result<TxIndexHandle> {
201202
Ok(Arc::new(Mutex::new(tx_index)))
202203
}
203204

205+
fn open_filter_index(config: &Config) -> Result<FilterIndexHandle> {
206+
let filters_dir = config.data_dir.join("filters");
207+
std::fs::create_dir_all(&filters_dir)
208+
.with_context(|| format!("create filters_dir {}", filters_dir.display()))?;
209+
let filter_index: Box<dyn bitcoin_rs_filters::FilterIndexLike> =
210+
match config.storage_backend.as_str() {
211+
#[cfg(feature = "rocksdb")]
212+
"rocksdb" => Box::new(bitcoin_rs_filters::FilterIndex::new(
213+
bitcoin_rs_storage::RocksDbStore::open(&filters_dir).map_err(anyhow::Error::new)?,
214+
)),
215+
#[cfg(feature = "fjall")]
216+
"fjall" => Box::new(bitcoin_rs_filters::FilterIndex::new(
217+
bitcoin_rs_storage::FjallStore::open(&filters_dir).map_err(anyhow::Error::new)?,
218+
)),
219+
#[cfg(feature = "redb")]
220+
"redb" => Box::new(bitcoin_rs_filters::FilterIndex::new(
221+
bitcoin_rs_storage::RedbStore::open(&filters_dir).map_err(anyhow::Error::new)?,
222+
)),
223+
#[cfg(feature = "mdbx")]
224+
"mdbx" => Box::new(bitcoin_rs_filters::FilterIndex::new(
225+
bitcoin_rs_storage::MdbxStore::open(&filters_dir).map_err(anyhow::Error::new)?,
226+
)),
227+
other => bail!("unsupported storage backend for filter index: {other}"),
228+
};
229+
Ok(Arc::new(filter_index))
230+
}
231+
204232
/// Aggregate handle to a running node.
205233
pub struct NodeState {
206234
config: Config,
@@ -209,6 +237,7 @@ pub struct NodeState {
209237
utxo: Arc<UtxoSet>,
210238
coin_stats: Arc<bitcoin_rs_coinstats::CoinStatsListener>,
211239
tx_index: TxIndexHandle,
240+
filter_index: FilterIndexHandle,
212241
mempool: Arc<RwLock<Mempool>>,
213242
chain_tip: Arc<ArcSwapOption<TipSnapshot>>,
214243
applied_tip: Arc<ArcSwapOption<TipSnapshot>>,
@@ -241,6 +270,7 @@ impl NodeState {
241270
.with_context(|| format!("create data_dir {}", config.data_dir.display()))?;
242271
let storage = NodeStorage::open(&config)?;
243272
let tx_index = open_tx_index(&config)?;
273+
let filter_index = open_filter_index(&config)?;
244274
let mut utxo_set = bitcoin_rs_utxo::UtxoSet::new();
245275
let coin_stats_listener = bitcoin_rs_coinstats::CoinStatsListener::new(
246276
bitcoin_rs_coinstats::CoinStats::default(),
@@ -273,6 +303,7 @@ impl NodeState {
273303
utxo: Arc::clone(&utxo),
274304
coin_stats: Arc::clone(&coin_stats),
275305
tx_index: Arc::clone(&tx_index),
306+
filter_index: Arc::clone(&filter_index),
276307
mempool: Arc::clone(&mempool),
277308
blocks: Arc::clone(&blocks),
278309
transactions: Arc::clone(&transactions),
@@ -295,6 +326,7 @@ impl NodeState {
295326
utxo,
296327
coin_stats,
297328
tx_index,
329+
filter_index,
298330
mempool,
299331
chain_tip,
300332
applied_tip,
@@ -350,6 +382,12 @@ impl NodeState {
350382
Arc::clone(&self.tx_index)
351383
}
352384

385+
/// Returns the shared compact-filter index handle.
386+
#[must_use]
387+
pub fn filter_index(&self) -> FilterIndexHandle {
388+
Arc::clone(&self.filter_index)
389+
}
390+
353391
/// Returns the shared mempool handle.
354392
#[must_use]
355393
pub fn mempool(&self) -> Arc<RwLock<Mempool>> {
@@ -498,6 +536,7 @@ impl NodeState {
498536
utxo: Arc::clone(&self.utxo),
499537
coin_stats: Arc::clone(&self.coin_stats),
500538
tx_index: Arc::clone(&self.tx_index),
539+
filter_index: Arc::clone(&self.filter_index),
501540
mempool: Arc::clone(&self.mempool),
502541
blocks: Arc::clone(&self.blocks),
503542
transactions: Arc::clone(&self.transactions),
@@ -598,6 +637,22 @@ mod tests {
598637
Ok(())
599638
}
600639

640+
#[test]
641+
fn open_constructs_filter_index() -> anyhow::Result<()> {
642+
let dir = tempfile::tempdir()?;
643+
let mut config = crate::Config::default_for_network(crate::Network::Regtest);
644+
config.data_dir = dir.path().join("node");
645+
config.p2p_listen.clear();
646+
let state = NodeState::open(config)?;
647+
let a = state.filter_index();
648+
let b = state.filter_index();
649+
assert!(
650+
Arc::ptr_eq(&a, &b),
651+
"filter_index handle stable across calls"
652+
);
653+
Ok(())
654+
}
655+
601656
#[test]
602657
fn open_constructs_block_sync_orchestrator() -> anyhow::Result<()> {
603658
let dir = tempfile::tempdir()?;

crates/node/src/sync.rs

Lines changed: 27 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_filters::{FilterIndexError, FilterIndexLike};
308309
use bitcoin_rs_index::{IndexError, IndexRowCounts, IndexerLike};
309310
use bitcoin_rs_mempool::{Mempool, MempoolLimits};
310311
use bitcoin_rs_p2p::PeerInfo;
@@ -482,6 +483,7 @@ mod tests {
482483
bitcoin_rs_coinstats::CoinStats::default(),
483484
)),
484485
tx_index: noop_tx_index(),
486+
filter_index: noop_filter_index(),
485487
mempool: Arc::new(RwLock::new(Mempool::new(MempoolLimits::default()))),
486488
blocks: Arc::new(RwLock::new(Vec::new())),
487489
transactions: Arc::new(RwLock::new(HashMap::<Txid, Transaction>::new())),
@@ -505,6 +507,31 @@ mod tests {
505507
Arc::new(Mutex::new(indexer))
506508
}
507509

510+
struct NoopFilterIndex;
511+
512+
impl FilterIndexLike for NoopFilterIndex {
513+
fn put_filter(
514+
&self,
515+
_block_hash: bitcoin_rs_primitives::Hash256,
516+
_prev_header: bitcoin_rs_primitives::Hash256,
517+
_filter_bytes: &[u8],
518+
) -> Result<bitcoin_rs_primitives::Hash256, FilterIndexError> {
519+
Ok(bitcoin_rs_primitives::Hash256::default())
520+
}
521+
522+
fn filter_header(
523+
&self,
524+
_block_hash: bitcoin_rs_primitives::Hash256,
525+
) -> Result<Option<bitcoin_rs_primitives::Hash256>, FilterIndexError> {
526+
Ok(None)
527+
}
528+
}
529+
530+
fn noop_filter_index() -> Arc<Box<dyn FilterIndexLike>> {
531+
let filter_index: Box<dyn FilterIndexLike> = Box::new(NoopFilterIndex);
532+
Arc::new(filter_index)
533+
}
534+
508535
fn test_header(prev_blockhash: BlockHash, height: u32) -> BlockHeader {
509536
let mut merkle = [0_u8; 32];
510537
merkle[..4].copy_from_slice(&height.to_le_bytes());

crates/node/tests/sync_smoke.rs

Lines changed: 27 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_filters::{FilterIndexError, FilterIndexLike};
1011
use bitcoin_rs_index::{IndexError, IndexRowCounts, IndexerLike};
1112
use bitcoin_rs_mempool::{Mempool, MempoolLimits};
1213
use bitcoin_rs_node::{BlockSync, Network, apply::ApplyHandles};
@@ -162,6 +163,7 @@ fn apply_handles(
162163
bitcoin_rs_coinstats::CoinStats::default(),
163164
)),
164165
tx_index: noop_tx_index(),
166+
filter_index: noop_filter_index(),
165167
mempool: Arc::new(RwLock::new(Mempool::new(MempoolLimits::default()))),
166168
blocks: Arc::new(RwLock::new(Vec::new())),
167169
transactions: Arc::new(RwLock::new(HashMap::<Txid, Transaction>::new())),
@@ -181,6 +183,31 @@ fn noop_tx_index() -> Arc<Mutex<Box<dyn IndexerLike>>> {
181183
Arc::new(Mutex::new(indexer))
182184
}
183185

186+
struct NoopFilterIndex;
187+
188+
impl FilterIndexLike for NoopFilterIndex {
189+
fn put_filter(
190+
&self,
191+
_block_hash: bitcoin_rs_primitives::Hash256,
192+
_prev_header: bitcoin_rs_primitives::Hash256,
193+
_filter_bytes: &[u8],
194+
) -> Result<bitcoin_rs_primitives::Hash256, FilterIndexError> {
195+
Ok(bitcoin_rs_primitives::Hash256::default())
196+
}
197+
198+
fn filter_header(
199+
&self,
200+
_block_hash: bitcoin_rs_primitives::Hash256,
201+
) -> Result<Option<bitcoin_rs_primitives::Hash256>, FilterIndexError> {
202+
Ok(None)
203+
}
204+
}
205+
206+
fn noop_filter_index() -> Arc<Box<dyn FilterIndexLike>> {
207+
let filter_index: Box<dyn FilterIndexLike> = Box::new(NoopFilterIndex);
208+
Arc::new(filter_index)
209+
}
210+
184211
fn regtest_genesis_block() -> Result<bitcoin::Block, Box<dyn std::error::Error>> {
185212
use bitcoin::consensus::Decodable as _;
186213

0 commit comments

Comments
 (0)