npm package: @hazbase/zk
Description: An SDK helper that integrates with the hazBase backend to ZK (Groth16) KYC/threshold proofs using Poseidon commitments and Merkle membership proofs.
Author: IndieSquare Inc
Latest version: 0.1.0

# @hazbase/zk
[![npm version](https://badge.fury.io/js/@hazbase%2Fzk.svg)](https://badge.fury.io/js/@hazbase%2Fzk)
[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)

## 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
```bash
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`)

```ts
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)`

```ts
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