Remove alloc feature gate for MerkleNode
What changed, and why it matters
This commit refactors how Bitcoin Merkle roots are calculated so the code works even when the optional 'alloc' memory allocator feature is disabled. It replaces a growable vector with a fixed-size 15-slot array, which limits the no-alloc path to at most 32,767 transaction IDs. The change is a feature-portability improvement, not a security fix, and it explicitly returns None when the no-alloc limit is exceeded rather than overflowing or corrupting state.
No immediate security action required. Reviewers should verify that the 32,768 no-alloc limit is acceptable for all intended no-std use cases and that the ArrayVec capacity of 15 correctly bounds every possible Merkle tree depth for that input size. Consider whether callers relying on calculate_root in no-alloc environments handle the new None case appropriately.
Security signals we found
Bounds check added: returns None when no-alloc stack reaches capacity, preventing ArrayVec overflow/panic.
Existing duplicate-leaf rejection (CVE-2012-2459 mitigation) is preserved and now also runs without alloc.
No new unsafe code, no new cryptographic operations, no change to hashing algorithm.
Public API expands: calculate_root, combine, and from_leaf become available without the alloc feature.
Evidence from the diff
The patch removes #[cfg(feature = “alloc”)] gates from the MerkleNode trait and its implementations for TxMerkleNode and WitnessMerkleNode. In calculate_root, when alloc is enabled the existing Vec<(usize, Self)> stack is kept; when alloc is disabled it uses ArrayVec<(usize, Self), 15>. A guard returns None if the stack reaches length 15, which corresponds to an input iterator of 32,768 or more leaves. Tests are updated to run without alloc and a new test verifies the 32,768 boundary behavior. The commit also updates the public API snapshot and mutants.toml to reflect the now-unconditionally-available methods.
Changed components
primitives/src/merkle_tree.rsprimitives/src/hash_types/transaction_merkle_node.rsprimitives/src/hash_types/witness_merkle_node.rsprimitives/src/transaction.rsInspect captured patch +43 / −24
diff --git a/.cargo/mutants.toml b/.cargo/mutants.toml
index 83a47a2e..005039b0 100644
--- a/.cargo/mutants.toml
+++ b/.cargo/mutants.toml
@@ -51,6 +51,7 @@ exclude_re = [
"primitives/.* <impl Decoder for WitnessDecoder>::push_bytes", # Replacing == with != causes an infinite loop
"primitives/.* WitnessDecoder::resize_if_needed", # Replacing *= with += still resizes the buffer making the mutant untestable.
"primitives/.* replace \\+ with \\* in MerkleNode::calculate_root", # Replacing + with * causes an infinite loop
+ "primitives/.* replace == with != in MerkleNode::calculate_root", # Replacing == with != isn't caught unless alloc is disabled.
# consensus_encoding - most of these are for mutations in the logic used to determine when to stop encoding or decoding.
"consensus_encoding/.* <impl Decoder for ArrayDecoder<N>>::push_bytes", # Mutations cause an infinite loop
diff --git a/api/primitives/no-features.txt b/api/primitives/no-features.txt
index d710f241..9a811381 100644
--- a/api/primitives/no-features.txt
+++ b/api/primitives/no-features.txt
@@ -514,12 +514,15 @@ pub fn bitcoin_primitives::TxMerkleNode::as_ref(&self) -> &[u8; 32]
pub fn bitcoin_primitives::TxMerkleNode::as_ref(&self) -> &[u8]
pub fn bitcoin_primitives::TxMerkleNode::borrow(&self) -> &[u8; 32]
pub fn bitcoin_primitives::TxMerkleNode::borrow(&self) -> &[u8]
+pub fn bitcoin_primitives::TxMerkleNode::calculate_root<I: core::iter::traits::iterator::Iterator<Item = bitcoin_primitives::Txid>>(iter: I) -> core::option::Option<Self>
pub fn bitcoin_primitives::TxMerkleNode::clone(&self) -> bitcoin_primitives::TxMerkleNode
pub fn bitcoin_primitives::TxMerkleNode::cmp(&self, other: &bitcoin_primitives::TxMerkleNode) -> core::cmp::Ordering
+pub fn bitcoin_primitives::TxMerkleNode::combine(&self, other: &Self) -> Self
pub fn bitcoin_primitives::TxMerkleNode::decoder() -> Self::Decoder
pub fn bitcoin_primitives::TxMerkleNode::encoder(&self) -> Self::Encoder
pub fn bitcoin_primitives::TxMerkleNode::eq(&self, other: &bitcoin_primitives::TxMerkleNode) -> bool
pub fn bitcoin_primitives::TxMerkleNode::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+pub fn bitcoin_primitives::TxMerkleNode::from_leaf(leaf: bitcoin_primitives::Txid) -> Self
pub fn bitcoin_primitives::TxMerkleNode::hash<__H: core::hash::Hasher>(&self, state: &mut __H)
pub fn bitcoin_primitives::TxMerkleNode::partial_cmp(&self, other: &bitcoin_primitives::TxMerkleNode) -> core::option::Option<core::cmp::Ordering>
pub fn bitcoin_primitives::Txid::as_ref(&self) -> &[u8; 32]
@@ -546,12 +549,15 @@ pub fn bitcoin_primitives::WitnessMerkleNode::as_ref(&self) -> &[u8; 32]
pub fn bitcoin_primitives::WitnessMerkleNode::as_ref(&self) -> &[u8]
pub fn bitcoin_primitives::WitnessMerkleNode::borrow(&self) -> &[u8; 32]
pub fn bitcoin_primitives::WitnessMerkleNode::borrow(&self) -> &[u8]
+pub fn bitcoin_primitives::WitnessMerkleNode::calculate_root<I: core::iter::traits::iterator::Iterator<Item = bitcoin_primitives::Wtxid>>(iter: I) -> core::option::Option<Self>
pub fn bitcoin_primitives::WitnessMerkleNode::clone(&self) -> bitcoin_primitives::WitnessMerkleNode
pub fn bitcoin_primitives::WitnessMerkleNode::cmp(&self, other: &bitcoin_primitives::WitnessMerkleNode) -> core::cmp::Ordering
+pub fn bitcoin_primitives::WitnessMerkleNode::combine(&self, other: &Self) -> Self
pub fn bitcoin_primitives::WitnessMerkleNode::decoder() -> Self::Decoder
pub fn bitcoin_primitives::WitnessMerkleNode::encoder(&self) -> Self::Encoder
pub fn bitcoin_primitives::WitnessMerkleNode::eq(&self, other: &bitcoin_primitives::WitnessMerkleNode) -> bool
pub fn bitcoin_primitives::WitnessMerkleNode::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+pub fn bitcoin_primitives::WitnessMerkleNode::from_leaf(leaf: bitcoin_primitives::Wtxid) -> Self
pub fn bitcoin_primitives::WitnessMerkleNode::hash<__H: core::hash::Hasher>(&self, state: &mut __H)
pub fn bitcoin_primitives::WitnessMerkleNode::partial_cmp(&self, other: &bitcoin_primitives::WitnessMerkleNode) -> core::option::Option<core::cmp::Ordering>
pub fn bitcoin_primitives::Wtxid::as_ref(&self) -> &[u8; 32]
diff --git a/primitives/src/hash_types/transaction_merkle_node.rs b/primitives/src/hash_types/transaction_merkle_node.rs
index a5d37c97..624b6922 100644
--- a/primitives/src/hash_types/transaction_merkle_node.rs
+++ b/primitives/src/hash_types/transaction_merkle_node.rs
@@ -12,9 +12,7 @@ use arbitrary::{Arbitrary, Unstructured};
use hashes::sha256d;
use internals::write_err;
-#[cfg(feature = "alloc")]
use crate::merkle_tree::MerkleNode;
-#[cfg(feature = "alloc")]
use crate::Txid;
/// A hash of the Merkle tree branch or root for transactions.
@@ -28,7 +26,6 @@ type Inner = sha256d::Hash;
include!("./generic.rs");
-#[cfg(feature = "alloc")]
impl TxMerkleNode {
/// Convert a [`Txid`] hash to a leaf node of the tree.
pub fn from_leaf(leaf: Txid) -> Self { MerkleNode::from_leaf(leaf) }
diff --git a/primitives/src/hash_types/witness_merkle_node.rs b/primitives/src/hash_types/witness_merkle_node.rs
index 1eb27b43..b57af827 100644
--- a/primitives/src/hash_types/witness_merkle_node.rs
+++ b/primitives/src/hash_types/witness_merkle_node.rs
@@ -12,9 +12,7 @@ use arbitrary::{Arbitrary, Unstructured};
use hashes::sha256d;
use internals::write_err;
-#[cfg(feature = "alloc")]
use crate::merkle_tree::MerkleNode;
-#[cfg(feature = "alloc")]
use crate::Wtxid;
/// A hash corresponding to the Merkle tree root for witness data.
@@ -28,7 +26,6 @@ type Inner = sha256d::Hash;
include!("./generic.rs");
-#[cfg(feature = "alloc")]
impl WitnessMerkleNode {
/// Convert a [`Wtxid`] hash to a leaf node of the tree.
pub fn from_leaf(leaf: Wtxid) -> Self { MerkleNode::from_leaf(leaf) }
diff --git a/primitives/src/merkle_tree.rs b/primitives/src/merkle_tree.rs
index e6bb0b65..468e7f26 100644
--- a/primitives/src/merkle_tree.rs
+++ b/primitives/src/merkle_tree.rs
@@ -11,13 +11,12 @@
// C'est la vie.
#[cfg(feature = "alloc")]
use alloc::vec::Vec;
+#[cfg(not(feature = "alloc"))]
+use internals::array_vec::ArrayVec;
-#[cfg(feature = "alloc")]
use hashes::{HashEngine, sha256d};
-#[cfg(feature = "alloc")]
use crate::hash_types::{Txid, Wtxid};
-#[cfg(feature = "alloc")]
use crate::transaction::TxIdentifier;
#[doc(inline)]
@@ -32,7 +31,6 @@ pub use crate::hash_types::{TxMerkleNode, TxMerkleNodeEncoder, WitnessMerkleNode
///
/// Other Merkle trees in Bitcoin, such as those used in Taproot commitments,
/// do not use this algorithm and cannot use this trait.
-#[cfg(feature = "alloc")]
pub(crate) trait MerkleNode: Copy + PartialEq {
/// The hash (TXID or WTXID) of a transaction in the tree.
type Leaf: TxIdentifier;
@@ -50,13 +48,23 @@ pub(crate) trait MerkleNode: Copy + PartialEq {
/// transactions will always be invalid, so there is no harm in us refusing to
/// compute their merkle roots.
///
+ /// Also returns `None` if the `alloc` feature is disabled and `iter` has more than
+ /// 32,767 transactions.
+ ///
/// 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> {
{
+ #[cfg(feature = "alloc")]
let mut stack = Vec::<(usize, Self)>::with_capacity(32);
+ #[cfg(not(feature = "alloc"))]
+ let mut stack = ArrayVec::<(usize, Self), 15>::new();
+
// Start with a standard Merkle tree root computation...
for (mut n, leaf) in iter.enumerate() {
+ #[cfg(not(feature = "alloc"))]
+ // This is the only time that the stack actually grows, rather than being combined.
+ if stack.len() == 15 { return None; }
stack.push((0, Self::from_leaf(leaf)));
while n & 1 == 1 {
@@ -106,7 +114,6 @@ pub(crate) trait MerkleNode: Copy + PartialEq {
// our hash traits, it should be possible to put bounds on `MerkleNode`
// and `MerkleNode::Leaf` which are sufficient to turn both methods into
// provided methods in the trait definition.
-#[cfg(feature = "alloc")]
impl MerkleNode for TxMerkleNode {
type Leaf = Txid;
fn from_leaf(leaf: Self::Leaf) -> Self { Self::from_byte_array(leaf.to_byte_array()) }
@@ -118,7 +125,6 @@ impl MerkleNode for TxMerkleNode {
Self::from_byte_array(sha256d::Hash::from_engine(encoder).to_byte_array())
}
}
-#[cfg(feature = "alloc")]
impl MerkleNode for WitnessMerkleNode {
type Leaf = Wtxid;
fn from_leaf(leaf: Self::Leaf) -> Self { Self::from_byte_array(leaf.to_byte_array()) }
@@ -133,11 +139,9 @@ impl MerkleNode for WitnessMerkleNode {
#[cfg(test)]
mod tests {
- #[cfg(feature = "alloc")]
use crate::hash_types::*;
// Helper to make a Txid, TxMerkleNode pair with a single number byte array
- #[cfg(feature = "alloc")]
fn make_leaf_node(byte: u8) -> (Txid, TxMerkleNode) {
let leaf = Txid::from_byte_array([byte; 32]);
let node = TxMerkleNode::from_leaf(leaf);
@@ -145,7 +149,6 @@ mod tests {
}
#[test]
- #[cfg(feature = "alloc")]
fn tx_merkle_node_single_leaf() {
let (leaf, node) = make_leaf_node(1);
let root = TxMerkleNode::calculate_root([leaf].into_iter());
@@ -154,7 +157,6 @@ mod tests {
}
#[test]
- #[cfg(feature = "alloc")]
fn tx_merkle_node_two_leaves() {
let (leaf1, node1) = make_leaf_node(1);
let (leaf2, node2) = make_leaf_node(2);
@@ -169,7 +171,6 @@ mod tests {
}
#[test]
- #[cfg(feature = "alloc")]
fn tx_merkle_node_duplicate_leaves() {
let leaf = Txid::from_byte_array([3; 32]);
// Duplicate transaction list should be rejected (CVE 2012‑2459).
@@ -178,13 +179,11 @@ mod tests {
}
#[test]
- #[cfg(feature = "alloc")]
fn tx_merkle_node_empty() {
assert!(TxMerkleNode::calculate_root([].into_iter()).is_none(), "Empty iterator should return None");
}
#[test]
- #[cfg(feature = "alloc")]
fn tx_merkle_node_2n_minus_1_unbalanced_tree() {
// Test a tree with 2^n - 1 unique nodes and at least 3 layers deep.
let (leaf1, node1) = make_leaf_node(1);
@@ -243,7 +242,30 @@ mod tests {
}
#[test]
- #[cfg(feature = "alloc")]
+ fn tx_merkle_node_oversize_tree() {
+ // Confirm that with no-alloc, we return None for iter length >= 32768
+ let root = TxMerkleNode::calculate_root((0..32768u32).map(|i| {
+ let mut buf = [0u8; 32];
+ buf[..4].copy_from_slice(&i.to_le_bytes());
+ Txid::from_byte_array(buf)
+ }));
+
+ // We just want to confirm that we return None at the 32768 element boundary.
+ #[cfg(feature = "alloc")]
+ assert_ne!(root, None);
+ #[cfg(not(feature = "alloc"))]
+ assert_eq!(root, None);
+
+ // Check just under the boundary
+ let root = TxMerkleNode::calculate_root((0..32767u32).map(|i| {
+ let mut buf = [0u8; 32];
+ buf[..4].copy_from_slice(&i.to_le_bytes());
+ Txid::from_byte_array(buf)
+ }));
+ assert_ne!(root, None);
+ }
+
+ #[test]
fn witness_merkle_node_single_leaf() {
let leaf = Wtxid::from_byte_array([1; 32]);
let root = WitnessMerkleNode::calculate_root([leaf].into_iter());
@@ -253,7 +275,6 @@ mod tests {
}
#[test]
- #[cfg(feature = "alloc")]
fn witness_merkle_node_duplicate_leaves() {
let leaf = Wtxid::from_byte_array([2; 32]);
let root = WitnessMerkleNode::calculate_root([leaf, leaf].into_iter());
diff --git a/primitives/src/transaction.rs b/primitives/src/transaction.rs
index ffee0263..370c60fd 100644
--- a/primitives/src/transaction.rs
+++ b/primitives/src/transaction.rs
@@ -250,12 +250,9 @@ impl From<&Transaction> for Wtxid {
}
/// Trait that abstracts over a transaction identifier i.e., `Txid` and `Wtxid`.
-#[cfg(feature = "alloc")]
pub(crate) trait TxIdentifier: AsRef<[u8]> {}
-#[cfg(feature = "alloc")]
impl TxIdentifier for Txid {}
-#[cfg(feature = "alloc")]
impl TxIdentifier for Wtxid {}
// Duplicated in `bitcoin`.
Why this scored 18/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.