A BLS12-381 KZG commitment verifier written in Noir.
Experimental, unaudited software. Do not use in production. The pairing backend is a third-party library that has not been audited, and this library has no audit of its own.
Add this to your Nargo.toml:
kzg_verifier = { tag = "v0.2.0", git = "https://github.com/RajeshRk18/kzg-verifier.git" }Requires Noir >= 1.0.0-beta.24.
Verification needs an evaluation point x, a claimed evaluation y, a
commitment C, an opening proof pi, and a verifier key holding the two G2
points from the trusted setup.
use kzg_verifier::assert_kzg;
use kzg_verifier::serde::{fr_from_be_bytes, g1_from_be_bytes, g2_from_be_bytes};
use kzg_verifier::VerifierKey;
fn main(
g2_x_c0: [u8; 48], g2_x_c1: [u8; 48], g2_y_c0: [u8; 48], g2_y_c1: [u8; 48],
tau_g2_x_c0: [u8; 48], tau_g2_x_c1: [u8; 48],
tau_g2_y_c0: [u8; 48], tau_g2_y_c1: [u8; 48],
x: [u8; 32], y: [u8; 32],
commitment_x: [u8; 48], commitment_y: [u8; 48],
proof_x: [u8; 48], proof_y: [u8; 48],
) {
let vk = VerifierKey::from_tau_g2(
g2_from_be_bytes(g2_x_c0, g2_x_c1, g2_y_c0, g2_y_c1),
g2_from_be_bytes(tau_g2_x_c0, tau_g2_x_c1, tau_g2_y_c0, tau_g2_y_c1),
);
assert_kzg(
vk,
fr_from_be_bytes(x),
fr_from_be_bytes(y),
g1_from_be_bytes(commitment_x, commitment_y),
g1_from_be_bytes(proof_x, proof_y),
);
}verify_kzg returns a bool if you need to branch on the result; assert_kzg
constrains it so the result cannot be silently discarded.
The equation
e(C - y*G1, G2) == e(pi, tau*G2 - x*G2)
is rearranged by bilinearity into
e(C - y*G1 + x*pi, G2) * e(-pi, tau*G2) == 1
which moves the evaluation point onto G1. G1 work is then a single constrained
linear expression from noir_bigcurve, and no G2 scalar multiplication is
performed at all.
g1_from_be_bytes is the trust boundary for prover-supplied points. It enforces:
- canonical coordinates in
[0, q) - on-curve membership
- prime-order subgroup membership
fr_from_be_bytes enforces canonical scalars in [0, r).
Canonical range is checked in this library rather than delegated: bignum
v0.10.0's from_be_bytes accepts values in [modulus, 2^MOD_BITS).
G2 points are not validated. They are trusted-setup material. Validate them off-circuit once and pin them to the verifier key.
nargo test subgroup # input validation, no pairing: seconds
nargo test --exact tests::rejects_off_curve_point
nargo test --exact tests::rejects_noncanonical_scalar
nargo test e2e_ --test-threads 1 # full verification: ~13 min and ~6.6 GB eachFilters are substring matches, so nargo test rejects_ also picks up the
expensive e2e_rejects_* cases; use --exact to avoid that.
Test vectors are generated by tools/gen_test_vectors.py using py_ecc as an
independent implementation; the generator asserts the pairing relation natively
before emitting bytes.
nargo info on a bin crate calling verify_kzg (Noir 1.0.0-beta.24):
| Component | ACIR opcodes |
|---|---|
| Final exponentiation | 673,461 |
ell evaluations (2 loops x 68) |
345,168 |
| G2 line arithmetic (2 loops) | 256,752 |
| Fp12 squarings (2 loops x 62) | 192,448 |
| Input validation + G1 MSM | 104,204 |
| Remainder (G2 decode, selects) | ~180,749 |
| Total | 1,752,782 |
Per Miller loop: 63 doublings, 5 additions, 68 ell, 62 Fp12 squarings.
Atomic costs: Fp2 mul 128, Fp12 square 1,552, Fp12 mul 2,148, ell 2,538.
fp6::mul_interleavedin the pairing dependency uses unconstrained values as factors insideevaluate_quadratic_expression.- One opening verification takes ~13 min and peaks near 6.6 GB on my Macbook M4 Air. Run the pairing tests with
--test-threads 1so it doesnt get OOM'd. - Only single openings are supported, no batching or multi-opening.