Skip to main content

Try it Live

Run BLS12-381 examples in the interactive playground
Future Plans: This page is planned and under active development. Examples are placeholders and will be replaced with accurate, tested content.

Signature Aggregation

BLS signature aggregation is the killer feature enabling Ethereum’s proof-of-stake consensus with thousands of validators.

Benefits

  • Bandwidth: n signatures → 1 signature (48 bytes vs 48n bytes)
  • Verification: 1 pairing check vs n checks
  • Storage: Constant size regardless of validator count
  • Non-interactive: No coordination required

Aggregation Strategies

Same Message Aggregation

All validators sign identical message (beacon block):
const blockRoot = computeBlockRoot(block);
const signatures = validators.map(v => v.sign(blockRoot));
const aggregated = await aggregateSignatures(signatures);
// Size: 48 bytes regardless of validator count
Verification: Single pairing check after aggregating public keys

Different Message Aggregation

Each validator signs different attestation:
// Attest to different source/target checkpoints
const attestations = validators.map((v, i) => ({
  signature: v.sign(attestationData[i]),
  data: attestationData[i]
}));
Verification: Multi-pairing check (n+1 pairings)

Ethereum Use Cases

Sync Committee (512 validators)

interface SyncAggregate {
  syncCommitteeBits: BitVector[512];  // Participation flags
  syncCommitteeSignature: Signature;  // Aggregated 48 bytes
}

async function aggregateSyncCommittee(
  validators: Validator[],
  blockRoot: Uint8Array
): Promise<SyncAggregate> {
  const signatures: Uint8Array[] = [];
  const bits: boolean[] = [];

  for (let i = 0; i < 512; i++) {
    if (validators[i].isOnline()) {
      signatures.push(await validators[i].sign(blockRoot));
      bits[i] = true;
    } else {
      bits[i] = false;
    }
  }

  return {
    syncCommitteeBits: bits,
    syncCommitteeSignature: await aggregateSignatures(signatures)
  };
}
Result: 512 signatures → 48 bytes + 64 byte bitfield

Attestation Aggregation

// Aggregate attestations for same epoch/slot
const aggregatedAttestation = {
  aggregationBits: BitList[MAX_VALIDATORS],
  data: AttestationData,
  signature: AggregateSignature  // All attesting validators
};

Optimizations

Incremental Aggregation

Add signatures one-by-one as they arrive:
class SignatureAggregator {
  private current: Uint8Array | null = null;

  async add(signature: Uint8Array): Promise<void> {
    if (this.current === null) {
      this.current = signature;
    } else {
      const input = new Uint8Array(256);
      input.set(this.current, 0);
      input.set(signature, 128);

      const output = new Uint8Array(128);
      await bls12_381.g1Add(input, output);
      this.current = output;
    }
  }

  getAggregate(): Uint8Array | null {
    return this.current;
  }
}

Precomputed Public Key Aggregates

Cache aggregated public keys for known validator sets:
const syncCommitteePubkeyCache = new Map<number, Uint8Array>();

async function getAggregatedPubkey(
  epoch: number,
  participants: boolean[]
): Promise<Uint8Array> {
  const cacheKey = hashParticipants(epoch, participants);

  if (!syncCommitteePubkeyCache.has(cacheKey)) {
    const pubkeys = getSyncCommittee(epoch)
      .filter((_, i) => participants[i]);
    const aggregated = await aggregateG2Points(pubkeys);
    syncCommitteePubkeyCache.set(cacheKey, aggregated);
  }

  return syncCommitteePubkeyCache.get(cacheKey)!;
}

Security

Rogue Key Attacks

Prevention: Proof-of-possession required at validator deposit
// Validator must prove they know private key
const pop = await generateProofOfPossession(privkey, pubkey);

// Verified before allowing validator registration
const isValid = await verifyProofOfPossession(pubkey, pop);

Aggregate Verification

async function verifyAggregateSignature(
  signature: Uint8Array,
  publicKeys: Uint8Array[],
  messages: Uint8Array[]
): Promise<boolean> {
  // Check prevents rogue key attack
  // All pubkeys must have valid proof-of-possession

  if (publicKeys.length !== messages.length) {
    throw new Error("Mismatched pubkeys and messages");
  }

  // Build multi-pairing check
  return batchVerifySignatures(
    [signature],
    publicKeys,
    messages
  );
}

Performance

Aggregation (100 signatures):
  • Time: ~1.5 ms (15 μs per addition)
  • Result: Single 48-byte signature
Verification:
  • Individual: ~2ms × 100 = 200ms
  • Aggregated (same msg): ~2ms
  • Aggregated (diff msg): ~2ms + 23ms × 100 = ~2.3s
Savings: 100x faster for same-message verification