@cofhe/sdk 1.1.0-alpha.0 → 1.1.0-alpha.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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @cofhe/sdk Changelog
2
2
 
3
+ ## 1.0.1
4
+
5
+ ### Patch Changes
6
+
7
+ - fcc819c: **Breaking:** `encryptInputs(...).execute()` now binds the consuming contract into the ZK proof itself, and the CoFHE verifier requires it. Upgrade to 1.0.1: 1.0.0 is deprecated because of an issue in ZK verification, and once the verifier is updated a proof from 1.0.0 fails with `ZK_VERIFY_FAILED`. No code change is needed beyond the upgrade; `setConsumingContract(...)` was already required.
8
+
9
+ `execute()` now throws `CofheError` with code `CONSUMING_CONTRACT_INVALID` when `setConsumingContract(...)` was given something that is not an address, before TFHE is initialised or any proving starts. The check is byte-level, 20 bytes of hex in any case, and does not validate the EIP-55 checksum, so every address 1.0.0 accepted still passes. Previously the bad address reached the verifier and failed there, after the proof was generated. `CofheErrorCode` gains `ConsumingContractInvalid`; an exhaustive `switch` over the enum needs the new arm.
10
+
3
11
  ## 1.0.0
4
12
 
5
13
  ### Major Changes
@@ -11,6 +11,7 @@ import { cofheMocksDecryptForTx } from './cofheMocksDecryptForTx';
11
11
  import { getPublicClientChainID, sleep } from '../utils';
12
12
  import { type DecryptPollCallbackFunction } from '../types';
13
13
  import { tnDecryptV2 } from './tnDecryptV2';
14
+ import { assertDecryptTimeoutMs } from './submitRetry';
14
15
 
15
16
  const DEFAULT_404_RETRY_TIMEOUT_MS = 10_000;
16
17
 
@@ -65,6 +66,7 @@ export class DecryptForTxBuilder extends BaseBuilder {
65
66
  private acpSelection: DecryptForTxACPSelection = 'unset';
66
67
  private pollCallback?: DecryptPollCallbackFunction;
67
68
  private retry404TimeoutMs = DEFAULT_404_RETRY_TIMEOUT_MS;
69
+ private timeoutMs?: number;
68
70
 
69
71
  constructor(params: DecryptForTxBuilderParams) {
70
72
  super({
@@ -153,6 +155,19 @@ export class DecryptForTxBuilder extends BaseBuilder {
153
155
  return this;
154
156
  }
155
157
 
158
+ /**
159
+ * The whole budget of this decrypt in milliseconds, submit retries and status polling together
160
+ * (5 minutes by default). Past it, `execute()` rejects with `DecryptFailed`; a request still
161
+ * processing on the threshold network is not cancelled.
162
+ */
163
+ setTimeout(this: DecryptForTxBuilderUnset, timeoutMs: number): DecryptForTxBuilderUnset;
164
+ setTimeout(this: DecryptForTxBuilderSelected, timeoutMs: number): DecryptForTxBuilderSelected;
165
+ setTimeout(timeoutMs: number): DecryptForTxBuilder {
166
+ assertDecryptTimeoutMs(timeoutMs, 'decryptForTx');
167
+ this.timeoutMs = timeoutMs;
168
+ return this;
169
+ }
170
+
156
171
  /**
157
172
  * Select "use acp" mode.
158
173
  *
@@ -326,6 +341,7 @@ export class DecryptForTxBuilder extends BaseBuilder {
326
341
  acp: wireAcp,
327
342
  thresholdNetworkUrl,
328
343
  retry404TimeoutMs: this.retry404TimeoutMs,
344
+ timeoutMs: this.timeoutMs,
329
345
  onPoll: this.pollCallback,
330
346
  });
331
347
 
@@ -13,6 +13,7 @@ import { cofheMocksDecryptForView } from './cofheMocksDecryptForView.js';
13
13
  import { tnSealOutputV2 } from './tnSealOutputV2.js';
14
14
  import { sleep } from '../utils.js';
15
15
  import { type DecryptPollCallbackFunction } from '../types.js';
16
+ import { assertDecryptTimeoutMs } from './submitRetry.js';
16
17
 
17
18
  const DEFAULT_404_RETRY_TIMEOUT_MS = 10_000;
18
19
 
@@ -51,6 +52,7 @@ export class DecryptForViewBuilder<U extends FheTypes> extends BaseBuilder {
51
52
  private acp?: ACP;
52
53
  private pollCallback?: DecryptPollCallbackFunction;
53
54
  private retry404TimeoutMs = DEFAULT_404_RETRY_TIMEOUT_MS;
55
+ private timeoutMs?: number;
54
56
 
55
57
  constructor(params: DecryptForViewBuilderParams<U>) {
56
58
  super({
@@ -134,6 +136,17 @@ export class DecryptForViewBuilder<U extends FheTypes> extends BaseBuilder {
134
136
  return this;
135
137
  }
136
138
 
139
+ /**
140
+ * The whole budget of this decrypt in milliseconds, submit retries and status polling together
141
+ * (5 minutes by default). Past it, `execute()` rejects with `SealOutputFailed`; a request still
142
+ * processing on the threshold network is not cancelled.
143
+ */
144
+ setTimeout(timeoutMs: number): DecryptForViewBuilder<U> {
145
+ assertDecryptTimeoutMs(timeoutMs, 'decryptForView');
146
+ this.timeoutMs = timeoutMs;
147
+ return this;
148
+ }
149
+
137
150
  /**
138
151
  * Select "use acp" mode (optional).
139
152
  *
@@ -295,6 +308,7 @@ export class DecryptForViewBuilder<U extends FheTypes> extends BaseBuilder {
295
308
  acp: wireAcp,
296
309
  thresholdNetworkUrl,
297
310
  retry404TimeoutMs: this.retry404TimeoutMs,
311
+ timeoutMs: this.timeoutMs,
298
312
  onPoll: this.pollCallback,
299
313
  });
300
314
  return ACPUtils.unseal(acp, sealed);
@@ -2,6 +2,22 @@ import { CofheError, CofheErrorCode } from '../error.js';
2
2
  import { mapApiErrorCodeToCofheErrorCode, parseApiErrorResponseBody } from './apiError.js';
3
3
 
4
4
  export const DEFAULT_404_RETRY_TIMEOUT_MS = 10_000;
5
+ /** The default budget of one decrypt, submit retries and status polling together. */
6
+ export const DEFAULT_DECRYPT_TIMEOUT_MS = 5 * 60 * 1000;
7
+
8
+ /**
9
+ * Validates a builder's `setTimeout(timeoutMs)` argument: the overall budget must be a positive,
10
+ * finite number of milliseconds (0 would time out before the first attempt).
11
+ */
12
+ export function assertDecryptTimeoutMs(timeoutMs: number, operationLabel: string): void {
13
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
14
+ throw new CofheError({
15
+ code: CofheErrorCode.InternalError,
16
+ message: `${operationLabel}: setTimeout(timeoutMs) expects a finite number greater than 0.`,
17
+ context: { timeoutMs },
18
+ });
19
+ }
20
+ }
5
21
 
6
22
  export type SubmitRetryableStatus = 204 | 404;
7
23
 
@@ -6,12 +6,16 @@ import { type DecryptPollCallbackFunction } from '../types';
6
6
  import { normalizeTnSignature, parseDecryptedBytesToBigInt } from './tnDecryptUtils';
7
7
  import { computeMinuteRampPollIntervalMs } from './polling.js';
8
8
  import { mapApiErrorCodeToCofheErrorCode, parseApiErrorResponseBody } from './apiError.js';
9
- import { classifySubmitResponse, normalize404RetryTimeoutMs, throwIfSubmitRetryTimedOut } from './submitRetry.js';
9
+ import {
10
+ DEFAULT_DECRYPT_TIMEOUT_MS,
11
+ classifySubmitResponse,
12
+ normalize404RetryTimeoutMs,
13
+ throwIfSubmitRetryTimedOut,
14
+ } from './submitRetry.js';
10
15
 
11
16
  // Polling configuration
12
17
  const POLL_INTERVAL_MS = 1000; // 1 second
13
18
  const POLL_MAX_INTERVAL_MS = 10_000; // 10 seconds
14
- const DECRYPT_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes total across submit + poll
15
19
  const SUBMIT_RETRY_INTERVAL_MS = 1000; // 1 second
16
20
 
17
21
  type DecryptSubmitResponseV2 = {
@@ -182,6 +186,7 @@ async function submitDecryptRequestV2(
182
186
  acp: ACPPublic | null,
183
187
  overallStartTime: number,
184
188
  retry404TimeoutMs: number,
189
+ timeoutMs: number,
185
190
  onPoll?: DecryptPollCallbackFunction
186
191
  ): Promise<DecryptSubmitResultV2> {
187
192
  const body: {
@@ -306,7 +311,7 @@ async function submitDecryptRequestV2(
306
311
  status: responseClassification.status,
307
312
  elapsedMs,
308
313
  retry404TimeoutMs,
309
- overallTimeoutMs: DECRYPT_TIMEOUT_MS,
314
+ overallTimeoutMs: timeoutMs,
310
315
  thresholdNetworkUrl,
311
316
  body,
312
317
  attemptIndex,
@@ -319,7 +324,7 @@ async function submitDecryptRequestV2(
319
324
  attemptIndex,
320
325
  elapsedMs,
321
326
  intervalMs: SUBMIT_RETRY_INTERVAL_MS,
322
- timeoutMs: DECRYPT_TIMEOUT_MS,
327
+ timeoutMs,
323
328
  });
324
329
 
325
330
  await new Promise((resolve) => setTimeout(resolve, SUBMIT_RETRY_INTERVAL_MS));
@@ -331,6 +336,7 @@ async function pollDecryptStatusV2(
331
336
  thresholdNetworkUrl: string,
332
337
  requestId: string,
333
338
  overallStartTime: number,
339
+ timeoutMs: number,
334
340
  onPoll?: DecryptPollCallbackFunction
335
341
  ): Promise<{ decryptedValue: bigint; signature: `0x${string}` }> {
336
342
  let attemptIndex = 0;
@@ -348,18 +354,18 @@ async function pollDecryptStatusV2(
348
354
  attemptIndex,
349
355
  elapsedMs,
350
356
  intervalMs,
351
- timeoutMs: DECRYPT_TIMEOUT_MS,
357
+ timeoutMs,
352
358
  });
353
359
 
354
- if (elapsedMs > DECRYPT_TIMEOUT_MS) {
360
+ if (elapsedMs > timeoutMs) {
355
361
  throw new CofheError({
356
362
  code: CofheErrorCode.DecryptFailed,
357
- message: `decrypt polling timed out after ${DECRYPT_TIMEOUT_MS}ms`,
363
+ message: `decrypt polling timed out after ${timeoutMs}ms`,
358
364
  hint: 'The request may still be processing. Try again later.',
359
365
  context: {
360
366
  thresholdNetworkUrl,
361
367
  requestId,
362
- timeoutMs: DECRYPT_TIMEOUT_MS,
368
+ timeoutMs,
363
369
  },
364
370
  });
365
371
  }
@@ -462,9 +468,12 @@ export async function tnDecryptV2(params: {
462
468
  acp: ACPPublic | null;
463
469
  thresholdNetworkUrl: string;
464
470
  retry404TimeoutMs?: number;
471
+ /** The whole budget, submit retries and status polling together; `DEFAULT_DECRYPT_TIMEOUT_MS` by default. */
472
+ timeoutMs?: number;
465
473
  onPoll?: DecryptPollCallbackFunction;
466
474
  }): Promise<{ decryptedValue: bigint; signature: `0x${string}` }> {
467
475
  const { thresholdNetworkUrl, ctHash, chainId, acp, retry404TimeoutMs, onPoll } = params;
476
+ const timeoutMs = params.timeoutMs ?? DEFAULT_DECRYPT_TIMEOUT_MS;
468
477
  const normalized404RetryTimeoutMs = normalize404RetryTimeoutMs({
469
478
  timeoutMs: retry404TimeoutMs,
470
479
  operationLabel: 'decrypt',
@@ -478,6 +487,7 @@ export async function tnDecryptV2(params: {
478
487
  acp,
479
488
  overallStartTime,
480
489
  normalized404RetryTimeoutMs,
490
+ timeoutMs,
481
491
  onPoll
482
492
  );
483
493
 
@@ -485,5 +495,5 @@ export async function tnDecryptV2(params: {
485
495
  return submitResult;
486
496
  }
487
497
 
488
- return await pollDecryptStatusV2(thresholdNetworkUrl, submitResult.requestId, overallStartTime, onPoll);
498
+ return await pollDecryptStatusV2(thresholdNetworkUrl, submitResult.requestId, overallStartTime, timeoutMs, onPoll);
489
499
  }
@@ -5,13 +5,17 @@ import { CofheError, CofheErrorCode } from '../error.js';
5
5
  import { type DecryptPollCallbackFunction } from '../types.js';
6
6
  import { computeMinuteRampPollIntervalMs } from './polling.js';
7
7
  import { mapApiErrorCodeToCofheErrorCode, parseApiErrorResponseBody } from './apiError.js';
8
- import { classifySubmitResponse, normalize404RetryTimeoutMs, throwIfSubmitRetryTimedOut } from './submitRetry.js';
8
+ import {
9
+ DEFAULT_DECRYPT_TIMEOUT_MS,
10
+ classifySubmitResponse,
11
+ normalize404RetryTimeoutMs,
12
+ throwIfSubmitRetryTimedOut,
13
+ } from './submitRetry.js';
9
14
  import {} from './tnDecryptUtils.js';
10
15
 
11
16
  // Polling configuration
12
17
  const POLL_INTERVAL_MS = 1000; // 1 second
13
18
  const POLL_MAX_INTERVAL_MS = 10_000; // 10 seconds
14
- const SEAL_OUTPUT_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes total across submit + poll
15
19
  const SUBMIT_RETRY_INTERVAL_MS = 1000; // 1 second
16
20
 
17
21
  // V2 API response types
@@ -141,6 +145,7 @@ async function submitSealOutputRequest(
141
145
  acp: ACPPublic,
142
146
  overallStartTime: number,
143
147
  retry404TimeoutMs: number,
148
+ timeoutMs: number,
144
149
  onPoll?: DecryptPollCallbackFunction
145
150
  ): Promise<SealOutputSubmitResult> {
146
151
  const body = {
@@ -254,7 +259,7 @@ async function submitSealOutputRequest(
254
259
  status: responseClassification.status,
255
260
  elapsedMs,
256
261
  retry404TimeoutMs,
257
- overallTimeoutMs: SEAL_OUTPUT_TIMEOUT_MS,
262
+ overallTimeoutMs: timeoutMs,
258
263
  thresholdNetworkUrl,
259
264
  body,
260
265
  attemptIndex,
@@ -267,7 +272,7 @@ async function submitSealOutputRequest(
267
272
  attemptIndex,
268
273
  elapsedMs,
269
274
  intervalMs: SUBMIT_RETRY_INTERVAL_MS,
270
- timeoutMs: SEAL_OUTPUT_TIMEOUT_MS,
275
+ timeoutMs,
271
276
  });
272
277
 
273
278
  await new Promise((resolve) => setTimeout(resolve, SUBMIT_RETRY_INTERVAL_MS));
@@ -282,6 +287,7 @@ async function pollSealOutputStatus(
282
287
  thresholdNetworkUrl: string,
283
288
  requestId: string,
284
289
  overallStartTime: number,
290
+ timeoutMs: number,
285
291
  onPoll?: DecryptPollCallbackFunction
286
292
  ): Promise<EthEncryptedData> {
287
293
  let attemptIndex = 0;
@@ -299,19 +305,19 @@ async function pollSealOutputStatus(
299
305
  attemptIndex,
300
306
  elapsedMs,
301
307
  intervalMs,
302
- timeoutMs: SEAL_OUTPUT_TIMEOUT_MS,
308
+ timeoutMs,
303
309
  });
304
310
 
305
311
  // Check timeout
306
- if (elapsedMs > SEAL_OUTPUT_TIMEOUT_MS) {
312
+ if (elapsedMs > timeoutMs) {
307
313
  throw new CofheError({
308
314
  code: CofheErrorCode.SealOutputFailed,
309
- message: `sealOutput polling timed out after ${SEAL_OUTPUT_TIMEOUT_MS}ms`,
315
+ message: `sealOutput polling timed out after ${timeoutMs}ms`,
310
316
  hint: 'The request may still be processing. Try again later.',
311
317
  context: {
312
318
  thresholdNetworkUrl,
313
319
  requestId,
314
- timeoutMs: SEAL_OUTPUT_TIMEOUT_MS,
320
+ timeoutMs,
315
321
  },
316
322
  });
317
323
  }
@@ -416,9 +422,12 @@ export async function tnSealOutputV2(params: {
416
422
  acp: ACPPublic;
417
423
  thresholdNetworkUrl: string;
418
424
  retry404TimeoutMs?: number;
425
+ /** The whole budget, submit retries and status polling together; `DEFAULT_DECRYPT_TIMEOUT_MS` by default. */
426
+ timeoutMs?: number;
419
427
  onPoll?: DecryptPollCallbackFunction;
420
428
  }): Promise<EthEncryptedData> {
421
429
  const { thresholdNetworkUrl, ctHash, chainId, acp, retry404TimeoutMs, onPoll } = params;
430
+ const timeoutMs = params.timeoutMs ?? DEFAULT_DECRYPT_TIMEOUT_MS;
422
431
  const normalized404RetryTimeoutMs = normalize404RetryTimeoutMs({
423
432
  timeoutMs: retry404TimeoutMs,
424
433
  operationLabel: 'sealOutput',
@@ -434,6 +443,7 @@ export async function tnSealOutputV2(params: {
434
443
  acp,
435
444
  overallStartTime,
436
445
  normalized404RetryTimeoutMs,
446
+ timeoutMs,
437
447
  onPoll
438
448
  );
439
449
 
@@ -442,5 +452,5 @@ export async function tnSealOutputV2(params: {
442
452
  }
443
453
 
444
454
  // Step 2: Poll for status until completed
445
- return await pollSealOutputStatus(thresholdNetworkUrl, submitResult.requestId, overallStartTime, onPoll);
455
+ return await pollSealOutputStatus(thresholdNetworkUrl, submitResult.requestId, overallStartTime, timeoutMs, onPoll);
446
456
  }
@@ -21,7 +21,7 @@ import { cofheMocksCheckEncryptableBits, cofheMocksZkVerifySign } from './cofheM
21
21
  import { hardhat } from 'viem/chains';
22
22
  import { fetchKeys, type FheKeyDeserializer } from '../fetchKeys.js';
23
23
  import { getZkVerifierUrlOrThrow } from '../config.js';
24
- import { type WalletClient } from 'viem';
24
+ import { type WalletClient, isAddress } from 'viem';
25
25
  import { sleep } from '../utils.js';
26
26
  import { BaseBuilder, type BaseBuilderParams } from '../baseBuilder.js';
27
27
  import { type KeysStorage } from '../keyStore.js';
@@ -256,19 +256,37 @@ export class EncryptInputsBuilder<T extends EncryptableItem[]> extends BaseBuild
256
256
  }
257
257
 
258
258
  /**
259
- * Asserts that this.consumingContract is populated
260
- * @throws {CofheError} If consumingContract is not set
259
+ * Asserts that this.consumingContract is populated and is an address.
260
+ * Runs before TFHE init, key fetching and proving, so a bad address costs
261
+ * nothing: the contract is bound into the proof, and a proof built for a
262
+ * non-address could only fail at the verifier, after the expensive step.
263
+ * @throws {CofheError} If consumingContract is not set or is not an address
261
264
  */
262
265
  private assertConsumingContract(): asserts this is this & { consumingContract: string } {
263
- if (this.consumingContract) return;
264
- throw new CofheError({
265
- code: CofheErrorCode.ConsumingContractUninitialized,
266
- message: 'Consuming contract is not set',
267
- hint: 'Use setConsumingContract(...) to set the contract that will consume the encrypted inputs.',
268
- context: {
269
- consumingContract: this.consumingContract,
270
- },
271
- });
266
+ if (!this.consumingContract) {
267
+ throw new CofheError({
268
+ code: CofheErrorCode.ConsumingContractUninitialized,
269
+ message: 'Consuming contract is not set',
270
+ hint: 'Use setConsumingContract(...) to set the contract that will consume the encrypted inputs.',
271
+ context: {
272
+ consumingContract: this.consumingContract,
273
+ },
274
+ });
275
+ }
276
+ // strict: false — 20 bytes of hex in any case, no EIP-55 checksum check. The
277
+ // verifier decodes the address case-insensitively and the chain compares bytes,
278
+ // so a mixed-case address with a wrong checksum works end to end and a patch
279
+ // release must keep accepting it.
280
+ if (!isAddress(this.consumingContract, { strict: false })) {
281
+ throw new CofheError({
282
+ code: CofheErrorCode.ConsumingContractInvalid,
283
+ message: 'Consuming contract is not an address',
284
+ hint: 'setConsumingContract(...) takes the 20-byte address of the contract that will consume the encrypted inputs.',
285
+ context: {
286
+ consumingContract: this.consumingContract,
287
+ },
288
+ });
289
+ }
272
290
  }
273
291
 
274
292
  /**
@@ -567,7 +585,7 @@ export class EncryptInputsBuilder<T extends EncryptableItem[]> extends BaseBuild
567
585
  this.fireStepStart(EncryptStep.Prove);
568
586
 
569
587
  // Construct metadata once (used by both worker and main thread paths)
570
- const metadata = constructZkPoKMetadata(this.account, this.securityZone, this.chainId);
588
+ const metadata = constructZkPoKMetadata(this.account, this.securityZone, this.chainId, this.consumingContract);
571
589
 
572
590
  let proof: Uint8Array | null = null;
573
591
  let usedWorker = false;
@@ -239,13 +239,24 @@ export const zkProve = async (
239
239
  };
240
240
 
241
241
  /**
242
- * Constructs the ZK Proof of Knowledge metadata for the proof
242
+ * Constructs the ZK Proof of Knowledge metadata for the proof:
243
+ * `securityZone (1) || account (20) || chainId (32, big-endian) || consumingContract (20)`.
244
+ *
245
+ * The verifier rebuilds the same bytes from the request fields before it checks the proof, so the
246
+ * proof is bound to the consuming contract itself, not only to the signature the verifier returns.
243
247
  * @internal - Used internally within the encrypt module
244
248
  */
245
- export const constructZkPoKMetadata = (accountAddr: string, securityZone: number, chainId: number): Uint8Array => {
246
- // Decode the account address from hex
249
+ export const constructZkPoKMetadata = (
250
+ accountAddr: string,
251
+ securityZone: number,
252
+ chainId: number,
253
+ consumingContract: string
254
+ ): Uint8Array => {
255
+ // Decode the account and contract addresses from hex
247
256
  const accountAddrNoPrefix = accountAddr.startsWith('0x') ? accountAddr.slice(2) : accountAddr;
248
257
  const accountBytes = hexToBytes(accountAddrNoPrefix);
258
+ const contractNoPrefix = consumingContract.startsWith('0x') ? consumingContract.slice(2) : consumingContract;
259
+ const contractBytes = hexToBytes(contractNoPrefix);
249
260
 
250
261
  // Encode chainId as 32 bytes (u256) in big-endian format
251
262
  const chainIdBytes = new Uint8Array(32);
@@ -259,10 +270,11 @@ export const constructZkPoKMetadata = (accountAddr: string, securityZone: number
259
270
  value = value >>> 8;
260
271
  }
261
272
 
262
- const metadata = new Uint8Array(1 + accountBytes.length + 32);
273
+ const metadata = new Uint8Array(1 + accountBytes.length + 32 + contractBytes.length);
263
274
  metadata[0] = securityZone;
264
275
  metadata.set(accountBytes, 1);
265
276
  metadata.set(chainIdBytes, 1 + accountBytes.length);
277
+ metadata.set(contractBytes, 1 + accountBytes.length + 32);
266
278
 
267
279
  return metadata;
268
280
  };
package/core/error.ts CHANGED
@@ -16,6 +16,7 @@ export enum CofheErrorCode {
16
16
  AccountUninitialized = 'ACCOUNT_UNINITIALIZED',
17
17
  ChainIdUninitialized = 'CHAIN_ID_UNINITIALIZED',
18
18
  ConsumingContractUninitialized = 'CONSUMING_CONTRACT_UNINITIALIZED',
19
+ ConsumingContractInvalid = 'CONSUMING_CONTRACT_INVALID',
19
20
  SealOutputFailed = 'SEAL_OUTPUT_FAILED',
20
21
  SealOutputReturnedNull = 'SEAL_OUTPUT_RETURNED_NULL',
21
22
  InvalidUtype = 'INVALID_UTYPE',
@@ -224,6 +224,21 @@ describe('DecryptForTxBuilder', () => {
224
224
 
225
225
  expect(() => builder.set404RetryTimeout(-1)).toThrow('set404RetryTimeout(timeoutMs) expects');
226
226
  });
227
+
228
+ it('should allow configuring the overall timeout', () => {
229
+ const builder = createTxBuilder();
230
+ const result = builder.setTimeout(20_000);
231
+
232
+ expect(result).toBe(builder);
233
+ expect((builder as any).timeoutMs).toBe(20_000);
234
+ });
235
+
236
+ it('should throw for an overall timeout that is not a positive number', () => {
237
+ const builder = createTxBuilder();
238
+
239
+ expect(() => builder.setTimeout(0)).toThrow('setTimeout(timeoutMs) expects');
240
+ expect(() => builder.setTimeout(Number.NaN)).toThrow('setTimeout(timeoutMs) expects');
241
+ });
227
242
  });
228
243
 
229
244
  // --- execute error paths ---
@@ -367,6 +382,21 @@ describe('DecryptForViewBuilder', () => {
367
382
 
368
383
  expect(() => builder.set404RetryTimeout(-1)).toThrow('set404RetryTimeout(timeoutMs) expects');
369
384
  });
385
+
386
+ it('should allow configuring the overall timeout', () => {
387
+ const builder = createViewBuilder(FheTypes.Uint32);
388
+ const result = builder.setTimeout(20_000);
389
+
390
+ expect(result).toBe(builder);
391
+ expect((builder as any).timeoutMs).toBe(20_000);
392
+ });
393
+
394
+ it('should throw for an overall timeout that is not a positive number', () => {
395
+ const builder = createViewBuilder(FheTypes.Uint32);
396
+
397
+ expect(() => builder.setTimeout(0)).toThrow('setTimeout(timeoutMs) expects');
398
+ expect(() => builder.setTimeout(-5)).toThrow('setTimeout(timeoutMs) expects');
399
+ });
370
400
  });
371
401
 
372
402
  // --- execute error paths ---
@@ -503,6 +533,16 @@ describe('execute – expired acp (regression, issue #272)', () => {
503
533
  expect(result.decryptedValue).toBe(7n);
504
534
  expect(tnDecryptV2).toHaveBeenCalledOnce();
505
535
  });
536
+
537
+ it('passes the overall timeout to the threshold network call', async () => {
538
+ vi.mocked(tnDecryptV2).mockResolvedValue({ decryptedValue: 7n, signature: '0x1234' });
539
+ const acp = createSignedSelfACP(now() + 3600);
540
+ seedACP(acp);
541
+
542
+ await selectTxACP('active acp', acp).setTimeout(20_000).execute();
543
+
544
+ expect(tnDecryptV2).toHaveBeenCalledWith(expect.objectContaining({ timeoutMs: 20_000 }));
545
+ });
506
546
  });
507
547
 
508
548
  describe('decryptForView', () => {
@@ -522,5 +562,16 @@ describe('execute – expired acp (regression, issue #272)', () => {
522
562
  await expect(selectViewACP('active acp', acp).execute()).rejects.toThrow('stubbed threshold network');
523
563
  expect(tnSealOutputV2).toHaveBeenCalledOnce();
524
564
  });
565
+
566
+ it('passes the overall timeout to the threshold network call', async () => {
567
+ vi.mocked(tnSealOutputV2).mockRejectedValue(new Error('stubbed threshold network'));
568
+ const acp = createSignedSelfACP(now() + 3600);
569
+ seedACP(acp);
570
+
571
+ await expect(selectViewACP('active acp', acp).setTimeout(20_000).execute()).rejects.toThrow(
572
+ 'stubbed threshold network'
573
+ );
574
+ expect(tnSealOutputV2).toHaveBeenCalledWith(expect.objectContaining({ timeoutMs: 20_000 }));
575
+ });
525
576
  });
526
577
  });
@@ -10,7 +10,7 @@ import {
10
10
  } from '../types.js';
11
11
  import { CofheError, CofheErrorCode } from '../error.js';
12
12
  import { fromHexString, toHexString } from '../utils.js';
13
- import { type PublicClient, createPublicClient, http, type WalletClient, createWalletClient } from 'viem';
13
+ import { type PublicClient, createPublicClient, http, type WalletClient, createWalletClient, isAddress } from 'viem';
14
14
  import { privateKeyToAccount } from 'viem/accounts';
15
15
  import { arbitrumSepolia } from 'viem/chains';
16
16
  import { type CofheConfig, createCofheConfigBase } from '../config.js';
@@ -53,9 +53,9 @@ const unpackMetadata = (metadata: string) => {
53
53
 
54
54
  export const deconstructZkPoKMetadata = (
55
55
  metadata: Uint8Array
56
- ): { accountAddr: string; securityZone: number; chainId: number } => {
57
- if (metadata.length < 53) {
58
- // 1 + 20 + 32 = 53 bytes minimum
56
+ ): { accountAddr: string; securityZone: number; chainId: number; contractAddr: string } => {
57
+ if (metadata.length < 73) {
58
+ // 1 + 20 + 32 + 20 = 73 bytes minimum
59
59
  throw new CofheError({
60
60
  code: CofheErrorCode.InternalError,
61
61
  message: 'Invalid metadata: insufficient length',
@@ -78,10 +78,14 @@ export const deconstructZkPoKMetadata = (
78
78
  chainId = (chainId << 8) | chainIdBytes[i];
79
79
  }
80
80
 
81
+ // Extract the consuming contract (last 20 bytes)
82
+ const contractAddr = '0x' + toHexString(metadata.slice(53, 73));
83
+
81
84
  return {
82
85
  accountAddr,
83
86
  securityZone,
84
87
  chainId,
88
+ contractAddr,
85
89
  };
86
90
  };
87
91
 
@@ -445,6 +449,38 @@ describe('EncryptInputsBuilder', () => {
445
449
  expect((error as CofheError).code).toBe(CofheErrorCode.ConsumingContractUninitialized);
446
450
  }
447
451
  });
452
+
453
+ it('accepts a mixed-case consuming contract whose checksum is wrong', async () => {
454
+ // The verifier decodes the address case-insensitively and the chain compares
455
+ // 20 bytes, so a 1.0.0 dApp with such an address must keep working.
456
+ const wrongChecksum = '0xBEEFbeefBEEFbeefBEEFbeefBEEFbeefBEEFbeef';
457
+ expect(isAddress(wrongChecksum)).toBe(false);
458
+ expect(isAddress(wrongChecksum, { strict: false })).toBe(true);
459
+
460
+ const result = await new EncryptInputsBuilder(createDefaultParams())
461
+ .setConsumingContract(wrongChecksum)
462
+ .execute();
463
+
464
+ expect(Array.isArray(result)).toBe(true);
465
+ });
466
+
467
+ it('rejects a consuming contract that is not an address before any encrypt step runs', async () => {
468
+ for (const notAnAddress of ['0x0', 'not-an-address', '0xbeefbeefbeefbeefbeefbeefbeefbeefbeefbee']) {
469
+ const steps = vi.fn();
470
+ const invalid = new EncryptInputsBuilder(createDefaultParams())
471
+ .setConsumingContract(notAnAddress)
472
+ .onStep(steps);
473
+ try {
474
+ await invalid.execute();
475
+ expect.unreachable(`execute() should have thrown for ${notAnAddress}`);
476
+ } catch (error) {
477
+ expect(error, notAnAddress).toBeInstanceOf(CofheError);
478
+ expect((error as CofheError).code, notAnAddress).toBe(CofheErrorCode.ConsumingContractInvalid);
479
+ }
480
+ // Rejected before initTfhe, so no step (and no proving) ever started.
481
+ expect(steps, notAnAddress).not.toHaveBeenCalled();
482
+ }
483
+ });
448
484
  });
449
485
 
450
486
  describe('zkVerifierUrl', () => {
@@ -595,6 +631,22 @@ describe('EncryptInputsBuilder', () => {
595
631
  expect(encryptedMetadata.chainId).toBe(defaultChainId);
596
632
  });
597
633
 
634
+ it('binds the consuming contract into the proof metadata', async () => {
635
+ mockFetch.mockClear();
636
+
637
+ await builder.execute();
638
+
639
+ const verifyCall = mockFetch.mock.calls.find(([url]) => url === `${MockZkVerifierUrl}/verifyBatch`);
640
+ expect(verifyCall).toBeDefined();
641
+ const body = JSON.parse(verifyCall![1].body as string);
642
+ const { metadata } = parseWithBigInt(new TextDecoder().decode(fromHexString(body.packed_list)));
643
+ const embedded = deconstructZkPoKMetadata(Uint8Array.from(Object.values(metadata) as number[]));
644
+ expect(embedded.accountAddr).toBe(defaultSender);
645
+ expect(embedded.securityZone).toBe(0);
646
+ expect(embedded.chainId).toBe(defaultChainId);
647
+ expect(embedded.contractAddr).toBe(defaultConsumingContract);
648
+ });
649
+
598
650
  it('should use overridden account when set', async () => {
599
651
  const overriddenSender = '0x5555555555555555555555555555555555555555';
600
652
  builder.setAccount(overriddenSender);
@@ -0,0 +1,27 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { constructZkPoKMetadata } from '../encrypt/zkPackProveVerify.js';
3
+ import { toHexString } from '../utils.js';
4
+
5
+ describe('constructZkPoKMetadata', () => {
6
+ const account = '0x1234567890123456789012345678901234567890';
7
+ const contract = '0xbeefbeefbeefbeefbeefbeefbeefbeefbeefbeef';
8
+
9
+ it('lays out security zone, account, chain id and the consuming contract', () => {
10
+ const metadata = constructZkPoKMetadata(account, 3, 0x010203, contract);
11
+
12
+ expect(metadata.length).toBe(1 + 20 + 32 + 20);
13
+ expect(metadata[0]).toBe(3);
14
+ expect('0x' + toHexString(metadata.slice(1, 21))).toBe(account);
15
+ const chainId = metadata.slice(21, 53);
16
+ expect(Array.from(chainId.slice(0, 29))).toEqual(new Array(29).fill(0));
17
+ expect(Array.from(chainId.slice(29))).toEqual([1, 2, 3]);
18
+ expect('0x' + toHexString(metadata.slice(53))).toBe(contract);
19
+ });
20
+
21
+ it('accepts the consuming contract with or without the 0x prefix', () => {
22
+ const withPrefix = constructZkPoKMetadata(account, 0, 1, contract);
23
+ const withoutPrefix = constructZkPoKMetadata(account, 0, 1, contract.slice(2));
24
+
25
+ expect(Array.from(withoutPrefix)).toEqual(Array.from(withPrefix));
26
+ });
27
+ });
package/dist/acps.js CHANGED
@@ -1,2 +1,2 @@
1
- export { ACPScope, ACPUtils, ACP_REVOKER_ABI, ACP_SHARE_REGISTRY_ABI, ACP_SHARE_TUPLE_ABI, ACP_STORE_DEFAULTS, ERC7984_ABI, GenerateSealingKey, ImportACPOptionsValidator, ImportACPValidator, LAYOUT_VERSION as SHARE_METADATA_VERSION, SelfACPOptionsValidator, SelfACPValidator, SharingACPOptionsValidator, SharingACPValidator, SignatureTypes, SignatureUtils, UNLABELLED, ValidationUtils, _acpStore, acpStore, addressNotZeroSchema, addressSchema, bytesNotEmptySchema, bytesSchema, clearAclServedAddresses, clearStaleStore, computeShareId, contractsSchema, decodeShareAppKey, decodeShareMetadata, decodeShareMetadataFull, describeShareLabels, describeShareMetadata, encodeShareMetadata, eventLogLabel, getACP, getACPs, getAclAddress, getAclServedAddresses, getActiveACP, getActiveACPHash, getSignatureTypesAndMessage, handlesSchema, readPostedShares, readShare, readSharesFor, removeACP, removeActiveACPHash, resetStore, seal, setACP, setActiveACPHash, shareAppKey, shareIdOfChainShare, toChainShare, unsealWithPrivateKey, validateImportACP, validateImportACPOptions, validateSelfACP, validateSelfACPOptions, validateSharingACP, validateSharingACPOptions, verifyShareLabel, verifyShareLabels, viewFunctionLabel } from './chunk-3L2IVPMI.js';
2
- import './chunk-J23FYAI2.js';
1
+ export { ACPScope, ACPUtils, ACP_REVOKER_ABI, ACP_SHARE_REGISTRY_ABI, ACP_SHARE_TUPLE_ABI, ACP_STORE_DEFAULTS, ERC7984_ABI, GenerateSealingKey, ImportACPOptionsValidator, ImportACPValidator, LAYOUT_VERSION as SHARE_METADATA_VERSION, SelfACPOptionsValidator, SelfACPValidator, SharingACPOptionsValidator, SharingACPValidator, SignatureTypes, SignatureUtils, UNLABELLED, ValidationUtils, _acpStore, acpStore, addressNotZeroSchema, addressSchema, bytesNotEmptySchema, bytesSchema, clearAclServedAddresses, clearStaleStore, computeShareId, contractsSchema, decodeShareAppKey, decodeShareMetadata, decodeShareMetadataFull, describeShareLabels, describeShareMetadata, encodeShareMetadata, eventLogLabel, getACP, getACPs, getAclAddress, getAclServedAddresses, getActiveACP, getActiveACPHash, getSignatureTypesAndMessage, handlesSchema, readPostedShares, readShare, readSharesFor, removeACP, removeActiveACPHash, resetStore, seal, setACP, setActiveACPHash, shareAppKey, shareIdOfChainShare, toChainShare, unsealWithPrivateKey, validateImportACP, validateImportACPOptions, validateSelfACP, validateSelfACPOptions, validateSharingACP, validateSharingACPOptions, verifyShareLabel, verifyShareLabels, viewFunctionLabel } from './chunk-WAA2U7CT.js';
2
+ import './chunk-OC5TGQGG.js';
@@ -1,4 +1,4 @@
1
- import { CofheError } from './chunk-J23FYAI2.js';
1
+ import { CofheError } from './chunk-OC5TGQGG.js';
2
2
  import 'viem';
3
3
 
4
4
  // core/types.ts