Skip to content

Commit 4ea5ea6

Browse files
committed
feat(electrum): batch transaction.get_merkle calls via batch_call
1 parent ec4fd97 commit 4ea5ea6

File tree

2 files changed

+57
-66
lines changed

2 files changed

+57
-66
lines changed

crates/electrum/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ workspace = true
1515
[dependencies]
1616
bdk_core = { path = "../core", version = "0.6.0" }
1717
electrum-client = { version = "0.23.1", features = [ "proxy" ], default-features = false }
18+
serde_json = "1.0"
1819

1920
[dev-dependencies]
2021
bdk_testenv = { path = "../testenv" }

crates/electrum/src/bdk_electrum_client.rs

Lines changed: 56 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,6 @@ use std::sync::{Arc, Mutex};
1212
/// We include a chain suffix of a certain length for the purpose of robustness.
1313
const CHAIN_SUFFIX_LENGTH: u32 = 8;
1414

15-
/// Maximum batch size for proof validation requests
16-
const MAX_BATCH_SIZE: usize = 100;
17-
1815
/// Wrapper around an [`electrum_client::ElectrumApi`] which includes an internal in-memory
1916
/// transaction cache to avoid re-fetching already downloaded transactions.
2017
#[derive(Debug)]
@@ -262,15 +259,15 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
262259
batch_size: usize,
263260
pending_anchors: &mut Vec<(Txid, usize)>,
264261
) -> Result<Option<u32>, Error> {
265-
let mut unused_spk_count = 0;
266-
let mut last_active_index = None;
262+
let mut unused_spk_count = 0_usize;
263+
let mut last_active_index = Option::<u32>::None;
267264

268-
'batch_loop: loop {
265+
loop {
269266
let spks = (0..batch_size)
270267
.map_while(|_| spks_with_expected_txids.next())
271268
.collect::<Vec<_>>();
272269
if spks.is_empty() {
273-
break;
270+
return Ok(last_active_index);
274271
}
275272

276273
let spk_histories = self
@@ -279,10 +276,10 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
279276

280277
for ((spk_index, spk), spk_history) in spks.into_iter().zip(spk_histories) {
281278
if spk_history.is_empty() {
282-
unused_spk_count += 1;
283-
if unused_spk_count >= stop_gap {
284-
break 'batch_loop;
285-
}
279+
match unused_spk_count.checked_add(1) {
280+
Some(i) if i < stop_gap => unused_spk_count = i,
281+
_ => return Ok(last_active_index),
282+
};
286283
} else {
287284
last_active_index = Some(spk_index);
288285
unused_spk_count = 0;
@@ -509,72 +506,65 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
509506
}
510507
}
511508

512-
// Fetch missing proofs in batches
513-
for chunk in to_fetch.chunks(MAX_BATCH_SIZE) {
514-
for &(txid, height, hash) in chunk {
515-
// Fetch the raw proof.
516-
let proof = self.inner.transaction_get_merkle(&txid, height)?;
517-
518-
// Validate against header, retrying once on stale header.
519-
let mut header = {
520-
let cache = self.block_header_cache.lock().unwrap();
521-
cache[&(height as u32)]
522-
};
523-
let mut valid = electrum_client::utils::validate_merkle_proof(
509+
// Batch all get_merkle calls.
510+
let mut batch = electrum_client::Batch::default();
511+
for &(txid, height, _) in &to_fetch {
512+
batch.raw(
513+
"blockchain.transaction.get_merkle".into(),
514+
vec![
515+
electrum_client::Param::String(format!("{:x}", txid)),
516+
electrum_client::Param::Usize(height),
517+
],
518+
);
519+
}
520+
let resps = self.inner.batch_call(&batch)?;
521+
522+
// Validate each proof, retrying once for each stale header.
523+
for ((txid, height, hash), resp) in to_fetch.into_iter().zip(resps.into_iter()) {
524+
let proof: electrum_client::GetMerkleRes = serde_json::from_value(resp)?;
525+
526+
let mut header = {
527+
let cache = self.block_header_cache.lock().unwrap();
528+
cache
529+
.get(&(height as u32))
530+
.copied()
531+
.expect("header already fetched above")
532+
};
533+
let mut valid =
534+
electrum_client::utils::validate_merkle_proof(&txid, &header.merkle_root, &proof);
535+
if !valid {
536+
header = self.inner.block_header(height)?;
537+
self.block_header_cache
538+
.lock()
539+
.unwrap()
540+
.insert(height as u32, header);
541+
valid = electrum_client::utils::validate_merkle_proof(
524542
&txid,
525543
&header.merkle_root,
526544
&proof,
527545
);
528-
if !valid {
529-
let new_header = self.inner.block_header(height)?;
530-
self.block_header_cache
531-
.lock()
532-
.unwrap()
533-
.insert(height as u32, new_header);
534-
header = new_header;
535-
valid = electrum_client::utils::validate_merkle_proof(
536-
&txid,
537-
&header.merkle_root,
538-
&proof,
539-
);
540-
}
546+
}
541547

542-
// Build and cache the anchor if merkle proof is valid.
543-
if valid {
544-
let anchor = ConfirmationBlockTime {
545-
confirmation_time: header.time as u64,
546-
block_id: BlockId {
547-
height: height as u32,
548-
hash,
549-
},
550-
};
551-
self.anchor_cache
552-
.lock()
553-
.unwrap()
554-
.insert((txid, hash), anchor);
555-
results.push((txid, anchor));
556-
}
548+
// Build and cache the anchor if merkle proof is valid.
549+
if valid {
550+
let anchor = ConfirmationBlockTime {
551+
confirmation_time: header.time as u64,
552+
block_id: BlockId {
553+
height: height as u32,
554+
hash,
555+
},
556+
};
557+
self.anchor_cache
558+
.lock()
559+
.unwrap()
560+
.insert((txid, hash), anchor);
561+
results.push((txid, anchor));
557562
}
558563
}
559564

560565
Ok(results)
561566
}
562567

563-
/// Validate a single transaction’s Merkle proof, cache its confirmation anchor, and update.
564-
#[allow(dead_code)]
565-
fn validate_anchor_for_update(
566-
&self,
567-
tx_update: &mut TxUpdate<ConfirmationBlockTime>,
568-
txid: Txid,
569-
confirmation_height: usize,
570-
) -> Result<(), Error> {
571-
let anchors = self.batch_fetch_anchors(&[(txid, confirmation_height)])?;
572-
for (txid, anchor) in anchors {
573-
tx_update.anchors.insert((anchor, txid));
574-
}
575-
Ok(())
576-
}
577-
578568
// Helper function which fetches the `TxOut`s of our relevant transactions' previous
579569
// transactions, which we do not have by default. This data is needed to calculate the
580570
// transaction fee.

0 commit comments

Comments
 (0)