Analysis of zk-Snark engineering principles based on Gnark

Orbiter_Finance
4 min readNov 24, 2022

--

Author: 0x60018 from @OrbiterResearch

1. How to use Gnark

Example:

Hash = mimc(PreImage)

mimc is the hash algorithm, providing Hash and proof to make the verifier be sure of the prover knows the PreImage.

1.1 Define the circuit

// CubicCircuit defines a simple circuit
type CubicCircuit struct {
PreImage frontend.Variable
Hash frontend.Variable `gnark:",public"`
}

// Define declares the circuit constraints
func (circuit *CubicCircuit) Define(api frontend.API) error {
mimc, _ := mimc.NewMiMC(api)
mimc.Write(circuit.PreImage)
api.AssertIsEqual(circuit.Hash, mimc.Sum())
return nil
}

Q1: Is there a more general Turing-complete language for writing circuits?

Q2: Is that what the Cairo language does for this purpose?

1.2 Compile the circuit and generate R1CS constraints

Note: In addition to the BN254 curve, there are also curves such as BLS12–381.

Detailed comparison: https://docs.gnark.consensys.net/en/latest/Concepts/schemes_curves/

The mathematical rationale for generating R1CS constraints is complicated and still needs further research.

// Generate constraints using BN254 curves
r1cs, err := frontend.Compile(ecc.BN254, r1cs.NewBuilder, &circuit)

1.3 Generate ProvingKey, VerifyingKey

pk, vk, err := groth16.Setup(r1cs)

1.4 Generates Solidity file of Verifier using VerifyingKey

verifySolidityPath := fmt.Sprintf("..%chardhat%ccontracts%cmimc_groth16.sol", os.PathSeparator, os.PathSeparator, os.PathSeparator)
f, _ := os.OpenFile(verifySolidityPath, os.O_CREATE|os.O_WRONLY, 0666)
defer f.Close()
vk.ExportSolidity(f)

1.5 Generate proof by applying Prove to PreImage, Hash

assignment := &CubicCircuit{PreImage: preImage, Hash: hash}
witness, _ := frontend.NewWitness(assignment, ecc.BN254)
publicWitness, _ := witness.Public()
proof, err := groth16.Prove(r1cs, pk, witness)

1.6 Save proof and publicWitness

// Save publicWitness
publicWitnessPath := fmt.Sprintf("..%chardhat%ctest%cmimc_public_witness.json", os.PathSeparator, os.PathSeparator, os.PathSeparator)
publicWitnessFile, _ := os.OpenFile(publicWitnessPath, os.O_CREATE|os.O_WRONLY, 0666)
defer publicWitnessFile.Close()
publicWitnessJson, _ := publicWitness.MarshalJSON()
publicWitnessFile.Write(publicWitnessJson)

// Save proof
proofPath := fmt.Sprintf("..%chardhat%ctest%cmimc.proof", os.PathSeparator, os.PathSeparator, os.PathSeparator)
proofFile, _ := os.OpenFile(proofPath, os.O_CREATE|os.O_WRONLY, 0666)
defer proofFile.Close()
proof.WriteRawTo(proofFile)

1.7 Send proof and publicWitness to the contract for verification

import { ethers } from "hardhat";
import { Signer, BigNumber, Contract, ContractFactory } from "ethers";
// import "ethers";
import { expect } from "chai";
import fs from "fs-extra";
import path from "path";

describe("mimc", function () {
let accounts: Signer[];
let contractFactory: ContractFactory;
let verifierContract: Contract;

before(async function () {
accounts = await ethers.getSigners();
contractFactory = await ethers.getContractFactory("Verifier");
});

it("deploy", async function () {
verifierContract = await contractFactory.deploy();
});

it("verifyProof", async function () {
const fpSize = 4 * 8;

const inputBuffer = await fs.readFile(
path.resolve(__dirname, "mimc_public_witness.input")
);
const input = JSON.parse(inputBuffer.toString()).Hash;
console.warn(input);

const proofBuffer = await fs.readFile(
path.resolve(__dirname, "mimc.proof")
);

const a: Buffer[] = [];
a[0] = proofBuffer.slice(fpSize * 0, fpSize * 1);
a[1] = proofBuffer.slice(fpSize * 1, fpSize * 2);

const b: Buffer[][] = [[], []];
b[0][0] = proofBuffer.slice(fpSize * 2, fpSize * 3);
b[0][1] = proofBuffer.slice(fpSize * 3, fpSize * 4);
b[1][0] = proofBuffer.slice(fpSize * 4, fpSize * 5);
b[1][1] = proofBuffer.slice(fpSize * 5, fpSize * 6);

const c: Buffer[] = [];
c[0] = proofBuffer.slice(fpSize * 6, fpSize * 7);
c[1] = proofBuffer.slice(fpSize * 7, fpSize * 8);

const resp = await verifierContract.verifyProof(a, b, c, [input]);
expect(resp).to.be.true;
});
});

In the above example, we can see that only proof and public input (Hash value) are submitted off-chain. The contract can verify whether the value of PreImage is known off-chain, which can realize privacy proof. But there are still some issues to be resolved:

Q1: Split the proof into a, b, and c. The function is to check that the elements in a, b, and c cannot be greater than or equal to PRIME_Q (value: 21888242871839275222246405745257275088696311157297823662689037894645226208583) during contract verification. What is the mathematical principle? Rules for R1CS constraints?

Q2: Proof generation in the above example needs to be generated by go language, so the proof cannot be generated on the web client. How can this be achieved using TypeScript? Can it be imported from gnark or is there another library that implements it? Is there a reference to Tornado.cash?

A2: Currently, snarkjs and zokrates use JavaScript to implement the zk-snark function, and Tornado.cash uses snarkjs

Q3: How to implement zk-rollup with zk-snark technology? or a merkle tree proof circuit? What is the relationship between merkle and zk-rollup (or no relationship at all?)

A3: zk-rollup uses merkle tree to store user amount, public key, nonce value and other information. The Operator converts the transaction into a state and generates a zk-snark proof to submit to the L1 contract for verification and change the user state (simple version)

2. Gnark source code structure

The following structural analysis is based on Gnark v0.7.1

Q: The operation functions provided under frontend => api.go are limited, and Turing-complete zk programming cannot be provided for the time being. Is circom more complete?

--

--

Orbiter_Finance
Orbiter_Finance

Written by Orbiter_Finance

Orbiter Finance is a decentralized cross-rollup Layer 2 bridge with a contract only on the destination side.

Responses (1)