@vocdoni/davinci-sdk 0.1.1 → 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/dist/index.js CHANGED
@@ -2,8 +2,28 @@
2
2
 
3
3
  var axios = require('axios');
4
4
  var davinciContracts = require('@vocdoni/davinci-contracts');
5
- var snarkjs = require('snarkjs');
6
5
  var ethers = require('ethers');
6
+ var snarkjs = require('snarkjs');
7
+ var circomlibjs = require('circomlibjs');
8
+
9
+ function _interopNamespaceDefault(e) {
10
+ var n = Object.create(null);
11
+ if (e) {
12
+ Object.keys(e).forEach(function (k) {
13
+ if (k !== 'default') {
14
+ var d = Object.getOwnPropertyDescriptor(e, k);
15
+ Object.defineProperty(n, k, d.get ? d : {
16
+ enumerable: true,
17
+ get: function () { return e[k]; }
18
+ });
19
+ }
20
+ });
21
+ }
22
+ n.default = e;
23
+ return Object.freeze(n);
24
+ }
25
+
26
+ var snarkjs__namespace = /*#__PURE__*/_interopNamespaceDefault(snarkjs);
7
27
 
8
28
  var ElectionResultsTypeNames = /* @__PURE__ */ ((ElectionResultsTypeNames2) => {
9
29
  ElectionResultsTypeNames2["SINGLE_CHOICE_MULTIQUESTION"] = "single-choice-multiquestion";
@@ -1882,97 +1902,6 @@ class ProcessOrchestrationService {
1882
1902
  }
1883
1903
  }
1884
1904
 
1885
- class CircomProof {
1886
- constructor(opts = {}) {
1887
- // simple in-memory cache keyed by URL
1888
- this.wasmCache = /* @__PURE__ */ new Map();
1889
- this.zkeyCache = /* @__PURE__ */ new Map();
1890
- this.vkeyCache = /* @__PURE__ */ new Map();
1891
- this.wasmUrl = opts.wasmUrl;
1892
- this.zkeyUrl = opts.zkeyUrl;
1893
- this.vkeyUrl = opts.vkeyUrl;
1894
- this.wasmHash = opts.wasmHash;
1895
- this.zkeyHash = opts.zkeyHash;
1896
- this.vkeyHash = opts.vkeyHash;
1897
- }
1898
- /**
1899
- * Computes SHA-256 hash of the given data and compares it with the expected hash.
1900
- * @param data - The data to hash (string or ArrayBuffer or Uint8Array)
1901
- * @param expectedHash - The expected SHA-256 hash in hexadecimal format
1902
- * @param filename - The filename for error reporting
1903
- * @throws Error if the computed hash doesn't match the expected hash
1904
- */
1905
- verifyHash(data, expectedHash, filename) {
1906
- let bytes;
1907
- if (typeof data === "string") {
1908
- bytes = new TextEncoder().encode(data);
1909
- } else if (data instanceof ArrayBuffer) {
1910
- bytes = new Uint8Array(data);
1911
- } else {
1912
- bytes = data;
1913
- }
1914
- const computedHash = ethers.sha256(bytes).slice(2);
1915
- if (computedHash.toLowerCase() !== expectedHash.toLowerCase()) {
1916
- throw new Error(
1917
- `Hash verification failed for ${filename}. Expected: ${expectedHash.toLowerCase()}, Computed: ${computedHash.toLowerCase()}`
1918
- );
1919
- }
1920
- }
1921
- /**
1922
- * Generate a zk‐SNARK proof.
1923
- * If you didn't pass wasmUrl/zkeyUrl in the constructor you must supply them here.
1924
- */
1925
- async generate(inputs, urls = {}) {
1926
- const wasmUrl = urls.wasmUrl ?? this.wasmUrl;
1927
- const zkeyUrl = urls.zkeyUrl ?? this.zkeyUrl;
1928
- if (!wasmUrl) throw new Error("`wasmUrl` is required to generate a proof");
1929
- if (!zkeyUrl) throw new Error("`zkeyUrl` is required to generate a proof");
1930
- let wasmBin = this.wasmCache.get(wasmUrl);
1931
- if (!wasmBin) {
1932
- const r = await fetch(wasmUrl);
1933
- if (!r.ok) throw new Error(`Failed to fetch wasm at ${wasmUrl}: ${r.status}`);
1934
- const buf = await r.arrayBuffer();
1935
- wasmBin = new Uint8Array(buf);
1936
- if (this.wasmHash) {
1937
- this.verifyHash(wasmBin, this.wasmHash, "circuit.wasm");
1938
- }
1939
- this.wasmCache.set(wasmUrl, wasmBin);
1940
- }
1941
- let zkeyBin = this.zkeyCache.get(zkeyUrl);
1942
- if (!zkeyBin) {
1943
- const r = await fetch(zkeyUrl);
1944
- if (!r.ok) throw new Error(`Failed to fetch zkey at ${zkeyUrl}: ${r.status}`);
1945
- const buf = await r.arrayBuffer();
1946
- zkeyBin = new Uint8Array(buf);
1947
- if (this.zkeyHash) {
1948
- this.verifyHash(zkeyBin, this.zkeyHash, "proving_key.zkey");
1949
- }
1950
- this.zkeyCache.set(zkeyUrl, zkeyBin);
1951
- }
1952
- const { proof, publicSignals } = await snarkjs.groth16.fullProve(inputs, wasmBin, zkeyBin);
1953
- return {
1954
- proof,
1955
- publicSignals
1956
- };
1957
- }
1958
- async verify(proof, publicSignals, urlOverride) {
1959
- const vkeyUrl = urlOverride ?? this.vkeyUrl;
1960
- if (!vkeyUrl) throw new Error("`vkeyUrl` is required to verify a proof");
1961
- let vk = this.vkeyCache.get(vkeyUrl);
1962
- if (!vk) {
1963
- const r = await fetch(vkeyUrl);
1964
- if (!r.ok) throw new Error(`Failed to fetch vkey at ${vkeyUrl}: ${r.status}`);
1965
- const vkeyText = await r.text();
1966
- if (this.vkeyHash) {
1967
- this.verifyHash(vkeyText, this.vkeyHash, "verification_key.json");
1968
- }
1969
- vk = JSON.parse(vkeyText);
1970
- this.vkeyCache.set(vkeyUrl, vk);
1971
- }
1972
- return snarkjs.groth16.verify(vk, publicSignals, proof);
1973
- }
1974
- }
1975
-
1976
1905
  var VoteStatus = /* @__PURE__ */ ((VoteStatus2) => {
1977
1906
  VoteStatus2["Pending"] = "pending";
1978
1907
  VoteStatus2["Verified"] = "verified";
@@ -1984,11 +1913,15 @@ var VoteStatus = /* @__PURE__ */ ((VoteStatus2) => {
1984
1913
  })(VoteStatus || {});
1985
1914
 
1986
1915
  class VoteOrchestrationService {
1987
- constructor(apiService, getCrypto, signer, censusProviders = {}, config = {}) {
1916
+ constructor(apiService, getBallotInputGenerator, signer, censusProviders = {}, config = {}) {
1988
1917
  this.apiService = apiService;
1989
- this.getCrypto = getCrypto;
1918
+ this.getBallotInputGenerator = getBallotInputGenerator;
1990
1919
  this.signer = signer;
1991
1920
  this.censusProviders = censusProviders;
1921
+ // Cache for circuit files
1922
+ this.wasmCache = /* @__PURE__ */ new Map();
1923
+ this.zkeyCache = /* @__PURE__ */ new Map();
1924
+ this.vkeyCache = /* @__PURE__ */ new Map();
1992
1925
  this.verifyCircuitFiles = config.verifyCircuitFiles ?? true;
1993
1926
  this.verifyProof = config.verifyProof ?? true;
1994
1927
  }
@@ -2038,7 +1971,7 @@ class VoteOrchestrationService {
2038
1971
  if (process.census.censusOrigin === CensusOrigin.CSP) {
2039
1972
  voteRequest.censusProof = censusProof;
2040
1973
  }
2041
- await this.submitVoteRequest(voteRequest);
1974
+ await this.apiService.sequencer.submitVote(voteRequest);
2042
1975
  const status = await this.apiService.sequencer.getVoteStatus(config.processId, voteId);
2043
1976
  return {
2044
1977
  voteId,
@@ -2187,30 +2120,29 @@ class VoteOrchestrationService {
2187
2120
  throw new Error(`Unsupported census origin: ${censusOrigin}`);
2188
2121
  }
2189
2122
  /**
2190
- * Generate vote proof inputs using DavinciCrypto
2123
+ * Generate vote proof inputs using BallotInputGenerator
2191
2124
  */
2192
2125
  async generateVoteProofInputs(processId, voterAddress, encryptionKey, ballotMode, choices, weight, customRandomness) {
2193
- const crypto = await this.getCrypto();
2126
+ const generator = await this.getBallotInputGenerator();
2194
2127
  this.validateChoices(choices, ballotMode);
2195
- const fieldValues = choices.map((choice) => choice.toString());
2196
- const inputs = {
2197
- address: voterAddress.replace(/^0x/, ""),
2198
- processID: processId.replace(/^0x/, ""),
2199
- encryptionKey: [encryptionKey.x, encryptionKey.y],
2200
- ballotMode,
2201
- weight,
2202
- fieldValues
2203
- };
2128
+ let k;
2204
2129
  if (customRandomness) {
2205
2130
  const hexRandomness = customRandomness.startsWith("0x") ? customRandomness : "0x" + customRandomness;
2206
- const k = BigInt(hexRandomness).toString();
2207
- inputs.k = k;
2131
+ k = BigInt(hexRandomness).toString();
2208
2132
  }
2209
- const cryptoOutput = await crypto.proofInputs(inputs);
2133
+ const result = await generator.generateInputs(
2134
+ processId.replace(/^0x/, ""),
2135
+ voterAddress.replace(/^0x/, ""),
2136
+ encryptionKey,
2137
+ ballotMode,
2138
+ choices,
2139
+ weight,
2140
+ k
2141
+ );
2210
2142
  return {
2211
- voteId: cryptoOutput.voteId,
2212
- cryptoOutput,
2213
- circomInputs: cryptoOutput.circomInputs
2143
+ voteId: result.voteId,
2144
+ cryptoOutput: result,
2145
+ circomInputs: result.circomInputs
2214
2146
  };
2215
2147
  }
2216
2148
  /**
@@ -2227,29 +2159,79 @@ class VoteOrchestrationService {
2227
2159
  }
2228
2160
  }
2229
2161
  /**
2230
- * Generate zk-SNARK proof using CircomProof
2162
+ * Verify hash of downloaded file
2163
+ */
2164
+ verifyHash(data, expectedHash, filename) {
2165
+ const computedHash = ethers.sha256(data).slice(2);
2166
+ if (computedHash.toLowerCase() !== expectedHash.toLowerCase()) {
2167
+ throw new Error(
2168
+ `Hash verification failed for ${filename}. Expected: ${expectedHash.toLowerCase()}, Computed: ${computedHash.toLowerCase()}`
2169
+ );
2170
+ }
2171
+ }
2172
+ /**
2173
+ * Generate zk-SNARK proof using snarkjs directly (no CircomProof wrapper)
2231
2174
  */
2232
2175
  async generateZkProof(circomInputs) {
2233
2176
  const info = await this.apiService.sequencer.getInfo();
2234
- const circomProof = new CircomProof({
2235
- wasmUrl: info.circuitUrl,
2236
- zkeyUrl: info.provingKeyUrl,
2237
- vkeyUrl: info.verificationKeyUrl,
2238
- // Only pass hashes if verifyCircuitFiles is enabled
2239
- ...this.verifyCircuitFiles && {
2240
- wasmHash: info.circuitHash,
2241
- zkeyHash: info.provingKeyHash,
2242
- vkeyHash: info.verificationKeyHash
2177
+ let wasmBytes = this.wasmCache.get(info.circuitUrl);
2178
+ if (!wasmBytes) {
2179
+ const response = await fetch(info.circuitUrl);
2180
+ if (!response.ok) {
2181
+ throw new Error(`Failed to fetch WASM at ${info.circuitUrl}: ${response.status}`);
2243
2182
  }
2244
- });
2245
- const { proof, publicSignals } = await circomProof.generate(circomInputs);
2183
+ const buffer = await response.arrayBuffer();
2184
+ wasmBytes = new Uint8Array(buffer);
2185
+ if (this.verifyCircuitFiles) {
2186
+ this.verifyHash(wasmBytes, info.circuitHash, "circuit.wasm");
2187
+ }
2188
+ this.wasmCache.set(info.circuitUrl, wasmBytes);
2189
+ }
2190
+ let zkeyBytes = this.zkeyCache.get(info.provingKeyUrl);
2191
+ if (!zkeyBytes) {
2192
+ const response = await fetch(info.provingKeyUrl);
2193
+ if (!response.ok) {
2194
+ throw new Error(`Failed to fetch zkey at ${info.provingKeyUrl}: ${response.status}`);
2195
+ }
2196
+ const buffer = await response.arrayBuffer();
2197
+ zkeyBytes = new Uint8Array(buffer);
2198
+ if (this.verifyCircuitFiles) {
2199
+ this.verifyHash(zkeyBytes, info.provingKeyHash, "proving_key.zkey");
2200
+ }
2201
+ this.zkeyCache.set(info.provingKeyUrl, zkeyBytes);
2202
+ }
2203
+ const { proof, publicSignals } = await snarkjs__namespace.groth16.fullProve(
2204
+ circomInputs,
2205
+ wasmBytes,
2206
+ zkeyBytes
2207
+ );
2246
2208
  if (this.verifyProof) {
2247
- const isValid = await circomProof.verify(proof, publicSignals);
2209
+ let vkey = this.vkeyCache.get(info.verificationKeyUrl);
2210
+ if (!vkey) {
2211
+ const response = await fetch(info.verificationKeyUrl);
2212
+ if (!response.ok) {
2213
+ throw new Error(`Failed to fetch vkey at ${info.verificationKeyUrl}: ${response.status}`);
2214
+ }
2215
+ const vkeyText = await response.text();
2216
+ if (this.verifyCircuitFiles) {
2217
+ const vkeyBytes = new TextEncoder().encode(vkeyText);
2218
+ this.verifyHash(vkeyBytes, info.verificationKeyHash, "verification_key.json");
2219
+ }
2220
+ vkey = JSON.parse(vkeyText);
2221
+ this.vkeyCache.set(info.verificationKeyUrl, vkey);
2222
+ }
2223
+ const isValid = await snarkjs__namespace.groth16.verify(vkey, publicSignals, proof);
2248
2224
  if (!isValid) {
2249
2225
  throw new Error("Generated proof is invalid");
2250
2226
  }
2251
2227
  }
2252
- return { proof, publicSignals };
2228
+ const voteProof = {
2229
+ pi_a: proof.pi_a,
2230
+ pi_b: proof.pi_b,
2231
+ pi_c: proof.pi_c,
2232
+ protocol: proof.protocol
2233
+ };
2234
+ return { proof: voteProof, publicSignals };
2253
2235
  }
2254
2236
  hexToBytes(hex) {
2255
2237
  const clean = hex.replace(/^0x/, "");
@@ -2264,22 +2246,6 @@ class VoteOrchestrationService {
2264
2246
  async signVote(voteId) {
2265
2247
  return this.signer.signMessage(this.hexToBytes(voteId));
2266
2248
  }
2267
- /**
2268
- * Submit the vote request to the sequencer
2269
- */
2270
- async submitVoteRequest(voteRequest) {
2271
- const ballotProof = {
2272
- pi_a: voteRequest.ballotProof.pi_a,
2273
- pi_b: voteRequest.ballotProof.pi_b,
2274
- pi_c: voteRequest.ballotProof.pi_c,
2275
- protocol: voteRequest.ballotProof.protocol
2276
- };
2277
- const request = {
2278
- ...voteRequest,
2279
- ballotProof
2280
- };
2281
- await this.apiService.sequencer.submitVote(request);
2282
- }
2283
2249
  }
2284
2250
 
2285
2251
  class OrganizationRegistryService extends SmartContractService {
@@ -2378,6 +2344,97 @@ class OrganizationRegistryService extends SmartContractService {
2378
2344
  }
2379
2345
  }
2380
2346
 
2347
+ class CircomProof {
2348
+ constructor(opts = {}) {
2349
+ // simple in-memory cache keyed by URL
2350
+ this.wasmCache = /* @__PURE__ */ new Map();
2351
+ this.zkeyCache = /* @__PURE__ */ new Map();
2352
+ this.vkeyCache = /* @__PURE__ */ new Map();
2353
+ this.wasmUrl = opts.wasmUrl;
2354
+ this.zkeyUrl = opts.zkeyUrl;
2355
+ this.vkeyUrl = opts.vkeyUrl;
2356
+ this.wasmHash = opts.wasmHash;
2357
+ this.zkeyHash = opts.zkeyHash;
2358
+ this.vkeyHash = opts.vkeyHash;
2359
+ }
2360
+ /**
2361
+ * Computes SHA-256 hash of the given data and compares it with the expected hash.
2362
+ * @param data - The data to hash (string or ArrayBuffer or Uint8Array)
2363
+ * @param expectedHash - The expected SHA-256 hash in hexadecimal format
2364
+ * @param filename - The filename for error reporting
2365
+ * @throws Error if the computed hash doesn't match the expected hash
2366
+ */
2367
+ verifyHash(data, expectedHash, filename) {
2368
+ let bytes;
2369
+ if (typeof data === "string") {
2370
+ bytes = new TextEncoder().encode(data);
2371
+ } else if (data instanceof ArrayBuffer) {
2372
+ bytes = new Uint8Array(data);
2373
+ } else {
2374
+ bytes = data;
2375
+ }
2376
+ const computedHash = ethers.sha256(bytes).slice(2);
2377
+ if (computedHash.toLowerCase() !== expectedHash.toLowerCase()) {
2378
+ throw new Error(
2379
+ `Hash verification failed for ${filename}. Expected: ${expectedHash.toLowerCase()}, Computed: ${computedHash.toLowerCase()}`
2380
+ );
2381
+ }
2382
+ }
2383
+ /**
2384
+ * Generate a zk‐SNARK proof.
2385
+ * If you didn't pass wasmUrl/zkeyUrl in the constructor you must supply them here.
2386
+ */
2387
+ async generate(inputs, urls = {}) {
2388
+ const wasmUrl = urls.wasmUrl ?? this.wasmUrl;
2389
+ const zkeyUrl = urls.zkeyUrl ?? this.zkeyUrl;
2390
+ if (!wasmUrl) throw new Error("`wasmUrl` is required to generate a proof");
2391
+ if (!zkeyUrl) throw new Error("`zkeyUrl` is required to generate a proof");
2392
+ let wasmBin = this.wasmCache.get(wasmUrl);
2393
+ if (!wasmBin) {
2394
+ const r = await fetch(wasmUrl);
2395
+ if (!r.ok) throw new Error(`Failed to fetch wasm at ${wasmUrl}: ${r.status}`);
2396
+ const buf = await r.arrayBuffer();
2397
+ wasmBin = new Uint8Array(buf);
2398
+ if (this.wasmHash) {
2399
+ this.verifyHash(wasmBin, this.wasmHash, "circuit.wasm");
2400
+ }
2401
+ this.wasmCache.set(wasmUrl, wasmBin);
2402
+ }
2403
+ let zkeyBin = this.zkeyCache.get(zkeyUrl);
2404
+ if (!zkeyBin) {
2405
+ const r = await fetch(zkeyUrl);
2406
+ if (!r.ok) throw new Error(`Failed to fetch zkey at ${zkeyUrl}: ${r.status}`);
2407
+ const buf = await r.arrayBuffer();
2408
+ zkeyBin = new Uint8Array(buf);
2409
+ if (this.zkeyHash) {
2410
+ this.verifyHash(zkeyBin, this.zkeyHash, "proving_key.zkey");
2411
+ }
2412
+ this.zkeyCache.set(zkeyUrl, zkeyBin);
2413
+ }
2414
+ const { proof, publicSignals } = await snarkjs.groth16.fullProve(inputs, wasmBin, zkeyBin);
2415
+ return {
2416
+ proof,
2417
+ publicSignals
2418
+ };
2419
+ }
2420
+ async verify(proof, publicSignals, urlOverride) {
2421
+ const vkeyUrl = urlOverride ?? this.vkeyUrl;
2422
+ if (!vkeyUrl) throw new Error("`vkeyUrl` is required to verify a proof");
2423
+ let vk = this.vkeyCache.get(vkeyUrl);
2424
+ if (!vk) {
2425
+ const r = await fetch(vkeyUrl);
2426
+ if (!r.ok) throw new Error(`Failed to fetch vkey at ${vkeyUrl}: ${r.status}`);
2427
+ const vkeyText = await r.text();
2428
+ if (this.vkeyHash) {
2429
+ this.verifyHash(vkeyText, this.vkeyHash, "verification_key.json");
2430
+ }
2431
+ vk = JSON.parse(vkeyText);
2432
+ this.vkeyCache.set(vkeyUrl, vk);
2433
+ }
2434
+ return snarkjs.groth16.verify(vk, publicSignals, proof);
2435
+ }
2436
+ }
2437
+
2381
2438
  class DavinciCrypto {
2382
2439
  constructor(opts) {
2383
2440
  this.initialized = false;
@@ -2561,6 +2618,518 @@ class DavinciCrypto {
2561
2618
  }
2562
2619
  }
2563
2620
 
2621
+ class DavinciCSP {
2622
+ constructor(opts) {
2623
+ this.initialized = false;
2624
+ const { wasmExecUrl, wasmUrl, initTimeoutMs, wasmExecHash, wasmHash } = opts;
2625
+ if (!wasmExecUrl) throw new Error("`wasmExecUrl` is required");
2626
+ if (!wasmUrl) throw new Error("`wasmUrl` is required");
2627
+ this.wasmExecUrl = wasmExecUrl;
2628
+ this.wasmUrl = wasmUrl;
2629
+ this.initTimeoutMs = initTimeoutMs ?? 5e3;
2630
+ this.wasmExecHash = wasmExecHash;
2631
+ this.wasmHash = wasmHash;
2632
+ }
2633
+ static {
2634
+ // Cache for wasm files
2635
+ this.wasmExecCache = /* @__PURE__ */ new Map();
2636
+ }
2637
+ static {
2638
+ this.wasmBinaryCache = /* @__PURE__ */ new Map();
2639
+ }
2640
+ /**
2641
+ * Computes SHA-256 hash of the given data and compares it with the expected hash.
2642
+ * @param data - The data to hash (string or ArrayBuffer)
2643
+ * @param expectedHash - The expected SHA-256 hash in hexadecimal format
2644
+ * @param filename - The filename for error reporting
2645
+ * @throws Error if the computed hash doesn't match the expected hash
2646
+ */
2647
+ verifyHash(data, expectedHash, filename) {
2648
+ let bytes;
2649
+ if (typeof data === "string") {
2650
+ bytes = new TextEncoder().encode(data);
2651
+ } else {
2652
+ bytes = new Uint8Array(data);
2653
+ }
2654
+ const computedHash = ethers.sha256(bytes).slice(2);
2655
+ if (computedHash.toLowerCase() !== expectedHash.toLowerCase()) {
2656
+ throw new Error(
2657
+ `Hash verification failed for ${filename}. Expected: ${expectedHash.toLowerCase()}, Computed: ${computedHash.toLowerCase()}`
2658
+ );
2659
+ }
2660
+ }
2661
+ /**
2662
+ * Must be awaited before calling CSP functions.
2663
+ * Safe to call multiple times.
2664
+ */
2665
+ async init() {
2666
+ if (this.initialized) return;
2667
+ let shimCode = DavinciCSP.wasmExecCache.get(this.wasmExecUrl);
2668
+ if (!shimCode) {
2669
+ const shim = await fetch(this.wasmExecUrl);
2670
+ if (!shim.ok) {
2671
+ throw new Error(`Failed to fetch wasm_exec.js from ${this.wasmExecUrl}`);
2672
+ }
2673
+ shimCode = await shim.text();
2674
+ if (this.wasmExecHash) {
2675
+ this.verifyHash(shimCode, this.wasmExecHash, "wasm_exec.js");
2676
+ }
2677
+ DavinciCSP.wasmExecCache.set(this.wasmExecUrl, shimCode);
2678
+ }
2679
+ new Function(shimCode)();
2680
+ if (typeof globalThis.Go !== "function") {
2681
+ throw new Error("Global `Go` constructor not found after loading wasm_exec.js");
2682
+ }
2683
+ this.go = new globalThis.Go();
2684
+ let bytes = DavinciCSP.wasmBinaryCache.get(this.wasmUrl);
2685
+ if (!bytes) {
2686
+ const resp = await fetch(this.wasmUrl);
2687
+ if (!resp.ok) {
2688
+ throw new Error(`Failed to fetch davinci_crypto.wasm from ${this.wasmUrl}`);
2689
+ }
2690
+ bytes = await resp.arrayBuffer();
2691
+ if (this.wasmHash) {
2692
+ this.verifyHash(bytes, this.wasmHash, "davinci_crypto.wasm");
2693
+ }
2694
+ DavinciCSP.wasmBinaryCache.set(this.wasmUrl, bytes);
2695
+ }
2696
+ const { instance } = await WebAssembly.instantiate(bytes, this.go.importObject);
2697
+ this.go.run(instance).catch(() => {
2698
+ });
2699
+ const deadline = Date.now() + this.initTimeoutMs;
2700
+ while (Date.now() < deadline && !globalThis.DavinciCrypto) {
2701
+ await new Promise((r) => setTimeout(r, 50));
2702
+ }
2703
+ if (!globalThis.DavinciCrypto) {
2704
+ throw new Error("`DavinciCrypto` not initialized within timeout");
2705
+ }
2706
+ this.initialized = true;
2707
+ }
2708
+ /**
2709
+ * Generate a CSP (Credential Service Provider) signature for census proof.
2710
+ * @param censusOrigin - The census origin type (e.g., CensusOrigin.CSP)
2711
+ * @param privKey - The private key in hex format
2712
+ * @param processId - The process ID in hex format
2713
+ * @param address - The address in hex format
2714
+ * @param weight - The vote weight as a decimal string
2715
+ * @returns The CSP proof as a parsed JSON object
2716
+ * @throws if called before `await init()`, or if Go returns an error
2717
+ */
2718
+ async cspSign(censusOrigin, privKey, processId, address, weight) {
2719
+ if (!this.initialized) {
2720
+ throw new Error("DavinciCSP not initialized \u2014 call `await init()` first");
2721
+ }
2722
+ const raw = globalThis.DavinciCrypto.cspSign(censusOrigin, privKey, processId, address, weight);
2723
+ if (raw.error) {
2724
+ throw new Error(`Go/WASM cspSign error: ${raw.error}`);
2725
+ }
2726
+ if (!raw.data) {
2727
+ throw new Error("Go/WASM cspSign returned no data");
2728
+ }
2729
+ return raw.data;
2730
+ }
2731
+ /**
2732
+ * Verify a CSP (Credential Service Provider) proof.
2733
+ * @param censusOrigin - The census origin type (e.g., CensusOrigin.CSP)
2734
+ * @param root - The census root
2735
+ * @param address - The address
2736
+ * @param weight - The vote weight as a decimal string
2737
+ * @param processId - The process ID
2738
+ * @param publicKey - The public key
2739
+ * @param signature - The signature
2740
+ * @returns The verification result
2741
+ * @throws if called before `await init()`, or if Go returns an error
2742
+ */
2743
+ async cspVerify(censusOrigin, root, address, weight, processId, publicKey, signature) {
2744
+ if (!this.initialized) {
2745
+ throw new Error("DavinciCSP not initialized \u2014 call `await init()` first");
2746
+ }
2747
+ const cspProof = {
2748
+ censusOrigin,
2749
+ root,
2750
+ address,
2751
+ weight,
2752
+ processId,
2753
+ publicKey,
2754
+ signature
2755
+ };
2756
+ const raw = globalThis.DavinciCrypto.cspVerify(JSON.stringify(cspProof));
2757
+ if (raw.error) {
2758
+ throw new Error(`Go/WASM cspVerify error: ${raw.error}`);
2759
+ }
2760
+ if (!raw.data) {
2761
+ throw new Error("Go/WASM cspVerify returned no data");
2762
+ }
2763
+ return raw.data;
2764
+ }
2765
+ /**
2766
+ * Generate a CSP (Credential Service Provider) census root.
2767
+ * @param censusOrigin - The census origin type (e.g., CensusOrigin.CSP)
2768
+ * @param privKey - The private key in hex format
2769
+ * @returns The census root as a hexadecimal string
2770
+ * @throws if called before `await init()`, or if Go returns an error
2771
+ */
2772
+ async cspCensusRoot(censusOrigin, privKey) {
2773
+ if (!this.initialized) {
2774
+ throw new Error("DavinciCSP not initialized \u2014 call `await init()` first");
2775
+ }
2776
+ const raw = globalThis.DavinciCrypto.cspCensusRoot(censusOrigin, privKey);
2777
+ if (raw.error) {
2778
+ throw new Error(`Go/WASM cspCensusRoot error: ${raw.error}`);
2779
+ }
2780
+ if (!raw.data) {
2781
+ throw new Error("Go/WASM cspCensusRoot returned no data");
2782
+ }
2783
+ return raw.data.root;
2784
+ }
2785
+ }
2786
+
2787
+ function getRandomBytes(n) {
2788
+ if (typeof globalThis.crypto !== "undefined" && globalThis.crypto.getRandomValues) {
2789
+ return globalThis.crypto.getRandomValues(new Uint8Array(n));
2790
+ }
2791
+ throw new Error("Crypto not available");
2792
+ }
2793
+ async function buildElGamal() {
2794
+ const babyjub = await circomlibjs.buildBabyjub();
2795
+ const F = babyjub.F;
2796
+ function randomScalar() {
2797
+ const bytes = getRandomBytes(32);
2798
+ let bi = 0n;
2799
+ for (let i = 0; i < bytes.length; i++) {
2800
+ bi += BigInt(bytes[i]) << BigInt(8 * i);
2801
+ }
2802
+ return bi % babyjub.order;
2803
+ }
2804
+ function generateKeyPair() {
2805
+ const privKey = randomScalar();
2806
+ const pubKey = babyjub.mulPointEscalar(babyjub.Base8, privKey);
2807
+ return { privKey, pubKey };
2808
+ }
2809
+ function encrypt(msg, pubKey, k) {
2810
+ const kVal = BigInt(k);
2811
+ const mVal = BigInt(msg);
2812
+ const c1 = babyjub.mulPointEscalar(babyjub.Base8, kVal);
2813
+ const s = babyjub.mulPointEscalar(pubKey, kVal);
2814
+ const mPoint = babyjub.mulPointEscalar(babyjub.Base8, mVal);
2815
+ const c2 = babyjub.addPoint(mPoint, s);
2816
+ return { c1, c2 };
2817
+ }
2818
+ return {
2819
+ babyjub,
2820
+ F,
2821
+ encrypt,
2822
+ generateKeyPair,
2823
+ randomScalar,
2824
+ packPoint: babyjub.packPoint,
2825
+ unpackPoint: babyjub.unpackPoint
2826
+ };
2827
+ }
2828
+
2829
+ const FIELD_MODULUS = 21888242871839275222246405745257275088548364400416034343698204186575808495617n;
2830
+ const SCALING_FACTOR = 6360561867910373094066688120553762416144456282423235903351243436111059670888n;
2831
+ function modInverse(a, m) {
2832
+ let [old_r, r] = [a, m];
2833
+ let [old_s, s] = [1n, 0n];
2834
+ while (r !== 0n) {
2835
+ const quotient = old_r / r;
2836
+ [old_r, r] = [r, old_r - quotient * r];
2837
+ [old_s, s] = [s, old_s - quotient * s];
2838
+ }
2839
+ return (old_s % m + m) % m;
2840
+ }
2841
+ function mod(n, m) {
2842
+ return (n % m + m) % m;
2843
+ }
2844
+ function fromRTEtoTE(x, y) {
2845
+ const negF = mod(-SCALING_FACTOR, FIELD_MODULUS);
2846
+ const negFInv = modInverse(negF, FIELD_MODULUS);
2847
+ const xTE = mod(x * negFInv, FIELD_MODULUS);
2848
+ return [xTE, y];
2849
+ }
2850
+ function hexToDecimal(hex) {
2851
+ const cleanHex = hex.startsWith("0x") || hex.startsWith("0X") ? hex : "0x" + hex;
2852
+ return BigInt(cleanHex).toString();
2853
+ }
2854
+ function parseBallotMode(ballotMode) {
2855
+ return {
2856
+ numFields: ballotMode.numFields,
2857
+ uniqueValues: ballotMode.uniqueValues ? 1 : 0,
2858
+ maxValue: parseInt(ballotMode.maxValue),
2859
+ minValue: parseInt(ballotMode.minValue),
2860
+ maxValueSum: parseInt(ballotMode.maxValueSum),
2861
+ minValueSum: parseInt(ballotMode.minValueSum),
2862
+ costExponent: ballotMode.costExponent,
2863
+ costFromWeight: ballotMode.costFromWeight ? 1 : 0
2864
+ };
2865
+ }
2866
+ class BallotBuilder {
2867
+ constructor(elgamal, poseidon) {
2868
+ this.elgamal = elgamal;
2869
+ this.poseidon = poseidon;
2870
+ this.F = poseidon.F;
2871
+ }
2872
+ static async build() {
2873
+ const elgamal = await buildElGamal();
2874
+ const poseidon = await circomlibjs.buildPoseidon();
2875
+ return new BallotBuilder(elgamal, poseidon);
2876
+ }
2877
+ randomK() {
2878
+ return this.elgamal.randomScalar().toString();
2879
+ }
2880
+ derivePoseidonChain(seedK, n) {
2881
+ let current = BigInt(seedK);
2882
+ const out = [current.toString()];
2883
+ for (let i = 0; i < n; i++) {
2884
+ const h = this.poseidon([current]);
2885
+ const hBig = BigInt(this.F.toString(h, 10));
2886
+ out.push(hBig.toString());
2887
+ current = hBig;
2888
+ }
2889
+ return out;
2890
+ }
2891
+ // Matches circuits/lib/multiposeidon.circom logic
2892
+ multiHash(inputs) {
2893
+ const nInputs = inputs.length;
2894
+ if (nInputs <= 16) {
2895
+ return this.F.toObject(this.poseidon(inputs));
2896
+ }
2897
+ const chunks = [];
2898
+ for (let i = 0; i < nInputs; i += 16) {
2899
+ chunks.push(inputs.slice(i, i + 16));
2900
+ }
2901
+ const intermediateHashes = [];
2902
+ for (const chunk of chunks) {
2903
+ intermediateHashes.push(this.F.toObject(this.poseidon(chunk)));
2904
+ }
2905
+ return this.F.toObject(this.poseidon(intermediateHashes));
2906
+ }
2907
+ encryptFields(fields, pubKey, seedK, nFields) {
2908
+ const paddedFields = [...fields];
2909
+ while (paddedFields.length < nFields) {
2910
+ paddedFields.push(0);
2911
+ }
2912
+ const ks = this.derivePoseidonChain(seedK, nFields);
2913
+ const cipherfields = [];
2914
+ for (let i = 0; i < nFields; i++) {
2915
+ const k = ks[i + 1];
2916
+ const msg = BigInt(paddedFields[i]);
2917
+ const enc = this.elgamal.encrypt(msg, pubKey, k);
2918
+ cipherfields.push([
2919
+ [this.elgamal.F.toString(enc.c1[0], 10), this.elgamal.F.toString(enc.c1[1], 10)],
2920
+ [this.elgamal.F.toString(enc.c2[0], 10), this.elgamal.F.toString(enc.c2[1], 10)]
2921
+ ]);
2922
+ }
2923
+ return { cipherfields, paddedFields };
2924
+ }
2925
+ computeVoteID(processId, address, k) {
2926
+ const h = this.poseidon([BigInt(processId), BigInt(address), BigInt(k)]);
2927
+ const hBig = BigInt(this.F.toString(h, 10));
2928
+ const mask = (1n << 160n) - 1n;
2929
+ return (hBig & mask).toString();
2930
+ }
2931
+ computeInputsHash(inputs) {
2932
+ return this.multiHash(inputs).toString();
2933
+ }
2934
+ /**
2935
+ * Creates a public key point from TE coordinates (as strings or bigints).
2936
+ * Use this when you already have coordinates in TE format.
2937
+ */
2938
+ createPubKeyFromTE(x, y) {
2939
+ return [this.elgamal.F.e(BigInt(x)), this.elgamal.F.e(BigInt(y))];
2940
+ }
2941
+ /**
2942
+ * Creates a public key point from RTE coordinates (as strings or bigints).
2943
+ * Use this when you have coordinates from a Gnark-based system (like the DAVINCI sequencer).
2944
+ * This automatically converts from RTE to TE format.
2945
+ */
2946
+ createPubKeyFromRTE(x, y) {
2947
+ const [xTE, yTE] = fromRTEtoTE(BigInt(x), BigInt(y));
2948
+ return [this.elgamal.F.e(xTE), this.elgamal.F.e(yTE)];
2949
+ }
2950
+ /**
2951
+ * Generates ballot inputs for the circuit.
2952
+ *
2953
+ * IMPORTANT: The pubKey must be in TE (Twisted Edwards) format as used by circomlibjs.
2954
+ * If you have RTE coordinates from a Gnark-based system (like DAVINCI sequencer),
2955
+ * use `createPubKeyFromRTE()` or `generateInputsFromSequencer()` instead.
2956
+ *
2957
+ * @param fields - The vote field values
2958
+ * @param weight - The voter's weight
2959
+ * @param pubKey - Public key as field elements [x, y] in TE format (use createPubKeyFromTE/RTE)
2960
+ * @param processId - Process ID as decimal string
2961
+ * @param address - Voter address as decimal string
2962
+ * @param k - Random k value for encryption
2963
+ * @param config - Ballot configuration
2964
+ * @param circuitCapacity - Number of fields the circuit supports (default: 8)
2965
+ */
2966
+ generateInputs(fields, weight, pubKey, processId, address, k, config, circuitCapacity = 8) {
2967
+ const activeFields = fields.length;
2968
+ const { cipherfields, paddedFields } = this.encryptFields(fields, pubKey, k, circuitCapacity);
2969
+ const voteId = this.computeVoteID(processId, address, k);
2970
+ const inputsList = [];
2971
+ const ffProcessID = mod(BigInt(processId), FIELD_MODULUS);
2972
+ inputsList.push(ffProcessID);
2973
+ inputsList.push(BigInt(activeFields));
2974
+ inputsList.push(BigInt(config.uniqueValues));
2975
+ inputsList.push(BigInt(config.maxValue));
2976
+ inputsList.push(BigInt(config.minValue));
2977
+ inputsList.push(BigInt(config.maxValueSum));
2978
+ inputsList.push(BigInt(config.minValueSum));
2979
+ inputsList.push(BigInt(config.costExponent));
2980
+ inputsList.push(BigInt(config.costFromWeight));
2981
+ inputsList.push(BigInt(this.elgamal.F.toString(pubKey[0], 10)));
2982
+ inputsList.push(BigInt(this.elgamal.F.toString(pubKey[1], 10)));
2983
+ inputsList.push(BigInt(address));
2984
+ inputsList.push(BigInt(voteId));
2985
+ for (const cf of cipherfields) {
2986
+ inputsList.push(BigInt(cf[0][0]));
2987
+ inputsList.push(BigInt(cf[0][1]));
2988
+ inputsList.push(BigInt(cf[1][0]));
2989
+ inputsList.push(BigInt(cf[1][1]));
2990
+ }
2991
+ inputsList.push(BigInt(weight));
2992
+ const inputsHash = this.computeInputsHash(inputsList);
2993
+ return {
2994
+ fields: paddedFields,
2995
+ weight,
2996
+ encryption_pubkey: [
2997
+ this.elgamal.F.toString(pubKey[0], 10),
2998
+ this.elgamal.F.toString(pubKey[1], 10)
2999
+ ],
3000
+ cipherfields,
3001
+ process_id: processId,
3002
+ address,
3003
+ k,
3004
+ vote_id: voteId,
3005
+ inputs_hash: inputsHash,
3006
+ // Config
3007
+ num_fields: activeFields,
3008
+ unique_values: config.uniqueValues,
3009
+ max_value: config.maxValue,
3010
+ min_value: config.minValue,
3011
+ max_value_sum: config.maxValueSum,
3012
+ min_value_sum: config.minValueSum,
3013
+ cost_exponent: config.costExponent,
3014
+ cost_from_weight: config.costFromWeight
3015
+ };
3016
+ }
3017
+ /**
3018
+ * Generates ballot inputs from sequencer data.
3019
+ * This is a convenience method that handles the RTE to TE conversion
3020
+ * for the public key automatically.
3021
+ *
3022
+ * @param sequencerData - Data from the DAVINCI sequencer
3023
+ * @param fields - The vote field values
3024
+ * @param weight - The voter's weight
3025
+ * @param k - Optional random k value (generated if not provided)
3026
+ * @param circuitCapacity - The number of fields the circuit supports (default: 8)
3027
+ */
3028
+ generateInputsFromSequencer(sequencerData, fields, weight, k, circuitCapacity = 8) {
3029
+ const processId = hexToDecimal(sequencerData.processId);
3030
+ const address = hexToDecimal(sequencerData.address);
3031
+ const [pubKeyX_TE, pubKeyY_TE] = fromRTEtoTE(
3032
+ BigInt(sequencerData.pubKeyX),
3033
+ BigInt(sequencerData.pubKeyY)
3034
+ );
3035
+ const pubKey = [this.elgamal.F.e(pubKeyX_TE), this.elgamal.F.e(pubKeyY_TE)];
3036
+ const config = parseBallotMode(sequencerData.ballotMode);
3037
+ const kValue = k ?? this.randomK();
3038
+ return this.generateInputs(fields, weight, pubKey, processId, address, kValue, config, circuitCapacity);
3039
+ }
3040
+ }
3041
+
3042
+ class BallotInputGenerator {
3043
+ constructor() {
3044
+ this.initialized = false;
3045
+ }
3046
+ /**
3047
+ * Initialize the ballot input generator
3048
+ * Must be called before generating inputs
3049
+ */
3050
+ async init() {
3051
+ if (this.initialized) return;
3052
+ this.builder = await BallotBuilder.build();
3053
+ this.initialized = true;
3054
+ }
3055
+ /**
3056
+ * Generate ballot inputs for voting
3057
+ * @param processId - Process ID (hex string without 0x)
3058
+ * @param address - Voter address (hex string without 0x)
3059
+ * @param encryptionKey - Encryption public key [x, y]
3060
+ * @param ballotMode - Ballot mode configuration
3061
+ * @param choices - Array of voter choices
3062
+ * @param weight - Voter weight
3063
+ * @param customK - Optional custom randomness (will be generated if not provided)
3064
+ * @returns Ballot inputs ready for proof generation
3065
+ */
3066
+ async generateInputs(processId, address, encryptionKey, ballotMode, choices, weight, customK) {
3067
+ if (!this.initialized || !this.builder) {
3068
+ throw new Error("BallotInputGenerator not initialized \u2014 call `await init()` first");
3069
+ }
3070
+ const ballotInputs = this.builder.generateInputsFromSequencer(
3071
+ {
3072
+ processId,
3073
+ address: "0x" + address,
3074
+ pubKeyX: encryptionKey.x,
3075
+ pubKeyY: encryptionKey.y,
3076
+ ballotMode: {
3077
+ numFields: ballotMode.numFields,
3078
+ uniqueValues: ballotMode.uniqueValues,
3079
+ maxValue: ballotMode.maxValue,
3080
+ minValue: ballotMode.minValue,
3081
+ maxValueSum: ballotMode.maxValueSum,
3082
+ minValueSum: ballotMode.minValueSum,
3083
+ costExponent: ballotMode.costExponent,
3084
+ costFromWeight: ballotMode.costFromWeight
3085
+ }
3086
+ },
3087
+ choices,
3088
+ parseInt(weight),
3089
+ customK,
3090
+ 8
3091
+ );
3092
+ const circomInputs = {
3093
+ fields: ballotInputs.fields.map((f) => f.toString()),
3094
+ num_fields: ballotInputs.num_fields.toString(),
3095
+ unique_values: ballotInputs.unique_values.toString(),
3096
+ max_value: ballotInputs.max_value.toString(),
3097
+ min_value: ballotInputs.min_value.toString(),
3098
+ max_value_sum: ballotInputs.max_value_sum.toString(),
3099
+ min_value_sum: ballotInputs.min_value_sum.toString(),
3100
+ cost_exponent: ballotInputs.cost_exponent.toString(),
3101
+ cost_from_weight: ballotInputs.cost_from_weight.toString(),
3102
+ address: ballotInputs.address,
3103
+ weight: ballotInputs.weight.toString(),
3104
+ process_id: ballotInputs.process_id,
3105
+ vote_id: ballotInputs.vote_id,
3106
+ encryption_pubkey: [
3107
+ ballotInputs.encryption_pubkey[0],
3108
+ ballotInputs.encryption_pubkey[1]
3109
+ ],
3110
+ k: ballotInputs.k,
3111
+ cipherfields: ballotInputs.cipherfields.flat(2),
3112
+ inputs_hash: ballotInputs.inputs_hash
3113
+ };
3114
+ const ciphertexts = ballotInputs.cipherfields.map((cf) => ({
3115
+ c1: [cf[0][0], cf[0][1]],
3116
+ c2: [cf[1][0], cf[1][1]]
3117
+ }));
3118
+ const output = {
3119
+ processId: "0x" + BigInt(ballotInputs.process_id).toString(16).padStart(64, "0"),
3120
+ address: "0x" + BigInt(ballotInputs.address).toString(16).padStart(40, "0"),
3121
+ ballot: {
3122
+ curveType: "bjj_iden3",
3123
+ ciphertexts
3124
+ },
3125
+ ballotInputsHash: ballotInputs.inputs_hash,
3126
+ voteId: "0x" + BigInt(ballotInputs.vote_id).toString(16).padStart(64, "0"),
3127
+ circomInputs
3128
+ };
3129
+ return output;
3130
+ }
3131
+ }
3132
+
2564
3133
  class DavinciSDK {
2565
3134
  constructor(config) {
2566
3135
  this.initialized = false;
@@ -2650,6 +3219,30 @@ class DavinciSDK {
2650
3219
  }
2651
3220
  return this.davinciCrypto;
2652
3221
  }
3222
+ /**
3223
+ * Get or initialize the DavinciCSP service for CSP cryptographic operations
3224
+ */
3225
+ async getCSP() {
3226
+ if (!this.davinciCSP) {
3227
+ const info = await this.apiService.sequencer.getInfo();
3228
+ this.davinciCSP = new DavinciCSP({
3229
+ wasmExecUrl: info.ballotProofWasmHelperExecJsUrl,
3230
+ wasmUrl: info.ballotProofWasmHelperUrl
3231
+ });
3232
+ await this.davinciCSP.init();
3233
+ }
3234
+ return this.davinciCSP;
3235
+ }
3236
+ /**
3237
+ * Get or initialize the BallotInputGenerator service for ballot input generation
3238
+ */
3239
+ async getBallotInputGenerator() {
3240
+ if (!this.ballotInputGenerator) {
3241
+ this.ballotInputGenerator = new BallotInputGenerator();
3242
+ await this.ballotInputGenerator.init();
3243
+ }
3244
+ return this.ballotInputGenerator;
3245
+ }
2653
3246
  /**
2654
3247
  * Get the process orchestration service for simplified process creation.
2655
3248
  * Requires a signer with a provider for blockchain interactions.
@@ -2676,7 +3269,7 @@ class DavinciSDK {
2676
3269
  if (!this._voteOrchestrator) {
2677
3270
  this._voteOrchestrator = new VoteOrchestrationService(
2678
3271
  this.apiService,
2679
- () => this.getCrypto(),
3272
+ () => this.getBallotInputGenerator(),
2680
3273
  this.config.signer,
2681
3274
  this.censusProviders,
2682
3275
  {
@@ -3529,6 +4122,7 @@ class DavinciSDK {
3529
4122
  }
3530
4123
  }
3531
4124
 
4125
+ exports.BallotInputGenerator = BallotInputGenerator;
3532
4126
  exports.BaseService = BaseService;
3533
4127
  exports.Census = Census;
3534
4128
  exports.CensusNotUpdatable = CensusNotUpdatable;
@@ -3537,6 +4131,7 @@ exports.CensusOrigin = CensusOrigin;
3537
4131
  exports.CircomProof = CircomProof;
3538
4132
  exports.ContractServiceError = ContractServiceError;
3539
4133
  exports.CspCensus = CspCensus;
4134
+ exports.DavinciCSP = DavinciCSP;
3540
4135
  exports.DavinciCrypto = DavinciCrypto;
3541
4136
  exports.DavinciSDK = DavinciSDK;
3542
4137
  exports.ElectionMetadataTemplate = ElectionMetadataTemplate;