From: Wei Chen Date: Tue, 27 May 2025 18:15:44 +0000 (+0000) Subject: feat(electrum): batch `transaction.get_merkle` calls via `batch_call` X-Git-Tag: bitcoind_rpc-0.21.0~17^2~1 X-Git-Url: http://internal-gitweb-vhost/script/%22https:/database/scripts/static/struct.ScanOptions.html?a=commitdiff_plain;h=4ea5ea6c490adb0652ad068bc816c2434c51da35;p=bdk feat(electrum): batch `transaction.get_merkle` calls via `batch_call` --- diff --git a/crates/electrum/Cargo.toml b/crates/electrum/Cargo.toml index 95982eef..c09bfcba 100644 --- a/crates/electrum/Cargo.toml +++ b/crates/electrum/Cargo.toml @@ -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" } diff --git a/crates/electrum/src/bdk_electrum_client.rs b/crates/electrum/src/bdk_electrum_client.rs index d68f62e1..dde9efeb 100644 --- a/crates/electrum/src/bdk_electrum_client.rs +++ b/crates/electrum/src/bdk_electrum_client.rs @@ -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 BdkElectrumClient { batch_size: usize, pending_anchors: &mut Vec<(Txid, usize)>, ) -> Result, 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::::None; - 'batch_loop: loop { + loop { let spks = (0..batch_size) .map_while(|_| spks_with_expected_txids.next()) .collect::>(); if spks.is_empty() { - break; + return Ok(last_active_index); } let spk_histories = self @@ -279,10 +276,10 @@ impl BdkElectrumClient { 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 BdkElectrumClient { } } - // 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, - 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.