Describe the bug
batch_fetch_anchors (crates/electrum/src/bdk_electrum_client.rs) derives each height's block hash from block_header_cache and, on an anchor_cache hit for (txid, hash), returns the cached anchor without fetching a header or validating a merkle proof. The header cache is only refreshed when a proof fails to validate, which never happens on the cached path. So once both caches hold the pre-reorg state for a (txid, height), a reorg that re-includes the transaction in a different block at the same height goes unnoticed: every subsequent call returns the anchor with the replaced block's hash, for the lifetime of the BdkElectrumClient.
fetch_tip_and_latest_blocks does pick up the new hash for the LocalChain, so the sync result has a chain with the new hash at that height and an anchor pointing at the old one. Canonicalization then treats the transaction as unconfirmed even though it is confirmed, and later syncs with the same client do not correct it. The existing test_batch_fetch_anchors_reorg_uses_new_hash only covers the case where the anchor cache is empty.
This issue was found by AI.
To Reproduce
Add the following to the test module in crates/electrum/src/bdk_electrum_client.rs (next to test_batch_fetch_anchors_reorg_uses_new_hash) and run cargo test -p bdk_electrum --lib batch_fetch_anchors_after_reorg_with_populated_caches:
#[cfg(feature = "default")]
#[test]
fn batch_fetch_anchors_after_reorg_with_populated_caches() -> anyhow::Result<()> {
let env = TestEnv::new()?;
let client = electrum_client::Client::new(env.electrsd.electrum_url.as_str()).unwrap();
let electrum_client = BdkElectrumClient::new(client);
env.mine_blocks(101, None)?;
let addr = env
.rpc_client()
.get_new_address(None, None)?
.address()?
.assume_checked();
let txid = env.send(&addr, Amount::from_sat(50_000))?;
env.mine_blocks(1, None)?;
env.wait_until_electrum_sees_block(Duration::from_secs(6))?;
let height = env.rpc_client().get_block_count()?.into_model().0 as usize;
// First call populates both the header cache and the anchor cache.
let pre_reorg = electrum_client.batch_fetch_anchors(&[(txid, height)])?;
let stale_hash = pre_reorg[0].1.block_id.hash;
// Replace the confirming block; the tx is re-included at the same height.
env.reorg(1)?;
env.wait_until_electrum_sees_block(Duration::from_secs(6))?;
let new_hash = electrum_client.inner.block_header(height)?.block_hash();
assert_ne!(new_hash, stale_hash);
let anchors = electrum_client.batch_fetch_anchors(&[(txid, height)])?;
assert_eq!(anchors[0].1.block_id.hash, new_hash);
Ok(())
}
Current output:
assertion `left == right` failed
left: 5cb52f60b4f1ef75606e44e9dc8356fd6ca043eb887a613d6c498f1095930652
right: 34b9db58142dc754829f9094e68c48a5b1c911f5d1ea84a531758fa906789605
Expected behavior
After a reorg, anchors returned by batch_fetch_anchors should reflect the block currently at that height rather than a previously cached one.
Describe the bug
batch_fetch_anchors(crates/electrum/src/bdk_electrum_client.rs) derives each height's block hash fromblock_header_cacheand, on ananchor_cachehit for(txid, hash), returns the cached anchor without fetching a header or validating a merkle proof. The header cache is only refreshed when a proof fails to validate, which never happens on the cached path. So once both caches hold the pre-reorg state for a(txid, height), a reorg that re-includes the transaction in a different block at the same height goes unnoticed: every subsequent call returns the anchor with the replaced block's hash, for the lifetime of theBdkElectrumClient.fetch_tip_and_latest_blocksdoes pick up the new hash for theLocalChain, so the sync result has a chain with the new hash at that height and an anchor pointing at the old one. Canonicalization then treats the transaction as unconfirmed even though it is confirmed, and later syncs with the same client do not correct it. The existingtest_batch_fetch_anchors_reorg_uses_new_hashonly covers the case where the anchor cache is empty.This issue was found by AI.
To Reproduce
Add the following to the
testmodule incrates/electrum/src/bdk_electrum_client.rs(next totest_batch_fetch_anchors_reorg_uses_new_hash) and runcargo test -p bdk_electrum --lib batch_fetch_anchors_after_reorg_with_populated_caches:Current output:
Expected behavior
After a reorg, anchors returned by
batch_fetch_anchorsshould reflect the block currently at that height rather than a previously cached one.