diff --git a/crates/bitcoind_rpc/examples/filter_iter.rs b/crates/bitcoind_rpc/examples/filter_iter.rs index 61422f64b..174f21215 100644 --- a/crates/bitcoind_rpc/examples/filter_iter.rs +++ b/crates/bitcoind_rpc/examples/filter_iter.rs @@ -1,15 +1,14 @@ -#![allow(clippy::print_stdout)] +#![allow(clippy::print_stdout, clippy::print_stderr)] use std::time::Instant; use anyhow::Context; -use bdk_bitcoind_rpc::bip158::{Event, EventInner, FilterIter}; +use bdk_bitcoind_rpc::bip158::{Event, 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; -use bitcoin::Address; // This example shows how BDK chain and tx-graph structures are updated using compact // filters syncing. Assumes a connection can be made to a bitcoin node via environment @@ -17,13 +16,13 @@ use bitcoin::Address; // Usage: `cargo run -p bdk_bitcoind_rpc --example filter_iter` -const EXTERNAL: &str = "tr([7d94197e]tprv8ZgxMBicQKsPe1chHGzaa84k1inY2nAXUL8iPSyWESPrEst4E5oCFXhPATqj5fvw34LDknJz7rtXyEC4fKoXryUdc9q87pTTzfQyv61cKdE/86'/1'/0'/0/*)#uswl2jj7"; -const INTERNAL: &str = "tr([7d94197e]tprv8ZgxMBicQKsPe1chHGzaa84k1inY2nAXUL8iPSyWESPrEst4E5oCFXhPATqj5fvw34LDknJz7rtXyEC4fKoXryUdc9q87pTTzfQyv61cKdE/86'/1'/0'/1/*)#dyt7h8zx"; +const EXTERNAL: &str = "tr([83737d5e/86'/1'/0']tpubDDR5GgtoxS8fJyjjvdahN4VzV5DV6jtbcyvVXhEKq2XtpxjxBXmxH3r8QrNbQqHg4bJM1EGkxi7Pjfkgnui9jQWqS7kxHvX6rhUeriLDKxz/0/*)"; +const INTERNAL: &str = "tr([83737d5e/86'/1'/0']tpubDDR5GgtoxS8fJyjjvdahN4VzV5DV6jtbcyvVXhEKq2XtpxjxBXmxH3r8QrNbQqHg4bJM1EGkxi7Pjfkgnui9jQWqS7kxHvX6rhUeriLDKxz/1/*)"; const SPK_COUNT: u32 = 25; const NETWORK: Network = Network::Signet; -const START_HEIGHT: u32 = 170_000; -const START_HASH: &str = "00000041c812a89f084f633e4cf47e819a2f6b1c0a15162355a930410522c99d"; +const START_HEIGHT: u32 = 205_000; +const START_HASH: &str = "0000002bd0f82f8c0c0f1e19128f84c938763641dba85c44bdb6aed1678d16cb"; fn main() -> anyhow::Result<()> { // Setup receiving chain and graph structures. @@ -52,37 +51,22 @@ fn main() -> anyhow::Result<()> { let rpc_client = bitcoincore_rpc::Client::new(&url, bitcoincore_rpc::Auth::CookieFile(cookie.into()))?; - // Initialize block emitter - let cp = chain.tip(); - let start_height = cp.height(); - let mut emitter = FilterIter::new_with_checkpoint(&rpc_client, cp); + // Initialize `FilterIter` + let mut spks = vec![]; for (_, desc) in graph.index.keychains() { - let spks = SpkIterator::new_with_range(desc, 0..SPK_COUNT).map(|(_, spk)| spk); - emitter.add_spks(spks); + spks.extend(SpkIterator::new_with_range(desc, 0..SPK_COUNT).map(|(_, s)| s)); } + let iter = FilterIter::new(&rpc_client, chain.tip(), spks); let start = Instant::now(); - // Sync - if let Some(tip) = emitter.get_tip()? { - let blocks_to_scan = tip.height - start_height; - - for event in emitter.by_ref() { - 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 - if let Some(cp) = emitter.chain_update() { - let _ = chain.apply_update(cp)?; + for res in iter { + let Event { cp, block } = res?; + let height = cp.height(); + let _ = chain.apply_update(cp)?; + if let Some(block) = block { + let _ = graph.apply_block_relevant(&block, height); + println!("Matched block {height}"); } } @@ -105,9 +89,18 @@ fn main() -> anyhow::Result<()> { } } - let unused_spk = graph.index.reveal_next_spk("external").unwrap().0 .1; - let unused_address = Address::from_script(&unused_spk, NETWORK)?; - println!("Next external address: {unused_address}"); + for canon_tx in graph.graph().list_canonical_txs( + &chain, + chain.tip().block_id(), + bdk_chain::CanonicalizationParams::default(), + ) { + if !canon_tx.chain_position.is_confirmed() { + eprintln!( + "ERROR: canonical tx should be confirmed {}", + canon_tx.tx_node.txid + ); + } + } Ok(()) } diff --git a/crates/bitcoind_rpc/src/bip158.rs b/crates/bitcoind_rpc/src/bip158.rs index 5419716b1..9461cda7f 100644 --- a/crates/bitcoind_rpc/src/bip158.rs +++ b/crates/bitcoind_rpc/src/bip158.rs @@ -6,262 +6,195 @@ //! [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 bitcoin::{bip158::BlockFilter, Block, 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`]. +use bitcoincore_rpc::{json::GetBlockHeaderResult, RpcApi}; + +/// Type that returns Bitcoin blocks by matching a list of script pubkeys (SPKs) against a +/// [`bip158::BlockFilter`](bitcoin::bip158::BlockFilter). +/// +/// * `FilterIter` talks to bitcoind via JSON-RPC interface, which is handled by the +/// [`bitcoincore_rpc::Client`]. +/// * Collect the script pubkeys (SPKs) you want to watch. These will usually correspond to wallet +/// addresses that have been handed out for receiving payments. +/// * Construct `FilterIter` with the RPC client, SPKs, and [`CheckPoint`]. The checkpoint tip +/// informs `FilterIter` of the height to begin scanning from. An error is thrown if `FilterIter` +/// is unable to find a common ancestor with the remote node. +/// * Scan blocks by calling `next` in a loop and processing the [`Event`]s. If a filter matched any +/// of the watched scripts, then the relevant [`Block`] is returned. Note that false positives may +/// occur. `FilterIter` will continue to yield events until it reaches the latest chain tip. +/// Events contain the updated checkpoint `cp` which may be incorporated into the local chain +/// state to stay in sync with the tip. #[derive(Debug)] -pub struct FilterIter<'c, C> { - // RPC client - client: &'c C, - // SPK inventory +pub struct FilterIter<'a> { + /// RPC client + client: &'a bitcoincore_rpc::Client, + /// SPK inventory spks: Vec, - // local cp - cp: Option, - // blocks map - blocks: BTreeMap, - // best height counter - height: Height, - // stop height - stop: Height, + /// checkpoint + cp: CheckPoint, + /// Header info, contains the prev and next hashes for each header. + header: Option, } -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 { +impl<'a> FilterIter<'a> { + /// Construct [`FilterIter`] with checkpoint, RPC client and SPKs. + pub fn new( + client: &'a bitcoincore_rpc::Client, + cp: CheckPoint, + spks: impl IntoIterator, + ) -> Self { Self { client, - spks: vec![], - cp: None, - blocks: BTreeMap::new(), - 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); + spks: spks.into_iter().collect(), + cp, + header: 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. + /// Return the agreement header with the remote node. /// - /// Returns `None` if the remote height is not strictly greater than the height of this - /// [`FilterIter`]. - 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); + /// Error if no agreement header is found. + fn find_base(&self) -> Result { + for cp in self.cp.iter() { + match self.client.get_block_header_info(&cp.hash()) { + Err(e) if is_not_found(&e) => continue, + Ok(header) if header.confirmations <= 0 => continue, + Ok(header) => return Ok(header), + Err(e) => return Err(Error::Rpc(e)), } } - - self.stop = tip_height; - - Ok(Some(BlockId { - height: tip_height, - hash: tip_hash, - })) + Err(Error::ReorgDepthExceeded) } } -/// Alias for a compact filter and associated block id. -type NextFilter = (BlockId, BlockFilter); - -/// Event inner type +/// Event returned by [`FilterIter`]. #[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(Height), +pub struct Event { + /// Checkpoint + pub cp: CheckPoint, + /// Block, will be `Some(..)` for matching blocks + pub block: Option, } impl Event { /// Whether this event contains a matching block. pub fn is_match(&self) -> bool { - matches!(self, Event::Block(_)) + self.block.is_some() } - /// Get the height of this event. - pub fn height(&self) -> Height { - match self { - Self::Block(EventInner { height, .. }) => *height, - Self::NoMatch(h) => *h, - } + /// Return the height of the event. + pub fn height(&self) -> u32 { + self.cp.height() } } -impl Iterator for FilterIter<'_, C> { +impl Iterator for FilterIter<'_> { type Item = Result; fn next(&mut self) -> Option { - (|| -> 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 - self.next_filter()?.map_or(Ok(None), |(block, filter)| { - let height = block.height; - let hash = block.hash; - - if self.spks.is_empty() { - Err(Error::NoScripts) - } 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 }; - Ok(Some(Event::Block(inner))) - } else { - Ok(Some(Event::NoMatch(height))) - } - }) - })() - .transpose() - } -} + (|| -> Result, Error> { + let mut cp = self.cp.clone(); + + let header = match self.header.take() { + Some(header) => header, + // If no header is cached we need to locate a base of the local + // checkpoint from which the scan may proceed. + None => self.find_base()?, + }; -impl FilterIter<'_, 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 _)?, + let mut next_hash = match header.next_block_hash { + Some(hash) => hash, + None => return Ok(None), }; - if cp.hash() == fetched_hash { - // ensure this block also exists in self - self.blocks.insert(height, cp.hash()); - return Ok(cp.block_id()); + + let mut next_header = self.client.get_block_header_info(&next_hash)?; + + // In case of a reorg, rewind by fetching headers of previous hashes until we find + // one with enough confirmations. + while next_header.confirmations < 0 { + let prev_hash = next_header + .previous_block_hash + .ok_or(Error::ReorgDepthExceeded)?; + let prev_header = self.client.get_block_header_info(&prev_hash)?; + next_header = prev_header; } - // 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; - } + next_hash = next_header.hash; + let next_height: u32 = next_header.height.try_into()?; + + cp = cp.insert(BlockId { + height: next_height, + hash: next_hash, + }); + + let mut block = None; + let filter = + BlockFilter::new(self.client.get_block_filter(&next_hash)?.filter.as_slice()); + if filter + .match_any(&next_hash, self.spks.iter().map(ScriptBuf::as_ref)) + .map_err(Error::Bip158)? + { + block = Some(self.client.get_block(&next_hash)?); + } - // 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"), - ) + // Store the next header + self.header = Some(next_header); + // Update self.cp + self.cp = cp.clone(); + + Ok(Some(Event { cp, block })) + })() + .transpose() } } -/// Errors that may occur during a compact filters sync. +/// Error that may be thrown by [`FilterIter`]. #[derive(Debug)] pub enum Error { - /// bitcoin bip158 error - Bip158(bip158::Error), - /// attempted to scan blocks without any script pubkeys - NoScripts, - /// `bitcoincore_rpc` error + /// RPC error Rpc(bitcoincore_rpc::Error), + /// `bitcoin::bip158` error + Bip158(bitcoin::bip158::Error), + /// Max reorg depth exceeded. + ReorgDepthExceeded, + /// Error converting an integer + TryFromInt(core::num::TryFromIntError), } +impl core::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Rpc(e) => write!(f, "{e}"), + Self::Bip158(e) => write!(f, "{e}"), + Self::ReorgDepthExceeded => write!(f, "maximum reorg depth exceeded"), + Self::TryFromInt(e) => write!(f, "{e}"), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for 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::NoScripts => write!(f, "no script pubkeys were provided to match with"), - Self::Rpc(e) => e.fmt(f), - } +impl From for Error { + fn from(e: core::num::TryFromIntError) -> Self { + Self::TryFromInt(e) } } -#[cfg(feature = "std")] -impl std::error::Error for Error {} +/// Whether the RPC error is a "not found" error (code: `-5`). +fn is_not_found(e: &bitcoincore_rpc::Error) -> bool { + matches!( + e, + bitcoincore_rpc::Error::JsonRpc(bitcoincore_rpc::jsonrpc::Error::Rpc(e)) + if e.code == -5 + ) +} diff --git a/crates/bitcoind_rpc/tests/test_filter_iter.rs b/crates/bitcoind_rpc/tests/test_filter_iter.rs index c8d3335a2..10f01e612 100644 --- a/crates/bitcoind_rpc/tests/test_filter_iter.rs +++ b/crates/bitcoind_rpc/tests/test_filter_iter.rs @@ -1,8 +1,7 @@ -use bitcoin::{constants, Address, Amount, Network, ScriptBuf}; - -use bdk_bitcoind_rpc::bip158::FilterIter; +use bdk_bitcoind_rpc::bip158::{Error, FilterIter}; use bdk_core::{BlockId, CheckPoint}; -use bdk_testenv::{anyhow, bitcoind, block_id, TestEnv}; +use bdk_testenv::{anyhow, bitcoind, TestEnv}; +use bitcoin::{Address, Amount, Network, ScriptBuf}; use bitcoincore_rpc::RpcApi; fn testenv() -> anyhow::Result { @@ -15,151 +14,144 @@ fn testenv() -> anyhow::Result { }) } -// 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<()> { +fn filter_iter_matches_blocks() -> anyhow::Result<()> { let env = testenv()?; + let addr = env + .rpc_client() + .get_new_address(None, None)? + .assume_checked(); + + let _ = env.mine_blocks(100, Some(addr.clone()))?; + assert_eq!(env.rpc_client().get_block_count()?, 101); + + // Send tx to external address to confirm at height = 102 + let _txid = env.send( + &Address::from_script( + &ScriptBuf::from_hex("0014446906a6560d8ad760db3156706e72e171f3a2aa")?, + Network::Regtest, + )?, + Amount::from_btc(0.42)?, + )?; + let _ = env.mine_blocks(1, None); - let genesis_hash = constants::genesis_block(Network::Regtest).block_hash(); - let genesis = BlockId { + let genesis_hash = env.genesis_hash()?; + let cp = CheckPoint::new(BlockId { height: 0, hash: genesis_hash, - }; + }); - 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 iter = FilterIter::new(&env.bitcoind.client, cp, [addr.script_pubkey()]); - // `FilterIter` will try to return up to ten recent blocks - // so we keep them for reference - let new_blocks: Vec = (2..12) - .zip(env.mine_blocks(10, None)?) - .map(BlockId::from) - .collect(); + for res in iter { + let event = res?; + let height = event.height(); + if (2..102).contains(&height) { + assert!(event.is_match(), "expected to match height {height}"); + } else { + assert!(!event.is_match()); + } + } - let new_tip = *new_blocks.last().unwrap(); + Ok(()) +} - struct TestCase { - // name - name: &'static str, - // local blocks - chain: Vec, - // expected blocks - exp: Vec, - } +#[test] +fn filter_iter_error_wrong_network() -> anyhow::Result<()> { + let env = testenv()?; + let _ = env.mine_blocks(10, None)?; - // 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 iter = FilterIter::new_with_checkpoint(env.rpc_client(), cp); - assert_eq!(iter.get_tip().unwrap(), Some(new_tip)); - let update_cp = iter.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); - }); + // Try to initialize FilterIter with a CP on the wrong network + let block_id = BlockId { + height: 0, + hash: bitcoin::hashes::Hash::hash(b"wrong-hash"), + }; + let cp = CheckPoint::new(block_id); + let mut iter = FilterIter::new(&env.bitcoind.client, cp, [ScriptBuf::new()]); + assert!(matches!(iter.next(), Some(Err(Error::ReorgDepthExceeded)))); Ok(()) } +// Test that while a reorg is detected we delay incrementing the best height #[test] -fn filter_iter_returns_matched_blocks() -> anyhow::Result<()> { - use bdk_bitcoind_rpc::bip158::{Event, EventInner}; +fn filter_iter_detects_reorgs() -> anyhow::Result<()> { + const MINE_TO: u32 = 16; + let env = testenv()?; let rpc = env.rpc_client(); - while rpc.get_block_count()? < 101 { + while rpc.get_block_count()? < MINE_TO as u64 { let _ = env.mine_blocks(1, None)?; } - // send tx - let spk = ScriptBuf::from_hex("0014446906a6560d8ad760db3156706e72e171f3a2aa")?; - let txid = env.send( - &Address::from_script(&spk, Network::Regtest)?, - Amount::from_btc(0.42)?, - )?; - let _ = env.mine_blocks(1, None); + let genesis_hash = env.genesis_hash()?; + let cp = CheckPoint::new(BlockId { + height: 0, + hash: genesis_hash, + }); - // match blocks - let mut iter = FilterIter::new_with_height(rpc, 1); - iter.add_spk(spk); - assert_eq!(iter.get_tip()?.unwrap().height, 102); + let spk = ScriptBuf::from_hex("0014446906a6560d8ad760db3156706e72e171f3a2aa")?; + let mut iter = FilterIter::new(&env.bitcoind.client, cp, [spk]); - for res in iter { - let event = res?; - match event { - event if event.height() <= 101 => assert!(!event.is_match()), - Event::Block(EventInner { height, block }) => { - assert_eq!(height, 102); - assert!(block.txdata.iter().any(|tx| tx.compute_txid() == txid)); - } - Event::NoMatch(_) => panic!("expected to match block 102"), + // Process events to height (MINE_TO - 1) + loop { + if iter.next().unwrap()?.height() == MINE_TO - 1 { + break; } } + for _ in 0..3 { + // Invalidate and remine 1 block + let _ = env.reorg(1)?; + + // Call next. If we detect a reorg, we'll see no change in the event height + assert_eq!(iter.next().unwrap()?.height(), MINE_TO - 1); + } + + // If no reorg, then height should increment normally from here on + assert_eq!(iter.next().unwrap()?.height(), MINE_TO); + assert!(iter.next().is_none()); + Ok(()) } #[test] -fn filter_iter_error_no_scripts() -> anyhow::Result<()> { - use bdk_bitcoind_rpc::bip158::Error; +fn event_checkpoint_connects_to_local_chain() -> anyhow::Result<()> { + use bitcoin::BlockHash; + use std::collections::BTreeMap; let env = testenv()?; - let _ = env.mine_blocks(2, None)?; + let _ = env.mine_blocks(15, None)?; + + let cp = env.make_checkpoint_tip(); + let mut chain = bdk_chain::local_chain::LocalChain::from_tip(cp.clone())?; + assert_eq!(chain.tip().height(), 16); + let old_hashes: Vec = [14, 15, 16] + .into_iter() + .map(|height| chain.get(height).unwrap().hash()) + .collect(); - let mut iter = FilterIter::new_with_height(env.rpc_client(), 1); - assert_eq!(iter.get_tip()?.unwrap().height, 3); + // Construct iter + let mut iter = FilterIter::new(&env.bitcoind.client, cp, vec![ScriptBuf::new()]); - // iterator should return three errors - for _ in 0..3 { - assert!(matches!(iter.next().unwrap(), Err(Error::NoScripts))); + // Now reorg 3 blocks (14, 15, 16) + let new_hashes: BTreeMap = (14..=16).zip(env.reorg(3)?).collect(); + + // Expect events from height 14 on... + while let Some(event) = iter.next().transpose()? { + let _ = chain + .apply_update(event.cp) + .expect("chain update should connect"); + } + + for height in 14..=16 { + let hash = chain.get(height).unwrap().hash(); + assert!(!old_hashes.contains(&hash), "Old hashes were reorged out"); + assert_eq!( + new_hashes.get(&height), + Some(&hash), + "Chain must include new hashes" + ); } - assert!(iter.next().is_none()); Ok(()) }