Validate Esplora merkle proof against the block header's merkle root
What changed, and why it matters
This commit fixes a security bug in LDK's Esplora client. The client was supposed to verify that a transaction was really included in a Bitcoin block by checking a cryptographic proof (a merkle proof) from the Esplora server. But it only checked that the proof contained the right transaction ID, not that the proof actually connected to the real block header. Because a single-leaf proof can be forged for any transaction ID, a malicious or compromised Esplora server could trick LDK into believing any transaction was confirmed in any block. That could trigger wrong channel state changes, premature force-closes, or other fund-risking behavior. The fix makes the client compare the computed merkle root from the proof against the block header's merkle root, which is the same check the Electrum client already did.
Upgrade to a rust-lightning release containing this commit. If running an LDK node that uses EsploraSyncClient, treat any transaction confirmations received before the patch as potentially attacker-chosen and avoid relying on them for irreversible channel actions. Consider switching to Electrum or a trusted Bitcoin RPC source as a temporary mitigation if patching is delayed.
Security signals we found
Missing cryptographic root-of-trust validation (merkle root discarded)
Single-party server (Esplora) could forge transaction confirmation
Forged confirmation propagates into Confirm implementations (ChannelManager / ChainMonitor)
Could cause premature channel state transitions and force-close races
Fix aligns Esplora validation with existing Electrum validation
Evidence from the diff
In lightning-transaction-sync/src/esplora.rs, EsploraSyncClient::get_confirmed_tx called PartialMerkleTree::extract_matches and discarded its returned merkle root with let _ = …, then only verified that matches[0] == txid. Since a single-leaf partial merkle tree can be constructed for an arbitrary txid via PartialMerkleTree::from_txids(&[txid], &[true]), this check was vacuous. A malicious Esplora server could return MerkleBlock { header: real_header, txn: forged_partial_tree } and convince LDK that any txid was confirmed in any block. The patch captures computed_merkle_root = merkle_block.txn.extract_matches(…).ok() and requires it to equal block_header.merkle_root, mirroring the Electrum sibling’s validate_merkle_proof behavior.
Changed components
lightning-transaction-sync/src/esplora.rsEsploraSyncClient::get_confirmed_txLDK Confirm trait implementations (ChannelManager, ChainMonitor)Inspect captured patch +7 / −2
diff --git a/lightning-transaction-sync/src/esplora.rs b/lightning-transaction-sync/src/esplora.rs
index 6caf7a6..7d3550d 100644
--- a/lightning-transaction-sync/src/esplora.rs
+++ b/lightning-transaction-sync/src/esplora.rs
@@ -361,8 +361,13 @@ impl<L: Logger> EsploraSyncClient<L> {
let mut matches = Vec::new();
let mut indexes = Vec::new();
- let _ = merkle_block.txn.extract_matches(&mut matches, &mut indexes);
- if indexes.len() != 1 || matches.len() != 1 || matches[0] != txid {
+ let computed_merkle_root =
+ merkle_block.txn.extract_matches(&mut matches, &mut indexes).ok();
+ if computed_merkle_root != Some(block_header.merkle_root)
+ || indexes.len() != 1
+ || matches.len() != 1
+ || matches[0] != txid
+ {
log_error!(self.logger, "Retrieved Merkle block for txid {} doesn't match expectations. This should not happen. Please verify server integrity.", txid);
return Err(InternalError::Failed);
}
Why this scored 88/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.