From: Brijesh-Thakkar Date: Wed, 30 Sep 2026 19:21:08 +0000 (+0530) Subject: fix(electrum): check batch response lengths before pairing X-Git-Url: http://internal-gitweb-vhost/-client-interface/%22https:/src/struct.EncoderStringWriter.html?a=commitdiff_plain;h=3f6c74cd25a055d86f8dbffdcad3703e55e18595;p=bdk fix(electrum): check batch response lengths before pairing `BdkElectrumClient` paired every batch request with its response via `zip` without checking that the response had one entry per request. A short response (possible with any non-stock `ElectrumApi` implementation) silently dropped trailing scripts, txids and anchors while `sync` still returned `Ok`, and a short `batch_block_header` response left `height_to_hash` incomplete so `height_to_hash[&h]` panicked. Add `check_batch_len` and call it after each of the five batch calls (`batch_script_get_history` x3, `batch_block_header`, `batch_transaction_get_merkle`), returning `Error::Message` on any length mismatch, in line with the other server-misbehaviour checks in this crate. Fixes #2305 --- diff --git a/crates/electrum/src/bdk_electrum_client.rs b/crates/electrum/src/bdk_electrum_client.rs index 090f1f5f..2612c261 100644 --- a/crates/electrum/src/bdk_electrum_client.rs +++ b/crates/electrum/src/bdk_electrum_client.rs @@ -313,6 +313,7 @@ impl BdkElectrumClient { let spk_histories = self .inner .batch_script_get_history(spks.iter().map(|(_, s)| s.spk.as_script()))?; + check_batch_len("batch_script_get_history", spks.len(), spk_histories.len())?; for ((spk_index, spk), spk_history) in spks.into_iter().zip(spk_histories) { let beyond_revealed = last_revealed.is_none_or(|lr| spk_index > lr); @@ -388,6 +389,11 @@ impl BdkElectrumClient { let histories = self .inner .batch_script_get_history(unique_spks.iter().map(|spk| spk.as_script()))?; + check_batch_len( + "batch_script_get_history", + unique_spks.len(), + histories.len(), + )?; let mut spk_map = HashMap::new(); for (spk, history) in unique_spks.into_iter().zip(histories) { spk_map.insert(spk, history); @@ -506,6 +512,11 @@ impl BdkElectrumClient { let spk_histories = self .inner .batch_script_get_history(scripts.iter().map(|spk| spk.as_script()))?; + check_batch_len( + "batch_script_get_history", + scripts.len(), + spk_histories.len(), + )?; for (tx, spk_history) in txs.into_iter().zip(spk_histories) { if let Some(res) = spk_history.into_iter().find(|res| res.tx_hash == tx.0) { @@ -557,6 +568,7 @@ impl BdkElectrumClient { if !missing_heights.is_empty() { let headers = self.inner.batch_block_header(missing_heights.clone())?; + check_batch_len("batch_block_header", missing_heights.len(), headers.len())?; for (height, header) in missing_heights.into_iter().zip(headers) { height_to_hash.insert(height, header.block_hash()); cache.insert(height, header); @@ -580,6 +592,7 @@ impl BdkElectrumClient { // Fetch merkle proofs. let proofs = self.inner.batch_transaction_get_merkle(to_fetch.iter())?; + check_batch_len("batch_transaction_get_merkle", to_fetch.len(), proofs.len())?; // Validate each proof, retrying once for each stale header. for ((txid, height), proof) in to_fetch.into_iter().zip(proofs) { @@ -658,6 +671,17 @@ impl BdkElectrumClient { } } +/// Ensure that a batch response has exactly one entry per request, so that pairing them with +/// `zip` or indexing can never silently drop entries or panic. +fn check_batch_len(method: &str, expected: usize, actual: usize) -> Result<(), Error> { + if expected != actual { + return Err(Error::Message(format!( + "electrum server returned {actual} responses for {expected} {method} requests" + ))); + } + Ok(()) +} + /// Return a [`CheckPoint`] of the latest tip, that connects with `prev_tip`. The latest blocks are /// fetched to construct checkpoint updates with the proper [`BlockHash`] in case of re-org. fn fetch_tip_and_latest_blocks(