BLS12-381: One Curve for Consensus and Privacy

⚠️ Disclaimer: I’m learning math behind modern validator engines. Math shared here has been copied from the respective sources, and it takes an Eon to truly grok it.

BLS12-381 is a pairing-friendly elliptic curve. It appears in Eth2 validator signatures, Filecoin storage proofs, Zcash shielded transactions, and - closer to the subject of this essay - in Commonware’s cryptographic toolkit for BFT consensus. The same algebraic structure that makes BLS threshold signatures possible also underpins several families of zero-knowledge proofs.

This essay traces that shared structure: pairings. It then shows how Commonware’s commonware-cryptography crate implements the consensus side - key generation, signing, threshold recovery, and timelock encryption - all built on top of the same curve that ZK systems rely on.

Pairings in 30 Seconds

A bilinear pairing is a function $e: \mathbb{G}_1 \times \mathbb{G}_2 \to \mathbb{G}_T$ that satisfies $e(aP, bQ) = e(P, Q)^{ab}$ for generators $P \in \mathbb{G}_1$, $Q \in \mathbb{G}_2$, and scalars $a, b$.

This single property enables two things at once:

  • Signature aggregation. Given signatures $\sigma_i = s_i \cdot H(m)$ from different signers, anyone can check the aggregate $\sigma = \sum \sigma_i$ against the aggregate public key via one pairing equation. No interaction between signers is required after key setup.
  • Proof verification. Groth16, PLONK, and Halo2 (on curves with pairings) encode a computation as polynomial commitments and check them with a constant number of pairing operations. The verifier never learns the witness.

BLS12-381 provides groups $\mathbb{G}_1$ (48-byte points), $\mathbb{G}_2$ (96-byte points), and $\mathbb{G}_T$ (384-byte elements) with ~128-bit security. These sizes matter: a BLS signature is 96 bytes (or 48 bytes in the min-sig variant), and a Groth16 proof is three group elements (~192 bytes total).

Consensus: BLS Threshold Signatures

Commonware’s BFT consensus (Simplex) relies on threshold BLS signatures to certify blocks. A validator set of $n$ participants shares a group key via Distributed Key Generation (DKG). Each validator holds a share of the private key; no single validator ever sees the full secret. When $t$ validators sign the same block, their partial signatures are interpolated into one threshold signature - verifiable against one group public key.

Signing and Verifying

The commonware-cryptography crate wraps the blst library behind Rust traits (Signer, Verifier) that are generic across curves. For BLS12-381 specifically:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
use commonware_cryptography::{
    bls12381, Signer as _, Verifier as _,
};
use commonware_math::algebra::Random;
use rand::rngs::OsRng;

let signer = bls12381::PrivateKey::random(&mut OsRng);

let namespace = b"consensus";
let msg = b"block-hash-abc123";

let sig = signer.sign(namespace, msg);
assert!(signer.public_key().verify(namespace, msg, &sig));

The namespace parameter prevents cross-domain replay: a signature produced for b"consensus" will not verify under b"execution", even for the same message bytes.

Distributed Key Generation

Before threshold signing can happen, the validator set runs a Joint-Feldman DKG. Each dealer broadcasts a polynomial commitment and sends private shares to each player over an authenticated channel. Players verify their shares against the commitment and return signed acknowledgements.

The crate exposes Dealer and Player state machines for the interactive protocol, but also a deal function for trusted single-dealer setups (useful in tests and devnets):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
use commonware_cryptography::bls12381::{
    dkg,
    primitives::{variant::MinPk, sharing::Mode},
};
use commonware_utils::{ordered::Set, N3f1};
use rand::rngs::OsRng;

let players: Set<u32> = (0..4).try_collect().unwrap();

let (output, shares) =
    dkg::deal::<MinPk, _, N3f1>(&mut OsRng, Mode::default(), players)
        .expect("deal should succeed");

// output.public() is the group public key
// shares maps each player to their private Share

N3f1 is Commonware’s fault model: $n \geq 3f + 1$, meaning the threshold is $2f + 1$. With four players, one can be Byzantine without compromising liveness or safety.

Threshold Recovery

After each validator signs a message with its share, a combiner collects $t$ partial signatures and recovers the full threshold signature via Lagrange interpolation in $\mathbb{G}_2$:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
use commonware_cryptography::bls12381::primitives::ops::threshold;
use commonware_utils::N3f1;
use commonware_parallel::Sequential;

// Given `sharing` (from DKG output) and `partials` (collected
// partial signatures from >= threshold participants):

let recovered_sig = threshold::recover::<MinPk, _, N3f1>(
    &sharing,
    &partials,
    &Sequential,
).expect("enough valid partials");

The recovered signature is deterministic: any subset of $t$ valid partial signatures produces the same output. This is critical for consensus, where multiple nodes must independently arrive at the same certified block.

Signature Aggregation

Separate from threshold signatures, the crate also supports standard BLS aggregation - combining independent signatures from distinct signers over the same message:

1
2
3
4
5
6
7
8
use commonware_cryptography::bls12381::primitives::ops::aggregate;

let agg_pk = aggregate::combine_public_keys::<MinPk, _>(&public_keys);
let agg_sig = aggregate::combine_signatures::<MinPk, _>(&signatures);

aggregate::verify_same_message::<MinPk>(
    &agg_pk, namespace, message, &agg_sig,
).expect("aggregate valid");

Verification costs one pairing regardless of how many signers contributed - O(1) instead of O(n). This is what makes BLS aggregation attractive for block certificates with large validator sets.

Privacy: ZK-SNARKs on the Same Curve

Groth16 - the proof system behind Zcash Sapling - operates natively on BLS12-381. A prover commits to a witness using elements in $\mathbb{G}_1$ and $\mathbb{G}_2$, and the verifier checks the proof with three pairings. The algebraic structure is identical: the bilinearity of $e$ lets the verifier confirm polynomial identities without learning the underlying values.

Halo2 can also target BLS12-381 when a pairing-based polynomial commitment scheme (KZG) is used. KZG commitments rely on a structured reference string (SRS) in $\mathbb{G}_1$ and evaluation proofs that are checked - again - via pairings.

The point is not that consensus and ZK share code paths. They share algebraic ground. The same curve parameters, the same pairing function, the same security assumption (the hardness of the discrete logarithm in these groups) underpin both systems.

BLS12-381 vs BN254

Many ZK systems - especially those targeting EVM verification - use BN254 instead of BLS12-381. The EVM has native precompiles for BN254 pairing checks (EIP-196, EIP-197), making on-chain proof verification cheap.

Both curves are pairing-friendly, but they differ:

PropertyBLS12-381BN254
Security level~128 bits~100 bits
G1 point size48 bytes32 bytes
Signature size96 / 48 bytes64 / 32 bytes
EVM precompileYes (EIP-2537)Yes (EIP-196/197)
Primary useConsensusEVM ZK proofs

A system that needs both BFT consensus and EVM-verifiable ZK proofs will use both curves: BLS12-381 for validator threshold signatures and DKG, BN254 for circuits whose proofs are checked on-chain. This is not redundancy - it is specialization. Each curve occupies the niche where its security/performance tradeoff fits best.

Timelock Encryption: a Pairing Bonus

Pairings unlock another construction that neither ECDSA nor EdDSA can provide: Identity-Based Encryption (IBE).

Commonware’s tle module implements timelock encryption using the Boneh-Franklin IBE scheme on BLS12-381. A message is encrypted against a target identifier (a round number, a timestamp). It can only be decrypted when a valid BLS signature over that target becomes available - typically produced by threshold signing at the designated round.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
use commonware_cryptography::bls12381::{
    tle::{encrypt, decrypt, Block},
    primitives::{ops::{keypair, sign_message}, variant::MinPk},
};
use rand::rngs::OsRng;

let (secret, public) = keypair::<_, MinPk>(&mut OsRng);
let target = 42u64.to_be_bytes();
let msg = Block::new(*b"secret payload 32 bytes here!!! ");

let ct = encrypt::<_, MinPk>(&mut OsRng, public, (b"_TLE_", &target), &msg);

// Once round 42's threshold signature is produced:
let sig = sign_message::<MinPk>(&secret, b"_TLE_", &target);
let pt = decrypt::<MinPk>(&sig, &ct).expect("valid signature decrypts");

assert_eq!(msg.as_ref(), pt.as_ref());

The pairing $e(rP,\ H(\text{target})) = e(P,\ H(\text{target}))^r$ is what ties decryption to the existence of a specific signature. No other signature scheme family provides this link without additional rounds of interaction.

Must Read

Closing Thoughts

BLS12-381 is not a general-purpose curve. It is a specialized algebraic object designed to make pairings efficient at a 128-bit security level. That specialization pays off twice:

  1. Consensus - BLS threshold signatures, DKG, VRF, and signature aggregation all fall out of the pairing equation. Commonware’s commonware-cryptography crate wraps blst into a trait-based Rust API covering each of these.
  2. Privacy - Groth16 and KZG-based proof systems use the same curve and the same pairing to verify polynomial commitments in zero knowledge.