Skip to content
LogoLogo

no_std

merkle-core, the crate with the traits, proof types, and errors, supports no_std targets with a global allocator. That lets embedded devices, enclaves, and WebAssembly modules share proof types with your servers and implement their own verifiers.

Feature flags

FeatureDefaultPurpose
stdYesStandard-library integration, including std::error::Error for MerkleError. Implies alloc.
allocVia stdHeap-backed types (Vec, String) for no_std builds. Required.

Disabling both is a compile error: proofs need Vec.

Cargo.toml
[dependencies]
merkle-core = { version = "0.5.0", default-features = false, features = ["alloc"] }

Check a bare-metal build from the repository:

rustup target add thumbv7em-none-eabihf
cargo check -p merkle-core \
  --no-default-features \
  --features alloc \
  --target thumbv7em-none-eabihf

What is available without std

Everything in merkle-core: HashFunction, MerkleTree, ProofVerifier, Serializable (bincode), MerkleProof, ProofNode, ProofSide, LeafIndex, NodeIndex, TreeMetadata, and MerkleError (without the std::error::Error impl).

A no_std verifier

This verifier uses only merkle-core types and core, so it compiles in a #![no_std] crate with alloc. It follows the same algorithm as BinaryMerkleTree::verify. Shown here with a host-side main that cross-checks it against the real tree:

use merkle_core::{
    traits::HashFunction,
    types::{MerkleProof, ProofSide},
};
 
/// Stateless binary-proof check using only merkle-core.
pub fn verify_binary<H: HashFunction>(
    expected_root: &H::Digest,
    leaf_data: &[u8],
    proof: &MerkleProof<H::Digest>,
) -> bool {
    if leaf_data.is_empty() || proof.leaf_index.value() >= proof.leaf_count {
        return false;
    }
    let Some(capacity) = proof.leaf_count.checked_next_power_of_two() else {
        return false;
    };
    if proof.path.len() != capacity.ilog2() as usize {
        return false;
    }
 
    let mut current = H::hash(leaf_data);
    let mut offset = proof.leaf_index.value();
    for node in &proof.path {
        let expected = if offset % 2 == 0 { ProofSide::Right } else { ProofSide::Left };
        if node.side != expected {
            return false;
        }
        current = match node.side {
            ProofSide::Left => H::hash_nodes(&node.hash, &current),
            ProofSide::Right => H::hash_nodes(&current, &node.hash),
        };
        offset /= 2;
    }
    current == *expected_root
}
 
fn main() -> Result<(), merkle_core::error::MerkleError> {
    use merkle_core::types::LeafIndex;
    use merkle_variants::BinaryMerkleTree;
    use merkleforge_hash::Sha256;
 
    let tree = BinaryMerkleTree::<Sha256>::from_leaves(["a", "b", "c"])?;
    let root = tree.root().expect("non-empty");
    let proof = tree.generate_proof(LeafIndex(2))?;
    assert!(verify_binary::<Sha256>(root, b"c", &proof));
    assert!(!verify_binary::<Sha256>(root, b"x", &proof));
    Ok(())
}

On the device, provide a HashFunction implementation backed by a no_std hash crate (for example sha2 with default-features = false), following the custom adapter guide.