@utxopia/sdk 0.1.0-alpha.5 → 0.1.0-alpha.6

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/dist/client.js CHANGED
@@ -183,8 +183,6 @@ export class UTXOpiaClient {
183
183
  const cached = this._tokenIdCache.get(mintAddress);
184
184
  if (cached !== undefined)
185
185
  return cached;
186
- // Requires PublicKey — import dynamically to avoid hard dep
187
- const mintBytes = hexToBytes(mintAddress.padStart(64, "0"));
188
186
  // If it's a base58 address, convert via PublicKey
189
187
  let bytes;
190
188
  try {
@@ -200,7 +198,9 @@ export class UTXOpiaClient {
200
198
  }
201
199
  }
202
200
  catch {
203
- bytes = mintBytes;
201
+ // Lazy on purpose: a base58 mint padded to 64 is not hex, so computing
202
+ // this eagerly threw before the branch above ever ran.
203
+ bytes = hexToBytes(mintAddress.padStart(64, "0"));
204
204
  }
205
205
  const tokenId = computeTokenId(bytes);
206
206
  this._tokenIdCache.set(mintAddress, tokenId);
package/dist/config.js CHANGED
@@ -175,8 +175,11 @@ export const MAINNET_CONFIG = {
175
175
  // Bitcoin Network
176
176
  bitcoinNetwork: "mainnet",
177
177
  esploraUrl: "https://mempool.space/api",
178
- // Circuit CDN
179
- circuitCdnUrl: "https://circuit.utxopia.com",
178
+ // Circuit CDN. Must carry the full versioned path: `resolveCircuitPath` turns a bare host
179
+ // into `<host>/circuits/groth16`, which is a DIFFERENT, stale build that still serves 200s
180
+ // for every shape with a different delta. Proofs built from it verify locally and are
181
+ // rejected on chain as "proof invalid".
182
+ circuitCdnUrl: "https://circuit.utxopia.com/circuits/v2/groth16",
180
183
  // Groth16 Verifier (placeholder)
181
184
  groth16VerifierProgramId: address("11111111111111111111111111111111"),
182
185
  // VK Hashes (placeholder - update when deployed)
package/dist/index.d.ts CHANGED
@@ -54,7 +54,7 @@ export { createFetchConnectionAdapter, createConnectionAdapterFromWeb3, createCo
54
54
  export { resolveSnsName, resolveStealthName, parseSnsStealthData, isSnsStealthAddress, isAuditorDisclosable, SnsComplianceFlags, SNS_COMPLIANCE_AUDITOR_OFFSET, SNS_COMPLIANCE_AUDITOR_BYTES, deriveParentDomainKey, SNS_STEALTH_DATA_SIZE, type SnsStealthAddress, } from "./sns-resolver";
55
55
  export { COMMITMENT_TREE_DISCRIMINATOR, parseCommitmentTreeData, isValidRoot, fetchCommitmentTree, getCommitmentIndex, saveCommitmentIndex, CommitmentTreeIndex, buildCommitmentTreeFromChain, fetchLeafIndexForCommitment, fetchMerkleProofForCommitment, getMerkleProofFromTree, type CommitmentTreeState, type RpcClient, type OnChainMerkleProof, } from "./commitment-tree";
56
56
  export { INSTRUCTION_DISCRIMINATORS, buildShieldInstructionData, buildShieldInstruction, type ShieldInstructionOptions, buildApproveRedemptionSigningInstructionData, buildApproveRedemptionSigningInstruction, buildCancelRedemptionInstructionData, buildCancelRedemptionInstruction, type CancelRedemptionInstructionOptions, bigintTo32Bytes, bytes32ToBigint, buildTransactInstructionData, buildTransactInstruction, buildRedeemInstructionData, buildUnshieldInstructionData, buildUnshieldInstruction, buildProposePoolUpdateInstructionData, buildProposePoolUpdateInstruction, buildExecutePoolUpdateInstructionData, buildExecutePoolUpdateInstruction, buildCancelPoolUpdateInstructionData, buildCancelPoolUpdateInstruction, buildRotateTreeInstructionData, buildRotateTreeInstruction, type RotateTreeOptions, buildMagicBlockDelegateInstructionData, buildMagicBlockDelegateInstruction, buildMagicBlockCommitInstructionData, buildMagicBlockCommitInstruction, buildMagicBlockPerPermissionInstructionData, buildMagicBlockPerPermissionInstruction, buildPolicyRequestHash, buildPolicyIntentParts, buildRegisterExitDestinationInstruction, buildRegisterExitDestinationInstructionData, MAX_POLICY_INTENT_PARTS, buildInitializePolicyApprovalInstructionData, buildInitializePolicyApprovalInstruction, buildPolicyApprovalDecisionInstruction, buildPolicyApprovalCommitInstruction, buildCompleteDepositPermissionedInstructionData, buildCompleteDepositPermissionedInstruction, buildShieldPermissionedInstructionData, buildShieldPermissionedInstruction, buildRotateAuditorInstructionData, buildRotateAuditorInstruction, type MagicBlockDelegateTarget, type MagicBlockDelegateInstructionOptions, type MagicBlockCommitInstructionOptions, type MagicBlockPerPermissionOperation, type MagicBlockPerPermissionMember, type MagicBlockPerPermissionInstructionOptions, type PolicyApprovalDecision, type InitializePolicyApprovalOptions, type CompleteDepositPermissionedOptions, type ShieldPermissionedInstructionOptions, type RotateAuditorOptions, buildVerifyTransactionInstructionData, buildCompleteDepositInstructionData, buildVerifyDepositInstructionData, buildVerifyDepositPermissionedInstructionData, buildSetPoolConfigInstructionData, parsePoolConfig, type ParsedPoolConfig, POOL_CONFIG_DISCRIMINATOR, POOL_CONFIG_LEN, POOL_SCRIPT_MAX_LEN, type Instruction, type ApproveRedemptionSigningInstructionOptions, type TransactInstructionOptions, type UnshieldInstructionOptions, type ProposePoolUpdateOptions, type ExecutePoolUpdateOptions, type CancelPoolUpdateOptions, } from "./instructions";
57
- export { VK_REGISTRY_DISCRIMINATOR, VK_REGISTRY_LEN, MAX_IC_POINTS, MAX_SAFE_JOINSPLIT_SIZE, INIT_VK_REGISTRY_DISCRIMINATOR, UPDATE_VK_REGISTRY_DISCRIMINATOR, joinSplitNumPublicInputs, computeVkHash, vkeyJsonToVkMaterial, buildVkRegistryData, parseVkRegistry, assertVkRegistryForShape, isVkRegistryReady, type JoinSplitVkMaterial, type SnarkjsVkeyJson, type ParsedVkRegistry, } from "./vk-registry";
57
+ export { VK_REGISTRY_DISCRIMINATOR, VK_REGISTRY_LEN, MAX_IC_POINTS, MAX_SAFE_JOINSPLIT_SIZE, INIT_VK_REGISTRY_DISCRIMINATOR, UPDATE_VK_REGISTRY_DISCRIMINATOR, joinSplitNumPublicInputs, computeVkHash, vkeyJsonToVkMaterial, buildVkRegistryData, parseVkRegistry, assertVkRegistryForShape, assertVkeyMatchesRegistry, isVkRegistryReady, type JoinSplitVkMaterial, type SnarkjsVkeyJson, type ParsedVkRegistry, } from "./vk-registry";
58
58
  export { fetchExplorerDeposits, fetchExplorerTransfers, fetchExplorerRedemptions, parseNullifierRecord, parseRedemptionRequest, NULLIFIER_RECORD_SIZE, REDEMPTION_REQUEST_SIZE, NULLIFIER_RECORD_DISCRIMINATOR, REDEMPTION_REQUEST_DISCRIMINATOR, OPERATION_TYPE_LABELS, type ExplorerDeposit, type ExplorerTransferEvent, type ExplorerRedemption, type IndexerLeaf, } from "./explorer";
59
59
  export { parseProgramEvents, parseNullifierSpentEvent, parseStealthAnnouncementEvent, parseSenderMemoEvent, parseBtcOriginAttestationEvent, parseAuditorCiphertextEvent, EVENT_NULLIFIER_SPENT, EVENT_STEALTH_ANNOUNCEMENT, EVENT_NULLIFIERS_BATCH, EVENT_ANNOUNCEMENTS_BATCH, EVENT_SENDER_MEMO, EVENT_BTC_ORIGIN_ATTESTATION, EVENT_AUDITOR_CIPHERTEXT, type NullifierSpentEvent, type StealthAnnouncementEvent, type SenderMemoEvent, type BtcOriginAttestationEvent, type AuditorCiphertextEvent, type ProgramEvent, } from "./events";
60
60
  export { AnnouncementClient, type AnnouncementClientConfig, type AnnouncementListener, } from "./announcement-client";
package/dist/index.js CHANGED
@@ -187,7 +187,7 @@ buildSetPoolConfigInstructionData, parsePoolConfig, POOL_CONFIG_DISCRIMINATOR, P
187
187
  // ==========================================================================
188
188
  // VK Registry (JoinSplit Groth16 on-chain verification keys)
189
189
  // ==========================================================================
190
- export { VK_REGISTRY_DISCRIMINATOR, VK_REGISTRY_LEN, MAX_IC_POINTS, MAX_SAFE_JOINSPLIT_SIZE, INIT_VK_REGISTRY_DISCRIMINATOR, UPDATE_VK_REGISTRY_DISCRIMINATOR, joinSplitNumPublicInputs, computeVkHash, vkeyJsonToVkMaterial, buildVkRegistryData, parseVkRegistry, assertVkRegistryForShape, isVkRegistryReady, } from "./vk-registry";
190
+ export { VK_REGISTRY_DISCRIMINATOR, VK_REGISTRY_LEN, MAX_IC_POINTS, MAX_SAFE_JOINSPLIT_SIZE, INIT_VK_REGISTRY_DISCRIMINATOR, UPDATE_VK_REGISTRY_DISCRIMINATOR, joinSplitNumPublicInputs, computeVkHash, vkeyJsonToVkMaterial, buildVkRegistryData, parseVkRegistry, assertVkRegistryForShape, assertVkeyMatchesRegistry, isVkRegistryReady, } from "./vk-registry";
191
191
  // ==========================================================================
192
192
  // ChadBuffer Relay
193
193
  // ==========================================================================
@@ -19,6 +19,14 @@ export interface ProofData {
19
19
  verificationKey?: Uint8Array;
20
20
  }
21
21
  export type CircuitType = `joinsplit_${number}x${number}`;
22
+ /**
23
+ * Fetch a circuit's `*.vkey.json` from the same base path its zkey comes from.
24
+ *
25
+ * Deliberately reads `circuitBasePath` rather than taking a URL: the point of the check this
26
+ * feeds (`assertVkeyMatchesRegistry`) is that the vkey describes the artifacts actually being
27
+ * proved with, and a caller-supplied URL could describe a different build entirely.
28
+ */
29
+ export declare function fetchCircuitVkey(circuitType: CircuitType): Promise<unknown>;
22
30
  /**
23
31
  * Set the base path for circuit artifacts
24
32
  */
@@ -41,6 +41,26 @@ const isBrowser = typeof window !== "undefined";
41
41
  const isNode = typeof process !== "undefined" && process.versions?.node;
42
42
  // Configurable circuit paths
43
43
  let circuitBasePath = isBrowser ? "/circuits/groth16" : "./circuits";
44
+ /**
45
+ * Fetch a circuit's `*.vkey.json` from the same base path its zkey comes from.
46
+ *
47
+ * Deliberately reads `circuitBasePath` rather than taking a URL: the point of the check this
48
+ * feeds (`assertVkeyMatchesRegistry`) is that the vkey describes the artifacts actually being
49
+ * proved with, and a caller-supplied URL could describe a different build entirely.
50
+ */
51
+ export async function fetchCircuitVkey(circuitType) {
52
+ const cached = vkeyCache.get(circuitType);
53
+ if (cached)
54
+ return cached;
55
+ const url = `${circuitBasePath}/${circuitType}/${circuitType}.vkey.json`;
56
+ const res = await fetch(url);
57
+ if (!res.ok) {
58
+ throw new Error(`Could not fetch circuit vkey ${url}: ${res.status} ${res.statusText}`);
59
+ }
60
+ const vkey = await res.json();
61
+ vkeyCache.set(circuitType, vkey);
62
+ return vkey;
63
+ }
44
64
  /**
45
65
  * Set the base path for circuit artifacts
46
66
  */
@@ -49,12 +69,14 @@ export function setCircuitPath(path) {
49
69
  return;
50
70
  circuitBasePath = path;
51
71
  circuitCache.clear();
72
+ vkeyCache.clear();
52
73
  artifactBytesCache.clear();
53
74
  artifactDownloadCache.clear();
54
75
  }
55
76
  // Lazy-loaded snarkjs module
56
77
  let snarkjs = null;
57
78
  const circuitCache = new Map();
79
+ const vkeyCache = new Map();
58
80
  let proverInitialized = false;
59
81
  /**
60
82
  * Load snarkjs module
@@ -98,3 +98,24 @@ export declare function parseVkRegistry(data: Uint8Array): ParsedVkRegistry;
98
98
  export declare function assertVkRegistryForShape(data: Uint8Array | null | undefined, nInputs: number, nOutputs: number): ParsedVkRegistry;
99
99
  /** True if `data` is an initialized `VkRegistry` for the given shape. */
100
100
  export declare function isVkRegistryReady(data: Uint8Array | null | undefined, nInputs: number, nOutputs: number): boolean;
101
+ /**
102
+ * Assert the circuit artifacts this client loaded correspond to the verifying key the chain
103
+ * will check against.
104
+ *
105
+ * `assertVkRegistryForShape` proves the registry exists and names the right shape;
106
+ * `setCircuitArtifactDigests` proves the artifacts are the bytes this build expects. Neither
107
+ * ties the two together, and that gap is a real one: a zkey from a different phase-2 setup
108
+ * proves fine, downloads fine, passes every integrity check, and is then rejected on chain as
109
+ * "proof invalid" — with nothing pointing at the version mismatch.
110
+ *
111
+ * It is not hypothetical. `https://circuit.utxopia.com/circuits/groth16` and
112
+ * `.../circuits/v2/groth16` both serve 200s for every shape, with different `delta` — same
113
+ * ceremony, different per-circuit setup. Both therefore satisfy the program's own vk_hash
114
+ * recomputation, which binds delta+IC to the compiled-in alpha/beta/gamma and so catches a
115
+ * foreign ceremony but NOT a stale phase-2 from the right one. This is the check that does.
116
+ *
117
+ * @param vkey The `*.vkey.json` fetched from the SAME base path as the zkey. Fetching
118
+ * it from anywhere else makes this assertion meaningless.
119
+ * @param registryData Raw `VkRegistry` account data for the shape.
120
+ */
121
+ export declare function assertVkeyMatchesRegistry(vkey: SnarkjsVkeyJson, registryData: Uint8Array | null | undefined, nInputs: number, nOutputs: number): void;
@@ -210,3 +210,42 @@ export function isVkRegistryReady(data, nInputs, nOutputs) {
210
210
  return false;
211
211
  }
212
212
  }
213
+ /**
214
+ * Assert the circuit artifacts this client loaded correspond to the verifying key the chain
215
+ * will check against.
216
+ *
217
+ * `assertVkRegistryForShape` proves the registry exists and names the right shape;
218
+ * `setCircuitArtifactDigests` proves the artifacts are the bytes this build expects. Neither
219
+ * ties the two together, and that gap is a real one: a zkey from a different phase-2 setup
220
+ * proves fine, downloads fine, passes every integrity check, and is then rejected on chain as
221
+ * "proof invalid" — with nothing pointing at the version mismatch.
222
+ *
223
+ * It is not hypothetical. `https://circuit.utxopia.com/circuits/groth16` and
224
+ * `.../circuits/v2/groth16` both serve 200s for every shape, with different `delta` — same
225
+ * ceremony, different per-circuit setup. Both therefore satisfy the program's own vk_hash
226
+ * recomputation, which binds delta+IC to the compiled-in alpha/beta/gamma and so catches a
227
+ * foreign ceremony but NOT a stale phase-2 from the right one. This is the check that does.
228
+ *
229
+ * @param vkey The `*.vkey.json` fetched from the SAME base path as the zkey. Fetching
230
+ * it from anywhere else makes this assertion meaningless.
231
+ * @param registryData Raw `VkRegistry` account data for the shape.
232
+ */
233
+ export function assertVkeyMatchesRegistry(vkey, registryData, nInputs, nOutputs) {
234
+ const registry = assertVkRegistryForShape(registryData, nInputs, nOutputs);
235
+ const expectedIc = joinSplitNumPublicInputs(nInputs, nOutputs) + 1;
236
+ if (vkey.IC.length !== expectedIc) {
237
+ throw new Error(`Circuit vkey for JoinSplit ${nInputs}x${nOutputs} has ${vkey.IC.length} IC points, ` +
238
+ `expected ${expectedIc} — the artifacts are for a different shape.`);
239
+ }
240
+ const local = computeVkHash(vkey);
241
+ if (!local.every((b, i) => b === registry.vkHash[i])) {
242
+ throw new Error(`Circuit artifacts for JoinSplit ${nInputs}x${nOutputs} do not match the on-chain ` +
243
+ `verifying key (artifact vkHash ${bytesToHex(local)}, registry ${bytesToHex(registry.vkHash)}). ` +
244
+ `A proof built from these would be rejected as invalid. Usually the circuit base URL ` +
245
+ `points at a stale build — check it resolves to the same version the registry was ` +
246
+ `registered from.`);
247
+ }
248
+ }
249
+ function bytesToHex(b) {
250
+ return Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
251
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@utxopia/sdk",
3
- "version": "0.1.0-alpha.5",
3
+ "version": "0.1.0-alpha.6",
4
4
  "description": "UTXOpia SDK - Private Bitcoin across Solana and Sui",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/src/client.ts CHANGED
@@ -269,8 +269,6 @@ export class UTXOpiaClient {
269
269
  const cached = this._tokenIdCache.get(mintAddress);
270
270
  if (cached !== undefined) return cached;
271
271
 
272
- // Requires PublicKey — import dynamically to avoid hard dep
273
- const mintBytes = hexToBytes(mintAddress.padStart(64, "0"));
274
272
  // If it's a base58 address, convert via PublicKey
275
273
  let bytes: Uint8Array;
276
274
  try {
@@ -284,7 +282,9 @@ export class UTXOpiaClient {
284
282
  bytes = new PublicKey(mintAddress).toBytes();
285
283
  }
286
284
  } catch {
287
- bytes = mintBytes;
285
+ // Lazy on purpose: a base58 mint padded to 64 is not hex, so computing
286
+ // this eagerly threw before the branch above ever ran.
287
+ bytes = hexToBytes(mintAddress.padStart(64, "0"));
288
288
  }
289
289
 
290
290
  const tokenId = computeTokenId(bytes);
package/src/config.ts CHANGED
@@ -358,8 +358,11 @@ export const MAINNET_CONFIG: NetworkConfig = {
358
358
  bitcoinNetwork: "mainnet",
359
359
  esploraUrl: "https://mempool.space/api",
360
360
 
361
- // Circuit CDN
362
- circuitCdnUrl: "https://circuit.utxopia.com",
361
+ // Circuit CDN. Must carry the full versioned path: `resolveCircuitPath` turns a bare host
362
+ // into `<host>/circuits/groth16`, which is a DIFFERENT, stale build that still serves 200s
363
+ // for every shape with a different delta. Proofs built from it verify locally and are
364
+ // rejected on chain as "proof invalid".
365
+ circuitCdnUrl: "https://circuit.utxopia.com/circuits/v2/groth16",
363
366
 
364
367
  // Groth16 Verifier (placeholder)
365
368
  groth16VerifierProgramId: address("11111111111111111111111111111111"),
package/src/index.ts CHANGED
@@ -807,6 +807,7 @@ export {
807
807
  buildVkRegistryData,
808
808
  parseVkRegistry,
809
809
  assertVkRegistryForShape,
810
+ assertVkeyMatchesRegistry,
810
811
  isVkRegistryReady,
811
812
  type JoinSplitVkMaterial,
812
813
  type SnarkjsVkeyJson,
package/src/prover/web.ts CHANGED
@@ -67,6 +67,27 @@ const isNode = typeof process !== "undefined" && process.versions?.node;
67
67
  // Configurable circuit paths
68
68
  let circuitBasePath = isBrowser ? "/circuits/groth16" : "./circuits";
69
69
 
70
+ /**
71
+ * Fetch a circuit's `*.vkey.json` from the same base path its zkey comes from.
72
+ *
73
+ * Deliberately reads `circuitBasePath` rather than taking a URL: the point of the check this
74
+ * feeds (`assertVkeyMatchesRegistry`) is that the vkey describes the artifacts actually being
75
+ * proved with, and a caller-supplied URL could describe a different build entirely.
76
+ */
77
+ export async function fetchCircuitVkey(circuitType: CircuitType): Promise<unknown> {
78
+ const cached = vkeyCache.get(circuitType);
79
+ if (cached) return cached;
80
+
81
+ const url = `${circuitBasePath}/${circuitType}/${circuitType}.vkey.json`;
82
+ const res = await fetch(url);
83
+ if (!res.ok) {
84
+ throw new Error(`Could not fetch circuit vkey ${url}: ${res.status} ${res.statusText}`);
85
+ }
86
+ const vkey = await res.json();
87
+ vkeyCache.set(circuitType, vkey);
88
+ return vkey;
89
+ }
90
+
70
91
  /**
71
92
  * Set the base path for circuit artifacts
72
93
  */
@@ -74,6 +95,7 @@ export function setCircuitPath(path: string): void {
74
95
  if (path === circuitBasePath) return;
75
96
  circuitBasePath = path;
76
97
  circuitCache.clear();
98
+ vkeyCache.clear();
77
99
  artifactBytesCache.clear();
78
100
  artifactDownloadCache.clear();
79
101
  }
@@ -87,6 +109,7 @@ interface CircuitArtifact {
87
109
  }
88
110
 
89
111
  const circuitCache = new Map<CircuitType, CircuitArtifact>();
112
+ const vkeyCache = new Map<CircuitType, unknown>();
90
113
  let proverInitialized = false;
91
114
 
92
115
  /**
@@ -293,3 +293,55 @@ export function isVkRegistryReady(
293
293
  return false;
294
294
  }
295
295
  }
296
+
297
+ /**
298
+ * Assert the circuit artifacts this client loaded correspond to the verifying key the chain
299
+ * will check against.
300
+ *
301
+ * `assertVkRegistryForShape` proves the registry exists and names the right shape;
302
+ * `setCircuitArtifactDigests` proves the artifacts are the bytes this build expects. Neither
303
+ * ties the two together, and that gap is a real one: a zkey from a different phase-2 setup
304
+ * proves fine, downloads fine, passes every integrity check, and is then rejected on chain as
305
+ * "proof invalid" — with nothing pointing at the version mismatch.
306
+ *
307
+ * It is not hypothetical. `https://circuit.utxopia.com/circuits/groth16` and
308
+ * `.../circuits/v2/groth16` both serve 200s for every shape, with different `delta` — same
309
+ * ceremony, different per-circuit setup. Both therefore satisfy the program's own vk_hash
310
+ * recomputation, which binds delta+IC to the compiled-in alpha/beta/gamma and so catches a
311
+ * foreign ceremony but NOT a stale phase-2 from the right one. This is the check that does.
312
+ *
313
+ * @param vkey The `*.vkey.json` fetched from the SAME base path as the zkey. Fetching
314
+ * it from anywhere else makes this assertion meaningless.
315
+ * @param registryData Raw `VkRegistry` account data for the shape.
316
+ */
317
+ export function assertVkeyMatchesRegistry(
318
+ vkey: SnarkjsVkeyJson,
319
+ registryData: Uint8Array | null | undefined,
320
+ nInputs: number,
321
+ nOutputs: number,
322
+ ): void {
323
+ const registry = assertVkRegistryForShape(registryData, nInputs, nOutputs);
324
+
325
+ const expectedIc = joinSplitNumPublicInputs(nInputs, nOutputs) + 1;
326
+ if (vkey.IC.length !== expectedIc) {
327
+ throw new Error(
328
+ `Circuit vkey for JoinSplit ${nInputs}x${nOutputs} has ${vkey.IC.length} IC points, ` +
329
+ `expected ${expectedIc} — the artifacts are for a different shape.`,
330
+ );
331
+ }
332
+
333
+ const local = computeVkHash(vkey);
334
+ if (!local.every((b, i) => b === registry.vkHash[i])) {
335
+ throw new Error(
336
+ `Circuit artifacts for JoinSplit ${nInputs}x${nOutputs} do not match the on-chain ` +
337
+ `verifying key (artifact vkHash ${bytesToHex(local)}, registry ${bytesToHex(registry.vkHash)}). ` +
338
+ `A proof built from these would be rejected as invalid. Usually the circuit base URL ` +
339
+ `points at a stale build — check it resolves to the same version the registry was ` +
340
+ `registered from.`,
341
+ );
342
+ }
343
+ }
344
+
345
+ function bytesToHex(b: Uint8Array): string {
346
+ return Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
347
+ }