The demo below models a (t,n) threshold scheme with small toy numbers. Each participant holds a key share — a piece of the private key. To sign a message, at least t participants each produce a partial signature using their share. When t partial signatures are combined they aggregate into one valid signature; with fewer than t shares the combination fails.
<!-- Demo layout: controls for t/n, participant cards, action buttons, status and signature display -->
<p class="hint">Each participant holds a <b>key share</b> — a piece of the private key. Click participants to toggle them on/off. Press <b>Sign</b> to combine partial signatures: exactly <i>t</i> active participants succeed; fewer always fail.</p>
<div class="controls">
<label>
<span class="lbl">t (min. signers)</span>
<input type="range" id="tSlider" min="2" max="4" value="3" step="1">
<span id="tVal" class="val">3</span>
</label>
<label>
<span class="lbl">n (total parties)</span>
<input type="range" id="nSlider" min="3" max="6" value="5" step="1">
<span id="nVal" class="val">5</span>
</label>
</div>
<div id="participants" class="participants"></div>
<div class="btns">
<button id="signBtn" type="button">Sign</button>
<button id="resetBtn" type="button" class="ghost">New key</button>
</div>
<div id="status" class="status"></div>
<div id="sigDisplay" class="sig-display"></div>
/* Styles: controls, participant cards (active = dark), status colors, monospace sig display */
* { 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; }
.controls { display: flex; gap: 1.4rem; flex-wrap: wrap; margin-bottom: .8rem; }
.controls label { display: flex; align-items: center; gap: .5rem; }
.lbl { font-weight: 600; color: #1d3557; min-width: 7rem; }
.val { font: 700 1rem ui-monospace, monospace; color: #1d3557; min-width: 1.4rem; }
input[type=range] { accent-color: #1d3557; width: 110px; }
.participants { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .8rem; }
.party { display: flex; flex-direction: column; align-items: center; gap: .3rem;
background: #e8eef3; border: 2px solid #cdd9e3; border-radius: 10px;
padding: .5rem .7rem; cursor: pointer; user-select: none; transition: all .15s; min-width: 60px; }
.party.active { background: #1d3557; border-color: #1d3557; color: #fff; }
.party .pid { font: 700 1rem ui-monospace, monospace; }
.party .share-lbl { font-size: .72rem; opacity: .75; }
.party .partial { font-size: .72rem; font-weight: 600; min-height: 1em; }
.party.active .partial { color: #a8d8b0; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .6rem; }
button { font: 600 14px system-ui; padding: .45rem 1rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.status { font-size: 1rem; font-weight: 600; min-height: 1.4em; margin-bottom: .4rem; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.sig-display { font: .8rem ui-monospace, monospace; color: #555; word-break: break-all;
background: #f4f7fa; border-radius: 6px; padding: .5rem .7rem; min-height: 2rem; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Toy threshold signing over Z/P — models Shamir secret sharing + additive Lagrange aggregation
// Small prime P so all arithmetic stays in safe integer range
const P = 65537;
// Modular arithmetic helpers
const mod = (a, m) => ((a % m) + m) % m;
const modInv = (a, m) => {
// Extended Euclidean algorithm for modular inverse
let [old_r, r, old_s, s] = [a, m, 1, 0];
while (r !== 0) { const q = Math.floor(old_r / r); [old_r, r] = [r, old_r - q * r]; [old_s, s] = [s, old_s - q * s]; }
return mod(old_s, m);
};
// Lagrange interpolation coefficient for participant i given the set xs of signers
function lagrangeCoeff(i, xs, p) {
let num = 1, den = 1;
for (const j of xs) {
if (j === i) continue;
num = mod(num * (-j), p);
den = mod(den * (i - j), p);
}
return mod(num * modInv(den, p), p);
}
// Global state: threshold T, party count N, secret key, key shares, active bitmask
const MESSAGE = 42;
let T = 3, N = 5;
let secretKey = 0, keyShares = [], active = [];
function setup() {
// Sample a random degree-(T-1) polynomial over Z/P with f(0) = secretKey
secretKey = 1 + Math.floor(Math.random() * (P - 2));
const coeffs = [secretKey];
for (let i = 1; i < T; i++) coeffs.push(1 + Math.floor(Math.random() * (P - 2)));
// Evaluate f at each party index i (1-indexed) to get the key share
keyShares = [];
for (let i = 1; i <= N; i++) {
let val = 0, xi = 1;
for (const c of coeffs) { val = mod(val + c * xi, P); xi = mod(xi * i, P); }
keyShares.push(val);
}
active = Array.from({ length: N }, (_, i) => i < T);
}
function renderParties() {
const div = document.getElementById('participants');
div.innerHTML = '';
for (let i = 0; i < N; i++) {
const el = document.createElement('div');
el.className = 'party' + (active[i] ? ' active' : '');
el.innerHTML = `<span class="pid">P${i + 1}</span><span class="share-lbl">key share</span><span class="partial" id="partial${i}"></span>`;
el.addEventListener('click', () => { active[i] = !active[i]; clearResult(); renderParties(); });
div.appendChild(el);
}
}
function clearResult() {
document.getElementById('status').textContent = '';
document.getElementById('status').className = 'status';
document.getElementById('sigDisplay').textContent = '';
for (let i = 0; i < N; i++) { const p = document.getElementById('partial' + i); if (p) p.textContent = ''; }
}
function sign() {
const signers = active.map((a, i) => a ? i + 1 : null).filter(Boolean);
const count = signers.length;
const st = document.getElementById('status');
if (count < T) {
st.textContent = i18nFmt('Need %t% active participants — only %count% selected. Cannot sign.', { count, t: T });
st.className = 'status bad';
document.getElementById('sigDisplay').textContent = '';
return;
}
// Signing: each signer contributes z_i = share_i * lagrange_coeff_i; sum = secretKey
const xs = signers.slice(0, T);
const partials = [];
for (const i of xs) {
const lc = lagrangeCoeff(i, xs, P);
// Partial signature: Lagrange-weighted key share (toy model; real FROST adds nonce terms)
const z_i = mod(keyShares[i - 1] * lc, P);
partials.push({ i, z_i, lc });
const el = document.getElementById('partial' + (i - 1));
if (el) el.textContent = `z=${z_i % 1000}…`;
}
// Aggregate partial signatures — sum equals secretKey by Lagrange interpolation
let z = 0;
for (const { z_i } of partials) z = mod(z + z_i, P);
const valid = z === secretKey;
if (valid) {
st.textContent = i18nFmt('Valid signature! %t% partial signatures combined correctly.', { t: xs.length });
st.className = 'status ok';
document.getElementById('sigDisplay').textContent = `z = ${z} (secret s = ${secretKey}) ✓`;
} else {
st.textContent = 'Aggregation error — this should not happen.';
st.className = 'status bad';
document.getElementById('sigDisplay').textContent = `z = ${z} ≠ ${secretKey}`;
}
}
function updateSliders() {
T = parseInt(document.getElementById('tSlider').value);
N = parseInt(document.getElementById('nSlider').value);
if (T > N) { T = N; document.getElementById('tSlider').value = T; }
document.getElementById('tVal').textContent = T;
document.getElementById('nVal').textContent = N;
setup();
clearResult();
renderParties();
}
document.getElementById('tSlider').addEventListener('input', updateSliders);
document.getElementById('nSlider').addEventListener('input', updateSliders);
document.getElementById('signBtn').addEventListener('click', sign);
document.getElementById('resetBtn').addEventListener('click', () => { setup(); clearResult(); renderParties(); });
setup();
renderParties();
<!-- Demo layout: controls for t/n, participant cards, action buttons, status and signature display -->
<p class="hint">Each participant holds a <b>key share</b> — a piece of the private key. Click participants to toggle them on/off. Press <b>Sign</b> to combine partial signatures: exactly <i>t</i> active participants succeed; fewer always fail.</p>
<div class="controls">
<label>
<span class="lbl">t (min. signers)</span>
<input type="range" id="tSlider" min="2" max="4" value="3" step="1">
<span id="tVal" class="val">3</span>
</label>
<label>
<span class="lbl">n (total parties)</span>
<input type="range" id="nSlider" min="3" max="6" value="5" step="1">
<span id="nVal" class="val">5</span>
</label>
</div>
<div id="participants" class="participants"></div>
<div class="btns">
<button id="signBtn" type="button">Sign</button>
<button id="resetBtn" type="button" class="ghost">New key</button>
</div>
<div id="status" class="status"></div>
<div id="sigDisplay" class="sig-display"></div>
/* Styles: controls, participant cards (active = dark), status colors, monospace sig display */
* { 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; }
.controls { display: flex; gap: 1.4rem; flex-wrap: wrap; margin-bottom: .8rem; }
.controls label { display: flex; align-items: center; gap: .5rem; }
.lbl { font-weight: 600; color: #1d3557; min-width: 7rem; }
.val { font: 700 1rem ui-monospace, monospace; color: #1d3557; min-width: 1.4rem; }
input[type=range] { accent-color: #1d3557; width: 110px; }
.participants { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .8rem; }
.party { display: flex; flex-direction: column; align-items: center; gap: .3rem;
background: #e8eef3; border: 2px solid #cdd9e3; border-radius: 10px;
padding: .5rem .7rem; cursor: pointer; user-select: none; transition: all .15s; min-width: 60px; }
.party.active { background: #1d3557; border-color: #1d3557; color: #fff; }
.party .pid { font: 700 1rem ui-monospace, monospace; }
.party .share-lbl { font-size: .72rem; opacity: .75; }
.party .partial { font-size: .72rem; font-weight: 600; min-height: 1em; }
.party.active .partial { color: #a8d8b0; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .6rem; }
button { font: 600 14px system-ui; padding: .45rem 1rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.status { font-size: 1rem; font-weight: 600; min-height: 1.4em; margin-bottom: .4rem; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.sig-display { font: .8rem ui-monospace, monospace; color: #555; word-break: break-all;
background: #f4f7fa; border-radius: 6px; padding: .5rem .7rem; min-height: 2rem; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Toy threshold signing over Z/P — models Shamir secret sharing + additive Lagrange aggregation
// Small prime P so all arithmetic stays in safe integer range
const P = 65537;
// Modular arithmetic helpers
const mod = (a, m) => ((a % m) + m) % m;
const modInv = (a, m) => {
// Extended Euclidean algorithm for modular inverse
let [old_r, r, old_s, s] = [a, m, 1, 0];
while (r !== 0) { const q = Math.floor(old_r / r); [old_r, r] = [r, old_r - q * r]; [old_s, s] = [s, old_s - q * s]; }
return mod(old_s, m);
};
// Lagrange interpolation coefficient for participant i given the set xs of signers
function lagrangeCoeff(i, xs, p) {
let num = 1, den = 1;
for (const j of xs) {
if (j === i) continue;
num = mod(num * (-j), p);
den = mod(den * (i - j), p);
}
return mod(num * modInv(den, p), p);
}
// Global state: threshold T, party count N, secret key, key shares, active bitmask
const MESSAGE = 42;
let T = 3, N = 5;
let secretKey = 0, keyShares = [], active = [];
function setup() {
// Sample a random degree-(T-1) polynomial over Z/P with f(0) = secretKey
secretKey = 1 + Math.floor(Math.random() * (P - 2));
const coeffs = [secretKey];
for (let i = 1; i < T; i++) coeffs.push(1 + Math.floor(Math.random() * (P - 2)));
// Evaluate f at each party index i (1-indexed) to get the key share
keyShares = [];
for (let i = 1; i <= N; i++) {
let val = 0, xi = 1;
for (const c of coeffs) { val = mod(val + c * xi, P); xi = mod(xi * i, P); }
keyShares.push(val);
}
active = Array.from({ length: N }, (_, i) => i < T);
}
function renderParties() {
const div = document.getElementById('participants');
div.innerHTML = '';
for (let i = 0; i < N; i++) {
const el = document.createElement('div');
el.className = 'party' + (active[i] ? ' active' : '');
el.innerHTML = `<span class="pid">P${i + 1}</span><span class="share-lbl">key share</span><span class="partial" id="partial${i}"></span>`;
el.addEventListener('click', () => { active[i] = !active[i]; clearResult(); renderParties(); });
div.appendChild(el);
}
}
function clearResult() {
document.getElementById('status').textContent = '';
document.getElementById('status').className = 'status';
document.getElementById('sigDisplay').textContent = '';
for (let i = 0; i < N; i++) { const p = document.getElementById('partial' + i); if (p) p.textContent = ''; }
}
function sign() {
const signers = active.map((a, i) => a ? i + 1 : null).filter(Boolean);
const count = signers.length;
const st = document.getElementById('status');
if (count < T) {
st.textContent = i18nFmt('Need %t% active participants — only %count% selected. Cannot sign.', { count, t: T });
st.className = 'status bad';
document.getElementById('sigDisplay').textContent = '';
return;
}
// Signing: each signer contributes z_i = share_i * lagrange_coeff_i; sum = secretKey
const xs = signers.slice(0, T);
const partials = [];
for (const i of xs) {
const lc = lagrangeCoeff(i, xs, P);
// Partial signature: Lagrange-weighted key share (toy model; real FROST adds nonce terms)
const z_i = mod(keyShares[i - 1] * lc, P);
partials.push({ i, z_i, lc });
const el = document.getElementById('partial' + (i - 1));
if (el) el.textContent = `z=${z_i % 1000}…`;
}
// Aggregate partial signatures — sum equals secretKey by Lagrange interpolation
let z = 0;
for (const { z_i } of partials) z = mod(z + z_i, P);
const valid = z === secretKey;
if (valid) {
st.textContent = i18nFmt('Valid signature! %t% partial signatures combined correctly.', { t: xs.length });
st.className = 'status ok';
document.getElementById('sigDisplay').textContent = `z = ${z} (secret s = ${secretKey}) ✓`;
} else {
st.textContent = 'Aggregation error — this should not happen.';
st.className = 'status bad';
document.getElementById('sigDisplay').textContent = `z = ${z} ≠ ${secretKey}`;
}
}
function updateSliders() {
T = parseInt(document.getElementById('tSlider').value);
N = parseInt(document.getElementById('nSlider').value);
if (T > N) { T = N; document.getElementById('tSlider').value = T; }
document.getElementById('tVal').textContent = T;
document.getElementById('nVal').textContent = N;
setup();
clearResult();
renderParties();
}
document.getElementById('tSlider').addEventListener('input', updateSliders);
document.getElementById('nSlider').addEventListener('input', updateSliders);
document.getElementById('signBtn').addEventListener('click', sign);
document.getElementById('resetBtn').addEventListener('click', () => { setup(); clearResult(); renderParties(); });
setup();
renderParties();
Adjust t and n with the sliders, toggle individual participants on or off, then click Sign. Notice that exactly t active participants succeed; t−1 always fails. The signature produced is identical in structure no matter which t participants contribute.
Comments
Loading comments...