SEI ZK Series Part 5 - Creating an onchain verifier on the sei blockchain
June 17th, 2025

Creating a Zero-Knowledge Onchain Verifier: A Beginner's Guide

Introduction

Zero-knowledge proofs (ZKPs) are cryptographic methods that allow one party (the prover) to prove to another party (the verifier) that a statement is true without revealing any additional information beyond the validity of the statement itself. When implemented on a blockchain, these verifiers enable powerful privacy-preserving applications.

This tutorial will guide you through creating a simple ZK onchain verifier using Circom and Solidity. We'll build a basic system where a user can prove they know a secret number without revealing it.

Prerequisites

Before we begin, make sure you have:

  • Node.js and npm installed

  • Basic understanding of Solidity

  • Familiarity with basic cryptography concepts

  • A code editor (VS Code recommended)

Setting Up the Development Environment

  1. First, let's install the necessary tools:
npm install -g circom
npm install -g snarkjs
  1. Create a new project directory and initialize it:
mkdir zk-verifier-tutorial
cd zk-verifier-tutorial
npm init -y

Understanding the Components

Our ZK system will consist of three main parts:

  1. A Circom circuit (the proving system)

  2. A Solidity verifier contract

  3. A frontend application to interact with the system

Step 1: Creating the Circom Circuit

Let's create a simple circuit that proves knowledge of a secret number. Create a file named circuit.circom:

pragma circom 2.0.0;

template SecretNumber() {
    // Private input: the secret number
    signal private input secret;

    // Public input: the hash of the secret number
    signal public input hash;

    // Output: whether the proof is valid
    signal output out;

    // Component to compute hash
    component hash = Poseidon(1);

    // Connect the secret to the hash component
    hash.inputs[0] <== secret;

    // Verify that the provided hash matches the computed hash
    hash.out === hash;

    // Set output to 1 if verification passes
    out <== 1;
}

component main = SecretNumber();

Step 2: Compiling the Circuit

  1. Create a build directory:
mkdir build
  1. Compile the circuit:
circom circuit.circom --r1cs --wasm --sym -o build
  1. Generate the proving and verification keys:
snarkjs groth16 setup build/circuit.r1cs pot12_final.ptau build/circuit_final.zkey

Step 3: Creating the Verifier Contract

Now, let's create a Solidity contract that will verify the proofs on-chain. Create a file named Verifier.sol:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/access/Ownable.sol";

contract SecretNumberVerifier is Ownable {
    // Event emitted when a valid proof is submitted
    event ProofVerified(address indexed prover, uint256 hash);

    // Mapping to store verified hashes
    mapping(uint256 => bool) public verifiedHashes;

    // Function to verify the proof
    function verifyProof(
        uint[2] memory a,
        uint[2][2] memory b,
        uint[2] memory c,
        uint[1] memory input
    ) public returns (bool) {
        // Verify the proof using the verifier contract
        bool isValid = verify(a, b, c, input);

        if (isValid) {
            verifiedHashes[input[0]] = true;
            emit ProofVerified(msg.sender, input[0]);
        }

        return isValid;
    }

    // Function to check if a hash has been verified
    function isHashVerified(uint256 hash) public view returns (bool) {
        return verifiedHashes[hash];
    }
}

Step 4: Testing the System

Let's create a simple test script to verify our implementation. Create a file named test.js:

const { expect } = require("chai");
const { ethers } = require("hardhat");
const snarkjs = require("snarkjs");

describe("SecretNumberVerifier", function () {
  let verifier;
  let secret = 42; // Our secret number

  beforeEach(async function () {
    const Verifier = await ethers.getContractFactory("SecretNumberVerifier");
    verifier = await Verifier.deploy();
    await verifier.deployed();
  });

  it("Should verify a valid proof", async function () {
    // Generate proof
    const { proof, publicSignals } = await snarkjs.groth16.fullProve(
      { secret: secret },
      "build/circuit.wasm",
      "build/circuit_final.zkey"
    );

    // Verify on-chain
    const result = await verifier.verifyProof(
      [proof.pi_a[0], proof.pi_a[1]],
      [
        [proof.pi_b[0][0], proof.pi_b[0][1]],
        [proof.pi_b[1][0], proof.pi_b[1][1]],
      ],
      [proof.pi_c[0], proof.pi_c[1]],
      [publicSignals[0]]
    );

    expect(result).to.be.true;
  });
});

Understanding How It Works

  1. The Circuit:

    • Takes a private input (the secret number) that only the prover knows

    • Uses the Poseidon hash function, which is specifically designed for zero-knowledge proofs and is more efficient than traditional hash functions like SHA-256 in ZK contexts

    • Performs a cryptographic verification by comparing the computed hash of the secret number with a provided hash value

    • Outputs a binary signal (1) if the verification succeeds, indicating that the prover indeed knows a number that produces the claimed hash

    • The circuit's constraints ensure that the prover cannot cheat by providing false inputs

  2. The Verifier Contract:

    • Acts as an on-chain verifier that receives and processes the zero-knowledge proof

    • Takes three main components as input:

      • The proof itself (containing the cryptographic evidence)

      • Public inputs (like the hash value that was claimed)

      • Verification key (pre-computed during the trusted setup)

    • Implements the Groth16 verification algorithm, which is a specific type of zk-SNARK that's particularly efficient for blockchain applications

    • Maintains a mapping of verified hashes to prevent replay attacks and enable future reference

    • Emits events that can be used by frontend applications to track successful verifications

    • Runs entirely on-chain, ensuring the verification process is trustless and decentralized

  3. The Proof Generation Process:

    • The prover starts with their secret number and the circuit's constraints

    • Uses snarkjs to generate a cryptographic proof that demonstrates knowledge of the secret number

    • The proof generation process involves:

      • Creating a witness (a set of values that satisfy the circuit's constraints)

      • Running the circuit with the witness to generate the proof

      • Formatting the proof according to the Groth16 protocol

    • The generated proof is then sent to the verifier contract

    • The contract performs the verification without ever learning the actual secret number

    • This entire process ensures that the prover can demonstrate knowledge of the secret number while maintaining complete privacy

Security Considerations

  1. Circuit Security:

    • Ensure your circuit is properly constrained

    • Use well-audited cryptographic primitives

    • Consider side-channel attacks

  2. Contract Security:

    • Implement proper access controls

    • Consider gas optimization

    • Add circuit-specific checks

  3. System Security:

    • Protect private keys

    • Implement proper frontend security

    • Consider replay attacks

Next Steps

  1. Advanced Features:

    • Add support for multiple secrets

    • Implement more complex circuits

    • Add frontend integration

  2. Optimization:

    • Optimize circuit size

    • Reduce gas costs

    • Improve proof generation time

  3. Integration:

    • Connect to a frontend application

    • Implement a user interface

    • Add more complex business logic

Resources

Conclusion

This tutorial has introduced you to the basics of creating a zero-knowledge onchain verifier. You've learned how to:

  • Create a basic Circom circuit

  • Generate and verify proofs

  • Deploy a verifier contract

  • Test the system

Remember that this is a simplified example. Real-world applications often require more complex circuits and additional security considerations. Always audit your code and consider professional review for production systems.

Subscribe to 4undRaiser
Receive the latest updates directly to your inbox.
Nft graphic
Mint this entry as an NFT to add it to your collection.
Verification
This entry has been permanently stored onchain and signed by its creator.
More from 4undRaiser

Skeleton

Skeleton

Skeleton