Skip to content

Commit 6d6f4ed

Browse files
committed
feat(electrum): prune stale Merkle proofs on chain reorg
1 parent 6ddec6d commit 6d6f4ed

File tree

1 file changed

+71
-25
lines changed

1 file changed

+71
-25
lines changed

crates/electrum/src/bdk_electrum_client.rs

Lines changed: 71 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ pub struct BdkElectrumClient<E> {
2626
/// The header cache
2727
block_header_cache: Mutex<HashMap<u32, Header>>,
2828
/// The Merkle proof cache
29-
merkle_cache: Mutex<HashMap<(Txid, u32), GetMerkleRes>>,
29+
merkle_cache: Mutex<HashMap<(Txid, BlockHash), GetMerkleRes>>,
3030
}
3131

3232
impl<E: ElectrumApi> BdkElectrumClient<E> {
@@ -315,7 +315,7 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
315315
}
316316
}
317317

318-
// Batch validate all collected transactions
318+
// Batch validate all collected transactions.
319319
if !txs_to_validate.is_empty() {
320320
let proofs = self.batch_fetch_merkle_proofs(&txs_to_validate)?;
321321
self.batch_validate_merkle_proofs(tx_update, proofs)?;
@@ -345,7 +345,7 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
345345
};
346346
debug_assert_eq!(op_tx.compute_txid(), op_txid);
347347

348-
// attempt to find the following transactions (alongside their chain positions), and
348+
// Attempt to find the following transactions (alongside their chain positions), and
349349
// add to our sparsechain `update`:
350350
let mut has_residing = false; // tx in which the outpoint resides
351351
let mut has_spending = false; // tx that spends the outpoint
@@ -370,7 +370,7 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
370370

371371
if !has_spending && res.tx_hash != op_txid {
372372
let res_tx = self.fetch_tx(res.tx_hash)?;
373-
// we exclude txs/anchors that do not spend our specified outpoint(s)
373+
// We exclude txs/anchors that do not spend our specified outpoint(s).
374374
has_spending = res_tx
375375
.input
376376
.iter()
@@ -392,7 +392,7 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
392392
}
393393
}
394394

395-
// Batch validate all collected transactions
395+
// Batch validate all collected transactions.
396396
if !txs_to_validate.is_empty() {
397397
let proofs = self.batch_fetch_merkle_proofs(&txs_to_validate)?;
398398
self.batch_validate_merkle_proofs(tx_update, proofs)?;
@@ -423,8 +423,8 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
423423
.map(|txo| &txo.script_pubkey)
424424
.expect("tx must have an output");
425425

426-
// because of restrictions of the Electrum API, we have to use the `script_get_history`
427-
// call to get confirmation status of our transaction
426+
// Because of restrictions of the Electrum API, we have to use the `script_get_history`
427+
// call to get confirmation status of our transaction.
428428
if let Some(r) = self
429429
.inner
430430
.script_get_history(spk)?
@@ -445,7 +445,7 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
445445
tx_update.txs.push(tx);
446446
}
447447

448-
// Batch validate all collected transactions
448+
// Batch validate all collected transactions.
449449
if !txs_to_validate.is_empty() {
450450
let proofs = self.batch_fetch_merkle_proofs(&txs_to_validate)?;
451451
self.batch_validate_merkle_proofs(tx_update, proofs)?;
@@ -454,47 +454,65 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
454454
Ok(())
455455
}
456456

457-
/// Batch fetch Merkle proofs for multiple transactions
457+
/// Batch fetch Merkle proofs for multiple transactions.
458458
fn batch_fetch_merkle_proofs(
459459
&self,
460460
txs_with_heights: &[(Txid, usize)],
461461
) -> Result<Vec<(Txid, GetMerkleRes)>, Error> {
462+
// Evict proofs whose block hash is no longer on our current chain.
463+
self.clear_stale_proofs()?;
464+
462465
let mut results = Vec::with_capacity(txs_with_heights.len());
463466
let mut to_fetch = Vec::new();
464467

465-
// Check cache first
468+
// Build a map for height to block hash conversions. This is for obtaining block hash data
469+
// with minimum `fetch_header` calls.
470+
let mut height_to_hash: HashMap<u32, BlockHash> = HashMap::new();
471+
for &(_, height) in txs_with_heights {
472+
let h = height as u32;
473+
if !height_to_hash.contains_key(&h) {
474+
// Try to obtain hash from the header cache, or fetch the header if absent.
475+
let hash = self.fetch_header(h)?.block_hash();
476+
height_to_hash.insert(h, hash);
477+
}
478+
}
479+
480+
// Check cache.
466481
{
467482
let merkle_cache = self.merkle_cache.lock().unwrap();
468483
for &(txid, height) in txs_with_heights {
469-
if let Some(proof) = merkle_cache.get(&(txid, height as u32)) {
484+
let h = height as u32;
485+
let hash = height_to_hash[&h];
486+
if let Some(proof) = merkle_cache.get(&(txid, hash)) {
470487
results.push((txid, proof.clone()));
471488
} else {
472-
to_fetch.push((txid, height));
489+
to_fetch.push((txid, height, hash));
473490
}
474491
}
475492
}
476493

477-
// Fetch missing proofs in batches
494+
// Fetch missing proofs in batches.
478495
for chunk in to_fetch.chunks(MAX_MERKLE_BATCH_SIZE) {
479-
for &(txid, height) in chunk {
480-
if let Ok(merkle_res) = self.inner.transaction_get_merkle(&txid, height) {
481-
let mut cache = self.merkle_cache.lock().unwrap();
482-
cache.insert((txid, height as u32), merkle_res.clone());
483-
results.push((txid, merkle_res));
484-
}
496+
for &(txid, height, hash) in chunk {
497+
let merkle_res = self.inner.transaction_get_merkle(&txid, height)?;
498+
self.merkle_cache
499+
.lock()
500+
.unwrap()
501+
.insert((txid, hash), merkle_res.clone());
502+
results.push((txid, merkle_res));
485503
}
486504
}
487505

488506
Ok(results)
489507
}
490508

491-
/// Batch validate Merkle proofs
509+
/// Batch validate Merkle proofs.
492510
fn batch_validate_merkle_proofs(
493511
&self,
494512
tx_update: &mut TxUpdate<ConfirmationBlockTime>,
495513
proofs: Vec<(Txid, GetMerkleRes)>,
496514
) -> Result<(), Error> {
497-
// Pre-fetch all required headers
515+
// Pre-fetch all required headers.
498516
let heights: HashSet<u32> = proofs
499517
.iter()
500518
.map(|(_, proof)| proof.block_height as u32)
@@ -505,7 +523,7 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
505523
headers.insert(height, self.fetch_header(height)?);
506524
}
507525

508-
// Validate proofs
526+
// Validate proofs.
509527
for (txid, merkle_res) in proofs {
510528
let height = merkle_res.block_height as u32;
511529
let header = headers.get(&height).unwrap();
@@ -516,7 +534,7 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
516534
&merkle_res,
517535
);
518536

519-
// Retry with updated header if validation fails
537+
// Retry with updated header if validation fails.
520538
if !is_confirmed_tx {
521539
let updated_header = self.update_header(height)?;
522540
headers.insert(height, updated_header);
@@ -545,15 +563,43 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
545563
Ok(())
546564
}
547565

548-
// Replace the old validate_merkle_for_anchor with optimized batch version
566+
/// Remove any proofs for blocks that may have been re-orged out.
567+
///
568+
/// Checks if the latest cached block hash matches the current chain tip. If not, evicts proofs
569+
/// for blocks that were re-orged out, stopping at the fork point.
570+
fn clear_stale_proofs(&self) -> Result<(), Error> {
571+
let mut cache = self.merkle_cache.lock().unwrap();
572+
573+
// Collect one (height, old_hash) pair per proof.
574+
let mut entries: Vec<(u32, BlockHash)> = cache
575+
.iter()
576+
.map(|((_, old_hash), res)| (res.block_height as u32, *old_hash))
577+
.collect();
578+
579+
// Sort descending and dedup so we only check each height once.
580+
entries.sort_unstable_by(|a, b| b.0.cmp(&a.0));
581+
entries.dedup();
582+
583+
// Evict any stale proofs until fork point is found.
584+
for (height, old_hash) in entries {
585+
let current_hash = self.fetch_header(height)?.block_hash();
586+
if current_hash == old_hash {
587+
break;
588+
}
589+
cache.retain(|&(_txid, bh), _| bh != old_hash);
590+
}
591+
Ok(())
592+
}
593+
594+
/// Validate the Merkle proof for a single transaction using the batch APIs.
549595
#[allow(dead_code)]
550596
fn validate_merkle_for_anchor(
551597
&self,
552598
tx_update: &mut TxUpdate<ConfirmationBlockTime>,
553599
txid: Txid,
554600
confirmation_height: usize,
555601
) -> Result<(), Error> {
556-
// Use the batch processing functions even for single tx
602+
// Use the batch processing functions even for single tx.
557603
let proofs = self.batch_fetch_merkle_proofs(&[(txid, confirmation_height)])?;
558604
self.batch_validate_merkle_proofs(tx_update, proofs)
559605
}

0 commit comments

Comments
 (0)