The API now consists of the methods `new` and `next`.
The local checkpoint and SPK inventory are provided to
the constructor. `next` is now responsible for locating
the point of agreement.
The next filter to fetch is determined by the `next_block_hash`
field of `GetBlockHeaderResult`. If the next header has negative
confirmations due to a reorg, we rewind the internal state until
we find a header still in the best chain.
`Event` is changed to a simple struct containing `cp` and optional
`block`. The checkpoint is updated on each iteration whether
or not it corresponds to a matching block. We use
`CheckPoint::insert` which will also purge evicted blocks
if needed.
Change implementation of `find_base` to use `get_block_header_info`
which helps to reduce the number of RPC calls.
Removed `EventInner`.
Add test `event_checkpoint_connects_to_local_chain` to check
the expected events after a reorg, and check that intermediate
updates can be applied to the local chain.
Co-authored-by: Musab1258 <habeebmusab@gmail.com>
Co-authored-by: Wei Chen <wzc110@gmail.com>
Co-authored-by: 志宇 <hello@evanlinjin.me>
-#![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
// 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.
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}");
}
}
}
}
- 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(())
}
//! [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;
+use bitcoincore_rpc::{json::GetBlockHeaderResult, RpcApi};
-/// Type that generates block [`Event`]s by matching a list of script pubkeys against a
-/// [`BlockFilter`].
+/// Type that returns Bitcoin blocks by matching a list of script pubkeys (SPKs) against a
+/// [`bip158::BlockFilter`].
#[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<ScriptBuf>,
- // local cp
- cp: Option<CheckPoint>,
- // blocks map
- blocks: BTreeMap<Height, BlockHash>,
- // 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<GetBlockHeaderResult>,
}
-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<Item = ScriptBuf>,
+ ) -> Self {
Self {
client,
- spks: vec![],
- cp: None,
- blocks: BTreeMap::new(),
- height,
- stop: 0,
+ spks: spks.into_iter().collect(),
+ cp,
+ header: None,
}
}
- /// 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<Item = ScriptBuf>) {
- 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<Option<NextFilter>, 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.
+ /// 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<Option<BlockId>, 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<GetBlockHeaderResult, Error> {
+ 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<Block>,
}
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<C: RpcApi> Iterator for FilterIter<'_, C> {
+impl Iterator for FilterIter<'_> {
type Item = Result<Event, Error>;
fn next(&mut self) -> Option<Self::Item> {
- (|| -> 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<Option<_>, 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<C: RpcApi> FilterIter<'_, C> {
- /// Returns the point of agreement between `self` and the given `cp`.
- fn find_base_with(&mut self, mut cp: CheckPoint) -> Result<BlockId, Error> {
- 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<CheckPoint> {
- 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<bitcoincore_rpc::Error> 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<core::num::TryFromIntError> 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
+ )
+}
-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<TestEnv> {
})
}
-// 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<BlockId> = (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<BlockId>,
- // expected blocks
- exp: Vec<BlockId>,
- }
+#[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<BlockHash> = [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<u32, BlockHash> = (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(())
}