Introducing bls12-381 — BLS12-381 pairings in pure Motoko
Hi everyone,
We just published bls12-381 to mops: a pure-Motoko implementation of the BLS12-381
curve — the field tower, G1 and G2 arithmetic, hash-to-curve, multi-scalar multiplication,
and pairing checks. No host functions, no system API, no Rust: it compiles with moc and
runs anywhere Motoko runs, including inside an ordinary IC canister.
The library was funded by ICDevs.org (a 501c3 non-profit). If you find it useful, please
consider chipping in at https://icdevs.org/donation.html.
mops add bls12-381
Repo: https://github.com/icdevsorg/bls12-381.mo (release v0.1.0)
Why we built it
BLS12-381 is everywhere the Internet Computer is: every IC certificate is a BLS signature
over it, vetKeys use it, and Ethereum’s Prague upgrade made it a precompile (EIP-2537) and
KZG commitments (EIP-4844) are built on it. We needed it first for
evm.mo, an EVM in Motoko, which has to execute the
EIP-2537 precompiles (G1ADD, G1MSM, G2ADD, G2MSM, PAIRING, MAP_FP_TO_G1, MAP_FP2_TO_G2)
exactly as Ethereum does. There was no Motoko implementation, so we wrote one, and then spent
most of the effort making it fast enough to live inside a canister’s instruction budget.
What’s in it
- Fields: Fp, Fp2, Fp6, Fp12 with dedicated squaring, inversion, square roots and
Frobenius maps. - Groups: G1 and G2 in projective coordinates — add, double, scalar multiply, affine
conversion, on-curve checks, and subgroup checks (G2 via the ψ endomorphism). - Pairing: an optimal-ate Miller loop over the NAF-recoded curve parameter with
sparse line multiplication, a multi-Miller loop that shares the squarings across
pairs, and a final exponentiation split into an easy part and a hard part driven by
cyclotomic squaring.pairing_check([(P1, Q1), (P2, Q2), …])is the one you usually
want: one Miller loop over all pairs, one final exponentiation. - Hash-to-curve helpers: the RFC 9380 simplified-SWU maps with the 11- and 3-isogenies
(map_fp_to_g1,map_fp2_to_g2). - MSM for G1 and G2, and the EIP-2537 encodings (padded Fp/Fp2, G1/G2, scalars) plus
compressed-G1 decoding.
Performance
Measured with mops bench (instruction counts on a replica):
| Operation | Instructions |
|---|---|
| one full pairing (Miller loop + final exponentiation) | 1.20 B |
| final exponentiation | 660 M |
| Miller loop, one pair | 539 M |
| multi-Miller loop, two pairs (vs 1.08 B for two separate loops) | 805 M |
map_fp_to_g1 |
141 M |
| G2 scalar multiply | 13.8 M |
| G1 scalar multiply | 5.6 M |
| Fp multiply | 22 K |
A complete BLS signature verification (hash-to-curve, subgroup checks, and a two-pair
pairing_check) measures 1.44 billion instructions inside
mo:ic-agent, which uses it to verify IC certificates. The IC’s
per-message limit is 40 billion instructions for an update call and 5 billion for a query,
so a verification fits comfortably in either.
One result worth sharing because it surprised us: fixed-limb Montgomery multiplication is
much slower than plain Nat in Motoko. We built a 6×64-bit-limb Montgomery field and
benchmarked it head to head. A Montgomery fp_mul took 1.86 million instructions, against
23 thousand for (a * b) % p on Nat: about 80× slower. Motoko’s arbitrary-precision
arithmetic runs in the runtime as native Wasm, and a limb loop written in Motoko cannot
compete with it. The
benchmark is in the repo (bench/montgomery_vs_nat.bench.mo) if you want to check it for
your own field code.
Correctness
- 42 PocketIC tests: field and curve laws, subgroup checks, the pairing identities
(e(P, Q) · e(-P, Q) = 1, bilinearity), MSM, encodings, and spot checks against
@noble/curvesfor scalar multiplication,
SWU and the isogeny maps. - 11 randomized fuzz tests comparing results against
@noble/curves. - evm.mo’s suites: EIP-2537 known-answer vectors, and EIP-4844 KZG negative vectors.
- Real IC certificates: mo:ic-agent uses it to verify
certificates from PocketIC and from mainnet in its test gate. That is an end-to-end check
that the pairing, subgroup checks and hash-to-curve compose correctly.
It has not had an independent cryptographic audit. Treat it accordingly before you
protect production assets with it, and please report anything that looks wrong. It is also
not constant-time — it is written for verifying public data (signatures, certificates,
proofs), not for handling secret keys.
Quick start
import BLS "mo:bls12-381";
// e(P, Q) · e(-P, Q) == 1
let p = BLS.G1_GEN;
let q = BLS.G2_GEN;
assert BLS.pairing_check([(p, q), (BLS.g1_neg(p), q)]);
// scalar multiplication and bilinearity: e(aP, Q) == e(P, aQ)
let a = 123456789;
assert BLS.pairing_check([(BLS.g1_mul(p, a), q), (BLS.g1_neg(p), BLS.g2_mul(q, a))]);
Feedback, issues and PRs are very welcome — especially benchmarks from your own workloads.