Skip to content

Commit c116974

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

2 files changed

Lines changed: 57 additions & 66 deletions

File tree

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;
@@ -499,72 +496,65 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
499496
}
500497
}
501498

502-
// Fetch missing proofs in batches
503-
for chunk in to_fetch.chunks(MAX_BATCH_SIZE) {
504-
for &(txid, height, hash) in chunk {
505-
// Fetch the raw proof.
506-
let proof = self.inner.transaction_get_merkle(&txid, height)?;
507-
508-
// Validate against header, retrying once on stale header.
509-
let mut header = {
510-
let cache = self.block_header_cache.lock().unwrap();
511-
cache[&(height as u32)]
512-
};
513-
let mut valid = electrum_client::utils::validate_merkle_proof(
499+
// Batch all get_merkle calls.
500+
let mut batch = electrum_client::Batch::default();
501+
for &(txid, height, _) in &to_fetch {
502+
batch.raw(
503+
"blockchain.transaction.get_merkle".into(),
504+
vec![
505+
electrum_client::Param::String(format!("{:x}", txid)),
506+
electrum_client::Param::Usize(height),
507+
],
508+
);
509+
}
510+
let resps = self.inner.batch_call(&batch)?;
511+
512+
// Validate each proof, retrying once for each stale header.
513+
for ((txid, height, hash), resp) in to_fetch.into_iter().zip(resps.into_iter()) {
514+
let proof: electrum_client::GetMerkleRes = serde_json::from_value(resp)?;
515+
516+
let mut header = {
517+
let cache = self.block_header_cache.lock().unwrap();
518+
cache
519+
.get(&(height as u32))
520+
.copied()
521+
.expect("header already fetched above")
522+
};
523+
let mut valid =
524+
electrum_client::utils::validate_merkle_proof(&txid, &header.merkle_root, &proof);
525+
if !valid {
526+
header = self.inner.block_header(height)?;
527+
self.block_header_cache
528+
.lock()
529+
.unwrap()
530+
.insert(height as u32, header);
531+
valid = electrum_client::utils::validate_merkle_proof(
514532
&txid,
515533
&header.merkle_root,
516534
&proof,
517535
);
518-
if !valid {
519-
let new_header = self.inner.block_header(height)?;
520-
self.block_header_cache
521-
.lock()
522-
.unwrap()
523-
.insert(height as u32, new_header);
524-
header = new_header;
525-
valid = electrum_client::utils::validate_merkle_proof(
526-
&txid,
527-
&header.merkle_root,
528-
&proof,
529-
);
530-
}
536+
}
531537

532-
// Build and cache the anchor if merkle proof is valid.
533-
if valid {
534-
let anchor = ConfirmationBlockTime {
535-
confirmation_time: header.time as u64,
536-
block_id: BlockId {
537-
height: height as u32,
538-
hash,
539-
},
540-
};
541-
self.anchor_cache
542-
.lock()
543-
.unwrap()
544-
.insert((txid, hash), anchor);
545-
results.push((txid, anchor));
546-
}
538+
// Build and cache the anchor if merkle proof is valid.
539+
if valid {
540+
let anchor = ConfirmationBlockTime {
541+
confirmation_time: header.time as u64,
542+
block_id: BlockId {
543+
height: height as u32,
544+
hash,
545+
},
546+
};
547+
self.anchor_cache
548+
.lock()
549+
.unwrap()
550+
.insert((txid, hash), anchor);
551+
results.push((txid, anchor));
547552
}
548553
}
549554

550555
Ok(results)
551556
}
552557

553-
/// Validate a single transaction’s Merkle proof, cache its confirmation anchor, and update.
554-
#[allow(dead_code)]
555-
fn validate_anchor_for_update(
556-
&self,
557-
tx_update: &mut TxUpdate<ConfirmationBlockTime>,
558-
txid: Txid,
559-
confirmation_height: usize,
560-
) -> Result<(), Error> {
561-
let anchors = self.batch_fetch_anchors(&[(txid, confirmation_height)])?;
562-
for (txid, anchor) in anchors {
563-
tx_update.anchors.insert((anchor, txid));
564-
}
565-
Ok(())
566-
}
567-
568558
// Helper function which fetches the `TxOut`s of our relevant transactions' previous
569559
// transactions, which we do not have by default. This data is needed to calculate the
570560
// transaction fee.

0 commit comments

Comments
 (0)