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.
- package/.github/workflows/ci.yml +29 -0
- package/.github/workflows/publish.yml +19 -0
- package/README.md +75 -0
- package/dist/core/fields.d.ts +16 -0
- package/dist/core/fields.js +30 -0
- package/dist/core/poseidon.d.ts +9 -0
- package/dist/core/poseidon.js +22 -0
- package/dist/core/tree.d.ts +15 -0
- package/dist/core/tree.js +56 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +20 -0
- package/dist/sdk/index.d.ts +45 -0
- package/dist/sdk/index.js +86 -0
- package/package.json +23 -0
- package/src/core/fields.ts +35 -0
- package/src/core/poseidon.ts +21 -0
- package/src/core/tree.ts +62 -0
- package/src/index.ts +4 -0
- package/src/sdk/index.ts +126 -0
- package/src/types.d.ts +1 -0
- package/tests/sdk.test.ts +136 -0
- package/tsconfig.json +17 -0
|
@@ -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,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;
|
package/dist/index.d.ts
ADDED
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();
|
package/src/core/tree.ts
ADDED
|
@@ -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
package/src/sdk/index.ts
ADDED
|
@@ -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
|
+
}
|