Below are five participants. Each one secretly holds a random value — their partial private key share — and broadcasts only a public commitment to it. Click Run DKG round to simulate one round: each participant contributes, the commitments are aggregated, and the final group public key is derived from the sum of contributions — without any participant ever learning the private key of another.
<!-- Distributed key generation demo layout -->
<p class="hint">Each participant secretly picks a random value and publishes only <b>g<sup>s</sup> mod p</b> — a commitment. The group public key is the product of all commitments: no one ever sees the full private key.</p>
<div id="participants" class="participants"></div>
<div class="status-row">
<div class="status" id="status">Press Run DKG round to start.</div>
</div>
<div id="pubkey-box" class="pubkey-box hidden">
<span class="pubkey-label">Group public key:</span>
<span class="pubkey-value" id="pubkey-value"></span>
</div>
<div class="btns">
<button id="btn-run" type="button">Run DKG round</button>
<button id="btn-reset" type="button" class="ghost">New round</button>
</div>
/* Styles for the DKG demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: 14px; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .7rem; line-height: 1.5; }
.participants { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: .6rem; }
.participant {
flex: 1 1 80px; min-width: 80px; max-width: 120px;
border: 1.5px solid #cdd9e3; border-radius: 10px;
padding: .5rem .4rem; text-align: center;
background: #f0f4f8; transition: background .25s, border-color .25s;
}
.participant.active { background: #dbeafe; border-color: #3b82f6; }
.participant.done { background: #dcfce7; border-color: #16a34a; }
.p-name { font-weight: 700; font-size: .8rem; color: #1d3557; margin-bottom: .3rem; }
.p-secret { font-size: .72rem; color: #888; margin-bottom: .2rem; font-style: italic; }
.p-commit { font-size: .78rem; color: #1d3557; font-family: ui-monospace, monospace; word-break: break-all; min-height: 1.2em; }
.p-commit.revealed { color: #15803d; font-weight: 600; }
.status-row { margin: .4rem 0; min-height: 1.5em; }
.status { font-size: .95rem; font-weight: 600; }
.status.ok { color: #15803d; }
.status.running { color: #1d4ed8; }
.pubkey-box {
display: flex; align-items: center; gap: .5rem;
background: #1d3557; color: #fff; border-radius: 10px;
padding: .55rem .8rem; margin: .5rem 0; font-size: .82rem; flex-wrap: wrap;
}
.pubkey-box.hidden { display: none; }
.pubkey-label { font-weight: 700; white-space: nowrap; }
.pubkey-value { font-family: ui-monospace, monospace; word-break: break-all; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-top: .3rem; }
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; }
button:disabled { opacity: .45; cursor: not-allowed; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Distributed Key Generation demo (simplified over integers mod prime).
// Each participant holds a secret s_i in Z_p; commits to g^s_i mod p.
// Small prime for demo (real DKG uses 2048-bit+ primes or elliptic curves)
const P = 9973;
// Generator g
const G = 7;
// Number of participants
const N = 5;
// Participant names
const NAMES = ['Alice', 'Bob', 'Carol', 'Dave', 'Eve'];
// Modular exponentiation using BigInt for exactness
function modpow(base, exp, mod) {
let result = 1n;
base = base % mod;
while (exp > 0n) {
if (exp % 2n === 1n) result = result * base % mod;
exp = exp / 2n;
base = base * base % mod;
}
return result;
}
// Pick a random secret in [1, p-1]
function randSecret() {
return BigInt(Math.floor(Math.random() * (P - 2)) + 1);
}
let participants = [];
let running = false;
function initParticipants() {
participants = NAMES.map((name, i) => ({
name,
secret: randSecret(),
commit: null,
el: null
}));
}
function renderParticipants() {
const container = document.getElementById('participants');
container.innerHTML = '';
participants.forEach((p, i) => {
const div = document.createElement('div');
div.className = 'participant';
div.id = 'p-' + i;
div.innerHTML =
fmt('<div class="p-name">%name%</div><div class="p-secret">Secret: ?</div><div class="p-commit" id="commit-%i%">waiting...</div>', { name: p.name, i: i });
container.appendChild(div);
p.el = div;
});
}
function setStatus(msg, cls) {
const s = document.getElementById('status');
s.textContent = msg;
s.className = 'status ' + (cls || '');
}
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
// Animate one DKG round: each participant commits, then aggregate
async function runDKG() {
if (running) return;
running = true;
document.getElementById('btn-run').disabled = true;
document.getElementById('pubkey-box').classList.add('hidden');
setStatus('Starting DKG protocol...', 'running');
await sleep(300);
// Phase 1: each participant computes and broadcasts their commitment
for (let i = 0; i < N; i++) {
const p = participants[i];
p.el.classList.add('active');
setStatus(fmt('Computing commitment for %name%...', { name: p.name }), 'running');
await sleep(400);
// Commitment = g^secret mod p
p.commit = modpow(BigInt(G), p.secret, BigInt(P));
const secretEl = p.el.querySelector('.p-secret');
secretEl.textContent = fmt('Secret: %secret%', { secret: p.secret.toString() });
const commitEl = document.getElementById('commit-' + i);
commitEl.textContent = 'g^s = ' + p.commit.toString();
commitEl.classList.add('revealed');
p.el.classList.remove('active');
p.el.classList.add('done');
await sleep(200);
}
// Phase 2: aggregate commitments into the group public key
setStatus('Aggregating public commitments...', 'running');
await sleep(400);
// Group public key Y = product of all g^s_i mod p = g^(sum s_i) mod p
let Y = 1n;
for (const p of participants) {
Y = Y * p.commit % BigInt(P);
}
// Reveal the group public key
const box = document.getElementById('pubkey-box');
box.classList.remove('hidden');
document.getElementById('pubkey-value').textContent = Y.toString();
setStatus('✓ Group public key established — no dealer needed!', 'ok');
console.log('Group public key:', Y.toString());
console.log('Individual secrets (never combined in a real DKG):', participants.map(p => p.secret.toString()).join(' + ') + ' mod ' + P + ' = ' + participants.reduce((a, p) => (a + p.secret) % BigInt(P), 0n).toString());
running = false;
document.getElementById('btn-run').disabled = false;
}
function reset() {
running = false;
initParticipants();
renderParticipants();
setStatus('Press Run DKG round to start.', '');
document.getElementById('pubkey-box').classList.add('hidden');
document.getElementById('btn-run').disabled = false;
}
document.getElementById('btn-run').addEventListener('click', runDKG);
document.getElementById('btn-reset').addEventListener('click', reset);
initParticipants();
renderParticipants();
<!-- Distributed key generation demo layout -->
<p class="hint">Each participant secretly picks a random value and publishes only <b>g<sup>s</sup> mod p</b> — a commitment. The group public key is the product of all commitments: no one ever sees the full private key.</p>
<div id="participants" class="participants"></div>
<div class="status-row">
<div class="status" id="status">Press Run DKG round to start.</div>
</div>
<div id="pubkey-box" class="pubkey-box hidden">
<span class="pubkey-label">Group public key:</span>
<span class="pubkey-value" id="pubkey-value"></span>
</div>
<div class="btns">
<button id="btn-run" type="button">Run DKG round</button>
<button id="btn-reset" type="button" class="ghost">New round</button>
</div>
/* Styles for the DKG demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: 14px; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .7rem; line-height: 1.5; }
.participants { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: .6rem; }
.participant {
flex: 1 1 80px; min-width: 80px; max-width: 120px;
border: 1.5px solid #cdd9e3; border-radius: 10px;
padding: .5rem .4rem; text-align: center;
background: #f0f4f8; transition: background .25s, border-color .25s;
}
.participant.active { background: #dbeafe; border-color: #3b82f6; }
.participant.done { background: #dcfce7; border-color: #16a34a; }
.p-name { font-weight: 700; font-size: .8rem; color: #1d3557; margin-bottom: .3rem; }
.p-secret { font-size: .72rem; color: #888; margin-bottom: .2rem; font-style: italic; }
.p-commit { font-size: .78rem; color: #1d3557; font-family: ui-monospace, monospace; word-break: break-all; min-height: 1.2em; }
.p-commit.revealed { color: #15803d; font-weight: 600; }
.status-row { margin: .4rem 0; min-height: 1.5em; }
.status { font-size: .95rem; font-weight: 600; }
.status.ok { color: #15803d; }
.status.running { color: #1d4ed8; }
.pubkey-box {
display: flex; align-items: center; gap: .5rem;
background: #1d3557; color: #fff; border-radius: 10px;
padding: .55rem .8rem; margin: .5rem 0; font-size: .82rem; flex-wrap: wrap;
}
.pubkey-box.hidden { display: none; }
.pubkey-label { font-weight: 700; white-space: nowrap; }
.pubkey-value { font-family: ui-monospace, monospace; word-break: break-all; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-top: .3rem; }
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; }
button:disabled { opacity: .45; cursor: not-allowed; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Distributed Key Generation demo (simplified over integers mod prime).
// Each participant holds a secret s_i in Z_p; commits to g^s_i mod p.
// Small prime for demo (real DKG uses 2048-bit+ primes or elliptic curves)
const P = 9973;
// Generator g
const G = 7;
// Number of participants
const N = 5;
// Participant names
const NAMES = ['Alice', 'Bob', 'Carol', 'Dave', 'Eve'];
// Modular exponentiation using BigInt for exactness
function modpow(base, exp, mod) {
let result = 1n;
base = base % mod;
while (exp > 0n) {
if (exp % 2n === 1n) result = result * base % mod;
exp = exp / 2n;
base = base * base % mod;
}
return result;
}
// Pick a random secret in [1, p-1]
function randSecret() {
return BigInt(Math.floor(Math.random() * (P - 2)) + 1);
}
let participants = [];
let running = false;
function initParticipants() {
participants = NAMES.map((name, i) => ({
name,
secret: randSecret(),
commit: null,
el: null
}));
}
function renderParticipants() {
const container = document.getElementById('participants');
container.innerHTML = '';
participants.forEach((p, i) => {
const div = document.createElement('div');
div.className = 'participant';
div.id = 'p-' + i;
div.innerHTML =
fmt('<div class="p-name">%name%</div><div class="p-secret">Secret: ?</div><div class="p-commit" id="commit-%i%">waiting...</div>', { name: p.name, i: i });
container.appendChild(div);
p.el = div;
});
}
function setStatus(msg, cls) {
const s = document.getElementById('status');
s.textContent = msg;
s.className = 'status ' + (cls || '');
}
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
// Animate one DKG round: each participant commits, then aggregate
async function runDKG() {
if (running) return;
running = true;
document.getElementById('btn-run').disabled = true;
document.getElementById('pubkey-box').classList.add('hidden');
setStatus('Starting DKG protocol...', 'running');
await sleep(300);
// Phase 1: each participant computes and broadcasts their commitment
for (let i = 0; i < N; i++) {
const p = participants[i];
p.el.classList.add('active');
setStatus(fmt('Computing commitment for %name%...', { name: p.name }), 'running');
await sleep(400);
// Commitment = g^secret mod p
p.commit = modpow(BigInt(G), p.secret, BigInt(P));
const secretEl = p.el.querySelector('.p-secret');
secretEl.textContent = fmt('Secret: %secret%', { secret: p.secret.toString() });
const commitEl = document.getElementById('commit-' + i);
commitEl.textContent = 'g^s = ' + p.commit.toString();
commitEl.classList.add('revealed');
p.el.classList.remove('active');
p.el.classList.add('done');
await sleep(200);
}
// Phase 2: aggregate commitments into the group public key
setStatus('Aggregating public commitments...', 'running');
await sleep(400);
// Group public key Y = product of all g^s_i mod p = g^(sum s_i) mod p
let Y = 1n;
for (const p of participants) {
Y = Y * p.commit % BigInt(P);
}
// Reveal the group public key
const box = document.getElementById('pubkey-box');
box.classList.remove('hidden');
document.getElementById('pubkey-value').textContent = Y.toString();
setStatus('✓ Group public key established — no dealer needed!', 'ok');
console.log('Group public key:', Y.toString());
console.log('Individual secrets (never combined in a real DKG):', participants.map(p => p.secret.toString()).join(' + ') + ' mod ' + P + ' = ' + participants.reduce((a, p) => (a + p.secret) % BigInt(P), 0n).toString());
running = false;
document.getElementById('btn-run').disabled = false;
}
function reset() {
running = false;
initParticipants();
renderParticipants();
setStatus('Press Run DKG round to start.', '');
document.getElementById('pubkey-box').classList.add('hidden');
document.getElementById('btn-run').disabled = false;
}
document.getElementById('btn-run').addEventListener('click', runDKG);
document.getElementById('btn-reset').addEventListener('click', reset);
initParticipants();
renderParticipants();
Notice the asymmetry: each participant can verify that everyone else committed honestly (commitments are public), but no one can reconstruct the full private key from fewer than t shares. The public key appears from collective math, not from a secret held by a single dealer.
Comments
Loading comments...