bitcoin: reject ambiguous merkle trees in MerkleNode::calculate_root
What changed, and why it matters
This commit fixes a bug in how the library calculates the Merkle root, a fingerprint used to summarize all transactions in a Bitcoin block. An old Bitcoin vulnerability (CVE 2012-2459) lets someone craft a block with duplicate transactions that produces the same fingerprint as a valid block, potentially tricking software into rejecting the real block. The patch makes the library refuse to compute the root whenever it sees the duplicate pattern, so the invalid block is simply rejected and no confusion can occur.
Review downstream callers of `compute_merkle_root`, `compute_witness_root`, and `MerkleNode::calculate_root` to ensure they handle the new `None` case rather than unwrapping, since the patch now returns `None` for duplicate transaction patterns as well as empty iterators. Consider whether the trait bound change from `Copy` to `Copy + PartialEq` breaks any external implementers.
Security signals we found
Fixes CVE 2012-2459 duplicate-transaction Merkle-root ambiguity
Adds equality-based rejection of duplicate sibling hashes in Merkle tree construction
Changes public API return semantics from empty-only None to also None on duplicate patterns
Tightens trait bound to PartialEq to support the new check
Updates test to assert that a forged duplicate block now fails validation
Evidence from the diff
The change updates MerkleNode::calculate_root in bitcoin/src/merkle_tree/mod.rs to detect consecutive duplicate sibling hashes during Merkle root computation and return None instead of continuing. Because Bitcoin blocks cannot contain duplicate transactions, any tree exhibiting such duplication is guaranteed-invalid, yet it can collide with a valid tree’s root (CVE 2012-2459). The trait bound is tightened from Copy to Copy + PartialEq to enable the equality check. Public wrappers compute_merkle_root and compute_witness_root in bitcoin/src/blockdata/block.rs are documented to return None in these cases, and the test expectation for a forged block is flipped from is_ok() to is_err().
Changed components
bitcoin/src/merkle_tree/mod.rsbitcoin/src/blockdata/block.rsMerkleNode::calculate_rootcompute_merkle_rootcompute_witness_rootInspect captured patch +43 / −8
diff --git a/bitcoin/src/blockdata/block.rs b/bitcoin/src/blockdata/block.rs
index 190d4f47..071ff73f 100644
--- a/bitcoin/src/blockdata/block.rs
+++ b/bitcoin/src/blockdata/block.rs
@@ -149,6 +149,14 @@ impl BlockUncheckedExt for Block<Unchecked> {
}
/// Computes the Merkle root for a list of transactions.
+///
+/// Returns `None` if the iterator was empty, or if the transaction list contains
+/// consecutive duplicates which would trigger CVE 2012-2459. Blocks with duplicate
+/// transactions will always be invalid, so there is no harm in us refusing to
+/// compute their merkle roots.
+///
+/// Unless you are certain your transaction list is nonempty and has no duplicates,
+/// you should not unwrap the `Option` returned by this method!
pub fn compute_merkle_root(transactions: &[Transaction]) -> Option<TxMerkleNode> {
let hashes = transactions.iter().map(|obj| obj.compute_txid());
TxMerkleNode::calculate_root(hashes)
@@ -170,6 +178,14 @@ pub fn compute_witness_commitment(
}
/// Computes the Merkle root of transactions hashed for witness.
+///
+/// Returns `None` if the iterator was empty, or if the transaction list contains
+/// consecutive duplicates which would trigger CVE 2012-2459. Blocks with duplicate
+/// transactions will always be invalid, so there is no harm in us refusing to
+/// compute their merkle roots.
+///
+/// Unless you are certain your transaction list is nonempty and has no duplicates,
+/// you should not unwrap the `Option` returned by this method!
pub fn compute_witness_root(transactions: &[Transaction]) -> Option<WitnessMerkleNode> {
let hashes = transactions.iter().enumerate().map(|(i, t)| {
if i == 0 {
@@ -938,6 +954,6 @@ mod tests {
let forged_block = Block::new_unchecked(header, transactions);
assert!(valid_block.validate().is_ok());
- assert!(forged_block.validate().is_ok()); // FIXME fixed in next commit
+ assert!(forged_block.validate().is_err());
}
}
diff --git a/bitcoin/src/merkle_tree/mod.rs b/bitcoin/src/merkle_tree/mod.rs
index 965604e0..760c57cf 100644
--- a/bitcoin/src/merkle_tree/mod.rs
+++ b/bitcoin/src/merkle_tree/mod.rs
@@ -63,7 +63,7 @@ impl Decodable for WitnessMerkleNode {
///
/// Other Merkle trees in Bitcoin, such as those used in Taproot commitments,
/// do not use this algorithm and cannot use this trait.
-pub trait MerkleNode: Copy {
+pub trait MerkleNode: Copy + PartialEq {
/// The hash (TXID or WTXID) of a transaction in the tree.
type Leaf: TxIdentifier;
@@ -74,7 +74,13 @@ pub trait MerkleNode: Copy {
/// Given an iterator of leaves, compute the Merkle root.
///
- /// Returns `None` if and only if the iterator was empty.
+ /// Returns `None` if the iterator was empty, or if the transaction list contains
+ /// consecutive duplicates which would trigger CVE 2012-2459. Blocks with duplicate
+ /// transactions will always be invalid, so there is no harm in us refusing to
+ /// compute their merkle roots.
+ ///
+ /// Unless you are certain your transaction list is nonempty and has no duplicates,
+ /// you should not unwrap the `Option` returned by this method!
fn calculate_root<I: Iterator<Item = Self::Leaf>>(iter: I) -> Option<Self> {
let mut stack = Vec::<(usize, Self)>::with_capacity(32);
// Start with a standard Merkle tree root computation...
@@ -84,6 +90,13 @@ pub trait MerkleNode: Copy {
while n & 1 == 1 {
let right = stack.pop().unwrap();
let left = stack.pop().unwrap();
+ if left.1 == right.1 {
+ // Reject duplicate trees since they are guaranteed-invalid (Bitcoin does
+ // not allow duplicate transactions in block) but can be used to confuse
+ // nodes about legitimate blocks. See CVE 2012-2459 and the block comment
+ // below.
+ return None;
+ }
debug_assert_eq!(left.0, right.0);
stack.push((left.0 + 1, left.1.combine(&right.1)));
n >>= 1;
@@ -91,11 +104,17 @@ pub trait MerkleNode: Copy {
}
// ...then, deal with incomplete trees. Bitcoin does a weird thing in
// which it doubles-up nodes of the tree to fill out the tree, rather
- // than treating incomplete branches specially. This, along with its
- // conflation of leaves with leaf hashes, makes its Merkle tree
- // construction theoretically (though probably not practically)
- // vulnerable to collisions. This is consensus logic so we just have
- // to accept it.
+ // than treating incomplete branches specially. This makes this tree
+ // construction vulnerable to collisions (see CVE 2012-2459).
+ //
+ // (It is also vulnerable to collisions because it does not distinguish
+ // between internal nodes and transactions, but this collisions of this
+ // form are probably impractical. It is likely that 64-byte transactions
+ // will be forbidden in the future which will close this for good.)
+ //
+ // This is consensus logic so we cannot fix the Merkle tree construction.
+ // Instead we just have to reject the clearly-invalid half of the collision
+ // (see previous comment).
while stack.len() > 1 {
let mut right = stack.pop().unwrap();
let left = stack.pop().unwrap();
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.