@cofhe/sdk 0.4.0 → 0.5.1
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 +38 -0
- package/adapters/{ethers5.test.ts → test/ethers5.test.ts} +2 -2
- package/adapters/{ethers6.test.ts → test/ethers6.test.ts} +2 -2
- package/adapters/{hardhat.hh2.test.ts → test/hardhat.hh2.test.ts} +2 -2
- package/adapters/{index.test.ts → test/index.test.ts} +1 -1
- package/adapters/{wagmi.test.ts → test/wagmi.test.ts} +1 -1
- package/chains/{chains.test.ts → test/chains.test.ts} +1 -1
- package/core/client.ts +11 -1
- package/core/clientTypes.ts +3 -1
- package/core/consts.ts +9 -0
- package/core/decrypt/cofheMocksDecryptForTx.ts +14 -3
- package/core/decrypt/decryptForTxBuilder.ts +16 -2
- package/core/decrypt/decryptForViewBuilder.ts +14 -7
- package/core/decrypt/polling.ts +14 -0
- package/core/decrypt/tnDecryptV2.ts +250 -110
- package/core/decrypt/tnSealOutputV2.ts +245 -104
- package/core/decrypt/verifyDecryptResult.ts +65 -0
- package/core/encrypt/cofheMocksZkVerifySign.ts +6 -6
- package/core/encrypt/zkPackProveVerify.ts +10 -19
- package/core/fetchKeys.ts +0 -2
- package/core/index.ts +9 -1
- package/core/keyStore.ts +5 -2
- package/core/permits.ts +5 -0
- package/core/{client.test.ts → test/client.test.ts} +7 -7
- package/core/{config.test.ts → test/config.test.ts} +1 -1
- package/core/test/decrypt.test.ts +252 -0
- package/core/test/decryptBuilders.test.ts +390 -0
- package/core/{encrypt → test}/encryptInputsBuilder.test.ts +61 -6
- package/core/{fetchKeys.test.ts → test/fetchKeys.test.ts} +3 -3
- package/core/{keyStore.test.ts → test/keyStore.test.ts} +5 -3
- package/core/{permits.test.ts → test/permits.test.ts} +42 -1
- package/core/test/pollCallbacks.test.ts +563 -0
- package/core/types.ts +13 -0
- package/dist/chains.d.cts +2 -2
- package/dist/chains.d.ts +2 -2
- package/dist/chunk-4FP4V35O.js +13 -0
- package/dist/{chunk-NWDKXBIP.js → chunk-MRCKUMOS.js} +62 -22
- package/dist/{chunk-MXND5SVN.js → chunk-S7OKGLFD.js} +485 -207
- package/dist/{clientTypes-kkrRdawm.d.ts → clientTypes-BSbwairE.d.cts} +23 -6
- package/dist/{clientTypes-ACVWbrXL.d.cts → clientTypes-DDmcgZ0a.d.ts} +23 -6
- package/dist/core.cjs +561 -244
- package/dist/core.d.cts +24 -6
- package/dist/core.d.ts +24 -6
- package/dist/core.js +3 -2
- package/dist/node.cjs +566 -246
- package/dist/node.d.cts +3 -3
- package/dist/node.d.ts +3 -3
- package/dist/node.js +14 -7
- package/dist/{permit-MZ502UBl.d.cts → permit-DnVMDT5h.d.cts} +34 -4
- package/dist/{permit-MZ502UBl.d.ts → permit-DnVMDT5h.d.ts} +34 -4
- package/dist/permits.cjs +66 -29
- package/dist/permits.d.cts +18 -13
- package/dist/permits.d.ts +18 -13
- package/dist/permits.js +2 -1
- package/dist/web.cjs +604 -256
- package/dist/web.d.cts +8 -4
- package/dist/web.d.ts +8 -4
- package/dist/web.js +49 -14
- package/dist/zkProve.worker.cjs +72 -64
- package/dist/zkProve.worker.js +71 -64
- package/node/index.ts +13 -4
- package/node/test/client.test.ts +25 -0
- package/node/test/config.test.ts +16 -0
- package/node/test/inherited.test.ts +244 -0
- package/node/test/tfheinit.test.ts +56 -0
- package/package.json +24 -22
- package/permits/permit.ts +31 -5
- package/permits/sealing.ts +1 -1
- package/permits/{localstorage.test.ts → test/localstorage.test.ts} +2 -2
- package/permits/{permit.test.ts → test/permit.test.ts} +35 -1
- package/permits/{sealing.test.ts → test/sealing.test.ts} +1 -1
- package/permits/{store.test.ts → test/store.test.ts} +2 -2
- package/permits/{validation.test.ts → test/validation.test.ts} +82 -6
- package/permits/types.ts +1 -1
- package/permits/validation.ts +42 -2
- package/web/const.ts +2 -0
- package/web/index.ts +40 -11
- package/web/storage.ts +18 -3
- package/web/{client.web.test.ts → test/client.web.test.ts} +13 -1
- package/web/test/config.web.test.ts +16 -0
- package/web/test/inherited.web.test.ts +245 -0
- package/web/test/tfheinit.web.test.ts +62 -0
- package/web/{worker.config.web.test.ts → test/worker.config.web.test.ts} +1 -1
- package/web/{worker.output.web.test.ts → test/worker.output.web.test.ts} +1 -1
- package/web/{workerManager.test.ts → test/workerManager.test.ts} +1 -1
- package/web/{workerManager.web.test.ts → test/workerManager.web.test.ts} +1 -1
- package/web/zkProve.worker.ts +94 -84
- package/node/client.test.ts +0 -147
- package/node/config.test.ts +0 -68
- package/node/encryptInputs.test.ts +0 -155
- package/web/config.web.test.ts +0 -69
- package/web/encryptInputs.web.test.ts +0 -172
- package/web/worker.builder.web.test.ts +0 -148
- /package/dist/{types-YiAC4gig.d.cts → types-C07FK-cL.d.cts} +0 -0
- /package/dist/{types-YiAC4gig.d.ts → types-C07FK-cL.d.ts} +0 -0
|
@@ -1,16 +1,38 @@
|
|
|
1
1
|
import { type Permission, type EthEncryptedData } from '@/permits';
|
|
2
2
|
|
|
3
3
|
import { CofheError, CofheErrorCode } from '../error.js';
|
|
4
|
+
import { type DecryptPollCallbackFunction } from '../types.js';
|
|
5
|
+
import { computeMinuteRampPollIntervalMs } from './polling.js';
|
|
4
6
|
|
|
5
7
|
// Polling configuration
|
|
6
8
|
const POLL_INTERVAL_MS = 1000; // 1 second
|
|
7
|
-
const
|
|
9
|
+
const POLL_MAX_INTERVAL_MS = 10_000; // 10 seconds
|
|
10
|
+
const SEAL_OUTPUT_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes total across submit + poll
|
|
11
|
+
const SUBMIT_RETRY_INTERVAL_MS = 1000; // 1 second
|
|
8
12
|
|
|
9
13
|
// V2 API response types
|
|
10
14
|
type SealOutputSubmitResponse = {
|
|
11
|
-
request_id: string;
|
|
15
|
+
request_id: string | null;
|
|
16
|
+
status?: string;
|
|
17
|
+
is_succeed?: boolean;
|
|
18
|
+
sealed?: {
|
|
19
|
+
data: number[];
|
|
20
|
+
public_key: number[];
|
|
21
|
+
nonce: number[];
|
|
22
|
+
};
|
|
23
|
+
sealed_data?: number[];
|
|
24
|
+
ephemeral_public_key?: number[];
|
|
25
|
+
nonce?: number[];
|
|
26
|
+
signature?: string;
|
|
27
|
+
encryption_type?: number;
|
|
28
|
+
error_message?: string | null;
|
|
29
|
+
message?: string;
|
|
12
30
|
};
|
|
13
31
|
|
|
32
|
+
type SealOutputSubmitResult =
|
|
33
|
+
| { kind: 'request_id'; requestId: string }
|
|
34
|
+
| { kind: 'completed'; sealed: EthEncryptedData };
|
|
35
|
+
|
|
14
36
|
type SealOutputStatusResponse = {
|
|
15
37
|
request_id: string;
|
|
16
38
|
status: 'PROCESSING' | 'COMPLETED';
|
|
@@ -52,83 +74,192 @@ function convertSealedData(sealed: SealOutputStatusResponse['sealed']): EthEncry
|
|
|
52
74
|
};
|
|
53
75
|
}
|
|
54
76
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
thresholdNetworkUrl: string,
|
|
60
|
-
ctHash: bigint | string,
|
|
61
|
-
chainId: number,
|
|
62
|
-
permission: Permission
|
|
63
|
-
): Promise<string> {
|
|
64
|
-
const body = {
|
|
65
|
-
ct_tempkey: BigInt(ctHash).toString(16).padStart(64, '0'),
|
|
66
|
-
host_chain_id: chainId,
|
|
67
|
-
permit: permission,
|
|
68
|
-
};
|
|
77
|
+
function getSealedDataFromSubmitResponse(
|
|
78
|
+
value: SealOutputSubmitResponse
|
|
79
|
+
): SealOutputStatusResponse['sealed'] | undefined {
|
|
80
|
+
if (value.sealed) return value.sealed;
|
|
69
81
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
},
|
|
77
|
-
body: JSON.stringify(body),
|
|
78
|
-
});
|
|
79
|
-
} catch (e) {
|
|
80
|
-
throw new CofheError({
|
|
81
|
-
code: CofheErrorCode.SealOutputFailed,
|
|
82
|
-
message: `sealOutput request failed`,
|
|
83
|
-
hint: 'Ensure the threshold network URL is valid and reachable.',
|
|
84
|
-
cause: e instanceof Error ? e : undefined,
|
|
85
|
-
context: {
|
|
86
|
-
thresholdNetworkUrl,
|
|
87
|
-
body,
|
|
88
|
-
},
|
|
89
|
-
});
|
|
82
|
+
if (Array.isArray(value.sealed_data) && Array.isArray(value.ephemeral_public_key) && Array.isArray(value.nonce)) {
|
|
83
|
+
return {
|
|
84
|
+
data: value.sealed_data,
|
|
85
|
+
public_key: value.ephemeral_public_key,
|
|
86
|
+
nonce: value.nonce,
|
|
87
|
+
};
|
|
90
88
|
}
|
|
91
89
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
let errorMessage = `HTTP ${response.status}`;
|
|
95
|
-
try {
|
|
96
|
-
const errorBody = await response.json();
|
|
97
|
-
errorMessage = errorBody.error_message || errorBody.message || errorMessage;
|
|
98
|
-
} catch {
|
|
99
|
-
// Ignore JSON parse errors, use status text
|
|
100
|
-
errorMessage = response.statusText || errorMessage;
|
|
101
|
-
}
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
102
92
|
|
|
93
|
+
function parseCompletedSealOutputResponse(params: {
|
|
94
|
+
value: Pick<SealOutputStatusResponse, 'sealed' | 'error_message' | 'is_succeed'>;
|
|
95
|
+
thresholdNetworkUrl: string;
|
|
96
|
+
requestId?: string | null;
|
|
97
|
+
}): EthEncryptedData {
|
|
98
|
+
const { value, thresholdNetworkUrl, requestId } = params;
|
|
99
|
+
|
|
100
|
+
if (value.is_succeed === false) {
|
|
101
|
+
const errorMessage = value.error_message || 'Unknown error';
|
|
103
102
|
throw new CofheError({
|
|
104
103
|
code: CofheErrorCode.SealOutputFailed,
|
|
105
104
|
message: `sealOutput request failed: ${errorMessage}`,
|
|
106
|
-
hint: 'Check the threshold network URL and request parameters.',
|
|
107
105
|
context: {
|
|
108
106
|
thresholdNetworkUrl,
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
body,
|
|
107
|
+
requestId,
|
|
108
|
+
response: value,
|
|
112
109
|
},
|
|
113
110
|
});
|
|
114
111
|
}
|
|
115
112
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
} catch (e) {
|
|
113
|
+
const sealed = 'sealed' in value ? value.sealed : getSealedDataFromSubmitResponse(value as SealOutputSubmitResponse);
|
|
114
|
+
|
|
115
|
+
if (!sealed) {
|
|
120
116
|
throw new CofheError({
|
|
121
|
-
code: CofheErrorCode.
|
|
122
|
-
message: `
|
|
123
|
-
cause: e instanceof Error ? e : undefined,
|
|
117
|
+
code: CofheErrorCode.SealOutputReturnedNull,
|
|
118
|
+
message: `sealOutput request completed but returned no sealed data`,
|
|
124
119
|
context: {
|
|
125
120
|
thresholdNetworkUrl,
|
|
126
|
-
|
|
121
|
+
requestId,
|
|
122
|
+
response: value,
|
|
127
123
|
},
|
|
128
124
|
});
|
|
129
125
|
}
|
|
130
126
|
|
|
131
|
-
|
|
127
|
+
return convertSealedData(sealed);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Submits a sealoutput request to the v2 API and returns the request_id
|
|
132
|
+
*/
|
|
133
|
+
async function submitSealOutputRequest(
|
|
134
|
+
thresholdNetworkUrl: string,
|
|
135
|
+
ctHash: bigint | string,
|
|
136
|
+
chainId: number,
|
|
137
|
+
permission: Permission,
|
|
138
|
+
overallStartTime: number,
|
|
139
|
+
onPoll?: DecryptPollCallbackFunction
|
|
140
|
+
): Promise<SealOutputSubmitResult> {
|
|
141
|
+
const body = {
|
|
142
|
+
ct_tempkey: BigInt(ctHash).toString(16).padStart(64, '0'),
|
|
143
|
+
host_chain_id: chainId,
|
|
144
|
+
permit: permission,
|
|
145
|
+
};
|
|
146
|
+
let attemptIndex = 0;
|
|
147
|
+
|
|
148
|
+
for (;;) {
|
|
149
|
+
let response: Response;
|
|
150
|
+
try {
|
|
151
|
+
response = await fetch(`${thresholdNetworkUrl}/v2/sealoutput`, {
|
|
152
|
+
method: 'POST',
|
|
153
|
+
headers: {
|
|
154
|
+
'Content-Type': 'application/json',
|
|
155
|
+
},
|
|
156
|
+
body: JSON.stringify(body),
|
|
157
|
+
});
|
|
158
|
+
} catch (e) {
|
|
159
|
+
throw new CofheError({
|
|
160
|
+
code: CofheErrorCode.SealOutputFailed,
|
|
161
|
+
message: `sealOutput request failed`,
|
|
162
|
+
hint: 'Ensure the threshold network URL is valid and reachable.',
|
|
163
|
+
cause: e instanceof Error ? e : undefined,
|
|
164
|
+
context: {
|
|
165
|
+
thresholdNetworkUrl,
|
|
166
|
+
body,
|
|
167
|
+
attemptIndex,
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Handle non-200 status codes
|
|
173
|
+
if (!response.ok) {
|
|
174
|
+
let errorMessage = `HTTP ${response.status}`;
|
|
175
|
+
try {
|
|
176
|
+
const errorBody = await response.json();
|
|
177
|
+
|
|
178
|
+
errorMessage = errorBody.error_message || errorBody.message || errorMessage;
|
|
179
|
+
} catch {
|
|
180
|
+
errorMessage = response.statusText || errorMessage;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
throw new CofheError({
|
|
184
|
+
code: CofheErrorCode.SealOutputFailed,
|
|
185
|
+
message: `sealOutput request failed: ${errorMessage}`,
|
|
186
|
+
hint: 'Check the threshold network URL and request parameters.',
|
|
187
|
+
context: {
|
|
188
|
+
thresholdNetworkUrl,
|
|
189
|
+
status: response.status,
|
|
190
|
+
statusText: response.statusText,
|
|
191
|
+
body,
|
|
192
|
+
attemptIndex,
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
let submitResponse: SealOutputSubmitResponse | undefined;
|
|
198
|
+
if (response.status !== 204) {
|
|
199
|
+
try {
|
|
200
|
+
submitResponse = (await response.json()) as SealOutputSubmitResponse;
|
|
201
|
+
} catch (e) {
|
|
202
|
+
throw new CofheError({
|
|
203
|
+
code: CofheErrorCode.SealOutputFailed,
|
|
204
|
+
message: `Failed to parse sealOutput submit response`,
|
|
205
|
+
cause: e instanceof Error ? e : undefined,
|
|
206
|
+
context: {
|
|
207
|
+
thresholdNetworkUrl,
|
|
208
|
+
body,
|
|
209
|
+
attemptIndex,
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (getSealedDataFromSubmitResponse(submitResponse)) {
|
|
215
|
+
return {
|
|
216
|
+
kind: 'completed',
|
|
217
|
+
sealed: parseCompletedSealOutputResponse({
|
|
218
|
+
value: submitResponse,
|
|
219
|
+
thresholdNetworkUrl,
|
|
220
|
+
requestId: submitResponse.request_id,
|
|
221
|
+
}),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (submitResponse.request_id) {
|
|
226
|
+
return { kind: 'request_id', requestId: submitResponse.request_id };
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// 204 means backend is aware of ct hash but didn't calculate it yet
|
|
231
|
+
if (response.status === 204) {
|
|
232
|
+
const elapsedMs = Date.now() - overallStartTime;
|
|
233
|
+
if (elapsedMs > SEAL_OUTPUT_TIMEOUT_MS) {
|
|
234
|
+
throw new CofheError({
|
|
235
|
+
code: CofheErrorCode.SealOutputFailed,
|
|
236
|
+
message: `sealOutput submit retried without receiving request_id for ${SEAL_OUTPUT_TIMEOUT_MS}ms`,
|
|
237
|
+
hint: 'The ciphertext may still be propagating. Try again later.',
|
|
238
|
+
context: {
|
|
239
|
+
thresholdNetworkUrl,
|
|
240
|
+
body,
|
|
241
|
+
attemptIndex,
|
|
242
|
+
timeoutMs: SEAL_OUTPUT_TIMEOUT_MS,
|
|
243
|
+
submitResponse,
|
|
244
|
+
status: response.status,
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
onPoll?.({
|
|
250
|
+
operation: 'sealoutput',
|
|
251
|
+
requestId: '',
|
|
252
|
+
attemptIndex,
|
|
253
|
+
elapsedMs,
|
|
254
|
+
intervalMs: SUBMIT_RETRY_INTERVAL_MS,
|
|
255
|
+
timeoutMs: SEAL_OUTPUT_TIMEOUT_MS,
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
await new Promise((resolve) => setTimeout(resolve, SUBMIT_RETRY_INTERVAL_MS));
|
|
259
|
+
attemptIndex += 1;
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
|
|
132
263
|
throw new CofheError({
|
|
133
264
|
code: CofheErrorCode.SealOutputFailed,
|
|
134
265
|
message: `sealOutput submit response missing request_id`,
|
|
@@ -136,31 +267,49 @@ async function submitSealOutputRequest(
|
|
|
136
267
|
thresholdNetworkUrl,
|
|
137
268
|
body,
|
|
138
269
|
submitResponse,
|
|
270
|
+
attemptIndex,
|
|
139
271
|
},
|
|
140
272
|
});
|
|
141
273
|
}
|
|
142
|
-
|
|
143
|
-
return submitResponse.request_id;
|
|
144
274
|
}
|
|
145
275
|
|
|
146
276
|
/**
|
|
147
277
|
* Polls for the sealoutput status until completed or timeout
|
|
148
278
|
*/
|
|
149
|
-
async function pollSealOutputStatus(
|
|
150
|
-
|
|
279
|
+
async function pollSealOutputStatus(
|
|
280
|
+
thresholdNetworkUrl: string,
|
|
281
|
+
requestId: string,
|
|
282
|
+
overallStartTime: number,
|
|
283
|
+
onPoll?: DecryptPollCallbackFunction
|
|
284
|
+
): Promise<EthEncryptedData> {
|
|
285
|
+
let attemptIndex = 0;
|
|
151
286
|
let completed = false;
|
|
152
287
|
|
|
153
288
|
while (!completed) {
|
|
289
|
+
const elapsedMs = Date.now() - overallStartTime;
|
|
290
|
+
const intervalMs = computeMinuteRampPollIntervalMs(elapsedMs, {
|
|
291
|
+
minIntervalMs: POLL_INTERVAL_MS,
|
|
292
|
+
maxIntervalMs: POLL_MAX_INTERVAL_MS,
|
|
293
|
+
});
|
|
294
|
+
onPoll?.({
|
|
295
|
+
operation: 'sealoutput',
|
|
296
|
+
requestId,
|
|
297
|
+
attemptIndex,
|
|
298
|
+
elapsedMs,
|
|
299
|
+
intervalMs,
|
|
300
|
+
timeoutMs: SEAL_OUTPUT_TIMEOUT_MS,
|
|
301
|
+
});
|
|
302
|
+
|
|
154
303
|
// Check timeout
|
|
155
|
-
if (
|
|
304
|
+
if (elapsedMs > SEAL_OUTPUT_TIMEOUT_MS) {
|
|
156
305
|
throw new CofheError({
|
|
157
306
|
code: CofheErrorCode.SealOutputFailed,
|
|
158
|
-
message: `sealOutput polling timed out after ${
|
|
307
|
+
message: `sealOutput polling timed out after ${SEAL_OUTPUT_TIMEOUT_MS}ms`,
|
|
159
308
|
hint: 'The request may still be processing. Try again later.',
|
|
160
309
|
context: {
|
|
161
310
|
thresholdNetworkUrl,
|
|
162
311
|
requestId,
|
|
163
|
-
timeoutMs:
|
|
312
|
+
timeoutMs: SEAL_OUTPUT_TIMEOUT_MS,
|
|
164
313
|
},
|
|
165
314
|
});
|
|
166
315
|
}
|
|
@@ -238,39 +387,16 @@ async function pollSealOutputStatus(thresholdNetworkUrl: string, requestId: stri
|
|
|
238
387
|
|
|
239
388
|
// Check if completed
|
|
240
389
|
if (statusResponse.status === 'COMPLETED') {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
message: `sealOutput request failed: ${errorMessage}`,
|
|
247
|
-
context: {
|
|
248
|
-
thresholdNetworkUrl,
|
|
249
|
-
requestId,
|
|
250
|
-
statusResponse,
|
|
251
|
-
},
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// Check if sealed data exists
|
|
256
|
-
if (!statusResponse.sealed) {
|
|
257
|
-
throw new CofheError({
|
|
258
|
-
code: CofheErrorCode.SealOutputReturnedNull,
|
|
259
|
-
message: `sealOutput request completed but returned no sealed data`,
|
|
260
|
-
context: {
|
|
261
|
-
thresholdNetworkUrl,
|
|
262
|
-
requestId,
|
|
263
|
-
statusResponse,
|
|
264
|
-
},
|
|
265
|
-
});
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// Convert and return the sealed data
|
|
269
|
-
return convertSealedData(statusResponse.sealed);
|
|
390
|
+
return parseCompletedSealOutputResponse({
|
|
391
|
+
value: statusResponse,
|
|
392
|
+
thresholdNetworkUrl,
|
|
393
|
+
requestId,
|
|
394
|
+
});
|
|
270
395
|
}
|
|
271
396
|
|
|
272
397
|
// Still processing, wait before next poll
|
|
273
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
398
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
399
|
+
attemptIndex += 1;
|
|
274
400
|
}
|
|
275
401
|
|
|
276
402
|
// This should never be reached, but TypeScript requires it
|
|
@@ -284,15 +410,30 @@ async function pollSealOutputStatus(thresholdNetworkUrl: string, requestId: stri
|
|
|
284
410
|
});
|
|
285
411
|
}
|
|
286
412
|
|
|
287
|
-
export async function tnSealOutputV2(
|
|
288
|
-
ctHash: bigint | string
|
|
289
|
-
chainId: number
|
|
290
|
-
permission: Permission
|
|
291
|
-
thresholdNetworkUrl: string
|
|
292
|
-
|
|
413
|
+
export async function tnSealOutputV2(params: {
|
|
414
|
+
ctHash: bigint | string;
|
|
415
|
+
chainId: number;
|
|
416
|
+
permission: Permission;
|
|
417
|
+
thresholdNetworkUrl: string;
|
|
418
|
+
onPoll?: DecryptPollCallbackFunction;
|
|
419
|
+
}): Promise<EthEncryptedData> {
|
|
420
|
+
const { thresholdNetworkUrl, ctHash, chainId, permission, onPoll } = params;
|
|
421
|
+
const overallStartTime = Date.now();
|
|
422
|
+
|
|
293
423
|
// Step 1: Submit the request and get request_id
|
|
294
|
-
const
|
|
424
|
+
const submitResult = await submitSealOutputRequest(
|
|
425
|
+
thresholdNetworkUrl,
|
|
426
|
+
ctHash,
|
|
427
|
+
chainId,
|
|
428
|
+
permission,
|
|
429
|
+
overallStartTime,
|
|
430
|
+
onPoll
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
if (submitResult.kind === 'completed') {
|
|
434
|
+
return submitResult.sealed;
|
|
435
|
+
}
|
|
295
436
|
|
|
296
437
|
// Step 2: Poll for status until completed
|
|
297
|
-
return await pollSealOutputStatus(thresholdNetworkUrl, requestId);
|
|
438
|
+
return await pollSealOutputStatus(thresholdNetworkUrl, submitResult.requestId, overallStartTime, onPoll);
|
|
298
439
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import {
|
|
2
|
+
encodePacked,
|
|
3
|
+
isAddressEqual,
|
|
4
|
+
keccak256,
|
|
5
|
+
parseAbi,
|
|
6
|
+
recoverAddress,
|
|
7
|
+
zeroAddress,
|
|
8
|
+
type Address,
|
|
9
|
+
type Hex,
|
|
10
|
+
type PublicClient,
|
|
11
|
+
} from 'viem';
|
|
12
|
+
import { TASK_MANAGER_ADDRESS } from '../consts.js';
|
|
13
|
+
|
|
14
|
+
const decryptResultSignerAbi = parseAbi(['function decryptResultSigner() view returns (address)']);
|
|
15
|
+
const UINT_TYPE_MASK = 0x7fn;
|
|
16
|
+
const TYPE_BYTE_OFFSET = 8n;
|
|
17
|
+
|
|
18
|
+
const getEncryptionTypeFromCtHash = (ctHash: bigint) => Number((ctHash >> TYPE_BYTE_OFFSET) & UINT_TYPE_MASK);
|
|
19
|
+
|
|
20
|
+
const buildDecryptResultHash = (ctHash: bigint, cleartext: bigint, chainId: number) => {
|
|
21
|
+
const encryptionType = getEncryptionTypeFromCtHash(ctHash);
|
|
22
|
+
|
|
23
|
+
return keccak256(
|
|
24
|
+
encodePacked(['uint256', 'uint32', 'uint64', 'uint256'], [cleartext, encryptionType, BigInt(chainId), ctHash])
|
|
25
|
+
);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Verifies a decrypt result signature **locally** (no `ctHash`/plaintext sent over RPC).
|
|
30
|
+
*
|
|
31
|
+
* The recovered signer must equal the on-chain configured `decryptResultSigner`.
|
|
32
|
+
*
|
|
33
|
+
* This mirrors the TaskManager decrypt-result hash format:
|
|
34
|
+
* `keccak256(abi.encodePacked(result, encType, chainId, ctHash))`
|
|
35
|
+
*
|
|
36
|
+
* The only on-chain read performed is `TaskManager.decryptResultSigner()` (via `eth_call`).
|
|
37
|
+
*
|
|
38
|
+
* Works with both production and mock deployments.
|
|
39
|
+
*/
|
|
40
|
+
export async function verifyDecryptResult(
|
|
41
|
+
handle: bigint | string,
|
|
42
|
+
cleartext: bigint,
|
|
43
|
+
signature: Hex,
|
|
44
|
+
publicClient: PublicClient
|
|
45
|
+
): Promise<boolean> {
|
|
46
|
+
const chainId = publicClient.chain?.id ?? (await publicClient.getChainId());
|
|
47
|
+
const expectedSigner = await publicClient.readContract({
|
|
48
|
+
address: TASK_MANAGER_ADDRESS,
|
|
49
|
+
abi: decryptResultSignerAbi,
|
|
50
|
+
functionName: 'decryptResultSigner',
|
|
51
|
+
args: [],
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (isAddressEqual(expectedSigner, zeroAddress)) return true;
|
|
55
|
+
|
|
56
|
+
const ctHash = BigInt(handle);
|
|
57
|
+
const messageHash = buildDecryptResultHash(ctHash, cleartext, chainId);
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const recovered = await recoverAddress({ hash: messageHash, signature });
|
|
61
|
+
return isAddressEqual(recovered, expectedSigner);
|
|
62
|
+
} catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type EncryptableItem, FheTypes } from '../types.js';
|
|
2
|
-
import {
|
|
2
|
+
import { type VerifyResult } from './zkPackProveVerify.js';
|
|
3
3
|
import {
|
|
4
4
|
createWalletClient,
|
|
5
5
|
http,
|
|
@@ -14,7 +14,7 @@ import { MockZkVerifierAbi } from './MockZkVerifierAbi.js';
|
|
|
14
14
|
import { hardhat } from 'viem/chains';
|
|
15
15
|
import { CofheError, CofheErrorCode } from '../error.js';
|
|
16
16
|
import { privateKeyToAccount, sign } from 'viem/accounts';
|
|
17
|
-
import { MOCKS_ZK_VERIFIER_SIGNER_PRIVATE_KEY, MOCKS_ZK_VERIFIER_ADDRESS } from '../consts.js';
|
|
17
|
+
import { MOCKS_ZK_VERIFIER_SIGNER_PRIVATE_KEY, MOCKS_ZK_VERIFIER_ADDRESS, TFHE_RS_ZK_MAX_BITS } from '../consts.js';
|
|
18
18
|
|
|
19
19
|
type EncryptableItemWithCtHash = EncryptableItem & {
|
|
20
20
|
ctHash: bigint;
|
|
@@ -69,14 +69,14 @@ export async function cofheMocksCheckEncryptableBits(items: EncryptableItem[]):
|
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
|
-
if (totalBits >
|
|
72
|
+
if (totalBits > TFHE_RS_ZK_MAX_BITS) {
|
|
73
73
|
throw new CofheError({
|
|
74
74
|
code: CofheErrorCode.ZkPackFailed,
|
|
75
|
-
message: `Total bits ${totalBits} exceeds ${
|
|
76
|
-
hint: `Ensure that the total bits of the items to encrypt does not exceed ${
|
|
75
|
+
message: `Total bits ${totalBits} exceeds ${TFHE_RS_ZK_MAX_BITS}`,
|
|
76
|
+
hint: `Ensure that the total bits of the items to encrypt does not exceed ${TFHE_RS_ZK_MAX_BITS}`,
|
|
77
77
|
context: {
|
|
78
78
|
totalBits,
|
|
79
|
-
maxBits:
|
|
79
|
+
maxBits: TFHE_RS_ZK_MAX_BITS,
|
|
80
80
|
items,
|
|
81
81
|
},
|
|
82
82
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { TFHE_RS_ZK_MAX_BITS, TFHE_RS_SAFE_SERIALIZATION_SIZE_LIMIT } from '../consts';
|
|
2
|
+
import { CofheError, CofheErrorCode } from '../error';
|
|
3
|
+
import { type EncryptableItem, FheTypes } from '../types';
|
|
4
|
+
import { toBigIntOrThrow, validateBigIntInRange, toHexString, hexToBytes } from '../utils';
|
|
4
5
|
|
|
5
6
|
// ===== TYPES =====
|
|
6
7
|
|
|
@@ -52,23 +53,14 @@ export type VerifyResult = {
|
|
|
52
53
|
};
|
|
53
54
|
|
|
54
55
|
export type ZkProvenCiphertextList = {
|
|
55
|
-
|
|
56
|
+
safe_serialize(serialized_size_limit: bigint): Uint8Array;
|
|
56
57
|
};
|
|
57
58
|
|
|
58
59
|
export type ZkCompactPkeCrs = {
|
|
59
60
|
free(): void;
|
|
60
|
-
serialize(compress: boolean): Uint8Array;
|
|
61
61
|
safe_serialize(serialized_size_limit: bigint): Uint8Array;
|
|
62
62
|
};
|
|
63
63
|
|
|
64
|
-
export type ZkCompactPkeCrsConstructor = {
|
|
65
|
-
deserialize(buffer: Uint8Array): ZkCompactPkeCrs;
|
|
66
|
-
safe_deserialize(buffer: Uint8Array, serialized_size_limit: bigint): ZkCompactPkeCrs;
|
|
67
|
-
from_config(config: unknown, max_num_bits: number): ZkCompactPkeCrs;
|
|
68
|
-
deserialize_from_public_params(buffer: Uint8Array): ZkCompactPkeCrs;
|
|
69
|
-
safe_deserialize_from_public_params(buffer: Uint8Array, serialized_size_limit: bigint): ZkCompactPkeCrs;
|
|
70
|
-
};
|
|
71
|
-
|
|
72
64
|
export type ZkCiphertextListBuilder = {
|
|
73
65
|
push_boolean(data: boolean): void;
|
|
74
66
|
push_u8(data: number): void;
|
|
@@ -98,7 +90,6 @@ export const MAX_UINT64: bigint = 18446744073709551615n; // 2^64 - 1
|
|
|
98
90
|
export const MAX_UINT128: bigint = 340282366920938463463374607431768211455n; // 2^128 - 1
|
|
99
91
|
export const MAX_UINT256: bigint = 115792089237316195423570985008687907853269984665640564039457584007913129640319n; // 2^256 - 1
|
|
100
92
|
export const MAX_UINT160: bigint = 1461501637330902918203684832716283019655932542975n; // 2^160 - 1
|
|
101
|
-
export const MAX_ENCRYPTABLE_BITS: number = 2048;
|
|
102
93
|
|
|
103
94
|
// ===== CORE FUNCTIONS =====
|
|
104
95
|
|
|
@@ -174,14 +165,14 @@ export const zkPack = (items: EncryptableItem[], builder: ZkCiphertextListBuilde
|
|
|
174
165
|
}
|
|
175
166
|
}
|
|
176
167
|
|
|
177
|
-
if (totalBits >
|
|
168
|
+
if (totalBits > TFHE_RS_ZK_MAX_BITS) {
|
|
178
169
|
throw new CofheError({
|
|
179
170
|
code: CofheErrorCode.ZkPackFailed,
|
|
180
|
-
message: `Total bits ${totalBits} exceeds ${
|
|
181
|
-
hint: `Ensure that the total bits of the items to encrypt does not exceed ${
|
|
171
|
+
message: `Total bits ${totalBits} exceeds ${TFHE_RS_ZK_MAX_BITS}`,
|
|
172
|
+
hint: `Ensure that the total bits of the items to encrypt does not exceed ${TFHE_RS_ZK_MAX_BITS}`,
|
|
182
173
|
context: {
|
|
183
174
|
totalBits,
|
|
184
|
-
maxBits:
|
|
175
|
+
maxBits: TFHE_RS_ZK_MAX_BITS,
|
|
185
176
|
items,
|
|
186
177
|
},
|
|
187
178
|
});
|
|
@@ -231,7 +222,7 @@ export const zkProve = async (
|
|
|
231
222
|
1 // ZkComputeLoad.Verify
|
|
232
223
|
);
|
|
233
224
|
|
|
234
|
-
resolve(compactList.
|
|
225
|
+
resolve(compactList.safe_serialize(TFHE_RS_SAFE_SERIALIZATION_SIZE_LIMIT));
|
|
235
226
|
}, 0);
|
|
236
227
|
});
|
|
237
228
|
};
|
package/core/fetchKeys.ts
CHANGED
package/core/index.ts
CHANGED
|
@@ -43,6 +43,9 @@ export type {
|
|
|
43
43
|
FheTypeValue,
|
|
44
44
|
// Decryption types
|
|
45
45
|
UnsealedItem,
|
|
46
|
+
DecryptPollCallbackFunction,
|
|
47
|
+
DecryptPollCallbackContext,
|
|
48
|
+
DecryptEndpoint,
|
|
46
49
|
// Util types
|
|
47
50
|
EncryptStepCallbackFunction as EncryptSetStateFn,
|
|
48
51
|
EncryptStepCallbackContext,
|
|
@@ -85,7 +88,7 @@ export type {
|
|
|
85
88
|
} from './encrypt/zkPackProveVerify.js';
|
|
86
89
|
export { zkProveWithWorker } from './encrypt/zkPackProveVerify.js';
|
|
87
90
|
|
|
88
|
-
//
|
|
91
|
+
// Consts (contract addresses, serialized size limits)
|
|
89
92
|
export {
|
|
90
93
|
TASK_MANAGER_ADDRESS,
|
|
91
94
|
MOCKS_ZK_VERIFIER_ADDRESS,
|
|
@@ -94,7 +97,12 @@ export {
|
|
|
94
97
|
MOCKS_DECRYPT_RESULT_SIGNER_PRIVATE_KEY,
|
|
95
98
|
MOCKS_THRESHOLD_NETWORK_ADDRESS,
|
|
96
99
|
TEST_BED_ADDRESS,
|
|
100
|
+
TFHE_RS_ZK_MAX_BITS,
|
|
101
|
+
TFHE_RS_SAFE_SERIALIZATION_SIZE_LIMIT,
|
|
97
102
|
} from './consts.js';
|
|
98
103
|
|
|
104
|
+
// Decrypt result verification
|
|
105
|
+
export { verifyDecryptResult } from './decrypt/verifyDecryptResult.js';
|
|
106
|
+
|
|
99
107
|
// Utils
|
|
100
108
|
export { fheTypeToString } from './utils.js';
|
package/core/keyStore.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { createStore, type StoreApi } from 'zustand/vanilla';
|
|
|
2
2
|
import { persist, createJSONStorage } from 'zustand/middleware';
|
|
3
3
|
import { produce } from 'immer';
|
|
4
4
|
import { type IStorage } from './types.js';
|
|
5
|
+
import { TFHE_RS_KEY_VERSION } from './consts.js';
|
|
5
6
|
|
|
6
7
|
// Type definitions
|
|
7
8
|
type ChainRecord<T> = Record<string, T>;
|
|
@@ -23,6 +24,8 @@ export type KeysStorage = {
|
|
|
23
24
|
rehydrateKeysStore: () => Promise<void>;
|
|
24
25
|
};
|
|
25
26
|
|
|
27
|
+
const KEYSTORE_NAME = `cofhesdk-keys-v${TFHE_RS_KEY_VERSION}`;
|
|
28
|
+
|
|
26
29
|
function isValidPersistedState(state: unknown): state is KeysStore {
|
|
27
30
|
if (state && typeof state === 'object') {
|
|
28
31
|
if ('fhe' in state && 'crs' in state) {
|
|
@@ -94,7 +97,7 @@ export function createKeysStore(storage: IStorage | null): KeysStorage {
|
|
|
94
97
|
|
|
95
98
|
const clearKeysStorage = async () => {
|
|
96
99
|
if (storage) {
|
|
97
|
-
await storage.removeItem(
|
|
100
|
+
await storage.removeItem(KEYSTORE_NAME);
|
|
98
101
|
}
|
|
99
102
|
// If no storage, this is a no-op
|
|
100
103
|
};
|
|
@@ -125,7 +128,7 @@ function createStoreWithPersit(storage: IStorage) {
|
|
|
125
128
|
onRehydrateStorage: () => (_state?, _error?) => {
|
|
126
129
|
if (_error) throw new Error(`onRehydrateStorage: Error rehydrating keys store: ${_error}`);
|
|
127
130
|
},
|
|
128
|
-
name:
|
|
131
|
+
name: KEYSTORE_NAME,
|
|
129
132
|
storage: createJSONStorage(() => storage),
|
|
130
133
|
merge: (persistedState, currentState) => {
|
|
131
134
|
const persisted = isValidPersistedState(persistedState) ? persistedState : DEFAULT_KEYS_STORE;
|