Generate and submit ZK proof

With the preparations done, the front-end code calling for the generation can be implemented. Firstly, we define a function for reading the prover data:

/**
 * Get prover data (separately loaded because the large json should not slow down initial site loading).
 *
 * @param path - Path to the prover data json file (relative to the public folder).
 * @returns JSON object with the prover data.
 */
export async function getProver(path: string) {
  const proverText = await fetch(path);
  const parsedFile = JSON.parse(await proverText.text());
  return parsedFile;
}

Then we assemble the input data for the ZK proof. It depends on what the proof will be about. In this example, we build an input for a non-trivial proof (KYC, age>=18, fraud investigation). It includes the following custom inputs:

const provider = new ethers.providers.Web3Provider(window.ethereum);

// fetch institution pubkey from chain because it is needed as proof input
const institutionContract = new ethers.Contract(
  institutionAddresses,
  galacticaInstitutionABI.abi,
  provider.getSigner(),
);
const institutionPubKeys: [string, string][] = [[
  BigNumber.from(await institutionContract.institutionPubKey(0)).toString(),
  BigNumber.from(await institutionContract.institutionPubKey(1)).toString(),
]];

const proofInput: any = {
  // general zkKYC inputs
  currentTime: await getCurrentBlockTime(),
  dAppAddress,
  investigationInstitutionPubKey: institutionPubKeys,
  // specific inputs to prove that the holder is at least 18 years old
  currentYear: dateNow.getUTCFullYear().toString(),
  currentMonth: (dateNow.getUTCMonth() + 1).toString(),
  currentDay: dateNow.getUTCDate().toString(),
  ageThreshold: '18',
  // the zkKYC itself is not needed here. It is filled by the snap for user privacy.
};

This is everything we need for calling the snap to generate the proof.

import {
  generateZKProof,
  ZkCertProof,
  ZkCertStandard,
} from '@galactica-net/snap-api';

const res: ZkCertProof = await generateZKProof({
  input: proofInput,
  prover: await getProver("/provers/exampleMockDApp.json"),
  requirements: {
    zkCertStandard: ZkCertStandard.ZkKYC,
    registryAddress: addresses.zkKYCRegistry,
  },
  userAddress: getUserAddress(),
  description: "This proof discloses that you hold a valid zkKYC and that your age is at least 18.",
  publicInputDescriptions: zkKYCAgeProofPublicInputDescriptions,
});

On this call, the user reviews what data is disclosed by the proof and either accepts or rejects it. The generation is automatically rejected, if the user has not setup and imported a matching zkCert, in this example a gip1 zkKYC.

If the request fails, it throws an error. It might also happen that the prover is unable to find a proof for the given input. The error then contains a backtrace to the Circom component that fails to satisfy an assertion. This component can give a hint on what condition fails. This could be for example:

  • Incorrect inputs

  • The user is less than 18 years old, according to the zkKYC.

  • Input values having the wrong format. Be careful when converting between Circom field elements and EVM variables.

Circom returns values in decimal form and we need to convert them into hex numbers before sending them in an EVM transaction:

// this function convert the proof output from snarkjs to parameter format for onchain solidity verifier
export function processProof(proof: any) {
  const piA = proof.pi_a
    .slice(0, 2)
    .map((value: any) => fromDecToHex(value, true));
  // for some reason the order of coordinate is reverse
  const piB = [
    [proof.pi_b[0][1], proof.pi_b[0][0]].map((value) =>
      fromDecToHex(value, true),
    ),
    [proof.pi_b[1][1], proof.pi_b[1][0]].map((value) =>
      fromDecToHex(value, true),
    ),
  ];

  const piC = proof.pi_c
    .slice(0, 2)
    .map((value: any) => fromDecToHex(value, true));
  return [piA, piB, piC];
}

// this function processes the public inputs
export function processPublicSignals(publicSignals: any) {
  return publicSignals.map((value: any) => fromDecToHex(value, true));
}

To simplify the user flow, we recommend to directly submit the proof after it has been generated:

// get contract to send proof to
const exampleDAppSC = new ethers.Contract(addresses.mockDApp, mockDAppABI.abi, signer);

let [a, b, c] = processProof(res.proof);
let publicInputs = processPublicSignals(res.publicSignals);

// this is the on-chain function that requires a ZKP
let tx = await exampleDAppSC.airdropToken(1, a, b, c, publicInputs);
const receipt = await tx.wait();

On success, most smart contracts mint a verification soul-bound token (SBT) for the user. These usually unlock using the smart contract until the SBT expires. So users do not have to spend time generating a ZKP for every transaction.

The on-chain verification can also fail. The error often reveals which requirement failed. If the verification of the ZKP fails, make sure to check the following:

  • Is the prover (wasm and zkey) compatible with the verifier in the smart contract? Both are generated from the circom compilation and need to match.

  • Is the timing correct? ZKPs containing the current time have a validity limit.

Last updated