<p class="hint">Add signers one by one. Each holds a secret key and produces a partial signature. Watch the aggregate public key and aggregate signature update — they satisfy the same verification equation as a single signer, revealing nothing about the number of participants.</p>
<div class="controls">
<button id="add-signer" type="button">+ Add signer</button>
<button id="verify" type="button">Verify aggregate</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
<div class="panel">
<div class="col" id="signers-col">
<div class="col-title">Individual signers</div>
<div id="signers-list"><em class="empty">No signers yet.</em></div>
</div>
<div class="col agg-col">
<div class="col-title">Aggregate (what the verifier sees)</div>
<div id="agg-block">
<div class="agg-row"><span class="lbl">Agg public key X</span><span class="val" id="agg-X">—</span></div>
<div class="agg-row"><span class="lbl">Agg nonce R</span><span class="val" id="agg-R">—</span></div>
<div class="agg-row"><span class="lbl">Agg response s</span><span class="val" id="agg-s">—</span></div>
<div class="agg-row"><span class="lbl">Challenge e</span><span class="val" id="agg-e">—</span></div>
</div>
<div class="status" id="status">Add at least one signer, then verify.</div>
</div>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .7rem; line-height: 1.5; }
.controls { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .8rem; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: default; }
.panel { display: flex; gap: .8rem; flex-wrap: wrap; }
.col { flex: 1 1 200px; background: #f4f7fa; border-radius: 10px; padding: .7rem .9rem; }
.agg-col { flex: 1 1 220px; }
.col-title { font-weight: 700; font-size: .82rem; text-transform: uppercase; letter-spacing: .04em;
color: #1d3557; margin-bottom: .5rem; border-bottom: 1px solid #d0dae5; padding-bottom: .3rem; }
.signer-card { background: #fff; border: 1px solid #cdd9e3; border-radius: 8px; padding: .5rem .7rem;
margin-bottom: .5rem; font-size: .82rem; }
.signer-card .sname { font-weight: 700; color: #1d3557; margin-bottom: .25rem; }
.signer-card .row { display: flex; justify-content: space-between; gap: .5rem; }
.signer-card .lk { color: #555; }
.signer-card .vk { font-family: ui-monospace, monospace; color: #222; }
.agg-row { display: flex; flex-direction: column; margin-bottom: .45rem; }
.lbl { font-size: .78rem; color: #555; font-weight: 600; }
.val { font-family: ui-monospace, monospace; font-size: .9rem; color: #222; word-break: break-all; }
.status { margin-top: .7rem; font-weight: 700; font-size: .9rem; min-height: 1.3em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.empty { color: #888; font-style: italic; font-size: .85rem; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Toy Schnorr aggregation demo (illustrative, NOT production crypto).
// We work modulo a small prime p with a generator g so numbers stay readable.
// Real Schnorr uses elliptic curves; the algebra is identical.
//
// Parameters (tiny, educational only):
// p = 9001 (prime), g = 7 (primitive root mod p), q = p-1 (group order, simplification)
// In a real curve, q is the curve order; here q = p-1 = 9000.
//
// Key: x_i random in [1, q-1]; X_i = g^x_i mod p
// Nonce: r_i random in [1, q-1]; R_i = g^r_i mod p
// Agg: X = prod(X_i) mod p, R = prod(R_i) mod p (multiplicative group analogue)
// e = hash(R, X, message) mod q
// s_i = (r_i + e * x_i) mod q
// s = sum(s_i) mod q
// Verify: g^s mod p == R * X^e mod p
//
// (In additive elliptic-curve notation, prod -> sum of points; the verification is
// s*G == R + e*X, exactly the same structure.)
const p = 9001n; // prime modulus
const g = 7n; // generator
const q = 9000n; // group order (p-1); real Schnorr uses prime-order subgroup
const MSG = 'kipu-demo-message';
function modpow(base, exp, mod) {
let result = 1n; base = base % mod;
while (exp > 0n) {
if (exp % 2n === 1n) result = result * base % mod;
exp >>= 1n; base = base * base % mod;
}
return result;
}
// Deterministic hash -> integer (simple djb2-style, demo only)
function hashToInt(str) {
let h = 5381n;
for (let i = 0; i < str.length; i++) h = (h * 33n + BigInt(str.charCodeAt(i))) % q;
return h === 0n ? 1n : h;
}
function randBig() {
// Pseudorandom [1, q-1] using Math.random seeded by time
const v = BigInt(Math.floor(Math.random() * 8999) + 1);
return v;
}
const signers = [];
const NAMES = ['Alice', 'Bob', 'Carol', 'Dave', 'Eve', 'Frank'];
function computeAgg() {
if (signers.length === 0) return null;
// Aggregate public key: product of X_i mod p
let X = 1n;
for (const s of signers) X = X * s.X % p;
// Aggregate nonce commitment: product of R_i mod p
let R = 1n;
for (const s of signers) R = R * s.R % p;
// Challenge: hash(R, X, message)
const e = hashToInt(R.toString() + ',' + X.toString() + ',' + MSG);
// Aggregate response: sum of s_i mod q
let sv = 0n;
for (const s of signers) {
const si = (s.r + e * s.x) % q;
sv = (sv + si) % q;
}
return { X, R, e, s: sv };
}
function verify(agg) {
if (!agg) return false;
const { X, R, e, s } = agg;
const lhs = modpow(g, s, p);
const rhs = R * modpow(X, e, p) % p;
return lhs === rhs;
}
function render() {
const list = document.getElementById('signers-list');
if (signers.length === 0) {
list.innerHTML = '<em class="empty">No signers yet.</em>';
} else {
list.innerHTML = signers.map((s, i) => "\n " + fmt(`<div class="signer-card">
<div class="sname">%v%</div>
<div class="row"><span class="lk">secret x</span><span class="vk">%x%</span></div>
<div class="row"><span class="lk">public X = g^x</span><span class="vk">%X%</span></div>
<div class="row"><span class="lk">nonce r</span><span class="vk">%r%</span></div>
<div class="row"><span class="lk">commit R = g^r</span><span class="vk">%R%</span></div>
</div>`, { v: NAMES[i] || 'Signer ' + (i+1), x: s.x, X: s.X, r: s.r, R: s.R })).join('');
}
const agg = computeAgg();
document.getElementById('agg-X').textContent = agg ? agg.X.toString() : '—';
document.getElementById('agg-R').textContent = agg ? agg.R.toString() : '—';
document.getElementById('agg-s').textContent = agg ? agg.s.toString() : '—';
document.getElementById('agg-e').textContent = agg ? agg.e.toString() : '—';
const st = document.getElementById('status');
st.textContent = `Add at least one signer, then verify.`;
st.className = 'status';
const addBtn = document.getElementById('add-signer');
addBtn.disabled = signers.length >= NAMES.length;
}
document.getElementById('add-signer').addEventListener('click', () => {
if (signers.length >= NAMES.length) return;
const x = randBig();
const X = modpow(g, x, p);
const r = randBig();
const R = modpow(g, r, p);
signers.push({ x, X, r, R });
render();
console.log(`Added signer`, NAMES[signers.length-1], '| x='+x+' X='+X+' r='+r+' R='+R);
});
document.getElementById('verify').addEventListener('click', () => {
const agg = computeAgg();
const st = document.getElementById('status');
if (!agg) { st.textContent = `Add at least one signer first.`; st.className = 'status'; return; }
const ok = verify(agg);
// Double-check via console
console.log(`Verify: g^s mod p =`, modpow(g, agg.s, p).toString(),
'| R*X^e mod p =', (agg.R * modpow(agg.X, agg.e, p) % p).toString(),
'| ok=', ok);
if (ok) {
st.textContent = fmt(`✓ Aggregate signature valid! (%length% signer%v%, one signature)`, { length: signers.length, v: signers.length>1?`s`:'' });
st.className = 'status ok';
} else {
st.textContent = `✗ Verification failed — something is wrong.`;
st.className = 'status bad';
}
});
document.getElementById('reset').addEventListener('click', () => {
signers.length = 0;
render();
});
render();
<p class="hint">Add signers one by one. Each holds a secret key and produces a partial signature. Watch the aggregate public key and aggregate signature update — they satisfy the same verification equation as a single signer, revealing nothing about the number of participants.</p>
<div class="controls">
<button id="add-signer" type="button">+ Add signer</button>
<button id="verify" type="button">Verify aggregate</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
<div class="panel">
<div class="col" id="signers-col">
<div class="col-title">Individual signers</div>
<div id="signers-list"><em class="empty">No signers yet.</em></div>
</div>
<div class="col agg-col">
<div class="col-title">Aggregate (what the verifier sees)</div>
<div id="agg-block">
<div class="agg-row"><span class="lbl">Agg public key X</span><span class="val" id="agg-X">—</span></div>
<div class="agg-row"><span class="lbl">Agg nonce R</span><span class="val" id="agg-R">—</span></div>
<div class="agg-row"><span class="lbl">Agg response s</span><span class="val" id="agg-s">—</span></div>
<div class="agg-row"><span class="lbl">Challenge e</span><span class="val" id="agg-e">—</span></div>
</div>
<div class="status" id="status">Add at least one signer, then verify.</div>
</div>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .7rem; line-height: 1.5; }
.controls { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .8rem; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: default; }
.panel { display: flex; gap: .8rem; flex-wrap: wrap; }
.col { flex: 1 1 200px; background: #f4f7fa; border-radius: 10px; padding: .7rem .9rem; }
.agg-col { flex: 1 1 220px; }
.col-title { font-weight: 700; font-size: .82rem; text-transform: uppercase; letter-spacing: .04em;
color: #1d3557; margin-bottom: .5rem; border-bottom: 1px solid #d0dae5; padding-bottom: .3rem; }
.signer-card { background: #fff; border: 1px solid #cdd9e3; border-radius: 8px; padding: .5rem .7rem;
margin-bottom: .5rem; font-size: .82rem; }
.signer-card .sname { font-weight: 700; color: #1d3557; margin-bottom: .25rem; }
.signer-card .row { display: flex; justify-content: space-between; gap: .5rem; }
.signer-card .lk { color: #555; }
.signer-card .vk { font-family: ui-monospace, monospace; color: #222; }
.agg-row { display: flex; flex-direction: column; margin-bottom: .45rem; }
.lbl { font-size: .78rem; color: #555; font-weight: 600; }
.val { font-family: ui-monospace, monospace; font-size: .9rem; color: #222; word-break: break-all; }
.status { margin-top: .7rem; font-weight: 700; font-size: .9rem; min-height: 1.3em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.empty { color: #888; font-style: italic; font-size: .85rem; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Toy Schnorr aggregation demo (illustrative, NOT production crypto).
// We work modulo a small prime p with a generator g so numbers stay readable.
// Real Schnorr uses elliptic curves; the algebra is identical.
//
// Parameters (tiny, educational only):
// p = 9001 (prime), g = 7 (primitive root mod p), q = p-1 (group order, simplification)
// In a real curve, q is the curve order; here q = p-1 = 9000.
//
// Key: x_i random in [1, q-1]; X_i = g^x_i mod p
// Nonce: r_i random in [1, q-1]; R_i = g^r_i mod p
// Agg: X = prod(X_i) mod p, R = prod(R_i) mod p (multiplicative group analogue)
// e = hash(R, X, message) mod q
// s_i = (r_i + e * x_i) mod q
// s = sum(s_i) mod q
// Verify: g^s mod p == R * X^e mod p
//
// (In additive elliptic-curve notation, prod -> sum of points; the verification is
// s*G == R + e*X, exactly the same structure.)
const p = 9001n; // prime modulus
const g = 7n; // generator
const q = 9000n; // group order (p-1); real Schnorr uses prime-order subgroup
const MSG = 'kipu-demo-message';
function modpow(base, exp, mod) {
let result = 1n; base = base % mod;
while (exp > 0n) {
if (exp % 2n === 1n) result = result * base % mod;
exp >>= 1n; base = base * base % mod;
}
return result;
}
// Deterministic hash -> integer (simple djb2-style, demo only)
function hashToInt(str) {
let h = 5381n;
for (let i = 0; i < str.length; i++) h = (h * 33n + BigInt(str.charCodeAt(i))) % q;
return h === 0n ? 1n : h;
}
function randBig() {
// Pseudorandom [1, q-1] using Math.random seeded by time
const v = BigInt(Math.floor(Math.random() * 8999) + 1);
return v;
}
const signers = [];
const NAMES = ['Alice', 'Bob', 'Carol', 'Dave', 'Eve', 'Frank'];
function computeAgg() {
if (signers.length === 0) return null;
// Aggregate public key: product of X_i mod p
let X = 1n;
for (const s of signers) X = X * s.X % p;
// Aggregate nonce commitment: product of R_i mod p
let R = 1n;
for (const s of signers) R = R * s.R % p;
// Challenge: hash(R, X, message)
const e = hashToInt(R.toString() + ',' + X.toString() + ',' + MSG);
// Aggregate response: sum of s_i mod q
let sv = 0n;
for (const s of signers) {
const si = (s.r + e * s.x) % q;
sv = (sv + si) % q;
}
return { X, R, e, s: sv };
}
function verify(agg) {
if (!agg) return false;
const { X, R, e, s } = agg;
const lhs = modpow(g, s, p);
const rhs = R * modpow(X, e, p) % p;
return lhs === rhs;
}
function render() {
const list = document.getElementById('signers-list');
if (signers.length === 0) {
list.innerHTML = '<em class="empty">No signers yet.</em>';
} else {
list.innerHTML = signers.map((s, i) => "\n " + fmt(`<div class="signer-card">
<div class="sname">%v%</div>
<div class="row"><span class="lk">secret x</span><span class="vk">%x%</span></div>
<div class="row"><span class="lk">public X = g^x</span><span class="vk">%X%</span></div>
<div class="row"><span class="lk">nonce r</span><span class="vk">%r%</span></div>
<div class="row"><span class="lk">commit R = g^r</span><span class="vk">%R%</span></div>
</div>`, { v: NAMES[i] || 'Signer ' + (i+1), x: s.x, X: s.X, r: s.r, R: s.R })).join('');
}
const agg = computeAgg();
document.getElementById('agg-X').textContent = agg ? agg.X.toString() : '—';
document.getElementById('agg-R').textContent = agg ? agg.R.toString() : '—';
document.getElementById('agg-s').textContent = agg ? agg.s.toString() : '—';
document.getElementById('agg-e').textContent = agg ? agg.e.toString() : '—';
const st = document.getElementById('status');
st.textContent = `Add at least one signer, then verify.`;
st.className = 'status';
const addBtn = document.getElementById('add-signer');
addBtn.disabled = signers.length >= NAMES.length;
}
document.getElementById('add-signer').addEventListener('click', () => {
if (signers.length >= NAMES.length) return;
const x = randBig();
const X = modpow(g, x, p);
const r = randBig();
const R = modpow(g, r, p);
signers.push({ x, X, r, R });
render();
console.log(`Added signer`, NAMES[signers.length-1], '| x='+x+' X='+X+' r='+r+' R='+R);
});
document.getElementById('verify').addEventListener('click', () => {
const agg = computeAgg();
const st = document.getElementById('status');
if (!agg) { st.textContent = `Add at least one signer first.`; st.className = 'status'; return; }
const ok = verify(agg);
// Double-check via console
console.log(`Verify: g^s mod p =`, modpow(g, agg.s, p).toString(),
'| R*X^e mod p =', (agg.R * modpow(agg.X, agg.e, p) % p).toString(),
'| ok=', ok);
if (ok) {
st.textContent = fmt(`✓ Aggregate signature valid! (%length% signer%v%, one signature)`, { length: signers.length, v: signers.length>1?`s`:'' });
st.className = 'status ok';
} else {
st.textContent = `✗ Verification failed — something is wrong.`;
st.className = 'status bad';
}
});
document.getElementById('reset').addEventListener('click', () => {
signers.length = 0;
render();
});
render();
Comments
Loading comments...