From 9e64ee5aedd69f632ef972a6b86111816c28ebb5 Mon Sep 17 00:00:00 2001 From: valued mammal Date: Sat, 14 Sep 2024 13:12:33 -0400 Subject: [PATCH 1/5] feat(rpc): introduce FilterIter --- crates/bitcoind_rpc/Cargo.toml | 3 + crates/bitcoind_rpc/examples/bip158.rs | 75 +++++++ crates/bitcoind_rpc/src/bip158.rs | 264 +++++++++++++++++++++++++ crates/bitcoind_rpc/src/lib.rs | 5 +- 4 files changed, 346 insertions(+), 1 deletion(-) create mode 100644 crates/bitcoind_rpc/examples/bip158.rs create mode 100644 crates/bitcoind_rpc/src/bip158.rs diff --git a/crates/bitcoind_rpc/Cargo.toml b/crates/bitcoind_rpc/Cargo.toml index 5694c895a..92c576f30 100644 --- a/crates/bitcoind_rpc/Cargo.toml +++ b/crates/bitcoind_rpc/Cargo.toml @@ -28,3 +28,6 @@ bdk_chain = { path = "../chain" } default = ["std"] std = ["bitcoin/std", "bdk_core/std"] serde = ["bitcoin/serde", "bdk_core/serde"] + +[[example]] +name = "bip158" diff --git a/crates/bitcoind_rpc/examples/bip158.rs b/crates/bitcoind_rpc/examples/bip158.rs new file mode 100644 index 000000000..689d1ea44 --- /dev/null +++ b/crates/bitcoind_rpc/examples/bip158.rs @@ -0,0 +1,75 @@ +#![allow(clippy::print_stdout)] +use bdk_bitcoind_rpc::bip158::{Event, EventInner, FilterIter}; +use bdk_chain::bitcoin::{constants::genesis_block, secp256k1::Secp256k1, Network}; +use bdk_chain::indexer::keychain_txout::KeychainTxOutIndex; +use bdk_chain::local_chain::LocalChain; +use bdk_chain::miniscript::Descriptor; +use bdk_chain::{BlockId, ConfirmationBlockTime, IndexedTxGraph, SpkIterator}; +use bdk_testenv::anyhow; + +// This example shows how BDK chain and tx-graph structures are updated using compact filters syncing. +// assumes a local Signet node, and "RPC_COOKIE" set in environment. + +// Usage: `cargo run -p bdk_bitcoind_rpc --example bip158` + +const EXTERNAL: &str = "tr([83737d5e/86h/1h/0h]tpubDDR5GgtoxS8fJyjjvdahN4VzV5DV6jtbcyvVXhEKq2XtpxjxBXmxH3r8QrNbQqHg4bJM1EGkxi7Pjfkgnui9jQWqS7kxHvX6rhUeriLDKxz/0/*)"; +const INTERNAL: &str = "tr([83737d5e/86h/1h/0h]tpubDDR5GgtoxS8fJyjjvdahN4VzV5DV6jtbcyvVXhEKq2XtpxjxBXmxH3r8QrNbQqHg4bJM1EGkxi7Pjfkgnui9jQWqS7kxHvX6rhUeriLDKxz/1/*)"; +const SPK_COUNT: u32 = 10; +const NETWORK: Network = Network::Signet; + +fn main() -> anyhow::Result<()> { + // Setup receiving chain and graph structures. + let secp = Secp256k1::new(); + let (descriptor, _) = Descriptor::parse_descriptor(&secp, EXTERNAL)?; + let (change_descriptor, _) = Descriptor::parse_descriptor(&secp, INTERNAL)?; + let (mut chain, _) = LocalChain::from_genesis_hash(genesis_block(NETWORK).block_hash()); + let mut graph = IndexedTxGraph::>::new({ + let mut index = KeychainTxOutIndex::default(); + index.insert_descriptor(0, descriptor.clone())?; + index.insert_descriptor(1, change_descriptor.clone())?; + index + }); + + // Assume a minimum birthday height + let block = BlockId { + height: 205_000, + hash: "0000002bd0f82f8c0c0f1e19128f84c938763641dba85c44bdb6aed1678d16cb".parse()?, + }; + let _ = chain.insert_block(block)?; + + // Configure RPC client + let rpc_client = bitcoincore_rpc::Client::new( + "127.0.0.1:38332", + bitcoincore_rpc::Auth::CookieFile(std::env::var("RPC_COOKIE")?.into()), + )?; + + // Initialize block emitter + let mut emitter = FilterIter::new_with_checkpoint(&rpc_client, chain.tip()); + for (_, desc) in graph.index.keychains() { + let spks = SpkIterator::new_with_range(desc, 0..SPK_COUNT).map(|(_, spk)| spk); + emitter.add_spks(spks); + } + + // Sync + if emitter.get_tip()?.is_some() { + // apply relevant blocks + for event in emitter.by_ref() { + if let Event::Block(EventInner { height, block }) = event? { + let _ = graph.apply_block_relevant(&block, height); + } + } + // update chain + if let Some(tip) = emitter.chain_update() { + let _ = chain.apply_update(tip)?; + } + } + + println!("Local tip: {}", chain.tip().height()); + + println!("Unspent"); + for (_, outpoint) in graph.index.outpoints() { + println!("{outpoint}"); + } + + Ok(()) +} diff --git a/crates/bitcoind_rpc/src/bip158.rs b/crates/bitcoind_rpc/src/bip158.rs new file mode 100644 index 000000000..1cc051620 --- /dev/null +++ b/crates/bitcoind_rpc/src/bip158.rs @@ -0,0 +1,264 @@ +//! Compact block filters sync over RPC, see also [BIP157][0]. +//! +//! This module is home to [`FilterIter`], a structure that returns bitcoin blocks by matching +//! a list of script pubkeys against a [BIP158][1] [`BlockFilter`]. +//! +//! [0]: https://github.com/bitcoin/bips/blob/master/bip-0157.mediawiki +//! [1]: https://github.com/bitcoin/bips/blob/master/bip-0158.mediawiki + +use bdk_core::collections::BTreeMap; +use core::fmt; + +use bdk_core::bitcoin; +use bdk_core::{BlockId, CheckPoint}; +use bitcoin::{ + bip158::{self, BlockFilter}, + Block, BlockHash, ScriptBuf, +}; +use bitcoincore_rpc; +use bitcoincore_rpc::RpcApi; + +/// Block height +type Height = u32; + +/// Type that generates block [`Event`]s by matching a list of script pubkeys against a +/// [`BlockFilter`]. +#[derive(Debug)] +pub struct FilterIter<'c, C> { + // RPC client + client: &'c C, + // SPK inventory + spks: Vec, + // local cp + cp: Option, + // blocks map + blocks: BTreeMap, + // next filter + next_filter: Option, + // best height counter + height: Height, + // stop height + stop: Height, +} + +impl<'c, C: RpcApi> FilterIter<'c, C> { + /// Construct [`FilterIter`] from a given `client` and start `height`. + pub fn new_with_height(client: &'c C, height: u32) -> Self { + Self { + client, + spks: vec![], + cp: None, + blocks: BTreeMap::new(), + next_filter: None, + height, + stop: 0, + } + } + + /// Construct [`FilterIter`] from a given `client` and [`CheckPoint`]. + pub fn new_with_checkpoint(client: &'c C, cp: CheckPoint) -> Self { + let mut filter_iter = Self::new_with_height(client, cp.height()); + filter_iter.cp = Some(cp); + filter_iter + } + + /// Extends `self` with an iterator of spks. + pub fn add_spks(&mut self, spks: impl IntoIterator) { + self.spks.extend(spks) + } + + /// Add spk to the list of spks to scan with. + pub fn add_spk(&mut self, spk: ScriptBuf) { + self.spks.push(spk); + } + + /// Get the next filter and increment the current best height. + /// + /// Returns `Ok(None)` when the stop height is exceeded. + fn next_filter(&mut self) -> Result, Error> { + if self.height > self.stop { + return Ok(None); + } + let height = self.height; + let hash = match self.blocks.get(&height) { + Some(h) => *h, + None => self.client.get_block_hash(height as u64)?, + }; + let filter_bytes = self.client.get_block_filter(&hash)?.filter; + let filter = BlockFilter::new(&filter_bytes); + self.height += 1; + Ok(Some((BlockId { height, hash }, filter))) + } + + /// Get the remote tip. + /// + /// Returns `None` if there's no difference between the height of this [`FilterIter`] and the + /// remote height. + pub fn get_tip(&mut self) -> Result, Error> { + let tip_hash = self.client.get_best_block_hash()?; + let mut header = self.client.get_block_header_info(&tip_hash)?; + let tip_height = header.height as u32; + if self.height == tip_height { + // nothing to do + return Ok(None); + } + self.blocks.insert(tip_height, tip_hash); + + // if we have a checkpoint we use a lookback of ten blocks + // to ensure consistency of the local chain + if let Some(cp) = self.cp.as_ref() { + // adjust start height to point of agreement + 1 + let base = self.find_base_with(cp.clone())?; + self.height = base.height + 1; + + for _ in 0..9 { + let hash = match header.previous_block_hash { + Some(hash) => hash, + None => break, + }; + header = self.client.get_block_header_info(&hash)?; + let height = header.height as u32; + if height < self.height { + break; + } + self.blocks.insert(height, hash); + } + } + + self.stop = tip_height; + + // get the first filter + self.next_filter = self.next_filter()?; + + Ok(Some(BlockId { + height: tip_height, + hash: self.blocks[&tip_height], + })) + } +} + +/// Alias for a compact filter and associated block id. +type NextFilter = (BlockId, BlockFilter); + +/// Event inner type +#[derive(Debug, Clone)] +pub struct EventInner { + /// Height + pub height: Height, + /// Block + pub block: Block, +} + +/// Kind of event produced by [`FilterIter`]. +#[derive(Debug, Clone)] +pub enum Event { + /// Block + Block(EventInner), + /// No match + NoMatch, +} + +impl Event { + /// Whether this event contains a matching block. + pub fn is_match(&self) -> bool { + matches!(self, Event::Block(_)) + } +} + +impl<'c, C: RpcApi> Iterator for FilterIter<'c, C> { + type Item = Result; + + fn next(&mut self) -> Option { + let (block, filter) = self.next_filter.clone()?; + + (|| -> Result<_, Error> { + // if the next filter matches any of our watched spks, get the block + // and return it, inserting relevant block ids along the way + let height = block.height; + let hash = block.hash; + let event = if self.spks.is_empty() { + Event::NoMatch + } else if filter + .match_any(&hash, self.spks.iter().map(|script| script.as_bytes())) + .map_err(Error::Bip158)? + { + let block = self.client.get_block(&hash)?; + self.blocks.insert(height, hash); + let inner = EventInner { height, block }; + Event::Block(inner) + } else { + Event::NoMatch + }; + + self.next_filter = self.next_filter()?; + Ok(Some(event)) + })() + .transpose() + } +} + +impl<'c, C: RpcApi> FilterIter<'c, C> { + /// Returns the point of agreement between `self` and the given `cp`. + fn find_base_with(&mut self, mut cp: CheckPoint) -> Result { + loop { + let height = cp.height(); + let fetched_hash = match self.blocks.get(&height) { + Some(hash) => *hash, + None if height == 0 => cp.hash(), + _ => self.client.get_block_hash(height as _)?, + }; + if cp.hash() == fetched_hash { + // ensure this block also exists in self + self.blocks.insert(height, cp.hash()); + return Ok(cp.block_id()); + } + // remember conflicts + self.blocks.insert(height, fetched_hash); + cp = cp.prev().expect("must break before genesis"); + } + } + + /// Returns a chain update from the newly scanned blocks. + /// + /// Returns `None` if this [`FilterIter`] was not constructed using a [`CheckPoint`], or + /// if no blocks have been fetched for example by using [`get_tip`](Self::get_tip). + pub fn chain_update(&mut self) -> Option { + if self.cp.is_none() || self.blocks.is_empty() { + return None; + } + + // note: to connect with the local chain we must guarantee that `self.blocks.first()` + // is also the point of agreement with `self.cp`. + Some( + CheckPoint::from_block_ids(self.blocks.iter().map(BlockId::from)) + .expect("blocks must be in order"), + ) + } +} + +/// Errors that may occur during a compact filters sync. +#[derive(Debug)] +pub enum Error { + /// bitcoin bip158 error + Bip158(bip158::Error), + /// bitcoincore_rpc error + Rpc(bitcoincore_rpc::Error), +} + +impl From for Error { + fn from(e: bitcoincore_rpc::Error) -> Self { + Self::Rpc(e) + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Bip158(e) => e.fmt(f), + Self::Rpc(e) => e.fmt(f), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for Error {} diff --git a/crates/bitcoind_rpc/src/lib.rs b/crates/bitcoind_rpc/src/lib.rs index 49121cead..3fa17ef19 100644 --- a/crates/bitcoind_rpc/src/lib.rs +++ b/crates/bitcoind_rpc/src/lib.rs @@ -11,9 +11,12 @@ use bdk_core::{BlockId, CheckPoint}; use bitcoin::{block::Header, Block, BlockHash, Transaction}; -pub use bitcoincore_rpc; use bitcoincore_rpc::bitcoincore_rpc_json; +pub mod bip158; + +pub use bitcoincore_rpc; + /// The [`Emitter`] is used to emit data sourced from [`bitcoincore_rpc::Client`]. /// /// Refer to [module-level documentation] for more. From aa8c49e18ab20b67fea55cd4c357737f6691a5d2 Mon Sep 17 00:00:00 2001 From: valued mammal Date: Sun, 15 Sep 2024 17:42:16 -0400 Subject: [PATCH 2/5] test(rpc): add bip158 tests --- crates/bitcoind_rpc/tests/bip158.rs | 114 ++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 crates/bitcoind_rpc/tests/bip158.rs diff --git a/crates/bitcoind_rpc/tests/bip158.rs b/crates/bitcoind_rpc/tests/bip158.rs new file mode 100644 index 000000000..0fff31500 --- /dev/null +++ b/crates/bitcoind_rpc/tests/bip158.rs @@ -0,0 +1,114 @@ +use bitcoin::{constants, Network}; + +use bdk_bitcoind_rpc::bip158::FilterIter; +use bdk_core::{BlockId, CheckPoint}; +use bdk_testenv::{anyhow, bitcoind, TestEnv}; +use bitcoincore_rpc::RpcApi; + +macro_rules! block_id { + ($height:expr, $hash:literal) => {{ + BlockId { + height: $height, + hash: bdk_core::bitcoin::hashes::Hash::hash($hash.as_bytes()), + } + }}; +} + +// Test the result of `chain_update` given a local checkpoint. +// +// new blocks +// 2--3--4--5--6--7--8--9--10--11 +// +// case 1: base below new blocks +// 0- +// case 2: base overlaps with new blocks +// 0--1--2--3--4 +// case 3: stale tip (with overlap) +// 0--1--2--3--4--x +// case 4: stale tip (no overlap) +// 0--x +#[test] +fn get_tip_and_chain_update() -> anyhow::Result<()> { + let mut conf = bitcoind::Conf::default(); + conf.args.push("-blockfilterindex=1"); + conf.args.push("-peerblockfilters=1"); + let env = TestEnv::new_with_config(bdk_testenv::Config { + bitcoind: conf, + ..Default::default() + })?; + + // Start by mining ten blocks + let hash = env.rpc_client().get_best_block_hash()?; + let header = env.rpc_client().get_block_header_info(&hash)?; + assert_eq!(header.height, 1); + let block_1 = BlockId { + height: header.height as u32, + hash, + }; + + let genesis_hash = constants::genesis_block(Network::Regtest).block_hash(); + let genesis = BlockId { + height: 0, + hash: genesis_hash, + }; + + // `FilterIter` will try to return up to ten recent blocks + // so we keep them for reference + let new_blocks: Vec = (2..=11) + .zip(env.mine_blocks(10, None)?) + .map(BlockId::from) + .collect(); + + struct TestCase { + // name + name: &'static str, + // local blocks + chain: Vec, + // expected blocks + exp: Vec, + } + + // For each test we create a new `FilterIter` with the checkpoint given + // by the blocks in the test chain. Then we sync to the remote tip and + // check the blocks that are returned in the chain update. + [ + TestCase { + name: "point of agreement below new blocks, expect base + new", + chain: vec![genesis, block_1], + exp: [block_1].into_iter().chain(new_blocks.clone()).collect(), + }, + TestCase { + name: "point of agreement genesis, expect base + new", + chain: vec![genesis], + exp: [genesis].into_iter().chain(new_blocks.clone()).collect(), + }, + TestCase { + name: "point of agreement within new blocks, expect base + remaining", + chain: new_blocks[..=2].to_vec(), + exp: new_blocks[2..].to_vec(), + }, + TestCase { + name: "stale tip within new blocks, expect base + corrected + remaining", + // base height: 4, stale height: 5 + chain: vec![new_blocks[2], block_id!(5, "E")], + exp: new_blocks[2..].to_vec(), + }, + TestCase { + name: "stale tip below new blocks, expect base + corrected + new", + chain: vec![genesis, block_id!(1, "A")], + exp: [genesis, block_1].into_iter().chain(new_blocks).collect(), + }, + ] + .into_iter() + .for_each(|test| { + let cp = CheckPoint::from_block_ids(test.chain).unwrap(); + let mut it = FilterIter::new_with_checkpoint(env.rpc_client(), cp); + let _ = it.get_tip().unwrap(); + let update_cp = it.chain_update().unwrap(); + let mut update_blocks: Vec<_> = update_cp.iter().map(|cp| cp.block_id()).collect(); + update_blocks.reverse(); + assert_eq!(update_blocks, test.exp, "{}", test.name); + }); + + Ok(()) +} From 352e65312bb8fcf34e4c70565fe06d037aed1e21 Mon Sep 17 00:00:00 2001 From: valued mammal Date: Fri, 20 Sep 2024 13:25:06 -0400 Subject: [PATCH 3/5] fix(rpc): bip158 `Event::NoMatch` includes the height --- crates/bitcoind_rpc/src/bip158.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/crates/bitcoind_rpc/src/bip158.rs b/crates/bitcoind_rpc/src/bip158.rs index 1cc051620..bb2bd1775 100644 --- a/crates/bitcoind_rpc/src/bip158.rs +++ b/crates/bitcoind_rpc/src/bip158.rs @@ -155,7 +155,7 @@ pub enum Event { /// Block Block(EventInner), /// No match - NoMatch, + NoMatch(Height), } impl Event { @@ -163,6 +163,14 @@ impl Event { pub fn is_match(&self) -> bool { matches!(self, Event::Block(_)) } + + /// Get the height of this event. + pub fn height(&self) -> Height { + match self { + Self::Block(EventInner { height, .. }) => *height, + Self::NoMatch(h) => *h, + } + } } impl<'c, C: RpcApi> Iterator for FilterIter<'c, C> { @@ -177,7 +185,7 @@ impl<'c, C: RpcApi> Iterator for FilterIter<'c, C> { let height = block.height; let hash = block.hash; let event = if self.spks.is_empty() { - Event::NoMatch + Event::NoMatch(height) } else if filter .match_any(&hash, self.spks.iter().map(|script| script.as_bytes())) .map_err(Error::Bip158)? @@ -187,7 +195,7 @@ impl<'c, C: RpcApi> Iterator for FilterIter<'c, C> { let inner = EventInner { height, block }; Event::Block(inner) } else { - Event::NoMatch + Event::NoMatch(height) }; self.next_filter = self.next_filter()?; From 881bf3d88b021518a0941ddfb685119c2a22c727 Mon Sep 17 00:00:00 2001 From: valued mammal Date: Fri, 20 Sep 2024 13:25:50 -0400 Subject: [PATCH 4/5] example(rpc): update bip158 example --- crates/bitcoind_rpc/examples/bip158.rs | 62 ++++++++++++++++++-------- 1 file changed, 44 insertions(+), 18 deletions(-) diff --git a/crates/bitcoind_rpc/examples/bip158.rs b/crates/bitcoind_rpc/examples/bip158.rs index 689d1ea44..7221942be 100644 --- a/crates/bitcoind_rpc/examples/bip158.rs +++ b/crates/bitcoind_rpc/examples/bip158.rs @@ -1,4 +1,6 @@ #![allow(clippy::print_stdout)] +use std::time::Instant; + use bdk_bitcoind_rpc::bip158::{Event, EventInner, FilterIter}; use bdk_chain::bitcoin::{constants::genesis_block, secp256k1::Secp256k1, Network}; use bdk_chain::indexer::keychain_txout::KeychainTxOutIndex; @@ -8,13 +10,13 @@ use bdk_chain::{BlockId, ConfirmationBlockTime, IndexedTxGraph, SpkIterator}; use bdk_testenv::anyhow; // This example shows how BDK chain and tx-graph structures are updated using compact filters syncing. -// assumes a local Signet node, and "RPC_COOKIE" set in environment. +// Assumes a local Signet node, and "RPC_COOKIE" set in environment. // Usage: `cargo run -p bdk_bitcoind_rpc --example bip158` -const EXTERNAL: &str = "tr([83737d5e/86h/1h/0h]tpubDDR5GgtoxS8fJyjjvdahN4VzV5DV6jtbcyvVXhEKq2XtpxjxBXmxH3r8QrNbQqHg4bJM1EGkxi7Pjfkgnui9jQWqS7kxHvX6rhUeriLDKxz/0/*)"; -const INTERNAL: &str = "tr([83737d5e/86h/1h/0h]tpubDDR5GgtoxS8fJyjjvdahN4VzV5DV6jtbcyvVXhEKq2XtpxjxBXmxH3r8QrNbQqHg4bJM1EGkxi7Pjfkgnui9jQWqS7kxHvX6rhUeriLDKxz/1/*)"; -const SPK_COUNT: u32 = 10; +const EXTERNAL: &str = "tr([7d94197e]tprv8ZgxMBicQKsPe1chHGzaa84k1inY2nAXUL8iPSyWESPrEst4E5oCFXhPATqj5fvw34LDknJz7rtXyEC4fKoXryUdc9q87pTTzfQyv61cKdE/86'/1'/0'/0/*)#uswl2jj7"; +const INTERNAL: &str = "tr([7d94197e]tprv8ZgxMBicQKsPe1chHGzaa84k1inY2nAXUL8iPSyWESPrEst4E5oCFXhPATqj5fvw34LDknJz7rtXyEC4fKoXryUdc9q87pTTzfQyv61cKdE/86'/1'/0'/1/*)#dyt7h8zx"; +const SPK_COUNT: u32 = 25; const NETWORK: Network = Network::Signet; fn main() -> anyhow::Result<()> { @@ -23,17 +25,17 @@ fn main() -> anyhow::Result<()> { let (descriptor, _) = Descriptor::parse_descriptor(&secp, EXTERNAL)?; let (change_descriptor, _) = Descriptor::parse_descriptor(&secp, INTERNAL)?; let (mut chain, _) = LocalChain::from_genesis_hash(genesis_block(NETWORK).block_hash()); - let mut graph = IndexedTxGraph::>::new({ + let mut graph = IndexedTxGraph::>::new({ let mut index = KeychainTxOutIndex::default(); - index.insert_descriptor(0, descriptor.clone())?; - index.insert_descriptor(1, change_descriptor.clone())?; + index.insert_descriptor("external", descriptor.clone())?; + index.insert_descriptor("internal", change_descriptor.clone())?; index }); // Assume a minimum birthday height let block = BlockId { - height: 205_000, - hash: "0000002bd0f82f8c0c0f1e19128f84c938763641dba85c44bdb6aed1678d16cb".parse()?, + height: 170_000, + hash: "00000041c812a89f084f633e4cf47e819a2f6b1c0a15162355a930410522c99d".parse()?, }; let _ = chain.insert_block(block)?; @@ -44,18 +46,31 @@ fn main() -> anyhow::Result<()> { )?; // Initialize block emitter - let mut emitter = FilterIter::new_with_checkpoint(&rpc_client, chain.tip()); + let cp = chain.tip(); + let start_height = cp.height(); + let mut emitter = FilterIter::new_with_checkpoint(&rpc_client, cp); for (_, desc) in graph.index.keychains() { let spks = SpkIterator::new_with_range(desc, 0..SPK_COUNT).map(|(_, spk)| spk); emitter.add_spks(spks); } + let start = Instant::now(); + // Sync - if emitter.get_tip()?.is_some() { - // apply relevant blocks + if let Some(tip) = emitter.get_tip()? { + let blocks_to_scan = tip.height - start_height; + for event in emitter.by_ref() { - if let Event::Block(EventInner { height, block }) = event? { - let _ = graph.apply_block_relevant(&block, height); + let event = event?; + let curr = event.height(); + // apply relevant blocks + if let Event::Block(EventInner { height, ref block }) = event { + let _ = graph.apply_block_relevant(block, height); + println!("Matched block {}", curr); + } + if curr % 1000 == 0 { + let progress = (curr - start_height) as f32 / blocks_to_scan as f32; + println!("[{:.2}%]", progress * 100.0); } } // update chain @@ -64,11 +79,22 @@ fn main() -> anyhow::Result<()> { } } + println!("\ntook: {}s", start.elapsed().as_secs()); println!("Local tip: {}", chain.tip().height()); - - println!("Unspent"); - for (_, outpoint) in graph.index.outpoints() { - println!("{outpoint}"); + let unspent: Vec<_> = graph + .graph() + .filter_chain_unspents( + &chain, + chain.tip().block_id(), + graph.index.outpoints().clone(), + ) + .collect(); + if !unspent.is_empty() { + println!("\nUnspent"); + for (index, utxo) in unspent { + // (k, index) | value | outpoint | + println!("{:?} | {} | {}", index, utxo.txout.value, utxo.outpoint,); + } } Ok(()) From 7d34480ee2265cf56c183f588108dd3eba854f73 Mon Sep 17 00:00:00 2001 From: valued mammal Date: Mon, 23 Sep 2024 08:41:19 -0400 Subject: [PATCH 5/5] fix(rpc): FilterIter returns None if spks is empty --- crates/bitcoind_rpc/src/bip158.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/bitcoind_rpc/src/bip158.rs b/crates/bitcoind_rpc/src/bip158.rs index bb2bd1775..78278bcc6 100644 --- a/crates/bitcoind_rpc/src/bip158.rs +++ b/crates/bitcoind_rpc/src/bip158.rs @@ -177,6 +177,10 @@ impl<'c, C: RpcApi> Iterator for FilterIter<'c, C> { type Item = Result; fn next(&mut self) -> Option { + if self.spks.is_empty() { + return None; + } + let (block, filter) = self.next_filter.clone()?; (|| -> Result<_, Error> { @@ -184,9 +188,7 @@ impl<'c, C: RpcApi> Iterator for FilterIter<'c, C> { // and return it, inserting relevant block ids along the way let height = block.height; let hash = block.hash; - let event = if self.spks.is_empty() { - Event::NoMatch(height) - } else if filter + let event = if filter .match_any(&hash, self.spks.iter().map(|script| script.as_bytes())) .map_err(Error::Bip158)? {