@cofhe/sdk 0.6.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/acps/acp.ts +411 -0
  3. package/acps/index.ts +70 -0
  4. package/{permits → acps}/onchain-utils.ts +49 -24
  5. package/acps/sealing.ts +90 -0
  6. package/acps/signature.ts +89 -0
  7. package/acps/store.ts +172 -0
  8. package/acps/test/acp.test.ts +615 -0
  9. package/acps/test/localstorage.test.ts +105 -0
  10. package/acps/test/sealing.test.ts +77 -0
  11. package/acps/test/store.test.ts +88 -0
  12. package/acps/test/validation.test.ts +361 -0
  13. package/acps/test-utils.ts +32 -0
  14. package/acps/types.ts +252 -0
  15. package/acps/validation.ts +392 -0
  16. package/adapters/test/ethers5.test.ts +4 -1
  17. package/adapters/test/ethers6.test.ts +4 -1
  18. package/adapters/test/wagmi.test.ts +5 -2
  19. package/chains/chains/stagingCofhe.ts +21 -0
  20. package/chains/index.ts +3 -1
  21. package/chains/test/chains.test.ts +2 -1
  22. package/core/acps.ts +625 -0
  23. package/core/client.ts +136 -39
  24. package/core/clientTypes.ts +52 -41
  25. package/core/config.ts +66 -5
  26. package/core/decrypt/MockThresholdNetworkAbi.ts +20 -11
  27. package/core/decrypt/apiError.ts +104 -0
  28. package/core/decrypt/cofheMocksDecryptForTx.ts +11 -11
  29. package/core/decrypt/cofheMocksDecryptForView.ts +7 -7
  30. package/core/decrypt/decryptForTxBuilder.ts +102 -102
  31. package/core/decrypt/decryptForViewBuilder.ts +90 -90
  32. package/core/decrypt/submitRetry.ts +38 -30
  33. package/core/decrypt/tnDecryptV1.ts +5 -5
  34. package/core/decrypt/tnDecryptV2.ts +25 -21
  35. package/core/decrypt/tnSealOutputV1.ts +4 -3
  36. package/core/decrypt/tnSealOutputV2.ts +24 -18
  37. package/core/encrypt/cofheMocksZkVerifySign.ts +59 -74
  38. package/core/encrypt/encryptInputsBuilder.ts +86 -52
  39. package/core/encrypt/zkPackProveVerify.ts +25 -18
  40. package/core/error.ts +34 -6
  41. package/core/index.ts +4 -14
  42. package/core/test/acpDefaults.test.ts +52 -0
  43. package/core/test/acps.test.ts +596 -0
  44. package/core/test/apiError.test.ts +130 -0
  45. package/core/test/client.test.ts +22 -19
  46. package/core/test/config.test.ts +25 -5
  47. package/core/test/decrypt.test.ts +40 -35
  48. package/core/test/decryptBuilders.test.ts +68 -68
  49. package/core/test/decryptErrorCodes.test.ts +217 -0
  50. package/core/test/encryptInputsBuilder.test.ts +72 -41
  51. package/core/test/pollCallbacks.test.ts +89 -18
  52. package/core/test/stagingRedirect.ts +18 -0
  53. package/core/test/submitRetry.test.ts +182 -0
  54. package/core/types.ts +9 -69
  55. package/dist/acp-Wi6isVQI.d.cts +407 -0
  56. package/dist/acp-Wi6isVQI.d.ts +407 -0
  57. package/dist/acps.cjs +1081 -0
  58. package/dist/acps.d.cts +480 -0
  59. package/dist/acps.d.ts +480 -0
  60. package/dist/acps.js +2 -0
  61. package/dist/chains.cjs +14 -1
  62. package/dist/chains.d.cts +30 -1
  63. package/dist/chains.d.ts +30 -1
  64. package/dist/chains.js +1 -1
  65. package/dist/{chunk-NOC3PYB7.js → chunk-43USWPEH.js} +930 -580
  66. package/dist/{chunk-MTRAXQXC.js → chunk-N6IDQRRU.js} +14 -2
  67. package/dist/chunk-Q7CBWGQX.js +1029 -0
  68. package/dist/{clientTypes-CyUvRRzA.d.ts → clientTypes-BN3nbzYM.d.ts} +342 -165
  69. package/dist/{clientTypes-BDy1qIBu.d.cts → clientTypes-CYZjFznO.d.cts} +342 -165
  70. package/dist/core.cjs +1358 -1002
  71. package/dist/core.d.cts +31 -9
  72. package/dist/core.d.ts +31 -9
  73. package/dist/core.js +3 -3
  74. package/dist/node.cjs +1293 -952
  75. package/dist/node.d.cts +2 -2
  76. package/dist/node.d.ts +2 -2
  77. package/dist/node.js +3 -3
  78. package/dist/web.cjs +1293 -952
  79. package/dist/web.d.cts +2 -2
  80. package/dist/web.d.ts +2 -2
  81. package/dist/web.js +3 -3
  82. package/node/test/inherited.test.ts +75 -65
  83. package/node/test/tfheinit.test.ts +23 -8
  84. package/package.json +6 -6
  85. package/web/test/client.web.test.ts +5 -1
  86. package/web/test/inherited.web.test.ts +75 -65
  87. package/web/test/tfheinit.web.test.ts +14 -5
  88. package/web/test/worker.config.web.test.ts +38 -23
  89. package/web/test/worker.output.web.test.ts +25 -24
  90. package/core/encrypt/encryptUtils.ts +0 -67
  91. package/core/permits.ts +0 -216
  92. package/core/test/permits.test.ts +0 -596
  93. package/dist/chunk-VB62WYPL.js +0 -978
  94. package/dist/permit-DnVMDT5h.d.cts +0 -376
  95. package/dist/permit-DnVMDT5h.d.ts +0 -376
  96. package/dist/permits.cjs +0 -1026
  97. package/dist/permits.d.cts +0 -353
  98. package/dist/permits.d.ts +0 -353
  99. package/dist/permits.js +0 -2
  100. package/permits/index.ts +0 -68
  101. package/permits/permit.ts +0 -385
  102. package/permits/sealing.ts +0 -131
  103. package/permits/signature.ts +0 -79
  104. package/permits/store.ts +0 -157
  105. package/permits/test/localstorage.test.ts +0 -113
  106. package/permits/test/permit.test.ts +0 -557
  107. package/permits/test/sealing.test.ts +0 -84
  108. package/permits/test/store.test.ts +0 -88
  109. package/permits/test/validation.test.ts +0 -361
  110. package/permits/test-utils.ts +0 -28
  111. package/permits/types.ts +0 -204
  112. package/permits/validation.ts +0 -327
  113. /package/{permits → acps}/utils.ts +0 -0
@@ -1,10 +1,12 @@
1
- import { type Permission, type EthEncryptedData } from '@/permits';
1
+ import { type ACPPublic, type EthEncryptedData } from '@/acps';
2
2
  import { cofheFetch } from '../debug.js';
3
3
 
4
4
  import { CofheError, CofheErrorCode } from '../error.js';
5
5
  import { type DecryptPollCallbackFunction } from '../types.js';
6
6
  import { computeMinuteRampPollIntervalMs } from './polling.js';
7
+ import { mapApiErrorCodeToCofheErrorCode, parseApiErrorResponseBody } from './apiError.js';
7
8
  import { classifySubmitResponse, normalize404RetryTimeoutMs, throwIfSubmitRetryTimedOut } from './submitRetry.js';
9
+ import {} from './tnDecryptUtils.js';
8
10
 
9
11
  // Polling configuration
10
12
  const POLL_INTERVAL_MS = 1000; // 1 second
@@ -136,7 +138,7 @@ async function submitSealOutputRequest(
136
138
  thresholdNetworkUrl: string,
137
139
  ctHash: bigint | string,
138
140
  chainId: number,
139
- permission: Permission,
141
+ acp: ACPPublic,
140
142
  overallStartTime: number,
141
143
  retry404TimeoutMs: number,
142
144
  onPoll?: DecryptPollCallbackFunction
@@ -144,9 +146,10 @@ async function submitSealOutputRequest(
144
146
  const body = {
145
147
  ct_tempkey: BigInt(ctHash).toString(16).padStart(64, '0'),
146
148
  host_chain_id: chainId,
147
- permit: permission,
149
+ acp,
148
150
  };
149
151
  let attemptIndex = 0;
152
+ let last404ApiErrorMessage: string | undefined;
150
153
 
151
154
  for (;;) {
152
155
  let response: Response;
@@ -175,17 +178,20 @@ async function submitSealOutputRequest(
175
178
  });
176
179
  }
177
180
 
178
- const responseClassification = await classifySubmitResponse({ response });
181
+ const responseClassification = await classifySubmitResponse({
182
+ response,
183
+ fallbackErrorCode: CofheErrorCode.SealOutputFailed,
184
+ });
179
185
 
180
186
  if (responseClassification.kind === 'fatal-http') {
181
187
  throw new CofheError({
182
- code: CofheErrorCode.SealOutputFailed,
188
+ code: responseClassification.cofheErrorCode,
189
+ apiErrorCode: responseClassification.apiErrorCode,
183
190
  message: `sealOutput request failed: ${responseClassification.errorMessage}`,
184
191
  hint: 'Check the threshold network URL and request parameters.',
185
192
  context: {
186
193
  thresholdNetworkUrl,
187
- status: response.status,
188
- statusText: response.statusText,
194
+ status: responseClassification.status,
189
195
  body,
190
196
  attemptIndex,
191
197
  },
@@ -236,6 +242,10 @@ async function submitSealOutputRequest(
236
242
  });
237
243
  }
238
244
 
245
+ if (responseClassification.status === 404) {
246
+ last404ApiErrorMessage = responseClassification.apiErrorMessage;
247
+ }
248
+
239
249
  const elapsedMs = Date.now() - overallStartTime;
240
250
 
241
251
  throwIfSubmitRetryTimedOut({
@@ -248,6 +258,7 @@ async function submitSealOutputRequest(
248
258
  thresholdNetworkUrl,
249
259
  body,
250
260
  attemptIndex,
261
+ lastKnownErrorMessage: last404ApiErrorMessage,
251
262
  });
252
263
 
253
264
  onPoll?.({
@@ -344,16 +355,11 @@ async function pollSealOutputStatus(
344
355
 
345
356
  // Handle other non-200 status codes
346
357
  if (!response.ok) {
347
- let errorMessage = `HTTP ${response.status}`;
348
- try {
349
- const errorBody = await response.json();
350
- errorMessage = errorBody.error_message || errorBody.message || errorMessage;
351
- } catch {
352
- errorMessage = response.statusText || errorMessage;
353
- }
358
+ const { apiErrorCode, errorMessage } = await parseApiErrorResponseBody(response);
354
359
 
355
360
  throw new CofheError({
356
- code: CofheErrorCode.SealOutputFailed,
361
+ code: mapApiErrorCodeToCofheErrorCode(apiErrorCode, CofheErrorCode.SealOutputFailed),
362
+ apiErrorCode,
357
363
  message: `sealOutput status poll failed: ${errorMessage}`,
358
364
  context: {
359
365
  thresholdNetworkUrl,
@@ -407,12 +413,12 @@ async function pollSealOutputStatus(
407
413
  export async function tnSealOutputV2(params: {
408
414
  ctHash: bigint | string;
409
415
  chainId: number;
410
- permission: Permission;
416
+ acp: ACPPublic;
411
417
  thresholdNetworkUrl: string;
412
418
  retry404TimeoutMs?: number;
413
419
  onPoll?: DecryptPollCallbackFunction;
414
420
  }): Promise<EthEncryptedData> {
415
- const { thresholdNetworkUrl, ctHash, chainId, permission, retry404TimeoutMs, onPoll } = params;
421
+ const { thresholdNetworkUrl, ctHash, chainId, acp, retry404TimeoutMs, onPoll } = params;
416
422
  const normalized404RetryTimeoutMs = normalize404RetryTimeoutMs({
417
423
  timeoutMs: retry404TimeoutMs,
418
424
  operationLabel: 'sealOutput',
@@ -425,7 +431,7 @@ export async function tnSealOutputV2(params: {
425
431
  thresholdNetworkUrl,
426
432
  ctHash,
427
433
  chainId,
428
- permission,
434
+ acp,
429
435
  overallStartTime,
430
436
  normalized404RetryTimeoutMs,
431
437
  onPoll
@@ -1,15 +1,6 @@
1
1
  import { type EncryptableItem, FheTypes } from '../types.js';
2
- import { type VerifyResult } from './zkPackProveVerify.js';
3
- import {
4
- createWalletClient,
5
- http,
6
- encodePacked,
7
- keccak256,
8
- hashMessage,
9
- toBytes,
10
- type PublicClient,
11
- type WalletClient,
12
- } from 'viem';
2
+ import { type VerifyBatchResult } from './zkPackProveVerify.js';
3
+ import { createWalletClient, http, encodePacked, keccak256, type PublicClient, type WalletClient } from 'viem';
13
4
  import { MockZkVerifierAbi } from './MockZkVerifierAbi.js';
14
5
  import { hardhat } from 'viem/chains';
15
6
  import { CofheError, CofheErrorCode } from '../error.js';
@@ -178,89 +169,80 @@ async function insertCtHashes(items: EncryptableItemWithCtHash[], walletClient:
178
169
  }
179
170
 
180
171
  /**
181
- * The mocks verify the EncryptedInputs' signature against the known proof signer account.
182
- * Locally, we create the proof signatures from the known proof signer account.
172
+ * The mocks verify a batch's signature against the known verifier signer account.
173
+ * Locally, we create the single batch signature from the known signer account, over
174
+ * keccak256(h_0 || h_1 || ... || h_n), where each h_i is the same per-item message hash
175
+ * used by the on-chain digest (`ctHash || utype || securityZone || sender || chainId ||
176
+ * consumingContract`, per cofhe-contracts#77's contract-binding fix). This is the one
177
+ * canonical signer implementation used by the mocks - there is no separate per-item signing
178
+ * path.
183
179
  */
184
- async function createProofSignatures(
180
+ async function createBatchProofSignature(
185
181
  items: EncryptableItemWithCtHash[],
186
182
  securityZone: number,
187
- account: string
188
- ): Promise<`0x${string}`[]> {
189
- let signatures: `0x${string}`[] = [];
190
-
191
- // Create wallet client for the encrypted input signer
192
- // This wallet won't send a transaction, so gas isn't needed
193
- // This wallet doesn't need to be connected to the network
194
- let encInputSignerClient: WalletClient | undefined;
195
-
196
- try {
197
- encInputSignerClient = createMockZkVerifierSigner();
198
- } catch (err) {
199
- throw new CofheError({
200
- code: CofheErrorCode.ZkMocksCreateProofSignatureFailed,
201
- message: `mockZkVerifySign createProofSignatures failed while creating wallet client`,
202
- cause: err instanceof Error ? err : undefined,
203
- context: {
204
- MOCKS_ZK_VERIFIER_SIGNER_PRIVATE_KEY,
205
- },
206
- });
207
- }
208
-
183
+ account: string,
184
+ consumingContract: string
185
+ ): Promise<`0x${string}`> {
209
186
  try {
210
- for (const item of items) {
211
- // Pack the data into bytes and hash it
212
- const packedData = encodePacked(
213
- ['uint256', 'uint8', 'uint8', 'address', 'uint256'],
214
- [BigInt(item.ctHash), item.utype, securityZone, account as `0x${string}`, BigInt(hardhat.id)]
215
- );
216
- const messageHash = keccak256(packedData);
187
+ // Compute each item's per-item message hash h_i
188
+ const itemHashes = items.map((item) =>
189
+ keccak256(
190
+ encodePacked(
191
+ ['uint256', 'uint8', 'uint8', 'address', 'uint256', 'address'],
192
+ [
193
+ BigInt(item.ctHash),
194
+ item.utype,
195
+ securityZone,
196
+ account as `0x${string}`,
197
+ BigInt(hardhat.id),
198
+ consumingContract as `0x${string}`,
199
+ ]
200
+ )
201
+ )
202
+ );
217
203
 
218
- const signature = await sign({
219
- hash: messageHash,
220
- privateKey: MOCKS_ZK_VERIFIER_SIGNER_PRIVATE_KEY,
221
- to: 'hex',
222
- });
204
+ // Fold into one batch digest: keccak256(h_0 || h_1 || ... || h_n)
205
+ const batchDigest = keccak256(
206
+ encodePacked(
207
+ itemHashes.map(() => 'bytes32' as const),
208
+ itemHashes
209
+ )
210
+ );
223
211
 
224
- signatures.push(signature);
225
- }
212
+ // Sign once for the whole batch
213
+ return await sign({
214
+ hash: batchDigest,
215
+ privateKey: MOCKS_ZK_VERIFIER_SIGNER_PRIVATE_KEY,
216
+ to: 'hex',
217
+ });
226
218
  } catch (err) {
227
219
  throw new CofheError({
228
220
  code: CofheErrorCode.ZkMocksCreateProofSignatureFailed,
229
- message: `mockZkVerifySign createProofSignatures failed while calling signMessage`,
221
+ message: `mockZkVerifySign createBatchProofSignature failed while signing the batch digest`,
230
222
  cause: err instanceof Error ? err : undefined,
231
223
  context: {
232
224
  items,
233
225
  securityZone,
226
+ consumingContract,
234
227
  },
235
228
  });
236
229
  }
237
-
238
- if (signatures.length !== items.length) {
239
- throw new CofheError({
240
- code: CofheErrorCode.ZkMocksCreateProofSignatureFailed,
241
- message: `mockZkVerifySign createProofSignatures returned incorrect number of signatures`,
242
- context: {
243
- items,
244
- securityZone,
245
- },
246
- });
247
- }
248
-
249
- return signatures;
250
230
  }
251
231
 
252
232
  /**
253
- * Transforms the encryptable items into EncryptedInputs ready to be used in a transaction on the hardhat chain.
254
- * The EncryptedInputs are returned in the same format as from CoFHE, and include on-chain verifiable signatures.
233
+ * Transforms the encryptable items into a batch-verified result ready to be used in a
234
+ * transaction on the hardhat chain. Mirrors the shape returned by CoFHE's `/verifyBatch`:
235
+ * per-item ctHash/ctType, plus one shared on-chain verifiable signature for the whole batch.
255
236
  */
256
237
  export async function cofheMocksZkVerifySign(
257
238
  items: EncryptableItem[],
258
239
  account: string,
259
240
  securityZone: number,
241
+ consumingContract: string,
260
242
  publicClient: PublicClient,
261
243
  walletClient: WalletClient,
262
244
  zkvWalletClient: WalletClient | undefined
263
- ): Promise<VerifyResult[]> {
245
+ ): Promise<VerifyBatchResult> {
264
246
  // Use config._internal?.zkvWalletClient if provided, otherwise use a mock zk verifier signer
265
247
  const _walletClient = zkvWalletClient ?? createMockZkVerifierSigner();
266
248
 
@@ -270,12 +252,15 @@ export async function cofheMocksZkVerifySign(
270
252
  // Insert the ctHashes into the MockZkVerifier contract
271
253
  await insertCtHashes(encryptableItems, _walletClient);
272
254
 
273
- // Locally create the proof signatures from the known proof signer account
274
- const signatures = await createProofSignatures(encryptableItems, securityZone, account);
255
+ // Locally create the single batch signature from the known signer account
256
+ const signature = await createBatchProofSignature(encryptableItems, securityZone, account, consumingContract);
275
257
 
276
- // Return the ctHashes and signatures in the same format as CoFHE
277
- return encryptableItems.map((item, index) => ({
278
- ct_hash: item.ctHash.toString(),
279
- signature: signatures[index],
280
- }));
258
+ // Return the ctHashes/ctTypes and the batch signature, in the same shape as CoFHE's /verifyBatch
259
+ return {
260
+ outputs: encryptableItems.map((item) => ({
261
+ ct_hash: item.ctHash.toString(),
262
+ ct_type: item.utype,
263
+ })),
264
+ signature,
265
+ };
281
266
  }
@@ -1,10 +1,11 @@
1
1
  import {
2
2
  type ZkBuilderAndCrsGenerator,
3
3
  type ZkProveWorkerFunction,
4
+ type VerifyBatchResult,
4
5
  zkPack,
5
6
  zkProve,
6
7
  zkProveWithWorker,
7
- zkVerify,
8
+ zkVerifyBatch,
8
9
  constructZkPoKMetadata,
9
10
  } from './zkPackProveVerify.js';
10
11
  import { CofheError, CofheErrorCode } from '../error.js';
@@ -12,8 +13,6 @@ import {
12
13
  type EncryptStepCallbackFunction,
13
14
  EncryptStep,
14
15
  type EncryptableItem,
15
- type EncryptedItemInput,
16
- type EncryptedItemInputs,
17
16
  type TfheInitializer,
18
17
  type EncryptStepCallbackContext,
19
18
  type HashPlusProofResult,
@@ -48,11 +47,20 @@ type EncryptInputsBuilderParams<T extends EncryptableItem[]> = BaseBuilderParams
48
47
  * config, tfhePublicKeyDeserializer, compactPkeCrsDeserializer, and zkBuilderAndCrsGenerator are required to be set in the builder.
49
48
  */
50
49
 
51
- export class EncryptInputsBuilder<T extends EncryptableItem[], HPP extends boolean = false> extends BaseBuilder {
50
+ /**
51
+ * The builder as returned by `client.encryptInputs(...)`, before a consuming contract has been set.
52
+ *
53
+ * `execute()` is intentionally absent: the verifier binds the consuming contract into the signed
54
+ * digest, so no batch can be produced until the caller commits to the contract that will consume it.
55
+ * `setConsumingContract(...)` returns the full {@link EncryptInputsBuilder}, unlocking `execute()`.
56
+ */
57
+ export type EncryptInputsBuilderUnset<T extends EncryptableItem[]> = Omit<EncryptInputsBuilder<T>, 'execute'>;
58
+
59
+ export class EncryptInputsBuilder<T extends EncryptableItem[]> extends BaseBuilder {
52
60
  private securityZone: number;
61
+ protected consumingContract: string | undefined;
53
62
  private stepCallback?: EncryptStepCallbackFunction;
54
63
  private inputItems: [...T];
55
- private hpp: boolean = false;
56
64
 
57
65
  private zkvWalletClient: WalletClient | undefined;
58
66
 
@@ -156,7 +164,9 @@ export class EncryptInputsBuilder<T extends EncryptableItem[], HPP extends boole
156
164
  *
157
165
  * @returns The chainable EncryptInputsBuilder instance.
158
166
  */
159
- setAccount(account: string): EncryptInputsBuilder<T, HPP> {
167
+ setAccount(this: EncryptInputsBuilder<T>, account: string): EncryptInputsBuilder<T>;
168
+ setAccount(this: EncryptInputsBuilderUnset<T>, account: string): EncryptInputsBuilderUnset<T>;
169
+ setAccount(account: string): EncryptInputsBuilder<T> {
160
170
  this.account = account;
161
171
  return this;
162
172
  }
@@ -179,7 +189,9 @@ export class EncryptInputsBuilder<T extends EncryptableItem[], HPP extends boole
179
189
  *
180
190
  * @returns The chainable EncryptInputsBuilder instance.
181
191
  */
182
- setChainId(chainId: number): EncryptInputsBuilder<T, HPP> {
192
+ setChainId(this: EncryptInputsBuilder<T>, chainId: number): EncryptInputsBuilder<T>;
193
+ setChainId(this: EncryptInputsBuilderUnset<T>, chainId: number): EncryptInputsBuilderUnset<T>;
194
+ setChainId(chainId: number): EncryptInputsBuilder<T> {
183
195
  this.chainId = chainId;
184
196
  return this;
185
197
  }
@@ -202,7 +214,9 @@ export class EncryptInputsBuilder<T extends EncryptableItem[], HPP extends boole
202
214
  *
203
215
  * @returns The chainable EncryptInputsBuilder instance.
204
216
  */
205
- setSecurityZone(securityZone: number): EncryptInputsBuilder<T, HPP> {
217
+ setSecurityZone(this: EncryptInputsBuilder<T>, securityZone: number): EncryptInputsBuilder<T>;
218
+ setSecurityZone(this: EncryptInputsBuilderUnset<T>, securityZone: number): EncryptInputsBuilderUnset<T>;
219
+ setSecurityZone(securityZone: number): EncryptInputsBuilder<T> {
206
220
  this.securityZone = securityZone;
207
221
  return this;
208
222
  }
@@ -212,18 +226,44 @@ export class EncryptInputsBuilder<T extends EncryptableItem[], HPP extends boole
212
226
  }
213
227
 
214
228
  /**
229
+ * @param address - The contract that will consume the resulting hashes+signature (i.e. the
230
+ * contract that will call `FHE.asEuint*`/`FHE.asEuint*s` with them).
231
+ *
232
+ * Required before `execute()` - the verifier binds this address into the signed digest, so a
233
+ * batch signed for one contract cannot be replayed into another.
234
+ *
215
235
  * Example:
216
236
  * ```typescript
217
237
  * const encrypted = await encryptInputs([Encryptable.uint128(10n)])
218
- * .asHashPlusProof()
238
+ * .setConsumingContract("0x123...890")
219
239
  * .execute();
220
240
  * ```
221
241
  *
222
- * @returns Chainable EncryptInputsBuilder instance that will return a HashPlusProofResult instead of an array of EncryptedItemInputs.
242
+ * @returns The chainable EncryptInputsBuilder instance.
223
243
  */
224
- asHashPlusProof(): EncryptInputsBuilder<T, true> {
225
- this.hpp = true;
226
- return this as unknown as EncryptInputsBuilder<T, true>;
244
+ setConsumingContract(address: string): EncryptInputsBuilder<T> {
245
+ this.consumingContract = address;
246
+ return this;
247
+ }
248
+
249
+ getConsumingContract(): string | undefined {
250
+ return this.consumingContract;
251
+ }
252
+
253
+ /**
254
+ * Asserts that this.consumingContract is populated
255
+ * @throws {CofheError} If consumingContract is not set
256
+ */
257
+ private assertConsumingContract(): asserts this is this & { consumingContract: string } {
258
+ if (this.consumingContract) return;
259
+ throw new CofheError({
260
+ code: CofheErrorCode.ConsumingContractUninitialized,
261
+ message: 'Consuming contract is not set',
262
+ hint: 'Use setConsumingContract(...) to set the contract that will consume the encrypted inputs.',
263
+ context: {
264
+ consumingContract: this.consumingContract,
265
+ },
266
+ });
227
267
  }
228
268
 
229
269
  /**
@@ -240,7 +280,9 @@ export class EncryptInputsBuilder<T extends EncryptableItem[], HPP extends boole
240
280
  *
241
281
  * @returns The chainable EncryptInputsBuilder instance.
242
282
  */
243
- setUseWorker(useWorker: boolean): EncryptInputsBuilder<T, HPP> {
283
+ setUseWorker(this: EncryptInputsBuilder<T>, useWorker: boolean): EncryptInputsBuilder<T>;
284
+ setUseWorker(this: EncryptInputsBuilderUnset<T>, useWorker: boolean): EncryptInputsBuilderUnset<T>;
285
+ setUseWorker(useWorker: boolean): EncryptInputsBuilder<T> {
244
286
  this.useWorker = useWorker;
245
287
  return this;
246
288
  }
@@ -277,7 +319,9 @@ export class EncryptInputsBuilder<T extends EncryptableItem[], HPP extends boole
277
319
  *
278
320
  * @returns The EncryptInputsBuilder instance.
279
321
  */
280
- onStep(callback: EncryptStepCallbackFunction): EncryptInputsBuilder<T, HPP> {
322
+ onStep(this: EncryptInputsBuilder<T>, callback: EncryptStepCallbackFunction): EncryptInputsBuilder<T>;
323
+ onStep(this: EncryptInputsBuilderUnset<T>, callback: EncryptStepCallbackFunction): EncryptInputsBuilderUnset<T>;
324
+ onStep(callback: EncryptStepCallbackFunction): EncryptInputsBuilder<T> {
281
325
  this.stepCallback = callback;
282
326
  return this;
283
327
  }
@@ -433,10 +477,11 @@ export class EncryptInputsBuilder<T extends EncryptableItem[], HPP extends boole
433
477
  * cofheMocksInsertPackedHashes - stores the ctHashes and their plaintext values for on-chain mocking of FHE operations.
434
478
  * cofheMocksZkCreateProofSignatures - creates signatures to be included in the encrypted inputs. The signers address is known and verified in the mock contracts.
435
479
  */
436
- private async mocksExecute(): Promise<[...EncryptedItemInputs<T>]> {
480
+ private async mocksExecute(): Promise<VerifyBatchResult> {
437
481
  this.assertAccount();
438
482
  this.assertPublicClient();
439
483
  this.assertWalletClient();
484
+ this.assertConsumingContract();
440
485
 
441
486
  const [initTfheDelay, fetchKeysDelay, packDelay, proveDelay, verifyDelay] = this.resolveEncryptDelays();
442
487
 
@@ -468,31 +513,27 @@ export class EncryptInputsBuilder<T extends EncryptableItem[], HPP extends boole
468
513
 
469
514
  this.fireStepStart(EncryptStep.Verify);
470
515
  await sleep(verifyDelay);
471
- const signedResults = await cofheMocksZkVerifySign(
516
+ const result = await cofheMocksZkVerifySign(
472
517
  this.inputItems,
473
518
  this.account,
474
519
  this.securityZone,
520
+ this.consumingContract,
475
521
  this.publicClient,
476
522
  this.walletClient,
477
523
  this.zkvWalletClient
478
524
  );
479
- const encryptedInputs: EncryptedItemInput[] = signedResults.map(({ ct_hash, signature }, index) => ({
480
- ctHash: BigInt(ct_hash),
481
- securityZone: this.securityZone,
482
- utype: this.inputItems[index].utype,
483
- signature,
484
- }));
485
525
  this.fireStepEnd(EncryptStep.Verify, { isMocks: true, mockSleep: verifyDelay });
486
526
 
487
- return encryptedInputs as [...EncryptedItemInputs<T>];
527
+ return result;
488
528
  }
489
529
 
490
530
  /**
491
531
  * In the production context, perform a true encryption with the CoFHE coprocessor.
492
532
  */
493
- private async productionExecute(): Promise<[...EncryptedItemInputs<T>]> {
533
+ private async productionExecute(): Promise<VerifyBatchResult> {
494
534
  this.assertAccount();
495
535
  this.assertChainId();
536
+ this.assertConsumingContract();
496
537
 
497
538
  this.fireStepStart(EncryptStep.InitTfhe);
498
539
 
@@ -555,32 +596,28 @@ export class EncryptInputsBuilder<T extends EncryptableItem[], HPP extends boole
555
596
 
556
597
  const zkVerifierUrl = await this.getZkVerifierUrl();
557
598
 
558
- const verifyResults = await zkVerify(zkVerifierUrl, proof, this.account, this.securityZone, this.chainId);
559
- // Add securityZone and utype to the verify results
560
- const encryptedInputs: EncryptedItemInput[] = verifyResults.map(
561
- ({ ct_hash, signature }: { ct_hash: string; signature: `0x${string}` }, index: number) => ({
562
- ctHash: BigInt(ct_hash),
563
- securityZone: this.securityZone,
564
- utype: this.inputItems[index].utype,
565
- signature,
566
- })
599
+ const result = await zkVerifyBatch(
600
+ zkVerifierUrl,
601
+ proof,
602
+ this.account,
603
+ this.securityZone,
604
+ this.chainId,
605
+ this.consumingContract
567
606
  );
568
607
 
569
608
  this.fireStepEnd(EncryptStep.Verify);
570
609
 
571
- return encryptedInputs as [...EncryptedItemInputs<T>];
610
+ return result;
572
611
  }
573
612
 
574
- private structsToHashPlusProof(inItems: [...EncryptedItemInputs<T>]): HashPlusProofResult<T> {
575
- let hashes: string[] = [];
576
- let proof: string = '';
577
-
578
- for (const item of inItems) {
579
- hashes.push('0x' + item.ctHash.toString(16).padStart(64, '0'));
580
- proof += item.signature;
581
- }
613
+ /**
614
+ * Converts a batch-verified result (per-item ctHash/ctType + one shared signature) into the
615
+ * tuple returned by `execute()`: per-item hashes in input order, followed by the single signature.
616
+ */
617
+ private buildBatchResult(result: VerifyBatchResult): HashPlusProofResult<T> {
618
+ const hashes = result.outputs.map((output) => '0x' + BigInt(output.ct_hash).toString(16).padStart(64, '0'));
582
619
 
583
- return [...hashes, proof] as unknown as HashPlusProofResult<T>;
620
+ return [...hashes, result.signature] as unknown as HashPlusProofResult<T>;
584
621
  }
585
622
 
586
623
  /**
@@ -602,17 +639,14 @@ export class EncryptInputsBuilder<T extends EncryptableItem[], HPP extends boole
602
639
  *
603
640
  * @returns The encrypted inputs.
604
641
  */
605
- async execute(): Promise<HPP extends true ? HashPlusProofResult<T> : [...EncryptedItemInputs<T>]> {
606
- type Result = HPP extends true ? HashPlusProofResult<T> : [...EncryptedItemInputs<T>];
607
-
608
- let items: [...EncryptedItemInputs<T>];
642
+ async execute(): Promise<HashPlusProofResult<T>> {
643
+ let result: VerifyBatchResult;
609
644
 
610
645
  // On hardhat chain, interact with MockZkVerifier contract instead of CoFHE
611
- if (this.chainId === hardhat.id) items = await this.mocksExecute();
646
+ if (this.chainId === hardhat.id) result = await this.mocksExecute();
612
647
  // On other chains, interact with CoFHE coprocessor
613
- else items = await this.productionExecute();
648
+ else result = await this.productionExecute();
614
649
 
615
- if (this.hpp) return this.structsToHashPlusProof(items) as unknown as Result;
616
- return items as unknown as Result;
650
+ return this.buildBatchResult(result);
617
651
  }
618
652
  }
@@ -41,14 +41,19 @@ export interface ZkProveWorkerResponse {
41
41
  error?: string;
42
42
  }
43
43
 
44
- export type VerifyResultRaw = {
44
+ export type VerifyBatchOutputRaw = {
45
45
  ct_hash: string;
46
+ ct_type: number;
47
+ };
48
+
49
+ export type VerifyBatchResultRaw = {
50
+ ciphertexts: VerifyBatchOutputRaw[];
46
51
  signature: string;
47
52
  recid: number;
48
53
  };
49
54
 
50
- export type VerifyResult = {
51
- ct_hash: string;
55
+ export type VerifyBatchResult = {
56
+ outputs: VerifyBatchOutputRaw[];
52
57
  signature: `0x${string}`;
53
58
  };
54
59
 
@@ -256,13 +261,14 @@ export const constructZkPoKMetadata = (accountAddr: string, securityZone: number
256
261
  return metadata;
257
262
  };
258
263
 
259
- export const zkVerify = async (
264
+ export const zkVerifyBatch = async (
260
265
  verifierUrl: string,
261
266
  serializedBytes: Uint8Array,
262
267
  address: string,
263
268
  securityZone: number,
264
- chainId: number
265
- ): Promise<VerifyResult[]> => {
269
+ chainId: number,
270
+ consumingContract: string
271
+ ): Promise<VerifyBatchResult> => {
266
272
  // Convert bytearray to hex string
267
273
  const packed_list = toHexString(serializedBytes);
268
274
 
@@ -274,13 +280,14 @@ export const zkVerify = async (
274
280
  account_addr: address,
275
281
  security_zone: sz_byte[0],
276
282
  chain_id: chainId,
283
+ contract_address: consumingContract,
277
284
  };
278
285
 
279
286
  const body = JSON.stringify(payload);
280
287
 
281
288
  // Send request to verification server
282
289
  try {
283
- const response = await fetch(`${verifierUrl}/verify`, {
290
+ const response = await fetch(`${verifierUrl}/verifyBatch`, {
284
291
  method: 'POST',
285
292
  headers: {
286
293
  'Content-Type': 'application/json',
@@ -293,34 +300,34 @@ export const zkVerify = async (
293
300
  const errorBody = await response.text();
294
301
  throw new CofheError({
295
302
  code: CofheErrorCode.ZkVerifyFailed,
296
- message: `HTTP error! ZK proof verification failed - ${errorBody}`,
303
+ message: `HTTP error! ZK batch proof verification failed - ${errorBody}`,
297
304
  });
298
305
  }
299
306
 
300
- const json = (await response.json()) as { status: string; data: VerifyResultRaw[]; error: string };
307
+ const json = (await response.json()) as { status: string; data: VerifyBatchResultRaw; error: string };
301
308
 
302
309
  if (json.status !== 'success') {
303
310
  throw new CofheError({
304
311
  code: CofheErrorCode.ZkVerifyFailed,
305
- message: `ZK proof verification response malformed - ${json.error}`,
312
+ message: `ZK batch proof verification response malformed - ${json.error}`,
306
313
  });
307
314
  }
308
315
 
309
- return json.data.map(({ ct_hash, signature, recid }) => {
310
- return {
311
- ct_hash,
312
- signature: concatSigRecid(signature, recid),
313
- };
314
- });
316
+ const { ciphertexts, signature, recid } = json.data;
317
+
318
+ return {
319
+ outputs: ciphertexts,
320
+ signature: concatSigRecid(signature, recid),
321
+ };
315
322
  } catch (e) {
316
323
  throw new CofheError({
317
324
  code: CofheErrorCode.ZkVerifyFailed,
318
- message: `ZK proof verification failed`,
325
+ message: `ZK batch proof verification failed`,
319
326
  cause: e instanceof Error ? e : undefined,
320
327
  });
321
328
  }
322
329
  };
323
330
 
324
- const concatSigRecid = (signature: string, recid: number): `0x${string}` => {
331
+ export const concatSigRecid = (signature: string, recid: number): `0x${string}` => {
325
332
  return `${signature}${(recid + 27).toString(16).padStart(2, '0')}` as `0x${string}`;
326
333
  };