From 69056ee6ed8005957f8c61a859cabe42ec95a362 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Mon, 26 Jan 2026 08:12:49 +0000 Subject: [PATCH 1/3] feat(core): Introduce `FromBlockHeader` trait This allows us to use any subset of a `Header` to construct checkpoint data. Currently, only `BlockHash` and `Header` implement this. Chain sources can bound the checkpoint data generic to this trait, so all checkpoint data types that implement `FromBlockHeader` is supported by the chain source. --- crates/core/src/checkpoint.rs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/crates/core/src/checkpoint.rs b/crates/core/src/checkpoint.rs index 0a394122a..36bd8cbf9 100644 --- a/crates/core/src/checkpoint.rs +++ b/crates/core/src/checkpoint.rs @@ -55,6 +55,24 @@ impl Drop for CPInner { } } +/// Trait that converts [`Header`] to subset data. +pub trait FromBlockHeader { + /// Returns the subset data from a block `header`. + fn from_blockheader(header: Header) -> Self; +} + +impl FromBlockHeader for BlockHash { + fn from_blockheader(header: Header) -> Self { + header.block_hash() + } +} + +impl FromBlockHeader for Header { + fn from_blockheader(header: Header) -> Self { + header + } +} + /// Trait that converts [`CheckPoint`] `data` to [`BlockHash`]. /// /// Implementations of [`ToBlockHash`] must always return the block's consensus-defined hash. If @@ -204,7 +222,7 @@ impl CheckPoint { // Methods where `D: ToBlockHash` impl CheckPoint where - D: ToBlockHash + fmt::Debug + Copy, + D: ToBlockHash + fmt::Debug + Clone, { const MTP_BLOCK_COUNT: u32 = 11; From c989f8f165d7a6f24cd17ac11bd8aad9bef0c077 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Mon, 26 Jan 2026 11:52:31 +0000 Subject: [PATCH 2/3] feat(bitcoind_rpc)!: Emitters support different checkpoint data types --- crates/bitcoind_rpc/src/bip158.rs | 141 +++++++++++------- crates/bitcoind_rpc/src/lib.rs | 69 ++++++--- crates/bitcoind_rpc/tests/test_filter_iter.rs | 4 +- 3 files changed, 131 insertions(+), 83 deletions(-) diff --git a/crates/bitcoind_rpc/src/bip158.rs b/crates/bitcoind_rpc/src/bip158.rs index 81963def6..15387b68b 100644 --- a/crates/bitcoind_rpc/src/bip158.rs +++ b/crates/bitcoind_rpc/src/bip158.rs @@ -6,8 +6,13 @@ //! [0]: https://github.com/bitcoin/bips/blob/master/bip-0157.mediawiki //! [1]: https://github.com/bitcoin/bips/blob/master/bip-0158.mediawiki +use core::fmt::Debug; + use bdk_core::bitcoin; use bdk_core::CheckPoint; +use bdk_core::FromBlockHeader; +use bdk_core::ToBlockHash; +use bitcoin::block::Header; use bitcoin::BlockHash; use bitcoin::{bip158::BlockFilter, Block, ScriptBuf}; use bitcoincore_rpc; @@ -29,22 +34,25 @@ use bitcoincore_rpc::{json::GetBlockHeaderResult, RpcApi}; /// 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<'a> { +pub struct FilterIter<'a, D = BlockHash> { /// RPC client client: &'a bitcoincore_rpc::Client, /// SPK inventory spks: Vec, /// checkpoint - cp: CheckPoint, + cp: CheckPoint, /// Header info, contains the prev and next hashes for each header. header: Option, } -impl<'a> FilterIter<'a> { +impl<'a, D> FilterIter<'a, D> +where + D: ToBlockHash + Clone + Debug, +{ /// Construct [`FilterIter`] with checkpoint, RPC client and SPKs. pub fn new( client: &'a bitcoincore_rpc::Client, - cp: CheckPoint, + cp: CheckPoint, spks: impl IntoIterator, ) -> Self { Self { @@ -69,13 +77,76 @@ impl<'a> FilterIter<'a> { } Err(Error::ReorgDepthExceeded) } + + fn try_next_with(&mut self, to_data: F) -> Result>, Error> + where + F: Fn(Header) -> D, + { + 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()?, + }; + + let mut next_hash = match header.next_block_hash { + Some(hash) => hash, + None => return Ok(None), + }; + + 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; + } + + next_hash = next_header.hash; + let next_height: u32 = next_header.height.try_into()?; + + cp = cp.insert( + next_height, + to_data(self.client.get_block_header(&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)?); + } + + // Store the next header + self.header = Some(next_header); + // Update self.cp + self.cp = cp.clone(); + + Ok(Some(Event { cp, block })) + } + + /// Get the next event with a custom checkpoint data type. + pub fn next_with(&mut self, to_data: F) -> Option, Error>> + where + F: Fn(Header) -> D, + { + self.try_next_with(to_data).transpose() + } } /// Event returned by [`FilterIter`]. #[derive(Debug, Clone)] -pub struct Event { +pub struct Event { /// Checkpoint - pub cp: CheckPoint, + pub cp: CheckPoint, /// Block, will be `Some(..)` for matching blocks pub block: Option, } @@ -92,60 +163,14 @@ impl Event { } } -impl Iterator for FilterIter<'_> { - type Item = Result; +impl Iterator for FilterIter<'_, D> +where + D: ToBlockHash + FromBlockHeader + Clone + Debug, +{ + type Item = Result, Error>; fn next(&mut self) -> Option { - (|| -> 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()?, - }; - - let mut next_hash = match header.next_block_hash { - Some(hash) => hash, - None => return Ok(None), - }; - - 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; - } - - next_hash = next_header.hash; - let next_height: u32 = next_header.height.try_into()?; - - cp = cp.insert(next_height, 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)?); - } - - // Store the next header - self.header = Some(next_header); - // Update self.cp - self.cp = cp.clone(); - - Ok(Some(Event { cp, block })) - })() - .transpose() + self.try_next_with(D::from_blockheader).transpose() } } diff --git a/crates/bitcoind_rpc/src/lib.rs b/crates/bitcoind_rpc/src/lib.rs index 30fc556be..831e48e60 100644 --- a/crates/bitcoind_rpc/src/lib.rs +++ b/crates/bitcoind_rpc/src/lib.rs @@ -16,10 +16,11 @@ extern crate alloc; use alloc::sync::Arc; use bdk_core::collections::{HashMap, HashSet}; -use bdk_core::{BlockId, CheckPoint}; +use bdk_core::{BlockId, CheckPoint, FromBlockHeader, ToBlockHash}; use bitcoin::{Block, BlockHash, Transaction, Txid}; use bitcoincore_rpc::{bitcoincore_rpc_json, RpcApi}; use core::ops::Deref; +use std::fmt::Debug; pub mod bip158; @@ -30,13 +31,13 @@ pub use bitcoincore_rpc; /// Refer to [module-level documentation] for more. /// /// [module-level documentation]: crate -pub struct Emitter { +pub struct Emitter { client: C, start_height: u32, /// The checkpoint of the last-emitted block that is in the best chain. If it is later found /// that the block is no longer in the best chain, it will be popped off from here. - last_cp: CheckPoint, + last_cp: CheckPoint, /// The block result returned from rpc of the last-emitted block. As this result contains the /// next block's block hash (which we use to fetch the next block), we set this to `None` @@ -62,10 +63,11 @@ pub struct Emitter { /// to start empty (i.e. with no unconfirmed transactions). pub const NO_EXPECTED_MEMPOOL_TXS: core::iter::Empty> = core::iter::empty(); -impl Emitter +impl Emitter where C: Deref, C::Target: RpcApi, + D: ToBlockHash + Clone + Debug, { /// Construct a new [`Emitter`]. /// @@ -80,7 +82,7 @@ where /// If it is known that the wallet is empty, [`NO_EXPECTED_MEMPOOL_TXS`] can be used. pub fn new( client: C, - last_cp: CheckPoint, + last_cp: CheckPoint, start_height: u32, expected_mempool_txs: impl IntoIterator>>, ) -> Self { @@ -198,9 +200,18 @@ where Ok(mempool_event) } - /// Emit the next block height and block (if any). - pub fn next_block(&mut self) -> Result>, bitcoincore_rpc::Error> { - if let Some((checkpoint, block)) = poll(self, move |hash, client| client.get_block(hash))? { + /// Emit the next block, using `to_data` to construct checkpoint data from the block. + /// + /// This is the alternative to [`next_block`](Self::next_block) when [`FromBlockHeader`] isn't + /// implemented for `D`. + pub fn next_block_with( + &mut self, + to_data: F, + ) -> Result>, bitcoincore_rpc::Error> + where + F: Fn(&Block) -> D, + { + if let Some((checkpoint, block)) = poll(self, to_data)? { // Stop tracking unconfirmed transactions that have been confirmed in this block. for tx in &block.txdata { self.mempool_snapshot.remove(&tx.compute_txid()); @@ -209,6 +220,14 @@ where } Ok(None) } + + /// Emit the next block height and block (if any). + pub fn next_block(&mut self) -> Result>, bitcoincore_rpc::Error> + where + D: FromBlockHeader, + { + self.next_block_with(|block| D::from_blockheader(block.header)) + } } /// A new emission from mempool. @@ -223,7 +242,7 @@ pub struct MempoolEvent { /// A newly emitted block from [`Emitter`]. #[derive(Debug)] -pub struct BlockEvent { +pub struct BlockEvent { /// The block. pub block: B, @@ -235,7 +254,7 @@ pub struct BlockEvent { /// /// This is important as BDK structures require block-to-apply to be connected with another /// block in the original chain. - pub checkpoint: CheckPoint, + pub checkpoint: CheckPoint, } impl BlockEvent { @@ -264,17 +283,17 @@ impl BlockEvent { } } -enum PollResponse { +enum PollResponse { Block(bitcoincore_rpc_json::GetBlockResult), NoMoreBlocks, /// Fetched block is not in the best chain. BlockNotInBestChain, - AgreementFound(bitcoincore_rpc_json::GetBlockResult, CheckPoint), + AgreementFound(bitcoincore_rpc_json::GetBlockResult, CheckPoint), /// Force the genesis checkpoint down the receiver's throat. AgreementPointNotFound(BlockHash), } -fn poll_once(emitter: &Emitter) -> Result +fn poll_once(emitter: &Emitter) -> Result, bitcoincore_rpc::Error> where C: Deref, C::Target: RpcApi, @@ -328,30 +347,32 @@ where Ok(PollResponse::AgreementPointNotFound(genesis_hash)) } -fn poll( - emitter: &mut Emitter, - get_item: F, -) -> Result, V)>, bitcoincore_rpc::Error> +fn poll( + emitter: &mut Emitter, + to_cp_data: F, +) -> Result, Block)>, bitcoincore_rpc::Error> where C: Deref, C::Target: RpcApi, - F: Fn(&BlockHash, &C::Target) -> Result, + D: ToBlockHash + Clone + Debug, + F: Fn(&Block) -> D, { + let client = &emitter.client; loop { match poll_once(emitter)? { PollResponse::Block(res) => { let height = res.height as u32; - let hash = res.hash; - let item = get_item(&hash, &emitter.client)?; + let block = client.get_block(&res.hash)?; + let cp_data = to_cp_data(&block); let new_cp = emitter .last_cp .clone() - .push(height, hash) + .push(height, cp_data) .expect("must push"); emitter.last_cp = new_cp.clone(); emitter.last_block = Some(res); - return Ok(Some((new_cp, item))); + return Ok(Some((new_cp, block))); } PollResponse::NoMoreBlocks => { emitter.last_block = None; @@ -368,7 +389,9 @@ where continue; } PollResponse::AgreementPointNotFound(genesis_hash) => { - emitter.last_cp = CheckPoint::new(0, genesis_hash); + let block = client.get_block(&genesis_hash)?; + let cp_data = to_cp_data(&block); + emitter.last_cp = CheckPoint::new(0, cp_data); emitter.last_block = None; continue; } diff --git a/crates/bitcoind_rpc/tests/test_filter_iter.rs b/crates/bitcoind_rpc/tests/test_filter_iter.rs index eafe8250b..6a4028a69 100644 --- a/crates/bitcoind_rpc/tests/test_filter_iter.rs +++ b/crates/bitcoind_rpc/tests/test_filter_iter.rs @@ -1,7 +1,7 @@ use bdk_bitcoind_rpc::bip158::{Error, FilterIter}; use bdk_core::CheckPoint; use bdk_testenv::{anyhow, bitcoind, TestEnv}; -use bitcoin::{Address, Amount, Network, ScriptBuf}; +use bitcoin::{Address, Amount, BlockHash, Network, ScriptBuf}; use bitcoincore_rpc::RpcApi; fn testenv() -> anyhow::Result { @@ -59,7 +59,7 @@ fn filter_iter_error_wrong_network() -> anyhow::Result<()> { let _ = env.mine_blocks(10, None)?; // Try to initialize FilterIter with a CP on the wrong network - let cp = CheckPoint::new(0, bitcoin::hashes::Hash::hash(b"wrong-hash")); + let cp = CheckPoint::::new(0, bitcoin::hashes::Hash::hash(b"wrong-hash")); let mut iter = FilterIter::new(&env.bitcoind.client, cp, [ScriptBuf::new()]); assert!(matches!(iter.next(), Some(Err(Error::ReorgDepthExceeded)))); From 12f345b924c2d1ea8287f066dc12668767cc8b92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Tue, 27 Jan 2026 09:05:18 +0000 Subject: [PATCH 3/3] feat(electrum,examples)!: Sync/full-scan with custom checkpoint data --- crates/electrum/src/bdk_electrum_client.rs | 187 +++++++++++++++------ examples/example_electrum/src/main.rs | 2 +- 2 files changed, 140 insertions(+), 49 deletions(-) diff --git a/crates/electrum/src/bdk_electrum_client.rs b/crates/electrum/src/bdk_electrum_client.rs index 05b501871..d89fa54d1 100644 --- a/crates/electrum/src/bdk_electrum_client.rs +++ b/crates/electrum/src/bdk_electrum_client.rs @@ -4,11 +4,11 @@ use bdk_core::{ spk_client::{ FullScanRequest, FullScanResponse, SpkWithExpectedTxids, SyncRequest, SyncResponse, }, - BlockId, CheckPoint, ConfirmationBlockTime, TxUpdate, + BlockId, CheckPoint, ConfirmationBlockTime, FromBlockHeader, ToBlockHash, TxUpdate, }; use electrum_client::{ElectrumApi, Error, HeaderNotification}; -use std::convert::TryInto; use std::sync::{Arc, Mutex}; +use std::{convert::TryInto, fmt::Debug}; /// We include a chain suffix of a certain length for the purpose of robustness. const CHAIN_SUFFIX_LENGTH: u32 = 8; @@ -111,18 +111,53 @@ impl BdkElectrumClient { /// [`CalculateFeeError::MissingTxOut`]: ../bdk_chain/tx_graph/enum.CalculateFeeError.html#variant.MissingTxOut /// [`Wallet.calculate_fee`]: ../bdk_wallet/struct.Wallet.html#method.calculate_fee /// [`Wallet.calculate_fee_rate`]: ../bdk_wallet/struct.Wallet.html#method.calculate_fee_rate - pub fn full_scan( + pub fn full_scan( &self, - request: impl Into>, + request: impl Into>, stop_gap: usize, batch_size: usize, fetch_prev_txouts: bool, - ) -> Result, Error> { - let mut request: FullScanRequest = request.into(); + ) -> Result, Error> + where + K: Ord + Clone, + D: ToBlockHash + FromBlockHeader + Debug + Clone, + { + self.full_scan_with( + request, + stop_gap, + batch_size, + fetch_prev_txouts, + D::from_blockheader, + ) + } + + /// Performs a full scan with a custom checkpoint data type `D`. + /// + /// This is equivalent to [`full_scan`](Self::full_scan) with an additional `to_data` closure + /// that you have to pass in. `to_data` should convert a block header to the checkpoint data + /// type. + pub fn full_scan_with( + &self, + request: impl Into>, + stop_gap: usize, + batch_size: usize, + fetch_prev_txouts: bool, + to_data: F, + ) -> Result, Error> + where + K: Ord + Clone, + D: ToBlockHash + Debug + Clone, + F: Fn(Header) -> D, + { + let mut request: FullScanRequest = request.into(); let start_time = request.start_time(); let tip_and_latest_blocks = match request.chain_tip() { - Some(chain_tip) => Some(fetch_tip_and_latest_blocks(&self.inner, chain_tip)?), + Some(chain_tip) => Some(fetch_tip_and_latest_blocks( + &self.inner, + chain_tip, + &to_data, + )?), None => None, }; @@ -158,11 +193,16 @@ impl BdkElectrumClient { } let chain_update = match tip_and_latest_blocks { - Some((chain_tip, latest_blocks)) => Some(chain_update( - chain_tip, - &latest_blocks, - tx_update.anchors.iter().cloned(), - )?), + Some((chain_tip, latest_blocks)) => { + let header_cache = self.block_header_cache.lock().unwrap(); + Some(chain_update( + chain_tip, + &latest_blocks, + &header_cache, + tx_update.anchors.iter().cloned(), + to_data, + )?) + } _ => None, }; @@ -195,17 +235,42 @@ impl BdkElectrumClient { /// [`CalculateFeeError::MissingTxOut`]: ../bdk_chain/tx_graph/enum.CalculateFeeError.html#variant.MissingTxOut /// [`Wallet.calculate_fee`]: ../bdk_wallet/struct.Wallet.html#method.calculate_fee /// [`Wallet.calculate_fee_rate`]: ../bdk_wallet/struct.Wallet.html#method.calculate_fee_rate - pub fn sync( + pub fn sync( + &self, + request: impl Into>, + batch_size: usize, + fetch_prev_txouts: bool, + ) -> Result, Error> + where + D: ToBlockHash + FromBlockHeader + Debug + Clone, + { + self.sync_with(request, batch_size, fetch_prev_txouts, D::from_blockheader) + } + + /// Performs a sync with a custom checkpoint data type `D`. + /// + /// This is equivalent to [`sync`](Self::sync) with an additional `to_data` closure that is + /// passed in. `to_data` should convert a block header to the checkpoint data type. + pub fn sync_with( &self, - request: impl Into>, + request: impl Into>, batch_size: usize, fetch_prev_txouts: bool, - ) -> Result { - let mut request: SyncRequest = request.into(); + to_data: F, + ) -> Result, Error> + where + D: ToBlockHash + Debug + Clone, + F: Fn(Header) -> D, + { + let mut request: SyncRequest = request.into(); let start_time = request.start_time(); let tip_and_latest_blocks = match request.chain_tip() { - Some(chain_tip) => Some(fetch_tip_and_latest_blocks(&self.inner, chain_tip)?), + Some(chain_tip) => Some(fetch_tip_and_latest_blocks( + &self.inner, + chain_tip, + &to_data, + )?), None => None, }; @@ -248,11 +313,16 @@ impl BdkElectrumClient { } let chain_update = match tip_and_latest_blocks { - Some((chain_tip, latest_blocks)) => Some(chain_update( - chain_tip, - &latest_blocks, - tx_update.anchors.iter().cloned(), - )?), + Some((chain_tip, latest_blocks)) => { + let header_cache = self.block_header_cache.lock().unwrap(); + Some(chain_update( + chain_tip, + &latest_blocks, + &header_cache, + tx_update.anchors.iter().cloned(), + to_data, + )?) + } None => None, }; @@ -605,10 +675,15 @@ impl BdkElectrumClient { /// Return a [`CheckPoint`] of the latest tip, that connects with `prev_tip`. The latest blocks are /// fetched to construct checkpoint updates with the proper [`BlockHash`] in case of re-org. -fn fetch_tip_and_latest_blocks( +fn fetch_tip_and_latest_blocks( client: &impl ElectrumApi, - prev_tip: CheckPoint, -) -> Result<(CheckPoint, BTreeMap), Error> { + prev_tip: CheckPoint, + to_data: F, +) -> Result<(CheckPoint, BTreeMap), Error> +where + D: ToBlockHash + Debug + Clone, + F: Fn(Header) -> D, +{ let HeaderNotification { height, .. } = client.block_headers_subscribe()?; let new_tip_height = height as u32; @@ -620,31 +695,29 @@ fn fetch_tip_and_latest_blocks( // Atomically fetch the latest `CHAIN_SUFFIX_LENGTH` count of blocks from Electrum. We use this // to construct our checkpoint update. - let mut new_blocks = { + let mut new_headers = { let start_height = new_tip_height.saturating_sub(CHAIN_SUFFIX_LENGTH - 1); - let hashes = client + let headers = client .block_headers(start_height as _, CHAIN_SUFFIX_LENGTH as _)? - .headers - .into_iter() - .map(|h| h.block_hash()); - (start_height..).zip(hashes).collect::>() + .headers; + (start_height..).zip(headers).collect::>() }; // Find the "point of agreement" (if any). let agreement_cp = { - let mut agreement_cp = Option::>::None; + let mut agreement_cp = Option::>::None; for cp in prev_tip.iter() { let cp_block = cp.block_id(); - let hash = match new_blocks.get(&cp_block.height) { - Some(&hash) => hash, + let hash = match new_headers.get(&cp_block.height) { + Some(header) => header.block_hash(), None => { assert!( new_tip_height >= cp_block.height, "already checked that electrum's tip cannot be smaller" ); - let hash = client.block_header(cp_block.height as _)?.block_hash(); - new_blocks.insert(cp_block.height, hash); - hash + let header = client.block_header(cp_block.height as _)?; + new_headers.insert(cp_block.height, header); + cp_block.hash } }; if hash == cp_block.hash { @@ -656,38 +729,56 @@ fn fetch_tip_and_latest_blocks( .ok_or_else(|| Error::Message("cannot find agreement block with server".to_string()))? }; - let extension = new_blocks + let extension = new_headers .iter() .filter({ let agreement_height = agreement_cp.height(); move |(height, _)| **height > agreement_height }) - .map(|(&height, &hash)| (height, hash)); + .map(|(&height, &header)| (height, to_data(header))); let new_tip = agreement_cp .extend(extension) .expect("extension heights already checked to be greater than agreement height"); - Ok((new_tip, new_blocks)) + Ok((new_tip, new_headers)) } // Add a corresponding checkpoint per anchor height if it does not yet exist. Checkpoints should not // surpass `latest_blocks`. -fn chain_update( - mut tip: CheckPoint, - latest_blocks: &BTreeMap, +fn chain_update( + mut tip: CheckPoint, + latest_headers: &BTreeMap, + block_header_cache: &HashMap, anchors: impl Iterator, -) -> Result, Error> { + to_data: F, +) -> Result, Error> +where + D: ToBlockHash + Debug + Clone, + F: Fn(Header) -> D, +{ for (anchor, _txid) in anchors { let height = anchor.block_id.height; // Checkpoint uses the `BlockHash` from `latest_blocks` so that the hash will be consistent // in case of a re-org. if tip.get(height).is_none() && height <= tip.height() { - let hash = match latest_blocks.get(&height) { - Some(&hash) => hash, - None => anchor.block_id.hash, + let header = match latest_headers.get(&height) { + Some(header) => Some(header), + None => { + let height = anchor.block_id.height; + let anchor_blockhash = anchor.block_id.hash; + block_header_cache + .get(&height) + // This check is necessary as `block_hash_header` is not in lockstep with + // the checkpoint chain. + // We assume reorg depths do not exceed `latest_blocks` so anchor + // blockhashes below `latect_blocks` are always in chain. + .filter(|header| header.block_hash() == anchor_blockhash) + } }; - tip = tip.insert(height, hash); + if let Some(header) = header { + tip = tip.insert(height, to_data(*header)); + } } } Ok(tip) diff --git a/examples/example_electrum/src/main.rs b/examples/example_electrum/src/main.rs index aa89f07e1..d4096c413 100644 --- a/examples/example_electrum/src/main.rs +++ b/examples/example_electrum/src/main.rs @@ -176,7 +176,7 @@ fn main() -> anyhow::Result<()> { }; let res = client - .full_scan::<_>(request, stop_gap, scan_options.batch_size, false) + .full_scan::<_, _>(request, stop_gap, scan_options.batch_size, false) .context("scanning the blockchain")?; ( res.chain_update,