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
| Feature | Default | Purpose |
|---|---|---|
std | Yes | Standard-library integration, including std::error::Error for MerkleError. Implies alloc. |
alloc | Via std | Heap-backed types (Vec, String) for no_std builds. Required. |
Disabling both is a compile error: proofs need Vec.
[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-eabihfWhat 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, ¤t),
ProofSide::Right => H::hash_nodes(¤t, &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.