@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.
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-chainmerklePath:{ 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
- Node.js: 18+ (ESM recommended)
- Deps:
snarkjs,circomlibjs,ethers - MTC: use with
@hazbase/kitMultiTrustCredentialHelper
npm i @hazbase/zkThis 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.
This section demonstrates:
- Issuer issues a metric (builds Merkle leaf + anchor root + Merkle path) and stores anchorRoot on-chain.
- Holder generates a proof using issuer-provided merklePath (and
rand). - Anyone verifies on-chain (no issuer involvement required at verification time).
Assumption: issuer stores
randand can deliver it to the holder when needed (your current operational model).
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);
}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
);
}PoseidonHelper.init(): Promise<void>PoseidonHelper.toF(x): bigintPoseidonHelper.H1(x) / H2(a,b) / H3(a,b,c): bigintPoseidonHelper.genSalt(): bigint
-
generateProof(subject, holderAddr, opts)→Promise<ProofBundle>- Requires:
mtcAddress,chainIdmerklePath(issuer-provided{ root, siblings, pathPos })randifmode != 0(issuer-provided in your operational model)
- Requires:
-
generateProofAllowlist(args)→Promise<{ proof, publicSignals, ... }>- Uses
mtcAddressfor domain separation - Produces publicSignals aligned to your allowlist circuit layout
- Uses
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, ... }
- Convenience helper for issuers:
anchor mism(on-chain revert)- You stored the wrong value on-chain.
Metric.leafFullmust beanchorRoot(Merkle root), not leaf commitment.
- You stored the wrong value on-chain.
- Proof verifies locally but fails on-chain
- Check
mtcAddress/chainIddomain separation parity - Ensure
merklePath.rootequals on-chainMetric.leafFull - Ensure
addris uint160 bounded in circuit (should be in your latest circuits)
- Check
Apache-2.0