@utxopia/sdk 0.1.0-alpha.2 → 0.1.0-alpha.4

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.
Files changed (142) hide show
  1. package/README.md +214 -108
  2. package/{packages/sdk/dist → dist}/client.d.ts +25 -1
  3. package/{packages/sdk/dist → dist}/client.js +36 -1
  4. package/{packages/sdk/dist → dist}/crypto-ed25519.d.ts +14 -0
  5. package/{packages/sdk/dist → dist}/crypto-ed25519.js +14 -0
  6. package/{packages/sdk/dist → dist}/index.d.ts +3 -3
  7. package/{packages/sdk/dist → dist}/index.js +3 -3
  8. package/{packages/sdk/dist → dist}/instructions.d.ts +28 -11
  9. package/{packages/sdk/dist → dist}/instructions.js +45 -15
  10. package/{packages/sdk/dist → dist}/psbt.d.ts +12 -2
  11. package/{packages/sdk/dist → dist}/psbt.js +17 -12
  12. package/{packages/sdk/dist → dist}/stealth.d.ts +104 -9
  13. package/{packages/sdk/dist → dist}/stealth.js +146 -14
  14. package/{packages/sdk/dist → dist}/taproot.d.ts +39 -2
  15. package/{packages/sdk/dist → dist}/taproot.js +54 -2
  16. package/package.json +86 -63
  17. package/src/announcement-client.ts +457 -0
  18. package/src/auditor-ciphertext.ts +181 -0
  19. package/src/auditor.ts +409 -0
  20. package/src/bitcoin/ika.ts +103 -0
  21. package/src/bitcoin/index.ts +5 -0
  22. package/src/bound-params.ts +322 -0
  23. package/src/chadbuffer.ts +603 -0
  24. package/src/circomlibjs.d.ts +51 -0
  25. package/src/claim-link.ts +53 -0
  26. package/src/client.ts +638 -0
  27. package/src/commitment-tree.ts +736 -0
  28. package/src/config.ts +772 -0
  29. package/src/core/esplora.ts +332 -0
  30. package/src/core/mempool.ts +159 -0
  31. package/src/crypto-babyjub.ts +385 -0
  32. package/src/crypto-ed25519.ts +297 -0
  33. package/src/crypto.ts +199 -0
  34. package/src/event-client.ts +231 -0
  35. package/src/events.ts +384 -0
  36. package/src/explorer.ts +300 -0
  37. package/src/index.ts +902 -0
  38. package/src/instructions.ts +2820 -0
  39. package/src/keys.ts +1228 -0
  40. package/src/logger.ts +41 -0
  41. package/src/magicblock.ts +278 -0
  42. package/src/merkle.ts +197 -0
  43. package/src/note.ts +754 -0
  44. package/src/pda.ts +516 -0
  45. package/src/pool-state.ts +176 -0
  46. package/src/poseidon.ts +175 -0
  47. package/src/prover/index.ts +19 -0
  48. package/src/prover/mobile.ts +303 -0
  49. package/src/prover/web.ts +771 -0
  50. package/src/psbt.ts +345 -0
  51. package/src/selective-disclosure.ts +284 -0
  52. package/src/sender-memo.ts +343 -0
  53. package/src/snarkjs.d.ts +19 -0
  54. package/src/sns-resolver.ts +333 -0
  55. package/src/solana/connection.ts +189 -0
  56. package/src/solana/priority-fee.ts +201 -0
  57. package/src/spend-doc.ts +163 -0
  58. package/src/stealth.ts +1477 -0
  59. package/src/taproot.ts +707 -0
  60. package/src/token-registry.ts +207 -0
  61. package/src/utils/encoding.ts +33 -0
  62. package/src/vk-registry.ts +295 -0
  63. package/LICENSE +0 -21
  64. package/packages/btc-client/src/esplora-client.ts +0 -153
  65. package/packages/btc-client/src/index.ts +0 -3
  66. package/packages/btc-client/src/op-return.ts +0 -93
  67. package/packages/btc-client/src/types.ts +0 -112
  68. package/packages/sdk/README.md +0 -277
  69. /package/{packages/sdk/dist → dist}/announcement-client.d.ts +0 -0
  70. /package/{packages/sdk/dist → dist}/announcement-client.js +0 -0
  71. /package/{packages/sdk/dist → dist}/auditor-ciphertext.d.ts +0 -0
  72. /package/{packages/sdk/dist → dist}/auditor-ciphertext.js +0 -0
  73. /package/{packages/sdk/dist → dist}/auditor.d.ts +0 -0
  74. /package/{packages/sdk/dist → dist}/auditor.js +0 -0
  75. /package/{packages/sdk/dist → dist}/bitcoin/ika.d.ts +0 -0
  76. /package/{packages/sdk/dist → dist}/bitcoin/ika.js +0 -0
  77. /package/{packages/sdk/dist → dist}/bitcoin/index.d.ts +0 -0
  78. /package/{packages/sdk/dist → dist}/bitcoin/index.js +0 -0
  79. /package/{packages/sdk/dist → dist}/bound-params.d.ts +0 -0
  80. /package/{packages/sdk/dist → dist}/bound-params.js +0 -0
  81. /package/{packages/sdk/dist → dist}/chadbuffer.d.ts +0 -0
  82. /package/{packages/sdk/dist → dist}/chadbuffer.js +0 -0
  83. /package/{packages/sdk/dist → dist}/claim-link.d.ts +0 -0
  84. /package/{packages/sdk/dist → dist}/claim-link.js +0 -0
  85. /package/{packages/sdk/dist → dist}/commitment-tree.d.ts +0 -0
  86. /package/{packages/sdk/dist → dist}/commitment-tree.js +0 -0
  87. /package/{packages/sdk/dist → dist}/config.d.ts +0 -0
  88. /package/{packages/sdk/dist → dist}/config.js +0 -0
  89. /package/{packages/sdk/dist → dist}/core/esplora.d.ts +0 -0
  90. /package/{packages/sdk/dist → dist}/core/esplora.js +0 -0
  91. /package/{packages/sdk/dist → dist}/core/mempool.d.ts +0 -0
  92. /package/{packages/sdk/dist → dist}/core/mempool.js +0 -0
  93. /package/{packages/sdk/dist → dist}/crypto-babyjub.d.ts +0 -0
  94. /package/{packages/sdk/dist → dist}/crypto-babyjub.js +0 -0
  95. /package/{packages/sdk/dist → dist}/crypto.d.ts +0 -0
  96. /package/{packages/sdk/dist → dist}/crypto.js +0 -0
  97. /package/{packages/sdk/dist → dist}/event-client.d.ts +0 -0
  98. /package/{packages/sdk/dist → dist}/event-client.js +0 -0
  99. /package/{packages/sdk/dist → dist}/events.d.ts +0 -0
  100. /package/{packages/sdk/dist → dist}/events.js +0 -0
  101. /package/{packages/sdk/dist → dist}/explorer.d.ts +0 -0
  102. /package/{packages/sdk/dist → dist}/explorer.js +0 -0
  103. /package/{packages/sdk/dist → dist}/keys.d.ts +0 -0
  104. /package/{packages/sdk/dist → dist}/keys.js +0 -0
  105. /package/{packages/sdk/dist → dist}/logger.d.ts +0 -0
  106. /package/{packages/sdk/dist → dist}/logger.js +0 -0
  107. /package/{packages/sdk/dist → dist}/magicblock.d.ts +0 -0
  108. /package/{packages/sdk/dist → dist}/magicblock.js +0 -0
  109. /package/{packages/sdk/dist → dist}/merkle.d.ts +0 -0
  110. /package/{packages/sdk/dist → dist}/merkle.js +0 -0
  111. /package/{packages/sdk/dist → dist}/note.d.ts +0 -0
  112. /package/{packages/sdk/dist → dist}/note.js +0 -0
  113. /package/{packages/sdk/dist → dist}/pda.d.ts +0 -0
  114. /package/{packages/sdk/dist → dist}/pda.js +0 -0
  115. /package/{packages/sdk/dist → dist}/pool-state.d.ts +0 -0
  116. /package/{packages/sdk/dist → dist}/pool-state.js +0 -0
  117. /package/{packages/sdk/dist → dist}/poseidon.d.ts +0 -0
  118. /package/{packages/sdk/dist → dist}/poseidon.js +0 -0
  119. /package/{packages/sdk/dist → dist}/prover/index.d.ts +0 -0
  120. /package/{packages/sdk/dist → dist}/prover/index.js +0 -0
  121. /package/{packages/sdk/dist → dist}/prover/mobile.d.ts +0 -0
  122. /package/{packages/sdk/dist → dist}/prover/mobile.js +0 -0
  123. /package/{packages/sdk/dist → dist}/prover/web.d.ts +0 -0
  124. /package/{packages/sdk/dist → dist}/prover/web.js +0 -0
  125. /package/{packages/sdk/dist → dist}/selective-disclosure.d.ts +0 -0
  126. /package/{packages/sdk/dist → dist}/selective-disclosure.js +0 -0
  127. /package/{packages/sdk/dist → dist}/sender-memo.d.ts +0 -0
  128. /package/{packages/sdk/dist → dist}/sender-memo.js +0 -0
  129. /package/{packages/sdk/dist → dist}/sns-resolver.d.ts +0 -0
  130. /package/{packages/sdk/dist → dist}/sns-resolver.js +0 -0
  131. /package/{packages/sdk/dist → dist}/solana/connection.d.ts +0 -0
  132. /package/{packages/sdk/dist → dist}/solana/connection.js +0 -0
  133. /package/{packages/sdk/dist → dist}/solana/priority-fee.d.ts +0 -0
  134. /package/{packages/sdk/dist → dist}/solana/priority-fee.js +0 -0
  135. /package/{packages/sdk/dist → dist}/spend-doc.d.ts +0 -0
  136. /package/{packages/sdk/dist → dist}/spend-doc.js +0 -0
  137. /package/{packages/sdk/dist → dist}/token-registry.d.ts +0 -0
  138. /package/{packages/sdk/dist → dist}/token-registry.js +0 -0
  139. /package/{packages/sdk/dist → dist}/utils/encoding.d.ts +0 -0
  140. /package/{packages/sdk/dist → dist}/utils/encoding.js +0 -0
  141. /package/{packages/sdk/dist → dist}/vk-registry.d.ts +0 -0
  142. /package/{packages/sdk/dist → dist}/vk-registry.js +0 -0
@@ -0,0 +1,771 @@
1
+ /**
2
+ * WASM-based Groth16 Proof Generator for UTXOPIA
3
+ *
4
+ * Universal prover that works in both Browser and Node.js environments.
5
+ * Uses Groth16 proofs via snarkjs with lazy loading.
6
+ *
7
+ * JOINSPLIT MODEL:
8
+ * - Commitment = Poseidon(npk, token, amount)
9
+ * - Nullifier = Poseidon(nullifyingKey, leafIndex)
10
+ * - Signature = EdDSA-Poseidon over (merkleRoot, boundParamsHash, nullifiers..., commitmentsOut...)
11
+ */
12
+
13
+ import {
14
+ poseidonHashSync,
15
+ computeJoinSplitCommitmentSync,
16
+ computeJoinSplitNullifierSync,
17
+ } from "../poseidon";
18
+ import { BN254_FIELD_PRIME } from "../crypto";
19
+ import { TREE_DEPTH } from "../merkle";
20
+ import { getConfig } from "../config";
21
+ import { MAX_SAFE_JOINSPLIT_SIZE } from "../vk-registry";
22
+ import type { Address } from "@solana/kit";
23
+
24
+ /** Maximum satoshis (total BTC supply) */
25
+ const MAX_SATOSHIS = 21_000_000n * 100_000_000n;
26
+
27
+ /**
28
+ * Validate that proof inputs are within BN254 field bounds.
29
+ */
30
+ function validateFieldInputs(fields: Record<string, bigint>): void {
31
+ for (const [name, value] of Object.entries(fields)) {
32
+ if (value < 0n) {
33
+ throw new Error(`Invalid proof input: ${name} is negative`);
34
+ }
35
+ if (value >= BN254_FIELD_PRIME) {
36
+ throw new Error(`Invalid proof input: ${name} exceeds BN254 field prime`);
37
+ }
38
+ }
39
+ }
40
+
41
+ function validateAmount(amount: bigint, label: string): void {
42
+ if (amount <= 0n) {
43
+ throw new Error(`Invalid proof input: ${label} must be positive`);
44
+ }
45
+ if (amount > MAX_SATOSHIS) {
46
+ throw new Error(`Invalid proof input: ${label} exceeds total BTC supply`);
47
+ }
48
+ }
49
+
50
+ export interface MerkleProofInput {
51
+ siblings: bigint[];
52
+ indices: number[];
53
+ }
54
+
55
+ export interface ProofData {
56
+ proof: Uint8Array;
57
+ publicInputs: string[];
58
+ verificationKey?: Uint8Array;
59
+ }
60
+
61
+ export type CircuitType = `joinsplit_${number}x${number}`;
62
+
63
+ /** Names of non-JoinSplit auxiliary circuits (selective disclosure). */
64
+ export type AuxCircuitName =
65
+ | "ownership"
66
+ | "range_sum"
67
+ | "range_sum_4"
68
+ | "range_sum_16";
69
+
70
+ // Environment detection
71
+ const isBrowser = typeof window !== "undefined";
72
+ const isNode = typeof process !== "undefined" && process.versions?.node;
73
+
74
+ // Configurable circuit paths
75
+ let circuitBasePath = isBrowser ? "/circuits/groth16" : "./circuits";
76
+
77
+ /**
78
+ * Set the base path for circuit artifacts
79
+ */
80
+ export function setCircuitPath(path: string): void {
81
+ if (path === circuitBasePath) return;
82
+ circuitBasePath = path;
83
+ circuitCache.clear();
84
+ artifactBytesCache.clear();
85
+ artifactDownloadCache.clear();
86
+ }
87
+
88
+ /**
89
+ * Get the current circuit base path
90
+ */
91
+ export function getCircuitPath(): string {
92
+ return circuitBasePath;
93
+ }
94
+
95
+ // Lazy-loaded snarkjs module
96
+ let snarkjs: any = null;
97
+
98
+ interface CircuitArtifact {
99
+ wasmPath: string;
100
+ zkeyPath: string;
101
+ }
102
+
103
+ const circuitCache = new Map<CircuitType, CircuitArtifact>();
104
+ let proverInitialized = false;
105
+
106
+ /**
107
+ * Load snarkjs module
108
+ */
109
+ async function ensureSnarkjsLoaded(): Promise<void> {
110
+ if (snarkjs) return;
111
+
112
+ console.log("[Prover] Loading snarkjs module...");
113
+ // Prefer an app-provided instance: browser bundlers resolve snarkjs's
114
+ // `browser` export themselves and hand it over via globalThis, since the
115
+ // string-indirected import below is (deliberately) opaque to them.
116
+ snarkjs = (globalThis as { snarkjs?: unknown }).snarkjs ?? null;
117
+ if (!snarkjs) {
118
+ const snarkjsModule = "snarkjs";
119
+ snarkjs = await import(snarkjsModule).catch(() => null);
120
+ }
121
+
122
+ if (!snarkjs) {
123
+ throw new Error(
124
+ "Groth16 prover requires the snarkjs package. " +
125
+ "Install it with: bun add snarkjs"
126
+ );
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Fully-downloaded circuit artifacts, keyed by URL. Proving reads zkey
132
+ * sections non-sequentially; over HTTP that becomes thousands of small
133
+ * range requests, which dominates proving time. Downloading once into
134
+ * memory (Railgun-style) makes proving IO-free.
135
+ */
136
+ const artifactBytesCache = new Map<string, Uint8Array>();
137
+ const artifactDownloadCache = new Map<string, Promise<Uint8Array>>();
138
+
139
+ type FastFileMem = { type: "mem"; data: Uint8Array };
140
+
141
+ /**
142
+ * Expected SHA-256 of each artifact, keyed by its path under the circuit base — e.g.
143
+ * `joinsplit_1x1/joinsplit_1x1_js/joinsplit_1x1.wasm`. Empty until a caller supplies one.
144
+ */
145
+ let artifactDigests: Record<string, string> | null = null;
146
+
147
+ /**
148
+ * Pin the circuit artifacts this app is willing to prove with.
149
+ *
150
+ * Worth doing even though it costs a hash per download. The `.wasm` is the witness generator:
151
+ * it is handed the spending key, the nullifying key, note randomness, amounts and the full
152
+ * Merkle path in the clear. It arrives over plain `fetch`, where subresource integrity does not
153
+ * apply, and CDNs typically serve it `immutable, max-age=31536000` — so a substituted file keeps
154
+ * working out of browser and edge caches long after the origin is cleaned up. The proofs it
155
+ * produces still verify, so nothing fails and no user notices.
156
+ *
157
+ * Digests must come from the consuming build (they change whenever circuits are rebuilt), which
158
+ * is why the SDK cannot ship them. Once set, verification is enforced and fails closed: an
159
+ * artifact with no recorded digest is refused rather than trusted.
160
+ */
161
+ export function setCircuitArtifactDigests(digests: Record<string, string> | null): void {
162
+ artifactDigests = digests;
163
+ // Anything admitted under the old policy must be re-checked under the new one.
164
+ artifactBytesCache.clear();
165
+ }
166
+
167
+ const toHex = (buf: ArrayBuffer) =>
168
+ Array.from(new Uint8Array(buf), (b) => b.toString(16).padStart(2, "0")).join("");
169
+
170
+ async function verifyArtifactBytes(url: string, bytes: Uint8Array): Promise<void> {
171
+ if (!artifactDigests) return;
172
+ // Key by path-under-base so one manifest works across origins (local dev vs CDN).
173
+ const key = Object.keys(artifactDigests).find((k) => url.endsWith(k));
174
+ if (!key) {
175
+ throw new Error(
176
+ `No integrity digest recorded for circuit artifact ${url} — refusing to prove. ` +
177
+ `Regenerate the artifact manifest so it covers every shape this origin serves.`,
178
+ );
179
+ }
180
+ const actual = toHex(
181
+ await crypto.subtle.digest("SHA-256", bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer),
182
+ );
183
+ if (actual !== artifactDigests[key]) {
184
+ throw new Error(
185
+ `Circuit artifact ${key} failed its integrity check (expected ${artifactDigests[key]}, ` +
186
+ `got ${actual}). Refusing to prove against an artifact this build does not recognise.`,
187
+ );
188
+ }
189
+ }
190
+
191
+ async function fetchArtifactToMemory(url: string): Promise<FastFileMem> {
192
+ const cached = artifactBytesCache.get(url);
193
+ if (cached) return { type: "mem", data: cached };
194
+
195
+ let download = artifactDownloadCache.get(url);
196
+ if (!download) {
197
+ download = (async () => {
198
+ const res = await fetch(url, { cache: "force-cache" });
199
+ if (!res.ok) {
200
+ throw new Error(`Failed to fetch circuit artifact ${url}: HTTP ${res.status}`);
201
+ }
202
+ const bytes = new Uint8Array(await res.arrayBuffer());
203
+ // Before the cache, so a rejected artifact is never reachable from it.
204
+ await verifyArtifactBytes(url, bytes);
205
+ artifactBytesCache.set(url, bytes);
206
+ return bytes;
207
+ })();
208
+ artifactDownloadCache.set(url, download);
209
+ void download.finally(() => artifactDownloadCache.delete(url)).catch(() => {});
210
+ }
211
+
212
+ const bytes = await download;
213
+ return { type: "mem", data: bytes };
214
+ }
215
+
216
+ /**
217
+ * In browsers (or with remote artifact URLs), resolve artifacts to in-memory
218
+ * fastfile objects; in Node, local file paths are already fast.
219
+ */
220
+ async function resolveProveArtifacts(
221
+ artifacts: CircuitArtifact,
222
+ ): Promise<{ wasm: string | FastFileMem; zkey: string | FastFileMem }> {
223
+ const isRemote = artifacts.zkeyPath.startsWith("http://") || artifacts.zkeyPath.startsWith("https://");
224
+ if (!isBrowser && !isRemote) {
225
+ return { wasm: artifacts.wasmPath, zkey: artifacts.zkeyPath };
226
+ }
227
+ const [wasm, zkey] = await Promise.all([
228
+ fetchArtifactToMemory(artifacts.wasmPath),
229
+ fetchArtifactToMemory(artifacts.zkeyPath),
230
+ ]);
231
+ return { wasm, zkey };
232
+ }
233
+
234
+ /**
235
+ * Resolve circuit artifact paths
236
+ */
237
+ function getCircuitArtifactPaths(circuitType: CircuitType): CircuitArtifact {
238
+ if (circuitCache.has(circuitType)) {
239
+ return circuitCache.get(circuitType)!;
240
+ }
241
+
242
+ const name = circuitType; // "joinsplit_NxM"
243
+ const artifact: CircuitArtifact = {
244
+ wasmPath: `${circuitBasePath}/${name}/${name}_js/${name}.wasm`,
245
+ zkeyPath: `${circuitBasePath}/${name}/${name}.zkey`,
246
+ };
247
+
248
+ circuitCache.set(circuitType, artifact);
249
+ return artifact;
250
+ }
251
+
252
+ function getJoinSplitCircuitType(nInputs: number, nOutputs: number): CircuitType {
253
+ if (
254
+ !Number.isInteger(nInputs) ||
255
+ !Number.isInteger(nOutputs) ||
256
+ nInputs < 1 ||
257
+ nOutputs < 1 ||
258
+ nInputs + nOutputs > MAX_SAFE_JOINSPLIT_SIZE
259
+ ) {
260
+ throw new Error(
261
+ `Invalid JoinSplit dimensions: ${nInputs}x${nOutputs} ` +
262
+ `(N+M must be 2..${MAX_SAFE_JOINSPLIT_SIZE})`,
263
+ );
264
+ }
265
+ return `joinsplit_${nInputs}x${nOutputs}`;
266
+ }
267
+
268
+ /**
269
+ * Download the selected JoinSplit artifacts into the browser cache and the
270
+ * prover's in-memory fastfile cache without generating a proof. Calling this
271
+ * while the user reviews a transaction removes CDN latency from the confirm
272
+ * path. Concurrent preloads and proofs share the same downloads.
273
+ */
274
+ export async function preloadJoinSplitCircuit(
275
+ nInputs: number,
276
+ nOutputs: number,
277
+ ): Promise<void> {
278
+ const circuitType = getJoinSplitCircuitType(nInputs, nOutputs);
279
+ await resolveProveArtifacts(getCircuitArtifactPaths(circuitType));
280
+ }
281
+
282
+ type InputMap = Record<string, string | string[] | number[] | string[][] | number[][]>;
283
+
284
+ // Detect bun runtime (snarkjs WASM hangs in bun)
285
+ const isBun = typeof process !== "undefined" && !!(process as any).versions?.bun;
286
+
287
+ /**
288
+ * Generate a Groth16 proof for a circuit with given inputs.
289
+ * Uses Node.js subprocess when running in bun (snarkjs WASM incompatibility).
290
+ */
291
+ async function generateProof(
292
+ circuitType: CircuitType,
293
+ inputs: InputMap
294
+ ): Promise<ProofData> {
295
+ console.log(`[Prover] Generating ${circuitType} Groth16 proof...`);
296
+ const startTime = typeof performance !== "undefined" ? performance.now() : Date.now();
297
+
298
+ const artifacts = getCircuitArtifactPaths(circuitType);
299
+
300
+ let proof: any;
301
+ let publicSignals: string[];
302
+
303
+ if (isBun && isNode) {
304
+ const result = await generateProofViaNodeSubprocess(artifacts, inputs);
305
+ proof = result.proof;
306
+ publicSignals = result.publicSignals;
307
+ } else {
308
+ await ensureSnarkjsLoaded();
309
+ const resolved = await resolveProveArtifacts(artifacts);
310
+ // Browser: ffjavascript's worker pool can deadlock (proofs hang
311
+ // indefinitely); single-thread proving completes in seconds.
312
+ const proverOptions = isBrowser ? { singleThread: true } : undefined;
313
+ const result = await snarkjs.groth16.fullProve(
314
+ inputs,
315
+ resolved.wasm,
316
+ resolved.zkey,
317
+ undefined,
318
+ undefined,
319
+ proverOptions
320
+ );
321
+ proof = result.proof;
322
+ publicSignals = result.publicSignals;
323
+ }
324
+
325
+ const elapsed = (typeof performance !== "undefined" ? performance.now() : Date.now()) - startTime;
326
+ console.log(`[Prover] Groth16 proof generated in ${elapsed.toFixed(0)}ms`);
327
+
328
+ const proofBytes = serializeProof(proof);
329
+ console.log(`[Prover] Proof size: ${proofBytes.length} bytes`);
330
+
331
+ return {
332
+ proof: proofBytes,
333
+ publicInputs: publicSignals,
334
+ };
335
+ }
336
+
337
+ /**
338
+ * Generate proof via Node.js subprocess (for bun compatibility)
339
+ */
340
+ async function generateProofViaNodeSubprocess(
341
+ artifacts: CircuitArtifact,
342
+ inputs: InputMap
343
+ ): Promise<{ proof: any; publicSignals: string[] }> {
344
+ // Build a CommonJS-aware `require` that works in both CJS and ESM execution
345
+ // contexts. In ESM, `require` isn't a global; `module.createRequire(url)`
346
+ // creates one bound to a given module URL.
347
+ // `import.meta.url` is only valid in ESM modules; in CJS bundles
348
+ // (`type: "commonjs"`) we fall back to the inherited `require`. Both
349
+ // bundlers (esbuild / tsc) preserve `import.meta.url` correctly.
350
+ let _require: (m: string) => any;
351
+ if (typeof (globalThis as any).require === "function") {
352
+ _require = (globalThis as any).require;
353
+ } else {
354
+ const nodeModuleSpecifier = "node:" + "module";
355
+ const { createRequire } = await import(nodeModuleSpecifier);
356
+ _require = createRequire(import.meta.url);
357
+ }
358
+ const { execFileSync } = _require("child_process");
359
+ const fs = _require("fs");
360
+ const path = _require("path");
361
+ const os = _require("os");
362
+
363
+ const wasmPath = path.resolve(artifacts.wasmPath);
364
+ const zkeyPath = path.resolve(artifacts.zkeyPath);
365
+
366
+ // Private 0700 tmp dir so the prover input (nullifyingKey, values, leaf indices)
367
+ // is not world-readable in the shared circuit-artifact dir.
368
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "utxopia-prover-"));
369
+ const tmpInput = path.join(tmpDir, "input.json");
370
+ const tmpProof = path.join(tmpDir, "proof.json");
371
+ const tmpPublic = path.join(tmpDir, "public.json");
372
+
373
+ fs.writeFileSync(tmpInput, JSON.stringify(inputs), { mode: 0o600 });
374
+
375
+ try {
376
+ // Use execFileSync to avoid shell injection via file paths
377
+ // Take the last 5 argv entries — Node 24's --eval TypeScript transform
378
+ // can prepend extra paths, so positional destructuring [,, ...] breaks.
379
+ const script = `
380
+ const snarkjs = require('snarkjs');
381
+ const fs = require('fs');
382
+ const [inputPath, wasmP, zkeyP, proofPath, publicPath] = process.argv.slice(-5);
383
+ (async () => {
384
+ const input = JSON.parse(fs.readFileSync(inputPath, 'utf8'));
385
+ const { proof, publicSignals } = await snarkjs.groth16.fullProve(input, wasmP, zkeyP);
386
+ fs.writeFileSync(proofPath, JSON.stringify(proof));
387
+ fs.writeFileSync(publicPath, JSON.stringify(publicSignals));
388
+ process.exit(0);
389
+ })().catch(e => { console.error(e); process.exit(1); });
390
+ `;
391
+ execFileSync("node", ["-e", script, tmpInput, wasmPath, zkeyPath, tmpProof, tmpPublic], {
392
+ timeout: 120000,
393
+ });
394
+
395
+ const proof = JSON.parse(fs.readFileSync(tmpProof, "utf8"));
396
+ const publicSignals: string[] = JSON.parse(fs.readFileSync(tmpPublic, "utf8"));
397
+
398
+ return { proof, publicSignals };
399
+ } finally {
400
+ // Best-effort overwrite-then-remove of the secret-bearing input, then the dir.
401
+ try { fs.writeFileSync(tmpInput, "0".repeat(64), { mode: 0o600 }); } catch {}
402
+ try { fs.unlinkSync(tmpInput); } catch {}
403
+ try { fs.unlinkSync(tmpProof); } catch {}
404
+ try { fs.unlinkSync(tmpPublic); } catch {}
405
+ try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
406
+ }
407
+ }
408
+
409
+ /**
410
+ * Serialize snarkjs Groth16 proof to 256 bytes (2 G1 + 1 G2 on BN254)
411
+ */
412
+ function serializeProof(proof: any): Uint8Array {
413
+ const bytes = new Uint8Array(256);
414
+
415
+ const piA = proof.pi_a;
416
+ const piB = proof.pi_b;
417
+ const piC = proof.pi_c;
418
+
419
+ // G1 point A (64 bytes)
420
+ writeBigIntBE(bytes, 0, BigInt(piA[0]), 32);
421
+ writeBigIntBE(bytes, 32, BigInt(piA[1]), 32);
422
+
423
+ // G2 point B (128 bytes): [x_imag, x_real, y_imag, y_real]
424
+ writeBigIntBE(bytes, 64, BigInt(piB[0][1]), 32);
425
+ writeBigIntBE(bytes, 96, BigInt(piB[0][0]), 32);
426
+ writeBigIntBE(bytes, 128, BigInt(piB[1][1]), 32);
427
+ writeBigIntBE(bytes, 160, BigInt(piB[1][0]), 32);
428
+
429
+ // G1 point C (64 bytes)
430
+ writeBigIntBE(bytes, 192, BigInt(piC[0]), 32);
431
+ writeBigIntBE(bytes, 224, BigInt(piC[1]), 32);
432
+
433
+ return bytes;
434
+ }
435
+
436
+ function writeBigIntBE(buf: Uint8Array, offset: number, value: bigint, length: number): void {
437
+ for (let i = length - 1; i >= 0; i--) {
438
+ buf[offset + i] = Number(value & 0xFFn);
439
+ value >>= 8n;
440
+ }
441
+ }
442
+
443
+ // ==========================================================================
444
+ // Public API
445
+ // ==========================================================================
446
+
447
+ /**
448
+ * Initialize the prover (preloads snarkjs module)
449
+ */
450
+ export async function initProver(): Promise<void> {
451
+ await ensureSnarkjsLoaded();
452
+ proverInitialized = true;
453
+ console.log("[Prover] Groth16 prover initialized and ready");
454
+ }
455
+
456
+ /**
457
+ * Generate a Groth16 proof for any circuit by name. Useful for non-JoinSplit
458
+ * circuits (ownership, range_sum) that follow the same artifact layout
459
+ * `<circuitBasePath>/<name>/<name>_js/<name>.wasm` + `<circuitBasePath>/<name>/<name>.zkey`.
460
+ *
461
+ * Returns `{ proof: 256 bytes, publicInputs }` matching `ProofData`.
462
+ */
463
+ export async function generateGenericGroth16Proof(
464
+ circuitName: string,
465
+ inputs: Record<string, string | string[] | number[] | string[][] | number[][]>,
466
+ ): Promise<ProofData> {
467
+ // Reuse the same artifact lookup pattern as JoinSplit variants.
468
+ const artifacts: CircuitArtifact = {
469
+ wasmPath: `${circuitBasePath}/${circuitName}/${circuitName}_js/${circuitName}.wasm`,
470
+ zkeyPath: `${circuitBasePath}/${circuitName}/${circuitName}.zkey`,
471
+ };
472
+
473
+ let proof: any;
474
+ let publicSignals: string[];
475
+
476
+ if (isBun && isNode) {
477
+ const result = await generateProofViaNodeSubprocess(artifacts, inputs);
478
+ proof = result.proof;
479
+ publicSignals = result.publicSignals;
480
+ } else {
481
+ await ensureSnarkjsLoaded();
482
+ const resolved = await resolveProveArtifacts(artifacts);
483
+ const proverOptions = isBrowser ? { singleThread: true } : undefined;
484
+ const result = await snarkjs.groth16.fullProve(
485
+ inputs,
486
+ resolved.wasm,
487
+ resolved.zkey,
488
+ undefined,
489
+ undefined,
490
+ proverOptions,
491
+ );
492
+ proof = result.proof;
493
+ publicSignals = result.publicSignals;
494
+ }
495
+
496
+ return {
497
+ proof: serializeProof(proof),
498
+ publicInputs: publicSignals,
499
+ };
500
+ }
501
+
502
+ /**
503
+ * Check if prover is available in current environment
504
+ */
505
+ export async function isProverAvailable(): Promise<boolean> {
506
+ try {
507
+ await ensureSnarkjsLoaded();
508
+ return true;
509
+ } catch {
510
+ return false;
511
+ }
512
+ }
513
+
514
+ /**
515
+ * Verify a Groth16 proof against a verifying key + public signals.
516
+ *
517
+ * Accepts the snarkjs-native shapes:
518
+ * - `vkey`: the JSON emitted by `snarkjs zkey export verificationkey`
519
+ * - `publicSignals`: array of decimal strings
520
+ * - `proof`: `{ pi_a, pi_b, pi_c, protocol, curve }` as produced by `snarkjs.groth16.fullProve`
521
+ *
522
+ * Lazy-loads snarkjs so calling code doesn't pay the bundle cost unless it
523
+ * verifies. Returns true on a valid proof, false on an invalid one; throws
524
+ * if snarkjs is missing or the inputs are structurally malformed.
525
+ */
526
+ export async function verifyGroth16Proof(
527
+ vkey: unknown,
528
+ publicSignals: ReadonlyArray<string>,
529
+ proof: unknown,
530
+ ): Promise<boolean> {
531
+ await ensureSnarkjsLoaded();
532
+ return snarkjs.groth16.verify(vkey, publicSignals, proof);
533
+ }
534
+
535
+ // ==========================================================================
536
+ // JoinSplit Proof Generation (Railgun-aligned)
537
+ // ==========================================================================
538
+
539
+ /**
540
+ * JoinSplit proof inputs
541
+ */
542
+ export interface JoinSplitProofInputs {
543
+ nInputs: number;
544
+ nOutputs: number;
545
+ merkleRoot: bigint;
546
+ boundParamsHash: bigint;
547
+ token: bigint;
548
+ publicKey: [bigint, bigint]; // BJJ (x, y)
549
+ signature: [bigint, bigint, bigint]; // EdDSA-Poseidon (R8x, R8y, S)
550
+ nullifyingKey: bigint;
551
+ inputs: Array<{
552
+ random: bigint;
553
+ value: bigint;
554
+ leafIndex: bigint;
555
+ merkleProof: { siblings: bigint[]; indices: number[] };
556
+ }>;
557
+ outputs: Array<{ npk: bigint; value: bigint }>;
558
+ }
559
+
560
+ /**
561
+ * Generate a JoinSplit proof
562
+ *
563
+ * Unified prover for all JoinSplit variants.
564
+ * Selects the joinsplit_NxM circuit based on nInputs/nOutputs.
565
+ */
566
+ export async function generateJoinSplitProof(inputs: JoinSplitProofInputs): Promise<ProofData> {
567
+ const { nInputs, nOutputs } = inputs;
568
+
569
+ const variantName = getJoinSplitCircuitType(nInputs, nOutputs);
570
+
571
+ // Validate array lengths match declared dimensions
572
+ if (inputs.inputs.length !== nInputs) {
573
+ throw new Error(`Expected ${nInputs} inputs, got ${inputs.inputs.length}`);
574
+ }
575
+ if (inputs.outputs.length !== nOutputs) {
576
+ throw new Error(`Expected ${nOutputs} outputs, got ${inputs.outputs.length}`);
577
+ }
578
+
579
+ validateFieldInputs({
580
+ merkleRoot: inputs.merkleRoot,
581
+ boundParamsHash: inputs.boundParamsHash,
582
+ token: inputs.token,
583
+ publicKeyX: inputs.publicKey[0],
584
+ publicKeyY: inputs.publicKey[1],
585
+ nullifyingKey: inputs.nullifyingKey,
586
+ });
587
+
588
+ // Validate input amounts and Merkle proof depths
589
+ let totalIn = 0n;
590
+ for (let i = 0; i < inputs.inputs.length; i++) {
591
+ const inp = inputs.inputs[i];
592
+ validateAmount(inp.value, `input[${i}].value`);
593
+ validateFieldInputs({ [`input[${i}].random`]: inp.random });
594
+ if (inp.merkleProof.siblings.length !== TREE_DEPTH) {
595
+ throw new Error(
596
+ `input[${i}].merkleProof: expected ${TREE_DEPTH} siblings, got ${inp.merkleProof.siblings.length}`
597
+ );
598
+ }
599
+ if (inp.merkleProof.indices.length !== TREE_DEPTH) {
600
+ throw new Error(
601
+ `input[${i}].merkleProof: expected ${TREE_DEPTH} indices, got ${inp.merkleProof.indices.length}`
602
+ );
603
+ }
604
+ totalIn += inp.value;
605
+ }
606
+
607
+ // Validate output amounts
608
+ let totalOut = 0n;
609
+ for (let i = 0; i < inputs.outputs.length; i++) {
610
+ const out = inputs.outputs[i];
611
+ validateAmount(out.value, `output[${i}].value`);
612
+ validateFieldInputs({ [`output[${i}].npk`]: out.npk });
613
+ totalOut += out.value;
614
+ }
615
+
616
+ // Validate conservation of value (inputs must equal outputs)
617
+ if (totalIn !== totalOut) {
618
+ throw new Error(
619
+ `Value mismatch: inputs sum to ${totalIn} sats but outputs sum to ${totalOut} sats`
620
+ );
621
+ }
622
+
623
+ // Compute nullifiers
624
+ const nullifiers: bigint[] = [];
625
+ for (const inp of inputs.inputs) {
626
+ const nullifier = computeJoinSplitNullifierSync(inputs.nullifyingKey, inp.leafIndex);
627
+ nullifiers.push(nullifier);
628
+ }
629
+
630
+ // Compute output commitments
631
+ const commitmentsOut: bigint[] = [];
632
+ for (const out of inputs.outputs) {
633
+ const commitment = computeJoinSplitCommitmentSync(out.npk, inputs.token, out.value);
634
+ commitmentsOut.push(commitment);
635
+ }
636
+
637
+ // Compute message hash: Poseidon(merkleRoot, boundParamsHash, nullifiers..., commitmentsOut...)
638
+ const hashInputs: bigint[] = [
639
+ inputs.merkleRoot,
640
+ inputs.boundParamsHash,
641
+ ...nullifiers,
642
+ ...commitmentsOut,
643
+ ];
644
+ const _msgHash = poseidonHashSync(hashInputs);
645
+
646
+ // Build circuit inputs
647
+ const circuitInputs: InputMap = {
648
+ merkleRoot: inputs.merkleRoot.toString(),
649
+ boundParamsHash: inputs.boundParamsHash.toString(),
650
+ nullifiers: nullifiers.map(n => n.toString()),
651
+ commitmentsOut: commitmentsOut.map(c => c.toString()),
652
+ token: inputs.token.toString(),
653
+ publicKey: inputs.publicKey.map(p => p.toString()),
654
+ signature: inputs.signature.map(s => s.toString()),
655
+ nullifyingKey: inputs.nullifyingKey.toString(),
656
+ randomIn: inputs.inputs.map(i => i.random.toString()),
657
+ valueIn: inputs.inputs.map(i => i.value.toString()),
658
+ leavesIndices: inputs.inputs.map(i => i.leafIndex.toString()),
659
+ npkOut: inputs.outputs.map(o => o.npk.toString()),
660
+ valueOut: inputs.outputs.map(o => o.value.toString()),
661
+ };
662
+
663
+ // Circuit derives path-direction bits from leavesIndices, so only siblings are passed.
664
+ const pathElements: string[][] = [];
665
+ for (const inp of inputs.inputs) {
666
+ pathElements.push(inp.merkleProof.siblings.map(s => s.toString()));
667
+ }
668
+ circuitInputs.pathElements = pathElements as any;
669
+
670
+ return generateProof(variantName, circuitInputs);
671
+ }
672
+
673
+ // ==========================================================================
674
+ // Circuit Availability & Verification
675
+ // ==========================================================================
676
+
677
+ /**
678
+ * Check if circuit artifacts exist for a given circuit type
679
+ */
680
+ export async function circuitExists(circuitType: CircuitType): Promise<boolean> {
681
+ try {
682
+ const artifacts = getCircuitArtifactPaths(circuitType);
683
+ const isUrl = artifacts.wasmPath.startsWith("http://") || artifacts.wasmPath.startsWith("https://");
684
+
685
+ if (isBrowser || isUrl) {
686
+ // Browser or remote URL (S3, CDN) — use fetch
687
+ const [wasmRes, zkeyRes] = await Promise.all([
688
+ fetch(artifacts.wasmPath, { method: "HEAD" }),
689
+ fetch(artifacts.zkeyPath, { method: "HEAD" }),
690
+ ]);
691
+ return wasmRes.ok && zkeyRes.ok;
692
+ }
693
+ // Node/Bun with local paths
694
+ const _require = new Function("m", "return require(m)") as (m: string) => any;
695
+ const { existsSync } = _require("fs");
696
+ return existsSync(artifacts.wasmPath) && existsSync(artifacts.zkeyPath);
697
+ } catch {
698
+ return false;
699
+ }
700
+ }
701
+
702
+ /**
703
+ * Convert proof to raw bytes for on-chain submission
704
+ */
705
+ export function proofToBytes(proof: ProofData): Uint8Array {
706
+ return proof.proof;
707
+ }
708
+
709
+ /**
710
+ * Cleanup all cached resources
711
+ */
712
+ export async function cleanup(): Promise<void> {
713
+ circuitCache.clear();
714
+ console.log("[Prover] Cleaned up all cached resources");
715
+ }
716
+
717
+ // ==========================================================================
718
+ // Solana Instruction Building (for Groth16 on-chain verification)
719
+ // ==========================================================================
720
+
721
+ /**
722
+ * Groth16 verifier program ID (from current config)
723
+ */
724
+ export function getGroth16VerifierProgramId(): Address {
725
+ const config = getConfig();
726
+ return config.groth16VerifierProgramId;
727
+ }
728
+
729
+ /**
730
+ * Build instruction data for Groth16 verification
731
+ */
732
+ export function buildVerifyInstructionData(
733
+ proof: Uint8Array,
734
+ publicSignals: string[],
735
+ vkHash: string
736
+ ): Uint8Array {
737
+ const piBytes = publicSignals.flatMap((pi) => {
738
+ const bytes = new Array(32).fill(0);
739
+ const bigint = BigInt(pi);
740
+ for (let i = 31; i >= 0; i--) {
741
+ bytes[i] = Number((bigint >> BigInt((31 - i) * 8)) & 0xFFn);
742
+ }
743
+ return bytes;
744
+ });
745
+
746
+ const cleanHex = vkHash.startsWith("0x") ? vkHash.slice(2) : vkHash;
747
+ const vkHashBytes = new Uint8Array(cleanHex.length / 2);
748
+ for (let i = 0; i < vkHashBytes.length; i++) {
749
+ vkHashBytes[i] = parseInt(cleanHex.substr(i * 2, 2), 16);
750
+ }
751
+
752
+ const totalSize = proof.length + 4 + piBytes.length + 32;
753
+ const data = new Uint8Array(totalSize);
754
+ let offset = 0;
755
+
756
+ data.set(proof, offset);
757
+ offset += proof.length;
758
+
759
+ const piCount = publicSignals.length;
760
+ data[offset++] = piCount & 0xff;
761
+ data[offset++] = (piCount >> 8) & 0xff;
762
+ data[offset++] = (piCount >> 16) & 0xff;
763
+ data[offset++] = (piCount >> 24) & 0xff;
764
+
765
+ data.set(new Uint8Array(piBytes), offset);
766
+ offset += piBytes.length;
767
+
768
+ data.set(vkHashBytes, offset);
769
+
770
+ return data;
771
+ }