]> Untitled Git - bdk/commitdiff
fix(electrum): refresh header cache with headers fetched for the chain update
author志宇 <hello@evanlinjin.me>
Fri, 18 Sep 2026 09:36:19 +0000 (09:36 +0000)
committer志宇 <hello@evanlinjin.me>
Fri, 18 Sep 2026 10:14:26 +0000 (10:14 +0000)
`block_header_cache` was only written by `batch_fetch_anchors`, so after a
reorg it kept serving the pre-reorg header for that height. The anchor
cache is keyed by `(txid, hash)` from that header, so a tx re-mined at the
same height hit the anchor cache with the stale hash, skipped proof
validation, and was never re-anchored to the replacement block.

`fetch_tip_and_latest_blocks` now writes the headers it fetches (the
chain suffix and the agreement-walk checkpoints) into the header cache.

crates/electrum/src/bdk_electrum_client.rs

index a7c943150a6ac0b1e51ff564aec656c1d9afbb4c..d80525ba3bf0b9cb85abcef088bcf02f01f7003a 100644 (file)
@@ -132,7 +132,11 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
         let start_time = request.start_time();
 
         let tip_and_latest_blocks = match request.chain_tip() {
-            Some(chain_tip) => Some(fetch_tip_and_latest_blocks(&self.inner, chain_tip)?),
+            Some(chain_tip) => Some(fetch_tip_and_latest_blocks(
+                &self.inner,
+                chain_tip,
+                &mut self.block_header_cache.lock().unwrap(),
+            )?),
             None => None,
         };
 
@@ -217,7 +221,11 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
         let start_time = request.start_time();
 
         let tip_and_latest_blocks = match request.chain_tip() {
-            Some(chain_tip) => Some(fetch_tip_and_latest_blocks(&self.inner, chain_tip)?),
+            Some(chain_tip) => Some(fetch_tip_and_latest_blocks(
+                &self.inner,
+                chain_tip,
+                &mut self.block_header_cache.lock().unwrap(),
+            )?),
             None => None,
         };
 
@@ -652,6 +660,7 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
 fn fetch_tip_and_latest_blocks(
     client: &impl ElectrumApi,
     prev_tip: CheckPoint<BlockHash>,
+    block_header_cache: &mut HashMap<u32, Header>,
 ) -> Result<(CheckPoint<BlockHash>, BTreeMap<u32, BlockHash>), Error> {
     let HeaderNotification { height, .. } = client.block_headers_subscribe()?;
     let new_tip_height = height as u32;
@@ -666,12 +675,14 @@ fn fetch_tip_and_latest_blocks(
     // to construct our checkpoint update.
     let mut new_blocks = {
         let start_height = new_tip_height.saturating_sub(CHAIN_SUFFIX_LENGTH - 1);
-        let hashes = client
+        let headers = client
             .block_headers(start_height as _, CHAIN_SUFFIX_LENGTH as _)?
-            .headers
-            .into_iter()
-            .map(|h| h.block_hash());
-        (start_height..).zip(hashes).collect::<BTreeMap<u32, _>>()
+            .headers;
+        block_header_cache.extend((start_height..).zip(headers.iter().copied()));
+
+        (start_height..)
+            .zip(headers.into_iter().map(|h| h.block_hash()))
+            .collect::<BTreeMap<u32, BlockHash>>()
     };
 
     // Find the "point of agreement" (if any).
@@ -686,7 +697,9 @@ fn fetch_tip_and_latest_blocks(
                         new_tip_height >= cp_block.height,
                         "already checked that electrum's tip cannot be smaller"
                     );
-                    let hash = client.block_header(cp_block.height as _)?.block_hash();
+                    let header = client.block_header(cp_block.height as _)?;
+                    block_header_cache.insert(cp_block.height, header);
+                    let hash = header.block_hash();
                     new_blocks.insert(cp_block.height, hash);
                     hash
                 }