Check Electrum Merkle leaf risk by base size
What changed, and why it matters
This commit fixes a bug in how rust-lightning's Electrum transaction sync checks for a known Bitcoin Merkle-tree weakness. The old code rejected transactions whose total byte size was exactly 64, but an attacker could pad a transaction with extra witness data to push the total size above 64 while keeping the non-witness (base) size at 64. Because Bitcoin txids and Merkle leaves are computed from the non-witness data, the bypass could let a malicious 64-byte-base transaction be treated as a valid Merkle leaf, potentially allowing fake transaction confirmations. The fix checks the base (non-witness) size instead of total size and applies the check in more code paths.
Review and merge promptly; this is a correctness fix for a known Bitcoin Merkle-tree attack vector. Consider whether any other size-based checks in the codebase rely on total_size when base_size is security-relevant. No CVE or advisory is referenced in the commit materials.
Security signals we found
Fixes incorrect size check that could be bypassed with witness padding
Addresses known Bitcoin Merkle leaf-node weakness (bitslog 2018)
Changes total_size to base_size for txid-relevant serialization
Expands unsafe-leaf check to additional confirmation code path
Credits external security research group (Project Loupe)
Evidence from the diff
The patch changes the Electrum confirmation check from tx.total_size() == 64 to tx.base_size() == 64. It introduces is_potentially_unsafe_merkle_leaf() in common.rs and uses it in both transaction_get handling and get_confirmed_tx(). get_confirmed_tx now returns Result
Changed components
lightning-transaction-sync/src/common.rslightning-transaction-sync/src/electrum.rsElectrumSyncClientget_confirmed_txtransaction_get confirmation loopInspect captured patch +31 / −15
diff --git a/lightning-transaction-sync/src/common.rs b/lightning-transaction-sync/src/common.rs
index 88e52de..bafc9dc 100644
--- a/lightning-transaction-sync/src/common.rs
+++ b/lightning-transaction-sync/src/common.rs
@@ -133,6 +133,10 @@ impl FilterQueue {
}
}
+pub(crate) fn is_potentially_unsafe_merkle_leaf(tx: &Transaction) -> bool {
+ tx.base_size() == 64
+}
+
#[derive(Debug)]
pub(crate) struct ConfirmedTx {
pub tx: Transaction,
diff --git a/lightning-transaction-sync/src/electrum.rs b/lightning-transaction-sync/src/electrum.rs
index cb93724..540cfc1 100644
--- a/lightning-transaction-sync/src/electrum.rs
+++ b/lightning-transaction-sync/src/electrum.rs
@@ -5,7 +5,7 @@
// http://opensource.org/licenses/MIT>, at your option. You may not use this file except in
// accordance with one or both of these licenses.
-use crate::common::{ConfirmedTx, FilterQueue, SyncState};
+use crate::common::{is_potentially_unsafe_merkle_leaf, ConfirmedTx, FilterQueue, SyncState};
use crate::error::{InternalError, TxSyncError};
use electrum_client::utils::validate_merkle_proof;
@@ -277,14 +277,9 @@ impl<L: Logger> ElectrumSyncClient<L> {
for txid in &sync_state.watched_transactions {
match self.client.transaction_get(&txid) {
Ok(tx) => {
- // Bitcoin Core's Merkle tree implementation has no way to discern between
- // internal and leaf node entries. As a consequence it is susceptible to an
- // attacker injecting additional transactions by crafting 64-byte
- // transactions matching an inner Merkle node's hash (see
- // https://web.archive.org/web/20240329003521/https://bitslog.com/2018/06/09/leaf-node-weakness-in-bitcoin-merkle-tree-design/).
- // To protect against this (highly unlikely) attack vector, we check that the
- // transaction is at least 65 bytes in length.
- if tx.total_size() == 64 {
+ // Skip before using an arbitrary returned output to look up the
+ // transaction's script history.
+ if is_potentially_unsafe_merkle_leaf(&tx) {
log_error!(self.logger, "Skipping transaction {} due to retrieving potentially invalid tx data.", txid);
continue;
}
@@ -340,8 +335,9 @@ impl<L: Logger> ElectrumSyncClient<L> {
continue;
}
let prob_conf_height = history.height as u32;
- let confirmed_tx = self.get_confirmed_tx(tx, prob_conf_height)?;
- confirmed_txs.push(confirmed_tx);
+ if let Some(confirmed_tx) = self.get_confirmed_tx(tx, prob_conf_height)? {
+ confirmed_txs.push(confirmed_tx);
+ }
}
if filtered_history.next().is_some() {
log_error!(
@@ -384,8 +380,11 @@ impl<L: Logger> ElectrumSyncClient<L> {
}
let prob_conf_height = possible_output_spend.height as u32;
- let confirmed_tx = self.get_confirmed_tx(&tx, prob_conf_height)?;
- confirmed_txs.push(confirmed_tx);
+ if let Some(confirmed_tx) =
+ self.get_confirmed_tx(&tx, prob_conf_height)?
+ {
+ confirmed_txs.push(confirmed_tx);
+ }
},
Err(e) => {
log_trace!(
@@ -450,8 +449,21 @@ impl<L: Logger> ElectrumSyncClient<L> {
fn get_confirmed_tx(
&self, tx: &Transaction, prob_conf_height: u32,
- ) -> Result<ConfirmedTx, InternalError> {
+ ) -> Result<Option<ConfirmedTx>, InternalError> {
let txid = tx.compute_txid();
+ // Bitcoin Core's Merkle tree implementation has no way to discern between internal and
+ // leaf node entries. As a consequence it is susceptible to an attacker injecting
+ // additional transactions by crafting 64-byte transactions matching an inner Merkle
+ // node's hash (see https://web.archive.org/web/20240329003521/https://bitslog.com/2018/06/09/leaf-node-weakness-in-bitcoin-merkle-tree-design/).
+ if is_potentially_unsafe_merkle_leaf(tx) {
+ log_error!(
+ self.logger,
+ "Skipping transaction {} due to retrieving potentially invalid tx data.",
+ txid
+ );
+ return Ok(None);
+ }
+
match self.client.transaction_get_merkle(&txid, prob_conf_height as usize) {
Ok(merkle_res) => {
debug_assert_eq!(prob_conf_height, merkle_res.block_height as u32);
@@ -473,7 +485,7 @@ impl<L: Logger> ElectrumSyncClient<L> {
block_height: prob_conf_height,
pos,
};
- Ok(confirmed_tx)
+ Ok(Some(confirmed_tx))
},
Err(e) => {
log_error!(
Why this scored 62/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.