← Back to blogs

Understanding Polynomial Commitment KZG using Airdrop Distribution (Part 2)

Understanding Polynomial Commitment KZG using Airdrop Distribution (Part 2) — thumbnail
cryptography zero knowledge

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:

  1. Polynomial Commitment: Creating a commitment to a polynomial like $P(x) = 2x^2 + 3x + 5$ without exposing coefficients.
  2. Coefficient Hiding: KZG generates commitments that conceal polynomial coefficients while proving degree knowledge.
  3. Zero-Knowledge Properties: Verification occurs without revealing polynomial details or coefficients.
  4. Verification: Others validate commitments without knowing underlying polynomial specifics.
  5. 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:

← Back to blogs