diff --git a/crates/electrum/src/bdk_electrum_client.rs b/crates/electrum/src/bdk_electrum_client.rs index 80101f1673..3cf47bca57 100644 --- a/crates/electrum/src/bdk_electrum_client.rs +++ b/crates/electrum/src/bdk_electrum_client.rs @@ -88,87 +88,59 @@ impl BdkElectrumClient { stop_gap: usize, batch_size: usize, fetch_prev_txouts: bool, - ) -> Result, Error> { + ) -> 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). + // TODO: Refactor `scanned_spks` because we no longer run `full_scan` multiple times for reorgs. 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)), - ); - } - } - - // 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 (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::>>(); - // Fetch previous `TxOut`s for fee calculation if flag is enabled. - if fetch_prev_txouts { - self.fetch_prev_txout(&mut graph_update)?; + if !request_spks.is_empty() { + 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)), + ); } + } - let chain_update = tip; + // Fetch previous `TxOut`s for fee calculation if flag is enabled. + if fetch_prev_txouts { + self.fetch_prev_txout(&mut graph_update)?; + } - 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::>(); + let chain_update = tip; - break FullScanResult { - graph_update, - chain_update, - last_active_indices: keychain_update, - }; - }; + let last_active_indices = 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::>(); - 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,12 +162,10 @@ 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, _) = construct_update_tip(&self.inner, request.chain_tip)?; let cps = tip @@ -212,10 +182,10 @@ impl BdkElectrumClient { self.fetch_prev_txout(&mut full_scan_res.graph_update)?; } - Ok(ElectrumSyncResult(SyncResult { + Ok(SyncResult { chain_update: full_scan_res.chain_update, graph_update: full_scan_res.graph_update, - })) + }) } /// Populate the `graph_update` with transactions/anchors associated with the given `spks`. @@ -228,7 +198,7 @@ impl BdkElectrumClient { fn populate_with_spks( &self, cps: &BTreeMap>, - graph_update: &mut TxGraph, + graph_update: &mut TxGraph, spks: &mut impl Iterator, stop_gap: usize, batch_size: usize, @@ -263,34 +233,17 @@ impl BdkElectrumClient { 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( + cps, + 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 @@ -300,7 +253,7 @@ impl BdkElectrumClient { 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 +277,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(cps, graph_update, res.tx_hash, res.height)?; } if !has_spending && res.tx_hash != op_txid { @@ -340,9 +291,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(cps, graph_update, res.tx_hash, res.height)?; } } } @@ -353,7 +302,7 @@ impl BdkElectrumClient { 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,83 +320,76 @@ 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(cps, 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); - -impl ElectrumSyncResult { - /// Return [`SyncResult`] with [`ConfirmationHeightAnchor`]. - pub fn with_confirmation_height_anchor(self) -> SyncResult { - self.0 + // 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, + cps: &BTreeMap>, + 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 header = self.inner.block_header(merkle_res.block_height)?; + let is_confirmed_tx = electrum_client::utils::validate_merkle_proof( + &txid, + &header.merkle_root, + &merkle_res, + ); + + if is_confirmed_tx { + if let Some(anchor) = determine_tx_anchor( + cps, + merkle_res.block_height as i32, + header.time as u64, + txid, + ) { + let _ = graph_update.insert_anchor(txid, anchor); + } + } + } + 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( +fn _try_into_confirmation_time_result( graph_update: TxGraph, client: &impl ElectrumApi, ) -> Result, Error> { @@ -545,7 +487,7 @@ fn construct_update_tip( } /// 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. +/// these concatenations into a [`ConfirmationTimeHeightAnchor`] 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. @@ -554,8 +496,9 @@ fn construct_update_tip( fn determine_tx_anchor( cps: &BTreeMap>, raw_height: i32, + confirmation_time: u64, txid: Txid, -) -> Option { +) -> 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 @@ -565,9 +508,10 @@ fn determine_tx_anchor( .expect("must deserialize genesis coinbase txid") { let anchor_block = cps.values().next()?.block_id(); - return Some(ConfirmationHeightAnchor { - anchor_block, + return Some(ConfirmationTimeHeightAnchor { confirmation_height: 0, + confirmation_time: 1231006505, + anchor_block, }); } match raw_height { @@ -581,9 +525,10 @@ fn determine_tx_anchor( if h > anchor_block.height { None } else { - Some(ConfirmationHeightAnchor { - anchor_block, + Some(ConfirmationTimeHeightAnchor { confirmation_height: h, + confirmation_time, + anchor_block, }) } } diff --git a/crates/electrum/tests/test_electrum.rs b/crates/electrum/tests/test_electrum.rs index c105ecca25..11582353c0 100644 --- a/crates/electrum/tests/test_electrum.rs +++ b/crates/electrum/tests/test_electrum.rs @@ -62,14 +62,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) @@ -155,13 +152,11 @@ fn tx_can_become_unconfirmed_after_reorg() -> 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([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) @@ -186,13 +181,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/example-crates/example_electrum/src/main.rs b/example-crates/example_electrum/src/main.rs index 8467d2699a..eee406399f 100644 --- a/example-crates/example_electrum/src/main.rs +++ b/example-crates/example_electrum/src/main.rs @@ -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, @@ -313,8 +312,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)); @@ -341,7 +339,12 @@ fn main() -> anyhow::Result<()> { let (_, keychain_changeset) = graph.index.reveal_to_target_multi(&keychain_update); indexed_tx_graph_changeset.append(keychain_changeset.into()); } - indexed_tx_graph_changeset.append(graph.apply_update(graph_update)); + indexed_tx_graph_changeset.append(graph.apply_update(graph_update.map_anchors(|a| { + ConfirmationHeightAnchor { + confirmation_height: a.confirmation_height, + anchor_block: a.anchor_block, + } + }))); (chain_changeset, indexed_tx_graph_changeset) }; diff --git a/example-crates/wallet_electrum/src/main.rs b/example-crates/wallet_electrum/src/main.rs index e6c01c20be..75d13f1a8c 100644 --- a/example-crates/wallet_electrum/src/main.rs +++ b/example-crates/wallet_electrum/src/main.rs @@ -57,9 +57,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);