zero-knowledge circuit with circom for Sujiko

·

4 min read

Zero-knowledge proof (ZKP) is a cryptographic method by which one party (the prover) can prove to another party (the verifier) that a given statement is true, without revealing any specific information about the statement itself.

ZKPs offer a breakthrough in privacy and security. They enable you to validate transactions without revealing the details behind them. This is particularly beneficial for applications that require privacy, such as voting systems or private blockchain transactions.

zero-knowledge proof system is generated based on complex math. Which is always good to know but to get started one can learn circom, a domain specific language that allows to write the proof generation logic (circuits) using circom code. This is very useful as once you compile the code circom compiler runs all the mathmatical constraints. As zkp circuit developer you can start build zkp applications without getting confused my complex math.

Here i showed an example of how to build a sujiko circuit in circom. The idea is that an end user can prove that they have a solution of the puzzle without revealing the answers.

How Circom Works:

Developers define the circuit in Circom's domain-specific language. Once the circuit is defined, it is compiled into a format that can be used by a ZKP system.

With the compiled circuit, proofs can be generated to attest the truth of statements. Verification: These proofs can then be verified by others, ensuring the statement's validity without revealing its details.

The combination of ZKPs and Circom creates solution for applications needing the highest levels of security and privacy. Circom provides developers with a streamlined way to harness the power of ZKPs, making the process of creating and verifying proofs more efficient.

For instance, in a blockchain context, rather than showing the entire transaction data for validation, one could use a circuit developed in Circom to prove the transaction's validity without revealing its details. This would not only speed up validation times but also ensure that personal or sensitive transaction data remains confidential.

checkout circom's official documentation here: https://docs.circom.io/getting-started/writing-circuits/

Below example, a circuit is built for sujiko game with tests, for installation of necessary packages please checkout my github

sujiko.circom

pragma circom 2.0.0;

include "../node_modules/circomlib/circuits/comparators.circom";


template SujikoVerifier() {
    signal input grid[9];
    signal input sums[4];
    signal output out;


// Compute the four corner sums
    sums[0] === grid[0] + grid[1] + grid[3] + grid[4];
    sums[1] === grid[1] + grid[2] + grid[4] + grid[5];
    sums[2] === grid[3] + grid[4] + grid[6] + grid[7];
    sums[3] === grid[4] + grid[5] + grid[7] + grid[8];

// uniqueness check
    var n = 9;
    component isEq[n * (n - 1) / 2];

    var index = 0;

    for (var i = 0; i < n; i++) {
        for (var j = i+1; j < n; j++) {
            isEq[index] = IsEqual();
            isEq[index].in[0] <== grid[i];
            isEq[index].in[1] <== grid[j];
            isEq[index].out === 0;
            index++;
        }
    }

    out <== 1;
}

component main {public [sums]} = SujikoVerifier();

You can then compile and run the circuits.

  1. Compilation :

    Circom sujiko.circom —r1cs —wasm

  2. Run this circuit:

    Node ./circuit_js/generate_witness.js ./circuit_js/sujiko.wasm input.json witness.wtns

We will not cover the frontend that would require generating zkey and ceremony setup with poweroftau. Which we will cover next.

Running test to ensure the circuit will work.

const chai = require("chai");
const { wasm } = require("circom_tester");
const path = require("path");
const F1Field = require("ffjavascript").F1Field;
const Scalar = require("ffjavascript").Scalar;
exports.p = Scalar.fromString(
  "21888242871839275222246405745257275088548364400416034343698204186575808495617",
);
const Fr = new F1Field(exports.p);

const wasm_tester = require("circom_tester").wasm;

const assert = chai.assert;

describe("Sujiko Tester ", function () {
  this.timeout(100000);

  it("Should create a Sujiko circuit", async () => {
    const circuit = await wasm_tester(
      path.join(__dirname, "../", "sujiko.circom"),
    );
    await circuit.loadConstraints();
    let witness;

    const expectedOutput = 1;

    witness = await circuit.calculateWitness(
      {
        sums: ["21", "18", "16", "15"],
        grid: ["9", "3", "5", "7", "2", "8", "6", "1", "4"],
      },
      true,
    );
    console.log(`Witness[1] Value: ${Fr.toString(witness[1])}`);
    console.log(`Witness[0] Value: ${Fr.toString(witness[0])}`);

    assert(Fr.eq(Fr.e(witness[0]), Fr.e(1)));
    assert(Fr.eq(Fr.e(witness[1]), Fr.e(expectedOutput)));

    // try {
    //   witness = await circuit.calculateWitness(
    //     {
    //       cornerSums: ["20", "22", "14", "20"],
    //       grid: ["7", "9", "2", "1", "3", "8", "6", "4", "5"],
    //     },
    //     true,
    //   );
    //   //const expectedOutput2 = 1;
    //   assert(Fr.eq(Fr.e(witness[0]), Fr.e(1)));
    //   //assert(Fr.eq(Fr.e(witness[1]), Fr.e(expectedOutput2)));
    //   console.log("PASSING!");
    // } catch (e) {
    //   console.log("Circuit is reverting . FAILING!!!");
    // }
  });



// test 2 
it("Should fail due to wrong order in a row", async function () {

    const circuit = await wasm_tester(
      path.join(__dirname, "../", "sujiko.circom"),
    );
    await circuit.loadConstraints();
    // The number 1 in the first row of solved is twice
    let input = {
        sums: ["20", "22", "14", "20"],
        grid: ["7", "9", "2", "1", "3", "8", "6", "4", "5"],

    };
    try {
      await circuit.calculateWitness(input);
    } catch (err) {
      console.log(err);
      //assert(err.message.includes("Assert Failed"));
    }
  });



//test 3

it("Should pass", async function () {
     const circuit = await wasm_tester(
      path.join(__dirname, "../", "sujiko.circom"),
    );
    // The number 1 in the first row of solved is twice
    let input = {
        sums: ["21", "18", "16", "15"],
        grid: ["9", "3", "5", 
               "7", "2", "8", 
               "6", "1", "4"],
    };
    try {
      await circuit.calculateWitness(input);
    } catch (err) {
      console.log(err);
      //assert(err.message.includes("Assert Failed"));
    }
  });
});