diff --git a/crates/chain/src/chain_data.rs b/crates/chain/src/chain_data.rs index ae0976de5..92ccdb65e 100644 --- a/crates/chain/src/chain_data.rs +++ b/crates/chain/src/chain_data.rs @@ -74,11 +74,11 @@ impl ConfirmationTime { } } -impl From> for ConfirmationTime { - fn from(observed_as: ChainPosition) -> Self { +impl From> for ConfirmationTime { + fn from(observed_as: ChainPosition) -> Self { match observed_as { ChainPosition::Confirmed(a) => Self::Confirmed { - height: a.confirmation_height, + height: a.block_id.height, time: a.confirmation_time, }, ChainPosition::Unconfirmed(last_seen) => Self::Unconfirmed { last_seen }, @@ -145,9 +145,7 @@ impl From<(&u32, &BlockHash)> for BlockId { } } -/// An [`Anchor`] implementation that also records the exact confirmation height of the transaction. -/// -/// Note that the confirmation block and the anchor block can be different here. +/// An [`Anchor`] implementation that also records the exact confirmation time of the transaction. /// /// Refer to [`Anchor`] for more details. #[derive(Debug, Default, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, core::hash::Hash)] @@ -156,70 +154,27 @@ impl From<(&u32, &BlockHash)> for BlockId { derive(serde::Deserialize, serde::Serialize), serde(crate = "serde_crate") )] -pub struct ConfirmationHeightAnchor { - /// The exact confirmation height of the transaction. - /// - /// It is assumed that this value is never larger than the height of the anchor block. - pub confirmation_height: u32, +pub struct ConfirmationBlockTime { /// The anchor block. - pub anchor_block: BlockId, -} - -impl Anchor for ConfirmationHeightAnchor { - fn anchor_block(&self) -> BlockId { - self.anchor_block - } - - fn confirmation_height_upper_bound(&self) -> u32 { - self.confirmation_height - } -} - -impl AnchorFromBlockPosition for ConfirmationHeightAnchor { - fn from_block_position(_block: &bitcoin::Block, block_id: BlockId, _tx_pos: usize) -> Self { - Self { - anchor_block: block_id, - confirmation_height: block_id.height, - } - } -} - -/// An [`Anchor`] implementation that also records the exact confirmation time and height of the -/// transaction. -/// -/// Note that the confirmation block and the anchor block can be different here. -/// -/// Refer to [`Anchor`] for more details. -#[derive(Debug, Default, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, core::hash::Hash)] -#[cfg_attr( - feature = "serde", - derive(serde::Deserialize, serde::Serialize), - serde(crate = "serde_crate") -)] -pub struct ConfirmationTimeHeightAnchor { - /// The confirmation height of the transaction being anchored. - pub confirmation_height: u32, + pub block_id: BlockId, /// The confirmation time of the transaction being anchored. pub confirmation_time: u64, - /// The anchor block. - pub anchor_block: BlockId, } -impl Anchor for ConfirmationTimeHeightAnchor { +impl Anchor for ConfirmationBlockTime { fn anchor_block(&self) -> BlockId { - self.anchor_block + self.block_id } fn confirmation_height_upper_bound(&self) -> u32 { - self.confirmation_height + self.block_id.height } } -impl AnchorFromBlockPosition for ConfirmationTimeHeightAnchor { +impl AnchorFromBlockPosition for ConfirmationBlockTime { fn from_block_position(block: &bitcoin::Block, block_id: BlockId, _tx_pos: usize) -> Self { Self { - anchor_block: block_id, - confirmation_height: block_id.height, + block_id, confirmation_time: block.header.time as _, } } @@ -305,19 +260,19 @@ mod test { #[test] fn chain_position_ord() { - let unconf1 = ChainPosition::::Unconfirmed(10); - let unconf2 = ChainPosition::::Unconfirmed(20); - let conf1 = ChainPosition::Confirmed(ConfirmationHeightAnchor { - confirmation_height: 9, - anchor_block: BlockId { - height: 20, + let unconf1 = ChainPosition::::Unconfirmed(10); + let unconf2 = ChainPosition::::Unconfirmed(20); + let conf1 = ChainPosition::Confirmed(ConfirmationBlockTime { + confirmation_time: 20, + block_id: BlockId { + height: 9, ..Default::default() }, }); - let conf2 = ChainPosition::Confirmed(ConfirmationHeightAnchor { - confirmation_height: 12, - anchor_block: BlockId { - height: 15, + let conf2 = ChainPosition::Confirmed(ConfirmationBlockTime { + confirmation_time: 15, + block_id: BlockId { + height: 12, ..Default::default() }, }); diff --git a/crates/chain/src/spk_client.rs b/crates/chain/src/spk_client.rs index 1ddf7a6d1..3457dfef7 100644 --- a/crates/chain/src/spk_client.rs +++ b/crates/chain/src/spk_client.rs @@ -1,7 +1,7 @@ //! Helper types for spk-based blockchain clients. use crate::{ - collections::BTreeMap, local_chain::CheckPoint, ConfirmationTimeHeightAnchor, Indexed, TxGraph, + collections::BTreeMap, local_chain::CheckPoint, ConfirmationBlockTime, Indexed, TxGraph, }; use alloc::boxed::Box; use bitcoin::{OutPoint, Script, ScriptBuf, Txid}; @@ -176,7 +176,7 @@ impl SyncRequest { /// Data returned from a spk-based blockchain client sync. /// /// See also [`SyncRequest`]. -pub struct SyncResult { +pub struct SyncResult { /// The update to apply to the receiving [`TxGraph`]. pub graph_update: TxGraph, /// The update to apply to the receiving [`LocalChain`](crate::local_chain::LocalChain). @@ -317,7 +317,7 @@ impl FullScanRequest { /// Data returned from a spk-based blockchain client full scan. /// /// See also [`FullScanRequest`]. -pub struct FullScanResult { +pub struct FullScanResult { /// The update to apply to the receiving [`LocalChain`](crate::local_chain::LocalChain). pub graph_update: TxGraph, /// The update to apply to the receiving [`TxGraph`]. diff --git a/crates/chain/src/tx_data_traits.rs b/crates/chain/src/tx_data_traits.rs index 80e29d912..8a324f6a5 100644 --- a/crates/chain/src/tx_data_traits.rs +++ b/crates/chain/src/tx_data_traits.rs @@ -20,8 +20,7 @@ use alloc::vec::Vec; /// # use bdk_chain::local_chain::LocalChain; /// # use bdk_chain::tx_graph::TxGraph; /// # use bdk_chain::BlockId; -/// # use bdk_chain::ConfirmationHeightAnchor; -/// # use bdk_chain::ConfirmationTimeHeightAnchor; +/// # use bdk_chain::ConfirmationBlockTime; /// # use bdk_chain::example_utils::*; /// # use bitcoin::hashes::Hash; /// // Initialize the local chain with two blocks. @@ -50,39 +49,19 @@ use alloc::vec::Vec; /// }, /// ); /// -/// // Insert `tx` into a `TxGraph` that uses `ConfirmationHeightAnchor` as the anchor type. -/// // This anchor records the anchor block and the confirmation height of the transaction. -/// // When a transaction is anchored with `ConfirmationHeightAnchor`, the anchor block and -/// // confirmation block can be different. However, the confirmation block cannot be higher than -/// // the anchor block and both blocks must be in the same chain for the anchor to be valid. -/// let mut graph_b = TxGraph::::default(); -/// let _ = graph_b.insert_tx(tx.clone()); -/// graph_b.insert_anchor( -/// tx.compute_txid(), -/// ConfirmationHeightAnchor { -/// anchor_block: BlockId { -/// height: 2, -/// hash: Hash::hash("second".as_bytes()), -/// }, -/// confirmation_height: 1, -/// }, -/// ); -/// -/// // Insert `tx` into a `TxGraph` that uses `ConfirmationTimeHeightAnchor` as the anchor type. -/// // This anchor records the anchor block, the confirmation height and time of the transaction. -/// // When a transaction is anchored with `ConfirmationTimeHeightAnchor`, the anchor block and -/// // confirmation block can be different. However, the confirmation block cannot be higher than -/// // the anchor block and both blocks must be in the same chain for the anchor to be valid. -/// let mut graph_c = TxGraph::::default(); +/// // Insert `tx` into a `TxGraph` that uses `ConfirmationBlockTime` as the anchor type. +/// // This anchor records the anchor block and the confirmation time of the transaction. When a +/// // transaction is anchored with `ConfirmationBlockTime`, the anchor block and confirmation block +/// // of the transaction is the same block. +/// let mut graph_c = TxGraph::::default(); /// let _ = graph_c.insert_tx(tx.clone()); /// graph_c.insert_anchor( /// tx.compute_txid(), -/// ConfirmationTimeHeightAnchor { -/// anchor_block: BlockId { +/// ConfirmationBlockTime { +/// block_id: BlockId { /// height: 2, /// hash: Hash::hash("third".as_bytes()), /// }, -/// confirmation_height: 1, /// confirmation_time: 123, /// }, /// ); diff --git a/crates/chain/tests/test_indexed_tx_graph.rs b/crates/chain/tests/test_indexed_tx_graph.rs index 7ea335ce0..01d25c061 100644 --- a/crates/chain/tests/test_indexed_tx_graph.rs +++ b/crates/chain/tests/test_indexed_tx_graph.rs @@ -10,7 +10,7 @@ use bdk_chain::{ indexed_tx_graph::{self, IndexedTxGraph}, indexer::keychain_txout::KeychainTxOutIndex, local_chain::LocalChain, - tx_graph, Balance, ChainPosition, ConfirmationHeightAnchor, DescriptorExt, Merge, + tx_graph, Balance, ChainPosition, ConfirmationBlockTime, DescriptorExt, Merge, }; use bitcoin::{ secp256k1::Secp256k1, Amount, OutPoint, Script, ScriptBuf, Transaction, TxIn, TxOut, @@ -32,7 +32,7 @@ fn insert_relevant_txs() { let spk_0 = descriptor.at_derivation_index(0).unwrap().script_pubkey(); let spk_1 = descriptor.at_derivation_index(9).unwrap().script_pubkey(); - let mut graph = IndexedTxGraph::>::new( + let mut graph = IndexedTxGraph::>::new( KeychainTxOutIndex::new(10), ); let _ = graph @@ -140,7 +140,7 @@ fn test_list_owned_txouts() { let (desc_2, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), common::DESCRIPTORS[3]).unwrap(); - let mut graph = IndexedTxGraph::>::new( + let mut graph = IndexedTxGraph::>::new( KeychainTxOutIndex::new(10), ); @@ -250,9 +250,9 @@ fn test_list_owned_txouts() { local_chain .get(height) .map(|cp| cp.block_id()) - .map(|anchor_block| ConfirmationHeightAnchor { - anchor_block, - confirmation_height: anchor_block.height, + .map(|block_id| ConfirmationBlockTime { + block_id, + confirmation_time: 100, }), ) })); @@ -261,8 +261,7 @@ fn test_list_owned_txouts() { // A helper lambda to extract and filter data from the graph. let fetch = - |height: u32, - graph: &IndexedTxGraph>| { + |height: u32, graph: &IndexedTxGraph>| { let chain_tip = local_chain .get(height) .map(|cp| cp.block_id()) diff --git a/crates/chain/tests/test_tx_graph.rs b/crates/chain/tests/test_tx_graph.rs index dc8f0144c..8ddf7f30a 100644 --- a/crates/chain/tests/test_tx_graph.rs +++ b/crates/chain/tests/test_tx_graph.rs @@ -7,7 +7,7 @@ use bdk_chain::{ collections::*, local_chain::LocalChain, tx_graph::{ChangeSet, TxGraph}, - Anchor, BlockId, ChainOracle, ChainPosition, ConfirmationHeightAnchor, Merge, + Anchor, BlockId, ChainOracle, ChainPosition, ConfirmationBlockTime, Merge, }; use bitcoin::{ absolute, hashes::Hash, transaction, Amount, BlockHash, OutPoint, ScriptBuf, SignedAmount, @@ -935,7 +935,7 @@ fn test_chain_spends() { ..common::new_tx(0) }; - let mut graph = TxGraph::::default(); + let mut graph = TxGraph::::default(); let _ = graph.insert_tx(tx_0.clone()); let _ = graph.insert_tx(tx_1.clone()); @@ -944,9 +944,9 @@ fn test_chain_spends() { for (ht, tx) in [(95, &tx_0), (98, &tx_1)] { let _ = graph.insert_anchor( tx.compute_txid(), - ConfirmationHeightAnchor { - anchor_block: tip.block_id(), - confirmation_height: ht, + ConfirmationBlockTime { + block_id: tip.get(ht).unwrap().block_id(), + confirmation_time: 100, }, ); } @@ -959,9 +959,12 @@ fn test_chain_spends() { OutPoint::new(tx_0.compute_txid(), 0) ), Some(( - ChainPosition::Confirmed(&ConfirmationHeightAnchor { - anchor_block: tip.block_id(), - confirmation_height: 98 + ChainPosition::Confirmed(&ConfirmationBlockTime { + block_id: BlockId { + hash: tip.get(98).unwrap().hash(), + height: 98, + }, + confirmation_time: 100 }), tx_1.compute_txid(), )), @@ -971,9 +974,12 @@ fn test_chain_spends() { assert_eq!( graph.get_chain_position(&local_chain, tip.block_id(), tx_0.compute_txid()), // Some(ObservedAs::Confirmed(&local_chain.get_block(95).expect("block expected"))), - Some(ChainPosition::Confirmed(&ConfirmationHeightAnchor { - anchor_block: tip.block_id(), - confirmation_height: 95 + Some(ChainPosition::Confirmed(&ConfirmationBlockTime { + block_id: BlockId { + hash: tip.get(95).unwrap().hash(), + height: 95, + }, + confirmation_time: 100 })) ); diff --git a/crates/electrum/src/bdk_electrum_client.rs b/crates/electrum/src/bdk_electrum_client.rs index 32be25886..93c9dea74 100644 --- a/crates/electrum/src/bdk_electrum_client.rs +++ b/crates/electrum/src/bdk_electrum_client.rs @@ -1,14 +1,16 @@ use bdk_chain::{ - bitcoin::{OutPoint, ScriptBuf, Transaction, Txid}, - collections::{BTreeMap, HashMap, HashSet}, + bitcoin::{block::Header, BlockHash, OutPoint, ScriptBuf, Transaction, Txid}, + collections::{BTreeMap, HashMap}, local_chain::CheckPoint, spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult}, tx_graph::TxGraph, - BlockId, ConfirmationHeightAnchor, ConfirmationTimeHeightAnchor, + Anchor, BlockId, ConfirmationBlockTime, }; -use core::str::FromStr; use electrum_client::{ElectrumApi, Error, HeaderNotification}; -use std::sync::{Arc, Mutex}; +use std::{ + collections::BTreeSet, + sync::{Arc, Mutex}, +}; /// We include a chain suffix of a certain length for the purpose of robustness. const CHAIN_SUFFIX_LENGTH: u32 = 8; @@ -21,6 +23,8 @@ pub struct BdkElectrumClient { pub inner: E, /// The transaction cache tx_cache: Mutex>>, + /// The header cache + block_header_cache: Mutex>, } impl BdkElectrumClient { @@ -29,6 +33,7 @@ impl BdkElectrumClient { Self { inner: client, tx_cache: Default::default(), + block_header_cache: Default::default(), } } @@ -65,6 +70,33 @@ impl BdkElectrumClient { Ok(tx) } + /// Fetch block header of given `height`. + /// + /// If it hits the cache it will return the cached version and avoid making the request. + fn fetch_header(&self, height: u32) -> Result { + let block_header_cache = self.block_header_cache.lock().unwrap(); + + if let Some(header) = block_header_cache.get(&height) { + return Ok(*header); + } + + drop(block_header_cache); + + self.update_header(height) + } + + /// Update a block header at given `height`. Returns the updated header. + fn update_header(&self, height: u32) -> Result { + let header = self.inner.block_header(height as usize)?; + + self.block_header_cache + .lock() + .unwrap() + .insert(height, header); + + Ok(header) + } + /// Broadcasts a transaction to the network. /// /// This is a re-export of [`ElectrumApi::transaction_broadcast`]. @@ -88,87 +120,32 @@ impl BdkElectrumClient { stop_gap: usize, batch_size: usize, fetch_prev_txouts: bool, - ) -> Result, Error> { - let mut request_spks = request.spks_by_keychain; - - // We keep track of already-scanned spks just in case a reorg happens and we need to do a - // rescan. We need to keep track of this as iterators in `keychain_spks` are "unbounded" so - // cannot be collected. In addition, we keep track of whether an spk has an active tx - // history for determining the `last_active_index`. - // * key: (keychain, spk_index) that identifies the spk. - // * val: (script_pubkey, has_tx_history). - let mut scanned_spks = BTreeMap::<(K, u32), (ScriptBuf, bool)>::new(); - - let update = loop { - let (tip, _) = construct_update_tip(&self.inner, request.chain_tip.clone())?; - let mut graph_update = TxGraph::::default(); - let cps = tip - .iter() - .take(10) - .map(|cp| (cp.height(), cp)) - .collect::>(); - - if !request_spks.is_empty() { - if !scanned_spks.is_empty() { - scanned_spks.append( - &mut self.populate_with_spks( - &cps, - &mut graph_update, - &mut scanned_spks - .iter() - .map(|(i, (spk, _))| (i.clone(), spk.clone())), - stop_gap, - batch_size, - )?, - ); - } - for (keychain, keychain_spks) in &mut request_spks { - scanned_spks.extend( - self.populate_with_spks( - &cps, - &mut graph_update, - keychain_spks, - stop_gap, - batch_size, - )? - .into_iter() - .map(|(spk_i, spk)| ((keychain.clone(), spk_i), spk)), - ); - } + ) -> Result, Error> { + let (tip, latest_blocks) = + fetch_tip_and_latest_blocks(&self.inner, request.chain_tip.clone())?; + let mut graph_update = TxGraph::::default(); + let mut last_active_indices = BTreeMap::::new(); + + for (keychain, spks) in request.spks_by_keychain { + if let Some(last_active_index) = + self.populate_with_spks(&mut graph_update, spks, stop_gap, batch_size)? + { + last_active_indices.insert(keychain, last_active_index); } + } - // check for reorgs during scan process - let server_blockhash = self.inner.block_header(tip.height() as usize)?.block_hash(); - if tip.hash() != server_blockhash { - continue; // reorg - } + let chain_update = chain_update(tip, &latest_blocks, graph_update.all_anchors())?; - // Fetch previous `TxOut`s for fee calculation if flag is enabled. - if fetch_prev_txouts { - self.fetch_prev_txout(&mut graph_update)?; - } - - let chain_update = tip; - - let keychain_update = request_spks - .into_keys() - .filter_map(|k| { - scanned_spks - .range((k.clone(), u32::MIN)..=(k.clone(), u32::MAX)) - .rev() - .find(|(_, (_, active))| *active) - .map(|((_, i), _)| (k, *i)) - }) - .collect::>(); - - break FullScanResult { - graph_update, - chain_update, - last_active_indices: keychain_update, - }; - }; + // Fetch previous `TxOut`s for fee calculation if flag is enabled. + if fetch_prev_txouts { + self.fetch_prev_txout(&mut graph_update)?; + } - Ok(ElectrumFullScanResult(update)) + Ok(FullScanResult { + graph_update, + chain_update, + last_active_indices, + }) } /// Sync a set of scripts with the blockchain (via an Electrum client) for the data specified @@ -190,32 +167,31 @@ impl BdkElectrumClient { request: SyncRequest, batch_size: usize, fetch_prev_txouts: bool, - ) -> Result { + ) -> Result { let full_scan_req = FullScanRequest::from_chain_tip(request.chain_tip.clone()) .set_spks_for_keychain((), request.spks.enumerate().map(|(i, spk)| (i as u32, spk))); - let mut full_scan_res = self - .full_scan(full_scan_req, usize::MAX, batch_size, false)? - .with_confirmation_height_anchor(); + let mut full_scan_res = self.full_scan(full_scan_req, usize::MAX, batch_size, false)?; + let (tip, latest_blocks) = + fetch_tip_and_latest_blocks(&self.inner, request.chain_tip.clone())?; - let (tip, _) = construct_update_tip(&self.inner, request.chain_tip)?; - let cps = tip - .iter() - .take(10) - .map(|cp| (cp.height(), cp)) - .collect::>(); + self.populate_with_txids(&mut full_scan_res.graph_update, request.txids)?; + self.populate_with_outpoints(&mut full_scan_res.graph_update, request.outpoints)?; - self.populate_with_txids(&cps, &mut full_scan_res.graph_update, request.txids)?; - self.populate_with_outpoints(&cps, &mut full_scan_res.graph_update, request.outpoints)?; + let chain_update = chain_update( + tip, + &latest_blocks, + full_scan_res.graph_update.all_anchors(), + )?; // Fetch previous `TxOut`s for fee calculation if flag is enabled. if fetch_prev_txouts { self.fetch_prev_txout(&mut full_scan_res.graph_update)?; } - Ok(ElectrumSyncResult(SyncResult { - chain_update: full_scan_res.chain_update, + Ok(SyncResult { + chain_update, graph_update: full_scan_res.graph_update, - })) + }) } /// Populate the `graph_update` with transactions/anchors associated with the given `spks`. @@ -223,84 +199,55 @@ impl BdkElectrumClient { /// Transactions that contains an output with requested spk, or spends form an output with /// requested spk will be added to `graph_update`. Anchors of the aforementioned transactions are /// also included. - /// - /// Checkpoints (in `cps`) are used to create anchors. The `tx_cache` is self-explanatory. - fn populate_with_spks( + fn populate_with_spks( &self, - cps: &BTreeMap, - graph_update: &mut TxGraph, - spks: &mut impl Iterator, + graph_update: &mut TxGraph, + mut spks: impl Iterator, stop_gap: usize, batch_size: usize, - ) -> Result, Error> { + ) -> Result, Error> { let mut unused_spk_count = 0_usize; - let mut scanned_spks = BTreeMap::new(); + let mut last_active_index = Option::::None; loop { let spks = (0..batch_size) .map_while(|_| spks.next()) .collect::>(); if spks.is_empty() { - return Ok(scanned_spks); + return Ok(last_active_index); } let spk_histories = self .inner .batch_script_get_history(spks.iter().map(|(_, s)| s.as_script()))?; - for ((spk_index, spk), spk_history) in spks.into_iter().zip(spk_histories) { + for ((spk_index, _spk), spk_history) in spks.into_iter().zip(spk_histories) { if spk_history.is_empty() { - scanned_spks.insert(spk_index, (spk, false)); - unused_spk_count += 1; - if unused_spk_count > stop_gap { - return Ok(scanned_spks); + unused_spk_count = unused_spk_count.saturating_add(1); + if unused_spk_count >= stop_gap { + return Ok(last_active_index); } continue; } else { - scanned_spks.insert(spk_index, (spk, true)); + last_active_index = Some(spk_index); unused_spk_count = 0; } for tx_res in spk_history { let _ = graph_update.insert_tx(self.fetch_tx(tx_res.tx_hash)?); - if let Some(anchor) = determine_tx_anchor(cps, tx_res.height, tx_res.tx_hash) { - let _ = graph_update.insert_anchor(tx_res.tx_hash, anchor); - } + self.validate_merkle_for_anchor(graph_update, tx_res.tx_hash, tx_res.height)?; } } } } - // Helper function which fetches the `TxOut`s of our relevant transactions' previous transactions, - // which we do not have by default. This data is needed to calculate the transaction fee. - fn fetch_prev_txout( - &self, - graph_update: &mut TxGraph, - ) -> Result<(), Error> { - let full_txs: Vec> = - graph_update.full_txs().map(|tx_node| tx_node.tx).collect(); - for tx in full_txs { - for vin in &tx.input { - let outpoint = vin.previous_output; - let vout = outpoint.vout; - let prev_tx = self.fetch_tx(outpoint.txid)?; - let txout = prev_tx.output[vout as usize].clone(); - let _ = graph_update.insert_txout(outpoint, txout); - } - } - Ok(()) - } - /// Populate the `graph_update` with associated transactions/anchors of `outpoints`. /// /// Transactions in which the outpoint resides, and transactions that spend from the outpoint are /// included. Anchors of the aforementioned transactions are included. - /// - /// Checkpoints (in `cps`) are used to create anchors. The `tx_cache` is self-explanatory. fn populate_with_outpoints( &self, - cps: &BTreeMap, - graph_update: &mut TxGraph, + graph_update: &mut TxGraph, outpoints: impl IntoIterator, ) -> Result<(), Error> { for outpoint in outpoints { @@ -324,9 +271,7 @@ impl BdkElectrumClient { if !has_residing && res.tx_hash == op_txid { has_residing = true; let _ = graph_update.insert_tx(Arc::clone(&op_tx)); - if let Some(anchor) = determine_tx_anchor(cps, res.height, res.tx_hash) { - let _ = graph_update.insert_anchor(res.tx_hash, anchor); - } + self.validate_merkle_for_anchor(graph_update, res.tx_hash, res.height)?; } if !has_spending && res.tx_hash != op_txid { @@ -340,9 +285,7 @@ impl BdkElectrumClient { continue; } let _ = graph_update.insert_tx(Arc::clone(&res_tx)); - if let Some(anchor) = determine_tx_anchor(cps, res.height, res.tx_hash) { - let _ = graph_update.insert_anchor(res.tx_hash, anchor); - } + self.validate_merkle_for_anchor(graph_update, res.tx_hash, res.height)?; } } } @@ -352,8 +295,7 @@ impl BdkElectrumClient { /// Populate the `graph_update` with transactions/anchors of the provided `txids`. fn populate_with_txids( &self, - cps: &BTreeMap, - graph_update: &mut TxGraph, + graph_update: &mut TxGraph, txids: impl IntoIterator, ) -> Result<(), Error> { for txid in txids { @@ -371,120 +313,100 @@ impl BdkElectrumClient { // because of restrictions of the Electrum API, we have to use the `script_get_history` // call to get confirmation status of our transaction - let anchor = match self + if let Some(r) = self .inner .script_get_history(spk)? .into_iter() .find(|r| r.tx_hash == txid) { - Some(r) => determine_tx_anchor(cps, r.height, txid), - None => continue, - }; + self.validate_merkle_for_anchor(graph_update, txid, r.height)?; + } let _ = graph_update.insert_tx(tx); - if let Some(anchor) = anchor { - let _ = graph_update.insert_anchor(txid, anchor); - } } Ok(()) } -} - -/// The result of [`BdkElectrumClient::full_scan`]. -/// -/// This can be transformed into a [`FullScanResult`] with either [`ConfirmationHeightAnchor`] or -/// [`ConfirmationTimeHeightAnchor`] anchor types. -pub struct ElectrumFullScanResult(FullScanResult); - -impl ElectrumFullScanResult { - /// Return [`FullScanResult`] with [`ConfirmationHeightAnchor`]. - pub fn with_confirmation_height_anchor(self) -> FullScanResult { - self.0 - } - /// Return [`FullScanResult`] with [`ConfirmationTimeHeightAnchor`]. - /// - /// This requires additional calls to the Electrum server. - pub fn with_confirmation_time_height_anchor( - self, - client: &BdkElectrumClient, - ) -> Result, Error> { - let res = self.0; - Ok(FullScanResult { - graph_update: try_into_confirmation_time_result(res.graph_update, &client.inner)?, - chain_update: res.chain_update, - last_active_indices: res.last_active_indices, - }) - } -} - -/// The result of [`BdkElectrumClient::sync`]. -/// -/// This can be transformed into a [`SyncResult`] with either [`ConfirmationHeightAnchor`] or -/// [`ConfirmationTimeHeightAnchor`] anchor types. -pub struct ElectrumSyncResult(SyncResult); + // Helper function which checks if a transaction is confirmed by validating the merkle proof. + // An anchor is inserted if the transaction is validated to be in a confirmed block. + fn validate_merkle_for_anchor( + &self, + graph_update: &mut TxGraph, + txid: Txid, + confirmation_height: i32, + ) -> Result<(), Error> { + if let Ok(merkle_res) = self + .inner + .transaction_get_merkle(&txid, confirmation_height as usize) + { + let mut header = self.fetch_header(merkle_res.block_height as u32)?; + let mut is_confirmed_tx = electrum_client::utils::validate_merkle_proof( + &txid, + &header.merkle_root, + &merkle_res, + ); + + // Merkle validation will fail if the header in `block_header_cache` is outdated, so we + // want to check if there is a new header and validate against the new one. + if !is_confirmed_tx { + header = self.update_header(merkle_res.block_height as u32)?; + is_confirmed_tx = electrum_client::utils::validate_merkle_proof( + &txid, + &header.merkle_root, + &merkle_res, + ); + } -impl ElectrumSyncResult { - /// Return [`SyncResult`] with [`ConfirmationHeightAnchor`]. - pub fn with_confirmation_height_anchor(self) -> SyncResult { - self.0 + if is_confirmed_tx { + let _ = graph_update.insert_anchor( + txid, + ConfirmationBlockTime { + confirmation_time: header.time as u64, + block_id: BlockId { + height: merkle_res.block_height as u32, + hash: header.block_hash(), + }, + }, + ); + } + } + Ok(()) } - /// Return [`SyncResult`] with [`ConfirmationTimeHeightAnchor`]. - /// - /// This requires additional calls to the Electrum server. - pub fn with_confirmation_time_height_anchor( - self, - client: &BdkElectrumClient, - ) -> Result, Error> { - let res = self.0; - Ok(SyncResult { - graph_update: try_into_confirmation_time_result(res.graph_update, &client.inner)?, - chain_update: res.chain_update, - }) + // Helper function which fetches the `TxOut`s of our relevant transactions' previous transactions, + // which we do not have by default. This data is needed to calculate the transaction fee. + fn fetch_prev_txout( + &self, + graph_update: &mut TxGraph, + ) -> Result<(), Error> { + let full_txs: Vec> = + graph_update.full_txs().map(|tx_node| tx_node.tx).collect(); + for tx in full_txs { + for vin in &tx.input { + let outpoint = vin.previous_output; + let vout = outpoint.vout; + let prev_tx = self.fetch_tx(outpoint.txid)?; + let txout = prev_tx.output[vout as usize].clone(); + let _ = graph_update.insert_txout(outpoint, txout); + } + } + Ok(()) } } -fn try_into_confirmation_time_result( - graph_update: TxGraph, - client: &impl ElectrumApi, -) -> Result, Error> { - let relevant_heights = graph_update - .all_anchors() - .iter() - .map(|(a, _)| a.confirmation_height) - .collect::>(); - - let height_to_time = relevant_heights - .clone() - .into_iter() - .zip( - client - .batch_block_header(relevant_heights)? - .into_iter() - .map(|bh| bh.time as u64), - ) - .collect::>(); - - Ok(graph_update.map_anchors(|a| ConfirmationTimeHeightAnchor { - anchor_block: a.anchor_block, - confirmation_height: a.confirmation_height, - confirmation_time: height_to_time[&a.confirmation_height], - })) -} - -/// Return a [`CheckPoint`] of the latest tip, that connects with `prev_tip`. -fn construct_update_tip( +/// Return a [`CheckPoint`] of the latest tip, that connects with `prev_tip`. The latest blocks are +/// fetched to construct checkpoint updates with the proper [`BlockHash`] in case of re-org. +fn fetch_tip_and_latest_blocks( client: &impl ElectrumApi, prev_tip: CheckPoint, -) -> Result<(CheckPoint, Option), Error> { +) -> Result<(CheckPoint, BTreeMap), Error> { let HeaderNotification { height, .. } = client.block_headers_subscribe()?; let new_tip_height = height as u32; // If electrum returns a tip height that is lower than our previous tip, then checkpoints do // not need updating. We just return the previous tip and use that as the point of agreement. if new_tip_height < prev_tip.height() { - return Ok((prev_tip.clone(), Some(prev_tip.height()))); + return Ok((prev_tip, BTreeMap::new())); } // Atomically fetch the latest `CHAIN_SUFFIX_LENGTH` count of blocks from Electrum. We use this @@ -527,10 +449,13 @@ fn construct_update_tip( let agreement_height = agreement_cp.as_ref().map(CheckPoint::height); let new_tip = new_blocks - .into_iter() + .iter() // Prune `new_blocks` to only include blocks that are actually new. - .filter(|(height, _)| Some(*height) > agreement_height) - .map(|(height, hash)| BlockId { height, hash }) + .filter(|(height, _)| Some(*<&u32>::clone(height)) > agreement_height) + .map(|(height, hash)| BlockId { + height: *height, + hash: *hash, + }) .fold(agreement_cp, |prev_cp, block| { Some(match prev_cp { Some(cp) => cp.push(block).expect("must extend checkpoint"), @@ -539,51 +464,28 @@ fn construct_update_tip( }) .expect("must have at least one checkpoint"); - Ok((new_tip, agreement_height)) + Ok((new_tip, new_blocks)) } -/// A [tx status] comprises of a concatenation of `tx_hash:height:`s. We transform a single one of -/// these concatenations into a [`ConfirmationHeightAnchor`] if possible. -/// -/// We use the lowest possible checkpoint as the anchor block (from `cps`). If an anchor block -/// cannot be found, or the transaction is unconfirmed, [`None`] is returned. -/// -/// [tx status](https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-basics.html#status) -fn determine_tx_anchor( - cps: &BTreeMap, - raw_height: i32, - txid: Txid, -) -> Option { - // The electrum API has a weird quirk where an unconfirmed transaction is presented with a - // height of 0. To avoid invalid representation in our data structures, we manually set - // transactions residing in the genesis block to have height 0, then interpret a height of 0 as - // unconfirmed for all other transactions. - if txid - == Txid::from_str("4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b") - .expect("must deserialize genesis coinbase txid") - { - let anchor_block = cps.values().next()?.block_id(); - return Some(ConfirmationHeightAnchor { - anchor_block, - confirmation_height: 0, - }); - } - match raw_height { - h if h <= 0 => { - debug_assert!(h == 0 || h == -1, "unexpected height ({}) from electrum", h); - None - } - h => { - let h = h as u32; - let anchor_block = cps.range(h..).next().map(|(_, cp)| cp.block_id())?; - if h > anchor_block.height { - None - } else { - Some(ConfirmationHeightAnchor { - anchor_block, - confirmation_height: h, - }) - } +// Add a corresponding checkpoint per anchor height if it does not yet exist. Checkpoints should not +// surpass `latest_blocks`. +fn chain_update( + mut tip: CheckPoint, + latest_blocks: &BTreeMap, + anchors: &BTreeSet<(A, Txid)>, +) -> Result { + for anchor in anchors { + let height = anchor.0.anchor_block().height; + + // Checkpoint uses the `BlockHash` from `latest_blocks` so that the hash will be consistent + // in case of a re-org. + if tip.get(height).is_none() && height <= tip.height() { + let hash = match latest_blocks.get(&height) { + Some(&hash) => hash, + None => anchor.0.anchor_block().hash, + }; + tip = tip.insert(BlockId { hash, height }); } } + Ok(tip) } diff --git a/crates/electrum/tests/test_electrum.rs b/crates/electrum/tests/test_electrum.rs index 1befb326c..825454331 100644 --- a/crates/electrum/tests/test_electrum.rs +++ b/crates/electrum/tests/test_electrum.rs @@ -1,15 +1,17 @@ use bdk_chain::{ - bitcoin::{hashes::Hash, Address, Amount, ScriptBuf, WScriptHash}, + bitcoin::{hashes::Hash, Address, Amount, ScriptBuf, Txid, WScriptHash}, local_chain::LocalChain, - spk_client::SyncRequest, - Balance, ConfirmationTimeHeightAnchor, IndexedTxGraph, SpkTxOutIndex, + spk_client::{FullScanRequest, SyncRequest}, + Balance, ConfirmationBlockTime, IndexedTxGraph, SpkTxOutIndex, }; use bdk_electrum::BdkElectrumClient; use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, TestEnv}; +use std::collections::{BTreeSet, HashSet}; +use std::str::FromStr; fn get_balance( recv_chain: &LocalChain, - recv_graph: &IndexedTxGraph>, + recv_graph: &IndexedTxGraph>, ) -> anyhow::Result { let chain_tip = recv_chain.tip().block_id(); let outpoints = recv_graph.index.outpoints().clone(); @@ -19,6 +21,222 @@ fn get_balance( Ok(balance) } +#[test] +pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> { + let env = TestEnv::new()?; + let electrum_client = electrum_client::Client::new(env.electrsd.electrum_url.as_str())?; + let client = BdkElectrumClient::new(electrum_client); + + let receive_address0 = + Address::from_str("bcrt1qc6fweuf4xjvz4x3gx3t9e0fh4hvqyu2qw4wvxm")?.assume_checked(); + let receive_address1 = + Address::from_str("bcrt1qfjg5lv3dvc9az8patec8fjddrs4aqtauadnagr")?.assume_checked(); + + let misc_spks = [ + receive_address0.script_pubkey(), + receive_address1.script_pubkey(), + ]; + + let _block_hashes = env.mine_blocks(101, None)?; + let txid1 = env.bitcoind.client.send_to_address( + &receive_address1, + Amount::from_sat(10000), + None, + None, + None, + None, + Some(1), + None, + )?; + let txid2 = env.bitcoind.client.send_to_address( + &receive_address0, + Amount::from_sat(20000), + None, + None, + None, + None, + Some(1), + None, + )?; + env.mine_blocks(1, None)?; + env.wait_until_electrum_sees_block()?; + + // use a full checkpoint linked list (since this is not what we are testing) + let cp_tip = env.make_checkpoint_tip(); + + let sync_update = { + let request = SyncRequest::from_chain_tip(cp_tip.clone()).set_spks(misc_spks); + client.sync(request, 1, true)? + }; + + assert!( + { + let update_cps = sync_update + .chain_update + .iter() + .map(|cp| cp.block_id()) + .collect::>(); + let superset_cps = cp_tip + .iter() + .map(|cp| cp.block_id()) + .collect::>(); + superset_cps.is_superset(&update_cps) + }, + "update should not alter original checkpoint tip since we already started with all checkpoints", + ); + + let graph_update = sync_update.graph_update; + // Check to see if we have the floating txouts available from our two created transactions' + // previous outputs in order to calculate transaction fees. + for tx in graph_update.full_txs() { + // Retrieve the calculated fee from `TxGraph`, which will panic if we do not have the + // floating txouts available from the transactions' previous outputs. + let fee = graph_update.calculate_fee(&tx.tx).expect("Fee must exist"); + + // Retrieve the fee in the transaction data from `bitcoind`. + let tx_fee = env + .bitcoind + .client + .get_transaction(&tx.txid, None) + .expect("Tx must exist") + .fee + .expect("Fee must exist") + .abs() + .to_unsigned() + .expect("valid `Amount`"); + + // Check that the calculated fee matches the fee from the transaction data. + assert_eq!(fee, tx_fee); + } + + let mut graph_update_txids: Vec = graph_update.full_txs().map(|tx| tx.txid).collect(); + graph_update_txids.sort(); + let mut expected_txids = vec![txid1, txid2]; + expected_txids.sort(); + assert_eq!(graph_update_txids, expected_txids); + + Ok(()) +} + +/// Test the bounds of the address scan depending on the `stop_gap`. +#[test] +pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> { + let env = TestEnv::new()?; + let electrum_client = electrum_client::Client::new(env.electrsd.electrum_url.as_str())?; + let client = BdkElectrumClient::new(electrum_client); + let _block_hashes = env.mine_blocks(101, None)?; + + // Now let's test the gap limit. First of all get a chain of 10 addresses. + let addresses = [ + "bcrt1qj9f7r8r3p2y0sqf4r3r62qysmkuh0fzep473d2ar7rcz64wqvhssjgf0z4", + "bcrt1qmm5t0ch7vh2hryx9ctq3mswexcugqe4atkpkl2tetm8merqkthas3w7q30", + "bcrt1qut9p7ej7l7lhyvekj28xknn8gnugtym4d5qvnp5shrsr4nksmfqsmyn87g", + "bcrt1qqz0xtn3m235p2k96f5wa2dqukg6shxn9n3txe8arlrhjh5p744hsd957ww", + "bcrt1q9c0t62a8l6wfytmf2t9lfj35avadk3mm8g4p3l84tp6rl66m48sqrme7wu", + "bcrt1qkmh8yrk2v47cklt8dytk8f3ammcwa4q7dzattedzfhqzvfwwgyzsg59zrh", + "bcrt1qvgrsrzy07gjkkfr5luplt0azxtfwmwq5t62gum5jr7zwcvep2acs8hhnp2", + "bcrt1qw57edarcg50ansq8mk3guyrk78rk0fwvrds5xvqeupteu848zayq549av8", + "bcrt1qvtve5ekf6e5kzs68knvnt2phfw6a0yjqrlgat392m6zt9jsvyxhqfx67ef", + "bcrt1qw03ddumfs9z0kcu76ln7jrjfdwam20qtffmkcral3qtza90sp9kqm787uk", + ]; + let addresses: Vec<_> = addresses + .into_iter() + .map(|s| Address::from_str(s).unwrap().assume_checked()) + .collect(); + let spks: Vec<_> = addresses + .iter() + .enumerate() + .map(|(i, addr)| (i as u32, addr.script_pubkey())) + .collect(); + + // Then receive coins on the 4th address. + let txid_4th_addr = env.bitcoind.client.send_to_address( + &addresses[3], + Amount::from_sat(10000), + None, + None, + None, + None, + Some(1), + None, + )?; + env.mine_blocks(1, None)?; + env.wait_until_electrum_sees_block()?; + + // use a full checkpoint linked list (since this is not what we are testing) + let cp_tip = env.make_checkpoint_tip(); + + // A scan with a stop_gap of 3 won't find the transaction, but a scan with a gap limit of 4 + // will. + let full_scan_update = { + let request = + FullScanRequest::from_chain_tip(cp_tip.clone()).set_spks_for_keychain(0, spks.clone()); + client.full_scan(request, 3, 1, false)? + }; + assert!(full_scan_update.graph_update.full_txs().next().is_none()); + assert!(full_scan_update.last_active_indices.is_empty()); + let full_scan_update = { + let request = + FullScanRequest::from_chain_tip(cp_tip.clone()).set_spks_for_keychain(0, spks.clone()); + client.full_scan(request, 4, 1, false)? + }; + assert_eq!( + full_scan_update + .graph_update + .full_txs() + .next() + .unwrap() + .txid, + txid_4th_addr + ); + assert_eq!(full_scan_update.last_active_indices[&0], 3); + + // Now receive a coin on the last address. + let txid_last_addr = env.bitcoind.client.send_to_address( + &addresses[addresses.len() - 1], + Amount::from_sat(10000), + None, + None, + None, + None, + Some(1), + None, + )?; + env.mine_blocks(1, None)?; + env.wait_until_electrum_sees_block()?; + + // A scan with gap limit 5 won't find the second transaction, but a scan with gap limit 6 will. + // The last active indice won't be updated in the first case but will in the second one. + let full_scan_update = { + let request = + FullScanRequest::from_chain_tip(cp_tip.clone()).set_spks_for_keychain(0, spks.clone()); + client.full_scan(request, 5, 1, false)? + }; + let txs: HashSet<_> = full_scan_update + .graph_update + .full_txs() + .map(|tx| tx.txid) + .collect(); + assert_eq!(txs.len(), 1); + assert!(txs.contains(&txid_4th_addr)); + assert_eq!(full_scan_update.last_active_indices[&0], 3); + let full_scan_update = { + let request = + FullScanRequest::from_chain_tip(cp_tip.clone()).set_spks_for_keychain(0, spks.clone()); + client.full_scan(request, 6, 1, false)? + }; + let txs: HashSet<_> = full_scan_update + .graph_update + .full_txs() + .map(|tx| tx.txid) + .collect(); + assert_eq!(txs.len(), 2); + assert!(txs.contains(&txid_4th_addr) && txs.contains(&txid_last_addr)); + assert_eq!(full_scan_update.last_active_indices[&0], 9); + + Ok(()) +} + /// Ensure that [`ElectrumExt`] can sync properly. /// /// 1. Mine 101 blocks. @@ -44,7 +262,7 @@ fn scan_detects_confirmed_tx() -> anyhow::Result<()> { // Setup receiver. let (mut recv_chain, _) = LocalChain::from_genesis_hash(env.bitcoind.client.get_block_hash(0)?); - let mut recv_graph = IndexedTxGraph::::new({ + let mut recv_graph = IndexedTxGraph::::new({ let mut recv_index = SpkTxOutIndex::default(); recv_index.insert_spk((), spk_to_track.clone()); recv_index @@ -61,14 +279,11 @@ fn scan_detects_confirmed_tx() -> anyhow::Result<()> { // Sync up to tip. env.wait_until_electrum_sees_block()?; - let update = client - .sync( - SyncRequest::from_chain_tip(recv_chain.tip()) - .chain_spks(core::iter::once(spk_to_track)), - 5, - true, - )? - .with_confirmation_time_height_anchor(&client)?; + let update = client.sync( + SyncRequest::from_chain_tip(recv_chain.tip()).chain_spks(core::iter::once(spk_to_track)), + 5, + true, + )?; let _ = recv_chain .apply_update(update.chain_update) @@ -137,7 +352,7 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> { // Setup receiver. let (mut recv_chain, _) = LocalChain::from_genesis_hash(env.bitcoind.client.get_block_hash(0)?); - let mut recv_graph = IndexedTxGraph::::new({ + let mut recv_graph = IndexedTxGraph::::new({ let mut recv_index = SpkTxOutIndex::default(); recv_index.insert_spk((), spk_to_track.clone()); recv_index @@ -147,20 +362,20 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> { env.mine_blocks(101, Some(addr_to_mine))?; // Create transactions that are tracked by our receiver. + let mut txids = vec![]; + let mut hashes = vec![]; for _ in 0..REORG_COUNT { - env.send(&addr_to_track, SEND_AMOUNT)?; - env.mine_blocks(1, None)?; + txids.push(env.send(&addr_to_track, SEND_AMOUNT)?); + hashes.extend(env.mine_blocks(1, None)?); } // Sync up to tip. env.wait_until_electrum_sees_block()?; - let update = client - .sync( - SyncRequest::from_chain_tip(recv_chain.tip()).chain_spks([spk_to_track.clone()]), - 5, - false, - )? - .with_confirmation_time_height_anchor(&client)?; + let update = client.sync( + SyncRequest::from_chain_tip(recv_chain.tip()).chain_spks([spk_to_track.clone()]), + 5, + false, + )?; let _ = recv_chain .apply_update(update.chain_update) @@ -169,6 +384,13 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> { // Retain a snapshot of all anchors before reorg process. let initial_anchors = update.graph_update.all_anchors(); + let anchors: Vec<_> = initial_anchors.iter().cloned().collect(); + assert_eq!(anchors.len(), REORG_COUNT); + for i in 0..REORG_COUNT { + let (anchor, txid) = anchors[i]; + assert_eq!(anchor.block_id.hash, hashes[i]); + assert_eq!(txid, txids[i]); + } // Check if initial balance is correct. assert_eq!( @@ -185,13 +407,11 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> { env.reorg_empty_blocks(depth)?; env.wait_until_electrum_sees_block()?; - let update = client - .sync( - SyncRequest::from_chain_tip(recv_chain.tip()).chain_spks([spk_to_track.clone()]), - 5, - false, - )? - .with_confirmation_time_height_anchor(&client)?; + let update = client.sync( + SyncRequest::from_chain_tip(recv_chain.tip()).chain_spks([spk_to_track.clone()]), + 5, + false, + )?; let _ = recv_chain .apply_update(update.chain_update) diff --git a/crates/esplora/src/async_ext.rs b/crates/esplora/src/async_ext.rs index 939c43a2f..70895a43a 100644 --- a/crates/esplora/src/async_ext.rs +++ b/crates/esplora/src/async_ext.rs @@ -6,7 +6,7 @@ use bdk_chain::{ bitcoin::{BlockHash, OutPoint, ScriptBuf, TxOut, Txid}, collections::BTreeMap, local_chain::CheckPoint, - BlockId, ConfirmationTimeHeightAnchor, TxGraph, + BlockId, ConfirmationBlockTime, TxGraph, }; use bdk_chain::{Anchor, Indexed}; use esplora_client::{Amount, TxStatus}; @@ -240,10 +240,10 @@ async fn full_scan_for_index_and_graph( >, stop_gap: usize, parallel_requests: usize, -) -> Result<(TxGraph, BTreeMap), Error> { +) -> Result<(TxGraph, BTreeMap), Error> { type TxsOfSpkIndex = (u32, Vec); let parallel_requests = Ord::max(parallel_requests, 1); - let mut graph = TxGraph::::default(); + let mut graph = TxGraph::::default(); let mut last_active_indexes = BTreeMap::::new(); for (keychain, spks) in keychain_spks { @@ -333,7 +333,7 @@ async fn sync_for_index_and_graph( txids: impl IntoIterator + Send> + Send, outpoints: impl IntoIterator + Send> + Send, parallel_requests: usize, -) -> Result, Error> { +) -> Result, Error> { let mut graph = full_scan_for_index_and_graph( client, [( diff --git a/crates/esplora/src/blocking_ext.rs b/crates/esplora/src/blocking_ext.rs index adad25c2e..dc95a350b 100644 --- a/crates/esplora/src/blocking_ext.rs +++ b/crates/esplora/src/blocking_ext.rs @@ -6,7 +6,7 @@ use bdk_chain::spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncRe use bdk_chain::{ bitcoin::{Amount, BlockHash, OutPoint, ScriptBuf, TxOut, Txid}, local_chain::CheckPoint, - BlockId, ConfirmationTimeHeightAnchor, TxGraph, + BlockId, ConfirmationBlockTime, TxGraph, }; use bdk_chain::{Anchor, Indexed}; use esplora_client::TxStatus; @@ -219,10 +219,10 @@ fn full_scan_for_index_and_graph_blocking( keychain_spks: BTreeMap>>, stop_gap: usize, parallel_requests: usize, -) -> Result<(TxGraph, BTreeMap), Error> { +) -> Result<(TxGraph, BTreeMap), Error> { type TxsOfSpkIndex = (u32, Vec); let parallel_requests = Ord::max(parallel_requests, 1); - let mut tx_graph = TxGraph::::default(); + let mut tx_graph = TxGraph::::default(); let mut last_active_indices = BTreeMap::::new(); for (keychain, spks) in keychain_spks { @@ -315,7 +315,7 @@ fn sync_for_index_and_graph_blocking( txids: impl IntoIterator, outpoints: impl IntoIterator, parallel_requests: usize, -) -> Result, Error> { +) -> Result, Error> { let (mut tx_graph, _) = full_scan_for_index_and_graph_blocking( client, { diff --git a/crates/esplora/src/lib.rs b/crates/esplora/src/lib.rs index 535167ff2..718d3cf9c 100644 --- a/crates/esplora/src/lib.rs +++ b/crates/esplora/src/lib.rs @@ -16,7 +16,7 @@ //! [`TxGraph`]: bdk_chain::tx_graph::TxGraph //! [`example_esplora`]: https://github.com/bitcoindevkit/bdk/tree/master/example-crates/example_esplora -use bdk_chain::{BlockId, ConfirmationTimeHeightAnchor}; +use bdk_chain::{BlockId, ConfirmationBlockTime}; use esplora_client::TxStatus; pub use esplora_client; @@ -31,7 +31,7 @@ mod async_ext; #[cfg(feature = "async")] pub use async_ext::*; -fn anchor_from_status(status: &TxStatus) -> Option { +fn anchor_from_status(status: &TxStatus) -> Option { if let TxStatus { block_height: Some(height), block_hash: Some(hash), @@ -39,9 +39,8 @@ fn anchor_from_status(status: &TxStatus) -> Option .. } = status.clone() { - Some(ConfirmationTimeHeightAnchor { - anchor_block: BlockId { height, hash }, - confirmation_height: height, + Some(ConfirmationBlockTime { + block_id: BlockId { height, hash }, confirmation_time: time, }) } else { diff --git a/crates/sqlite/src/store.rs b/crates/sqlite/src/store.rs index 5a5468167..5b7992518 100644 --- a/crates/sqlite/src/store.rs +++ b/crates/sqlite/src/store.rs @@ -547,10 +547,7 @@ mod test { use bdk_chain::bitcoin::{secp256k1, BlockHash, OutPoint}; use bdk_chain::miniscript::Descriptor; use bdk_chain::CombinedChangeSet; - use bdk_chain::{ - indexed_tx_graph, tx_graph, BlockId, ConfirmationHeightAnchor, - ConfirmationTimeHeightAnchor, DescriptorExt, - }; + use bdk_chain::{indexed_tx_graph, tx_graph, BlockId, ConfirmationBlockTime, DescriptorExt}; use std::str::FromStr; use std::sync::Arc; @@ -561,37 +558,15 @@ mod test { } #[test] - fn insert_and_load_aggregate_changesets_with_confirmation_time_height_anchor() { + fn insert_and_load_aggregate_changesets_with_confirmation_block_time_anchor() { let (test_changesets, agg_test_changesets) = - create_test_changesets(&|height, time, hash| ConfirmationTimeHeightAnchor { - confirmation_height: height, + create_test_changesets(&|height, time, hash| ConfirmationBlockTime { confirmation_time: time, - anchor_block: (height, hash).into(), + block_id: (height, hash).into(), }); let conn = Connection::open_in_memory().expect("in memory connection"); - let mut store = Store::::new(conn) - .expect("create new memory db store"); - - test_changesets.iter().for_each(|changeset| { - store.write(changeset).expect("write changeset"); - }); - - let agg_changeset = store.read().expect("aggregated changeset"); - - assert_eq!(agg_changeset, Some(agg_test_changesets)); - } - - #[test] - fn insert_and_load_aggregate_changesets_with_confirmation_height_anchor() { - let (test_changesets, agg_test_changesets) = - create_test_changesets(&|height, _time, hash| ConfirmationHeightAnchor { - confirmation_height: height, - anchor_block: (height, hash).into(), - }); - - let conn = Connection::open_in_memory().expect("in memory connection"); - let mut store = Store::::new(conn) + let mut store = Store::::new(conn) .expect("create new memory db store"); test_changesets.iter().for_each(|changeset| { diff --git a/crates/wallet/src/wallet/export.rs b/crates/wallet/src/wallet/export.rs index 37f7fac0e..4b49db144 100644 --- a/crates/wallet/src/wallet/export.rs +++ b/crates/wallet/src/wallet/export.rs @@ -128,7 +128,7 @@ impl FullyNodedExport { let blockheight = if include_blockheight { wallet.transactions().next().map_or(0, |canonical_tx| { match canonical_tx.chain_position { - bdk_chain::ChainPosition::Confirmed(a) => a.confirmation_height, + bdk_chain::ChainPosition::Confirmed(a) => a.block_id.height, bdk_chain::ChainPosition::Unconfirmed(_) => 0, } }) @@ -214,7 +214,7 @@ mod test { use core::str::FromStr; use crate::std::string::ToString; - use bdk_chain::{BlockId, ConfirmationTimeHeightAnchor}; + use bdk_chain::{BlockId, ConfirmationBlockTime}; use bitcoin::hashes::Hash; use bitcoin::{transaction, BlockHash, Network, Transaction}; @@ -233,15 +233,20 @@ mod test { }; let txid = transaction.compute_txid(); let block_id = BlockId { - height: 5001, + height: 5000, hash: BlockHash::all_zeros(), }; wallet.insert_checkpoint(block_id).unwrap(); + wallet + .insert_checkpoint(BlockId { + height: 5001, + hash: BlockHash::all_zeros(), + }) + .unwrap(); wallet.insert_tx(transaction); - let anchor = ConfirmationTimeHeightAnchor { - confirmation_height: 5000, + let anchor = ConfirmationBlockTime { confirmation_time: 0, - anchor_block: block_id, + block_id, }; let mut graph = TxGraph::default(); let _ = graph.insert_anchor(txid, anchor); diff --git a/crates/wallet/src/wallet/mod.rs b/crates/wallet/src/wallet/mod.rs index 8c049b0e0..9db21ac71 100644 --- a/crates/wallet/src/wallet/mod.rs +++ b/crates/wallet/src/wallet/mod.rs @@ -28,7 +28,7 @@ use bdk_chain::{ }, spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult}, tx_graph::{CanonicalTx, TxGraph, TxNode}, - BlockId, ChainPosition, ConfirmationTime, ConfirmationTimeHeightAnchor, FullTxOut, Indexed, + BlockId, ChainPosition, ConfirmationBlockTime, ConfirmationTime, FullTxOut, Indexed, IndexedTxGraph, Merge, }; use bitcoin::sighash::{EcdsaSighashType, TapSighashType}; @@ -104,7 +104,7 @@ pub struct Wallet { signers: Arc, change_signers: Arc, chain: LocalChain, - indexed_graph: IndexedTxGraph>, + indexed_graph: IndexedTxGraph>, stage: ChangeSet, network: Network, secp: SecpCtx, @@ -120,7 +120,7 @@ pub struct Update { pub last_active_indices: BTreeMap, /// Update for the wallet's internal [`TxGraph`]. - pub graph: TxGraph, + pub graph: TxGraph, /// Update for the wallet's internal [`LocalChain`]. /// @@ -149,7 +149,7 @@ impl From for Update { } /// The changes made to a wallet by applying an [`Update`]. -pub type ChangeSet = bdk_chain::CombinedChangeSet; +pub type ChangeSet = bdk_chain::CombinedChangeSet; /// A derived address and the index it was found at. /// For convenience this automatically derefs to `Address` @@ -1007,7 +1007,7 @@ impl Wallet { /// match canonical_tx.chain_position { /// ChainPosition::Confirmed(anchor) => println!( /// "tx is confirmed at height {}, we know this since {}:{} is in the best chain", - /// anchor.confirmation_height, anchor.anchor_block.height, anchor.anchor_block.hash, + /// anchor.block_id.height, anchor.block_id.height, anchor.block_id.hash, /// ), /// ChainPosition::Unconfirmed(last_seen) => println!( /// "tx is last seen at {}, it is unconfirmed as it is not anchored in the best chain", @@ -1020,7 +1020,7 @@ impl Wallet { pub fn get_tx( &self, txid: Txid, - ) -> Option, ConfirmationTimeHeightAnchor>> { + ) -> Option, ConfirmationBlockTime>> { let graph = self.indexed_graph.graph(); Some(CanonicalTx { @@ -1076,8 +1076,7 @@ impl Wallet { /// Iterate over the transactions in the wallet. pub fn transactions( &self, - ) -> impl Iterator, ConfirmationTimeHeightAnchor>> + '_ - { + ) -> impl Iterator, ConfirmationBlockTime>> + '_ { self.indexed_graph .graph() .list_canonical_txs(&self.chain, self.chain.tip().block_id()) @@ -1807,7 +1806,7 @@ impl Wallet { .graph() .get_chain_position(&self.chain, chain_tip, input.previous_output.txid) .map(|chain_position| match chain_position { - ChainPosition::Confirmed(a) => a.confirmation_height, + ChainPosition::Confirmed(a) => a.block_id.height, ChainPosition::Unconfirmed(_) => u32::MAX, }); let current_height = sign_options @@ -2245,7 +2244,7 @@ impl Wallet { } /// Get a reference to the inner [`TxGraph`]. - pub fn tx_graph(&self) -> &TxGraph { + pub fn tx_graph(&self) -> &TxGraph { self.indexed_graph.graph() } @@ -2253,7 +2252,7 @@ impl Wallet { /// because they haven't been broadcast. pub fn unbroadcast_transactions( &self, - ) -> impl Iterator, ConfirmationTimeHeightAnchor>> { + ) -> impl Iterator, ConfirmationBlockTime>> { self.tx_graph().txs_with_no_anchor_or_last_seen() } @@ -2373,8 +2372,8 @@ impl Wallet { } } -impl AsRef> for Wallet { - fn as_ref(&self) -> &bdk_chain::tx_graph::TxGraph { +impl AsRef> for Wallet { + fn as_ref(&self) -> &bdk_chain::tx_graph::TxGraph { self.indexed_graph.graph() } } @@ -2413,7 +2412,7 @@ where fn new_local_utxo( keychain: KeychainKind, derivation_index: u32, - full_txo: FullTxOut, + full_txo: FullTxOut, ) -> LocalOutput { LocalOutput { outpoint: full_txo.outpoint, @@ -2476,7 +2475,7 @@ macro_rules! floating_rate { macro_rules! doctest_wallet { () => {{ use $crate::bitcoin::{BlockHash, Transaction, absolute, TxOut, Network, hashes::Hash}; - use $crate::chain::{ConfirmationTimeHeightAnchor, BlockId, TxGraph}; + use $crate::chain::{ConfirmationBlockTime, BlockId, TxGraph}; use $crate::wallet::{Update, Wallet}; use $crate::KeychainKind; let descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/0/*)"; @@ -2499,13 +2498,13 @@ macro_rules! doctest_wallet { }], }; let txid = tx.txid(); - let block = BlockId { height: 1_000, hash: BlockHash::all_zeros() }; - let _ = wallet.insert_checkpoint(block); + let block_id = BlockId { height: 500, hash: BlockHash::all_zeros() }; + let _ = wallet.insert_checkpoint(block_id); + let _ = wallet.insert_checkpoint(BlockId { height: 1_000, hash: BlockHash::all_zeros() }); let _ = wallet.insert_tx(tx); - let anchor = ConfirmationTimeHeightAnchor { - confirmation_height: 500, + let anchor = ConfirmationBlockTime { confirmation_time: 50_000, - anchor_block: block, + block_id, }; let mut graph = TxGraph::default(); let _ = graph.insert_anchor(txid, anchor); diff --git a/crates/wallet/tests/common.rs b/crates/wallet/tests/common.rs index c124ba213..9774ec985 100644 --- a/crates/wallet/tests/common.rs +++ b/crates/wallet/tests/common.rs @@ -1,5 +1,5 @@ #![allow(unused)] -use bdk_chain::{BlockId, ConfirmationTime, ConfirmationTimeHeightAnchor, TxGraph}; +use bdk_chain::{BlockId, ConfirmationBlockTime, ConfirmationTime, TxGraph}; use bdk_wallet::{ wallet::{Update, Wallet}, KeychainKind, LocalOutput, @@ -65,6 +65,12 @@ pub fn get_funded_wallet_with_change(descriptor: &str, change: &str) -> (Wallet, ], }; + wallet + .insert_checkpoint(BlockId { + height: 42, + hash: BlockHash::all_zeros(), + }) + .unwrap(); wallet .insert_checkpoint(BlockId { height: 1_000, @@ -205,9 +211,8 @@ pub fn insert_anchor_from_conf(wallet: &mut Wallet, txid: Txid, position: Confir .local_chain() .range(height..) .last() - .map(|anchor_cp| ConfirmationTimeHeightAnchor { - anchor_block: anchor_cp.block_id(), - confirmation_height: height, + .map(|anchor_cp| ConfirmationBlockTime { + block_id: anchor_cp.block_id(), confirmation_time: time, }) .expect("confirmation height cannot be greater than tip"); diff --git a/example-crates/example_bitcoind_rpc_polling/src/main.rs b/example-crates/example_bitcoind_rpc_polling/src/main.rs index 1749d2f3e..c71b18fed 100644 --- a/example-crates/example_bitcoind_rpc_polling/src/main.rs +++ b/example-crates/example_bitcoind_rpc_polling/src/main.rs @@ -16,7 +16,7 @@ use bdk_chain::{ indexed_tx_graph, indexer::keychain_txout, local_chain::{self, LocalChain}, - ConfirmationTimeHeightAnchor, IndexedTxGraph, Merge, + ConfirmationBlockTime, IndexedTxGraph, Merge, }; use example_cli::{ anyhow, @@ -38,7 +38,7 @@ const DB_COMMIT_DELAY: Duration = Duration::from_secs(60); type ChangeSet = ( local_chain::ChangeSet, - indexed_tx_graph::ChangeSet>, + indexed_tx_graph::ChangeSet>, ); #[derive(Debug)] diff --git a/example-crates/example_electrum/src/main.rs b/example-crates/example_electrum/src/main.rs index 1e93bf37d..31e8e7041 100644 --- a/example-crates/example_electrum/src/main.rs +++ b/example-crates/example_electrum/src/main.rs @@ -10,7 +10,7 @@ use bdk_chain::{ indexer::keychain_txout, local_chain::{self, LocalChain}, spk_client::{FullScanRequest, SyncRequest}, - ConfirmationHeightAnchor, Merge, + ConfirmationBlockTime, Merge, }; use bdk_electrum::{ electrum_client::{self, Client, ElectrumApi}, @@ -100,7 +100,7 @@ pub struct ScanOptions { type ChangeSet = ( local_chain::ChangeSet, - indexed_tx_graph::ChangeSet>, + indexed_tx_graph::ChangeSet>, ); fn main() -> anyhow::Result<()> { @@ -193,8 +193,7 @@ fn main() -> anyhow::Result<()> { let res = client .full_scan::<_>(request, stop_gap, scan_options.batch_size, false) - .context("scanning the blockchain")? - .with_confirmation_height_anchor(); + .context("scanning the blockchain")?; ( res.chain_update, res.graph_update, @@ -317,8 +316,7 @@ fn main() -> anyhow::Result<()> { let res = client .sync(request, scan_options.batch_size, false) - .context("scanning the blockchain")? - .with_confirmation_height_anchor(); + .context("scanning the blockchain")?; // drop lock on graph and chain drop((graph, chain)); @@ -340,7 +338,7 @@ fn main() -> anyhow::Result<()> { let chain_changeset = chain.apply_update(chain_update)?; let mut indexed_tx_graph_changeset = - indexed_tx_graph::ChangeSet::::default(); + indexed_tx_graph::ChangeSet::::default(); if let Some(keychain_update) = keychain_update { let keychain_changeset = graph.index.reveal_to_target_multi(&keychain_update); indexed_tx_graph_changeset.merge(keychain_changeset.into()); diff --git a/example-crates/example_esplora/src/main.rs b/example-crates/example_esplora/src/main.rs index 8f69efb28..ffa2ea24e 100644 --- a/example-crates/example_esplora/src/main.rs +++ b/example-crates/example_esplora/src/main.rs @@ -10,7 +10,7 @@ use bdk_chain::{ indexer::keychain_txout, local_chain::{self, LocalChain}, spk_client::{FullScanRequest, SyncRequest}, - ConfirmationTimeHeightAnchor, Merge, + ConfirmationBlockTime, Merge, }; use bdk_esplora::{esplora_client, EsploraExt}; @@ -26,7 +26,7 @@ const DB_PATH: &str = ".bdk_esplora_example.db"; type ChangeSet = ( local_chain::ChangeSet, - indexed_tx_graph::ChangeSet>, + indexed_tx_graph::ChangeSet>, ); #[derive(Subcommand, Debug, Clone)] diff --git a/example-crates/wallet_electrum/src/main.rs b/example-crates/wallet_electrum/src/main.rs index 2adf090aa..bda0e91cd 100644 --- a/example-crates/wallet_electrum/src/main.rs +++ b/example-crates/wallet_electrum/src/main.rs @@ -63,9 +63,7 @@ fn main() -> Result<(), anyhow::Error> { }) .inspect_spks_for_all_keychains(|_, _, _| std::io::stdout().flush().expect("must flush")); - let mut update = client - .full_scan(request, STOP_GAP, BATCH_SIZE, false)? - .with_confirmation_time_height_anchor(&client)?; + let mut update = client.full_scan(request, STOP_GAP, BATCH_SIZE, false)?; let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs(); let _ = update.graph_update.update_last_seen_unconfirmed(now);