Each HotStuff round passes a block through three consecutive voting phases: Prepare, Pre-Commit, and Commit. A block is finalized only once all three phases succeed for it — forming a chain of three consecutive certified blocks (the three-chain rule).
<!-- HotStuff pipeline demo — three-phase BFT consensus visualization -->
<div class="hs-wrap">
<p class="hint">Each round, a new block enters at <b>Prepare</b> and every active block advances one phase. A block is <b>committed</b> (finalized) only after three consecutive certified phases. Click <b>Next round</b> to step through the pipeline.</p>
<div id="pipeline" class="pipeline"></div>
<div class="controls">
<button id="btn-next" type="button">Next round →</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div id="status" class="status"></div>
</div>
/* Styles for the HotStuff pipeline demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hs-wrap { padding: .5rem; max-width: 680px; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.5; }
/* Pipeline grid: rows = phases, columns = blocks */
.pipeline { display: flex; flex-direction: column; gap: 6px; margin-bottom: .7rem; }
.row { display: flex; align-items: center; gap: 0; }
.row-label { width: 80px; font-size: .78rem; font-weight: 700; color: #555; flex-shrink: 0; }
/* Individual pipeline cell styling */
.cell {
width: 68px; height: 52px; border-radius: 8px; border: 1.5px solid #cdd9e3;
display: flex; flex-direction: column; align-items: center; justify-content: center;
font-size: .7rem; font-weight: 600; transition: background .25s, border-color .25s;
background: #f0f4f8; color: #667; cursor: default; flex-shrink: 0; position: relative;
}
.cell + .cell { margin-left: 4px; }
.cell .blk { font-size: .85rem; font-weight: 800; color: #1d3557; }
.cell .phase { font-size: .62rem; text-transform: uppercase; letter-spacing: .04em; margin-top: 2px; }
/* Color-coding by phase: blue=prepare, yellow=precommit, green=commit/done */
.cell.prepare { background: #dbeafe; border-color: #3b82f6; color: #1d3557; }
.cell.precommit { background: #fef9c3; border-color: #eab308; color: #713f12; }
.cell.commit { background: #dcfce7; border-color: #22c55e; color: #14532d; }
.cell.done { background: #bbf7d0; border-color: #16a34a; color: #14532d; opacity: .75; }
.cell.empty { background: #f8f9fa; border-color: #e2e8f0; color: #aaa; }
/* Arrow between cells and status message */
.arrow { font-size: 1.1rem; color: #94a3b8; padding: 0 1px; flex-shrink: 0; }
.status { font-size: .92rem; font-weight: 600; min-height: 1.3em; color: #1d3557; }
.status.commit-msg { color: #15803d; }
.controls { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .5rem; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1.5px 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] ?? ''));
// HotStuff pipelined BFT consensus demo.
// Three phases: Prepare, Pre-Commit, Commit. A block is committed
// once all three phases complete (the three-chain rule).
// Internal phase identifiers (not shown to user)
const PHASES = ['prepare', 'precommit', 'commit'];
const PHASE_LABELS = { prepare: 'Prepare', precommit: 'Pre-Commit', commit: 'Commit' };
// Pipeline configuration: total blocks and lane count
const TOTAL_BLOCKS = 5;
const ROWS = 3; // one lane per phase
// Global simulation state
// step = number of "next block" clicks fired so far
let step = 0;
// Phase index per block: -1=not started, 0=prepare, 1=precommit, 2=commit, 3=done
// pipelinePhase[i] = index into PHASES for block i (0=prepare,1=precommit,2=commit,3=done)
// -1 = not yet entered pipeline
const pipelinePhase = Array(TOTAL_BLOCKS).fill(-1);
// Set of block indices that have been fully committed
const committed = new Set();
function advance() {
// Advance all in-flight blocks by one phase; introduce a new block each step
// Each click introduces one new block into the pipeline and advances all active blocks by one phase.
// Block i enters on step i+1 (so block 0 on step 1, block 1 on step 2, ...).
for (let i = 0; i < TOTAL_BLOCKS; i++) {
if (step >= i + 1 && pipelinePhase[i] < 3) {
pipelinePhase[i]++;
if (pipelinePhase[i] === 3) committed.add(i);
}
}
}
function phaseClass(p) {
// Map phase index to CSS class name for color-coding
if (p === -1) return 'empty';
if (p === 0) return 'prepare';
if (p === 1) return 'precommit';
if (p === 2) return 'commit';
return 'done';
}
function phaseLabel(p) {
if (p === -1) return '';
if (p === 3) return '✓ done';
return PHASE_LABELS[PHASES[p]];
}
function render() {
const container = document.getElementById('pipeline');
container.innerHTML = '';
// Build the grid: one row per pipeline phase lane
// Row k shows blocks whose pipeline phase == k (i.e., which "lane" they are in).
// We render one row per phase lane, labeling it with the phase name.
for (let lane = 0; lane < ROWS; lane++) {
const row = document.createElement('div');
row.className = 'row';
const lbl = document.createElement('div');
lbl.className = 'row-label';
lbl.textContent = PHASE_LABELS[PHASES[lane]];
row.appendChild(lbl);
// Render each block's cell in this lane
for (let i = 0; i < TOTAL_BLOCKS; i++) {
if (i > 0) {
const arr = document.createElement('span');
arr.className = 'arrow';
arr.textContent = '→';
row.appendChild(arr);
}
const cell = document.createElement('div');
const p = pipelinePhase[i];
// Highlight a block only in its current active lane
// A block is "active" in this lane if it is currently at that pipeline phase.
const activeInLane = (p === lane);
const isCommitted = (p === 3);
if (activeInLane) {
cell.className = 'cell ' + phaseClass(p);
} else if (isCommitted && lane === ROWS - 1) {
cell.className = 'cell done';
} else {
cell.className = 'cell empty';
}
cell.innerHTML = '<span class="blk">B' + (i + 1) + '</span>' +
'<span class="phase">' + (activeInLane ? PHASE_LABELS[PHASES[lane]] : (isCommitted && lane === ROWS - 1 ? '✓ done' : '')) + '</span>';
row.appendChild(cell);
}
container.appendChild(row);
}
}
function updateStatus() {
const s = document.getElementById('status');
const btn = document.getElementById('btn-next');
if (step === 0) {
s.textContent = 'Press "Next round" to introduce the first block.';
s.className = 'status';
} else if (committed.size > 0) {
const list = [...committed].map(i => 'B' + (i + 1)).join(', ');
s.textContent = fmt('Committed: %list% — finalized by the three-chain rule!', { list: list });
s.className = 'status commit-msg';
} else {
s.textContent = fmt('Round %step% — blocks advancing through the pipeline.', { step: step });
s.className = 'status';
}
if (step >= TOTAL_BLOCKS + 2) {
btn.disabled = true;
btn.textContent = 'Pipeline full';
}
}
document.getElementById('btn-next').addEventListener('click', () => {
if (step < TOTAL_BLOCKS + 2) {
step++;
advance();
render();
updateStatus();
}
});
document.getElementById('btn-reset').addEventListener('click', () => {
step = 0;
pipelinePhase.fill(-1);
committed.clear();
document.getElementById('btn-next').disabled = false;
document.getElementById('btn-next').textContent = 'Next round →';
render();
updateStatus();
});
// Initialize the display on page load
render();
updateStatus();
<!-- HotStuff pipeline demo — three-phase BFT consensus visualization -->
<div class="hs-wrap">
<p class="hint">Each round, a new block enters at <b>Prepare</b> and every active block advances one phase. A block is <b>committed</b> (finalized) only after three consecutive certified phases. Click <b>Next round</b> to step through the pipeline.</p>
<div id="pipeline" class="pipeline"></div>
<div class="controls">
<button id="btn-next" type="button">Next round →</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div id="status" class="status"></div>
</div>
/* Styles for the HotStuff pipeline demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hs-wrap { padding: .5rem; max-width: 680px; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.5; }
/* Pipeline grid: rows = phases, columns = blocks */
.pipeline { display: flex; flex-direction: column; gap: 6px; margin-bottom: .7rem; }
.row { display: flex; align-items: center; gap: 0; }
.row-label { width: 80px; font-size: .78rem; font-weight: 700; color: #555; flex-shrink: 0; }
/* Individual pipeline cell styling */
.cell {
width: 68px; height: 52px; border-radius: 8px; border: 1.5px solid #cdd9e3;
display: flex; flex-direction: column; align-items: center; justify-content: center;
font-size: .7rem; font-weight: 600; transition: background .25s, border-color .25s;
background: #f0f4f8; color: #667; cursor: default; flex-shrink: 0; position: relative;
}
.cell + .cell { margin-left: 4px; }
.cell .blk { font-size: .85rem; font-weight: 800; color: #1d3557; }
.cell .phase { font-size: .62rem; text-transform: uppercase; letter-spacing: .04em; margin-top: 2px; }
/* Color-coding by phase: blue=prepare, yellow=precommit, green=commit/done */
.cell.prepare { background: #dbeafe; border-color: #3b82f6; color: #1d3557; }
.cell.precommit { background: #fef9c3; border-color: #eab308; color: #713f12; }
.cell.commit { background: #dcfce7; border-color: #22c55e; color: #14532d; }
.cell.done { background: #bbf7d0; border-color: #16a34a; color: #14532d; opacity: .75; }
.cell.empty { background: #f8f9fa; border-color: #e2e8f0; color: #aaa; }
/* Arrow between cells and status message */
.arrow { font-size: 1.1rem; color: #94a3b8; padding: 0 1px; flex-shrink: 0; }
.status { font-size: .92rem; font-weight: 600; min-height: 1.3em; color: #1d3557; }
.status.commit-msg { color: #15803d; }
.controls { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .5rem; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1.5px 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] ?? ''));
// HotStuff pipelined BFT consensus demo.
// Three phases: Prepare, Pre-Commit, Commit. A block is committed
// once all three phases complete (the three-chain rule).
// Internal phase identifiers (not shown to user)
const PHASES = ['prepare', 'precommit', 'commit'];
const PHASE_LABELS = { prepare: 'Prepare', precommit: 'Pre-Commit', commit: 'Commit' };
// Pipeline configuration: total blocks and lane count
const TOTAL_BLOCKS = 5;
const ROWS = 3; // one lane per phase
// Global simulation state
// step = number of "next block" clicks fired so far
let step = 0;
// Phase index per block: -1=not started, 0=prepare, 1=precommit, 2=commit, 3=done
// pipelinePhase[i] = index into PHASES for block i (0=prepare,1=precommit,2=commit,3=done)
// -1 = not yet entered pipeline
const pipelinePhase = Array(TOTAL_BLOCKS).fill(-1);
// Set of block indices that have been fully committed
const committed = new Set();
function advance() {
// Advance all in-flight blocks by one phase; introduce a new block each step
// Each click introduces one new block into the pipeline and advances all active blocks by one phase.
// Block i enters on step i+1 (so block 0 on step 1, block 1 on step 2, ...).
for (let i = 0; i < TOTAL_BLOCKS; i++) {
if (step >= i + 1 && pipelinePhase[i] < 3) {
pipelinePhase[i]++;
if (pipelinePhase[i] === 3) committed.add(i);
}
}
}
function phaseClass(p) {
// Map phase index to CSS class name for color-coding
if (p === -1) return 'empty';
if (p === 0) return 'prepare';
if (p === 1) return 'precommit';
if (p === 2) return 'commit';
return 'done';
}
function phaseLabel(p) {
if (p === -1) return '';
if (p === 3) return '✓ done';
return PHASE_LABELS[PHASES[p]];
}
function render() {
const container = document.getElementById('pipeline');
container.innerHTML = '';
// Build the grid: one row per pipeline phase lane
// Row k shows blocks whose pipeline phase == k (i.e., which "lane" they are in).
// We render one row per phase lane, labeling it with the phase name.
for (let lane = 0; lane < ROWS; lane++) {
const row = document.createElement('div');
row.className = 'row';
const lbl = document.createElement('div');
lbl.className = 'row-label';
lbl.textContent = PHASE_LABELS[PHASES[lane]];
row.appendChild(lbl);
// Render each block's cell in this lane
for (let i = 0; i < TOTAL_BLOCKS; i++) {
if (i > 0) {
const arr = document.createElement('span');
arr.className = 'arrow';
arr.textContent = '→';
row.appendChild(arr);
}
const cell = document.createElement('div');
const p = pipelinePhase[i];
// Highlight a block only in its current active lane
// A block is "active" in this lane if it is currently at that pipeline phase.
const activeInLane = (p === lane);
const isCommitted = (p === 3);
if (activeInLane) {
cell.className = 'cell ' + phaseClass(p);
} else if (isCommitted && lane === ROWS - 1) {
cell.className = 'cell done';
} else {
cell.className = 'cell empty';
}
cell.innerHTML = '<span class="blk">B' + (i + 1) + '</span>' +
'<span class="phase">' + (activeInLane ? PHASE_LABELS[PHASES[lane]] : (isCommitted && lane === ROWS - 1 ? '✓ done' : '')) + '</span>';
row.appendChild(cell);
}
container.appendChild(row);
}
}
function updateStatus() {
const s = document.getElementById('status');
const btn = document.getElementById('btn-next');
if (step === 0) {
s.textContent = 'Press "Next round" to introduce the first block.';
s.className = 'status';
} else if (committed.size > 0) {
const list = [...committed].map(i => 'B' + (i + 1)).join(', ');
s.textContent = fmt('Committed: %list% — finalized by the three-chain rule!', { list: list });
s.className = 'status commit-msg';
} else {
s.textContent = fmt('Round %step% — blocks advancing through the pipeline.', { step: step });
s.className = 'status';
}
if (step >= TOTAL_BLOCKS + 2) {
btn.disabled = true;
btn.textContent = 'Pipeline full';
}
}
document.getElementById('btn-next').addEventListener('click', () => {
if (step < TOTAL_BLOCKS + 2) {
step++;
advance();
render();
updateStatus();
}
});
document.getElementById('btn-reset').addEventListener('click', () => {
step = 0;
pipelinePhase.fill(-1);
committed.clear();
document.getElementById('btn-next').disabled = false;
document.getElementById('btn-next').textContent = 'Next round →';
render();
updateStatus();
});
// Initialize the display on page load
render();
updateStatus();
The key insight is pipelining: the Prepare vote for block k+1 doubles as the Pre-Commit vote for block k and the Commit vote for block k−1. Each new block advances the entire pipeline by one step, so the throughput stays linear even as the chain grows.
Comments
Loading comments...