@unlink-xyz/core 0.1.0 → 0.1.2
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/.eslintrc.json +4 -0
- package/account/zkAccount.test.ts +316 -0
- package/account/zkAccount.ts +222 -0
- package/clients/broadcaster.ts +67 -0
- package/clients/http.ts +94 -0
- package/clients/indexer.ts +150 -0
- package/config.ts +39 -0
- package/core.ts +17 -0
- package/dist/account/railgun-imports-prototype.d.ts +12 -0
- package/dist/account/railgun-imports-prototype.d.ts.map +1 -0
- package/dist/account/railgun-imports-prototype.js +30 -0
- package/dist/clients/indexer.d.ts.map +1 -1
- package/dist/clients/indexer.js +1 -1
- package/dist/state/hydrator.d.ts +16 -0
- package/dist/state/hydrator.d.ts.map +1 -0
- package/dist/state/hydrator.js +18 -0
- package/dist/state/job-store.d.ts +12 -0
- package/dist/state/job-store.d.ts.map +1 -0
- package/dist/state/job-store.js +118 -0
- package/dist/state/jobs.d.ts +50 -0
- package/dist/state/jobs.d.ts.map +1 -0
- package/dist/state/jobs.js +1 -0
- package/dist/state.d.ts +83 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +171 -0
- package/dist/transactions/deposit.d.ts +0 -2
- package/dist/transactions/deposit.d.ts.map +1 -1
- package/dist/transactions/deposit.js +5 -9
- package/dist/transactions/note-sync.d.ts.map +1 -1
- package/dist/transactions/note-sync.js +1 -1
- package/dist/transactions/shield.d.ts +5 -0
- package/dist/transactions/shield.d.ts.map +1 -0
- package/dist/transactions/shield.js +93 -0
- package/dist/transactions/transact.d.ts +0 -5
- package/dist/transactions/transact.d.ts.map +1 -1
- package/dist/transactions/transact.js +2 -2
- package/dist/transactions/utils.d.ts +10 -0
- package/dist/transactions/utils.d.ts.map +1 -0
- package/dist/transactions/utils.js +17 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/utils/time.d.ts +2 -0
- package/dist/utils/time.d.ts.map +1 -0
- package/dist/utils/time.js +3 -0
- package/dist/utils/witness.d.ts +11 -0
- package/dist/utils/witness.d.ts.map +1 -0
- package/dist/utils/witness.js +19 -0
- package/errors.ts +20 -0
- package/index.ts +17 -0
- package/key-derivation/babyjubjub.ts +11 -0
- package/key-derivation/bech32.test.ts +90 -0
- package/key-derivation/bech32.ts +124 -0
- package/key-derivation/bip32.ts +56 -0
- package/key-derivation/bip39.ts +76 -0
- package/key-derivation/bytes.ts +118 -0
- package/key-derivation/hash.ts +13 -0
- package/key-derivation/index.ts +7 -0
- package/key-derivation/wallet-node.ts +155 -0
- package/keys.ts +47 -0
- package/package.json +4 -5
- package/prover/config.ts +104 -0
- package/prover/index.ts +1 -0
- package/prover/prover.integration.test.ts +162 -0
- package/prover/prover.test.ts +309 -0
- package/prover/prover.ts +405 -0
- package/prover/registry.test.ts +90 -0
- package/prover/registry.ts +82 -0
- package/schema.ts +17 -0
- package/setup-artifacts.sh +57 -0
- package/state/index.ts +2 -0
- package/state/merkle/hydrator.ts +69 -0
- package/state/merkle/index.ts +12 -0
- package/state/merkle/merkle-tree.test.ts +50 -0
- package/state/merkle/merkle-tree.ts +163 -0
- package/state/store/ciphertext-store.ts +28 -0
- package/state/store/index.ts +24 -0
- package/state/store/job-store.ts +162 -0
- package/state/store/jobs.ts +64 -0
- package/state/store/leaf-store.ts +39 -0
- package/state/store/note-store.ts +177 -0
- package/state/store/nullifier-store.ts +39 -0
- package/state/store/records.ts +61 -0
- package/state/store/root-store.ts +34 -0
- package/state/store/store.ts +25 -0
- package/state.test.ts +235 -0
- package/storage/index.ts +3 -0
- package/storage/indexeddb.test.ts +99 -0
- package/storage/indexeddb.ts +235 -0
- package/storage/memory.test.ts +59 -0
- package/storage/memory.ts +93 -0
- package/transactions/deposit.test.ts +160 -0
- package/transactions/deposit.ts +227 -0
- package/transactions/index.ts +20 -0
- package/transactions/note-sync.test.ts +155 -0
- package/transactions/note-sync.ts +452 -0
- package/transactions/reconcile.ts +73 -0
- package/transactions/transact.test.ts +451 -0
- package/transactions/transact.ts +811 -0
- package/transactions/types.ts +141 -0
- package/tsconfig.json +14 -0
- package/types/global.d.ts +15 -0
- package/types.ts +24 -0
- package/utils/async.ts +15 -0
- package/utils/bigint.ts +34 -0
- package/utils/crypto.test.ts +69 -0
- package/utils/crypto.ts +58 -0
- package/utils/json-codec.ts +38 -0
- package/utils/polling.ts +6 -0
- package/utils/signature.ts +16 -0
- package/utils/validators.test.ts +64 -0
- package/utils/validators.ts +86 -0
package/keys.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { KeyValidationError } from "./errors.js";
|
|
2
|
+
|
|
3
|
+
export const RESERVED_PREFIXES = [
|
|
4
|
+
"meta:",
|
|
5
|
+
"notes:",
|
|
6
|
+
"leaves:",
|
|
7
|
+
"roots:",
|
|
8
|
+
"nullifiers:",
|
|
9
|
+
"ciphertexts:",
|
|
10
|
+
"jobs:",
|
|
11
|
+
"proof_cache:",
|
|
12
|
+
"cfg:",
|
|
13
|
+
"idx:",
|
|
14
|
+
"locks:",
|
|
15
|
+
] as const;
|
|
16
|
+
|
|
17
|
+
/** Canonical builders for storage key namespaces used across core */
|
|
18
|
+
export const keys = {
|
|
19
|
+
note: (c: number, i: number) => `notes:${c}:${i}`,
|
|
20
|
+
leaf: (c: number, i: number) => `leaves:${c}:${i}`,
|
|
21
|
+
ciphertext: (c: number, i: number) => `ciphertexts:${c}:${i}`,
|
|
22
|
+
/** Track note indices that remain unspent for a given master public key (mpk acts as the account identifier). */
|
|
23
|
+
unspent: (c: number, mpk: string, i: number) =>
|
|
24
|
+
`idx:notes:unspent:${c}:${mpk}:${i}`,
|
|
25
|
+
unspentPrefix: (c: number, mpk: string) => `idx:notes:unspent:${c}:${mpk}:`,
|
|
26
|
+
nullifierObs: (c: number, n: string) => `nullifiers:${c}:${n}`,
|
|
27
|
+
nullToIndex: (c: number, n: string) => `idx:nullifier:${c}:${n}`,
|
|
28
|
+
nullifier: (c: number, n: string) => `nullifiers:${c}:${n}`,
|
|
29
|
+
root: (c: number, value: string) => `roots:${c}:${value}`,
|
|
30
|
+
latestRoot: (c: number) => `roots:latest:${c}`,
|
|
31
|
+
rootCursor: (c: number) => `meta:roots:cursor:${c}`,
|
|
32
|
+
cursor: (c: number) => `meta:sync:cursors:${c}`,
|
|
33
|
+
aggregate: (c: number, a: string) => `idx:notes:agg:${c}:${a}`,
|
|
34
|
+
job: (relayId: string) => `jobs:${relayId}`,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const MAX_KEY_LEN = 512;
|
|
38
|
+
|
|
39
|
+
export function validateKey(key: string) {
|
|
40
|
+
if (!key) throw new KeyValidationError("key must not be empty");
|
|
41
|
+
if (key.length > MAX_KEY_LEN)
|
|
42
|
+
throw new KeyValidationError(`key exceeds ${MAX_KEY_LEN}`);
|
|
43
|
+
if (!RESERVED_PREFIXES.some((p) => key.startsWith(p)))
|
|
44
|
+
throw new KeyValidationError(
|
|
45
|
+
"key must start with a reserved namespace prefix",
|
|
46
|
+
);
|
|
47
|
+
}
|
package/package.json
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unlink-xyz/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": {
|
|
9
|
-
"
|
|
10
|
-
"
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js"
|
|
11
11
|
}
|
|
12
12
|
},
|
|
13
|
-
"files": ["dist"],
|
|
14
13
|
"publishConfig": {
|
|
15
|
-
"access": "
|
|
14
|
+
"access": "public"
|
|
16
15
|
},
|
|
17
16
|
"scripts": {
|
|
18
17
|
"build": "tsc -p tsconfig.json",
|
package/prover/config.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration for the prover system
|
|
3
|
+
* Handles artifact loading from local files or remote artifact server
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Environment-based configuration for artifact loading
|
|
8
|
+
*/
|
|
9
|
+
export interface ProverConfig {
|
|
10
|
+
/** Base URL for the artifact server (browser mode) */
|
|
11
|
+
rpcUrl: string;
|
|
12
|
+
/** Whether to enable artifact caching */
|
|
13
|
+
enableCache: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Default configuration
|
|
18
|
+
* Can be overridden via environment variables
|
|
19
|
+
*/
|
|
20
|
+
export const DEFAULT_CONFIG: ProverConfig = {
|
|
21
|
+
rpcUrl:
|
|
22
|
+
typeof process !== "undefined" && process.env?.UNLINK_RPC_URL
|
|
23
|
+
? process.env.UNLINK_RPC_URL
|
|
24
|
+
: "http://localhost:3000",
|
|
25
|
+
enableCache: true,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Artifact file types
|
|
30
|
+
*/
|
|
31
|
+
export enum ArtifactType {
|
|
32
|
+
WASM = "joinsplit.wasm",
|
|
33
|
+
ZKEY = "joinsplit.zkey",
|
|
34
|
+
VKEY = "joinsplit.vkey.json",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Build artifact URL for a given circuit and artifact type
|
|
39
|
+
* @param circuitName Full circuit name (e.g., "joinsplit_2x3_16")
|
|
40
|
+
* @param artifactType Type of artifact to fetch
|
|
41
|
+
* @param baseUrl Base URL of the artifact server
|
|
42
|
+
* @returns Full URL to the artifact
|
|
43
|
+
*/
|
|
44
|
+
export function buildArtifactUrl(
|
|
45
|
+
circuitName: string,
|
|
46
|
+
artifactType: ArtifactType,
|
|
47
|
+
baseUrl: string = DEFAULT_CONFIG.rpcUrl,
|
|
48
|
+
): string {
|
|
49
|
+
// Remove trailing slash from baseUrl
|
|
50
|
+
const cleanBaseUrl = baseUrl.replace(/\/$/, "");
|
|
51
|
+
return `${cleanBaseUrl}/artifacts/${circuitName}/${artifactType}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get relative path for local artifact loading
|
|
56
|
+
* Used in Node.js environment when artifacts are bundled
|
|
57
|
+
* @param circuitName Full circuit name (e.g., "joinsplit_2x3_16")
|
|
58
|
+
* @param artifactType Type of artifact
|
|
59
|
+
* @returns Relative path to artifact
|
|
60
|
+
*/
|
|
61
|
+
export function getLocalArtifactPath(
|
|
62
|
+
circuitName: string,
|
|
63
|
+
artifactType: ArtifactType,
|
|
64
|
+
): string[] {
|
|
65
|
+
// Return multiple candidate paths for different build scenarios
|
|
66
|
+
return [
|
|
67
|
+
// When compiled: dist/prover/ -> ../../artifacts/circuits/{circuit}/
|
|
68
|
+
`../../artifacts/circuits/${circuitName}/${artifactType}`,
|
|
69
|
+
// When in source: prover/ -> ../artifacts/circuits/{circuit}/
|
|
70
|
+
`../artifacts/circuits/${circuitName}/${artifactType}`,
|
|
71
|
+
];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Runtime detection utilities
|
|
76
|
+
*/
|
|
77
|
+
export const Runtime = {
|
|
78
|
+
/**
|
|
79
|
+
* Check if running in browser environment
|
|
80
|
+
*/
|
|
81
|
+
isBrowser(): boolean {
|
|
82
|
+
return typeof window !== "undefined" && typeof document !== "undefined";
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Check if running in Node.js environment
|
|
87
|
+
*/
|
|
88
|
+
isNode(): boolean {
|
|
89
|
+
return (
|
|
90
|
+
typeof process !== "undefined" &&
|
|
91
|
+
process.versions != null &&
|
|
92
|
+
process.versions.node != null
|
|
93
|
+
);
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get environment description for debugging
|
|
98
|
+
*/
|
|
99
|
+
getEnvironment(): string {
|
|
100
|
+
if (this.isBrowser()) return "browser";
|
|
101
|
+
if (this.isNode()) return "node";
|
|
102
|
+
return "unknown";
|
|
103
|
+
},
|
|
104
|
+
};
|
package/prover/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./prover.js";
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { access } from "fs/promises";
|
|
2
|
+
import { dirname, join } from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
import * as snarkjs from "snarkjs";
|
|
5
|
+
import { beforeAll, describe, expect, it } from "vitest";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
getVerificationKey,
|
|
9
|
+
proveTransaction,
|
|
10
|
+
type JoinsplitProofInput,
|
|
11
|
+
} from "./prover.js";
|
|
12
|
+
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = dirname(__filename);
|
|
15
|
+
|
|
16
|
+
describe("prover integration tests", () => {
|
|
17
|
+
beforeAll(async () => {
|
|
18
|
+
// Check if artifacts exist for 2x3 circuit (used in integration tests)
|
|
19
|
+
try {
|
|
20
|
+
await access(
|
|
21
|
+
join(
|
|
22
|
+
__dirname,
|
|
23
|
+
"../artifacts/circuits/joinsplit_2x3_16/joinsplit.wasm",
|
|
24
|
+
),
|
|
25
|
+
);
|
|
26
|
+
await access(
|
|
27
|
+
join(
|
|
28
|
+
__dirname,
|
|
29
|
+
"../artifacts/circuits/joinsplit_2x3_16/joinsplit.zkey",
|
|
30
|
+
),
|
|
31
|
+
);
|
|
32
|
+
await access(
|
|
33
|
+
join(
|
|
34
|
+
__dirname,
|
|
35
|
+
"../artifacts/circuits/joinsplit_2x3_16/joinsplit.vkey.json",
|
|
36
|
+
),
|
|
37
|
+
);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
"Circuit artifacts not found. Please run 'pnpm run circuits:build' first.",
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("generates and verifies a proof generated from circuits input data", async () => {
|
|
46
|
+
const rawInput = {
|
|
47
|
+
merkleRoot:
|
|
48
|
+
"6651503891008261868318637545520517015441423030408583353730529205210575769897",
|
|
49
|
+
boundParamsHash:
|
|
50
|
+
"4367284183670237676694687139389936823583752501847632190787848034614846786866",
|
|
51
|
+
nullifiers: [
|
|
52
|
+
"11485370912550667629147418652010973429416469356401563507077544465653018582229",
|
|
53
|
+
"12458947539527367086074368910577127304083837870968412606004383792911977699092",
|
|
54
|
+
],
|
|
55
|
+
commitmentsOut: [
|
|
56
|
+
"21220431320512498255211940241902464585964672736998306204392012684848332107713",
|
|
57
|
+
"10812352772790655187825976376417683652931326614980053685809861928409318557101",
|
|
58
|
+
"2361786391388492851501376027004057215483918983555223791892300314116572980398",
|
|
59
|
+
],
|
|
60
|
+
token: "611382286831621467233887798921843936019654057231",
|
|
61
|
+
publicKey: [
|
|
62
|
+
"17403090114984634995691076088606286689905312870166298587771402004938333701956",
|
|
63
|
+
"11686443728124206965533675395218715181279824020209406379329530395240161730269",
|
|
64
|
+
],
|
|
65
|
+
signature: [
|
|
66
|
+
"13337325824273672513648456655099758722924690095445255254284844332733013061113",
|
|
67
|
+
"11478044465897161338488651611555416955979346707535934533682695529282557178771",
|
|
68
|
+
"2690614056970046123325708657516589911150638516730022213521930951131599801259",
|
|
69
|
+
],
|
|
70
|
+
randomIn: [
|
|
71
|
+
"311410881652967579423188304066739460260",
|
|
72
|
+
"161576146678088229090350665564580980145",
|
|
73
|
+
],
|
|
74
|
+
valueIn: ["10000000000", "20000000000"],
|
|
75
|
+
pathElements: [
|
|
76
|
+
[
|
|
77
|
+
"14943413005531767156170859665453505874291109952298492592265028467982121566535",
|
|
78
|
+
"17140872132393658386372884137443465756166862794960926853080892710475197765189",
|
|
79
|
+
"6371505487407425546372285321506278506957375353941268479986307401454583028710",
|
|
80
|
+
"14682603619753984551536111997488330801501968646131197748838572499913532539651",
|
|
81
|
+
"4177329909171803290170279196019319447087145475250139657920728261184066492383",
|
|
82
|
+
"9144726083425293457189930182175584375755866170755203037168330473435257599943",
|
|
83
|
+
"8236371367060628245320813572019740297071112030680295018688014025332724941914",
|
|
84
|
+
"20704513355749821470300998799414731362245300238021178553122435057113528378476",
|
|
85
|
+
"16736886623818773524263138779809916105169520362832964900272252437275369959388",
|
|
86
|
+
"16415580715665869864634788741927065679186058985255106992778939765291984104497",
|
|
87
|
+
"5715205213015845364387052206849889843188282449933545435544812025608946666822",
|
|
88
|
+
"11013175031749877081979736330659999751637067247483574805902945041305483682579",
|
|
89
|
+
"10708260192616993577686743465610377735773279483333447100671640147037242870779",
|
|
90
|
+
"17708681376628530799996447981717676562184543188562031708516142906406141650368",
|
|
91
|
+
"3967876508977884960877910167550068853181553556854105674925421366723704424223",
|
|
92
|
+
"18077967785446169488603147322170139942753858441115263187838141084900323103797",
|
|
93
|
+
],
|
|
94
|
+
[
|
|
95
|
+
"17155689016641574060256511555039569894135211745581590863068780884287641639347",
|
|
96
|
+
"17140872132393658386372884137443465756166862794960926853080892710475197765189",
|
|
97
|
+
"6371505487407425546372285321506278506957375353941268479986307401454583028710",
|
|
98
|
+
"14682603619753984551536111997488330801501968646131197748838572499913532539651",
|
|
99
|
+
"4177329909171803290170279196019319447087145475250139657920728261184066492383",
|
|
100
|
+
"9144726083425293457189930182175584375755866170755203037168330473435257599943",
|
|
101
|
+
"8236371367060628245320813572019740297071112030680295018688014025332724941914",
|
|
102
|
+
"20704513355749821470300998799414731362245300238021178553122435057113528378476",
|
|
103
|
+
"16736886623818773524263138779809916105169520362832964900272252437275369959388",
|
|
104
|
+
"16415580715665869864634788741927065679186058985255106992778939765291984104497",
|
|
105
|
+
"5715205213015845364387052206849889843188282449933545435544812025608946666822",
|
|
106
|
+
"11013175031749877081979736330659999751637067247483574805902945041305483682579",
|
|
107
|
+
"10708260192616993577686743465610377735773279483333447100671640147037242870779",
|
|
108
|
+
"17708681376628530799996447981717676562184543188562031708516142906406141650368",
|
|
109
|
+
"3967876508977884960877910167550068853181553556854105674925421366723704424223",
|
|
110
|
+
"18077967785446169488603147322170139942753858441115263187838141084900323103797",
|
|
111
|
+
],
|
|
112
|
+
],
|
|
113
|
+
leavesIndices: [0, 1],
|
|
114
|
+
nullifyingKey:
|
|
115
|
+
"18659841675752504949640201002407906598138517454751630186524244551004513393000",
|
|
116
|
+
npkOut: [
|
|
117
|
+
"5969622909025228762817370697760280284869505282539549033240181049624056088653",
|
|
118
|
+
"2100582032716223613287570049117084073292258181991442907862227237312284306360",
|
|
119
|
+
"742175203644052806963513562623187657396500499529893903278346823527475860966",
|
|
120
|
+
],
|
|
121
|
+
valueOut: ["10000000000", "10000000000", "10000000000"],
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// Format circuit inputs (convert strings to bigints)
|
|
125
|
+
// Using the exact field names the circuit expects
|
|
126
|
+
const circuitInput: JoinsplitProofInput = {
|
|
127
|
+
merkleRoot: BigInt(rawInput.merkleRoot),
|
|
128
|
+
boundParamsHash: BigInt(rawInput.boundParamsHash),
|
|
129
|
+
nullifiers: rawInput.nullifiers.map((n: string) => BigInt(n)),
|
|
130
|
+
commitmentsOut: rawInput.commitmentsOut.map((c: string) => BigInt(c)),
|
|
131
|
+
token: BigInt(rawInput.token),
|
|
132
|
+
publicKey: rawInput.publicKey.map((pk: string) => BigInt(pk)),
|
|
133
|
+
signature: rawInput.signature.map((s: string) => BigInt(s)),
|
|
134
|
+
randomIn: rawInput.randomIn.map((r: string) => BigInt(r)),
|
|
135
|
+
valueIn: rawInput.valueIn.map((v: string) => BigInt(v)),
|
|
136
|
+
pathElements: rawInput.pathElements.map((pe: string[]) =>
|
|
137
|
+
pe.map((e) => BigInt(e)),
|
|
138
|
+
),
|
|
139
|
+
leavesIndices: rawInput.leavesIndices.map((i: number) => BigInt(i)),
|
|
140
|
+
nullifyingKey: BigInt(rawInput.nullifyingKey),
|
|
141
|
+
npkOut: rawInput.npkOut.map((npk: string) => BigInt(npk)),
|
|
142
|
+
valueOut: rawInput.valueOut.map((v: string) => BigInt(v)),
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// Generate proof using real circuit (automatically selects 2x3 based on input dimensions)
|
|
146
|
+
const { proof, publicSignals } = await proveTransaction(circuitInput);
|
|
147
|
+
|
|
148
|
+
// Verify the generated proof (2 inputs, 3 outputs)
|
|
149
|
+
const vkey = await getVerificationKey(2, 3);
|
|
150
|
+
const isValid = await snarkjs.groth16.verify(vkey, publicSignals, proof);
|
|
151
|
+
|
|
152
|
+
// Assertions
|
|
153
|
+
expect(isValid).toBe(true);
|
|
154
|
+
expect(proof.protocol).toBe("groth16");
|
|
155
|
+
expect(proof.curve).toBe("bn128");
|
|
156
|
+
expect(publicSignals).toHaveLength(7); // Should match nPublic
|
|
157
|
+
|
|
158
|
+
// Verify public signals match expected values from circuit
|
|
159
|
+
expect(publicSignals[0]).toBe(rawInput.merkleRoot);
|
|
160
|
+
expect(publicSignals[1]).toBe(rawInput.boundParamsHash);
|
|
161
|
+
}, 30000); // Increase timeout as proof generation can take time
|
|
162
|
+
});
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { access } from "fs/promises";
|
|
2
|
+
import { dirname, join } from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
import * as snarkjs from "snarkjs";
|
|
5
|
+
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
|
6
|
+
|
|
7
|
+
import { CoreError } from "../errors.js";
|
|
8
|
+
import {
|
|
9
|
+
getVerificationKey,
|
|
10
|
+
proveTransaction,
|
|
11
|
+
verifyProof,
|
|
12
|
+
type JoinsplitProofInput,
|
|
13
|
+
} from "./prover.js";
|
|
14
|
+
|
|
15
|
+
// Mock snarkjs module
|
|
16
|
+
vi.mock("snarkjs");
|
|
17
|
+
|
|
18
|
+
// Test helpers
|
|
19
|
+
function createMockInput(
|
|
20
|
+
overrides?: Partial<JoinsplitProofInput>,
|
|
21
|
+
): JoinsplitProofInput {
|
|
22
|
+
return {
|
|
23
|
+
merkleRoot: 1n,
|
|
24
|
+
boundParamsHash: 2n,
|
|
25
|
+
nullifiers: [3n, 4n],
|
|
26
|
+
commitmentsOut: [5n, 6n, 7n],
|
|
27
|
+
token: 8n,
|
|
28
|
+
publicKey: [9n, 10n],
|
|
29
|
+
signature: [11n, 11n, 11n],
|
|
30
|
+
randomIn: [12n, 13n],
|
|
31
|
+
valueIn: [14n, 15n],
|
|
32
|
+
pathElements: [[0n], [0n]],
|
|
33
|
+
leavesIndices: [16n, 17n],
|
|
34
|
+
nullifyingKey: 18n,
|
|
35
|
+
npkOut: [19n, 20n, 21n],
|
|
36
|
+
valueOut: [22n, 23n, 24n],
|
|
37
|
+
...overrides,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function createMockProof() {
|
|
42
|
+
return {
|
|
43
|
+
pi_a: ["1", "2", "3"],
|
|
44
|
+
pi_b: [
|
|
45
|
+
["4", "5"],
|
|
46
|
+
["6", "7"],
|
|
47
|
+
["8", "9"],
|
|
48
|
+
],
|
|
49
|
+
pi_c: ["10", "11", "12"],
|
|
50
|
+
protocol: "groth16" as const,
|
|
51
|
+
curve: "bn128" as const,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function createMockPublicSignals() {
|
|
56
|
+
// 2x3 circuit has 7 public signals: [root, hash, nullifier1, nullifier2, commit1, commit2, commit3]
|
|
57
|
+
return ["25", "26", "27", "28", "29", "30", "31"];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
describe("prover", () => {
|
|
61
|
+
beforeAll(async () => {
|
|
62
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
63
|
+
const __dirname = dirname(__filename);
|
|
64
|
+
|
|
65
|
+
// Check if artifacts exist for 2x3 circuit (used in these tests)
|
|
66
|
+
try {
|
|
67
|
+
await access(
|
|
68
|
+
join(
|
|
69
|
+
__dirname,
|
|
70
|
+
"../artifacts/circuits/joinsplit_2x3_16/joinsplit.wasm",
|
|
71
|
+
),
|
|
72
|
+
);
|
|
73
|
+
await access(
|
|
74
|
+
join(
|
|
75
|
+
__dirname,
|
|
76
|
+
"../artifacts/circuits/joinsplit_2x3_16/joinsplit.zkey",
|
|
77
|
+
),
|
|
78
|
+
);
|
|
79
|
+
await access(
|
|
80
|
+
join(
|
|
81
|
+
__dirname,
|
|
82
|
+
"../artifacts/circuits/joinsplit_2x3_16/joinsplit.vkey.json",
|
|
83
|
+
),
|
|
84
|
+
);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
"Circuit artifacts not found. Please run 'pnpm run circuits:build' first.",
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Mock verify to return true by default (proveTransaction calls it internally)
|
|
92
|
+
const verifyMock = vi.mocked(snarkjs.groth16.verify);
|
|
93
|
+
verifyMock.mockResolvedValue(true);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
afterEach(() => {
|
|
97
|
+
vi.clearAllMocks();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("getVerificationKey", () => {
|
|
101
|
+
it("loads and returns the verification key", async () => {
|
|
102
|
+
// Test uses 2 inputs, 3 outputs (see createMockInput)
|
|
103
|
+
const vkey = await getVerificationKey(2, 3);
|
|
104
|
+
|
|
105
|
+
expect(vkey).toBeDefined();
|
|
106
|
+
expect(vkey.protocol).toBe("groth16");
|
|
107
|
+
expect(vkey.curve).toBe("bn128");
|
|
108
|
+
expect(vkey.nPublic).toBe(7); // 2 (root + hash) + 2 (nullifiers) + 3 (commitments) = 7
|
|
109
|
+
expect(vkey.vk_alpha_1).toBeInstanceOf(Array);
|
|
110
|
+
expect(vkey.vk_beta_2).toBeInstanceOf(Array);
|
|
111
|
+
expect(vkey.vk_gamma_2).toBeInstanceOf(Array);
|
|
112
|
+
expect(vkey.vk_delta_2).toBeInstanceOf(Array);
|
|
113
|
+
expect(vkey.vk_alphabeta_12).toBeInstanceOf(Array);
|
|
114
|
+
expect(vkey.IC).toBeInstanceOf(Array);
|
|
115
|
+
expect(vkey.IC.length).toBe(8); // nPublic + 1
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("fetchArtifact", () => {
|
|
120
|
+
it("successfully loads wasm and zkey files", async () => {
|
|
121
|
+
const mockInput = createMockInput();
|
|
122
|
+
const mockProof = createMockProof();
|
|
123
|
+
const mockPublicSignals = createMockPublicSignals();
|
|
124
|
+
|
|
125
|
+
const fullProveMock = vi.mocked(snarkjs.groth16.fullProve);
|
|
126
|
+
fullProveMock.mockResolvedValue({
|
|
127
|
+
proof: mockProof,
|
|
128
|
+
publicSignals: mockPublicSignals,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// This should not throw file loading errors
|
|
132
|
+
const result = await proveTransaction(mockInput);
|
|
133
|
+
|
|
134
|
+
expect(result).toBeDefined();
|
|
135
|
+
expect(result.proof).toEqual(mockProof);
|
|
136
|
+
expect(result.publicSignals).toEqual(mockPublicSignals);
|
|
137
|
+
|
|
138
|
+
// Verify fullProve was called with buffers (artifacts were loaded)
|
|
139
|
+
expect(fullProveMock).toHaveBeenCalledTimes(1);
|
|
140
|
+
const call = fullProveMock.mock.calls[0];
|
|
141
|
+
expect(call[1]).toBeInstanceOf(Uint8Array); // wasm bytes
|
|
142
|
+
expect(call[2]).toBeInstanceOf(Uint8Array); // zkey bytes
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe("proveTransaction", () => {
|
|
147
|
+
it("generates proof with valid input", async () => {
|
|
148
|
+
const mockInput = createMockInput({
|
|
149
|
+
merkleRoot: 100n,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const mockProof = createMockProof();
|
|
153
|
+
const mockPublicSignals = createMockPublicSignals();
|
|
154
|
+
|
|
155
|
+
const fullProveMock = vi.mocked(snarkjs.groth16.fullProve);
|
|
156
|
+
fullProveMock.mockResolvedValue({
|
|
157
|
+
proof: mockProof,
|
|
158
|
+
publicSignals: mockPublicSignals,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const result = await proveTransaction(mockInput);
|
|
162
|
+
|
|
163
|
+
expect(result.proof).toEqual(mockProof);
|
|
164
|
+
expect(result.publicSignals).toEqual(mockPublicSignals);
|
|
165
|
+
expect(fullProveMock).toHaveBeenCalledTimes(1);
|
|
166
|
+
|
|
167
|
+
// Verify the call was made with input signals and file buffers
|
|
168
|
+
const call = fullProveMock.mock.calls[0];
|
|
169
|
+
expect(call[0]).toBeDefined(); // inputSignals
|
|
170
|
+
expect(call[1]).toBeInstanceOf(Uint8Array); // wasm bytes
|
|
171
|
+
expect(call[2]).toBeInstanceOf(Uint8Array); // zkey bytes
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("wraps errors with CoreError", async () => {
|
|
175
|
+
const mockInput = createMockInput({
|
|
176
|
+
nullifiers: [3n],
|
|
177
|
+
commitmentsOut: [4n],
|
|
178
|
+
randomIn: [9n],
|
|
179
|
+
valueIn: [10n],
|
|
180
|
+
pathElements: [[0n]],
|
|
181
|
+
leavesIndices: [11n],
|
|
182
|
+
npkOut: [13n],
|
|
183
|
+
valueOut: [14n],
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const mockError = new Error("snarkjs internal error");
|
|
187
|
+
const fullProveMock = vi.mocked(snarkjs.groth16.fullProve);
|
|
188
|
+
fullProveMock.mockRejectedValue(mockError);
|
|
189
|
+
|
|
190
|
+
await expect(proveTransaction(mockInput)).rejects.toThrow(CoreError);
|
|
191
|
+
await expect(proveTransaction(mockInput)).rejects.toThrow(
|
|
192
|
+
"proof generation failed: snarkjs internal error",
|
|
193
|
+
);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe("verifyProof", () => {
|
|
198
|
+
it("verifies a valid proof successfully", async () => {
|
|
199
|
+
const mockProof = createMockProof();
|
|
200
|
+
const mockPublicSignals = createMockPublicSignals();
|
|
201
|
+
|
|
202
|
+
const verifyMock = vi.mocked(snarkjs.groth16.verify);
|
|
203
|
+
verifyMock.mockResolvedValue(true);
|
|
204
|
+
|
|
205
|
+
const result = await verifyProof(mockProof, mockPublicSignals);
|
|
206
|
+
|
|
207
|
+
expect(result).toBe(true);
|
|
208
|
+
expect(verifyMock).toHaveBeenCalledTimes(1);
|
|
209
|
+
|
|
210
|
+
// Verify it was called with vkey, publicSignals, and proof
|
|
211
|
+
const call = verifyMock.mock.calls[0];
|
|
212
|
+
expect(call[0]).toBeDefined(); // vkey
|
|
213
|
+
expect(call[1]).toEqual(mockPublicSignals);
|
|
214
|
+
expect(call[2]).toEqual(mockProof);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("returns false for invalid proof", async () => {
|
|
218
|
+
const mockProof = createMockProof();
|
|
219
|
+
const mockPublicSignals = createMockPublicSignals();
|
|
220
|
+
|
|
221
|
+
const verifyMock = vi.mocked(snarkjs.groth16.verify);
|
|
222
|
+
verifyMock.mockResolvedValue(false);
|
|
223
|
+
|
|
224
|
+
const result = await verifyProof(mockProof, mockPublicSignals);
|
|
225
|
+
|
|
226
|
+
expect(result).toBe(false);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("wraps errors with CoreError", async () => {
|
|
230
|
+
const mockProof = createMockProof();
|
|
231
|
+
const mockPublicSignals = createMockPublicSignals();
|
|
232
|
+
|
|
233
|
+
const mockError = new Error("verification error");
|
|
234
|
+
const verifyMock = vi.mocked(snarkjs.groth16.verify);
|
|
235
|
+
verifyMock.mockRejectedValue(mockError);
|
|
236
|
+
|
|
237
|
+
await expect(verifyProof(mockProof, mockPublicSignals)).rejects.toThrow(
|
|
238
|
+
CoreError,
|
|
239
|
+
);
|
|
240
|
+
await expect(verifyProof(mockProof, mockPublicSignals)).rejects.toThrow(
|
|
241
|
+
"proof verification failed",
|
|
242
|
+
);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("completes full prove and verify workflow", async () => {
|
|
246
|
+
const mockInput = createMockInput();
|
|
247
|
+
const mockProof = createMockProof();
|
|
248
|
+
const mockPublicSignals = createMockPublicSignals();
|
|
249
|
+
|
|
250
|
+
// Mock proof generation
|
|
251
|
+
const fullProveMock = vi.mocked(snarkjs.groth16.fullProve);
|
|
252
|
+
fullProveMock.mockResolvedValue({
|
|
253
|
+
proof: mockProof,
|
|
254
|
+
publicSignals: mockPublicSignals,
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// Mock verification
|
|
258
|
+
const verifyMock = vi.mocked(snarkjs.groth16.verify);
|
|
259
|
+
verifyMock.mockResolvedValue(true);
|
|
260
|
+
|
|
261
|
+
// Generate proof
|
|
262
|
+
const proofResult = await proveTransaction(mockInput);
|
|
263
|
+
|
|
264
|
+
// Verify proof
|
|
265
|
+
const verificationResult = await verifyProof(
|
|
266
|
+
proofResult.proof,
|
|
267
|
+
proofResult.publicSignals,
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
expect(verificationResult).toBe(true);
|
|
271
|
+
expect(fullProveMock).toHaveBeenCalledTimes(1);
|
|
272
|
+
expect(verifyMock).toHaveBeenCalledTimes(2); // 1 internal + 1 explicit
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
describe("artifact caching", () => {
|
|
277
|
+
it("loads artifacts only once across multiple calls", async () => {
|
|
278
|
+
const mockInput = createMockInput();
|
|
279
|
+
const mockProof = createMockProof();
|
|
280
|
+
const mockPublicSignals = createMockPublicSignals();
|
|
281
|
+
|
|
282
|
+
const fullProveMock = vi.mocked(snarkjs.groth16.fullProve);
|
|
283
|
+
fullProveMock.mockResolvedValue({
|
|
284
|
+
proof: mockProof,
|
|
285
|
+
publicSignals: mockPublicSignals,
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
const verifyMock = vi.mocked(snarkjs.groth16.verify);
|
|
289
|
+
verifyMock.mockResolvedValue(true);
|
|
290
|
+
|
|
291
|
+
// Make multiple calls
|
|
292
|
+
await proveTransaction(mockInput);
|
|
293
|
+
await proveTransaction(mockInput);
|
|
294
|
+
await getVerificationKey(2, 3); // 2 inputs, 3 outputs
|
|
295
|
+
await verifyProof(mockProof, mockPublicSignals);
|
|
296
|
+
|
|
297
|
+
// All functions should work and be called
|
|
298
|
+
expect(fullProveMock).toHaveBeenCalledTimes(2);
|
|
299
|
+
expect(verifyMock).toHaveBeenCalledTimes(3); // 2 internal + 1 explicit
|
|
300
|
+
|
|
301
|
+
// Verify artifacts are being reused (same Uint8Array instances)
|
|
302
|
+
const firstCall = fullProveMock.mock.calls[0];
|
|
303
|
+
const secondCall = fullProveMock.mock.calls[1];
|
|
304
|
+
|
|
305
|
+
expect(firstCall[1]).toBe(secondCall[1]); // Same wasm buffer instance
|
|
306
|
+
expect(firstCall[2]).toBe(secondCall[2]); // Same zkey buffer instance
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
});
|