https://github.com/hazbase/zk
An SDK helper that integrates with the hazBase backend to ZK (Groth16) KYC/threshold proofs using Poseidon commitments and Merkle membership proofs.
https://github.com/hazbase/zk
backend blockchain dapps ethereum evm web3 zero-knowledge zk
Last synced: 2 months ago
JSON representation
An SDK helper that integrates with the hazBase backend to ZK (Groth16) KYC/threshold proofs using Poseidon commitments and Merkle membership proofs.
- Host: GitHub
- URL: https://github.com/hazbase/zk
- Owner: hazbase
- License: apache-2.0
- Created: 2025-09-29T03:32:20.000Z (9 months ago)
- Default Branch: main
- Last Pushed: 2025-12-29T15:17:39.000Z (5 months ago)
- Last Synced: 2026-01-01T19:31:23.916Z (5 months ago)
- Topics: backend, blockchain, dapps, ethereum, evm, web3, zero-knowledge, zk
- Language: TypeScript
- Homepage: https://lp.hazbase.com
- Size: 19.6 MB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
Awesome Lists containing this project
README
# @hazbase/zk
[](https://badge.fury.io/js/@hazbase%2Fzk)
[](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`
- `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`
- 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