Below are four data blocks. We hash each one, then hash the pairs up to a single root. Edit any block and watch the root change — that is tamper-evidence in action. Then pick a leaf and press Verify to see its proof path: the few sibling hashes that recompute the root.
<p class="hint">Edit any of the four data blocks. Each leaf is hashed, then pairs are hashed up to a single <b>root</b>. Change one byte and the root changes — that is tamper-evidence. Pick a leaf and press <b>Verify</b> to see its proof path: the few sibling hashes that recompute the root.</p>
<div class="leaves" id="leaves"></div>
<div class="tree" id="tree"></div>
<div class="root-row">
<span class="root-label">Merkle root</span>
<code class="root" id="root">--------</code>
</div>
<div class="status" id="status">Edit a block, or verify a leaf.</div>
<div class="btns">
<button id="verify" type="button">Verify selected leaf</button>
<button id="reset" type="button" class="ghost">Reset blocks</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .8rem; line-height: 1.45; }
.leaves { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; margin-bottom: .8rem; }
.leaf { display: flex; flex-direction: column; gap: 4px; }
.leaf input { font: 600 13px ui-monospace, monospace; padding: .4rem; border: 1px solid #adb1b8;
border-radius: 7px; width: 100%; text-align: center; }
.leaf input:focus { outline: 2px solid #1d3557; border-color: #1d3557; }
.leaf .lh { font: 600 11px ui-monospace, monospace; color: #1d3557; background: #e8eef3;
border: 1px solid #cdd9e3; border-radius: 6px; padding: .25rem; text-align: center; cursor: pointer; }
.leaf .lh.sel { background: #1d3557; color: #fff; border-color: #1d3557; }
.leaf .lh.path { background: #ffe6a3; border-color: #e0b84d; color: #6a4e00; }
.tree { display: flex; flex-direction: column; align-items: center; gap: 6px; margin-bottom: .6rem; }
.level { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }
.node { font: 600 11px ui-monospace, monospace; padding: .3rem .5rem; border-radius: 6px;
background: #eef1f4; border: 1px solid #cdd5dd; color: #2a3b4d; }
.node.path { background: #ffe6a3; border-color: #e0b84d; color: #6a4e00; }
.root-row { display: flex; align-items: center; gap: .6rem; margin: .3rem 0 .6rem; flex-wrap: wrap; }
.root-label { font-weight: 700; color: #1d3557; }
.root { font: 700 15px ui-monospace, monospace; background: #1d3557; color: #fff;
padding: .35rem .6rem; border-radius: 7px; letter-spacing: 1px; }
.status { font-size: .95rem; font-weight: 600; margin: .4rem 0; min-height: 1.4em; line-height: 1.4; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// A Merkle tree hashes data in pairs up to a single root. Editing any leaf flips the root (tamper-evidence). A membership proof reveals only the sibling hash at each level — log2(n) hashes — and re-hashing along that path must land on the published root. We use a tiny deterministic hash (FNV-1a, 8 hex chars) so the demo runs with no imports; real systems use SHA-256.
const DEFAULTS = ['alice→bob', 'bob→carol', 'carol→dave', 'dave→alice'];
let blocks = DEFAULTS.slice();
let selected = 0;
function h(str){ // FNV-1a 32-bit -> 8 hex chars
let x = 0x811c9dc5;
for(let i=0;i<str.length;i++){ x ^= str.charCodeAt(i); x = Math.imul(x, 0x01000193) >>> 0; }
return ('0000000'+x.toString(16)).slice(-8);
}
// Build the tree: return array of levels, level[0]=leaf hashes, last level=[root].
function buildLevels(data){
let level = data.map(h);
const levels = [level];
while(level.length > 1){
const next = [];
for(let i=0;i<level.length;i+=2){
const left = level[i];
const right = (i+1 < level.length) ? level[i+1] : level[i]; // duplicate last if odd
next.push(h(left + right));
}
levels.push(next);
level = next;
}
return levels;
}
function root(data){ const L = buildLevels(data); return L[L.length-1][0]; }
// Proof for leaf index `idx`: list of {hash, isRight} siblings bottom-up.
function proof(data, idx){
const levels = buildLevels(data);
const pf = [];
let i = idx;
for(let l=0; l<levels.length-1; l++){
const level = levels[l];
const isRightSibling = (i % 2 === 0); // we are left -> sibling is to the right
let sib = isRightSibling ? i+1 : i-1;
if(sib >= level.length) sib = i; // odd node duplicated
pf.push({ hash: level[sib], isRight: isRightSibling });
i = Math.floor(i/2);
}
return pf;
}
// Recompute root from a leaf value + its proof path.
function rootFromProof(leafValue, pf){
let acc = h(leafValue);
for(const step of pf){
acc = step.isRight ? h(acc + step.hash) : h(step.hash + acc);
}
return acc;
}
function setStatus(t, cls){ const s=document.getElementById('status'); s.textContent=t; s.className='status '+(cls||''); }
function render(pathSet){
pathSet = pathSet || new Set();
const leaves = buildLevels(blocks)[0];
// leaf editors
const lc = document.getElementById('leaves'); lc.innerHTML='';
blocks.forEach((b,i)=>{
const wrap = document.createElement('div'); wrap.className='leaf';
const inp = document.createElement('input'); inp.value=b; inp.setAttribute('aria-label',fmt('block %i%', { i: i }));
inp.addEventListener('input', ()=>{ blocks[i]=inp.value; render(); setStatus('Block edited — root recomputed.',''); });
const lh = document.createElement('div');
lh.className='lh'+(i===selected?' sel':'')+(pathSet.has('leaf'+i)?' path':'');
lh.textContent=leaves[i];
lh.title=fmt('click to select leaf %i%', { i: i });
lh.addEventListener('click', ()=>{ selected=i; render(); });
wrap.appendChild(inp); wrap.appendChild(lh); lc.appendChild(wrap);
});
// internal levels (skip leaves; skip root row, shown separately)
const levels = buildLevels(blocks);
const tc = document.getElementById('tree'); tc.innerHTML='';
for(let l=1; l<levels.length-1; l++){
const row = document.createElement('div'); row.className='level';
levels[l].forEach((hh,j)=>{
const n=document.createElement('div');
n.className='node'+(pathSet.has(l+':'+j)?' path':'');
n.textContent=hh; row.appendChild(n);
});
tc.appendChild(row);
}
document.getElementById('root').textContent = root(blocks);
}
function verify(){
const r = root(blocks);
const pf = proof(blocks, selected);
const recomputed = rootFromProof(blocks[selected], pf);
// mark the proof path for highlighting
const pathSet = new Set(['leaf'+selected]);
let i = selected;
const levels = buildLevels(blocks);
for(let l=0;l<levels.length-1;l++){
const isRight = (i%2===0); let sib = isRight ? i+1 : i-1;
if(sib>=levels[l].length) sib=i;
if(l>0) pathSet.add(l+':'+sib);
i=Math.floor(i/2);
}
render(pathSet);
if(recomputed === r){
setStatus(fmt('✓ Verified leaf %selected% with %length% sibling hash%v% — recomputed root %recomputed% matches.', { selected: selected, length: pf.length, v: pf.length===1?'':'es', recomputed: recomputed }),'ok');
console.log('proof', pf, 'root', r);
} else {
setStatus('✗ Proof does not reach the root.','bad');
}
}
document.getElementById('verify').addEventListener('click', verify);
document.getElementById('reset').addEventListener('click', ()=>{ blocks=DEFAULTS.slice(); selected=0; render(); setStatus('Blocks reset.',''); });
render();
<p class="hint">Edit any of the four data blocks. Each leaf is hashed, then pairs are hashed up to a single <b>root</b>. Change one byte and the root changes — that is tamper-evidence. Pick a leaf and press <b>Verify</b> to see its proof path: the few sibling hashes that recompute the root.</p>
<div class="leaves" id="leaves"></div>
<div class="tree" id="tree"></div>
<div class="root-row">
<span class="root-label">Merkle root</span>
<code class="root" id="root">--------</code>
</div>
<div class="status" id="status">Edit a block, or verify a leaf.</div>
<div class="btns">
<button id="verify" type="button">Verify selected leaf</button>
<button id="reset" type="button" class="ghost">Reset blocks</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .8rem; line-height: 1.45; }
.leaves { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; margin-bottom: .8rem; }
.leaf { display: flex; flex-direction: column; gap: 4px; }
.leaf input { font: 600 13px ui-monospace, monospace; padding: .4rem; border: 1px solid #adb1b8;
border-radius: 7px; width: 100%; text-align: center; }
.leaf input:focus { outline: 2px solid #1d3557; border-color: #1d3557; }
.leaf .lh { font: 600 11px ui-monospace, monospace; color: #1d3557; background: #e8eef3;
border: 1px solid #cdd9e3; border-radius: 6px; padding: .25rem; text-align: center; cursor: pointer; }
.leaf .lh.sel { background: #1d3557; color: #fff; border-color: #1d3557; }
.leaf .lh.path { background: #ffe6a3; border-color: #e0b84d; color: #6a4e00; }
.tree { display: flex; flex-direction: column; align-items: center; gap: 6px; margin-bottom: .6rem; }
.level { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }
.node { font: 600 11px ui-monospace, monospace; padding: .3rem .5rem; border-radius: 6px;
background: #eef1f4; border: 1px solid #cdd5dd; color: #2a3b4d; }
.node.path { background: #ffe6a3; border-color: #e0b84d; color: #6a4e00; }
.root-row { display: flex; align-items: center; gap: .6rem; margin: .3rem 0 .6rem; flex-wrap: wrap; }
.root-label { font-weight: 700; color: #1d3557; }
.root { font: 700 15px ui-monospace, monospace; background: #1d3557; color: #fff;
padding: .35rem .6rem; border-radius: 7px; letter-spacing: 1px; }
.status { font-size: .95rem; font-weight: 600; margin: .4rem 0; min-height: 1.4em; line-height: 1.4; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// A Merkle tree hashes data in pairs up to a single root. Editing any leaf flips the root (tamper-evidence). A membership proof reveals only the sibling hash at each level — log2(n) hashes — and re-hashing along that path must land on the published root. We use a tiny deterministic hash (FNV-1a, 8 hex chars) so the demo runs with no imports; real systems use SHA-256.
const DEFAULTS = ['alice→bob', 'bob→carol', 'carol→dave', 'dave→alice'];
let blocks = DEFAULTS.slice();
let selected = 0;
function h(str){ // FNV-1a 32-bit -> 8 hex chars
let x = 0x811c9dc5;
for(let i=0;i<str.length;i++){ x ^= str.charCodeAt(i); x = Math.imul(x, 0x01000193) >>> 0; }
return ('0000000'+x.toString(16)).slice(-8);
}
// Build the tree: return array of levels, level[0]=leaf hashes, last level=[root].
function buildLevels(data){
let level = data.map(h);
const levels = [level];
while(level.length > 1){
const next = [];
for(let i=0;i<level.length;i+=2){
const left = level[i];
const right = (i+1 < level.length) ? level[i+1] : level[i]; // duplicate last if odd
next.push(h(left + right));
}
levels.push(next);
level = next;
}
return levels;
}
function root(data){ const L = buildLevels(data); return L[L.length-1][0]; }
// Proof for leaf index `idx`: list of {hash, isRight} siblings bottom-up.
function proof(data, idx){
const levels = buildLevels(data);
const pf = [];
let i = idx;
for(let l=0; l<levels.length-1; l++){
const level = levels[l];
const isRightSibling = (i % 2 === 0); // we are left -> sibling is to the right
let sib = isRightSibling ? i+1 : i-1;
if(sib >= level.length) sib = i; // odd node duplicated
pf.push({ hash: level[sib], isRight: isRightSibling });
i = Math.floor(i/2);
}
return pf;
}
// Recompute root from a leaf value + its proof path.
function rootFromProof(leafValue, pf){
let acc = h(leafValue);
for(const step of pf){
acc = step.isRight ? h(acc + step.hash) : h(step.hash + acc);
}
return acc;
}
function setStatus(t, cls){ const s=document.getElementById('status'); s.textContent=t; s.className='status '+(cls||''); }
function render(pathSet){
pathSet = pathSet || new Set();
const leaves = buildLevels(blocks)[0];
// leaf editors
const lc = document.getElementById('leaves'); lc.innerHTML='';
blocks.forEach((b,i)=>{
const wrap = document.createElement('div'); wrap.className='leaf';
const inp = document.createElement('input'); inp.value=b; inp.setAttribute('aria-label',fmt('block %i%', { i: i }));
inp.addEventListener('input', ()=>{ blocks[i]=inp.value; render(); setStatus('Block edited — root recomputed.',''); });
const lh = document.createElement('div');
lh.className='lh'+(i===selected?' sel':'')+(pathSet.has('leaf'+i)?' path':'');
lh.textContent=leaves[i];
lh.title=fmt('click to select leaf %i%', { i: i });
lh.addEventListener('click', ()=>{ selected=i; render(); });
wrap.appendChild(inp); wrap.appendChild(lh); lc.appendChild(wrap);
});
// internal levels (skip leaves; skip root row, shown separately)
const levels = buildLevels(blocks);
const tc = document.getElementById('tree'); tc.innerHTML='';
for(let l=1; l<levels.length-1; l++){
const row = document.createElement('div'); row.className='level';
levels[l].forEach((hh,j)=>{
const n=document.createElement('div');
n.className='node'+(pathSet.has(l+':'+j)?' path':'');
n.textContent=hh; row.appendChild(n);
});
tc.appendChild(row);
}
document.getElementById('root').textContent = root(blocks);
}
function verify(){
const r = root(blocks);
const pf = proof(blocks, selected);
const recomputed = rootFromProof(blocks[selected], pf);
// mark the proof path for highlighting
const pathSet = new Set(['leaf'+selected]);
let i = selected;
const levels = buildLevels(blocks);
for(let l=0;l<levels.length-1;l++){
const isRight = (i%2===0); let sib = isRight ? i+1 : i-1;
if(sib>=levels[l].length) sib=i;
if(l>0) pathSet.add(l+':'+sib);
i=Math.floor(i/2);
}
render(pathSet);
if(recomputed === r){
setStatus(fmt('✓ Verified leaf %selected% with %length% sibling hash%v% — recomputed root %recomputed% matches.', { selected: selected, length: pf.length, v: pf.length===1?'':'es', recomputed: recomputed }),'ok');
console.log('proof', pf, 'root', r);
} else {
setStatus('✗ Proof does not reach the root.','bad');
}
}
document.getElementById('verify').addEventListener('click', verify);
document.getElementById('reset').addEventListener('click', ()=>{ blocks=DEFAULTS.slice(); selected=0; render(); setStatus('Blocks reset.',''); });
render();
Notice the asymmetry that makes Merkle trees powerful. To distrust the whole dataset, you only need to compare one number — the root. To prove a single leaf belongs, you don't need the other million leaves; you need only the log(n) hashes along its path. With four leaves that's two hashes; with a billion leaves it is about thirty.
Comments
Loading comments...