@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 +8 -0
- package/core/decrypt/decryptForTxBuilder.ts +16 -0
- package/core/decrypt/decryptForViewBuilder.ts +14 -0
- package/core/decrypt/submitRetry.ts +16 -0
- package/core/decrypt/tnDecryptV2.ts +19 -9
- package/core/decrypt/tnSealOutputV2.ts +19 -9
- package/core/encrypt/encryptInputsBuilder.ts +31 -13
- package/core/encrypt/zkPackProveVerify.ts +16 -4
- package/core/error.ts +1 -0
- package/core/test/decryptBuilders.test.ts +51 -0
- package/core/test/encryptInputsBuilder.test.ts +56 -4
- package/core/test/zkPoKMetadata.test.ts +27 -0
- package/dist/acps.js +2 -2
- package/dist/{chunk-WJGTPW7K.js → chunk-3SD2UCRE.js} +1 -1
- package/dist/{chunk-2EO4JSR7.js → chunk-KHM6LYLU.js} +86 -39
- package/dist/{chunk-J23FYAI2.js → chunk-OC5TGQGG.js} +1 -0
- package/dist/{chunk-3L2IVPMI.js → chunk-WAA2U7CT.js} +1 -1
- package/dist/{clientTypes-RXwLVWou.d.cts → clientTypes-BCAMwrqk.d.cts} +20 -2
- package/dist/{clientTypes-CnslFzxB.d.ts → clientTypes-LvslG7o0.d.ts} +20 -2
- package/dist/core.cjs +83 -35
- package/dist/core.d.cts +3 -2
- package/dist/core.d.ts +3 -2
- package/dist/core.js +4 -4
- package/dist/node.cjs +82 -35
- package/dist/node.d.cts +1 -1
- package/dist/node.d.ts +1 -1
- package/dist/node.js +4 -4
- package/dist/web.cjs +82 -35
- package/dist/web.d.cts +1 -1
- package/dist/web.d.ts +1 -1
- package/dist/web.js +4 -4
- package/dist/zkProve.worker.js +2 -2
- package/package.json +1 -1
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 {
|
|
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:
|
|
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
|
|
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
|
|
357
|
+
timeoutMs,
|
|
352
358
|
});
|
|
353
359
|
|
|
354
|
-
if (elapsedMs >
|
|
360
|
+
if (elapsedMs > timeoutMs) {
|
|
355
361
|
throw new CofheError({
|
|
356
362
|
code: CofheErrorCode.DecryptFailed,
|
|
357
|
-
message: `decrypt polling timed out after ${
|
|
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
|
|
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 {
|
|
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:
|
|
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
|
|
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
|
|
308
|
+
timeoutMs,
|
|
303
309
|
});
|
|
304
310
|
|
|
305
311
|
// Check timeout
|
|
306
|
-
if (elapsedMs >
|
|
312
|
+
if (elapsedMs > timeoutMs) {
|
|
307
313
|
throw new CofheError({
|
|
308
314
|
code: CofheErrorCode.SealOutputFailed,
|
|
309
|
-
message: `sealOutput polling timed out after ${
|
|
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
|
|
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
|
-
*
|
|
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)
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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 = (
|
|
246
|
-
|
|
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 <
|
|
58
|
-
// 1 + 20 + 32 =
|
|
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-
|
|
2
|
-
import './chunk-
|
|
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';
|