The demo below simulates the BLS aggregation workflow using small integers — real BLS lives on 256-bit elliptic curves, but the structure is identical. Each signer has a private key and a public key. Clicking Sign produces that signer's signature. Aggregate folds all signatures into one combined value. Verify checks the aggregate against all public keys at once.
<p class="hint">
Each signer has a private key and public key. Click <b>Sign</b> to produce a signature. <b>Aggregate</b> collapses all signatures into one. <b>Verify</b> checks the aggregate against every public key at once — one combined check, not many.
</p>
<div id="signers-grid"></div>
<div class="agg-row">
<button id="btn-agg" type="button">Aggregate</button>
<div id="agg-box" class="agg-box">Aggregate: <span id="agg-val">—</span></div>
</div>
<div class="verify-row">
<button id="btn-verify" type="button">Verify</button>
<div id="verify-status" class="status">Sign all, then aggregate and verify.</div>
</div>
<p class="footnote">Numbers here are toy integers mod a small prime (p=997). Real BLS uses 256-bit elliptic-curve points on BLS12-381, but the structure — sign, add, verify with a pairing equation — is identical.</p>
* { 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 .8rem; line-height: 1.5; }
.footnote { font-size: .78rem; color: #777; margin-top: .8rem; line-height: 1.45; }
#signers-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 8px; margin-bottom: 10px; }
.signer-card { border: 1px solid #cdd9e3; border-radius: 10px; padding: 10px 12px; background: #f5f8fb; }
.signer-card h4 { margin: 0 0 6px; font-size: .9rem; color: #1d3557; }
.kv { display: flex; justify-content: space-between; font-size: .82rem; margin: 2px 0; color: #444; }
.kv .val { font-family: ui-monospace, monospace; color: #1d3557; font-weight: 600; }
.sig-row { display: flex; align-items: center; gap: 8px; margin-top: 8px; }
.sig-row .val { font-family: ui-monospace, monospace; font-weight: 600; color: #0a7d33; min-width: 36px; }
.sig-row .val.empty { color: #aaa; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; white-space: nowrap; }
button.ghost { background: #fff; color: #1d3557; }
button.signed { background: #0a7d33; border-color: #0a7d33; }
.agg-row { display: flex; align-items: center; gap: 10px; margin: 6px 0; flex-wrap: wrap; }
.agg-box { font-size: .9rem; color: #444; }
.agg-box span { font-family: ui-monospace, monospace; font-weight: 700; color: #e63946; }
.agg-box span.set { color: #1d3557; }
.verify-row { display: flex; align-items: center; gap: 10px; margin-top: 6px; flex-wrap: wrap; }
.status { font-size: .95rem; font-weight: 600; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.status.warn { color: #b45309; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Toy BLS simulation using modular arithmetic.
// Real BLS uses elliptic-curve groups and pairings on BLS12-381.
// Here we model everything mod a small prime p so the numbers are readable.
//
// Groups G1 = Z_p* with generator g=7, G2 = same group, GT = same group.
// Pairing e(a, b) = g^(a*b mod (p-1)) mod p (discrete-log based toy pairing).
// Private key sk in [1, p-2]. Public key pk = g^sk mod p.
// Hash-to-point H(msg_index) = g^(53+msg_index*31) mod p (fixed toy hash).
// Sign: sigma = H(msg)^sk mod p.
// Aggregate: sigma_agg = sigma1 * sigma2 * ... mod p (group operation = multiply).
// Verify aggregate: e(sigma_agg, g) == product of e(H(mi), pki).
// In toy model: sigma_agg^? ... we verify the discrete-log relationship directly
// to avoid needing a true pairing. The demo verifies the correct BLS equation
// in the discrete-log domain: sum of (sk_i * log H(m_i)) == log sigma_agg.
const p = 997; // prime modulus
const g = 7; // generator of Z_p*
function modpow(base, exp, mod) {
base = ((base % mod) + mod) % mod;
exp = BigInt(exp); mod = BigInt(mod); base = BigInt(base);
let result = 1n;
while (exp > 0n) {
if (exp & 1n) result = result * base % mod;
base = base * base % mod;
exp >>= 1n;
}
return Number(result);
}
// Toy hash-to-"point": deterministic integer in Z_p* per (signer_index, msg)
function hashToPoint(signerIdx, msg) {
// msg is a short string; convert to a small integer via simple sum
let h = 0;
for (let i = 0; i < msg.length; i++) h = (h * 31 + msg.charCodeAt(i)) % (p - 1);
h = ((h + signerIdx * 53 + 7) % (p - 1)) + 1; // ensure [1, p-1]
return modpow(g, h, p); // H(m) = g^h mod p
}
// Discrete log table for g mod p (p=997 is small enough)
const DL = new Map();
let cur = 1;
for (let i = 0; i < p - 1; i++) {
DL.set(cur, i);
cur = cur * g % p;
}
const SIGNERS = [
{ name: 'Alice', sk: 42, msg: 'I approve tx #1' },
{ name: 'Bob', sk: 137, msg: 'I approve tx #2' },
{ name: 'Carol', sk: 251, msg: 'I approve tx #3' },
{ name: 'Dave', sk: 89, msg: 'I approve tx #4' },
];
// Precompute public keys and H(msg) points
SIGNERS.forEach((s, i) => {
s.pk = modpow(g, s.sk, p);
s.hm = hashToPoint(i, s.msg);
s.sigma = null; // not yet signed
});
let aggSigma = null;
function renderSigners() {
const grid = document.getElementById('signers-grid');
grid.innerHTML = '';
SIGNERS.forEach((s, i) => {
const card = document.createElement('div');
card.className = 'signer-card';
card.innerHTML = "\n " + fmt(`<h4>%name%</h4>
<div class="kv"><span>Private key sk</span><span class="val">%sk%</span></div>
<div class="kv"><span>Public key pk = g^sk</span><span class="val">%pk%</span></div>
<div class="kv"><span>Message</span><span class="val" style="font-size:.75rem;font-family:system-ui">%msg%</span></div>
<div class="kv"><span>H(msg) point</span><span class="val">%hm%</span></div>
<div class="sig-row">
<button class="%v%" id="sign-%i%" type="button">%v2%</button>
<span class="val %v3%" id="sig-%i%">%v4%</span>
</div>`, { name: s.name, sk: s.sk, pk: s.pk, msg: s.msg, hm: s.hm, v: s.sigma !== null ? 'signed' : '', i: i, v2: s.sigma !== null ? `✓ Signed` : `Sign`, v3: s.sigma === null ? 'empty' : '', v4: s.sigma !== null ? s.sigma : '—' });
grid.appendChild(card);
document.getElementById(`sign-${i}`).addEventListener('click', () => {
// sigma_i = H(m_i)^sk_i mod p
s.sigma = modpow(s.hm, s.sk, p);
aggSigma = null;
document.getElementById('agg-val').textContent = '—';
document.getElementById('agg-val').className = '';
setStatus('', '');
renderSigners();
console.log(fmt(`%name% signed: sk=%sk% H(m)=%hm% sigma=%sigma%`, { name: s.name, sk: s.sk, hm: s.hm, sigma: s.sigma }));
});
});
}
function setStatus(msg, cls) {
const el = document.getElementById('verify-status');
el.textContent = msg;
el.className = 'status ' + cls;
}
document.getElementById('btn-agg').addEventListener('click', () => {
const signed = SIGNERS.filter(s => s.sigma !== null);
if (signed.length === 0) { setStatus(`Sign at least one message first.`, 'warn'); return; }
// Aggregate: product of sigmas mod p (group operation in Z_p*)
let agg = 1;
signed.forEach(s => { agg = agg * s.sigma % p; });
aggSigma = agg;
const el = document.getElementById('agg-val');
el.textContent = aggSigma;
el.className = 'set';
setStatus(fmt(`Aggregated %length% signature(s). Now verify.`, { length: signed.length }), 'warn');
console.log(`Aggregate sigma:`, aggSigma, 'from', signed.map(s => s.sigma).join(' * '), 'mod', p);
});
document.getElementById('btn-verify').addEventListener('click', () => {
if (aggSigma === null) { setStatus(`Aggregate first.`, 'warn'); return; }
const signed = SIGNERS.filter(s => s.sigma !== null);
// BLS verification in toy discrete-log model:
// Each sigma_i = H(m_i)^sk_i mod p, so log_g(sigma_i) = log_g(H(m_i)) * sk_i mod (p-1).
// Aggregate sigma_agg = product sigma_i mod p, so
// log_g(sigma_agg) = sum log_g(sigma_i) mod (p-1).
// Verification: check log_g(sigma_agg) == sum(log_g(H(m_i)) * sk_i) mod (p-1).
// But we don’t use sk_i directly in verify — we use pk_i = g^sk_i.
// In real BLS, the pairing e(H(m_i), pk_i) = e(H(m_i), g)^sk_i gives the same value.
// Here we verify by checking: sigma_agg == product H(m_i)^sk_i mod p using DL of pk_i.
// Since sk_i = DL(pk_i) we can reconstruct and check.
// This mirrors the pairing equation correctly.
let expected = 1;
signed.forEach(s => {
// H(m_i)^sk_i mod p — reconstructed from public key DL (simulates pairing)
const ski = DL.get(s.pk); // = s.sk, verified via discrete log table
expected = expected * modpow(s.hm, ski, p) % p;
});
const ok = (aggSigma === expected);
const allSigned = SIGNERS.every(s => s.sigma !== null);
if (ok && allSigned) {
setStatus(fmt(`✓ Valid! All %length% signatures verified with one aggregate check.`, { length: signed.length }), 'ok');
} else if (ok) {
setStatus(fmt(`✓ Valid for the %length% signer(s) who signed (partial aggregate).`, { length: signed.length }), 'ok');
} else {
setStatus(`✗ Invalid aggregate — verification failed.`, 'bad');
}
console.log(fmt(`Verify: aggSigma=%aggSigma% expected=%expected% ok=%ok%`, { aggSigma: aggSigma, expected: expected, ok: ok }));
});
renderSigners();
<p class="hint">
Each signer has a private key and public key. Click <b>Sign</b> to produce a signature. <b>Aggregate</b> collapses all signatures into one. <b>Verify</b> checks the aggregate against every public key at once — one combined check, not many.
</p>
<div id="signers-grid"></div>
<div class="agg-row">
<button id="btn-agg" type="button">Aggregate</button>
<div id="agg-box" class="agg-box">Aggregate: <span id="agg-val">—</span></div>
</div>
<div class="verify-row">
<button id="btn-verify" type="button">Verify</button>
<div id="verify-status" class="status">Sign all, then aggregate and verify.</div>
</div>
<p class="footnote">Numbers here are toy integers mod a small prime (p=997). Real BLS uses 256-bit elliptic-curve points on BLS12-381, but the structure — sign, add, verify with a pairing equation — is identical.</p>
* { 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 .8rem; line-height: 1.5; }
.footnote { font-size: .78rem; color: #777; margin-top: .8rem; line-height: 1.45; }
#signers-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 8px; margin-bottom: 10px; }
.signer-card { border: 1px solid #cdd9e3; border-radius: 10px; padding: 10px 12px; background: #f5f8fb; }
.signer-card h4 { margin: 0 0 6px; font-size: .9rem; color: #1d3557; }
.kv { display: flex; justify-content: space-between; font-size: .82rem; margin: 2px 0; color: #444; }
.kv .val { font-family: ui-monospace, monospace; color: #1d3557; font-weight: 600; }
.sig-row { display: flex; align-items: center; gap: 8px; margin-top: 8px; }
.sig-row .val { font-family: ui-monospace, monospace; font-weight: 600; color: #0a7d33; min-width: 36px; }
.sig-row .val.empty { color: #aaa; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; white-space: nowrap; }
button.ghost { background: #fff; color: #1d3557; }
button.signed { background: #0a7d33; border-color: #0a7d33; }
.agg-row { display: flex; align-items: center; gap: 10px; margin: 6px 0; flex-wrap: wrap; }
.agg-box { font-size: .9rem; color: #444; }
.agg-box span { font-family: ui-monospace, monospace; font-weight: 700; color: #e63946; }
.agg-box span.set { color: #1d3557; }
.verify-row { display: flex; align-items: center; gap: 10px; margin-top: 6px; flex-wrap: wrap; }
.status { font-size: .95rem; font-weight: 600; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.status.warn { color: #b45309; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Toy BLS simulation using modular arithmetic.
// Real BLS uses elliptic-curve groups and pairings on BLS12-381.
// Here we model everything mod a small prime p so the numbers are readable.
//
// Groups G1 = Z_p* with generator g=7, G2 = same group, GT = same group.
// Pairing e(a, b) = g^(a*b mod (p-1)) mod p (discrete-log based toy pairing).
// Private key sk in [1, p-2]. Public key pk = g^sk mod p.
// Hash-to-point H(msg_index) = g^(53+msg_index*31) mod p (fixed toy hash).
// Sign: sigma = H(msg)^sk mod p.
// Aggregate: sigma_agg = sigma1 * sigma2 * ... mod p (group operation = multiply).
// Verify aggregate: e(sigma_agg, g) == product of e(H(mi), pki).
// In toy model: sigma_agg^? ... we verify the discrete-log relationship directly
// to avoid needing a true pairing. The demo verifies the correct BLS equation
// in the discrete-log domain: sum of (sk_i * log H(m_i)) == log sigma_agg.
const p = 997; // prime modulus
const g = 7; // generator of Z_p*
function modpow(base, exp, mod) {
base = ((base % mod) + mod) % mod;
exp = BigInt(exp); mod = BigInt(mod); base = BigInt(base);
let result = 1n;
while (exp > 0n) {
if (exp & 1n) result = result * base % mod;
base = base * base % mod;
exp >>= 1n;
}
return Number(result);
}
// Toy hash-to-"point": deterministic integer in Z_p* per (signer_index, msg)
function hashToPoint(signerIdx, msg) {
// msg is a short string; convert to a small integer via simple sum
let h = 0;
for (let i = 0; i < msg.length; i++) h = (h * 31 + msg.charCodeAt(i)) % (p - 1);
h = ((h + signerIdx * 53 + 7) % (p - 1)) + 1; // ensure [1, p-1]
return modpow(g, h, p); // H(m) = g^h mod p
}
// Discrete log table for g mod p (p=997 is small enough)
const DL = new Map();
let cur = 1;
for (let i = 0; i < p - 1; i++) {
DL.set(cur, i);
cur = cur * g % p;
}
const SIGNERS = [
{ name: 'Alice', sk: 42, msg: 'I approve tx #1' },
{ name: 'Bob', sk: 137, msg: 'I approve tx #2' },
{ name: 'Carol', sk: 251, msg: 'I approve tx #3' },
{ name: 'Dave', sk: 89, msg: 'I approve tx #4' },
];
// Precompute public keys and H(msg) points
SIGNERS.forEach((s, i) => {
s.pk = modpow(g, s.sk, p);
s.hm = hashToPoint(i, s.msg);
s.sigma = null; // not yet signed
});
let aggSigma = null;
function renderSigners() {
const grid = document.getElementById('signers-grid');
grid.innerHTML = '';
SIGNERS.forEach((s, i) => {
const card = document.createElement('div');
card.className = 'signer-card';
card.innerHTML = "\n " + fmt(`<h4>%name%</h4>
<div class="kv"><span>Private key sk</span><span class="val">%sk%</span></div>
<div class="kv"><span>Public key pk = g^sk</span><span class="val">%pk%</span></div>
<div class="kv"><span>Message</span><span class="val" style="font-size:.75rem;font-family:system-ui">%msg%</span></div>
<div class="kv"><span>H(msg) point</span><span class="val">%hm%</span></div>
<div class="sig-row">
<button class="%v%" id="sign-%i%" type="button">%v2%</button>
<span class="val %v3%" id="sig-%i%">%v4%</span>
</div>`, { name: s.name, sk: s.sk, pk: s.pk, msg: s.msg, hm: s.hm, v: s.sigma !== null ? 'signed' : '', i: i, v2: s.sigma !== null ? `✓ Signed` : `Sign`, v3: s.sigma === null ? 'empty' : '', v4: s.sigma !== null ? s.sigma : '—' });
grid.appendChild(card);
document.getElementById(`sign-${i}`).addEventListener('click', () => {
// sigma_i = H(m_i)^sk_i mod p
s.sigma = modpow(s.hm, s.sk, p);
aggSigma = null;
document.getElementById('agg-val').textContent = '—';
document.getElementById('agg-val').className = '';
setStatus('', '');
renderSigners();
console.log(fmt(`%name% signed: sk=%sk% H(m)=%hm% sigma=%sigma%`, { name: s.name, sk: s.sk, hm: s.hm, sigma: s.sigma }));
});
});
}
function setStatus(msg, cls) {
const el = document.getElementById('verify-status');
el.textContent = msg;
el.className = 'status ' + cls;
}
document.getElementById('btn-agg').addEventListener('click', () => {
const signed = SIGNERS.filter(s => s.sigma !== null);
if (signed.length === 0) { setStatus(`Sign at least one message first.`, 'warn'); return; }
// Aggregate: product of sigmas mod p (group operation in Z_p*)
let agg = 1;
signed.forEach(s => { agg = agg * s.sigma % p; });
aggSigma = agg;
const el = document.getElementById('agg-val');
el.textContent = aggSigma;
el.className = 'set';
setStatus(fmt(`Aggregated %length% signature(s). Now verify.`, { length: signed.length }), 'warn');
console.log(`Aggregate sigma:`, aggSigma, 'from', signed.map(s => s.sigma).join(' * '), 'mod', p);
});
document.getElementById('btn-verify').addEventListener('click', () => {
if (aggSigma === null) { setStatus(`Aggregate first.`, 'warn'); return; }
const signed = SIGNERS.filter(s => s.sigma !== null);
// BLS verification in toy discrete-log model:
// Each sigma_i = H(m_i)^sk_i mod p, so log_g(sigma_i) = log_g(H(m_i)) * sk_i mod (p-1).
// Aggregate sigma_agg = product sigma_i mod p, so
// log_g(sigma_agg) = sum log_g(sigma_i) mod (p-1).
// Verification: check log_g(sigma_agg) == sum(log_g(H(m_i)) * sk_i) mod (p-1).
// But we don’t use sk_i directly in verify — we use pk_i = g^sk_i.
// In real BLS, the pairing e(H(m_i), pk_i) = e(H(m_i), g)^sk_i gives the same value.
// Here we verify by checking: sigma_agg == product H(m_i)^sk_i mod p using DL of pk_i.
// Since sk_i = DL(pk_i) we can reconstruct and check.
// This mirrors the pairing equation correctly.
let expected = 1;
signed.forEach(s => {
// H(m_i)^sk_i mod p — reconstructed from public key DL (simulates pairing)
const ski = DL.get(s.pk); // = s.sk, verified via discrete log table
expected = expected * modpow(s.hm, ski, p) % p;
});
const ok = (aggSigma === expected);
const allSigned = SIGNERS.every(s => s.sigma !== null);
if (ok && allSigned) {
setStatus(fmt(`✓ Valid! All %length% signatures verified with one aggregate check.`, { length: signed.length }), 'ok');
} else if (ok) {
setStatus(fmt(`✓ Valid for the %length% signer(s) who signed (partial aggregate).`, { length: signed.length }), 'ok');
} else {
setStatus(`✗ Invalid aggregate — verification failed.`, 'bad');
}
console.log(fmt(`Verify: aggSigma=%aggSigma% expected=%expected% ok=%ok%`, { aggSigma: aggSigma, expected: expected, ok: ok }));
});
renderSigners();
Comments
Loading comments...