-----|-------------------------|------------------------|---------------------------------|
| Naive Full Hash | ~1,000,000 hashes | O(n) | ~32 MB | None |
| Standard Merkle Tree | ~20 hashes | O(log n) | ~640 Bytes | Static/Manual tracing |
| Interactive Merkle Visualizer | ~20 hashes | O(log n) | ~640 Bytes | Real-time diff, live proof replay, level inspection |
Key Findings:
- The sweet spot lies in preserving all intermediate levels during construction to enable O(log n) proof extraction while maintaining a lightweight DOM/SVG rendering layer (~150 lines).
- Live diffing between baseline and modified trees immediately surfaces propagation failures, reducing debugging time from hours to seconds.
- Protocol-strict byte concatenation and explicit
side tracking ensure 100% cross-implementation verification compatibility.
Core Solution
The implementation is split into a pure-logic core (merkle.js) and a DOM/SVG rendering layer. The core exports four verifier functions and three UI driver functions, all built around crypto.subtle.digest("SHA-256", β¦) for consistent browser/Node 18+ execution.
1. Hashing Core
const enc = new TextEncoder();
export async function sha256Hex(bytes) {
const buf = bytes instanceof Uint8Array ? bytes : enc.encode(bytes);
const digest = await crypto.subtle.digest("SHA-256", buf);
return bytesToHex(new Uint8Array(digest));
}
export async function hashLeaf(text) {
return sha256Hex(text);
}
export async function hashPair(leftHex, rightHex) {
const buf = new Uint8Array(64);
buf.set(hexToBytes(leftHex), 0);
buf.set(hexToBytes(rightHex), 32);
return sha256Hex(buf);
}
2. Tree Construction
export async function buildTree(leaves) {
if (!leaves.length) return { levels: [[]], root: null };
const leafHashes = await Promise.all(leaves.map(hashLeaf));
const levels = [leafHashes];
while (levels[levels.length - 1].length > 1) {
const cur = levels[levels.length - 1];
const next = [];
for (let i = 0; i < cur.length; i += 2) {
const left = cur[i];
const right = i + 1 < cur.length ? cur[i + 1] : cur[i]; // duplicate odd
next.push(await hashPair(left, right));
}
levels.push(next);
}
return { levels, root: levels[levels.length - 1][0] };
}
3. Proof Generation
export function getProof(levels, index) {
if (!levels.length || !levels[0].length) return [];
const proof = [];
let i = index;
for (let lvl = 0; lvl < levels.length - 1; lvl++) {
const layer = levels[lvl];
const isRight = i % 2 === 1;
const sibIdx = isRight ? i - 1 : i + 1;
const sibling = sibIdx < layer.length ? layer[sibIdx] : layer[i]; // odd-end self-dup
proof.push({ hash: sibling, side: isRight ? "left" : "right" });
i = Math.floor(i / 2);
}
return proof;
}
4. Proof Verification
export async function verifyProof(leafText, proof, expectedRoot) {
let running = await hashLeaf(leafText);
for (const step of proof) {
if (step.side === "left") {
running = await hashPair(step.hash, running);
} else {
running = await hashPair(running, step.hash);
}
}
return { ok: running === expectedRoot, computedRoot: running };
}
5. Visualization Diffing
export function diffLevels(a, b) {
const changed =
Pitfall Guide
- Hex String vs. Byte Concatenation in
hashPair: Concatenating hex representations (sha256("ab" + "cd")) produces a completely different root than concatenating raw bytes (sha256(bytes_of("ab") || bytes_of("cd"))). Verifiers compiled against one convention will silently reject proofs generated with the other. Always decode to Uint8Array before pairing.
- Ignoring the
side Parameter: The order of concatenation during verification is protocol-critical. If the running hash is the left child, you must compute hashPair(running, sibling). If it's the right child, hashPair(sibling, running). Omitting or misassigning side causes deterministic verification failure without explicit error messages.
- Odd-Node Convention Mismatch: Different ecosystems handle odd-length levels differently. Bitcoin duplicates the last node (
H(H(L2)||H(L2))), while Certificate Transparency promotes it unchanged. Ethereum's Patricia trie uses a fundamentally different structure. Pick one convention, document it explicitly, and enforce it across all proof generation and verification routines.
- Premature Level Discarding: Production optimizers often drop intermediate levels after computing the root to save memory. This breaks proof generation and visualization. Maintain the full
levels array during construction, or implement a separate proof-extraction pass that reconstructs the path on-demand.
- Synchronous Assumptions Around
crypto.subtle: crypto.subtle.digest is strictly asynchronous in both browsers and Node 18+. Wrapping it in synchronous logic or forgetting await in tree construction/proof generation introduces race conditions, corrupted state, and non-deterministic root hashes.
Deliverables
- π Merkle Tree Visualizer Blueprint: Complete architecture diagram detailing the data flow between
merkle.js (pure logic), the DOM/SVG rendering layer, and the proof verification engine. Includes state management patterns for baseline vs. modified tree tracking.
- β
Protocol Compliance Checklist: 12-point verification checklist covering byte concatenation standards,
side ordering validation, odd-node convention alignment, async handling, and cross-environment hash consistency (Browser vs. Node 18+).
- βοΈ Configuration & Testing Templates: Ready-to-use
merkle.js module scaffold, node --test harness configuration, SVG binding boilerplate, and 17 unit test cases (including explicit byte-concatenation validation and proof replay assertions).