Skip to content
/ zk Public

An SDK helper that integrates with the hazBase backend to ZK (Groth16) KYC/threshold proofs using Poseidon commitments and Merkle membership proofs.

License

Notifications You must be signed in to change notification settings

hazbase/zk

Repository files navigation

@hazbase/zk

npm version License

Overview

@hazbase/zk is a utility toolkit for Poseidon hashing, Merkle trees, and Groth16 proofs.

It is designed to be used together with MultiTrustCredential (MTC) and provides low-level building blocks to create commitment-based proofs (e.g., score ≥ threshold, allowlist membership) with minimal disclosure.

Key concept (important)

In the latest MTC design, the on-chain metric field Metric.leafFull stores the anchor root:

  • anchorRoot: Merkle root (anchor) stored on-chain (Metric.leafFull)
  • leafCommitment: commitment used inside the Merkle leaf (e.g., Poseidon(score, rand)), not stored on-chain
  • merklePath: { root, siblings, pathPos } provided by the issuer to the holder/prover

Proof generation requires anchorRoot + merklePath. The prover should not try to recompute root locally from currentRoot only.

Core capabilities:

  • Poseidon helpers (init, toF, H1/H2/H3, genSalt)
  • Deterministic allowlist Merkle utilities (normalize → deduplicate → sort ascending → pad)
  • Merkle path generation utilities for issuers
  • Groth16 proof generation:
    • generateProof (baseline / threshold comparisons)
    • generateProofAllowlist (allowlist membership)
    • generateProofRange, generateProofDelta (RANGE/DELTA predicates, if enabled in your build)
  • First-class integration with MTC (@hazbase/kit) for on-chain proof flows

Requirements

  • Node.js: 18+ (ESM recommended)
  • Deps: snarkjs, circomlibjs, ethers
  • MTC: use with @hazbase/kit MultiTrustCredentialHelper

Installation

npm i @hazbase/zk

Configuration

This package does not read environment variables directly. Provide paths to circuit assets and network/domain info explicitly from your application.

Domain separation uses:

  • domain = keccak256(abi.encode(chainId, mtcAddress)) mod Fr

So pass chainId and MTC contract address (mtcAddress) when generating proofs/paths.


Quick start (Issuer → Holder → Verifier)

This section demonstrates:

  1. Issuer issues a metric (builds Merkle leaf + anchor root + Merkle path) and stores anchorRoot on-chain.
  2. Holder generates a proof using issuer-provided merklePath (and rand).
  3. Anyone verifies on-chain (no issuer involvement required at verification time).

Assumption: issuer stores rand and can deliver it to the holder when needed (your current operational model).


A) Proof of Threshold (baseline: proveMetric)

import { ethers } from "ethers";
import { PoseidonHelper, genValuesWithAnchor, generateProof } from "@hazbase/zk";
import { MultiTrustCredentialHelper } from "@hazbase/kit";

async function run() {
  const provider = new ethers.JsonRpcProvider(process.env.RPC_URL!);
  const admin = new ethers.Wallet(process.env.ADMIN_KEY!, provider);
  const issuer = new ethers.Wallet(process.env.ISSUER_KEY!, provider);
  const student = new ethers.Wallet(process.env.STUDENT_KEY!, provider);

  // Deploy & attach MTC
  const { address } = await MultiTrustCredentialHelper.deploy({ admin: admin.address }, admin);
  const mtc = MultiTrustCredentialHelper.attach(address, admin);

  // Register a commitment metric (score) and authorize issuer
  const metricId = ethers.id("exam-score");
  const ROLE = ethers.id("EXAM_SCORE_ROLE");
  await mtc.registerMetric(metricId, "ExamScore", ROLE, true, MultiTrustCredentialHelper.CompareMask.GTE);
  await mtc.contract.grantRole(ROLE, issuer.address);

  // (Issuer) build commitment + insert into issuer Merkle tree to get anchorRoot + merklePath
  const realScore = 80n;

  // In production, issuer maintains a persistent Merkle tree state.
  // Here we demonstrate with an empty tree and index 0 for simplicity.
  const issued = await genValuesWithAnchor({
    score: realScore,
    walletAddress: student.address,
    chainId: 11155111,
    mtcAddress: mtc.address,
    currentRoot: 0n,
    nextIndex: 0
  });

  // Store anchorRoot on-chain (maps to Metric.leafFull)
  await mtc.connect(issuer).mint(student.address, {
    metricId,
    value: 0,
    anchorRoot: issued.anchorRoot, // IMPORTANT: anchorRoot (Merkle root), not leafCommitment
    uri: "",
    expiresAt: 0
  });

  // (Holder) generate proof using issuer-provided merklePath and issuer-provided rand
  // Issuer must deliver:
  //   - issued.merklePath (root/siblings/pathPos)
  //   - issued.rand (for score commitment)
  const proofBundle = await generateProof(
    {
      govId: "X987654",
      name: "Alice Chember",
      dobYMD: 12345678,
      country: 392
    },
    student.address,
    {
      mode: "GTE",
      threshold: 60n,
      score: realScore,
      rand: issued.rand,
      idNull: PoseidonHelper.genSalt(),
      chainId: 11155111,
      mtcAddress: mtc.address,
      merklePath: issued.merklePath
    }
  );

  // (Holder) verify on-chain via baseline proveMetric
  const tokenId = MultiTrustCredentialHelper.tokenIdFor(student.address);
  const { a, b, c } = proofBundle.proof;

  await mtc.connect(student).proveMetric(tokenId, metricId, a, b, c, proofBundle.publicSignals);
}

B) Proof of Allowlist Membership (ZKEx: provePredicate)

This uses the ZKEx flow (provePredicate) and predicate profiles on the MTC contract. You must configure:

  • setPredicateAllowed(metricId, predicateType, true)
  • setPredicateProfile(metricId, predicateType, verifier, signalsLen, anchorIndex, addrIndex, epochIndex, epochCheck, requireMaskZero)
import { ethers } from "ethers";
import { PoseidonHelper, generateProofAllowlist } from "@hazbase/zk";
import { MultiTrustCredentialHelper } from "@hazbase/kit";

async function run() {
  const provider = new ethers.JsonRpcProvider(process.env.RPC_URL!);
  const admin = new ethers.Wallet(process.env.ADMIN_KEY!, provider);
  const issuer = new ethers.Wallet(process.env.ISSUER_KEY!, provider);
  const alice  = new ethers.Wallet(process.env.ALICE_KEY!, provider);

  const { address } = await MultiTrustCredentialHelper.deploy({ admin: admin.address }, admin);
  const mtc = MultiTrustCredentialHelper.attach(address, admin);

  // Register an allowlist metric (compareMask must be 0 if requireMaskZero=true in profile)
  const metricId = ethers.id("country-code");
  const ROLE = ethers.id("COUNTRY_CODE_ROLE");
  await mtc.registerMetric(metricId, "CountryCode", ROLE, true, MultiTrustCredentialHelper.CompareMask.NONE);
  await mtc.contract.grantRole(ROLE, issuer.address);

  // Configure predicate (ALLOWLIST) on-chain (admin)
  const pred = MultiTrustCredentialHelper.PredicateType.ALLOWLIST;

  await mtc.setPredicateAllowed(metricId, pred, true);

  // Legacy allowlist layout:
  //   [0]=issuerRoot(anchor), [1]=allowRoot, [2]=nullifier, [3]=addr, [4]=statementHash, [5]=leaf
  await mtc.setPredicateProfile(
    metricId,
    pred,
    process.env.ALLOWLIST_VERIFIER_ADDRESS as `0x${string}`,
    6,   // signalsLen
    0,   // anchorIndex
    3,   // addrIndex
    0,   // epochIndex (unused)
    false, // epochCheck
    true   // requireMaskZero
  );

  const allowValues = [392n, 840n, 124n]; // JP/US/CA (example)
  const idNull = PoseidonHelper.genSalt();

  // Issuer must provide issuer-side Merkle path data to the holder/prover.
  // This call demonstrates proof generation; issuer path wiring is application-specific.
  const proofBundle = await generateProofAllowlist({
    list: allowValues,
    policyId: metricId,
    policyVersion: 1,
    addr: alice.address,
    value: 392n,
    salt: PoseidonHelper.genSalt(),
    idNull,
    chainId: 11155111,
    mtcAddress: mtc.address
  });

  const tokenId = MultiTrustCredentialHelper.tokenIdFor(alice.address);
  await mtc.connect(alice).provePredicate(
    tokenId,
    metricId,
    pred,
    proofBundle.proof,
    proofBundle.publicSignals
  );
}

Function reference (Core API)

Poseidon / Field

  • PoseidonHelper.init(): Promise<void>
  • PoseidonHelper.toF(x): bigint
  • PoseidonHelper.H1(x) / H2(a,b) / H3(a,b,c): bigint
  • PoseidonHelper.genSalt(): bigint

Proof generators

  • generateProof(subject, holderAddr, opts)Promise<ProofBundle>

    • Requires:
      • mtcAddress, chainId
      • merklePath (issuer-provided { root, siblings, pathPos })
      • rand if mode != 0 (issuer-provided in your operational model)
  • generateProofAllowlist(args)Promise<{ proof, publicSignals, ... }>

    • Uses mtcAddress for domain separation
    • Produces publicSignals aligned to your allowlist circuit layout

Issuer utilities

  • genScoreCommitment(score, rand?)
  • genValuesWithAnchor(opts)
    • Convenience helper for issuers:
      • Generates a score commitment
      • Builds treeLeaf = Poseidon(leafCommitment, addr, domain)
      • Inserts into a local Merkle tree and returns { anchorRoot, merklePath, rand, ... }

Troubleshooting

  • anchor mism (on-chain revert)
    • You stored the wrong value on-chain. Metric.leafFull must be anchorRoot (Merkle root), not leaf commitment.
  • Proof verifies locally but fails on-chain
    • Check mtcAddress/chainId domain separation parity
    • Ensure merklePath.root equals on-chain Metric.leafFull
    • Ensure addr is uint160 bounded in circuit (should be in your latest circuits)

License

Apache-2.0

About

An SDK helper that integrates with the hazBase backend to ZK (Groth16) KYC/threshold proofs using Poseidon commitments and Merkle membership proofs.

Topics

Resources

License

Contributing

Stars

Watchers

Forks