merkle-tree-poseidon-sdk 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,29 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ build:
11
+ runs-on: ubuntu-latest
12
+
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+
16
+ - name: Use Node.js
17
+ uses: actions/setup-node@v4
18
+ with:
19
+ node-version: "20"
20
+ cache: "npm"
21
+
22
+ - name: Install dependencies
23
+ run: npm install
24
+
25
+ - name: Run tests
26
+ run: npm test
27
+
28
+ - name: Build
29
+ run: npm run build
@@ -0,0 +1,19 @@
1
+ name: Publish to NPM
2
+
3
+ on:
4
+ release:
5
+ types: [created]
6
+
7
+ jobs:
8
+ publish:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+ - uses: actions/setup-node@v4
13
+ with:
14
+ node-version: "20"
15
+ registry-url: "https://registry.npmjs.org"
16
+ - run: npm install
17
+ - run: npm publish --access public
18
+ env:
19
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
package/README.md ADDED
@@ -0,0 +1,75 @@
1
+ # Merkle Tree Poseidon SDK
2
+
3
+ A TypeScript SDK for generating Merkle Tree proofs and selective disclosure inputs compatible with Poseidon-based Circom circuits.
4
+
5
+ This SDK is the companion to the official [HARA-ORG/circom-zk](https://github.com/HARA-ORG/circom-zk) repository, which contains the Circom circuit implementations and verification keys.
6
+
7
+ ## Features
8
+
9
+ - **Poseidon Hashing**: Uses `circomlibjs` for Snark-friendly hashing on the BN254 curve.
10
+ - **Merkle Tree Proofs**: Generates inclusion proofs for a fixed-depth (8) Merkle Tree.
11
+ - **Selective Disclosure**: Supports generating proof inputs for:
12
+ - **Numeric/Date Claims**: Proves that a value is greater than or equal to a threshold (e.g., age verification).
13
+ - **String Claims**: Proves equality to a known value without revealing the value itself (using Poseidon hashes).
14
+ - **Identity Binding**: Binds credentials to a public commitment (e.g., a wallet address).
15
+
16
+ ## Project Structure
17
+
18
+ ```text
19
+ src/
20
+ ├── core/ # Low-level primitives (Poseidon, Merkle, Encoding)
21
+ ├── sdk/ # Main SDK interface
22
+ └── index.ts # Library entry point
23
+ tests/
24
+ └── sdk.test.ts # Logic simulation (mirrors Circom constraints)
25
+ ```
26
+
27
+ ## Installation
28
+
29
+ ```bash
30
+ npm install
31
+ ```
32
+
33
+ ## Scripts
34
+
35
+ ### Run Logic Simulation
36
+
37
+ Verifies the SDK's logic against a TypeScript implementation of the `SelectiveDisclosure.circom` constraints.
38
+
39
+ ```bash
40
+ npm test
41
+ ```
42
+
43
+ ### Build SDK
44
+
45
+ Compiles TypeScript into the `dist/` directory.
46
+
47
+ ```bash
48
+ npm run build
49
+ ```
50
+
51
+ ## Usage Example
52
+
53
+ ```typescript
54
+ import { ZkIdentitySDK } from "merkle-tree-poseidon-sdk";
55
+
56
+ // 1. Define fields
57
+ const fields = [{ label: "credit_score", type: "number", value: 750 }];
58
+
59
+ // 2. Initialize SDK and build tree with a salt
60
+ const sdk = new ZkIdentitySDK(fields);
61
+ const salt = BigInt(123456);
62
+ await sdk.build(salt);
63
+
64
+ // 3. Generate input for SelectiveDisclosure circuit
65
+ const proofInput = await sdk.generateProofInput({
66
+ label: "credit_score",
67
+ identitySecret: BigInt("0x..."),
68
+ publicCommitment: BigInt("0x..."),
69
+ threshold: 700n,
70
+ });
71
+ ```
72
+
73
+ ## License
74
+
75
+ MIT
@@ -0,0 +1,16 @@
1
+ export type FieldElement = bigint;
2
+ export declare const CredentialType: {
3
+ readonly TEXT: 0n;
4
+ readonly EMAIL: 1n;
5
+ readonly NUMBER: 2n;
6
+ readonly DATE: 3n;
7
+ readonly ATTACHMENT: 4n;
8
+ readonly LONG_TEXT: 5n;
9
+ };
10
+ export interface CredentialField {
11
+ key: FieldElement;
12
+ typ: FieldElement;
13
+ value: FieldElement;
14
+ }
15
+ export declare function stringToField(input: string): FieldElement;
16
+ export declare function dateToField(dateStr: string): FieldElement;
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CredentialType = void 0;
4
+ exports.stringToField = stringToField;
5
+ exports.dateToField = dateToField;
6
+ const crypto_1 = require("crypto");
7
+ exports.CredentialType = {
8
+ TEXT: 0n,
9
+ EMAIL: 1n,
10
+ NUMBER: 2n,
11
+ DATE: 3n,
12
+ ATTACHMENT: 4n,
13
+ LONG_TEXT: 5n,
14
+ };
15
+ function stringToField(input) {
16
+ const BN254_PRIME = 21888242871839275222246405745257275088548364400416034343698204186575808495617n;
17
+ const bytes = Buffer.from(input, "utf8");
18
+ if (bytes.length <= 30) {
19
+ return BigInt("0x" + bytes.toString("hex")) % BN254_PRIME;
20
+ }
21
+ const digest = (0, crypto_1.createHash)("sha256").update(bytes).digest();
22
+ digest[0] &= 0x1f; // 253-bit truncation
23
+ return BigInt("0x" + digest.toString("hex")) % BN254_PRIME;
24
+ }
25
+ function dateToField(dateStr) {
26
+ const clean = dateStr.replace(/-/g, "");
27
+ if (!/^\d{8}$/.test(clean))
28
+ throw new Error(`Invalid date: ${dateStr}`);
29
+ return BigInt(clean);
30
+ }
@@ -0,0 +1,9 @@
1
+ import { FieldElement } from "./fields";
2
+ declare class PoseidonHasher {
3
+ private poseidon;
4
+ private F;
5
+ init(): Promise<void>;
6
+ hash(inputs: FieldElement[]): FieldElement;
7
+ }
8
+ export declare const poseidonHasher: PoseidonHasher;
9
+ export {};
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.poseidonHasher = void 0;
4
+ const circomlibjs_1 = require("circomlibjs");
5
+ class PoseidonHasher {
6
+ constructor() {
7
+ this.poseidon = null;
8
+ this.F = null;
9
+ }
10
+ async init() {
11
+ if (!this.poseidon) {
12
+ this.poseidon = await (0, circomlibjs_1.buildPoseidon)();
13
+ this.F = this.poseidon.F;
14
+ }
15
+ }
16
+ hash(inputs) {
17
+ if (!this.poseidon)
18
+ throw new Error("Call init() first");
19
+ return this.F.toObject(this.poseidon(inputs.map((x) => this.F.e(x))));
20
+ }
21
+ }
22
+ exports.poseidonHasher = new PoseidonHasher();
@@ -0,0 +1,15 @@
1
+ import { FieldElement } from "./fields";
2
+ export declare class MerkleTree {
3
+ private leaves;
4
+ private zeroLeaf;
5
+ private layers;
6
+ constructor();
7
+ init(zeroInputs?: FieldElement[]): Promise<void>;
8
+ setLeaf(index: number, leaf: FieldElement): void;
9
+ build(): bigint;
10
+ get root(): FieldElement;
11
+ generateProof(index: number): {
12
+ pathElements: FieldElement[];
13
+ pathIndices: number[];
14
+ };
15
+ }
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MerkleTree = void 0;
4
+ const poseidon_1 = require("./poseidon");
5
+ const TREE_DEPTH = 8;
6
+ const TREE_SIZE = 2 ** TREE_DEPTH; // 256
7
+ class MerkleTree {
8
+ constructor() {
9
+ this.leaves = [];
10
+ this.layers = [];
11
+ }
12
+ async init(zeroInputs = [0n, 0n, 0n, 0n]) {
13
+ await poseidon_1.poseidonHasher.init();
14
+ this.zeroLeaf = poseidon_1.poseidonHasher.hash(zeroInputs);
15
+ this.leaves = new Array(TREE_SIZE).fill(this.zeroLeaf);
16
+ }
17
+ setLeaf(index, leaf) {
18
+ if (index < 0 || index >= TREE_SIZE)
19
+ throw new Error("Index out of bounds");
20
+ this.leaves[index] = leaf;
21
+ this.layers = []; // invalidate cache
22
+ }
23
+ build() {
24
+ this.layers = [this.leaves.slice()];
25
+ let current = this.leaves.slice();
26
+ for (let d = 0; d < TREE_DEPTH; d++) {
27
+ const next = [];
28
+ for (let i = 0; i < current.length; i += 2) {
29
+ next.push(poseidon_1.poseidonHasher.hash([current[i], current[i + 1]]));
30
+ }
31
+ this.layers.push(next);
32
+ current = next;
33
+ }
34
+ return current[0];
35
+ }
36
+ get root() {
37
+ if (this.layers.length === 0)
38
+ this.build();
39
+ return this.layers[TREE_DEPTH][0];
40
+ }
41
+ generateProof(index) {
42
+ if (this.layers.length === 0)
43
+ this.build();
44
+ const pathElements = [];
45
+ const pathIndices = [];
46
+ let cur = index;
47
+ for (let d = 0; d < TREE_DEPTH; d++) {
48
+ const isRight = cur % 2 === 1;
49
+ pathIndices.push(isRight ? 1 : 0);
50
+ pathElements.push(this.layers[d][isRight ? cur - 1 : cur + 1]);
51
+ cur = Math.floor(cur / 2);
52
+ }
53
+ return { pathElements, pathIndices };
54
+ }
55
+ }
56
+ exports.MerkleTree = MerkleTree;
@@ -0,0 +1,4 @@
1
+ export * from "./sdk/index";
2
+ export * from "./core/poseidon";
3
+ export * from "./core/fields";
4
+ export * from "./core/tree";
package/dist/index.js ADDED
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./sdk/index"), exports);
18
+ __exportStar(require("./core/poseidon"), exports);
19
+ __exportStar(require("./core/fields"), exports);
20
+ __exportStar(require("./core/tree"), exports);
@@ -0,0 +1,45 @@
1
+ import { FieldElement } from "../core/fields";
2
+ export interface UserField {
3
+ label: string;
4
+ type: "text" | "email" | "number" | "date" | "attachment" | "long_text";
5
+ value: string | number | bigint;
6
+ required?: boolean;
7
+ }
8
+ export interface SDKProofInput {
9
+ key: string;
10
+ typ: string;
11
+ value: string;
12
+ salt: string;
13
+ pathElements: string[];
14
+ pathIndices: number[];
15
+ identitySecret: string;
16
+ credentialRoot: string;
17
+ publicCommitment: string;
18
+ threshold: string;
19
+ expectedValueHash: string;
20
+ }
21
+ export declare class ZkIdentitySDK {
22
+ private tree;
23
+ private fields;
24
+ private salt;
25
+ constructor(fields: UserField[]);
26
+ /**
27
+ * Encodes a user-friendly field value into a BN254 field element.
28
+ */
29
+ private encodeValue;
30
+ /**
31
+ * Builds the Merkle Tree using the provided salt.
32
+ * Indices are auto-generated based on the order in the constructor.
33
+ */
34
+ build(salt: FieldElement): Promise<FieldElement>;
35
+ get root(): FieldElement;
36
+ /**
37
+ * Generates the input for a ZK proof for a specific field (by label).
38
+ */
39
+ generateProofInput(params: {
40
+ label: string;
41
+ identitySecret: FieldElement;
42
+ publicCommitment: FieldElement;
43
+ threshold?: FieldElement;
44
+ }): Promise<SDKProofInput>;
45
+ }
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ZkIdentitySDK = void 0;
4
+ const tree_1 = require("../core/tree");
5
+ const poseidon_1 = require("../core/poseidon");
6
+ const fields_1 = require("../core/fields");
7
+ class ZkIdentitySDK {
8
+ constructor(fields) {
9
+ this.salt = null;
10
+ this.fields = fields;
11
+ this.tree = new tree_1.MerkleTree();
12
+ }
13
+ /**
14
+ * Encodes a user-friendly field value into a BN254 field element.
15
+ */
16
+ encodeValue(type, value) {
17
+ if (typeof value === "bigint")
18
+ return value;
19
+ if (typeof value === "number")
20
+ return BigInt(value);
21
+ switch (type) {
22
+ case "date":
23
+ return (0, fields_1.dateToField)(value);
24
+ case "number":
25
+ return BigInt(value);
26
+ default:
27
+ return (0, fields_1.stringToField)(value);
28
+ }
29
+ }
30
+ /**
31
+ * Builds the Merkle Tree using the provided salt.
32
+ * Indices are auto-generated based on the order in the constructor.
33
+ */
34
+ async build(salt) {
35
+ this.salt = salt;
36
+ await this.tree.init();
37
+ for (let i = 0; i < this.fields.length; i++) {
38
+ const field = this.fields[i];
39
+ const key = (0, fields_1.stringToField)(field.label);
40
+ const typ = fields_1.CredentialType[field.type.toUpperCase()];
41
+ const val = this.encodeValue(field.type, field.value);
42
+ // leaf = Poseidon(key, typ, value, salt)
43
+ const leaf = poseidon_1.poseidonHasher.hash([key, typ, val, salt]);
44
+ this.tree.setLeaf(i, leaf);
45
+ }
46
+ return this.tree.build();
47
+ }
48
+ get root() {
49
+ return this.tree.root;
50
+ }
51
+ /**
52
+ * Generates the input for a ZK proof for a specific field (by label).
53
+ */
54
+ async generateProofInput(params) {
55
+ if (this.salt === null)
56
+ throw new Error("Call build(salt) first");
57
+ const index = this.fields.findIndex(f => f.label === params.label);
58
+ if (index === -1)
59
+ throw new Error(`Field with label "${params.label}" not found`);
60
+ const field = this.fields[index];
61
+ const key = (0, fields_1.stringToField)(field.label);
62
+ const typ = fields_1.CredentialType[field.type.toUpperCase()];
63
+ const val = this.encodeValue(field.type, field.value);
64
+ const { pathElements, pathIndices } = this.tree.generateProof(index);
65
+ // Compute expectedValueHash for string/equality types
66
+ let expectedValueHash = 0n;
67
+ const isNumeric = field.type === "number" || field.type === "date";
68
+ if (!isNumeric) {
69
+ expectedValueHash = poseidon_1.poseidonHasher.hash([val]);
70
+ }
71
+ return {
72
+ key: key.toString(),
73
+ typ: typ.toString(),
74
+ value: val.toString(),
75
+ salt: this.salt.toString(),
76
+ pathElements: pathElements.map(x => x.toString()),
77
+ pathIndices,
78
+ identitySecret: params.identitySecret.toString(),
79
+ credentialRoot: this.tree.root.toString(),
80
+ publicCommitment: params.publicCommitment.toString(),
81
+ threshold: (params.threshold ?? 0n).toString(),
82
+ expectedValueHash: expectedValueHash.toString(),
83
+ };
84
+ }
85
+ }
86
+ exports.ZkIdentitySDK = ZkIdentitySDK;
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "merkle-tree-poseidon-sdk",
3
+ "version": "1.0.0",
4
+ "author": "ardial@hara.ag",
5
+ "description": "TypeScript SDK for Merkle Tree and Poseidon Selective Disclosure",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "scripts": {
9
+ "build": "tsc",
10
+ "test": "ts-node -T tests/sdk.test.ts",
11
+ "prepublishOnly": "npm test && npm run build"
12
+ },
13
+ "dependencies": {
14
+ "circomlibjs": "^0.1.7",
15
+ "merkletreejs": "^0.3.11",
16
+ "snarkjs": "^0.7.4"
17
+ },
18
+ "devDependencies": {
19
+ "@types/node": "^20.11.0",
20
+ "ts-node": "^10.9.2",
21
+ "typescript": "^5.3.3"
22
+ }
23
+ }
@@ -0,0 +1,35 @@
1
+ import { createHash } from "crypto";
2
+
3
+ export type FieldElement = bigint;
4
+
5
+ export const CredentialType = {
6
+ TEXT: 0n,
7
+ EMAIL: 1n,
8
+ NUMBER: 2n,
9
+ DATE: 3n,
10
+ ATTACHMENT: 4n,
11
+ LONG_TEXT: 5n,
12
+ } as const;
13
+
14
+ export interface CredentialField {
15
+ key: FieldElement; // Numeric label identifier
16
+ typ: FieldElement; // CredentialType value
17
+ value: FieldElement; // The credential value
18
+ }
19
+
20
+ export function stringToField(input: string): FieldElement {
21
+ const BN254_PRIME = 21888242871839275222246405745257275088548364400416034343698204186575808495617n;
22
+ const bytes = Buffer.from(input, "utf8");
23
+ if (bytes.length <= 30) {
24
+ return BigInt("0x" + bytes.toString("hex")) % BN254_PRIME;
25
+ }
26
+ const digest = createHash("sha256").update(bytes).digest();
27
+ digest[0] &= 0x1f; // 253-bit truncation
28
+ return BigInt("0x" + digest.toString("hex")) % BN254_PRIME;
29
+ }
30
+
31
+ export function dateToField(dateStr: string): FieldElement {
32
+ const clean = dateStr.replace(/-/g, "");
33
+ if (!/^\d{8}$/.test(clean)) throw new Error(`Invalid date: ${dateStr}`);
34
+ return BigInt(clean);
35
+ }
@@ -0,0 +1,21 @@
1
+ import { buildPoseidon } from "circomlibjs";
2
+ import { FieldElement } from "./fields";
3
+
4
+ class PoseidonHasher {
5
+ private poseidon: any = null;
6
+ private F: any = null;
7
+
8
+ async init() {
9
+ if (!this.poseidon) {
10
+ this.poseidon = await buildPoseidon();
11
+ this.F = this.poseidon.F;
12
+ }
13
+ }
14
+
15
+ hash(inputs: FieldElement[]): FieldElement {
16
+ if (!this.poseidon) throw new Error("Call init() first");
17
+ return this.F.toObject(this.poseidon(inputs.map((x) => this.F.e(x)))) as bigint;
18
+ }
19
+ }
20
+
21
+ export const poseidonHasher = new PoseidonHasher();
@@ -0,0 +1,62 @@
1
+ import { poseidonHasher } from "./poseidon";
2
+ import { FieldElement } from "./fields";
3
+
4
+ const TREE_DEPTH = 8;
5
+ const TREE_SIZE = 2 ** TREE_DEPTH; // 256
6
+
7
+ export class MerkleTree {
8
+ private leaves: FieldElement[] = [];
9
+ private zeroLeaf!: FieldElement;
10
+ private layers: FieldElement[][] = [];
11
+
12
+ constructor() { }
13
+
14
+ async init(zeroInputs: FieldElement[] = [0n, 0n, 0n, 0n]) {
15
+ await poseidonHasher.init();
16
+ this.zeroLeaf = poseidonHasher.hash(zeroInputs);
17
+ this.leaves = new Array(TREE_SIZE).fill(this.zeroLeaf);
18
+ }
19
+
20
+ setLeaf(index: number, leaf: FieldElement) {
21
+ if (index < 0 || index >= TREE_SIZE) throw new Error("Index out of bounds");
22
+ this.leaves[index] = leaf;
23
+ this.layers = []; // invalidate cache
24
+ }
25
+
26
+ build() {
27
+ this.layers = [this.leaves.slice()];
28
+ let current = this.leaves.slice();
29
+
30
+ for (let d = 0; d < TREE_DEPTH; d++) {
31
+ const next: FieldElement[] = [];
32
+ for (let i = 0; i < current.length; i += 2) {
33
+ next.push(poseidonHasher.hash([current[i], current[i + 1]]));
34
+ }
35
+ this.layers.push(next);
36
+ current = next;
37
+ }
38
+ return current[0];
39
+ }
40
+
41
+ get root(): FieldElement {
42
+ if (this.layers.length === 0) this.build();
43
+ return this.layers[TREE_DEPTH][0];
44
+ }
45
+
46
+ generateProof(index: number): { pathElements: FieldElement[]; pathIndices: number[] } {
47
+ if (this.layers.length === 0) this.build();
48
+
49
+ const pathElements: FieldElement[] = [];
50
+ const pathIndices: number[] = [];
51
+ let cur = index;
52
+
53
+ for (let d = 0; d < TREE_DEPTH; d++) {
54
+ const isRight = cur % 2 === 1;
55
+ pathIndices.push(isRight ? 1 : 0);
56
+ pathElements.push(this.layers[d][isRight ? cur - 1 : cur + 1]);
57
+ cur = Math.floor(cur / 2);
58
+ }
59
+
60
+ return { pathElements, pathIndices };
61
+ }
62
+ }
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from "./sdk/index";
2
+ export * from "./core/poseidon";
3
+ export * from "./core/fields";
4
+ export * from "./core/tree";
@@ -0,0 +1,126 @@
1
+ import { MerkleTree } from "../core/tree";
2
+ import { poseidonHasher } from "../core/poseidon";
3
+ import {
4
+ stringToField,
5
+ dateToField,
6
+ CredentialType,
7
+ FieldElement
8
+ } from "../core/fields";
9
+
10
+ export interface UserField {
11
+ label: string;
12
+ type: "text" | "email" | "number" | "date" | "attachment" | "long_text";
13
+ value: string | number | bigint;
14
+ required?: boolean;
15
+ }
16
+
17
+ export interface SDKProofInput {
18
+ key: string;
19
+ typ: string;
20
+ value: string;
21
+ salt: string;
22
+ pathElements: string[];
23
+ pathIndices: number[];
24
+ identitySecret: string;
25
+ credentialRoot: string;
26
+ publicCommitment: string;
27
+ threshold: string;
28
+ expectedValueHash: string;
29
+ }
30
+
31
+ export class ZkIdentitySDK {
32
+ private tree: MerkleTree;
33
+ private fields: UserField[];
34
+ private salt: FieldElement | null = null;
35
+
36
+ constructor(fields: UserField[]) {
37
+ this.fields = fields;
38
+ this.tree = new MerkleTree();
39
+ }
40
+
41
+ /**
42
+ * Encodes a user-friendly field value into a BN254 field element.
43
+ */
44
+ private encodeValue(type: string, value: any): FieldElement {
45
+ if (typeof value === "bigint") return value;
46
+ if (typeof value === "number") return BigInt(value);
47
+
48
+ switch (type) {
49
+ case "date":
50
+ return dateToField(value);
51
+ case "number":
52
+ return BigInt(value);
53
+ default:
54
+ return stringToField(value);
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Builds the Merkle Tree using the provided salt.
60
+ * Indices are auto-generated based on the order in the constructor.
61
+ */
62
+ async build(salt: FieldElement): Promise<FieldElement> {
63
+ this.salt = salt;
64
+ await this.tree.init();
65
+
66
+ for (let i = 0; i < this.fields.length; i++) {
67
+ const field = this.fields[i];
68
+ const key = stringToField(field.label);
69
+ const typ = CredentialType[field.type.toUpperCase() as keyof typeof CredentialType];
70
+ const val = this.encodeValue(field.type, field.value);
71
+
72
+ // leaf = Poseidon(key, typ, value, salt)
73
+ const leaf = poseidonHasher.hash([key, typ, val, salt]);
74
+ this.tree.setLeaf(i, leaf);
75
+ }
76
+
77
+ return this.tree.build();
78
+ }
79
+
80
+ get root(): FieldElement {
81
+ return this.tree.root;
82
+ }
83
+
84
+ /**
85
+ * Generates the input for a ZK proof for a specific field (by label).
86
+ */
87
+ async generateProofInput(params: {
88
+ label: string;
89
+ identitySecret: FieldElement;
90
+ publicCommitment: FieldElement;
91
+ threshold?: FieldElement;
92
+ }): Promise<SDKProofInput> {
93
+ if (this.salt === null) throw new Error("Call build(salt) first");
94
+
95
+ const index = this.fields.findIndex(f => f.label === params.label);
96
+ if (index === -1) throw new Error(`Field with label "${params.label}" not found`);
97
+
98
+ const field = this.fields[index];
99
+ const key = stringToField(field.label);
100
+ const typ = CredentialType[field.type.toUpperCase() as keyof typeof CredentialType];
101
+ const val = this.encodeValue(field.type, field.value);
102
+
103
+ const { pathElements, pathIndices } = this.tree.generateProof(index);
104
+
105
+ // Compute expectedValueHash for string/equality types
106
+ let expectedValueHash = 0n;
107
+ const isNumeric = field.type === "number" || field.type === "date";
108
+ if (!isNumeric) {
109
+ expectedValueHash = poseidonHasher.hash([val]);
110
+ }
111
+
112
+ return {
113
+ key: key.toString(),
114
+ typ: typ.toString(),
115
+ value: val.toString(),
116
+ salt: this.salt.toString(),
117
+ pathElements: pathElements.map(x => x.toString()),
118
+ pathIndices,
119
+ identitySecret: params.identitySecret.toString(),
120
+ credentialRoot: this.tree.root.toString(),
121
+ publicCommitment: params.publicCommitment.toString(),
122
+ threshold: (params.threshold ?? 0n).toString(),
123
+ expectedValueHash: expectedValueHash.toString(),
124
+ };
125
+ }
126
+ }
package/src/types.d.ts ADDED
@@ -0,0 +1 @@
1
+ declare module 'circomlibjs';
@@ -0,0 +1,136 @@
1
+ import { ZkIdentitySDK } from "../src/sdk";
2
+ import { poseidonHasher } from "../src/core/poseidon";
3
+ import { createHash } from "crypto";
4
+ import { CredentialType, stringToField } from "../src/core/fields";
5
+
6
+ async function runSimulation() {
7
+ console.log("━━━ SDK Logic Simulation (Circom Logic) ━━━\n");
8
+
9
+ // Initialize Poseidon
10
+ await poseidonHasher.init();
11
+
12
+ // 1. Setup Prover state
13
+ const identitySecret = BigInt("0x" + createHash("sha256").update("alice-secret-key").digest("hex"));
14
+ const publicCommitment = poseidonHasher.hash([identitySecret]);
15
+
16
+ const fields = [
17
+ { label: "full_name", type: "text" as const, value: "Alice Wanderer" },
18
+ { label: "email", type: "email" as const, value: "alice@example.com" },
19
+ { label: "credit_score", type: "number" as const, value: 750 },
20
+ { label: "Expiry Date", type: "date" as const, value: "2026-12-31" }
21
+ ];
22
+
23
+ const sdk = new ZkIdentitySDK(fields);
24
+ const salt = BigInt(987654321);
25
+ const root = await sdk.build(salt);
26
+
27
+ console.log("Prover state initialized.");
28
+ console.log(`Merkle Root: ${root}`);
29
+ console.log(`Public Commitment: ${publicCommitment}\n`);
30
+
31
+ // 2. Simulate Selective Disclosure for "credit_score" (Numeric)
32
+ console.log("--- Simulating Numeric Disclosure (credit_score >= 700) ---");
33
+ const threshold = 700n;
34
+ const numericInput = await sdk.generateProofInput({
35
+ label: "credit_score",
36
+ identitySecret,
37
+ publicCommitment,
38
+ threshold
39
+ });
40
+
41
+ // Simulated Circuit Verification Logic
42
+ const isVerifiedNumeric = await simulateCircuit(numericInput);
43
+ console.log(`Simulation Result: ${isVerifiedNumeric ? "✅ PASSED" : "❌ FAILED"}\n`);
44
+
45
+ // 3. Simulate Selective Disclosure for "email" (String Equality)
46
+ console.log("--- Simulating String Equality Disclosure (email) ---");
47
+ const emailInput = await sdk.generateProofInput({
48
+ label: "email",
49
+ identitySecret,
50
+ publicCommitment
51
+ });
52
+
53
+ // Simulated Circuit Verification Logic
54
+ const isVerifiedString = await simulateCircuit(emailInput);
55
+ console.log(`Simulation Result: ${isVerifiedString ? "✅ PASSED" : "❌ FAILED"}\n`);
56
+
57
+ console.log("━━━ All Simulations Complete ━━━");
58
+ }
59
+
60
+ /**
61
+ * Simulates the SelectiveDisclosure.circom logic in TypeScript.
62
+ */
63
+ async function simulateCircuit(input: any): Promise<boolean> {
64
+ const {
65
+ key,
66
+ typ,
67
+ value,
68
+ salt,
69
+ pathElements,
70
+ pathIndices,
71
+ identitySecret,
72
+ credentialRoot,
73
+ publicCommitment,
74
+ threshold,
75
+ expectedValueHash
76
+ } = input;
77
+
78
+ const bKey = BigInt(key);
79
+ const bTyp = BigInt(typ);
80
+ const bValue = BigInt(value);
81
+ const bSalt = BigInt(salt);
82
+ const bIdentitySecret = BigInt(identitySecret);
83
+ const bCredentialRoot = BigInt(credentialRoot);
84
+ const bPublicCommitment = BigInt(publicCommitment);
85
+ const bThreshold = BigInt(threshold);
86
+ const bExpectedValueHash = BigInt(expectedValueHash);
87
+
88
+ // 1. IDENTITY BINDING
89
+ const computedCommitment = poseidonHasher.hash([bIdentitySecret]);
90
+ if (computedCommitment !== bPublicCommitment) {
91
+ console.error("❌ Simulation: Identity Binding failed");
92
+ return false;
93
+ }
94
+
95
+ // 2. LEAF CONSTRUCTION
96
+ const leaf = poseidonHasher.hash([bKey, bTyp, bValue, bSalt]);
97
+
98
+ // 3. MERKLE ROOT VERIFICATION
99
+ let current = leaf;
100
+ for (let i = 0; i < pathElements.length; i++) {
101
+ const element = BigInt(pathElements[i]);
102
+ const index = pathIndices[i];
103
+ if (index === 0) {
104
+ current = poseidonHasher.hash([current, element]);
105
+ } else {
106
+ current = poseidonHasher.hash([element, current]);
107
+ }
108
+ }
109
+
110
+ if (current !== bCredentialRoot) {
111
+ console.error("❌ Simulation: Merkle Proof failed");
112
+ return false;
113
+ }
114
+
115
+ // 4. TYPE ROUTING
116
+ const isNumeric = bTyp === 2n || bTyp === 3n; // NUMBER=2, DATE=3
117
+
118
+ if (isNumeric) {
119
+ // 4a. NUMERIC CHECK
120
+ if (bValue < bThreshold) {
121
+ console.error(`❌ Simulation: Numeric check failed (${bValue} < ${bThreshold})`);
122
+ return false;
123
+ }
124
+ } else {
125
+ // 4b. HASH EQUALITY CHECK
126
+ const valueHash = poseidonHasher.hash([bValue]);
127
+ if (valueHash !== bExpectedValueHash) {
128
+ console.error("❌ Simulation: Hash equality failed");
129
+ return false;
130
+ }
131
+ }
132
+
133
+ return true;
134
+ }
135
+
136
+ runSimulation().catch(console.error);
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "CommonJS",
5
+ "lib": ["ES2020"],
6
+ "declaration": true,
7
+ "outDir": "dist",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "moduleResolution": "node",
13
+ "resolveJsonModule": true
14
+ },
15
+ "include": ["src/**/*"],
16
+ "exclude": ["node_modules", "dist", "tests"]
17
+ }