diff --git a/crates/bitcoind_rpc/examples/filter_iter.rs b/crates/bitcoind_rpc/examples/filter_iter.rs index 174f21215..c0e755f9c 100644 --- a/crates/bitcoind_rpc/examples/filter_iter.rs +++ b/crates/bitcoind_rpc/examples/filter_iter.rs @@ -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::>::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")?; diff --git a/crates/bitcoind_rpc/src/bip158.rs b/crates/bitcoind_rpc/src/bip158.rs index 9461cda7f..81963def6 100644 --- a/crates/bitcoind_rpc/src/bip158.rs +++ b/crates/bitcoind_rpc/src/bip158.rs @@ -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, /// checkpoint - cp: CheckPoint, + cp: CheckPoint, /// Header info, contains the prev and next hashes for each header. header: Option, } @@ -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 = diff --git a/crates/bitcoind_rpc/src/lib.rs b/crates/bitcoind_rpc/src/lib.rs index eb58018e9..30fc556be 100644 --- a/crates/bitcoind_rpc/src/lib.rs +++ b/crates/bitcoind_rpc/src/lib.rs @@ -36,7 +36,7 @@ pub struct Emitter { /// 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` @@ -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, start_height: u32, expected_mempool_txs: impl IntoIterator>>, ) -> Self { @@ -235,7 +235,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 { @@ -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), /// Force the genesis checkpoint down the receiver's throat. AgreementPointNotFound(BlockHash), } @@ -331,7 +331,7 @@ where fn poll( emitter: &mut Emitter, get_item: F, -) -> Result, bitcoincore_rpc::Error> +) -> Result, 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(), diff --git a/crates/bitcoind_rpc/tests/test_emitter.rs b/crates/bitcoind_rpc/tests/test_emitter.rs index 9df3b404c..079551bf0 100644 --- a/crates/bitcoind_rpc/tests/test_emitter.rs +++ b/crates/bitcoind_rpc/tests/test_emitter.rs @@ -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::::new({ let mut index = SpkTxOutIndex::::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::::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, ); diff --git a/crates/bitcoind_rpc/tests/test_filter_iter.rs b/crates/bitcoind_rpc/tests/test_filter_iter.rs index f47d80f8b..eafe8250b 100644 --- a/crates/bitcoind_rpc/tests/test_filter_iter.rs +++ b/crates/bitcoind_rpc/tests/test_filter_iter.rs @@ -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]); diff --git a/crates/chain/benches/canonicalization.rs b/crates/chain/benches/canonicalization.rs index df9c08b01..bf11e1ebc 100644 --- a/crates/chain/benches/canonicalization.rs +++ b/crates/chain/benches/canonicalization.rs @@ -76,8 +76,12 @@ fn add_ancestor_tx(graph: &mut KeychainTxGraph, block_id: BlockId, locktime: u32 fn setup(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, _) = diff --git a/crates/chain/benches/indexer.rs b/crates/chain/benches/indexer.rs index 604d5803e..c1786a627 100644 --- a/crates/chain/benches/indexer.rs +++ b/crates/chain/benches/indexer.rs @@ -50,7 +50,12 @@ fn setup(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); diff --git a/crates/chain/src/local_chain.rs b/crates/chain/src/local_chain.rs index 0ab676e8d..81f4a1796 100644 --- a/crates/chain/src/local_chain.rs +++ b/crates/chain/src/local_chain.rs @@ -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 { +fn apply_changeset_to_checkpoint( + mut init_cp: CheckPoint, + changeset: &ChangeSet, +) -> Result, 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 = None; + let mut base: Option> = 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 { + tip: CheckPoint, +} + +impl PartialEq for LocalChain { + fn eq(&self, other: &Self) -> bool { + self.tip == other.tip + } } -impl ChainOracle for LocalChain { +impl ChainOracle for LocalChain { 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 { - 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 { - 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) -> Result { - if !blocks.contains_key(&0) { - return Err(MissingGenesisError); - } - - let mut tip: Option = 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 { - 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` +impl LocalChain { /// 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 LocalChain { + /// Get the highest checkpoint. + pub fn tip(&self) -> CheckPoint { + 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 { + 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> { + 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(&self, range: R) -> impl Iterator> + where + R: RangeBounds, + { + self.tip.range(range) + } +} + +// Methods where `D: ToBlockHash` +impl LocalChain +where + D: ToBlockHash + fmt::Debug + Copy, +{ + /// Constructs a [`LocalChain`] from genesis data. + pub fn from_genesis(data: D) -> (Self, ChangeSet) { + 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) -> Result { + 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) -> Result { + 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) -> Result { + 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, + ) -> Result, 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) -> 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 { + 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 { - if let Some(original_cp) = self.tip.get(block_id.height) { + pub fn insert_block( + &mut self, + height: u32, + data: D, + ) -> Result, 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::::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 { - let mut remove_from = Option::::None; + pub fn disconnect_from( + &mut self, + block_id: BlockId, + ) -> Result, MissingGenesisError> { + let mut remove_from = Option::>::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) -> 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 { - 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(&self, range: R) -> impl Iterator - where - R: RangeBounds, - { - 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 { /// 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>, + pub blocks: BTreeMap>, +} + +impl Default for ChangeSet { + fn default() -> Self { + ChangeSet { + blocks: BTreeMap::default(), + } + } } -impl Merge for ChangeSet { +impl Merge for ChangeSet { fn merge(&mut self, other: Self) { Merge::merge(&mut self.blocks, other.blocks) } @@ -426,24 +458,24 @@ impl Merge for ChangeSet { } } -impl)>> From for ChangeSet { - fn from(blocks: B) -> Self { +impl)>> From for ChangeSet { + fn from(blocks: I) -> Self { Self { blocks: blocks.into_iter().collect(), } } } -impl FromIterator<(u32, Option)> for ChangeSet { - fn from_iter)>>(iter: T) -> Self { +impl FromIterator<(u32, Option)> for ChangeSet { + fn from_iter)>>(iter: T) -> Self { Self { blocks: iter.into_iter().collect(), } } } -impl FromIterator<(u32, BlockHash)> for ChangeSet { - fn from_iter>(iter: T) -> Self { +impl FromIterator<(u32, D)> for ChangeSet { + fn from_iter>(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( + original_tip: CheckPoint, + update_tip: CheckPoint, +) -> Result<(CheckPoint, ChangeSet), CannotConnectError> +where + D: ToBlockHash + fmt::Debug + Copy, +{ + let mut changeset = ChangeSet::::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 = None; - let mut prev_update: Option = None; + let mut prev_orig: Option> = None; + let mut prev_update: Option> = 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); } diff --git a/crates/chain/src/tx_data_traits.rs b/crates/chain/src/tx_data_traits.rs index 74d1021f6..6e1100485 100644 --- a/crates/chain/src/tx_data_traits.rs +++ b/crates/chain/src/tx_data_traits.rs @@ -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::::from_blocks( /// [ /// (1, Hash::hash("first".as_bytes())), /// (2, Hash::hash("second".as_bytes())), diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 7bbdef63f..60b1f045d 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -1325,11 +1325,11 @@ impl TxGraph { /// # 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::::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 { diff --git a/crates/chain/tests/test_indexed_tx_graph.rs b/crates/chain/tests/test_indexed_tx_graph.rs index 1e87f009a..db91a34b3 100644 --- a/crates/chain/tests/test_indexed_tx_graph.rs +++ b/crates/chain/tests/test_indexed_tx_graph.rs @@ -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::::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 diff --git a/crates/chain/tests/test_local_chain.rs b/crates/chain/tests/test_local_chain.rs index 8adbce4af..7ad03f04f 100644 --- a/crates/chain/tests/test_local_chain.rs +++ b/crates/chain/tests/test_local_chain.rs @@ -1,5 +1,6 @@ #![cfg(feature = "miniscript")] +use std::collections::BTreeMap; use std::ops::{Bound, RangeBounds}; use bdk_chain::{ @@ -17,7 +18,7 @@ use proptest::prelude::*; struct TestLocalChain<'a> { name: &'static str, chain: LocalChain, - update: CheckPoint, + update: CheckPoint, exp: ExpectedResult<'a>, } @@ -368,8 +369,104 @@ 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", + ); + assert_eq!(chain, t.expected_final, "[{i}] unexpected final chain",); + } +} + +#[test] +fn local_chain_insert_header() { + fn header(prev_blockhash: BlockHash) -> Header { + Header { + version: bitcoin::block::Version::default(), + prev_blockhash, + merkle_root: bitcoin::hash_types::TxMerkleNode::all_zeros(), + time: 0, + bits: bitcoin::CompactTarget::default(), + nonce: 0, + } + } + + // Create consecutive headers of height `n`, where the genesis header is height 0. + fn build_headers(n: u32) -> Vec
{ + let mut headers = Vec::new(); + let genesis = header(hash!("_")); + headers.push(genesis); + for i in 1..=n { + let prev = headers[(i - 1) as usize].block_hash(); + headers.push(header(prev)); + } + headers + } + + let headers = build_headers(5); + + fn local_chain(data: Vec<(u32, Header)>) -> LocalChain
{ + bdk_chain::local_chain::LocalChain::from_blocks( + data.into_iter().collect::>(), + ) + .expect("chain must have genesis block") + } + + struct TestCase { + original: LocalChain
, + insert: (u32, Header), + expected_result: Result, AlterCheckPointError>, + expected_final: LocalChain
, + } + + let test_cases = [ + // Test case 1: start with only the genesis header and insert header at height 5. + TestCase { + original: local_chain(vec![(0, headers[0])]), + insert: (5, headers[5]), + expected_result: Ok([(5, Some(headers[5]))].into()), + expected_final: local_chain(vec![(0, headers[0]), (5, headers[5])]), + }, + // Test case 2: start with headers at heights 0 and 3. Insert header at height 4. + TestCase { + original: local_chain(vec![(0, headers[0]), (3, headers[3])]), + insert: (4, headers[4]), + expected_result: Ok([(4, Some(headers[4]))].into()), + expected_final: local_chain(vec![(0, headers[0]), (3, headers[3]), (4, headers[4])]), + }, + // Test case 3: start with headers at heights 0 and 4. Insert header at height 3. + TestCase { + original: local_chain(vec![(0, headers[0]), (4, headers[4])]), + insert: (3, headers[3]), + expected_result: Ok([(3, Some(headers[3]))].into()), + expected_final: local_chain(vec![(0, headers[0]), (3, headers[3]), (4, headers[4])]), + }, + // Test case 4: start with headers at heights 0 and 2. Insert the same header at height 2. + TestCase { + original: local_chain(vec![(0, headers[0]), (2, headers[2])]), + insert: (2, headers[2]), + expected_result: Ok([].into()), + expected_final: local_chain(vec![(0, headers[0]), (2, headers[2])]), + }, + // Test case 5: start with headers at heights 0 and 2. Insert conflicting header at height + // 2. + TestCase { + original: local_chain(vec![(0, headers[0]), (2, headers[2])]), + insert: (2, header(hash!("conflict"))), + expected_result: Err(AlterCheckPointError { + height: 2, + original_hash: headers[2].block_hash(), + update_hash: Some(header(hash!("conflict")).block_hash()), + }), + expected_final: local_chain(vec![(0, headers[0]), (2, headers[2])]), + }, + ]; + + for (i, t) in test_cases.into_iter().enumerate() { + let mut chain = t.original; + assert_eq!( + chain.insert_block(t.insert.0, t.insert.1), t.expected_result, "[{i}] unexpected result when inserting block", ); @@ -490,11 +587,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::::from_blocks(t.blocks.iter().copied()); match t.exp_result { Ok(_) => { assert!(result.is_ok(), "[{}:{}] should be Ok", i, t.name); @@ -639,18 +732,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 +922,15 @@ fn generate_height_range_bounds( ) } -fn generate_checkpoints(max_height: u32, max_count: usize) -> impl Strategy { +fn generate_checkpoints( + max_height: u32, + max_count: usize, +) -> impl Strategy> { 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") }) diff --git a/crates/chain/tests/test_tx_graph_conflicts.rs b/crates/chain/tests/test_tx_graph_conflicts.rs index 71944e404..cbd5d5417 100644 --- a/crates/chain/tests/test_tx_graph_conflicts.rs +++ b/crates/chain/tests/test_tx_graph_conflicts.rs @@ -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 = local_chain!( (0, hash!("A")), (1, hash!("B")), (2, hash!("C")), diff --git a/crates/core/src/checkpoint.rs b/crates/core/src/checkpoint.rs index 01b36e258..d0a9bacd7 100644 --- a/crates/core/src/checkpoint.rs +++ b/crates/core/src/checkpoint.rs @@ -1,7 +1,8 @@ +use core::fmt; use core::ops::RangeBounds; use alloc::sync::Arc; -use bitcoin::BlockHash; +use bitcoin::{block::Header, 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); +#[derive(Debug)] +pub struct CheckPoint(Arc>); + +impl Clone for CheckPoint { + 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 { + /// Block id + block_id: BlockId, + /// Data. + data: D, /// Previous checkpoint (if any). - prev: Option>, + prev: Option>>, } /// 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 Drop for CPInner { 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,75 @@ 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 always return the block’s consensus-defined hash. If +/// your type contains extra fields (timestamps, metadata, etc.), these must be ignored. For +/// example, [`BlockHash`] trivially returns itself, [`Header`] calls its `block_hash()`, and a +/// wrapper type around a [`Header`] should delegate to the header’s hash rather than derive one +/// from other 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, - ) -> Result> { - 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, - }; - - CheckPoint::new(prev_block_id) - .push(this_block_id) - .expect("must construct checkpoint") +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) } +} - /// 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 { - if self.height() < block.height { - Ok(Self(Arc::new(CPInner { - block, - prev: Some(self.0), - }))) - } else { - Err(self) - } +// Methods for any `D` +impl CheckPoint { + /// 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) -> Result { - 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 { + /// Get the previous checkpoint in the chain. + pub fn prev(&self) -> Option> { self.0.prev.clone().map(CheckPoint) } /// Iterate from this checkpoint in descending height. - pub fn iter(&self) -> CheckPointIter { + pub fn iter(&self) -> CheckPointIter { self.clone().into_iter() } @@ -173,7 +141,7 @@ impl CheckPoint { /// /// Note that we always iterate checkpoints in reverse height order (iteration starts at tip /// height). - pub fn range(&self, range: R) -> impl Iterator + pub fn range(&self, range: R) -> impl Iterator> where R: RangeBounds, { @@ -216,58 +184,122 @@ 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 CheckPoint +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. /// - /// 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 - /// it. If the height already existed and has a conflicting block hash then it will be purged - /// along with all block following it. The returned chain will have a tip of the `block_id` - /// passed in. Of course, if the `block_id` was already present then this just returns `self`. + /// 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) -> Result> { + 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) -> Result { + 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 data + /// we inserted and all pre-existing entries higher than it will be re-inserted after it. If the + /// height already existed and has a conflicting block hash then it will be purged along with + /// all entries following it. The returned chain will have a tip of the data passed in. Of + /// course, if the data was already present then this just returns `self`. /// /// # Panics /// /// 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"); - // if we have a conflict we just return the inserted block because the tail is by + // If we have a conflict we just return the inserted data because the tail is by // implication invalid. tail = vec![]; 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 { + 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>, +pub struct CheckPointIter { + current: Option>>, } -impl Iterator for CheckPointIter { - type Item = CheckPoint; +impl Iterator for CheckPointIter { + type Item = CheckPoint; fn next(&mut self) -> Option { let current = self.current.clone()?; @@ -276,9 +308,9 @@ impl Iterator for CheckPointIter { } } -impl IntoIterator for CheckPoint { - type Item = CheckPoint; - type IntoIter = CheckPointIter; +impl IntoIterator for CheckPoint { + type Item = CheckPoint; + type IntoIter = CheckPointIter; fn into_iter(self) -> Self::IntoIter { CheckPointIter { diff --git a/crates/core/src/spk_client.rs b/crates/core/src/spk_client.rs index 6f45aed5f..2c80e70af 100644 --- a/crates/core/src/spk_client.rs +++ b/crates/core/src/spk_client.rs @@ -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 = dyn FnMut(SyncItem, SyncProgress) + Send + 'static; @@ -112,8 +112,8 @@ impl From for SpkWithExpectedTxids { /// /// Construct with [`SyncRequest::builder`]. #[must_use] -pub struct SyncRequestBuilder { - inner: SyncRequest, +pub struct SyncRequestBuilder { + inner: SyncRequest, } impl SyncRequestBuilder<()> { @@ -123,11 +123,11 @@ impl SyncRequestBuilder<()> { } } -impl SyncRequestBuilder { +impl SyncRequestBuilder { /// 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) -> Self { self.inner.chain_tip = Some(cp); self } @@ -141,6 +141,7 @@ impl SyncRequestBuilder { /// 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,15 +159,15 @@ impl SyncRequestBuilder { /// let (newly_revealed_spks, _changeset) = indexer /// .reveal_to_target("descriptor_a", 21) /// .expect("keychain must exist"); - /// let _request = SyncRequest::builder() + /// let _request: SyncRequest = SyncRequest::builder() /// .spks_with_indexes(newly_revealed_spks) /// .build(); /// /// // Sync all revealed spks in the indexer. This time, spks may be derived from different - /// // keychains. Each spk will be indexed with `(&'static str, u32)` where `&'static str` is - /// // the keychain identifier and `u32` is the derivation index. + /// // keychains. Each spk will be indexed with `(&str, u32)` where `&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 SyncRequestBuilder { } /// Build the [`SyncRequest`]. - pub fn build(self) -> SyncRequest { + pub fn build(self) -> SyncRequest { self.inner } } @@ -226,7 +227,7 @@ impl SyncRequestBuilder { /// ```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 SyncRequestBuilder { /// .build(); /// ``` #[must_use] -pub struct SyncRequest { +pub struct SyncRequest { start_time: u64, - chain_tip: Option, + chain_tip: Option>, spks: VecDeque<(I, ScriptBuf)>, spks_consumed: usize, spk_expected_txids: HashMap>, @@ -253,13 +254,13 @@ pub struct SyncRequest { inspect: Box>, } -impl From> for SyncRequest { - fn from(builder: SyncRequestBuilder) -> Self { +impl From> for SyncRequest { + fn from(builder: SyncRequestBuilder) -> Self { builder.inner } } -impl SyncRequest { +impl SyncRequest { /// 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 SyncRequest { /// /// Use [`SyncRequest::builder`] to use the current timestamp as `start_time` (this requires /// `feature = "std"`). - pub fn builder_at(start_time: u64) -> SyncRequestBuilder { + pub fn builder_at(start_time: u64) -> SyncRequestBuilder { SyncRequestBuilder { inner: Self { start_time, @@ -291,7 +292,7 @@ impl SyncRequest { /// is not available. #[cfg(feature = "std")] #[cfg_attr(docsrs, doc(cfg(feature = "std")))] - pub fn builder() -> SyncRequestBuilder { + pub fn builder() -> SyncRequestBuilder { let start_time = std::time::UNIX_EPOCH .elapsed() .expect("failed to get current timestamp") @@ -317,7 +318,7 @@ impl SyncRequest { } /// Get the chain tip [`CheckPoint`] of this request (if any). - pub fn chain_tip(&self) -> Option { + pub fn chain_tip(&self) -> Option> { self.chain_tip.clone() } @@ -364,17 +365,17 @@ impl SyncRequest { pub fn iter_spks_with_expected_txids( &mut self, ) -> impl ExactSizeIterator + '_ { - SyncIter::::new(self) + SyncIter::::new(self) } /// Iterate over [`Txid`]s contained in this request. pub fn iter_txids(&mut self) -> impl ExactSizeIterator + '_ { - SyncIter::::new(self) + SyncIter::::new(self) } /// Iterate over [`OutPoint`]s contained in this request. pub fn iter_outpoints(&mut self) -> impl ExactSizeIterator + '_ { - SyncIter::::new(self) + SyncIter::::new(self) } fn _call_inspect(&mut self, item: SyncItem) { @@ -388,14 +389,14 @@ impl SyncRequest { /// See also [`SyncRequest`]. #[must_use] #[derive(Debug)] -pub struct SyncResponse { +pub struct SyncResponse { /// Relevant transaction data discovered during the scan. pub tx_update: crate::TxUpdate, /// Changes to the chain discovered during the scan. - pub chain_update: Option, + pub chain_update: Option>, } -impl Default for SyncResponse { +impl Default for SyncResponse { fn default() -> Self { Self { tx_update: Default::default(), @@ -415,15 +416,15 @@ impl SyncResponse { /// /// Construct with [`FullScanRequest::builder`]. #[must_use] -pub struct FullScanRequestBuilder { - inner: FullScanRequest, +pub struct FullScanRequestBuilder { + inner: FullScanRequest, } -impl FullScanRequestBuilder { +impl FullScanRequestBuilder { /// 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) -> Self { self.inner.chain_tip = Some(tip); self } @@ -450,7 +451,7 @@ impl FullScanRequestBuilder { } /// Build the [`FullScanRequest`]. - pub fn build(self) -> FullScanRequest { + pub fn build(self) -> FullScanRequest { self.inner } } @@ -463,20 +464,20 @@ impl FullScanRequestBuilder { /// 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 { +pub struct FullScanRequest { start_time: u64, - chain_tip: Option, + chain_tip: Option>, spks_by_keychain: BTreeMap> + Send>>, inspect: Box>, } -impl From> for FullScanRequest { - fn from(builder: FullScanRequestBuilder) -> Self { +impl From> for FullScanRequest { + fn from(builder: FullScanRequestBuilder) -> Self { builder.inner } } -impl FullScanRequest { +impl FullScanRequest { /// 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 FullScanRequest { /// /// Use [`FullScanRequest::builder`] to use the current timestamp as `start_time` (this /// requires `feature = "std`). - pub fn builder_at(start_time: u64) -> FullScanRequestBuilder { + pub fn builder_at(start_time: u64) -> FullScanRequestBuilder { FullScanRequestBuilder { inner: Self { start_time, @@ -502,7 +503,7 @@ impl FullScanRequest { /// "std"` is not available. #[cfg(feature = "std")] #[cfg_attr(docsrs, doc(cfg(feature = "std")))] - pub fn builder() -> FullScanRequestBuilder { + pub fn builder() -> FullScanRequestBuilder { let start_time = std::time::UNIX_EPOCH .elapsed() .expect("failed to get current timestamp") @@ -516,7 +517,7 @@ impl FullScanRequest { } /// Get the chain tip [`CheckPoint`] of this request (if any). - pub fn chain_tip(&self) -> Option { + pub fn chain_tip(&self) -> Option> { self.chain_tip.clone() } @@ -548,17 +549,17 @@ impl FullScanRequest { /// See also [`FullScanRequest`]. #[must_use] #[derive(Debug)] -pub struct FullScanResponse { +pub struct FullScanResponse { /// Relevant transaction data discovered during the scan. pub tx_update: crate::TxUpdate, /// 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, /// Changes to the chain discovered during the scan. - pub chain_update: Option, + pub chain_update: Option>, } -impl Default for FullScanResponse { +impl Default for FullScanResponse { fn default() -> Self { Self { tx_update: Default::default(), @@ -593,13 +594,13 @@ impl Iterator for KeychainSpkIter<'_, K> { } } -struct SyncIter<'r, I, Item> { - request: &'r mut SyncRequest, +struct SyncIter<'r, I, D, Item> { + request: &'r mut SyncRequest, marker: core::marker::PhantomData, } -impl<'r, I, Item> SyncIter<'r, I, Item> { - fn new(request: &'r mut SyncRequest) -> Self { +impl<'r, I, D, Item> SyncIter<'r, I, D, Item> { + fn new(request: &'r mut SyncRequest) -> 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 Iterator for SyncIter<'_, I, SpkWithExpectedTxids> { +impl Iterator for SyncIter<'_, I, D, SpkWithExpectedTxids> { type Item = SpkWithExpectedTxids; fn next(&mut self) -> Option { @@ -622,7 +626,7 @@ impl Iterator for SyncIter<'_, I, SpkWithExpectedTxids> { } } -impl Iterator for SyncIter<'_, I, Txid> { +impl Iterator for SyncIter<'_, I, D, Txid> { type Item = Txid; fn next(&mut self) -> Option { @@ -635,7 +639,7 @@ impl Iterator for SyncIter<'_, I, Txid> { } } -impl Iterator for SyncIter<'_, I, OutPoint> { +impl Iterator for SyncIter<'_, I, D, OutPoint> { type Item = OutPoint; fn next(&mut self) -> Option { diff --git a/crates/core/tests/test_checkpoint.rs b/crates/core/tests/test_checkpoint.rs index a5194e5b9..a47567618 100644 --- a/crates/core/tests/test_checkpoint.rs +++ b/crates/core/tests/test_checkpoint.rs @@ -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); } diff --git a/crates/electrum/benches/test_sync.rs b/crates/electrum/benches/test_sync.rs index 063fdd629..12ecf06aa 100644 --- a/crates/electrum/benches/test_sync.rs +++ b/crates/electrum/benches/test_sync.rs @@ -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 = diff --git a/crates/electrum/src/bdk_electrum_client.rs b/crates/electrum/src/bdk_electrum_client.rs index f5eee7a80..ea14fa14e 100644 --- a/crates/electrum/src/bdk_electrum_client.rs +++ b/crates/electrum/src/bdk_electrum_client.rs @@ -607,8 +607,8 @@ impl BdkElectrumClient { /// 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), Error> { + prev_tip: CheckPoint, +) -> Result<(CheckPoint, BTreeMap), 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::::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) { @@ -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, latest_blocks: &BTreeMap, anchors: impl Iterator, -) -> Result { +) -> Result, 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 = 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) diff --git a/crates/electrum/tests/test_electrum.rs b/crates/electrum/tests/test_electrum.rs index ec226ab61..7fcf5d801 100644 --- a/crates/electrum/tests/test_electrum.rs +++ b/crates/electrum/tests/test_electrum.rs @@ -93,7 +93,7 @@ pub fn detect_receive_tx_cancel() -> anyhow::Result<()> { let client = BdkElectrumClient::new(electrum_client); let mut graph = IndexedTxGraph::::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::::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::::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::::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::::new({ let mut recv_index = SpkTxOutIndex::default(); recv_index.insert_spk((), spk_to_track.clone()); diff --git a/crates/esplora/src/async_ext.rs b/crates/esplora/src/async_ext.rs index c0e55ab50..c9cb17c1e 100644 --- a/crates/esplora/src/async_ext.rs +++ b/crates/esplora/src/async_ext.rs @@ -227,9 +227,9 @@ async fn fetch_block( async fn chain_update( client: &esplora_client::AsyncClient, latest_blocks: &BTreeMap, - local_tip: &CheckPoint, + local_tip: &CheckPoint, anchors: &BTreeSet<(ConfirmationBlockTime, Txid)>, -) -> Result { +) -> Result, 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( }; 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( 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 diff --git a/crates/esplora/src/blocking_ext.rs b/crates/esplora/src/blocking_ext.rs index 5a52b7a09..5f8ab531c 100644 --- a/crates/esplora/src/blocking_ext.rs +++ b/crates/esplora/src/blocking_ext.rs @@ -212,9 +212,9 @@ fn fetch_block( fn chain_update( client: &esplora_client::BlockingClient, latest_blocks: &BTreeMap, - local_tip: &CheckPoint, + local_tip: &CheckPoint, anchors: &BTreeSet<(ConfirmationBlockTime, Txid)>, -) -> Result { +) -> Result, 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 diff --git a/crates/esplora/tests/async_ext.rs b/crates/esplora/tests/async_ext.rs index 987f04e41..c90c33112 100644 --- a/crates/esplora/tests/async_ext.rs +++ b/crates/esplora/tests/async_ext.rs @@ -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::::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(); diff --git a/crates/esplora/tests/blocking_ext.rs b/crates/esplora/tests/blocking_ext.rs index d6f8c448d..a09b3ccce 100644 --- a/crates/esplora/tests/blocking_ext.rs +++ b/crates/esplora/tests/blocking_ext.rs @@ -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::::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(); diff --git a/crates/testenv/src/lib.rs b/crates/testenv/src/lib.rs index 9faf43bf2..d569db12e 100644 --- a/crates/testenv/src/lib.rs +++ b/crates/testenv/src/lib.rs @@ -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 { + 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") } diff --git a/examples/example_cli/src/lib.rs b/examples/example_cli/src/lib.rs index db5f31d0f..96a41802f 100644 --- a/examples/example_cli/src/lib.rs +++ b/examples/example_cli/src/lib.rs @@ -826,7 +826,7 @@ pub fn init_or_load( 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::::create(db_magic, db_path)?;