This article explains how to implement KZG polynomial commitments for airdrop distribution, building on the Merkle proof concepts from Part 1. The goal is to demonstrate how polynomial commitments can verify valid airdrop claims without revealing the entire dataset.
What is KZG Commitment?
Polynomial commitment schemes allow proving information about a polynomial without revealing it entirely. KZG (Kate, Zaverucha, and Goldberg) accomplishes this through:
- Polynomial Commitment: Creating a commitment to a polynomial like $P(x) = 2x^2 + 3x + 5$ without exposing coefficients.
- Coefficient Hiding: KZG generates commitments that conceal polynomial coefficients while proving degree knowledge.
- Zero-Knowledge Properties: Verification occurs without revealing polynomial details or coefficients.
- Verification: Others validate commitments without knowing underlying polynomial specifics.
- Applications: KZG enables zero-knowledge proofs for privacy-preserving blockchain and secure computation systems.
For deeper understanding, the article recommends additional reading: Polytope's "Polynomial Commitments" and Scroll's "KZG in Practice".
Implementation Process
Stage 1: Data Commitment
Convert valid user data (address, amount pairs) to uniform-length hashes using keccak256:
let mut rdr = csv::Reader::from_path("./src/assets/data.csv").unwrap();
let mut protocol_outputs = OutPut::default();
let mut y_fs : Vec<Fr> = Vec::new();
let mut xs: Vec<U256> = Vec::new();
let mut x_now: U256 = U256::one();
for result in rdr.records() {
let record = result.unwrap();
let addr = record.get(0).unwrap().trim();
let amount = record.get(1).unwrap().trim();
let encoded_data = encode(&[
Token::Address(Address::from_str(addr).unwrap()),
Token::Uint(U256::from_dec_str(amount).unwrap())]);
let hash = keccak256(&encoded_data);
let y = U256::from_big_endian(&hash.clone());
let x = x_now.clone();
let y_f = Fr::from_str(y.clone().to_string().as_str()).unwrap();
xs.push(x);
y_fs.push(y_f);
let temp_output = OutputProof::new(x_now.as_u32(), addr.to_string(), amount.to_string());
protocol_outputs.proofs.insert(addr.to_string(), temp_output);
x_now += U256::one();
}
let poly = Evaluations::from_vec_and_domain(y_fs.clone(), D::new(y_fs.len()).unwrap()).interpolate();
This algorithm creates a polynomial where $f(1)$ equals the first user's hash, $f(2)$ equals the second user's hash, and so forth, through polynomial interpolation using the Fast Fourier Transform (FFT) via the Ark Works library.
Stage 2: Polynomial Commitment Setup
Generate a KZG commitment to the interpolated polynomial:
let rng = &mut test_rng();
let params = KZG10::<Bls12_381, UniPoly_381>::setup(poly.degree(), false, rng).unwrap();
let powers_of_g = params.powers_of_g.to_vec();
let powers_of_gamma_g = (0..=poly.degree())
.map(|i| params.powers_of_gamma_g[&i])
.collect();
let powers = Powers {
powers_of_g: ark_std::borrow::Cow::Owned(powers_of_g),
powers_of_gamma_g: ark_std::borrow::Cow::Owned(powers_of_gamma_g),
};
let vk = VerifierKey {
g: params.powers_of_g[0],
gamma_g: params.powers_of_gamma_g[&0],
h: params.h,
beta_h: params.beta_h,
prepared_h: params.prepared_h.clone(),
prepared_beta_h: params.prepared_beta_h.clone(),
};
// committing to the polynomial
let (comm, r) = KZG10::<Bls12_381, UniPoly_381>::commit(&powers, &poly, None, None).expect("Commitment failed");
The commitment becomes a point on the BLS12-381 G1 curve, stored in the verifying smart contract.
Verification Process
Proof Generation
Generate proofs for valid users using the polynomial representation:
let ty: Vec<_> = protocol_outputs.proofs.iter_mut().map(|mut proof| {
if proof.1.index < 10 {
let ppp = KZG10::<Bls12_381, UniPoly_381>::open(&powers, &poly, Fr::from(proof.1.index), &r).unwrap();
println!("proof: {:?}", ppp.w.to_string());
proof.1.borrow_mut().proof = ppp.w.to_string();
} else {
// proof generation is time consuming so we are only generating proofs for the first 10 evaluations
}
}).collect();
// this is how the output proof looks like — a single G2 point (x, y)
Unlike Merkle proofs requiring 18 leaves for verification, KZG requires only a single elliptic curve point as proof.
Smart Contract Verification
The verifying contract reconstructs the user's polynomial evaluation, retrieves the stored commitment, and performs a bilinear pairing:
// performing a simple test (THIS IS SIMILAR TO WHAT WOULD BE GOING ON THE VERIFIER SMART CONTRACT)
let value = poly.evaluate(&Fr::from(1));
let proof = KZG10::<Bls12_381, UniPoly_381>::open(&powers, &poly, Fr::from(1), &r).unwrap();
let check = KZG10::<Bls12_381, UniPoly_381>::check(
&vk,
&comm,
Fr::from(1), // this is the x-axis for user1
value,
&proof,
).unwrap();
println!("check: {:?}", check);
Current Limitations
KZG polynomial commitments cannot currently be implemented on the EVM because the BLS12-381 precompile — including bilinear pairing operations — remains undeployed. See EIP-2537: Precompile for BLS12-381 curve operations for details.
The author demonstrates verification using Rust and Ark Works, achieving constant-time verification once EIP-2537 deploys.
Resources
Code repositories:
- poly-commit-airdrop-tool: Polynomial interpolation, commitment, and proof generation.
- kzg-verify-lib: KZG verification library in development.