Introduction

Every time you send a signed message — a bank transfer, a vote, a blockchain transaction — the recipient must verify a signature. In most systems, if 1,000 people sign 1,000 different messages, the verifier must check 1,000 separate signatures. That cost is linear: twice the signers, twice the work.

BLS signatures (Boneh–Lynn–Shacham, 2001) break that rule. Using a piece of mathematics called an elliptic-curve pairing, you can aggregate any number of signatures — possibly from thousands of different public keys, signing different messages — into a single constant-size signature. The verifier checks one pairing equation and is done.

The saving is not just a constant factor. In systems like Ethereum's consensus layer, where hundreds of thousands of validators must collectively sign every block, the difference between "one signature per validator" and "one aggregate signature per block" is the difference between grinding to a halt and running in real time.

This article walks you through the intuition: what a pairing is, why aggregation works, and why the security rests on a hardness assumption related to the discrete logarithm problem on elliptic curves.

Aggregate and Verify

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();
Result
Console

Notice that after aggregation you have exactly one number regardless of how many signers contributed. Verification still confirms every individual message was signed by the correct key — that is the pairing magic at work. Click individual Sign buttons, then aggregate, then verify to see it succeed. Try verifying before all signers have signed to see it fail.

The Real Complexity

What makes BLS tick — and what keeps it honest?

The pairing e(P,Q)e(P, Q). A bilinear pairing takes two points on elliptic curves and returns a value in a finite field. "Bilinear" means: e(aP,bQ)=e(P,Q)abe(aP, bQ) = e(P, Q)^{ab}. This one identity is the entire engine of BLS.

  • Key generation: each signer picks a random private key sk and computes the public key pk = sk · G, where G is a fixed curve base point — a standard elliptic-curve scalar multiplication.
  • Signing: to sign message m, the signer hashes m to a curve point H(m) and computes the signature σ=sk⋅H(m)\sigma = sk \cdot H(m). This is also just scalar multiplication.
  • Aggregation: add all individual signatures as curve points —

    σagg=σ1+σ2+⋯+σn.\sigma_{\mathrm{agg}} = \sigma_1 + \sigma_2 + \cdots + \sigma_n.

    Addition on an elliptic curve is fast and constant-cost per signer.
  • Verification: check that

    e(σagg, G)=e(H(m1),pk1)⋅e(H(m2),pk2)⋯e(H(mn),pkn).e(\sigma_{\mathrm{agg}},\, G) = e(H(m_1), pk_1) \cdot e(H(m_2), pk_2) \cdots e(H(m_n), pk_n).

    Each pairing evaluation is the expensive step, but there are only n of them instead of n full verifications.

Security. The scheme is proven secure in the random oracle model under the co-Computational Diffie–Hellman (co-CDH) assumption: given GG, aGaG and H(m)H(m), it is hard to compute a⋅H(m)a \cdot H(m) without knowing a — a close cousin of the discrete logarithm problem. Breaking BLS would break co-CDH on the underlying curve (e.g. BLS12-381, the curve used by Ethereum).

The rogue-key attack. Naïve aggregation of public keys is vulnerable: a malicious signer can announce a "public key" that cancels out honest signers' contributions, forging a valid aggregate. The fix — requiring each signer to prove they hold the private key via a Proof of Possession — is mandatory in production. Ethereum's deposit contract enforces this.

Aggregation across messages vs a single message. Aggregating n signatures on n different messages requires n pairing evaluations on the verifier side. Aggregating n signatures on the same message reduces verification to just two pairings total — a massive further saving used in threshold and multi-signature settings.

Where It Matters

Wherever a crowd must speak with one voice and bandwidth is precious, BLS signatures show up:

  • Ethereum's consensus layer (beacon chain): roughly 500,000 validators each attest to the canonical chain every 12 seconds. Without aggregation the network would need to broadcast and process ~500,000 individual signatures per slot. With BLS aggregation, each committee collapses to a single aggregate signature — the most visible deployment of BLS at scale.
  • Byzantine Fault Tolerant (BFT) consensus: protocols like Tendermint and HotStuff use threshold BLS to let a committee of n nodes produce a single "quorum certificate" signature once ⌈2n/3⌉ signers agree, without any trusted dealer.
  • Threshold and multi-signature wallets: a multi-sig where k of n keyholders must co-sign a transaction can present one aggregate BLS signature on-chain, paying a flat verification cost instead of k separate checks.
  • Certificate transparency and audit logs: BLS lets many independent auditors co-sign a batch of log entries; the single aggregate is stored and verified cheaply.
  • Post-quantum caution: BLS relies on elliptic-curve pairings, which quantum computers could break with Shor's algorithm (see Shor's algorithm). The field is actively researching lattice-based aggregatable signatures as replacements.

In all these cases the key property is the same: aggregation is provably sound — no amount of clever manipulation by a subset of signers can produce a valid aggregate that includes a message no honest key ever signed.

Conclusion

BLS signatures are a beautiful example of mathematics earning its keep in engineering. A single algebraic identity — e(aP,bQ)=e(P,Q)abe(aP, bQ) = e(P, Q)^{ab} — makes it possible to collapse an arbitrarily large crowd of signatures into one compact proof that a verifier can check with a handful of pairing evaluations.

The scheme is provably secure, linearly efficient in aggregation, and already deployed at planetary scale: every Ethereum block carries aggregated BLS attestations from hundreds of thousands of validators. Understanding BLS means understanding how modern blockchains afford to be decentralized at all.

The open frontier is quantum resistance. If large-scale quantum computers arrive, the co-CDH assumption breaks, and the field will need aggregatable signatures built on harder problems — likely lattices. Until then, BLS remains the gold standard for signature aggregation, and the pairing trick at its heart is one of the most elegant ideas in modern cryptography.

Share this article

Pick a channel — or use your device's native share sheet.

Comments

Loading comments...

https://www.kipuhub.com/en/article/bls-signatures/Content licensed under CC BY-NC 4.0.