merkleforge-hash
Pluggable cryptographic hash adapters for
MerkleForge— SHA-256, Keccak-256, and BLAKE3.
⚠️ Research Software — Not Production Ready
MerkleForge is a final-year academic research project. This crate has not been independently security-audited. Use at your own risk.
merkleforge-hash provides the three hash function adapters used by
MerkleForge. Each one implements the
HashFunction
trait from merkle-core, so any tree variant in merkle-variants can
be driven by any adapter — swapping algorithms is a one-type-parameter change
with zero runtime overhead.
Contents
- Installation
- Quick start
- Adapters
- Choosing an adapter
- Benchmark comparison
- Domain separation
- Implementing a custom
HashFunction - Safety
- License
Installation
[]
= "0.1.1"
= "0.1.1"
All three adapters are compiled by default. There are no feature flags — the
upstream crates (sha2, tiny-keccak, blake3) are small and compile
quickly.
Quick start
use ;
// Hash a leaf pre-image
let sha_digest = hash;
let keccak_digest = hash;
let blake_digest = hash;
// All three produce 32-byte digests
assert_eq!;
assert_eq!;
assert_eq!;
// The digests are different — three distinct algorithms
assert_ne!;
assert_ne!;
// Hash two child nodes together to form a parent
let parent = hash_nodes;
assert_eq!;
Swapping the hash function that drives a tree:
// Before — SHA-256
let mut tree = new;
// After — BLAKE3, same API, zero other changes
let mut tree = new;
Adapters
Sha256
use ;
let digest = hash;
// digest: [u8; 32]
let parent = hash_nodes;
// parent: [u8; 32]
println!; // "SHA-256"
println!; // 32
Upstream crate: sha2
Hardware acceleration: The sha2 crate detects Intel SHA Extensions and
AVX2 at compile time and uses them automatically. On supported x86-64 CPUs
this yields roughly a 50% throughput improvement over the software path
(Drake, 2019).
Domain separation:
- Leaf hash:
SHA-256(0x00 || data) - Internal node hash:
SHA-256(0x01 || left || right)
empty() sentinel: Pre-computed as SHA-256(0x00) — avoids a runtime
hash call every time the tree needs an empty-slot placeholder.
empty = 6e340b9cffb37a989ca544e6bb780a2c78901d3fb3378768501a30617afa01d
Keccak256
use ;
let digest = hash;
// digest: [u8; 32] — identical to web3.utils.keccak256("transaction data")
println!; // "Keccak-256"
Upstream crate: tiny-keccak
with the keccak feature.
Important: Keccak-256 is not the same as NIST SHA-3. They use different padding. If you need to produce digests that match Ethereum tooling (
web3.utils.keccak256, Solidity'skeccak256(),ethers.utils.keccak256), useKeccak256. Using any SHA-3 crate will produce different output.
When to use: Any tree that must produce state roots verifiable by
Ethereum tooling — most importantly the MerklePatriciaTrie variant.
Domain separation:
- Leaf hash:
Keccak-256(0x00 || data) - Internal node hash:
Keccak-256(0x01 || left || right)
Known vector:
Keccak-256("") = c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470
Blake3
use ;
let digest = hash;
// digest: [u8; 32]
println!; // "BLAKE3"
Upstream crate: blake3
Domain separation: BLAKE3 has native support for context strings via
blake3::derive_key. This is cryptographically cleaner and more efficient
than prepending a prefix byte — no extra memory allocation, no length
extension risk.
// Internally, Blake3 uses:
const LEAF_CONTEXT: &str = "MerkleForge 2026 leaf v1";
const NODE_CONTEXT: &str = "MerkleForge 2026 internal-node v1";
// Leaf: blake3::derive_key("MerkleForge 2026 leaf v1", data)
// Node: blake3::derive_key("MerkleForge 2026 internal-node v1", left || right)
Because the context strings are distinct and fixed, leaf and internal-node digests are guaranteed to never collide regardless of input.
When to use: Throughput-sensitive workloads where Ethereum compatibility is not required — background indexers, proof batch generation, high-frequency state updates.
Choosing an adapter
Sha256 |
Keccak256 |
Blake3 |
|
|---|---|---|---|
| Algorithm | SHA-256 | Keccak-256 | BLAKE3 |
| Digest size | 32 bytes | 32 bytes | 32 bytes |
| Ethereum compatible | ✗ | ✅ Required for MPT | ✗ |
| Hardware acceleration | ✅ SHA/AVX2 extensions | ✗ software only | ✅ SIMD, multi-core |
| Software throughput | Moderate | Moderate | Fastest |
| Domain separation method | 0x00/0x01 prefix bytes |
0x00/0x01 prefix bytes |
derive_key context strings |
| Best for | Production x86-64, Bitcoin-style SPV | Any Ethereum-compatible tree | Maximum throughput, non-Ethereum |
Decision guide:
- Building a
MerklePatriciaTriewhose roots need to match Ethereum →Keccak256, no choice. - Running on a modern server with SHA Extensions or on ARM with SHA
instructions →
Sha256will match or beat BLAKE3 on hardware paths. - Building a binary or sparse tree where raw throughput matters and Ethereum
compatibility is not required →
Blake3. - Not sure? Start with
Sha256— it's the most battle-tested, has the widest hardware support, and will be easy to reason about in security reviews.
Benchmark comparison
Note: The numbers below are from published literature (AITCS, 2024; Drake, 2019). The
merkle-benchcrate will produce project-specific numbers on standardised hardware in Phase 5 of the implementation roadmap. Those results will replace this table.
Single-block throughput (software path, ~64 bytes)
| Algorithm | Approx. latency | Notes |
|---|---|---|
| BLAKE3 | ~100–120 ns | Fastest on software path across all input sizes |
| SHA-256 (software) | ~250–300 ns | ~2× slower than BLAKE3 without hardware extensions |
| SHA-256 (hardware) | ~120–150 ns | SHA Extensions close the gap significantly |
| Keccak-256 | ~300–400 ns | No hardware acceleration; consistently slowest |
Sustained throughput (large buffers, MB/s)
| Algorithm | Typical throughput | Scales across cores? |
|---|---|---|
| BLAKE3 | 1–4 GB/s | ✅ Internal tree parallelism |
| SHA-256 (hardware) | 500 MB/s–1 GB/s | ✗ Single-core |
| SHA-256 (software) | 200–400 MB/s | ✗ Single-core |
| Keccak-256 | 150–300 MB/s | ✗ Single-core |
These gaps directly determine tree construction speed. A tree with 1,000,000 leaves requires roughly 2,000,000 hash calls (one per leaf plus one per internal node). At 64 bytes per input:
| Algorithm | Estimated construction time (1M leaves) |
|---|---|
| BLAKE3 | ~200–400 ms |
| SHA-256 (hardware) | ~300–600 ms |
| SHA-256 (software) | ~500 ms–1 s |
| Keccak-256 | ~600 ms–1.2 s |
The merkle-bench Criterion suite (cargo bench --bench hash_throughput)
measures your specific hardware so you can make an informed decision rather
than relying on generalised figures.
Domain separation
All three adapters enforce domain separation between leaf hashes and internal-node hashes. This prevents a class of second-preimage attacks where an attacker constructs a proof by substituting an internal node for a leaf.
The attack without domain separation:
Suppose H(A || B) == H(leaf_data).
An attacker could present [A, B] as a "leaf" and fool a verifier
that checks only the final root, not whether the proof path is valid.
How each adapter prevents it:
| Adapter | Leaf | Internal node |
|---|---|---|
Sha256 |
SHA-256(0x00 || data) |
SHA-256(0x01 || left || right) |
Keccak256 |
Keccak-256(0x00 || data) |
Keccak-256(0x01 || left || right) |
Blake3 |
derive_key("MerkleForge 2026 leaf v1", data) |
derive_key("MerkleForge 2026 internal-node v1", left || right) |
The 0x00/0x01 byte prefix used by Sha256 and Keccak256 follows
RFC 6962 (Certificate Transparency). BLAKE3's derive_key mode is
equivalent but uses full context strings instead of a single byte, which is
both more readable and more collision-resistant at the domain boundary.
If you implement a custom HashFunction, you must apply the same
separation. The ProofVerifier in merkleforge-core assumes it.
Implementing a custom HashFunction
If none of the three adapters fit your use case — say, you need BLAKE2b for a specific protocol, or a truncated digest for a constrained environment — implement the trait directly in your own crate.
Minimal implementation
use HashFunction;
;
Full example — BLAKE2b-256
use HashFunction;
use ;
use U32;
;
Once implemented, Blake2b256 drops straight into any merkle-variants tree:
let mut tree = new;
tree.insert?;
Checklist before shipping a custom adapter
-
hashapplies a distinct domain prefix or context to leaf inputs -
hash_nodesapplies a different domain prefix or context to node inputs -
hash_nodesis non-commutative —H(A, B) ≠ H(B, A)for most inputs -
DigestimplementsAsRef<[u8]> + Clone + Debug + PartialEq + Eq + Send + Sync + 'static - The implementation is deterministic — same input always produces same output
-
digest_size()returns the correct byte length ofDigest - If you override
empty(), it equalshash(&[])or an equally valid sentinel
Using a non-32-byte digest
The Digest associated type is not constrained to [u8; 32]. You can use
any fixed-size array, or a custom newtype, as long as it satisfies the bounds:
;
Warning: Truncating a digest reduces collision resistance. A 128-bit digest has a birthday-bound collision probability of ~2⁻⁶⁴. Use a full-width digest for any production security context.
Safety
#[forbid(unsafe_code)] is set at the crate root. merkleforge-hash
contains no unsafe blocks. All three upstream crates (sha2, tiny-keccak,
blake3) are widely audited and used in production blockchain infrastructure.
License
Licensed under either of MIT or Apache-2.0 at your option.