]> Untitled Git - bdk/commitdiff
feat(electrum): batch `transaction.get_merkle` calls via `batch_call`
authorWei Chen <wzc110@gmail.com>
Tue, 27 May 2025 18:15:44 +0000 (18:15 +0000)
committerWei Chen <wzc110@gmail.com>
Fri, 27 Jun 2025 19:40:00 +0000 (19:40 +0000)
crates/electrum/Cargo.toml
crates/electrum/src/bdk_electrum_client.rs

index 95982eefa4f05f7477644766ba06c11e6678606c..c09bfcbacdbc924cc723b8e3a5dd07211fa8f53e 100644 (file)
@@ -15,6 +15,7 @@ workspace = true
 [dependencies]
 bdk_core = { path = "../core", version = "0.6.0" }
 electrum-client = { version = "0.23.1", features = [ "proxy" ], default-features = false }
+serde_json = "1.0"
 
 [dev-dependencies]
 bdk_testenv = { path = "../testenv" }
index d68f62e1b08320fe00575a407d15bec7182b7dee..dde9efebd180f95502d8fa87fc1d11afe7ffd6aa 100644 (file)
@@ -12,9 +12,6 @@ use std::sync::{Arc, Mutex};
 /// We include a chain suffix of a certain length for the purpose of robustness.
 const CHAIN_SUFFIX_LENGTH: u32 = 8;
 
-/// Maximum batch size for proof validation requests
-const MAX_BATCH_SIZE: usize = 100;
-
 /// Wrapper around an [`electrum_client::ElectrumApi`] which includes an internal in-memory
 /// transaction cache to avoid re-fetching already downloaded transactions.
 #[derive(Debug)]
@@ -262,15 +259,15 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
         batch_size: usize,
         pending_anchors: &mut Vec<(Txid, usize)>,
     ) -> Result<Option<u32>, Error> {
-        let mut unused_spk_count = 0;
-        let mut last_active_index = None;
+        let mut unused_spk_count = 0_usize;
+        let mut last_active_index = Option::<u32>::None;
 
-        'batch_loop: loop {
+        loop {
             let spks = (0..batch_size)
                 .map_while(|_| spks_with_expected_txids.next())
                 .collect::<Vec<_>>();
             if spks.is_empty() {
-                break;
+                return Ok(last_active_index);
             }
 
             let spk_histories = self
@@ -279,10 +276,10 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
 
             for ((spk_index, spk), spk_history) in spks.into_iter().zip(spk_histories) {
                 if spk_history.is_empty() {
-                    unused_spk_count += 1;
-                    if unused_spk_count >= stop_gap {
-                        break 'batch_loop;
-                    }
+                    match unused_spk_count.checked_add(1) {
+                        Some(i) if i < stop_gap => unused_spk_count = i,
+                        _ => return Ok(last_active_index),
+                    };
                 } else {
                     last_active_index = Some(spk_index);
                     unused_spk_count = 0;
@@ -509,72 +506,65 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
             }
         }
 
-        // Fetch missing proofs in batches
-        for chunk in to_fetch.chunks(MAX_BATCH_SIZE) {
-            for &(txid, height, hash) in chunk {
-                // Fetch the raw proof.
-                let proof = self.inner.transaction_get_merkle(&txid, height)?;
-
-                // Validate against header, retrying once on stale header.
-                let mut header = {
-                    let cache = self.block_header_cache.lock().unwrap();
-                    cache[&(height as u32)]
-                };
-                let mut valid = electrum_client::utils::validate_merkle_proof(
+        // Batch all get_merkle calls.
+        let mut batch = electrum_client::Batch::default();
+        for &(txid, height, _) in &to_fetch {
+            batch.raw(
+                "blockchain.transaction.get_merkle".into(),
+                vec![
+                    electrum_client::Param::String(format!("{:x}", txid)),
+                    electrum_client::Param::Usize(height),
+                ],
+            );
+        }
+        let resps = self.inner.batch_call(&batch)?;
+
+        // Validate each proof, retrying once for each stale header.
+        for ((txid, height, hash), resp) in to_fetch.into_iter().zip(resps.into_iter()) {
+            let proof: electrum_client::GetMerkleRes = serde_json::from_value(resp)?;
+
+            let mut header = {
+                let cache = self.block_header_cache.lock().unwrap();
+                cache
+                    .get(&(height as u32))
+                    .copied()
+                    .expect("header already fetched above")
+            };
+            let mut valid =
+                electrum_client::utils::validate_merkle_proof(&txid, &header.merkle_root, &proof);
+            if !valid {
+                header = self.inner.block_header(height)?;
+                self.block_header_cache
+                    .lock()
+                    .unwrap()
+                    .insert(height as u32, header);
+                valid = electrum_client::utils::validate_merkle_proof(
                     &txid,
                     &header.merkle_root,
                     &proof,
                 );
-                if !valid {
-                    let new_header = self.inner.block_header(height)?;
-                    self.block_header_cache
-                        .lock()
-                        .unwrap()
-                        .insert(height as u32, new_header);
-                    header = new_header;
-                    valid = electrum_client::utils::validate_merkle_proof(
-                        &txid,
-                        &header.merkle_root,
-                        &proof,
-                    );
-                }
+            }
 
-                // Build and cache the anchor if merkle proof is valid.
-                if valid {
-                    let anchor = ConfirmationBlockTime {
-                        confirmation_time: header.time as u64,
-                        block_id: BlockId {
-                            height: height as u32,
-                            hash,
-                        },
-                    };
-                    self.anchor_cache
-                        .lock()
-                        .unwrap()
-                        .insert((txid, hash), anchor);
-                    results.push((txid, anchor));
-                }
+            // Build and cache the anchor if merkle proof is valid.
+            if valid {
+                let anchor = ConfirmationBlockTime {
+                    confirmation_time: header.time as u64,
+                    block_id: BlockId {
+                        height: height as u32,
+                        hash,
+                    },
+                };
+                self.anchor_cache
+                    .lock()
+                    .unwrap()
+                    .insert((txid, hash), anchor);
+                results.push((txid, anchor));
             }
         }
 
         Ok(results)
     }
 
-    /// Validate a single transaction’s Merkle proof, cache its confirmation anchor, and update.
-    #[allow(dead_code)]
-    fn validate_anchor_for_update(
-        &self,
-        tx_update: &mut TxUpdate<ConfirmationBlockTime>,
-        txid: Txid,
-        confirmation_height: usize,
-    ) -> Result<(), Error> {
-        let anchors = self.batch_fetch_anchors(&[(txid, confirmation_height)])?;
-        for (txid, anchor) in anchors {
-            tx_update.anchors.insert((anchor, txid));
-        }
-        Ok(())
-    }
-
     // 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.