]> Untitled Git - bdk/commitdiff
refactor!: make `CheckPoint`, `LocalChain`, and `SpkClient` take a generic
authorWei Chen <wzc110@gmail.com>
Sat, 31 Aug 2024 11:06:44 +0000 (19:06 +0800)
committer志宇 <hello@evanlinjin.me>
Sun, 14 Sep 2025 23:20:01 +0000 (23:20 +0000)
25 files changed:
crates/bitcoind_rpc/examples/filter_iter.rs
crates/bitcoind_rpc/src/bip158.rs
crates/bitcoind_rpc/src/lib.rs
crates/bitcoind_rpc/tests/test_emitter.rs
crates/bitcoind_rpc/tests/test_filter_iter.rs
crates/chain/benches/canonicalization.rs
crates/chain/benches/indexer.rs
crates/chain/src/local_chain.rs
crates/chain/src/tx_data_traits.rs
crates/chain/src/tx_graph.rs
crates/chain/tests/test_indexed_tx_graph.rs
crates/chain/tests/test_local_chain.rs
crates/chain/tests/test_tx_graph_conflicts.rs
crates/core/src/checkpoint.rs
crates/core/src/spk_client.rs
crates/core/tests/test_checkpoint.rs
crates/electrum/benches/test_sync.rs
crates/electrum/src/bdk_electrum_client.rs
crates/electrum/tests/test_electrum.rs
crates/esplora/src/async_ext.rs
crates/esplora/src/blocking_ext.rs
crates/esplora/tests/async_ext.rs
crates/esplora/tests/blocking_ext.rs
crates/testenv/src/lib.rs
examples/example_cli/src/lib.rs

index 174f212157263ca5ee2a2179038058272e6ba376..c0e755f9ce44fa477cb9148f27eba3bab4baf3b8 100644 (file)
@@ -7,7 +7,7 @@ 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_chain::{ConfirmationBlockTime, IndexedTxGraph, SpkIterator};
 use bdk_testenv::anyhow;
 
 // This example shows how BDK chain and tx-graph structures are updated using compact
@@ -29,7 +29,7 @@ fn main() -> anyhow::Result<()> {
     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 chain, _) = LocalChain::from_genesis(genesis_block(NETWORK).block_hash());
 
     let mut graph = IndexedTxGraph::<ConfirmationBlockTime, KeychainTxOutIndex<&str>>::new({
         let mut index = KeychainTxOutIndex::default();
@@ -39,11 +39,7 @@ fn main() -> anyhow::Result<()> {
     });
 
     // Assume a minimum birthday height
-    let block = BlockId {
-        height: START_HEIGHT,
-        hash: START_HASH.parse()?,
-    };
-    let _ = chain.insert_block(block)?;
+    let _ = chain.insert_block(START_HEIGHT, START_HASH.parse()?)?;
 
     // Configure RPC client
     let url = std::env::var("RPC_URL").context("must set RPC_URL")?;
index 9461cda7f1e6bf8e4fd6af3e9ebfbd23a9225ee4..81963def6d8c1687ed3d8b4c70c1f6a46d5944f9 100644 (file)
@@ -7,7 +7,8 @@
 //! [1]: https://github.com/bitcoin/bips/blob/master/bip-0158.mediawiki
 
 use bdk_core::bitcoin;
-use bdk_core::{BlockId, CheckPoint};
+use bdk_core::CheckPoint;
+use bitcoin::BlockHash;
 use bitcoin::{bip158::BlockFilter, Block, ScriptBuf};
 use bitcoincore_rpc;
 use bitcoincore_rpc::{json::GetBlockHeaderResult, RpcApi};
@@ -34,7 +35,7 @@ pub struct FilterIter<'a> {
     /// SPK inventory
     spks: Vec<ScriptBuf>,
     /// checkpoint
-    cp: CheckPoint,
+    cp: CheckPoint<BlockHash>,
     /// Header info, contains the prev and next hashes for each header.
     header: Option<GetBlockHeaderResult>,
 }
@@ -125,10 +126,7 @@ impl Iterator for FilterIter<'_> {
             next_hash = next_header.hash;
             let next_height: u32 = next_header.height.try_into()?;
 
-            cp = cp.insert(BlockId {
-                height: next_height,
-                hash: next_hash,
-            });
+            cp = cp.insert(next_height, next_hash);
 
             let mut block = None;
             let filter =
index eb58018e93c1239dd912eba34a0ae59292f0eea8..30fc556bea12a32ef41b351e31c9376dd20857a2 100644 (file)
@@ -36,7 +36,7 @@ pub struct Emitter<C> {
 
     /// 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<BlockHash>,
 
     /// 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`
@@ -80,7 +80,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<BlockHash>,
         start_height: u32,
         expected_mempool_txs: impl IntoIterator<Item = impl Into<Arc<Transaction>>>,
     ) -> Self {
@@ -235,7 +235,7 @@ pub struct BlockEvent<B> {
     ///
     /// 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<BlockHash>,
 }
 
 impl<B> BlockEvent<B> {
@@ -269,7 +269,7 @@ enum PollResponse {
     NoMoreBlocks,
     /// Fetched block is not in the best chain.
     BlockNotInBestChain,
-    AgreementFound(bitcoincore_rpc_json::GetBlockResult, CheckPoint),
+    AgreementFound(bitcoincore_rpc_json::GetBlockResult, CheckPoint<BlockHash>),
     /// Force the genesis checkpoint down the receiver's throat.
     AgreementPointNotFound(BlockHash),
 }
@@ -331,7 +331,7 @@ where
 fn poll<C, V, F>(
     emitter: &mut Emitter<C>,
     get_item: F,
-) -> Result<Option<(CheckPoint, V)>, bitcoincore_rpc::Error>
+) -> Result<Option<(CheckPoint<BlockHash>, V)>, bitcoincore_rpc::Error>
 where
     C: Deref,
     C::Target: RpcApi,
@@ -347,7 +347,7 @@ where
                 let new_cp = emitter
                     .last_cp
                     .clone()
-                    .push(BlockId { height, hash })
+                    .push(height, hash)
                     .expect("must push");
                 emitter.last_cp = new_cp.clone();
                 emitter.last_block = Some(res);
@@ -368,10 +368,7 @@ where
                 continue;
             }
             PollResponse::AgreementPointNotFound(genesis_hash) => {
-                emitter.last_cp = CheckPoint::new(BlockId {
-                    height: 0,
-                    hash: genesis_hash,
-                });
+                emitter.last_cp = CheckPoint::new(0, genesis_hash);
                 emitter.last_block = None;
                 continue;
             }
@@ -411,7 +408,7 @@ mod test {
     #[test]
     fn test_expected_mempool_txids_accumulate_and_remove() -> anyhow::Result<()> {
         let env = TestEnv::new()?;
-        let chain = LocalChain::from_genesis_hash(env.rpc_client().get_block_hash(0)?).0;
+        let chain = LocalChain::from_genesis(env.rpc_client().get_block_hash(0)?).0;
         let chain_tip = chain.tip();
         let mut emitter = Emitter::new(
             env.rpc_client(),
index 9df3b404c02f704195205906f4bcfe579688dd71..079551bf01ce834edc21e05c18c65f0ca22d778f 100644 (file)
@@ -21,7 +21,7 @@ use bitcoincore_rpc::RpcApi;
 pub fn test_sync_local_chain() -> anyhow::Result<()> {
     let env = TestEnv::new()?;
     let network_tip = env.rpc_client().get_block_count()?;
-    let (mut local_chain, _) = LocalChain::from_genesis_hash(env.rpc_client().get_block_hash(0)?);
+    let (mut local_chain, _) = LocalChain::from_genesis(env.rpc_client().get_block_hash(0)?);
     let mut emitter = Emitter::new(
         env.rpc_client(),
         local_chain.tip(),
@@ -152,7 +152,7 @@ fn test_into_tx_graph() -> anyhow::Result<()> {
 
     env.mine_blocks(101, None)?;
 
-    let (mut chain, _) = LocalChain::from_genesis_hash(env.rpc_client().get_block_hash(0)?);
+    let (mut chain, _) = LocalChain::from_genesis(env.rpc_client().get_block_hash(0)?);
     let mut indexed_tx_graph = IndexedTxGraph::<BlockId, _>::new({
         let mut index = SpkTxOutIndex::<usize>::default();
         index.insert_spk(0, addr_0.script_pubkey());
@@ -252,10 +252,7 @@ fn ensure_block_emitted_after_reorg_is_at_reorg_height() -> anyhow::Result<()> {
     let env = TestEnv::new()?;
     let mut emitter = Emitter::new(
         env.rpc_client(),
-        CheckPoint::new(BlockId {
-            height: 0,
-            hash: env.rpc_client().get_block_hash(0)?,
-        }),
+        CheckPoint::new(0, env.rpc_client().get_block_hash(0)?),
         EMITTER_START_HEIGHT as _,
         NO_EXPECTED_MEMPOOL_TXS,
     );
@@ -286,7 +283,7 @@ fn process_block(
     block: Block,
     block_height: u32,
 ) -> anyhow::Result<()> {
-    recv_chain.apply_update(CheckPoint::from_header(&block.header, block_height))?;
+    recv_chain.apply_header(&block.header, block_height)?;
     let _ = recv_graph.apply_block(block, block_height);
     Ok(())
 }
@@ -334,10 +331,7 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
     let env = TestEnv::new()?;
     let mut emitter = Emitter::new(
         env.rpc_client(),
-        CheckPoint::new(BlockId {
-            height: 0,
-            hash: env.rpc_client().get_block_hash(0)?,
-        }),
+        CheckPoint::new(0, env.rpc_client().get_block_hash(0)?),
         0,
         NO_EXPECTED_MEMPOOL_TXS,
     );
@@ -351,7 +345,7 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
     let addr_to_track = Address::from_script(&spk_to_track, Network::Regtest)?;
 
     // setup receiver
-    let (mut recv_chain, _) = LocalChain::from_genesis_hash(env.rpc_client().get_block_hash(0)?);
+    let (mut recv_chain, _) = LocalChain::from_genesis(env.rpc_client().get_block_hash(0)?);
     let mut recv_graph = IndexedTxGraph::<BlockId, _>::new({
         let mut recv_index = SpkTxOutIndex::default();
         recv_index.insert_spk((), spk_to_track.clone());
@@ -425,10 +419,7 @@ fn mempool_avoids_re_emission() -> anyhow::Result<()> {
     let env = TestEnv::new()?;
     let mut emitter = Emitter::new(
         env.rpc_client(),
-        CheckPoint::new(BlockId {
-            height: 0,
-            hash: env.rpc_client().get_block_hash(0)?,
-        }),
+        CheckPoint::new(0, env.rpc_client().get_block_hash(0)?),
         0,
         NO_EXPECTED_MEMPOOL_TXS,
     );
@@ -498,10 +489,7 @@ fn no_agreement_point() -> anyhow::Result<()> {
     // start height is 99
     let mut emitter = Emitter::new(
         env.rpc_client(),
-        CheckPoint::new(BlockId {
-            height: 0,
-            hash: env.rpc_client().get_block_hash(0)?,
-        }),
+        CheckPoint::new(0, env.rpc_client().get_block_hash(0)?),
         (PREMINE_COUNT - 2) as u32,
         NO_EXPECTED_MEMPOOL_TXS,
     );
@@ -573,7 +561,7 @@ fn test_expect_tx_evicted() -> anyhow::Result<()> {
         .0;
     let spk = desc.at_derivation_index(0)?.script_pubkey();
 
-    let mut chain = LocalChain::from_genesis_hash(genesis_block(Network::Regtest).block_hash()).0;
+    let mut chain = LocalChain::from_genesis(genesis_block(Network::Regtest).block_hash()).0;
     let chain_tip = chain.tip().block_id();
 
     let mut index = SpkTxOutIndex::default();
@@ -594,7 +582,7 @@ fn test_expect_tx_evicted() -> anyhow::Result<()> {
     let mut emitter = Emitter::new(env.rpc_client(), chain.tip(), 1, core::iter::once(tx_1));
     while let Some(emission) = emitter.next_block()? {
         let height = emission.block_height();
-        chain.apply_update(CheckPoint::from_header(&emission.block.header, height))?;
+        chain.apply_header(&emission.block.header, height)?;
     }
 
     let changeset = graph.batch_insert_unconfirmed(emitter.mempool()?.update);
@@ -670,10 +658,7 @@ fn detect_new_mempool_txs() -> anyhow::Result<()> {
 
     let mut emitter = Emitter::new(
         env.rpc_client(),
-        CheckPoint::new(BlockId {
-            height: 0,
-            hash: env.rpc_client().get_block_hash(0)?,
-        }),
+        CheckPoint::new(0, env.rpc_client().get_block_hash(0)?),
         0,
         NO_EXPECTED_MEMPOOL_TXS,
     );
index f47d80f8b28e1f3f38f8629946b1fac01a81ff15..eafe8250ba14c56f95b07077e43b3aa967352399 100644 (file)
@@ -1,5 +1,5 @@
 use bdk_bitcoind_rpc::bip158::{Error, FilterIter};
-use bdk_core::{BlockId, CheckPoint};
+use bdk_core::CheckPoint;
 use bdk_testenv::{anyhow, bitcoind, TestEnv};
 use bitcoin::{Address, Amount, Network, ScriptBuf};
 use bitcoincore_rpc::RpcApi;
@@ -36,10 +36,7 @@ fn filter_iter_matches_blocks() -> anyhow::Result<()> {
     let _ = env.mine_blocks(1, None);
 
     let genesis_hash = env.genesis_hash()?;
-    let cp = CheckPoint::new(BlockId {
-        height: 0,
-        hash: genesis_hash,
-    });
+    let cp = CheckPoint::new(0, genesis_hash);
 
     let iter = FilterIter::new(&env.bitcoind.client, cp, [addr.script_pubkey()]);
 
@@ -62,11 +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 block_id = BlockId {
-        height: 0,
-        hash: bitcoin::hashes::Hash::hash(b"wrong-hash"),
-    };
-    let cp = CheckPoint::new(block_id);
+    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))));
 
@@ -85,10 +78,7 @@ fn filter_iter_detects_reorgs() -> anyhow::Result<()> {
     }
 
     let genesis_hash = env.genesis_hash()?;
-    let cp = CheckPoint::new(BlockId {
-        height: 0,
-        hash: genesis_hash,
-    });
+    let cp = CheckPoint::new(0, genesis_hash);
 
     let spk = ScriptBuf::from_hex("0014446906a6560d8ad760db3156706e72e171f3a2aa")?;
     let mut iter = FilterIter::new(&env.bitcoind.client, cp, [spk]);
index df9c08b01e2e3518707da522e00df891d225a6d9..bf11e1ebc33ef95fff0e99759d9437361965e951 100644 (file)
@@ -76,8 +76,12 @@ fn add_ancestor_tx(graph: &mut KeychainTxGraph, block_id: BlockId, locktime: u32
 
 fn setup<F: Fn(&mut KeychainTxGraph, &LocalChain)>(f: F) -> (KeychainTxGraph, LocalChain) {
     const DESC: &str = "tr([ab28dc00/86h/1h/0h]tpubDCdDtzAMZZrkwKBxwNcGCqe4FRydeD9rfMisoi7qLdraG79YohRfPW4YgdKQhpgASdvh612xXNY5xYzoqnyCgPbkpK4LSVcH5Xv4cK7johH/0/*)";
-    let cp = CheckPoint::from_block_ids([genesis_block_id(), tip_block_id()])
-        .expect("blocks must be chronological");
+    let cp = CheckPoint::from_blocks(
+        [genesis_block_id(), tip_block_id()]
+            .into_iter()
+            .map(|block_id| (block_id.height, block_id.hash)),
+    )
+    .expect("blocks must be chronological");
     let chain = LocalChain::from_tip(cp).unwrap();
 
     let (desc, _) =
index 604d5803e8c8394baae153e39a7022dceb9feb11..c1786a62723b917ee6828597bd3047c310199659 100644 (file)
@@ -50,7 +50,12 @@ fn setup<F: Fn(&mut KeychainTxGraph, &LocalChain)>(f: F) -> (KeychainTxGraph, Lo
         .unwrap()
         .0;
 
-    let cp = CheckPoint::from_block_ids([genesis_block_id(), tip_block_id()]).unwrap();
+    let cp = CheckPoint::from_blocks(
+        [genesis_block_id(), tip_block_id()]
+            .into_iter()
+            .map(|block_id| (block_id.height, block_id.hash)),
+    )
+    .unwrap();
     let chain = LocalChain::from_tip(cp).unwrap();
 
     let mut index = KeychainTxOutIndex::new(LOOKAHEAD, USE_SPK_CACHE);
index 0ab676e8d46d42e2ff818ce057a6946527228dfb..81f4a1796db4e66ed50b7833b181dbc4e3cd65e3 100644 (file)
@@ -1,38 +1,43 @@
 //! The [`LocalChain`] is a local implementation of [`ChainOracle`].
 
 use core::convert::Infallible;
+use core::fmt;
 use core::ops::RangeBounds;
 
 use crate::collections::BTreeMap;
 use crate::{BlockId, ChainOracle, Merge};
+use bdk_core::ToBlockHash;
 pub use bdk_core::{CheckPoint, CheckPointIter};
 use bitcoin::block::Header;
 use bitcoin::BlockHash;
 
 /// Apply `changeset` to the checkpoint.
-fn apply_changeset_to_checkpoint(
-    mut init_cp: CheckPoint,
-    changeset: &ChangeSet,
-) -> Result<CheckPoint, MissingGenesisError> {
+fn apply_changeset_to_checkpoint<D>(
+    mut init_cp: CheckPoint<D>,
+    changeset: &ChangeSet<D>,
+) -> Result<CheckPoint<D>, MissingGenesisError>
+where
+    D: ToBlockHash + fmt::Debug + Copy,
+{
     if let Some(start_height) = changeset.blocks.keys().next().cloned() {
         // changes after point of agreement
         let mut extension = BTreeMap::default();
         // point of agreement
-        let mut base: Option<CheckPoint> = None;
+        let mut base: Option<CheckPoint<D>> = None;
 
         for cp in init_cp.iter() {
             if cp.height() >= start_height {
-                extension.insert(cp.height(), cp.hash());
+                extension.insert(cp.height(), cp.data());
             } else {
                 base = Some(cp);
                 break;
             }
         }
 
-        for (&height, &hash) in &changeset.blocks {
-            match hash {
-                Some(hash) => {
-                    extension.insert(height, hash);
+        for (&height, &data) in &changeset.blocks {
+            match data {
+                Some(data) => {
+                    extension.insert(height, data);
                 }
                 None => {
                     extension.remove(&height);
@@ -42,7 +47,7 @@ fn apply_changeset_to_checkpoint(
 
         let new_tip = match base {
             Some(base) => base
-                .extend(extension.into_iter().map(BlockId::from))
+                .extend(extension)
                 .expect("extension is strictly greater than base"),
             None => LocalChain::from_blocks(extension)?.tip(),
         };
@@ -53,12 +58,18 @@ fn apply_changeset_to_checkpoint(
 }
 
 /// This is a local implementation of [`ChainOracle`].
-#[derive(Debug, Clone, PartialEq)]
-pub struct LocalChain {
-    tip: CheckPoint,
+#[derive(Debug, Clone)]
+pub struct LocalChain<D = BlockHash> {
+    tip: CheckPoint<D>,
+}
+
+impl<D> PartialEq for LocalChain<D> {
+    fn eq(&self, other: &Self) -> bool {
+        self.tip == other.tip
+    }
 }
 
-impl ChainOracle for LocalChain {
+impl<D> ChainOracle for LocalChain<D> {
     type Error = Infallible;
 
     fn is_block_in_chain(
@@ -83,101 +94,8 @@ impl ChainOracle for LocalChain {
     }
 }
 
-impl LocalChain {
-    /// Get the genesis hash.
-    pub fn genesis_hash(&self) -> BlockHash {
-        self.tip.get(0).expect("genesis must exist").hash()
-    }
-
-    /// Construct [`LocalChain`] from genesis `hash`.
-    #[must_use]
-    pub fn from_genesis_hash(hash: BlockHash) -> (Self, ChangeSet) {
-        let height = 0;
-        let chain = Self {
-            tip: CheckPoint::new(BlockId { height, hash }),
-        };
-        let changeset = chain.initial_changeset();
-        (chain, changeset)
-    }
-
-    /// Construct a [`LocalChain`] from an initial `changeset`.
-    pub fn from_changeset(changeset: ChangeSet) -> Result<Self, MissingGenesisError> {
-        let genesis_entry = changeset.blocks.get(&0).copied().flatten();
-        let genesis_hash = match genesis_entry {
-            Some(hash) => hash,
-            None => return Err(MissingGenesisError),
-        };
-
-        let (mut chain, _) = Self::from_genesis_hash(genesis_hash);
-        chain.apply_changeset(&changeset)?;
-
-        debug_assert!(chain._check_changeset_is_applied(&changeset));
-
-        Ok(chain)
-    }
-
-    /// Construct a [`LocalChain`] from a given `checkpoint` tip.
-    pub fn from_tip(tip: CheckPoint) -> Result<Self, MissingGenesisError> {
-        let genesis_cp = tip.iter().last().expect("must have at least one element");
-        if genesis_cp.height() != 0 {
-            return Err(MissingGenesisError);
-        }
-        Ok(Self { tip })
-    }
-
-    /// Constructs a [`LocalChain`] from a [`BTreeMap`] of height to [`BlockHash`].
-    ///
-    /// The [`BTreeMap`] enforces the height order. However, the caller must ensure the blocks are
-    /// all of the same chain.
-    pub fn from_blocks(blocks: BTreeMap<u32, BlockHash>) -> Result<Self, MissingGenesisError> {
-        if !blocks.contains_key(&0) {
-            return Err(MissingGenesisError);
-        }
-
-        let mut tip: Option<CheckPoint> = None;
-        for block in &blocks {
-            match tip {
-                Some(curr) => {
-                    tip = Some(
-                        curr.push(BlockId::from(block))
-                            .expect("BTreeMap is ordered"),
-                    )
-                }
-                None => tip = Some(CheckPoint::new(BlockId::from(block))),
-            }
-        }
-
-        Ok(Self {
-            tip: tip.expect("already checked to have genesis"),
-        })
-    }
-
-    /// Get the highest checkpoint.
-    pub fn tip(&self) -> CheckPoint {
-        self.tip.clone()
-    }
-
-    /// Applies the given `update` to the chain.
-    ///
-    /// The method returns [`ChangeSet`] on success. This represents the changes applied to `self`.
-    ///
-    /// There must be no ambiguity about which of the existing chain's blocks are still valid and
-    /// which are now invalid. That is, the new chain must implicitly connect to a definite block in
-    /// the existing chain and invalidate the block after it (if it exists) by including a block at
-    /// the same height but with a different hash to explicitly exclude it as a connection point.
-    ///
-    /// # Errors
-    ///
-    /// An error will occur if the update does not correctly connect with `self`.
-    ///
-    /// [module-level documentation]: crate::local_chain
-    pub fn apply_update(&mut self, update: CheckPoint) -> Result<ChangeSet, CannotConnectError> {
-        let (new_tip, changeset) = merge_chains(self.tip.clone(), update)?;
-        self.tip = new_tip;
-        debug_assert!(self._check_changeset_is_applied(&changeset));
-        Ok(changeset)
-    }
-
+// Methods for `LocalChain<BlockHash>`
+impl LocalChain<BlockHash> {
     /// Update the chain with a given [`Header`] at `height` which you claim is connected to a
     /// existing block in the chain.
     ///
@@ -227,8 +145,13 @@ impl LocalChain {
             conn => Some(conn),
         };
 
-        let update = CheckPoint::from_block_ids([conn, prev, Some(this)].into_iter().flatten())
-            .expect("block ids must be in order");
+        let update = CheckPoint::from_blocks(
+            [conn, prev, Some(this)]
+                .into_iter()
+                .flatten()
+                .map(|block_id| (block_id.height, block_id.hash)),
+        )
+        .expect("block ids must be in order");
 
         self.apply_update(update)
             .map_err(ApplyHeaderError::CannotConnect)
@@ -264,9 +187,130 @@ impl LocalChain {
                 ApplyHeaderError::CannotConnect(err) => err,
             })
     }
+}
+
+// Methods for any `D`
+impl<D> LocalChain<D> {
+    /// Get the highest checkpoint.
+    pub fn tip(&self) -> CheckPoint<D> {
+        self.tip.clone()
+    }
+
+    /// Get the genesis hash.
+    pub fn genesis_hash(&self) -> BlockHash {
+        self.tip.get(0).expect("genesis must exist").block_id().hash
+    }
+
+    /// Iterate over checkpoints in descending height order.
+    pub fn iter_checkpoints(&self) -> CheckPointIter<D> {
+        self.tip.iter()
+    }
+
+    /// Get checkpoint at given `height` (if it exists).
+    ///
+    /// This is a shorthand for calling [`CheckPoint::get`] on the [`tip`].
+    ///
+    /// [`tip`]: LocalChain::tip
+    pub fn get(&self, height: u32) -> Option<CheckPoint<D>> {
+        self.tip.get(height)
+    }
+
+    /// Iterate checkpoints over a height range.
+    ///
+    /// Note that we always iterate checkpoints in reverse height order (iteration starts at tip
+    /// height).
+    ///
+    /// This is a shorthand for calling [`CheckPoint::range`] on the [`tip`].
+    ///
+    /// [`tip`]: LocalChain::tip
+    pub fn range<R>(&self, range: R) -> impl Iterator<Item = CheckPoint<D>>
+    where
+        R: RangeBounds<u32>,
+    {
+        self.tip.range(range)
+    }
+}
+
+// Methods where `D: ToBlockHash`
+impl<D> LocalChain<D>
+where
+    D: ToBlockHash + fmt::Debug + Copy,
+{
+    /// Constructs a [`LocalChain`] from genesis data.
+    pub fn from_genesis(data: D) -> (Self, ChangeSet<D>) {
+        let height = 0;
+        let chain = Self {
+            tip: CheckPoint::new(height, data),
+        };
+        let changeset = chain.initial_changeset();
+
+        (chain, changeset)
+    }
+
+    /// Constructs a [`LocalChain`] from a [`BTreeMap`] of height and data `D`.
+    ///
+    /// The [`BTreeMap`] enforces the height order. However, the caller must ensure the blocks are
+    /// all of the same chain.
+    pub fn from_blocks(blocks: BTreeMap<u32, D>) -> Result<Self, MissingGenesisError> {
+        if !blocks.contains_key(&0) {
+            return Err(MissingGenesisError);
+        }
+
+        Ok(Self {
+            tip: CheckPoint::from_blocks(blocks).expect("blocks must be in order"),
+        })
+    }
+
+    /// Construct a [`LocalChain`] from an initial `changeset`.
+    pub fn from_changeset(changeset: ChangeSet<D>) -> Result<Self, MissingGenesisError> {
+        let genesis_entry = changeset.blocks.get(&0).copied().flatten();
+        let genesis_data = match genesis_entry {
+            Some(data) => data,
+            None => return Err(MissingGenesisError),
+        };
+
+        let (mut chain, _) = Self::from_genesis(genesis_data);
+        chain.apply_changeset(&changeset)?;
+        debug_assert!(chain._check_changeset_is_applied(&changeset));
+        Ok(chain)
+    }
+
+    /// Construct a [`LocalChain`] from a given `checkpoint` tip.
+    pub fn from_tip(tip: CheckPoint<D>) -> Result<Self, MissingGenesisError> {
+        let genesis_cp = tip.iter().last().expect("must have at least one element");
+        if genesis_cp.height() != 0 {
+            return Err(MissingGenesisError);
+        }
+
+        Ok(Self { tip })
+    }
+
+    /// Applies the given `update` to the chain.
+    ///
+    /// The method returns [`ChangeSet`] on success. This represents the changes applied to `self`.
+    ///
+    /// There must be no ambiguity about which of the existing chain's blocks are still valid and
+    /// which are now invalid. That is, the new chain must implicitly connect to a definite block in
+    /// the existing chain and invalidate the block after it (if it exists) by including a block at
+    /// the same height but with a different hash to explicitly exclude it as a connection point.
+    ///
+    /// # Errors
+    ///
+    /// An error will occur if the update does not correctly connect with `self`.
+    ///
+    /// [module-level documentation]: crate::local_chain
+    pub fn apply_update(
+        &mut self,
+        update: CheckPoint<D>,
+    ) -> Result<ChangeSet<D>, CannotConnectError> {
+        let (new_tip, changeset) = merge_chains(self.tip.clone(), update)?;
+        self.tip = new_tip;
+        debug_assert!(self._check_changeset_is_applied(&changeset));
+        Ok(changeset)
+    }
 
     /// Apply the given `changeset`.
-    pub fn apply_changeset(&mut self, changeset: &ChangeSet) -> Result<(), MissingGenesisError> {
+    pub fn apply_changeset(&mut self, changeset: &ChangeSet<D>) -> Result<(), MissingGenesisError> {
         let old_tip = self.tip.clone();
         let new_tip = apply_changeset_to_checkpoint(old_tip, changeset)?;
         self.tip = new_tip;
@@ -274,33 +318,52 @@ impl LocalChain {
         Ok(())
     }
 
-    /// Insert a [`BlockId`].
+    /// Derives an initial [`ChangeSet`], meaning that it can be applied to an empty chain to
+    /// recover the current chain.
+    pub fn initial_changeset(&self) -> ChangeSet<D> {
+        ChangeSet {
+            blocks: self
+                .tip
+                .iter()
+                .map(|cp| (cp.height(), Some(cp.data())))
+                .collect(),
+        }
+    }
+
+    /// Insert block into a [`LocalChain`].
     ///
     /// # Errors
     ///
     /// Replacing the block hash of an existing checkpoint will result in an error.
-    pub fn insert_block(&mut self, block_id: BlockId) -> Result<ChangeSet, AlterCheckPointError> {
-        if let Some(original_cp) = self.tip.get(block_id.height) {
+    pub fn insert_block(
+        &mut self,
+        height: u32,
+        data: D,
+    ) -> Result<ChangeSet<D>, AlterCheckPointError> {
+        if let Some(original_cp) = self.tip.get(height) {
             let original_hash = original_cp.hash();
-            if original_hash != block_id.hash {
+            if original_hash != data.to_blockhash() {
                 return Err(AlterCheckPointError {
-                    height: block_id.height,
+                    height,
                     original_hash,
-                    update_hash: Some(block_id.hash),
+                    update_hash: Some(data.to_blockhash()),
                 });
             }
             return Ok(ChangeSet::default());
         }
 
-        let mut changeset = ChangeSet::default();
-        changeset
-            .blocks
-            .insert(block_id.height, Some(block_id.hash));
+        let mut changeset = ChangeSet::<D>::default();
+        changeset.blocks.insert(height, Some(data));
         self.apply_changeset(&changeset)
             .map_err(|_| AlterCheckPointError {
                 height: 0,
                 original_hash: self.genesis_hash(),
-                update_hash: changeset.blocks.get(&0).cloned().flatten(),
+                update_hash: changeset
+                    .blocks
+                    .get(&0)
+                    .cloned()
+                    .flatten()
+                    .map(|d| d.to_blockhash()),
             })?;
         Ok(changeset)
     }
@@ -314,8 +377,11 @@ impl LocalChain {
     ///
     /// This will fail with [`MissingGenesisError`] if the caller attempts to disconnect from the
     /// genesis block.
-    pub fn disconnect_from(&mut self, block_id: BlockId) -> Result<ChangeSet, MissingGenesisError> {
-        let mut remove_from = Option::<CheckPoint>::None;
+    pub fn disconnect_from(
+        &mut self,
+        block_id: BlockId,
+    ) -> Result<ChangeSet<D>, MissingGenesisError> {
+        let mut remove_from = Option::<CheckPoint<D>>::None;
         let mut changeset = ChangeSet::default();
         for cp in self.tip().iter() {
             let cp_id = cp.block_id();
@@ -340,38 +406,20 @@ impl LocalChain {
         Ok(changeset)
     }
 
-    /// Derives an initial [`ChangeSet`], meaning that it can be applied to an empty chain to
-    /// recover the current chain.
-    pub fn initial_changeset(&self) -> ChangeSet {
-        ChangeSet {
-            blocks: self
-                .tip
-                .iter()
-                .map(|cp| {
-                    let block_id = cp.block_id();
-                    (block_id.height, Some(block_id.hash))
-                })
-                .collect(),
-        }
-    }
-
-    /// Iterate over checkpoints in descending height order.
-    pub fn iter_checkpoints(&self) -> CheckPointIter {
-        self.tip.iter()
-    }
-
-    fn _check_changeset_is_applied(&self, changeset: &ChangeSet) -> bool {
-        let mut curr_cp = self.tip.clone();
-        for (height, exp_hash) in changeset.blocks.iter().rev() {
-            match curr_cp.get(*height) {
-                Some(query_cp) => {
-                    if query_cp.height() != *height || Some(query_cp.hash()) != *exp_hash {
+    fn _check_changeset_is_applied(&self, changeset: &ChangeSet<D>) -> bool {
+        let mut cur = self.tip.clone();
+        for (&exp_height, exp_data) in changeset.blocks.iter().rev() {
+            match cur.get(exp_height) {
+                Some(cp) => {
+                    if cp.height() != exp_height
+                        || Some(cp.hash()) != exp_data.map(|d| d.to_blockhash())
+                    {
                         return false;
                     }
-                    curr_cp = query_cp;
+                    cur = cp;
                 }
                 None => {
-                    if exp_hash.is_some() {
+                    if exp_data.is_some() {
                         return false;
                     }
                 }
@@ -379,44 +427,28 @@ impl LocalChain {
         }
         true
     }
-
-    /// Get checkpoint at given `height` (if it exists).
-    ///
-    /// This is a shorthand for calling [`CheckPoint::get`] on the [`tip`].
-    ///
-    /// [`tip`]: LocalChain::tip
-    pub fn get(&self, height: u32) -> Option<CheckPoint> {
-        self.tip.get(height)
-    }
-
-    /// Iterate checkpoints over a height range.
-    ///
-    /// Note that we always iterate checkpoints in reverse height order (iteration starts at tip
-    /// height).
-    ///
-    /// This is a shorthand for calling [`CheckPoint::range`] on the [`tip`].
-    ///
-    /// [`tip`]: LocalChain::tip
-    pub fn range<R>(&self, range: R) -> impl Iterator<Item = CheckPoint>
-    where
-        R: RangeBounds<u32>,
-    {
-        self.tip.range(range)
-    }
 }
 
 /// The [`ChangeSet`] represents changes to [`LocalChain`].
-#[derive(Debug, Default, Clone, PartialEq)]
+#[derive(Debug, Clone, PartialEq)]
 #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
-pub struct ChangeSet {
+pub struct ChangeSet<D = BlockHash> {
     /// Changes to the [`LocalChain`] blocks.
     ///
     /// The key represents the block height, and the value either represents added a new
     /// [`CheckPoint`] (if [`Some`]), or removing a [`CheckPoint`] (if [`None`]).
-    pub blocks: BTreeMap<u32, Option<BlockHash>>,
+    pub blocks: BTreeMap<u32, Option<D>>,
+}
+
+impl<D> Default for ChangeSet<D> {
+    fn default() -> Self {
+        ChangeSet {
+            blocks: BTreeMap::default(),
+        }
+    }
 }
 
-impl Merge for ChangeSet {
+impl<D> Merge for ChangeSet<D> {
     fn merge(&mut self, other: Self) {
         Merge::merge(&mut self.blocks, other.blocks)
     }
@@ -426,24 +458,24 @@ impl Merge for ChangeSet {
     }
 }
 
-impl<B: IntoIterator<Item = (u32, Option<BlockHash>)>> From<B> for ChangeSet {
-    fn from(blocks: B) -> Self {
+impl<D, I: IntoIterator<Item = (u32, Option<D>)>> From<I> for ChangeSet<D> {
+    fn from(blocks: I) -> Self {
         Self {
             blocks: blocks.into_iter().collect(),
         }
     }
 }
 
-impl FromIterator<(u32, Option<BlockHash>)> for ChangeSet {
-    fn from_iter<T: IntoIterator<Item = (u32, Option<BlockHash>)>>(iter: T) -> Self {
+impl<D> FromIterator<(u32, Option<D>)> for ChangeSet<D> {
+    fn from_iter<T: IntoIterator<Item = (u32, Option<D>)>>(iter: T) -> Self {
         Self {
             blocks: iter.into_iter().collect(),
         }
     }
 }
 
-impl FromIterator<(u32, BlockHash)> for ChangeSet {
-    fn from_iter<T: IntoIterator<Item = (u32, BlockHash)>>(iter: T) -> Self {
+impl<D> FromIterator<(u32, D)> for ChangeSet<D> {
+    fn from_iter<T: IntoIterator<Item = (u32, D)>>(iter: T) -> Self {
         Self {
             blocks: iter
                 .into_iter()
@@ -557,11 +589,14 @@ impl std::error::Error for ApplyHeaderError {}
 ///   the update and original chain both have a block above the point of agreement, but their
 ///   heights do not overlap).
 /// - The update attempts to replace the genesis block of the original chain.
-fn merge_chains(
-    original_tip: CheckPoint,
-    update_tip: CheckPoint,
-) -> Result<(CheckPoint, ChangeSet), CannotConnectError> {
-    let mut changeset = ChangeSet::default();
+fn merge_chains<D>(
+    original_tip: CheckPoint<D>,
+    update_tip: CheckPoint<D>,
+) -> Result<(CheckPoint<D>, ChangeSet<D>), CannotConnectError>
+where
+    D: ToBlockHash + fmt::Debug + Copy,
+{
+    let mut changeset = ChangeSet::<D>::default();
 
     let mut orig = original_tip.iter();
     let mut update = update_tip.iter();
@@ -569,8 +604,8 @@ fn merge_chains(
     let mut curr_orig = None;
     let mut curr_update = None;
 
-    let mut prev_orig: Option<CheckPoint> = None;
-    let mut prev_update: Option<CheckPoint> = None;
+    let mut prev_orig: Option<CheckPoint<D>> = None;
+    let mut prev_update: Option<CheckPoint<D>> = None;
 
     let mut point_of_agreement_found = false;
 
@@ -599,7 +634,7 @@ fn merge_chains(
         match (curr_orig.as_ref(), curr_update.as_ref()) {
             // Update block that doesn't exist in the original chain
             (o, Some(u)) if Some(u.height()) > o.map(|o| o.height()) => {
-                changeset.blocks.insert(u.height(), Some(u.hash()));
+                changeset.blocks.insert(u.height(), Some(u.data()));
                 prev_update = curr_update.take();
             }
             // Original block that isn't in the update
@@ -650,7 +685,7 @@ fn merge_chains(
                 } else {
                     // We have an invalidation height so we set the height to the updated hash and
                     // also purge all the original chain block hashes above this block.
-                    changeset.blocks.insert(u.height(), Some(u.hash()));
+                    changeset.blocks.insert(u.height(), Some(u.data()));
                     for invalidated_height in potentially_invalidated_heights.drain(..) {
                         changeset.blocks.insert(invalidated_height, None);
                     }
index 74d1021f6236d5b8519b6c81d674ec497ddc0630..6e1100485476bd99219007b3989923385d242089 100644 (file)
@@ -20,8 +20,9 @@ use crate::{BlockId, ConfirmationBlockTime};
 /// # use bdk_chain::ConfirmationBlockTime;
 /// # use bdk_chain::example_utils::*;
 /// # use bitcoin::hashes::Hash;
+/// # use bitcoin::BlockHash;
 /// // Initialize the local chain with two blocks.
-/// let chain = LocalChain::from_blocks(
+/// let chain = LocalChain::<BlockHash>::from_blocks(
 ///     [
 ///         (1, Hash::hash("first".as_bytes())),
 ///         (2, Hash::hash("second".as_bytes())),
index 7bbdef63f0d01abe5668bf6aef53fb43d24403dd..60b1f045d1f5f93c7c51eab04aee0aa7ea55cf87 100644 (file)
@@ -1325,11 +1325,11 @@ impl<A: Anchor> TxGraph<A> {
     /// # use bdk_chain::tx_graph::TxGraph;
     /// # use bdk_chain::{local_chain::LocalChain, CanonicalizationParams, ConfirmationBlockTime};
     /// # use bdk_testenv::{hash, utils::new_tx};
-    /// # use bitcoin::{Amount, OutPoint, ScriptBuf, Transaction, TxIn, TxOut};
+    /// # use bitcoin::{Amount, BlockHash, OutPoint, ScriptBuf, Transaction, TxIn, TxOut};
     ///
     /// # let spk = ScriptBuf::from_hex("0014c692ecf13534982a9a2834565cbd37add8027140").unwrap();
     /// # let chain =
-    /// #     LocalChain::from_blocks((0..=15).map(|i| (i as u32, hash!("h"))).collect()).unwrap();
+    /// #     LocalChain::<BlockHash>::from_blocks((0..=15).map(|i| (i as u32, hash!("h"))).collect()).unwrap();
     /// # let mut graph: TxGraph = TxGraph::default();
     /// # let coinbase_tx = Transaction {
     /// #     input: vec![TxIn {
index 1e87f009a72212ce4792b752468ca2109d5f0dbb..db91a34b3c3032ccd82f455742bd52741040c62d 100644 (file)
@@ -24,8 +24,8 @@ use bdk_testenv::{
     TestEnv,
 };
 use bitcoin::{
-    secp256k1::Secp256k1, Address, Amount, Network, OutPoint, ScriptBuf, Transaction, TxIn, TxOut,
-    Txid,
+    secp256k1::Secp256k1, Address, Amount, BlockHash, Network, OutPoint, ScriptBuf, Transaction,
+    TxIn, TxOut, Txid,
 };
 use miniscript::Descriptor;
 
@@ -323,9 +323,10 @@ fn insert_relevant_txs() {
 #[test]
 fn test_list_owned_txouts() {
     // Create Local chains
-    let local_chain =
-        LocalChain::from_blocks((0..150).map(|i| (i as u32, hash!("random"))).collect())
-            .expect("must have genesis hash");
+    let local_chain = LocalChain::<BlockHash>::from_blocks(
+        (0..150).map(|i| (i as u32, hash!("random"))).collect(),
+    )
+    .expect("must have genesis hash");
 
     // Initiate IndexedTxGraph
 
@@ -751,9 +752,14 @@ fn test_get_chain_position() {
     });
 
     // Anchors to test
-    let blocks = vec![block_id!(0, "g"), block_id!(1, "A"), block_id!(2, "B")];
-
-    let cp = CheckPoint::from_block_ids(blocks.clone()).unwrap();
+    let blocks = [block_id!(0, "g"), block_id!(1, "A"), block_id!(2, "B")];
+
+    let cp = CheckPoint::from_blocks(
+        blocks
+            .iter()
+            .map(|block_id| (block_id.height, block_id.hash)),
+    )
+    .unwrap();
     let chain = LocalChain::from_tip(cp).unwrap();
 
     // The test will insert a transaction into the indexed tx graph along with any anchors and
index 8adbce4af4c0039815e3f86ef49166ab68567bd0..94a2498c1c20ea1942f4071be740c43e6fb578c1 100644 (file)
@@ -17,7 +17,7 @@ use proptest::prelude::*;
 struct TestLocalChain<'a> {
     name: &'static str,
     chain: LocalChain,
-    update: CheckPoint,
+    update: CheckPoint<BlockHash>,
     exp: ExpectedResult<'a>,
 }
 
@@ -368,8 +368,9 @@ fn local_chain_insert_block() {
 
     for (i, t) in test_cases.into_iter().enumerate() {
         let mut chain = t.original;
+        let block_id: BlockId = t.insert.into();
         assert_eq!(
-            chain.insert_block(t.insert.into()),
+            chain.insert_block(block_id.height, block_id.hash),
             t.expected_result,
             "[{i}] unexpected result when inserting block",
         );
@@ -490,11 +491,7 @@ fn checkpoint_from_block_ids() {
     ];
 
     for (i, t) in test_cases.into_iter().enumerate() {
-        let result = CheckPoint::from_block_ids(
-            t.blocks
-                .iter()
-                .map(|&(height, hash)| BlockId { height, hash }),
-        );
+        let result = CheckPoint::<BlockHash>::from_blocks(t.blocks.iter().copied());
         match t.exp_result {
             Ok(_) => {
                 assert!(result.is_ok(), "[{}:{}] should be Ok", i, t.name);
@@ -639,18 +636,23 @@ fn checkpoint_insert() {
     }
 
     for t in test_cases.into_iter() {
-        let chain = CheckPoint::from_block_ids(
-            genesis_block().chain(t.chain.iter().copied().map(BlockId::from)),
+        let chain = CheckPoint::from_blocks(
+            genesis_block()
+                .chain(t.chain.iter().copied().map(BlockId::from))
+                .map(|block_id| (block_id.height, block_id.hash)),
         )
         .expect("test formed incorrectly, must construct checkpoint chain");
 
-        let exp_final_chain = CheckPoint::from_block_ids(
-            genesis_block().chain(t.exp_final_chain.iter().copied().map(BlockId::from)),
+        let exp_final_chain = CheckPoint::from_blocks(
+            genesis_block()
+                .chain(t.exp_final_chain.iter().copied().map(BlockId::from))
+                .map(|block_id| (block_id.height, block_id.hash)),
         )
         .expect("test formed incorrectly, must construct checkpoint chain");
 
+        let BlockId { height, hash } = t.to_insert.into();
         assert_eq!(
-            chain.insert(t.to_insert.into()),
+            chain.insert(height, hash),
             exp_final_chain,
             "unexpected final chain"
         );
@@ -824,12 +826,15 @@ fn generate_height_range_bounds(
     )
 }
 
-fn generate_checkpoints(max_height: u32, max_count: usize) -> impl Strategy<Value = CheckPoint> {
+fn generate_checkpoints(
+    max_height: u32,
+    max_count: usize,
+) -> impl Strategy<Value = CheckPoint<BlockHash>> {
     proptest::collection::btree_set(1..max_height, 0..max_count).prop_map(|mut heights| {
         heights.insert(0); // must have genesis
-        CheckPoint::from_block_ids(heights.into_iter().map(|height| {
+        CheckPoint::from_blocks(heights.into_iter().map(|height| {
             let hash = bitcoin::hashes::Hash::hash(height.to_le_bytes().as_slice());
-            BlockId { height, hash }
+            (height, hash)
         }))
         .expect("blocks must be in order as it comes from btreeset")
     })
index 71944e404b9fbf7a9f8bc1f1183246f01ee1c04b..cbd5d54177f79e4d1062b9997ac3ee7b46966cc3 100644 (file)
@@ -3,9 +3,9 @@
 #[macro_use]
 mod common;
 
-use bdk_chain::{Balance, BlockId};
+use bdk_chain::{local_chain::LocalChain, Balance, BlockId};
 use bdk_testenv::{block_id, hash, local_chain};
-use bitcoin::{Amount, OutPoint, ScriptBuf};
+use bitcoin::{Amount, BlockHash, OutPoint, ScriptBuf};
 use common::*;
 use std::collections::{BTreeSet, HashSet};
 
@@ -32,7 +32,7 @@ struct Scenario<'a> {
 #[test]
 fn test_tx_conflict_handling() {
     // Create Local chains
-    let local_chain = local_chain!(
+    let local_chain: LocalChain<BlockHash> = local_chain!(
         (0, hash!("A")),
         (1, hash!("B")),
         (2, hash!("C")),
index 01b36e2587071138ff707bf145e94884dca71071..a3041296d4e7529832fd5a86ca91d16898623546 100644 (file)
@@ -1,7 +1,8 @@
+use core::fmt;
 use core::ops::RangeBounds;
 
 use alloc::sync::Arc;
-use bitcoin::BlockHash;
+use bitcoin::{block::Header, Block, BlockHash};
 
 use crate::BlockId;
 
@@ -9,16 +10,24 @@ use crate::BlockId;
 ///
 /// Checkpoints are cheaply cloneable and are useful to find the agreement point between two sparse
 /// block chains.
-#[derive(Debug, Clone)]
-pub struct CheckPoint(Arc<CPInner>);
+#[derive(Debug)]
+pub struct CheckPoint<D = BlockHash>(Arc<CPInner<D>>);
+
+impl<D> Clone for CheckPoint<D> {
+    fn clone(&self) -> Self {
+        CheckPoint(Arc::clone(&self.0))
+    }
+}
 
 /// The internal contents of [`CheckPoint`].
-#[derive(Debug, Clone)]
-struct CPInner {
-    /// Block id (hash and height).
-    block: BlockId,
+#[derive(Debug)]
+struct CPInner<D> {
+    /// Block id
+    block_id: BlockId,
+    /// Data.
+    data: D,
     /// Previous checkpoint (if any).
-    prev: Option<Arc<CPInner>>,
+    prev: Option<Arc<CPInner<D>>>,
 }
 
 /// When a `CPInner` is dropped we need to go back down the chain and manually remove any
@@ -26,7 +35,7 @@ struct CPInner {
 /// leads to recursive logic and stack overflows
 ///
 /// https://github.com/bitcoindevkit/bdk/issues/1634
-impl Drop for CPInner {
+impl<D> Drop for CPInner<D> {
     fn drop(&mut self) {
         // Take out `prev` so its `drop` won't be called when this drop is finished
         let mut current = self.prev.take();
@@ -49,116 +58,78 @@ impl Drop for CPInner {
     }
 }
 
-impl PartialEq for CheckPoint {
-    fn eq(&self, other: &Self) -> bool {
-        let self_cps = self.iter().map(|cp| cp.block_id());
-        let other_cps = other.iter().map(|cp| cp.block_id());
-        self_cps.eq(other_cps)
-    }
+/// Trait that converts [`CheckPoint`] `data` to [`BlockHash`].
+///
+/// Implementations of [`ToBlockHash`] must consist exclusively of the data to be hashed — do not
+/// include any extraneous fields.
+pub trait ToBlockHash {
+    /// Returns the [`BlockHash`] for the associated [`CheckPoint`] `data` type.
+    fn to_blockhash(&self) -> BlockHash;
 }
 
-impl CheckPoint {
-    /// Construct a new base block at the front of a linked list.
-    pub fn new(block: BlockId) -> Self {
-        Self(Arc::new(CPInner { block, prev: None }))
+impl ToBlockHash for BlockHash {
+    fn to_blockhash(&self) -> BlockHash {
+        *self
     }
+}
 
-    /// Construct a checkpoint from a list of [`BlockId`]s in ascending height order.
-    ///
-    /// # Errors
-    ///
-    /// This method will error if any of the follow occurs:
-    ///
-    /// - The `blocks` iterator is empty, in which case, the error will be `None`.
-    /// - The `blocks` iterator is not in ascending height order.
-    /// - The `blocks` iterator contains multiple [`BlockId`]s of the same height.
-    ///
-    /// The error type is the last successful checkpoint constructed (if any).
-    pub fn from_block_ids(
-        block_ids: impl IntoIterator<Item = BlockId>,
-    ) -> Result<Self, Option<Self>> {
-        let mut blocks = block_ids.into_iter();
-        let mut acc = CheckPoint::new(blocks.next().ok_or(None)?);
-        for id in blocks {
-            acc = acc.push(id).map_err(Some)?;
-        }
-        Ok(acc)
+impl ToBlockHash for Header {
+    fn to_blockhash(&self) -> BlockHash {
+        self.block_hash()
     }
+}
 
-    /// Construct a checkpoint from the given `header` and block `height`.
-    ///
-    /// If `header` is of the genesis block, the checkpoint won't have a [`prev`] node. Otherwise,
-    /// we return a checkpoint linked with the previous block.
-    ///
-    /// [`prev`]: CheckPoint::prev
-    pub fn from_header(header: &bitcoin::block::Header, height: u32) -> Self {
-        let hash = header.block_hash();
-        let this_block_id = BlockId { height, hash };
-
-        let prev_height = match height.checked_sub(1) {
-            Some(h) => h,
-            None => return Self::new(this_block_id),
-        };
-
-        let prev_block_id = BlockId {
-            height: prev_height,
-            hash: header.prev_blockhash,
-        };
+impl ToBlockHash for Block {
+    fn to_blockhash(&self) -> BlockHash {
+        self.block_hash()
+    }
+}
 
-        CheckPoint::new(prev_block_id)
-            .push(this_block_id)
-            .expect("must construct checkpoint")
+impl<D> PartialEq for CheckPoint<D> {
+    fn eq(&self, other: &Self) -> bool {
+        let self_cps = self.iter().map(|cp| cp.block_id());
+        let other_cps = other.iter().map(|cp| cp.block_id());
+        self_cps.eq(other_cps)
     }
+}
 
-    /// Puts another checkpoint onto the linked list representing the blockchain.
-    ///
-    /// Returns an `Err(self)` if the block you are pushing on is not at a greater height that the
-    /// one you are pushing on to.
-    pub fn push(self, block: BlockId) -> Result<Self, Self> {
-        if self.height() < block.height {
-            Ok(Self(Arc::new(CPInner {
-                block,
-                prev: Some(self.0),
-            })))
-        } else {
-            Err(self)
-        }
+// Methods for any `D`
+impl<D> CheckPoint<D> {
+    /// Get a reference of the `data` of the checkpoint.
+    pub fn data_ref(&self) -> &D {
+        &self.0.data
     }
 
-    /// Extends the checkpoint linked list by a iterator of block ids.
-    ///
-    /// Returns an `Err(self)` if there is block which does not have a greater height than the
-    /// previous one.
-    pub fn extend(self, blocks: impl IntoIterator<Item = BlockId>) -> Result<Self, Self> {
-        let mut curr = self.clone();
-        for block in blocks {
-            curr = curr.push(block).map_err(|_| self.clone())?;
-        }
-        Ok(curr)
+    /// Get the `data` of a the checkpoint.
+    pub fn data(&self) -> D
+    where
+        D: Clone,
+    {
+        self.0.data.clone()
     }
 
     /// Get the [`BlockId`] of the checkpoint.
     pub fn block_id(&self) -> BlockId {
-        self.0.block
+        self.0.block_id
     }
 
-    /// Get the height of the checkpoint.
+    /// Get the `height` of the checkpoint.
     pub fn height(&self) -> u32 {
-        self.0.block.height
+        self.block_id().height
     }
 
     /// Get the block hash of the checkpoint.
     pub fn hash(&self) -> BlockHash {
-        self.0.block.hash
+        self.block_id().hash
     }
 
-    /// Get the previous checkpoint in the chain
-    pub fn prev(&self) -> Option<CheckPoint> {
+    /// Get the previous checkpoint in the chain.
+    pub fn prev(&self) -> Option<CheckPoint<D>> {
         self.0.prev.clone().map(CheckPoint)
     }
 
     /// Iterate from this checkpoint in descending height.
-    pub fn iter(&self) -> CheckPointIter {
+    pub fn iter(&self) -> CheckPointIter<D> {
         self.clone().into_iter()
     }
 
@@ -173,7 +144,7 @@ impl CheckPoint {
     ///
     /// Note that we always iterate checkpoints in reverse height order (iteration starts at tip
     /// height).
-    pub fn range<R>(&self, range: R) -> impl Iterator<Item = CheckPoint>
+    pub fn range<R>(&self, range: R) -> impl Iterator<Item = CheckPoint<D>>
     where
         R: RangeBounds<u32>,
     {
@@ -216,7 +187,57 @@ impl CheckPoint {
         self.floor_at(self.height().checked_sub(offset)?)
     }
 
-    /// Inserts `block_id` at its height within the chain.
+    /// This method tests for `self` and `other` to have equal internal pointers.
+    pub fn eq_ptr(&self, other: &Self) -> bool {
+        Arc::as_ptr(&self.0) == Arc::as_ptr(&other.0)
+    }
+}
+
+// Methods where `D: ToBlockHash`
+impl<D> CheckPoint<D>
+where
+    D: ToBlockHash + fmt::Debug + Copy,
+{
+    /// Construct a new base [`CheckPoint`] from given `height` and `data` at the front of a linked
+    /// list.
+    pub fn new(height: u32, data: D) -> Self {
+        Self(Arc::new(CPInner {
+            block_id: BlockId {
+                height,
+                hash: data.to_blockhash(),
+            },
+            data,
+            prev: None,
+        }))
+    }
+
+    /// Construct from an iterator of block data.
+    ///
+    /// Returns `Err(None)` if `blocks` doesn't yield any data. If the blocks are not in ascending
+    /// height order, then returns an `Err(..)` containing the last checkpoint that would have been
+    /// extended.
+    pub fn from_blocks(blocks: impl IntoIterator<Item = (u32, D)>) -> Result<Self, Option<Self>> {
+        let mut blocks = blocks.into_iter();
+        let (height, data) = blocks.next().ok_or(None)?;
+        let mut cp = CheckPoint::new(height, data);
+        cp = cp.extend(blocks)?;
+
+        Ok(cp)
+    }
+
+    /// Extends the checkpoint linked list by a iterator containing `height` and `data`.
+    ///
+    /// Returns an `Err(self)` if there is block which does not have a greater height than the
+    /// previous one.
+    pub fn extend(self, blockdata: impl IntoIterator<Item = (u32, D)>) -> Result<Self, Self> {
+        let mut cp = self.clone();
+        for (height, data) in blockdata {
+            cp = cp.push(height, data)?;
+        }
+        Ok(cp)
+    }
+
+    /// Inserts `data` at its `height` within the chain.
     ///
     /// The effect of `insert` depends on whether a height already exists. If it doesn't the
     /// `block_id` we inserted and all pre-existing blocks higher than it will be re-inserted after
@@ -228,12 +249,12 @@ impl CheckPoint {
     ///
     /// This panics if called with a genesis block that differs from that of `self`.
     #[must_use]
-    pub fn insert(self, block_id: BlockId) -> Self {
+    pub fn insert(self, height: u32, data: D) -> Self {
         let mut cp = self.clone();
         let mut tail = vec![];
         let base = loop {
-            if cp.height() == block_id.height {
-                if cp.hash() == block_id.hash {
+            if cp.height() == height {
+                if cp.hash() == data.to_blockhash() {
                     return self;
                 }
                 assert_ne!(cp.height(), 0, "cannot replace genesis block");
@@ -243,31 +264,45 @@ impl CheckPoint {
                 break cp.prev().expect("can't be called on genesis block");
             }
 
-            if cp.height() < block_id.height {
+            if cp.height() < height {
                 break cp;
             }
 
-            tail.push(cp.block_id());
+            tail.push((cp.height(), cp.data()));
             cp = cp.prev().expect("will break before genesis block");
         };
 
-        base.extend(core::iter::once(block_id).chain(tail.into_iter().rev()))
+        base.extend(core::iter::once((height, data)).chain(tail.into_iter().rev()))
             .expect("tail is in order")
     }
 
-    /// This method tests for `self` and `other` to have equal internal pointers.
-    pub fn eq_ptr(&self, other: &Self) -> bool {
-        Arc::as_ptr(&self.0) == Arc::as_ptr(&other.0)
+    /// Puts another checkpoint onto the linked list representing the blockchain.
+    ///
+    /// Returns an `Err(self)` if the block you are pushing on is not at a greater height that the
+    /// one you are pushing on to.
+    pub fn push(self, height: u32, data: D) -> Result<Self, Self> {
+        if self.height() < height {
+            Ok(Self(Arc::new(CPInner {
+                block_id: BlockId {
+                    height,
+                    hash: data.to_blockhash(),
+                },
+                data,
+                prev: Some(self.0),
+            })))
+        } else {
+            Err(self)
+        }
     }
 }
 
 /// Iterates over checkpoints backwards.
-pub struct CheckPointIter {
-    current: Option<Arc<CPInner>>,
+pub struct CheckPointIter<D> {
+    current: Option<Arc<CPInner<D>>>,
 }
 
-impl Iterator for CheckPointIter {
-    type Item = CheckPoint;
+impl<D> Iterator for CheckPointIter<D> {
+    type Item = CheckPoint<D>;
 
     fn next(&mut self) -> Option<Self::Item> {
         let current = self.current.clone()?;
@@ -276,9 +311,9 @@ impl Iterator for CheckPointIter {
     }
 }
 
-impl IntoIterator for CheckPoint {
-    type Item = CheckPoint;
-    type IntoIter = CheckPointIter;
+impl<D> IntoIterator for CheckPoint<D> {
+    type Item = CheckPoint<D>;
+    type IntoIter = CheckPointIter<D>;
 
     fn into_iter(self) -> Self::IntoIter {
         CheckPointIter {
index 6f45aed5f990aba85062eba78be4daf0064cef9d..12bbba567912a29ecadf95df19c0a6c4d7841e6f 100644 (file)
@@ -4,7 +4,7 @@ use crate::{
     collections::{BTreeMap, HashMap, HashSet},
     CheckPoint, ConfirmationBlockTime, Indexed,
 };
-use bitcoin::{OutPoint, Script, ScriptBuf, Txid};
+use bitcoin::{BlockHash, OutPoint, Script, ScriptBuf, Txid};
 
 type InspectSync<I> = dyn FnMut(SyncItem<I>, SyncProgress) + Send + 'static;
 
@@ -112,8 +112,8 @@ impl From<ScriptBuf> for SpkWithExpectedTxids {
 ///
 /// Construct with [`SyncRequest::builder`].
 #[must_use]
-pub struct SyncRequestBuilder<I = ()> {
-    inner: SyncRequest<I>,
+pub struct SyncRequestBuilder<I = (), D = BlockHash> {
+    inner: SyncRequest<I, D>,
 }
 
 impl SyncRequestBuilder<()> {
@@ -123,11 +123,11 @@ impl SyncRequestBuilder<()> {
     }
 }
 
-impl<I> SyncRequestBuilder<I> {
+impl<I, D> SyncRequestBuilder<I, D> {
     /// Set the initial chain tip for the sync request.
     ///
     /// This is used to update [`LocalChain`](../../bdk_chain/local_chain/struct.LocalChain.html).
-    pub fn chain_tip(mut self, cp: CheckPoint) -> Self {
+    pub fn chain_tip(mut self, cp: CheckPoint<D>) -> Self {
         self.inner.chain_tip = Some(cp);
         self
     }
@@ -141,6 +141,7 @@ impl<I> SyncRequestBuilder<I> {
     /// html).
     ///
     /// ```rust
+    /// # use bdk_chain::bitcoin::BlockHash;
     /// # use bdk_chain::spk_client::SyncRequest;
     /// # use bdk_chain::indexer::keychain_txout::KeychainTxOutIndex;
     /// # use bdk_chain::miniscript::{Descriptor, DescriptorPublicKey};
@@ -158,7 +159,7 @@ impl<I> SyncRequestBuilder<I> {
     /// let (newly_revealed_spks, _changeset) = indexer
     ///     .reveal_to_target("descriptor_a", 21)
     ///     .expect("keychain must exist");
-    /// let _request = SyncRequest::builder()
+    /// let _request: SyncRequest<u32, BlockHash> = SyncRequest::builder()
     ///     .spks_with_indexes(newly_revealed_spks)
     ///     .build();
     ///
@@ -166,7 +167,7 @@ impl<I> SyncRequestBuilder<I> {
     /// // keychains. Each spk will be indexed with `(&'static str, u32)` where `&'static str` is
     /// // the keychain identifier and `u32` is the derivation index.
     /// let all_revealed_spks = indexer.revealed_spks(..);
-    /// let _request = SyncRequest::builder()
+    /// let _request: SyncRequest<(&str, u32), BlockHash> = SyncRequest::builder()
     ///     .spks_with_indexes(all_revealed_spks)
     ///     .build();
     /// # Ok::<_, bdk_chain::keychain_txout::InsertDescriptorError<_>>(())
@@ -212,7 +213,7 @@ impl<I> SyncRequestBuilder<I> {
     }
 
     /// Build the [`SyncRequest`].
-    pub fn build(self) -> SyncRequest<I> {
+    pub fn build(self) -> SyncRequest<I, D> {
         self.inner
     }
 }
@@ -226,7 +227,7 @@ impl<I> SyncRequestBuilder<I> {
 /// ```rust
 /// # use bdk_chain::{bitcoin::{hashes::Hash, ScriptBuf}, local_chain::LocalChain};
 /// # use bdk_chain::spk_client::SyncRequest;
-/// # let (local_chain, _) = LocalChain::from_genesis_hash(Hash::all_zeros());
+/// # let (local_chain, _) = LocalChain::from_genesis(Hash::all_zeros());
 /// # let scripts = [ScriptBuf::default(), ScriptBuf::default()];
 /// // Construct a sync request.
 /// let sync_request = SyncRequest::builder()
@@ -240,9 +241,9 @@ impl<I> SyncRequestBuilder<I> {
 ///     .build();
 /// ```
 #[must_use]
-pub struct SyncRequest<I = ()> {
+pub struct SyncRequest<I = (), D = BlockHash> {
     start_time: u64,
-    chain_tip: Option<CheckPoint>,
+    chain_tip: Option<CheckPoint<D>>,
     spks: VecDeque<(I, ScriptBuf)>,
     spks_consumed: usize,
     spk_expected_txids: HashMap<ScriptBuf, HashSet<Txid>>,
@@ -253,13 +254,13 @@ pub struct SyncRequest<I = ()> {
     inspect: Box<InspectSync<I>>,
 }
 
-impl<I> From<SyncRequestBuilder<I>> for SyncRequest<I> {
-    fn from(builder: SyncRequestBuilder<I>) -> Self {
+impl<I, D> From<SyncRequestBuilder<I, D>> for SyncRequest<I, D> {
+    fn from(builder: SyncRequestBuilder<I, D>) -> Self {
         builder.inner
     }
 }
 
-impl<I> SyncRequest<I> {
+impl<I, D> SyncRequest<I, D> {
     /// Start building [`SyncRequest`] with a given `start_time`.
     ///
     /// `start_time` specifies the start time of sync. Chain sources can use this value to set
@@ -268,7 +269,7 @@ impl<I> SyncRequest<I> {
     ///
     /// Use [`SyncRequest::builder`] to use the current timestamp as `start_time` (this requires
     /// `feature = "std"`).
-    pub fn builder_at(start_time: u64) -> SyncRequestBuilder<I> {
+    pub fn builder_at(start_time: u64) -> SyncRequestBuilder<I, D> {
         SyncRequestBuilder {
             inner: Self {
                 start_time,
@@ -291,7 +292,7 @@ impl<I> SyncRequest<I> {
     /// is not available.
     #[cfg(feature = "std")]
     #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
-    pub fn builder() -> SyncRequestBuilder<I> {
+    pub fn builder() -> SyncRequestBuilder<I, D> {
         let start_time = std::time::UNIX_EPOCH
             .elapsed()
             .expect("failed to get current timestamp")
@@ -317,7 +318,7 @@ impl<I> SyncRequest<I> {
     }
 
     /// Get the chain tip [`CheckPoint`] of this request (if any).
-    pub fn chain_tip(&self) -> Option<CheckPoint> {
+    pub fn chain_tip(&self) -> Option<CheckPoint<D>> {
         self.chain_tip.clone()
     }
 
@@ -364,17 +365,17 @@ impl<I> SyncRequest<I> {
     pub fn iter_spks_with_expected_txids(
         &mut self,
     ) -> impl ExactSizeIterator<Item = SpkWithExpectedTxids> + '_ {
-        SyncIter::<I, SpkWithExpectedTxids>::new(self)
+        SyncIter::<I, D, SpkWithExpectedTxids>::new(self)
     }
 
     /// Iterate over [`Txid`]s contained in this request.
     pub fn iter_txids(&mut self) -> impl ExactSizeIterator<Item = Txid> + '_ {
-        SyncIter::<I, Txid>::new(self)
+        SyncIter::<I, D, Txid>::new(self)
     }
 
     /// Iterate over [`OutPoint`]s contained in this request.
     pub fn iter_outpoints(&mut self) -> impl ExactSizeIterator<Item = OutPoint> + '_ {
-        SyncIter::<I, OutPoint>::new(self)
+        SyncIter::<I, D, OutPoint>::new(self)
     }
 
     fn _call_inspect(&mut self, item: SyncItem<I>) {
@@ -388,14 +389,14 @@ impl<I> SyncRequest<I> {
 /// See also [`SyncRequest`].
 #[must_use]
 #[derive(Debug)]
-pub struct SyncResponse<A = ConfirmationBlockTime> {
+pub struct SyncResponse<A = ConfirmationBlockTime, D = BlockHash> {
     /// Relevant transaction data discovered during the scan.
     pub tx_update: crate::TxUpdate<A>,
     /// Changes to the chain discovered during the scan.
-    pub chain_update: Option<CheckPoint>,
+    pub chain_update: Option<CheckPoint<D>>,
 }
 
-impl<A> Default for SyncResponse<A> {
+impl<A, D> Default for SyncResponse<A, D> {
     fn default() -> Self {
         Self {
             tx_update: Default::default(),
@@ -415,15 +416,15 @@ impl<A> SyncResponse<A> {
 ///
 /// Construct with [`FullScanRequest::builder`].
 #[must_use]
-pub struct FullScanRequestBuilder<K> {
-    inner: FullScanRequest<K>,
+pub struct FullScanRequestBuilder<K, D = BlockHash> {
+    inner: FullScanRequest<K, D>,
 }
 
-impl<K: Ord> FullScanRequestBuilder<K> {
+impl<K: Ord, D> FullScanRequestBuilder<K, D> {
     /// Set the initial chain tip for the full scan request.
     ///
     /// This is used to update [`LocalChain`](../../bdk_chain/local_chain/struct.LocalChain.html).
-    pub fn chain_tip(mut self, tip: CheckPoint) -> Self {
+    pub fn chain_tip(mut self, tip: CheckPoint<D>) -> Self {
         self.inner.chain_tip = Some(tip);
         self
     }
@@ -450,7 +451,7 @@ impl<K: Ord> FullScanRequestBuilder<K> {
     }
 
     /// Build the [`FullScanRequest`].
-    pub fn build(self) -> FullScanRequest<K> {
+    pub fn build(self) -> FullScanRequest<K, D> {
         self.inner
     }
 }
@@ -463,20 +464,20 @@ impl<K: Ord> FullScanRequestBuilder<K> {
 /// used scripts is not known. The full scan process also updates the chain from the given
 /// [`chain_tip`](FullScanRequestBuilder::chain_tip) (if provided).
 #[must_use]
-pub struct FullScanRequest<K> {
+pub struct FullScanRequest<K, D = BlockHash> {
     start_time: u64,
-    chain_tip: Option<CheckPoint>,
+    chain_tip: Option<CheckPoint<D>>,
     spks_by_keychain: BTreeMap<K, Box<dyn Iterator<Item = Indexed<ScriptBuf>> + Send>>,
     inspect: Box<InspectFullScan<K>>,
 }
 
-impl<K> From<FullScanRequestBuilder<K>> for FullScanRequest<K> {
-    fn from(builder: FullScanRequestBuilder<K>) -> Self {
+impl<K, D> From<FullScanRequestBuilder<K, D>> for FullScanRequest<K, D> {
+    fn from(builder: FullScanRequestBuilder<K, D>) -> Self {
         builder.inner
     }
 }
 
-impl<K: Ord + Clone> FullScanRequest<K> {
+impl<K: Ord + Clone, D> FullScanRequest<K, D> {
     /// Start building a [`FullScanRequest`] with a given `start_time`.
     ///
     /// `start_time` specifies the start time of sync. Chain sources can use this value to set
@@ -485,7 +486,7 @@ impl<K: Ord + Clone> FullScanRequest<K> {
     ///
     /// Use [`FullScanRequest::builder`] to use the current timestamp as `start_time` (this
     /// requires `feature = "std`).
-    pub fn builder_at(start_time: u64) -> FullScanRequestBuilder<K> {
+    pub fn builder_at(start_time: u64) -> FullScanRequestBuilder<K, D> {
         FullScanRequestBuilder {
             inner: Self {
                 start_time,
@@ -502,7 +503,7 @@ impl<K: Ord + Clone> FullScanRequest<K> {
     /// "std"` is not available.
     #[cfg(feature = "std")]
     #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
-    pub fn builder() -> FullScanRequestBuilder<K> {
+    pub fn builder() -> FullScanRequestBuilder<K, D> {
         let start_time = std::time::UNIX_EPOCH
             .elapsed()
             .expect("failed to get current timestamp")
@@ -516,7 +517,7 @@ impl<K: Ord + Clone> FullScanRequest<K> {
     }
 
     /// Get the chain tip [`CheckPoint`] of this request (if any).
-    pub fn chain_tip(&self) -> Option<CheckPoint> {
+    pub fn chain_tip(&self) -> Option<CheckPoint<D>> {
         self.chain_tip.clone()
     }
 
@@ -548,17 +549,17 @@ impl<K: Ord + Clone> FullScanRequest<K> {
 /// See also [`FullScanRequest`].
 #[must_use]
 #[derive(Debug)]
-pub struct FullScanResponse<K, A = ConfirmationBlockTime> {
+pub struct FullScanResponse<K, A = ConfirmationBlockTime, D = BlockHash> {
     /// Relevant transaction data discovered during the scan.
     pub tx_update: crate::TxUpdate<A>,
     /// Last active indices for the corresponding keychains (`K`). An index is active if it had a
     /// transaction associated with the script pubkey at that index.
     pub last_active_indices: BTreeMap<K, u32>,
     /// Changes to the chain discovered during the scan.
-    pub chain_update: Option<CheckPoint>,
+    pub chain_update: Option<CheckPoint<D>>,
 }
 
-impl<K, A> Default for FullScanResponse<K, A> {
+impl<K, A, D> Default for FullScanResponse<K, A, D> {
     fn default() -> Self {
         Self {
             tx_update: Default::default(),
@@ -593,13 +594,13 @@ impl<K: Ord + Clone> Iterator for KeychainSpkIter<'_, K> {
     }
 }
 
-struct SyncIter<'r, I, Item> {
-    request: &'r mut SyncRequest<I>,
+struct SyncIter<'r, I, D, Item> {
+    request: &'r mut SyncRequest<I, D>,
     marker: core::marker::PhantomData<Item>,
 }
 
-impl<'r, I, Item> SyncIter<'r, I, Item> {
-    fn new(request: &'r mut SyncRequest<I>) -> Self {
+impl<'r, I, D, Item> SyncIter<'r, I, D, Item> {
+    fn new(request: &'r mut SyncRequest<I, D>) -> Self {
         Self {
             request,
             marker: core::marker::PhantomData,
@@ -607,9 +608,12 @@ impl<'r, I, Item> SyncIter<'r, I, Item> {
     }
 }
 
-impl<'r, I, Item> ExactSizeIterator for SyncIter<'r, I, Item> where SyncIter<'r, I, Item>: Iterator {}
+impl<'r, I, D, Item> ExactSizeIterator for SyncIter<'r, I, D, Item> where
+    SyncIter<'r, I, D, Item>: Iterator
+{
+}
 
-impl<I> Iterator for SyncIter<'_, I, SpkWithExpectedTxids> {
+impl<I, D> Iterator for SyncIter<'_, I, D, SpkWithExpectedTxids> {
     type Item = SpkWithExpectedTxids;
 
     fn next(&mut self) -> Option<Self::Item> {
@@ -622,7 +626,7 @@ impl<I> Iterator for SyncIter<'_, I, SpkWithExpectedTxids> {
     }
 }
 
-impl<I> Iterator for SyncIter<'_, I, Txid> {
+impl<I, D> Iterator for SyncIter<'_, I, D, Txid> {
     type Item = Txid;
 
     fn next(&mut self) -> Option<Self::Item> {
@@ -635,7 +639,7 @@ impl<I> Iterator for SyncIter<'_, I, Txid> {
     }
 }
 
-impl<I> Iterator for SyncIter<'_, I, OutPoint> {
+impl<I, D> Iterator for SyncIter<'_, I, D, OutPoint> {
     type Item = OutPoint;
 
     fn next(&mut self) -> Option<Self::Item> {
index a5194e5b9c525c7b8549481b63d13d890eab70c6..a47567618cab1e725e0471cd309f7ffc145b7375 100644 (file)
@@ -1,5 +1,6 @@
-use bdk_core::{BlockId, CheckPoint};
+use bdk_core::CheckPoint;
 use bdk_testenv::{block_id, hash};
+use bitcoin::BlockHash;
 
 /// Inserting a block that already exists in the checkpoint chain must always succeed.
 #[test]
@@ -14,15 +15,22 @@ fn checkpoint_insert_existing() {
     // Index `i` allows us to test with chains of different length.
     // Index `j` allows us to test inserting different block heights.
     for i in 0..blocks.len() {
-        let cp_chain = CheckPoint::from_block_ids(blocks[..=i].iter().copied())
-            .expect("must construct valid chain");
+        let cp_chain = CheckPoint::from_blocks(
+            blocks[..=i]
+                .iter()
+                .copied()
+                .map(|block_id| (block_id.height, block_id.hash)),
+        )
+        .expect("must construct valid chain");
 
         for j in 0..=i {
             let block_to_insert = cp_chain
                 .get(j as u32)
                 .expect("cp of height must exist")
                 .block_id();
-            let new_cp_chain = cp_chain.clone().insert(block_to_insert);
+            let new_cp_chain = cp_chain
+                .clone()
+                .insert(block_to_insert.height, block_to_insert.hash);
 
             assert_eq!(
                 new_cp_chain, cp_chain,
@@ -39,15 +47,11 @@ fn checkpoint_destruction_is_sound() {
     // this could have caused a stack overflow due to drop recursion in Arc.
     // We test that a long linked list can clean itself up without blowing
     // out the stack.
-    let mut cp = CheckPoint::new(BlockId {
-        height: 0,
-        hash: hash!("g"),
-    });
+    let mut cp = CheckPoint::new(0, hash!("g"));
     let end = 10_000;
     for height in 1u32..end {
-        let hash = bitcoin::hashes::Hash::hash(height.to_be_bytes().as_slice());
-        let block = BlockId { height, hash };
-        cp = cp.push(block).unwrap();
+        let hash: BlockHash = bitcoin::hashes::Hash::hash(height.to_be_bytes().as_slice());
+        cp = cp.push(height, hash).unwrap();
     }
     assert_eq!(cp.iter().count() as u32, end);
 }
index 063fdd629669dd0d66297acce3fbe1c29de2a42d..12ecf06aa141d642fda6eebb2817be12dff4e138 100644 (file)
@@ -77,10 +77,7 @@ pub fn test_sync_performance(c: &mut Criterion) {
     );
 
     // Setup receiver.
-    let genesis_cp = CheckPoint::new(bdk_core::BlockId {
-        height: 0,
-        hash: env.bitcoind.client.get_block_hash(0).unwrap(),
-    });
+    let genesis_cp = CheckPoint::new(0, env.bitcoind.client.get_block_hash(0).unwrap());
 
     {
         let electrum_client =
index f5eee7a8090bb275f5363177bcd8bf09764b61b1..ea14fa14e29555b72565e2786e2fdf5e425a166d 100644 (file)
@@ -607,8 +607,8 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
 /// fetched to construct checkpoint updates with the proper [`BlockHash`] in case of re-org.
 fn fetch_tip_and_latest_blocks(
     client: &impl ElectrumApi,
-    prev_tip: CheckPoint,
-) -> Result<(CheckPoint, BTreeMap<u32, BlockHash>), Error> {
+    prev_tip: CheckPoint<BlockHash>,
+) -> Result<(CheckPoint<BlockHash>, BTreeMap<u32, BlockHash>), Error> {
     let HeaderNotification { height, .. } = client.block_headers_subscribe()?;
     let new_tip_height = height as u32;
 
@@ -632,7 +632,7 @@ fn fetch_tip_and_latest_blocks(
 
     // Find the "point of agreement" (if any).
     let agreement_cp = {
-        let mut agreement_cp = Option::<CheckPoint>::None;
+        let mut agreement_cp = Option::<CheckPoint<BlockHash>>::None;
         for cp in prev_tip.iter() {
             let cp_block = cp.block_id();
             let hash = match new_blocks.get(&cp_block.height) {
@@ -662,7 +662,7 @@ fn fetch_tip_and_latest_blocks(
             let agreement_height = agreement_cp.height();
             move |(height, _)| **height > agreement_height
         })
-        .map(|(&height, &hash)| BlockId { height, hash });
+        .map(|(&height, &hash)| (height, hash));
     let new_tip = agreement_cp
         .extend(extension)
         .expect("extension heights already checked to be greater than agreement height");
@@ -673,10 +673,10 @@ fn fetch_tip_and_latest_blocks(
 // 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,
+    mut tip: CheckPoint<BlockHash>,
     latest_blocks: &BTreeMap<u32, BlockHash>,
     anchors: impl Iterator<Item = (ConfirmationBlockTime, Txid)>,
-) -> Result<CheckPoint, Error> {
+) -> Result<CheckPoint<BlockHash>, Error> {
     for (anchor, _txid) in anchors {
         let height = anchor.block_id.height;
 
@@ -687,7 +687,7 @@ fn chain_update(
                 Some(&hash) => hash,
                 None => anchor.block_id.hash,
             };
-            tip = tip.insert(BlockId { hash, height });
+            tip = tip.insert(height, hash);
         }
     }
     Ok(tip)
@@ -699,7 +699,7 @@ mod test {
     use crate::{bdk_electrum_client::TxUpdate, electrum_client::ElectrumApi, BdkElectrumClient};
     use bdk_chain::bitcoin::Amount;
     use bdk_chain::bitcoin::{constants, Network, OutPoint, ScriptBuf, Transaction, TxIn};
-    use bdk_chain::{BlockId, CheckPoint};
+    use bdk_chain::CheckPoint;
     use bdk_core::{collections::BTreeMap, spk_client::SyncRequest};
     use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, utils::new_tx, TestEnv};
     use core::time::Duration;
@@ -748,10 +748,7 @@ mod test {
 
         let bogus_spks: Vec<ScriptBuf> = Vec::new();
         let bogus_genesis = constants::genesis_block(Network::Testnet).block_hash();
-        let bogus_cp = CheckPoint::new(BlockId {
-            height: 0,
-            hash: bogus_genesis,
-        });
+        let bogus_cp = CheckPoint::new(0, bogus_genesis);
 
         let req = SyncRequest::builder()
             .chain_tip(bogus_cp)
index ec226ab61f060cb0d31bc3c807c18996c612608e..7fcf5d801176a2cd845961c20284ccfb25665d2d 100644 (file)
@@ -93,7 +93,7 @@ pub fn detect_receive_tx_cancel() -> anyhow::Result<()> {
     let client = BdkElectrumClient::new(electrum_client);
 
     let mut graph = IndexedTxGraph::<ConfirmationBlockTime, _>::new(SpkTxOutIndex::<()>::default());
-    let (chain, _) = LocalChain::from_genesis_hash(env.bitcoind.client.get_block_hash(0)?);
+    let (chain, _) = LocalChain::from_genesis(env.bitcoind.client.get_block_hash(0)?);
 
     // Get receiving address.
     let receiver_spk = get_test_spk();
@@ -512,7 +512,7 @@ fn test_sync() -> anyhow::Result<()> {
     let addr_to_track = Address::from_script(&spk_to_track, bdk_chain::bitcoin::Network::Regtest)?;
 
     // Setup receiver.
-    let (mut recv_chain, _) = LocalChain::from_genesis_hash(env.bitcoind.client.get_block_hash(0)?);
+    let (mut recv_chain, _) = LocalChain::from_genesis(env.bitcoind.client.get_block_hash(0)?);
     let mut recv_graph = IndexedTxGraph::<ConfirmationBlockTime, _>::new({
         let mut recv_index = SpkTxOutIndex::default();
         recv_index.insert_spk((), spk_to_track.clone());
@@ -655,7 +655,7 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
     let addr_to_track = Address::from_script(&spk_to_track, bdk_chain::bitcoin::Network::Regtest)?;
 
     // Setup receiver.
-    let (mut recv_chain, _) = LocalChain::from_genesis_hash(env.bitcoind.client.get_block_hash(0)?);
+    let (mut recv_chain, _) = LocalChain::from_genesis(env.bitcoind.client.get_block_hash(0)?);
     let mut recv_graph = IndexedTxGraph::<ConfirmationBlockTime, _>::new({
         let mut recv_index = SpkTxOutIndex::default();
         recv_index.insert_spk((), spk_to_track.clone());
@@ -741,7 +741,7 @@ fn test_sync_with_coinbase() -> anyhow::Result<()> {
     let addr_to_track = Address::from_script(&spk_to_track, bdk_chain::bitcoin::Network::Regtest)?;
 
     // Setup receiver.
-    let (mut recv_chain, _) = LocalChain::from_genesis_hash(env.bitcoind.client.get_block_hash(0)?);
+    let (mut recv_chain, _) = LocalChain::from_genesis(env.bitcoind.client.get_block_hash(0)?);
     let mut recv_graph = IndexedTxGraph::<ConfirmationBlockTime, _>::new({
         let mut recv_index = SpkTxOutIndex::default();
         recv_index.insert_spk((), spk_to_track.clone());
@@ -776,7 +776,7 @@ fn test_check_fee_calculation() -> anyhow::Result<()> {
     let addr_to_track = Address::from_script(&spk_to_track, bdk_chain::bitcoin::Network::Regtest)?;
 
     // Setup receiver.
-    let (mut recv_chain, _) = LocalChain::from_genesis_hash(env.bitcoind.client.get_block_hash(0)?);
+    let (mut recv_chain, _) = LocalChain::from_genesis(env.bitcoind.client.get_block_hash(0)?);
     let mut recv_graph = IndexedTxGraph::<ConfirmationBlockTime, _>::new({
         let mut recv_index = SpkTxOutIndex::default();
         recv_index.insert_spk((), spk_to_track.clone());
index c0e55ab50764a342e91d51dd19497995692bdf1b..c9cb17c1e1e7d3cceb886128f37da8a9a217dc33 100644 (file)
@@ -227,9 +227,9 @@ async fn fetch_block<S: Sleeper>(
 async fn chain_update<S: Sleeper>(
     client: &esplora_client::AsyncClient<S>,
     latest_blocks: &BTreeMap<u32, BlockHash>,
-    local_tip: &CheckPoint,
+    local_tip: &CheckPoint<BlockHash>,
     anchors: &BTreeSet<(ConfirmationBlockTime, Txid)>,
-) -> Result<CheckPoint, Error> {
+) -> Result<CheckPoint<BlockHash>, Error> {
     let mut point_of_agreement = None;
     let mut local_cp_hash = local_tip.hash();
     let mut conflicts = vec![];
@@ -263,7 +263,7 @@ async fn chain_update<S: Sleeper>(
     };
 
     tip = tip
-        .extend(conflicts.into_iter().rev())
+        .extend(conflicts.into_iter().rev().map(|b| (b.height, b.hash)))
         .expect("evicted are in order");
 
     for (anchor, _txid) in anchors {
@@ -273,14 +273,14 @@ async fn chain_update<S: Sleeper>(
                 Some(hash) => hash,
                 None => continue,
             };
-            tip = tip.insert(BlockId { height, hash });
+            tip = tip.insert(height, hash);
         }
     }
 
     // insert the most recent blocks at the tip to make sure we update the tip and make the update
     // robust.
     for (&height, &hash) in latest_blocks.iter() {
-        tip = tip.insert(BlockId { height, hash });
+        tip = tip.insert(height, hash);
     }
 
     Ok(tip)
@@ -590,10 +590,7 @@ mod test {
 
         let genesis_hash =
             bitcoin::constants::genesis_block(bitcoin::Network::Testnet4).block_hash();
-        let cp = bdk_chain::CheckPoint::new(BlockId {
-            height: 0,
-            hash: genesis_hash,
-        });
+        let cp = bdk_chain::CheckPoint::new(0, genesis_hash);
 
         let anchors = BTreeSet::new();
         let res = chain_update(&client, &latest_blocks, &cp, &anchors).await;
@@ -666,7 +663,7 @@ mod test {
 
             // craft initial `local_chain`
             let local_chain = {
-                let (mut chain, _) = LocalChain::from_genesis_hash(env.genesis_hash()?);
+                let (mut chain, _) = LocalChain::from_genesis(env.genesis_hash()?);
                 // force `chain_update_blocking` to add all checkpoints in `t.initial_cps`
                 let anchors = t
                     .initial_cps
index 5a52b7a098f91c3f413d337c931ef024431339e6..5f8ab531c9f345497c2ede8146723ca2b276caf8 100644 (file)
@@ -212,9 +212,9 @@ fn fetch_block(
 fn chain_update(
     client: &esplora_client::BlockingClient,
     latest_blocks: &BTreeMap<u32, BlockHash>,
-    local_tip: &CheckPoint,
+    local_tip: &CheckPoint<BlockHash>,
     anchors: &BTreeSet<(ConfirmationBlockTime, Txid)>,
-) -> Result<CheckPoint, Error> {
+) -> Result<CheckPoint<BlockHash>, Error> {
     let mut point_of_agreement = None;
     let mut local_cp_hash = local_tip.hash();
     let mut conflicts = vec![];
@@ -248,7 +248,7 @@ fn chain_update(
     };
 
     tip = tip
-        .extend(conflicts.into_iter().rev())
+        .extend(conflicts.into_iter().rev().map(|b| (b.height, b.hash)))
         .expect("evicted are in order");
 
     for (anchor, _) in anchors {
@@ -258,14 +258,14 @@ fn chain_update(
                 Some(hash) => hash,
                 None => continue,
             };
-            tip = tip.insert(BlockId { height, hash });
+            tip = tip.insert(height, hash);
         }
     }
 
     // insert the most recent blocks at the tip to make sure we update the tip and make the update
     // robust.
     for (&height, &hash) in latest_blocks.iter() {
-        tip = tip.insert(BlockId { height, hash });
+        tip = tip.insert(height, hash);
     }
 
     Ok(tip)
@@ -556,10 +556,7 @@ mod test {
 
         let genesis_hash =
             bitcoin::constants::genesis_block(bitcoin::Network::Testnet4).block_hash();
-        let cp = bdk_chain::CheckPoint::new(BlockId {
-            height: 0,
-            hash: genesis_hash,
-        });
+        let cp = bdk_chain::CheckPoint::new(0, genesis_hash);
 
         let anchors = BTreeSet::new();
         let res = chain_update(&client, &latest_blocks, &cp, &anchors);
@@ -632,7 +629,7 @@ mod test {
 
             // craft initial `local_chain`
             let local_chain = {
-                let (mut chain, _) = LocalChain::from_genesis_hash(env.genesis_hash()?);
+                let (mut chain, _) = LocalChain::from_genesis(env.genesis_hash()?);
                 // force `chain_update_blocking` to add all checkpoints in `t.initial_cps`
                 let anchors = t
                     .initial_cps
index 987f04e41db467fd1604ea1246a890f7d1aa9ca4..c90c3311202075286e871d2d7779ef27781360f7 100644 (file)
@@ -30,7 +30,7 @@ pub async fn detect_receive_tx_cancel() -> anyhow::Result<()> {
     let client = Builder::new(base_url.as_str()).build_async()?;
 
     let mut graph = IndexedTxGraph::<ConfirmationBlockTime, _>::new(SpkTxOutIndex::<()>::default());
-    let (chain, _) = LocalChain::from_genesis_hash(env.bitcoind.client.get_block_hash(0)?);
+    let (chain, _) = LocalChain::from_genesis(env.bitcoind.client.get_block_hash(0)?);
 
     // Get receiving address.
     let receiver_spk = common::get_test_spk();
index d6f8c448d1b820440009b7d35f4a630c213a17e9..a09b3ccce718dc275b0648340abcd4403b9bb511 100644 (file)
@@ -30,7 +30,7 @@ pub fn detect_receive_tx_cancel() -> anyhow::Result<()> {
     let client = Builder::new(base_url.as_str()).build_blocking();
 
     let mut graph = IndexedTxGraph::<ConfirmationBlockTime, _>::new(SpkTxOutIndex::<()>::default());
-    let (chain, _) = LocalChain::from_genesis_hash(env.bitcoind.client.get_block_hash(0)?);
+    let (chain, _) = LocalChain::from_genesis(env.bitcoind.client.get_block_hash(0)?);
 
     // Get receiving address.
     let receiver_spk = common::get_test_spk();
index 9faf43bf202a17a0c2671d00f42e9cee6f962a7b..d569db12ef9aa58176e876654801b38b93ca7214 100644 (file)
@@ -9,7 +9,6 @@ use bdk_chain::{
         ScriptBuf, ScriptHash, Transaction, TxIn, TxOut, Txid,
     },
     local_chain::CheckPoint,
-    BlockId,
 };
 use bitcoincore_rpc::{
     bitcoincore_rpc_json::{GetBlockTemplateModes, GetBlockTemplateRules},
@@ -294,13 +293,13 @@ impl TestEnv {
     }
 
     /// Create a checkpoint linked list of all the blocks in the chain.
-    pub fn make_checkpoint_tip(&self) -> CheckPoint {
-        CheckPoint::from_block_ids((0_u32..).map_while(|height| {
+    pub fn make_checkpoint_tip(&self) -> CheckPoint<BlockHash> {
+        CheckPoint::from_blocks((0_u32..).map_while(|height| {
             self.bitcoind
                 .client
                 .get_block_hash(height as u64)
                 .ok()
-                .map(|hash| BlockId { height, hash })
+                .map(|hash| (height, hash))
         }))
         .expect("must craft tip")
     }
index db5f31d0f4b617a96459b4aa98d2ff21cc6cdefb..96a41802f56d4b15353344b1aa75f569d353ef3a 100644 (file)
@@ -826,7 +826,7 @@ pub fn init_or_load<CS: clap::Subcommand, S: clap::Args>(
 
             let chain = Mutex::new({
                 let (mut chain, _) =
-                    LocalChain::from_genesis_hash(constants::genesis_block(network).block_hash());
+                    LocalChain::from_genesis(constants::genesis_block(network).block_hash());
                 chain.apply_changeset(&changeset.local_chain)?;
                 chain
             });
@@ -896,7 +896,7 @@ where
 
         // create new
         let (_, chain_changeset) =
-            LocalChain::from_genesis_hash(constants::genesis_block(network).block_hash());
+            LocalChain::from_genesis(constants::genesis_block(network).block_hash());
         changeset.network = Some(network);
         changeset.local_chain = chain_changeset;
         let mut db = Store::<ChangeSet>::create(db_magic, db_path)?;