@zama-fhe/relayer-sdk 0.1.0-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/LICENSE +28 -0
- package/README.md +55 -0
- package/bin/relayer.js +61 -0
- package/bin/utils.js +14 -0
- package/bundle/fhevm.js +40713 -0
- package/bundle/fhevm.umd.cjs +24 -0
- package/bundle/kms_lib_bg.wasm +0 -0
- package/bundle/tfhe_bg.wasm +0 -0
- package/bundle/workerHelpers.js +2 -0
- package/bundle.d.ts +1 -0
- package/bundle.js +12 -0
- package/lib/config.d.ts +30 -0
- package/lib/index.d.ts +29 -0
- package/lib/init.d.ts +7 -0
- package/lib/kms_lib_bg.wasm +0 -0
- package/lib/node.cjs +1152 -0
- package/lib/node.d.ts +2 -0
- package/lib/relayer/decryptUtils.d.ts +2 -0
- package/lib/relayer/handles.d.ts +4 -0
- package/lib/relayer/network.d.ts +31 -0
- package/lib/relayer/network.test.d.ts +1 -0
- package/lib/relayer/publicDecrypt.d.ts +3 -0
- package/lib/relayer/publicDecrypt.test.d.ts +1 -0
- package/lib/relayer/sendEncryption.d.ts +41 -0
- package/lib/relayer/sendEncryption.test.d.ts +1 -0
- package/lib/relayer/userDecrypt.d.ts +11 -0
- package/lib/relayer/userDecrypt.test.d.ts +1 -0
- package/lib/sdk/encrypt.d.ts +34 -0
- package/lib/sdk/encrypt.test.d.ts +1 -0
- package/lib/sdk/encryptionTypes.d.ts +13 -0
- package/lib/sdk/keypair.d.ts +34 -0
- package/lib/sdk/keypair.test.d.ts +1 -0
- package/lib/test/index.d.ts +10 -0
- package/lib/tfhe.d.ts +7 -0
- package/lib/tfhe_bg.wasm +0 -0
- package/lib/utils.d.ts +9 -0
- package/lib/web.d.ts +2 -0
- package/lib/web.js +27305 -0
- package/lib/workerHelpers.js +24774 -0
- package/node.d.ts +1 -0
- package/node.js +1 -0
- package/package.json +99 -0
- package/web.d.ts +1 -0
- package/web.js +1 -0
package/lib/node.cjs
ADDED
|
@@ -0,0 +1,1152 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var ethers = require('ethers');
|
|
4
|
+
var nodeTfhe = require('node-tfhe');
|
|
5
|
+
var nodeTkms = require('node-tkms');
|
|
6
|
+
var createHash = require('keccak');
|
|
7
|
+
var fetchRetry = require('fetch-retry');
|
|
8
|
+
|
|
9
|
+
const SERIALIZED_SIZE_LIMIT_CIPHERTEXT = BigInt(1024 * 1024 * 512);
|
|
10
|
+
const SERIALIZED_SIZE_LIMIT_PK = BigInt(1024 * 1024 * 512);
|
|
11
|
+
const SERIALIZED_SIZE_LIMIT_CRS = BigInt(1024 * 1024 * 512);
|
|
12
|
+
const cleanURL = (url) => {
|
|
13
|
+
if (!url)
|
|
14
|
+
return '';
|
|
15
|
+
return url.endsWith('/') ? url.slice(0, -1) : url;
|
|
16
|
+
};
|
|
17
|
+
const numberToHex = (num) => {
|
|
18
|
+
let hex = num.toString(16);
|
|
19
|
+
return hex.length % 2 ? '0' + hex : hex;
|
|
20
|
+
};
|
|
21
|
+
const fromHexString = (hexString) => {
|
|
22
|
+
const arr = hexString.replace(/^(0x)/, '').match(/.{1,2}/g);
|
|
23
|
+
if (!arr)
|
|
24
|
+
return new Uint8Array();
|
|
25
|
+
return Uint8Array.from(arr.map((byte) => parseInt(byte, 16)));
|
|
26
|
+
};
|
|
27
|
+
const toHexString = (bytes, with0x = false) => `${with0x ? '0x' : ''}${bytes.reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), '')}`;
|
|
28
|
+
const bytesToBigInt = function (byteArray) {
|
|
29
|
+
if (!byteArray || byteArray?.length === 0) {
|
|
30
|
+
return BigInt(0);
|
|
31
|
+
}
|
|
32
|
+
const hex = Array.from(byteArray)
|
|
33
|
+
.map((b) => b.toString(16).padStart(2, '0')) // byte to hex
|
|
34
|
+
.join('');
|
|
35
|
+
return BigInt(`0x${hex}`);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const keyurlCache = {};
|
|
39
|
+
const getKeysFromRelayer = async (url, publicKeyId) => {
|
|
40
|
+
if (keyurlCache[url]) {
|
|
41
|
+
return keyurlCache[url];
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const response = await fetch(`${url}/v1/keyurl`);
|
|
45
|
+
if (!response.ok) {
|
|
46
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
47
|
+
}
|
|
48
|
+
const data = await response.json();
|
|
49
|
+
if (data) {
|
|
50
|
+
let pubKeyUrl;
|
|
51
|
+
// If no publicKeyId is provided, use the first one
|
|
52
|
+
// Warning: if there are multiple keys available, the first one will most likely never be the
|
|
53
|
+
// same between several calls (fetching the infos is non-deterministic)
|
|
54
|
+
if (!publicKeyId) {
|
|
55
|
+
pubKeyUrl = data.response.fhe_key_info[0].fhe_public_key.urls[0];
|
|
56
|
+
publicKeyId = data.response.fhe_key_info[0].fhe_public_key.data_id;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
// If a publicKeyId is provided, get the corresponding info
|
|
60
|
+
const keyInfo = data.response.fhe_key_info.find((info) => info.fhe_public_key.data_id === publicKeyId);
|
|
61
|
+
if (!keyInfo) {
|
|
62
|
+
throw new Error(`Could not find FHE key info with data_id ${publicKeyId}`);
|
|
63
|
+
}
|
|
64
|
+
// TODO: Get a given party's public key url instead of the first one
|
|
65
|
+
pubKeyUrl = keyInfo.fhe_public_key.urls[0];
|
|
66
|
+
}
|
|
67
|
+
const publicKeyResponse = await fetch(pubKeyUrl);
|
|
68
|
+
if (!publicKeyResponse.ok) {
|
|
69
|
+
throw new Error(`HTTP error! status: ${publicKeyResponse.status} on ${publicKeyResponse.url}`);
|
|
70
|
+
}
|
|
71
|
+
let publicKey;
|
|
72
|
+
if (typeof publicKeyResponse.bytes === 'function') {
|
|
73
|
+
// bytes is not widely supported yet
|
|
74
|
+
publicKey = await publicKeyResponse.bytes();
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
publicKey = new Uint8Array(await publicKeyResponse.arrayBuffer());
|
|
78
|
+
}
|
|
79
|
+
const publicParamsUrl = data.response.crs['2048'].urls[0];
|
|
80
|
+
const publicParamsId = data.response.crs['2048'].data_id;
|
|
81
|
+
const publicParams2048Response = await fetch(publicParamsUrl);
|
|
82
|
+
if (!publicParams2048Response.ok) {
|
|
83
|
+
throw new Error(`HTTP error! status: ${publicParams2048Response.status} on ${publicParams2048Response.url}`);
|
|
84
|
+
}
|
|
85
|
+
const publicParams2048 = await publicParams2048Response.bytes();
|
|
86
|
+
let pub_key;
|
|
87
|
+
try {
|
|
88
|
+
pub_key = nodeTfhe.TfheCompactPublicKey.safe_deserialize(publicKey, SERIALIZED_SIZE_LIMIT_PK);
|
|
89
|
+
}
|
|
90
|
+
catch (e) {
|
|
91
|
+
throw new Error('Invalid public key (deserialization failed)', {
|
|
92
|
+
cause: e,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
let crs;
|
|
96
|
+
try {
|
|
97
|
+
crs = nodeTfhe.CompactPkeCrs.safe_deserialize(new Uint8Array(publicParams2048), SERIALIZED_SIZE_LIMIT_CRS);
|
|
98
|
+
}
|
|
99
|
+
catch (e) {
|
|
100
|
+
throw new Error('Invalid crs (deserialization failed)', {
|
|
101
|
+
cause: e,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
const result = {
|
|
105
|
+
publicKey: pub_key,
|
|
106
|
+
publicKeyId,
|
|
107
|
+
publicParams: {
|
|
108
|
+
2048: {
|
|
109
|
+
publicParams: crs,
|
|
110
|
+
publicParamsId,
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
keyurlCache[url] = result;
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
throw new Error('No public key available');
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
catch (e) {
|
|
122
|
+
throw new Error('Impossible to fetch public key: wrong relayer url.', {
|
|
123
|
+
cause: e,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const abiKmsVerifier = [
|
|
129
|
+
'function getKmsSigners() view returns (address[])',
|
|
130
|
+
'function getThreshold() view returns (uint256)',
|
|
131
|
+
];
|
|
132
|
+
const abiInputVerifier = [
|
|
133
|
+
'function getCoprocessorSigners() view returns (address[])',
|
|
134
|
+
'function getThreshold() view returns (uint256)',
|
|
135
|
+
];
|
|
136
|
+
const getProvider = (config) => {
|
|
137
|
+
if (typeof config.network === 'string') {
|
|
138
|
+
return new ethers.JsonRpcProvider(config.network);
|
|
139
|
+
}
|
|
140
|
+
else if (config.network) {
|
|
141
|
+
return new ethers.BrowserProvider(config.network);
|
|
142
|
+
}
|
|
143
|
+
throw new Error('You must provide a network URL or a EIP1193 object (eg: window.ethereum)');
|
|
144
|
+
};
|
|
145
|
+
const getChainId = async (provider, config) => {
|
|
146
|
+
if (config.chainId && typeof config.chainId === 'number') {
|
|
147
|
+
return config.chainId;
|
|
148
|
+
}
|
|
149
|
+
else if (config.chainId && typeof config.chainId !== 'number') {
|
|
150
|
+
throw new Error('chainId must be a number.');
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
const chainId = (await provider.getNetwork()).chainId;
|
|
154
|
+
return Number(chainId);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
const getTfheCompactPublicKey = async (config) => {
|
|
158
|
+
if (config.relayerUrl && !config.publicKey) {
|
|
159
|
+
const inputs = await getKeysFromRelayer(cleanURL(config.relayerUrl));
|
|
160
|
+
return { publicKey: inputs.publicKey, publicKeyId: inputs.publicKeyId };
|
|
161
|
+
}
|
|
162
|
+
else if (config.publicKey && config.publicKey.data && config.publicKey.id) {
|
|
163
|
+
const buff = config.publicKey.data;
|
|
164
|
+
try {
|
|
165
|
+
return {
|
|
166
|
+
publicKey: nodeTfhe.TfheCompactPublicKey.safe_deserialize(buff, SERIALIZED_SIZE_LIMIT_PK),
|
|
167
|
+
publicKeyId: config.publicKey.id,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
catch (e) {
|
|
171
|
+
throw new Error('Invalid public key (deserialization failed)', {
|
|
172
|
+
cause: e,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
throw new Error('You must provide a public key with its public key ID.');
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
const getPublicParams = async (config) => {
|
|
181
|
+
if (config.relayerUrl && !config.publicParams) {
|
|
182
|
+
const inputs = await getKeysFromRelayer(cleanURL(config.relayerUrl));
|
|
183
|
+
return inputs.publicParams;
|
|
184
|
+
}
|
|
185
|
+
else if (config.publicParams && config.publicParams['2048']) {
|
|
186
|
+
const buff = config.publicParams['2048'].publicParams;
|
|
187
|
+
try {
|
|
188
|
+
return {
|
|
189
|
+
2048: {
|
|
190
|
+
publicParams: nodeTfhe.CompactPkeCrs.safe_deserialize(buff, SERIALIZED_SIZE_LIMIT_CRS),
|
|
191
|
+
publicParamsId: config.publicParams['2048'].publicParamsId,
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
catch (e) {
|
|
196
|
+
throw new Error('Invalid public key (deserialization failed)', {
|
|
197
|
+
cause: e,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
throw new Error('You must provide a valid CRS with its CRS ID.');
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
const getKMSSigners = async (provider, config) => {
|
|
206
|
+
const kmsContract = new ethers.Contract(config.kmsContractAddress, abiKmsVerifier, provider);
|
|
207
|
+
const signers = await kmsContract.getKmsSigners();
|
|
208
|
+
return signers;
|
|
209
|
+
};
|
|
210
|
+
const getKMSSignersThreshold = async (provider, config) => {
|
|
211
|
+
const kmsContract = new ethers.Contract(config.kmsContractAddress, abiKmsVerifier, provider);
|
|
212
|
+
const threshold = await kmsContract.getThreshold();
|
|
213
|
+
return Number(threshold); // threshold is always supposed to fit in a number
|
|
214
|
+
};
|
|
215
|
+
const getCoprocessorSigners = async (provider, config) => {
|
|
216
|
+
const inputContract = new ethers.Contract(config.inputVerifierContractAddress, abiInputVerifier, provider);
|
|
217
|
+
const signers = await inputContract.getCoprocessorSigners();
|
|
218
|
+
return signers;
|
|
219
|
+
};
|
|
220
|
+
const getCoprocessorSignersThreshold = async (provider, config) => {
|
|
221
|
+
const inputContract = new ethers.Contract(config.inputVerifierContractAddress, abiInputVerifier, provider);
|
|
222
|
+
const threshold = await inputContract.getThreshold();
|
|
223
|
+
return Number(threshold); // threshold is always supposed to fit in a number
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const NumEncryptedBits = {
|
|
227
|
+
0: 2, // ebool
|
|
228
|
+
2: 8, // euint8
|
|
229
|
+
3: 16, // euint16
|
|
230
|
+
4: 32, // euint32
|
|
231
|
+
5: 64, // euint64
|
|
232
|
+
6: 128, // euint128
|
|
233
|
+
7: 160, // eaddress
|
|
234
|
+
8: 256, // euint256
|
|
235
|
+
9: 512, // ebytes64
|
|
236
|
+
10: 1024, // ebytes128
|
|
237
|
+
11: 2048, // ebytes256
|
|
238
|
+
};
|
|
239
|
+
function checkEncryptedBits(handles) {
|
|
240
|
+
let total = 0;
|
|
241
|
+
for (const handle of handles) {
|
|
242
|
+
if (handle.length !== 66) {
|
|
243
|
+
throw new Error(`Handle ${handle} is not of valid length`);
|
|
244
|
+
}
|
|
245
|
+
const hexPair = handle.slice(-4, -2).toLowerCase();
|
|
246
|
+
const typeDiscriminant = parseInt(hexPair, 16);
|
|
247
|
+
if (!(typeDiscriminant in NumEncryptedBits)) {
|
|
248
|
+
throw new Error(`Handle ${handle} is not of valid type`);
|
|
249
|
+
}
|
|
250
|
+
total +=
|
|
251
|
+
NumEncryptedBits[typeDiscriminant];
|
|
252
|
+
// enforce 2048‑bit limit
|
|
253
|
+
if (total > 2048) {
|
|
254
|
+
throw new Error('Cannot decrypt more than 2048 encrypted bits in a single request');
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return total;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const aclABI$1 = [
|
|
261
|
+
'function persistAllowed(bytes32 handle, address account) view returns (bool)',
|
|
262
|
+
];
|
|
263
|
+
const MAX_USER_DECRYPT_CONTRACT_ADDRESSES = 10;
|
|
264
|
+
const MAX_USER_DECRYPT_DURATION_DAYS = BigInt(365);
|
|
265
|
+
function formatAccordingToType(decryptedBigInt, type) {
|
|
266
|
+
if (type === 0) {
|
|
267
|
+
// ebool
|
|
268
|
+
return decryptedBigInt === BigInt(1);
|
|
269
|
+
}
|
|
270
|
+
else if (type === 7) {
|
|
271
|
+
// eaddress
|
|
272
|
+
return ethers.getAddress('0x' + decryptedBigInt.toString(16).padStart(40, '0'));
|
|
273
|
+
}
|
|
274
|
+
else if (type === 9) {
|
|
275
|
+
// ebytes64
|
|
276
|
+
return '0x' + decryptedBigInt.toString(16).padStart(128, '0');
|
|
277
|
+
}
|
|
278
|
+
else if (type === 10) {
|
|
279
|
+
// ebytes128
|
|
280
|
+
return '0x' + decryptedBigInt.toString(16).padStart(256, '0');
|
|
281
|
+
}
|
|
282
|
+
else if (type === 11) {
|
|
283
|
+
// ebytes256
|
|
284
|
+
return '0x' + decryptedBigInt.toString(16).padStart(512, '0');
|
|
285
|
+
} // euintXXX
|
|
286
|
+
return decryptedBigInt;
|
|
287
|
+
}
|
|
288
|
+
function buildUserDecryptedResult(handles, listBigIntDecryptions) {
|
|
289
|
+
let typesList = [];
|
|
290
|
+
for (const handle of handles) {
|
|
291
|
+
const hexPair = handle.slice(-4, -2).toLowerCase();
|
|
292
|
+
const typeDiscriminant = parseInt(hexPair, 16);
|
|
293
|
+
typesList.push(typeDiscriminant);
|
|
294
|
+
}
|
|
295
|
+
let results = {};
|
|
296
|
+
handles.forEach((handle, idx) => (results[handle] = formatAccordingToType(listBigIntDecryptions[idx], typesList[idx])));
|
|
297
|
+
return results;
|
|
298
|
+
}
|
|
299
|
+
function checkDeadlineValidity(startTimestamp, durationDays) {
|
|
300
|
+
if (durationDays === BigInt(0)) {
|
|
301
|
+
throw Error('durationDays is null');
|
|
302
|
+
}
|
|
303
|
+
if (durationDays > MAX_USER_DECRYPT_DURATION_DAYS) {
|
|
304
|
+
throw Error(`durationDays is above max duration of ${MAX_USER_DECRYPT_DURATION_DAYS}`);
|
|
305
|
+
}
|
|
306
|
+
const currentTimestamp = BigInt(Math.floor(Date.now() / 1000));
|
|
307
|
+
if (startTimestamp > currentTimestamp) {
|
|
308
|
+
throw Error('startTimestamp is set in the future');
|
|
309
|
+
}
|
|
310
|
+
const durationInSeconds = durationDays * BigInt(86400);
|
|
311
|
+
if (startTimestamp + durationInSeconds < currentTimestamp) {
|
|
312
|
+
throw Error('User decrypt request has expired');
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
const userDecryptRequest = (kmsSigners, gatewayChainId, chainId, verifyingContractAddress, aclContractAddress, relayerUrl, provider) => async (_handles, privateKey, publicKey, signature, contractAddresses, userAddress, startTimestamp, durationDays) => {
|
|
316
|
+
// Casting handles if string
|
|
317
|
+
const handles = _handles.map((h) => ({
|
|
318
|
+
handle: typeof h.handle === 'string'
|
|
319
|
+
? toHexString(fromHexString(h.handle), true)
|
|
320
|
+
: toHexString(h.handle, true),
|
|
321
|
+
contractAddress: h.contractAddress,
|
|
322
|
+
}));
|
|
323
|
+
checkEncryptedBits(handles.map((h) => h.handle));
|
|
324
|
+
checkDeadlineValidity(BigInt(startTimestamp), BigInt(durationDays));
|
|
325
|
+
const acl = new ethers.ethers.Contract(aclContractAddress, aclABI$1, provider);
|
|
326
|
+
const verifications = handles.map(async ({ handle, contractAddress }) => {
|
|
327
|
+
const userAllowed = await acl.persistAllowed(handle, userAddress);
|
|
328
|
+
const contractAllowed = await acl.persistAllowed(handle, contractAddress);
|
|
329
|
+
if (!userAllowed) {
|
|
330
|
+
throw new Error(`User ${userAddress} is not authorized to user decrypt handle ${handle}!`);
|
|
331
|
+
}
|
|
332
|
+
if (!contractAllowed) {
|
|
333
|
+
throw new Error(`dapp contract ${contractAddress} is not authorized to user decrypt handle ${handle}!`);
|
|
334
|
+
}
|
|
335
|
+
if (userAddress === contractAddress) {
|
|
336
|
+
throw new Error(`userAddress ${userAddress} should not be equal to contractAddress when requesting user decryption!`);
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
const contractAddressesLength = contractAddresses.length;
|
|
340
|
+
if (contractAddressesLength === 0) {
|
|
341
|
+
throw Error('contractAddresses is empty');
|
|
342
|
+
}
|
|
343
|
+
if (contractAddressesLength > MAX_USER_DECRYPT_CONTRACT_ADDRESSES) {
|
|
344
|
+
throw Error(`contractAddresses max length of ${MAX_USER_DECRYPT_CONTRACT_ADDRESSES} exceeded`);
|
|
345
|
+
}
|
|
346
|
+
await Promise.all(verifications).catch((e) => {
|
|
347
|
+
throw e;
|
|
348
|
+
});
|
|
349
|
+
const payloadForRequest = {
|
|
350
|
+
handleContractPairs: handles,
|
|
351
|
+
requestValidity: {
|
|
352
|
+
startTimestamp: startTimestamp.toString(), // Convert to string
|
|
353
|
+
durationDays: durationDays.toString(), // Convert to string
|
|
354
|
+
},
|
|
355
|
+
contractsChainId: chainId.toString(), // Convert to string
|
|
356
|
+
contractAddresses: contractAddresses.map((c) => ethers.getAddress(c)),
|
|
357
|
+
userAddress: ethers.getAddress(userAddress),
|
|
358
|
+
signature: signature.replace(/^(0x)/, ''),
|
|
359
|
+
publicKey: publicKey.replace(/^(0x)/, ''),
|
|
360
|
+
};
|
|
361
|
+
const options = {
|
|
362
|
+
method: 'POST',
|
|
363
|
+
headers: {
|
|
364
|
+
'Content-Type': 'application/json',
|
|
365
|
+
},
|
|
366
|
+
body: JSON.stringify(payloadForRequest),
|
|
367
|
+
};
|
|
368
|
+
let pubKey;
|
|
369
|
+
let privKey;
|
|
370
|
+
try {
|
|
371
|
+
pubKey = nodeTkms.u8vec_to_cryptobox_pk(fromHexString(publicKey));
|
|
372
|
+
privKey = nodeTkms.u8vec_to_cryptobox_sk(fromHexString(privateKey));
|
|
373
|
+
}
|
|
374
|
+
catch (e) {
|
|
375
|
+
throw new Error('Invalid public or private key', { cause: e });
|
|
376
|
+
}
|
|
377
|
+
let response;
|
|
378
|
+
let json;
|
|
379
|
+
try {
|
|
380
|
+
response = await fetch(`${relayerUrl}/v1/user-decrypt`, options);
|
|
381
|
+
if (!response.ok) {
|
|
382
|
+
throw new Error(`User decrypt failed: relayer respond with HTTP code ${response.status}`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
catch (e) {
|
|
386
|
+
throw new Error("User decrypt failed: Relayer didn't respond", {
|
|
387
|
+
cause: e,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
try {
|
|
391
|
+
json = await response.json();
|
|
392
|
+
}
|
|
393
|
+
catch (e) {
|
|
394
|
+
throw new Error("User decrypt failed: Relayer didn't return a JSON", {
|
|
395
|
+
cause: e,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
if (json.status === 'failure') {
|
|
399
|
+
throw new Error("User decrypt failed: the user decryption didn't succeed for an unknown reason", { cause: json });
|
|
400
|
+
}
|
|
401
|
+
const client = nodeTkms.new_client(kmsSigners, userAddress, 'default');
|
|
402
|
+
try {
|
|
403
|
+
const buffer = new ArrayBuffer(32);
|
|
404
|
+
const view = new DataView(buffer);
|
|
405
|
+
view.setUint32(28, gatewayChainId, false);
|
|
406
|
+
const chainIdArrayBE = new Uint8Array(buffer);
|
|
407
|
+
const eip712Domain = {
|
|
408
|
+
name: 'Decryption',
|
|
409
|
+
version: '1',
|
|
410
|
+
chain_id: chainIdArrayBE,
|
|
411
|
+
verifying_contract: verifyingContractAddress,
|
|
412
|
+
salt: null,
|
|
413
|
+
};
|
|
414
|
+
const payloadForVerification = {
|
|
415
|
+
signature,
|
|
416
|
+
client_address: userAddress,
|
|
417
|
+
enc_key: publicKey.replace(/^0x/, ''),
|
|
418
|
+
ciphertext_handles: handles.map((h) => h.handle.replace(/^0x/, '')),
|
|
419
|
+
eip712_verifying_contract: verifyingContractAddress,
|
|
420
|
+
};
|
|
421
|
+
const decryption = nodeTkms.process_user_decryption_resp_from_js(client, payloadForVerification, eip712Domain, json.response, pubKey, privKey, true);
|
|
422
|
+
const listBigIntDecryptions = decryption.map((d) => bytesToBigInt(d.bytes));
|
|
423
|
+
const results = buildUserDecryptedResult(handles.map((h) => h.handle), listBigIntDecryptions);
|
|
424
|
+
return results;
|
|
425
|
+
}
|
|
426
|
+
catch (e) {
|
|
427
|
+
throw new Error('An error occured during decryption', { cause: e });
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
const checkEncryptedValue = (value, bits) => {
|
|
432
|
+
if (value == null)
|
|
433
|
+
throw new Error('Missing value');
|
|
434
|
+
let limit;
|
|
435
|
+
if (bits >= 8) {
|
|
436
|
+
limit = BigInt(`0x${new Array(bits / 8).fill(null).reduce((v) => `${v}ff`, '')}`);
|
|
437
|
+
}
|
|
438
|
+
else {
|
|
439
|
+
limit = BigInt(2 ** bits - 1);
|
|
440
|
+
}
|
|
441
|
+
if (typeof value !== 'number' && typeof value !== 'bigint')
|
|
442
|
+
throw new Error('Value must be a number or a bigint.');
|
|
443
|
+
if (value > limit) {
|
|
444
|
+
throw new Error(`The value exceeds the limit for ${bits}bits integer (${limit.toString()}).`);
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
const createEncryptedInput = ({ aclContractAddress, chainId, tfheCompactPublicKey, publicParams, contractAddress, userAddress, }) => {
|
|
448
|
+
if (!ethers.isAddress(contractAddress)) {
|
|
449
|
+
throw new Error('Contract address is not a valid address.');
|
|
450
|
+
}
|
|
451
|
+
if (!ethers.isAddress(userAddress)) {
|
|
452
|
+
throw new Error('User address is not a valid address.');
|
|
453
|
+
}
|
|
454
|
+
const publicKey = tfheCompactPublicKey;
|
|
455
|
+
const bits = [];
|
|
456
|
+
const builder = nodeTfhe.CompactCiphertextList.builder(publicKey);
|
|
457
|
+
let ciphertextWithZKProof = new Uint8Array(); // updated in `_prove`
|
|
458
|
+
const checkLimit = (added) => {
|
|
459
|
+
if (bits.reduce((acc, val) => acc + Math.max(2, val), 0) + added > 2048) {
|
|
460
|
+
throw Error('Packing more than 2048 bits in a single input ciphertext is unsupported');
|
|
461
|
+
}
|
|
462
|
+
if (bits.length + 1 > 256)
|
|
463
|
+
throw Error('Packing more than 256 variables in a single input ciphertext is unsupported');
|
|
464
|
+
};
|
|
465
|
+
return {
|
|
466
|
+
addBool(value) {
|
|
467
|
+
if (value == null)
|
|
468
|
+
throw new Error('Missing value');
|
|
469
|
+
if (typeof value !== 'boolean' &&
|
|
470
|
+
typeof value !== 'number' &&
|
|
471
|
+
typeof value !== 'bigint')
|
|
472
|
+
throw new Error('The value must be a boolean, a number or a bigint.');
|
|
473
|
+
if (Number(value) > 1)
|
|
474
|
+
throw new Error('The value must be 1 or 0.');
|
|
475
|
+
checkEncryptedValue(Number(value), 1);
|
|
476
|
+
checkLimit(2);
|
|
477
|
+
builder.push_boolean(!!value);
|
|
478
|
+
bits.push(1); // ebool takes 2 encrypted bits
|
|
479
|
+
return this;
|
|
480
|
+
},
|
|
481
|
+
add8(value) {
|
|
482
|
+
checkEncryptedValue(value, 8);
|
|
483
|
+
checkLimit(8);
|
|
484
|
+
builder.push_u8(Number(value));
|
|
485
|
+
bits.push(8);
|
|
486
|
+
return this;
|
|
487
|
+
},
|
|
488
|
+
add16(value) {
|
|
489
|
+
checkEncryptedValue(value, 16);
|
|
490
|
+
checkLimit(16);
|
|
491
|
+
builder.push_u16(Number(value));
|
|
492
|
+
bits.push(16);
|
|
493
|
+
return this;
|
|
494
|
+
},
|
|
495
|
+
add32(value) {
|
|
496
|
+
checkEncryptedValue(value, 32);
|
|
497
|
+
checkLimit(32);
|
|
498
|
+
builder.push_u32(Number(value));
|
|
499
|
+
bits.push(32);
|
|
500
|
+
return this;
|
|
501
|
+
},
|
|
502
|
+
add64(value) {
|
|
503
|
+
checkEncryptedValue(value, 64);
|
|
504
|
+
checkLimit(64);
|
|
505
|
+
builder.push_u64(BigInt(value));
|
|
506
|
+
bits.push(64);
|
|
507
|
+
return this;
|
|
508
|
+
},
|
|
509
|
+
add128(value) {
|
|
510
|
+
checkEncryptedValue(value, 128);
|
|
511
|
+
checkLimit(128);
|
|
512
|
+
builder.push_u128(BigInt(value));
|
|
513
|
+
bits.push(128);
|
|
514
|
+
return this;
|
|
515
|
+
},
|
|
516
|
+
addAddress(value) {
|
|
517
|
+
if (!ethers.isAddress(value)) {
|
|
518
|
+
throw new Error('The value must be a valid address.');
|
|
519
|
+
}
|
|
520
|
+
checkLimit(160);
|
|
521
|
+
builder.push_u160(BigInt(value));
|
|
522
|
+
bits.push(160);
|
|
523
|
+
return this;
|
|
524
|
+
},
|
|
525
|
+
add256(value) {
|
|
526
|
+
checkEncryptedValue(value, 256);
|
|
527
|
+
checkLimit(256);
|
|
528
|
+
builder.push_u256(BigInt(value));
|
|
529
|
+
bits.push(256);
|
|
530
|
+
return this;
|
|
531
|
+
},
|
|
532
|
+
addBytes64(value) {
|
|
533
|
+
if (value.length !== 64)
|
|
534
|
+
throw Error('Uncorrect length of input Uint8Array, should be 64 for an ebytes64');
|
|
535
|
+
const bigIntValue = bytesToBigInt(value);
|
|
536
|
+
checkEncryptedValue(bigIntValue, 512);
|
|
537
|
+
checkLimit(512);
|
|
538
|
+
builder.push_u512(bigIntValue);
|
|
539
|
+
bits.push(512);
|
|
540
|
+
return this;
|
|
541
|
+
},
|
|
542
|
+
addBytes128(value) {
|
|
543
|
+
if (value.length !== 128)
|
|
544
|
+
throw Error('Uncorrect length of input Uint8Array, should be 128 for an ebytes128');
|
|
545
|
+
const bigIntValue = bytesToBigInt(value);
|
|
546
|
+
checkEncryptedValue(bigIntValue, 1024);
|
|
547
|
+
checkLimit(1024);
|
|
548
|
+
builder.push_u1024(bigIntValue);
|
|
549
|
+
bits.push(1024);
|
|
550
|
+
return this;
|
|
551
|
+
},
|
|
552
|
+
addBytes256(value) {
|
|
553
|
+
if (value.length !== 256)
|
|
554
|
+
throw Error('Uncorrect length of input Uint8Array, should be 256 for an ebytes256');
|
|
555
|
+
const bigIntValue = bytesToBigInt(value);
|
|
556
|
+
checkEncryptedValue(bigIntValue, 2048);
|
|
557
|
+
checkLimit(2048);
|
|
558
|
+
builder.push_u2048(bigIntValue);
|
|
559
|
+
bits.push(2048);
|
|
560
|
+
return this;
|
|
561
|
+
},
|
|
562
|
+
getBits() {
|
|
563
|
+
return bits;
|
|
564
|
+
},
|
|
565
|
+
encrypt() {
|
|
566
|
+
const getClosestPP = () => {
|
|
567
|
+
const getKeys = (obj) => Object.keys(obj);
|
|
568
|
+
const totalBits = bits.reduce((total, v) => total + v, 0);
|
|
569
|
+
const ppTypes = getKeys(publicParams);
|
|
570
|
+
const closestPP = ppTypes.find((k) => Number(k) >= totalBits);
|
|
571
|
+
if (!closestPP) {
|
|
572
|
+
throw new Error(`Too many bits in provided values. Maximum is ${ppTypes[ppTypes.length - 1]}.`);
|
|
573
|
+
}
|
|
574
|
+
return closestPP;
|
|
575
|
+
};
|
|
576
|
+
const closestPP = getClosestPP();
|
|
577
|
+
const pp = publicParams[closestPP].publicParams;
|
|
578
|
+
const buffContract = fromHexString(contractAddress);
|
|
579
|
+
const buffUser = fromHexString(userAddress);
|
|
580
|
+
const buffAcl = fromHexString(aclContractAddress);
|
|
581
|
+
const buffChainId = fromHexString(chainId.toString(16).padStart(64, '0'));
|
|
582
|
+
const auxData = new Uint8Array(buffContract.length + buffUser.length + buffAcl.length + 32);
|
|
583
|
+
auxData.set(buffContract, 0);
|
|
584
|
+
auxData.set(buffUser, 20);
|
|
585
|
+
auxData.set(buffAcl, 40);
|
|
586
|
+
auxData.set(buffChainId, auxData.length - buffChainId.length);
|
|
587
|
+
const encrypted = builder.build_with_proof_packed(pp, auxData, nodeTfhe.ZkComputeLoad.Verify);
|
|
588
|
+
ciphertextWithZKProof = encrypted.safe_serialize(SERIALIZED_SIZE_LIMIT_CIPHERTEXT);
|
|
589
|
+
return ciphertextWithZKProof;
|
|
590
|
+
},
|
|
591
|
+
};
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
const ENCRYPTION_TYPES = {
|
|
595
|
+
1: 0, // ebool takes 2 encrypted bits
|
|
596
|
+
8: 2,
|
|
597
|
+
16: 3,
|
|
598
|
+
32: 4,
|
|
599
|
+
64: 5,
|
|
600
|
+
128: 6,
|
|
601
|
+
160: 7,
|
|
602
|
+
256: 8,
|
|
603
|
+
512: 9,
|
|
604
|
+
1024: 10,
|
|
605
|
+
2048: 11,
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
const MAX_UINT64 = BigInt('18446744073709551615'); // 2^64 - 1
|
|
609
|
+
const computeHandles = (ciphertextWithZKProof, bitwidths, aclContractAddress, chainId, ciphertextVersion) => {
|
|
610
|
+
// Should be identical to:
|
|
611
|
+
// https://github.com/zama-ai/fhevm-backend/blob/bae00d1b0feafb63286e94acdc58dc88d9c481bf/fhevm-engine/zkproof-worker/src/verifier.rs#L301
|
|
612
|
+
const blob_hash = createHash('keccak256')
|
|
613
|
+
.update(Buffer.from(ciphertextWithZKProof))
|
|
614
|
+
.digest();
|
|
615
|
+
const aclContractAddress20Bytes = Buffer.from(fromHexString(aclContractAddress));
|
|
616
|
+
const hex = chainId.toString(16).padStart(64, '0'); // 64 hex chars = 32 bytes
|
|
617
|
+
const chainId32Bytes = Buffer.from(hex, 'hex');
|
|
618
|
+
const handles = bitwidths.map((bitwidth, encryptionIndex) => {
|
|
619
|
+
const encryptionType = ENCRYPTION_TYPES[bitwidth];
|
|
620
|
+
const encryptionIndex1Byte = Buffer.from([encryptionIndex]);
|
|
621
|
+
const handleHash = createHash('keccak256')
|
|
622
|
+
.update(blob_hash)
|
|
623
|
+
.update(encryptionIndex1Byte)
|
|
624
|
+
.update(aclContractAddress20Bytes)
|
|
625
|
+
.update(chainId32Bytes)
|
|
626
|
+
.digest();
|
|
627
|
+
const dataInput = new Uint8Array(32);
|
|
628
|
+
dataInput.set(handleHash, 0);
|
|
629
|
+
// Check if chainId exceeds 8 bytes
|
|
630
|
+
if (BigInt(chainId) > MAX_UINT64) {
|
|
631
|
+
throw new Error('ChainId exceeds maximum allowed value (8 bytes)'); // fhevm assumes chainID is only taking up to 8 bytes
|
|
632
|
+
}
|
|
633
|
+
const chainId8Bytes = chainId32Bytes.slice(24, 32);
|
|
634
|
+
dataInput[21] = encryptionIndex;
|
|
635
|
+
chainId8Bytes.copy(dataInput, 22);
|
|
636
|
+
dataInput[30] = encryptionType;
|
|
637
|
+
dataInput[31] = ciphertextVersion;
|
|
638
|
+
return dataInput;
|
|
639
|
+
});
|
|
640
|
+
return handles;
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
const currentCiphertextVersion = () => {
|
|
644
|
+
return 0;
|
|
645
|
+
};
|
|
646
|
+
function isThresholdReached$1(coprocessorSigners, recoveredAddresses, threshold) {
|
|
647
|
+
const addressMap = new Map();
|
|
648
|
+
recoveredAddresses.forEach((address, index) => {
|
|
649
|
+
if (addressMap.has(address)) {
|
|
650
|
+
const duplicateValue = address;
|
|
651
|
+
throw new Error(`Duplicate coprocessor signer address found: ${duplicateValue} appears multiple times in recovered addresses`);
|
|
652
|
+
}
|
|
653
|
+
addressMap.set(address, index);
|
|
654
|
+
});
|
|
655
|
+
for (const address of recoveredAddresses) {
|
|
656
|
+
if (!coprocessorSigners.includes(address)) {
|
|
657
|
+
throw new Error(`Invalid address found: ${address} is not in the list of coprocessor signers`);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
return recoveredAddresses.length >= threshold;
|
|
661
|
+
}
|
|
662
|
+
const createRelayerEncryptedInput = (aclContractAddress, verifyingContractAddressInputVerification, chainId, gatewayChainId, relayerUrl, tfheCompactPublicKey, publicParams, coprocessorSigners, thresholdCoprocessorSigners) => (contractAddress, userAddress) => {
|
|
663
|
+
if (!ethers.isAddress(contractAddress)) {
|
|
664
|
+
throw new Error('Contract address is not a valid address.');
|
|
665
|
+
}
|
|
666
|
+
if (!ethers.isAddress(userAddress)) {
|
|
667
|
+
throw new Error('User address is not a valid address.');
|
|
668
|
+
}
|
|
669
|
+
const input = createEncryptedInput({
|
|
670
|
+
aclContractAddress,
|
|
671
|
+
chainId,
|
|
672
|
+
tfheCompactPublicKey,
|
|
673
|
+
publicParams,
|
|
674
|
+
contractAddress,
|
|
675
|
+
userAddress,
|
|
676
|
+
});
|
|
677
|
+
return {
|
|
678
|
+
_input: input,
|
|
679
|
+
addBool(value) {
|
|
680
|
+
input.addBool(value);
|
|
681
|
+
return this;
|
|
682
|
+
},
|
|
683
|
+
add8(value) {
|
|
684
|
+
input.add8(value);
|
|
685
|
+
return this;
|
|
686
|
+
},
|
|
687
|
+
add16(value) {
|
|
688
|
+
input.add16(value);
|
|
689
|
+
return this;
|
|
690
|
+
},
|
|
691
|
+
add32(value) {
|
|
692
|
+
input.add32(value);
|
|
693
|
+
return this;
|
|
694
|
+
},
|
|
695
|
+
add64(value) {
|
|
696
|
+
input.add64(value);
|
|
697
|
+
return this;
|
|
698
|
+
},
|
|
699
|
+
add128(value) {
|
|
700
|
+
input.add128(value);
|
|
701
|
+
return this;
|
|
702
|
+
},
|
|
703
|
+
add256(value) {
|
|
704
|
+
input.add256(value);
|
|
705
|
+
return this;
|
|
706
|
+
},
|
|
707
|
+
addBytes64(value) {
|
|
708
|
+
input.addBytes64(value);
|
|
709
|
+
return this;
|
|
710
|
+
},
|
|
711
|
+
addBytes128(value) {
|
|
712
|
+
input.addBytes128(value);
|
|
713
|
+
return this;
|
|
714
|
+
},
|
|
715
|
+
addBytes256(value) {
|
|
716
|
+
input.addBytes256(value);
|
|
717
|
+
return this;
|
|
718
|
+
},
|
|
719
|
+
addAddress(value) {
|
|
720
|
+
input.addAddress(value);
|
|
721
|
+
return this;
|
|
722
|
+
},
|
|
723
|
+
getBits() {
|
|
724
|
+
return input.getBits();
|
|
725
|
+
},
|
|
726
|
+
encrypt: async () => {
|
|
727
|
+
const bits = input.getBits();
|
|
728
|
+
const ciphertext = input.encrypt();
|
|
729
|
+
// https://github.com/zama-ai/fhevm-relayer/blob/978b08f62de060a9b50d2c6cc19fd71b5fb8d873/src/input_http_listener.rs#L13C1-L22C1
|
|
730
|
+
const payload = {
|
|
731
|
+
contractAddress: ethers.getAddress(contractAddress),
|
|
732
|
+
userAddress: ethers.getAddress(userAddress),
|
|
733
|
+
ciphertextWithInputVerification: toHexString(ciphertext),
|
|
734
|
+
contractChainId: '0x' + chainId.toString(16),
|
|
735
|
+
};
|
|
736
|
+
const options = {
|
|
737
|
+
method: 'POST',
|
|
738
|
+
headers: {
|
|
739
|
+
'Content-Type': 'application/json',
|
|
740
|
+
},
|
|
741
|
+
body: JSON.stringify(payload),
|
|
742
|
+
};
|
|
743
|
+
const url = `${relayerUrl}/v1/input-proof`;
|
|
744
|
+
let json;
|
|
745
|
+
try {
|
|
746
|
+
const response = await fetch(url, options);
|
|
747
|
+
if (!response.ok) {
|
|
748
|
+
throw new Error(`Relayer didn't response correctly. Bad status ${response.statusText}. Content: ${await response.text()}`);
|
|
749
|
+
}
|
|
750
|
+
try {
|
|
751
|
+
json = await response.json();
|
|
752
|
+
}
|
|
753
|
+
catch (e) {
|
|
754
|
+
throw new Error("Relayer didn't response correctly. Bad JSON.", {
|
|
755
|
+
cause: e,
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
catch (e) {
|
|
760
|
+
throw new Error("Relayer didn't response correctly.", {
|
|
761
|
+
cause: e,
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
const handles = computeHandles(ciphertext, bits, aclContractAddress, chainId, currentCiphertextVersion());
|
|
765
|
+
// Note that the hex strings returned by the relayer do have have the 0x prefix
|
|
766
|
+
if (json.response.handles && json.response.handles.length > 0) {
|
|
767
|
+
const responseHandles = json.response.handles.map(fromHexString);
|
|
768
|
+
if (handles.length != responseHandles.length) {
|
|
769
|
+
throw new Error(`Incorrect Handles list sizes: (expected) ${handles.length} != ${responseHandles.length} (received)`);
|
|
770
|
+
}
|
|
771
|
+
for (let index = 0; index < handles.length; index += 1) {
|
|
772
|
+
let handle = handles[index];
|
|
773
|
+
let responseHandle = responseHandles[index];
|
|
774
|
+
let expected = toHexString(handle);
|
|
775
|
+
let current = toHexString(responseHandle);
|
|
776
|
+
if (expected !== current) {
|
|
777
|
+
throw new Error(`Incorrect Handle ${index}: (expected) ${expected} != ${current} (received)`);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
const signatures = json.response.signatures;
|
|
782
|
+
// verify signatures for inputs:
|
|
783
|
+
const domain = {
|
|
784
|
+
name: 'InputVerification',
|
|
785
|
+
version: '1',
|
|
786
|
+
chainId: gatewayChainId,
|
|
787
|
+
verifyingContract: verifyingContractAddressInputVerification,
|
|
788
|
+
};
|
|
789
|
+
const types = {
|
|
790
|
+
CiphertextVerification: [
|
|
791
|
+
{ name: 'ctHandles', type: 'bytes32[]' },
|
|
792
|
+
{ name: 'userAddress', type: 'address' },
|
|
793
|
+
{ name: 'contractAddress', type: 'address' },
|
|
794
|
+
{ name: 'contractChainId', type: 'uint256' },
|
|
795
|
+
],
|
|
796
|
+
};
|
|
797
|
+
const recoveredAddresses = signatures.map((signature) => {
|
|
798
|
+
const sig = signature.startsWith('0x') ? signature : `0x${signature}`;
|
|
799
|
+
const recoveredAddress = ethers.ethers.verifyTypedData(domain, types, {
|
|
800
|
+
ctHandles: handles,
|
|
801
|
+
userAddress,
|
|
802
|
+
contractAddress,
|
|
803
|
+
contractChainId: chainId,
|
|
804
|
+
}, sig);
|
|
805
|
+
return recoveredAddress;
|
|
806
|
+
});
|
|
807
|
+
const thresholdReached = isThresholdReached$1(coprocessorSigners, recoveredAddresses, thresholdCoprocessorSigners);
|
|
808
|
+
if (!thresholdReached) {
|
|
809
|
+
throw Error('Coprocessor signers threshold is not reached');
|
|
810
|
+
}
|
|
811
|
+
// inputProof is len(list_handles) + numCoprocessorSigners + list_handles + signatureCoprocessorSigners (1+1+NUM_HANDLES*32+65*numSigners)
|
|
812
|
+
let inputProof = numberToHex(handles.length);
|
|
813
|
+
const numSigners = signatures.length;
|
|
814
|
+
inputProof += numberToHex(numSigners);
|
|
815
|
+
const listHandlesStr = handles.map((i) => toHexString(i));
|
|
816
|
+
listHandlesStr.map((handle) => (inputProof += handle));
|
|
817
|
+
signatures.map((signature) => (inputProof += signature.slice(2))); // removes the '0x' prefix from the `signature` string
|
|
818
|
+
return {
|
|
819
|
+
handles,
|
|
820
|
+
inputProof: fromHexString(inputProof),
|
|
821
|
+
};
|
|
822
|
+
},
|
|
823
|
+
};
|
|
824
|
+
};
|
|
825
|
+
|
|
826
|
+
const aclABI = [
|
|
827
|
+
'function isAllowedForDecryption(bytes32 handle) view returns (bool)',
|
|
828
|
+
];
|
|
829
|
+
function isThresholdReached(kmsSigners, recoveredAddresses, threshold) {
|
|
830
|
+
const addressMap = new Map();
|
|
831
|
+
recoveredAddresses.forEach((address, index) => {
|
|
832
|
+
if (addressMap.has(address)) {
|
|
833
|
+
const duplicateValue = address;
|
|
834
|
+
throw new Error(`Duplicate KMS signer address found: ${duplicateValue} appears multiple times in recovered addresses`);
|
|
835
|
+
}
|
|
836
|
+
addressMap.set(address, index);
|
|
837
|
+
});
|
|
838
|
+
for (const address of recoveredAddresses) {
|
|
839
|
+
if (!kmsSigners.includes(address)) {
|
|
840
|
+
throw new Error(`Invalid address found: ${address} is not in the list of KMS signers`);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
return recoveredAddresses.length >= threshold;
|
|
844
|
+
}
|
|
845
|
+
const CiphertextType = {
|
|
846
|
+
0: 'bool',
|
|
847
|
+
2: 'uint256',
|
|
848
|
+
3: 'uint256',
|
|
849
|
+
4: 'uint256',
|
|
850
|
+
5: 'uint256',
|
|
851
|
+
6: 'uint256',
|
|
852
|
+
7: 'address',
|
|
853
|
+
8: 'uint256',
|
|
854
|
+
9: 'bytes',
|
|
855
|
+
10: 'bytes',
|
|
856
|
+
11: 'bytes',
|
|
857
|
+
};
|
|
858
|
+
function deserializeDecryptedResult(handles, decryptedResult) {
|
|
859
|
+
let typesList = [];
|
|
860
|
+
for (const handle of handles) {
|
|
861
|
+
const hexPair = handle.slice(-4, -2).toLowerCase();
|
|
862
|
+
const typeDiscriminant = parseInt(hexPair, 16);
|
|
863
|
+
typesList.push(typeDiscriminant);
|
|
864
|
+
}
|
|
865
|
+
const restoredEncoded = '0x' +
|
|
866
|
+
'00'.repeat(32) + // dummy requestID (ignored)
|
|
867
|
+
decryptedResult.slice(2) +
|
|
868
|
+
'00'.repeat(32); // dummy empty bytes[] length (ignored)
|
|
869
|
+
const abiTypes = typesList.map((t) => {
|
|
870
|
+
const abiType = CiphertextType[t]; // all types are valid because this was supposedly checked already inside the `checkEncryptedBits` function
|
|
871
|
+
return abiType;
|
|
872
|
+
});
|
|
873
|
+
const coder = new ethers.AbiCoder();
|
|
874
|
+
const decoded = coder.decode(['uint256', ...abiTypes, 'bytes[]'], restoredEncoded);
|
|
875
|
+
// strip dummy first/last element
|
|
876
|
+
const rawValues = decoded.slice(1, 1 + typesList.length);
|
|
877
|
+
let results = {};
|
|
878
|
+
handles.forEach((handle, idx) => (results[handle] = rawValues[idx]));
|
|
879
|
+
return results;
|
|
880
|
+
}
|
|
881
|
+
const publicDecryptRequest = (kmsSigners, thresholdSigners, gatewayChainId, verifyingContractAddress, aclContractAddress, relayerUrl, provider) => async (_handles) => {
|
|
882
|
+
const acl = new ethers.ethers.Contract(aclContractAddress, aclABI, provider);
|
|
883
|
+
let handles;
|
|
884
|
+
try {
|
|
885
|
+
handles = await Promise.all(_handles.map(async (_handle) => {
|
|
886
|
+
const handle = typeof _handle === 'string'
|
|
887
|
+
? toHexString(fromHexString(_handle), true)
|
|
888
|
+
: toHexString(_handle, true);
|
|
889
|
+
const isAllowedForDecryption = await acl.isAllowedForDecryption(handle);
|
|
890
|
+
if (!isAllowedForDecryption) {
|
|
891
|
+
throw new Error(`Handle ${handle} is not allowed for public decryption!`);
|
|
892
|
+
}
|
|
893
|
+
return handle;
|
|
894
|
+
}));
|
|
895
|
+
}
|
|
896
|
+
catch (e) {
|
|
897
|
+
throw e;
|
|
898
|
+
}
|
|
899
|
+
const verifications = handles.map(async (ctHandle) => {
|
|
900
|
+
const isAllowedForDecryption = await acl.isAllowedForDecryption(ctHandle);
|
|
901
|
+
if (!isAllowedForDecryption) {
|
|
902
|
+
throw new Error(`Handle ${ctHandle} is not allowed for public decryption!`);
|
|
903
|
+
}
|
|
904
|
+
});
|
|
905
|
+
await Promise.all(verifications).catch((e) => {
|
|
906
|
+
throw e;
|
|
907
|
+
});
|
|
908
|
+
// check 2048 bits limit
|
|
909
|
+
checkEncryptedBits(handles);
|
|
910
|
+
const payloadForRequest = {
|
|
911
|
+
ciphertextHandles: handles,
|
|
912
|
+
};
|
|
913
|
+
const options = {
|
|
914
|
+
method: 'POST',
|
|
915
|
+
headers: {
|
|
916
|
+
'Content-Type': 'application/json',
|
|
917
|
+
},
|
|
918
|
+
body: JSON.stringify(payloadForRequest),
|
|
919
|
+
};
|
|
920
|
+
let response;
|
|
921
|
+
let json;
|
|
922
|
+
try {
|
|
923
|
+
response = await fetch(`${relayerUrl}/v1/public-decrypt`, options);
|
|
924
|
+
if (!response.ok) {
|
|
925
|
+
throw new Error(`Public decrypt failed: relayer respond with HTTP code ${response.status}`);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
catch (e) {
|
|
929
|
+
throw new Error("Public decrypt failed: Relayer didn't respond", {
|
|
930
|
+
cause: e,
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
try {
|
|
934
|
+
json = await response.json();
|
|
935
|
+
}
|
|
936
|
+
catch (e) {
|
|
937
|
+
throw new Error("Public decrypt failed: Relayer didn't return a JSON", {
|
|
938
|
+
cause: e,
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
if (json.status === 'failure') {
|
|
942
|
+
throw new Error("Public decrypt failed: the public decrypt didn't succeed for an unknown reason", { cause: json });
|
|
943
|
+
}
|
|
944
|
+
// verify signatures on decryption:
|
|
945
|
+
const domain = {
|
|
946
|
+
name: 'Decryption',
|
|
947
|
+
version: '1',
|
|
948
|
+
chainId: gatewayChainId,
|
|
949
|
+
verifyingContract: verifyingContractAddress,
|
|
950
|
+
};
|
|
951
|
+
const types = {
|
|
952
|
+
PublicDecryptVerification: [
|
|
953
|
+
{ name: 'ctHandles', type: 'bytes32[]' },
|
|
954
|
+
{ name: 'decryptedResult', type: 'bytes' },
|
|
955
|
+
],
|
|
956
|
+
};
|
|
957
|
+
const result = json.response[0];
|
|
958
|
+
const decryptedResult = result.decrypted_value.startsWith('0x')
|
|
959
|
+
? result.decrypted_value
|
|
960
|
+
: `0x${result.decrypted_value}`;
|
|
961
|
+
const signatures = result.signatures;
|
|
962
|
+
const recoveredAddresses = signatures.map((signature) => {
|
|
963
|
+
const sig = signature.startsWith('0x') ? signature : `0x${signature}`;
|
|
964
|
+
const recoveredAddress = ethers.ethers.verifyTypedData(domain, types, { ctHandles: handles, decryptedResult }, sig);
|
|
965
|
+
return recoveredAddress;
|
|
966
|
+
});
|
|
967
|
+
const thresholdReached = isThresholdReached(kmsSigners, recoveredAddresses, thresholdSigners);
|
|
968
|
+
if (!thresholdReached) {
|
|
969
|
+
throw Error('KMS signers threshold is not reached');
|
|
970
|
+
}
|
|
971
|
+
const results = deserializeDecryptedResult(handles, decryptedResult);
|
|
972
|
+
return results;
|
|
973
|
+
};
|
|
974
|
+
|
|
975
|
+
/**
|
|
976
|
+
* Creates an EIP712 structure specifically for user decrypt requests
|
|
977
|
+
*
|
|
978
|
+
* @param gatewayChainId The chain ID of the gateway
|
|
979
|
+
* @param verifyingContract The address of the contract that will verify the signature
|
|
980
|
+
* @param publicKey The user's public key as a hex string or Uint8Array
|
|
981
|
+
* @param contractAddresses Array of contract addresses that can access the decryption
|
|
982
|
+
* @param contractsChainId The chain ID where the contracts are deployed
|
|
983
|
+
* @param startTimestamp The timestamp when the decryption permission becomes valid
|
|
984
|
+
* @param durationDays How many days the decryption permission remains valid
|
|
985
|
+
* @returns EIP712 typed data structure for user decryption
|
|
986
|
+
*/
|
|
987
|
+
const createEIP712 = (gatewayChainId, verifyingContract, contractsChainId) => (publicKey, contractAddresses, startTimestamp, durationDays, delegatedAccount) => {
|
|
988
|
+
if (delegatedAccount && !ethers.isAddress(delegatedAccount))
|
|
989
|
+
throw new Error('Invalid delegated account.');
|
|
990
|
+
if (!ethers.isAddress(verifyingContract)) {
|
|
991
|
+
throw new Error('Invalid verifying contract address.');
|
|
992
|
+
}
|
|
993
|
+
if (!contractAddresses.every((c) => ethers.isAddress(c))) {
|
|
994
|
+
throw new Error('Invalid contract address.');
|
|
995
|
+
}
|
|
996
|
+
// Format the public key based on its type
|
|
997
|
+
const formattedPublicKey = typeof publicKey === 'string'
|
|
998
|
+
? publicKey.startsWith('0x')
|
|
999
|
+
? publicKey
|
|
1000
|
+
: `0x${publicKey}`
|
|
1001
|
+
: publicKey;
|
|
1002
|
+
// Convert timestamps to strings if they're bigints
|
|
1003
|
+
const formattedStartTimestamp = typeof startTimestamp === 'number'
|
|
1004
|
+
? startTimestamp.toString()
|
|
1005
|
+
: startTimestamp;
|
|
1006
|
+
const formattedDurationDays = typeof durationDays === 'number' ? durationDays.toString() : durationDays;
|
|
1007
|
+
const EIP712Domain = [
|
|
1008
|
+
{ name: 'name', type: 'string' },
|
|
1009
|
+
{ name: 'version', type: 'string' },
|
|
1010
|
+
{ name: 'chainId', type: 'uint256' },
|
|
1011
|
+
{ name: 'verifyingContract', type: 'address' },
|
|
1012
|
+
];
|
|
1013
|
+
const domain = {
|
|
1014
|
+
name: 'Decryption',
|
|
1015
|
+
version: '1',
|
|
1016
|
+
chainId: gatewayChainId,
|
|
1017
|
+
verifyingContract,
|
|
1018
|
+
};
|
|
1019
|
+
if (delegatedAccount) {
|
|
1020
|
+
return {
|
|
1021
|
+
types: {
|
|
1022
|
+
EIP712Domain,
|
|
1023
|
+
DelegatedUserDecryptRequestVerification: [
|
|
1024
|
+
{ name: 'publicKey', type: 'bytes' },
|
|
1025
|
+
{ name: 'contractAddresses', type: 'address[]' },
|
|
1026
|
+
{ name: 'contractsChainId', type: 'uint256' },
|
|
1027
|
+
{ name: 'startTimestamp', type: 'uint256' },
|
|
1028
|
+
{ name: 'durationDays', type: 'uint256' },
|
|
1029
|
+
{
|
|
1030
|
+
name: 'delegatedAccount',
|
|
1031
|
+
type: 'address',
|
|
1032
|
+
},
|
|
1033
|
+
],
|
|
1034
|
+
},
|
|
1035
|
+
primaryType: 'DelegatedUserDecryptRequestVerification',
|
|
1036
|
+
domain,
|
|
1037
|
+
message: {
|
|
1038
|
+
publicKey: formattedPublicKey,
|
|
1039
|
+
contractAddresses,
|
|
1040
|
+
contractsChainId,
|
|
1041
|
+
startTimestamp: formattedStartTimestamp,
|
|
1042
|
+
durationDays: formattedDurationDays,
|
|
1043
|
+
delegatedAccount: delegatedAccount,
|
|
1044
|
+
},
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
return {
|
|
1048
|
+
types: {
|
|
1049
|
+
EIP712Domain,
|
|
1050
|
+
UserDecryptRequestVerification: [
|
|
1051
|
+
{ name: 'publicKey', type: 'bytes' },
|
|
1052
|
+
{ name: 'contractAddresses', type: 'address[]' },
|
|
1053
|
+
{ name: 'contractsChainId', type: 'uint256' },
|
|
1054
|
+
{ name: 'startTimestamp', type: 'uint256' },
|
|
1055
|
+
{ name: 'durationDays', type: 'uint256' },
|
|
1056
|
+
],
|
|
1057
|
+
},
|
|
1058
|
+
primaryType: 'UserDecryptRequestVerification',
|
|
1059
|
+
domain,
|
|
1060
|
+
message: {
|
|
1061
|
+
publicKey: formattedPublicKey,
|
|
1062
|
+
contractAddresses,
|
|
1063
|
+
contractsChainId,
|
|
1064
|
+
startTimestamp: formattedStartTimestamp,
|
|
1065
|
+
durationDays: formattedDurationDays,
|
|
1066
|
+
},
|
|
1067
|
+
};
|
|
1068
|
+
};
|
|
1069
|
+
const generateKeypair = () => {
|
|
1070
|
+
const keypair = nodeTkms.cryptobox_keygen();
|
|
1071
|
+
return {
|
|
1072
|
+
publicKey: toHexString(nodeTkms.cryptobox_pk_to_u8vec(nodeTkms.cryptobox_get_pk(keypair))),
|
|
1073
|
+
privateKey: toHexString(nodeTkms.cryptobox_sk_to_u8vec(keypair)),
|
|
1074
|
+
};
|
|
1075
|
+
};
|
|
1076
|
+
|
|
1077
|
+
global.fetch = fetchRetry(global.fetch, { retries: 5, retryDelay: 500 });
|
|
1078
|
+
const createInstance = async (config) => {
|
|
1079
|
+
const { verifyingContractAddressDecryption, verifyingContractAddressInputVerification, publicKey, kmsContractAddress, aclContractAddress, gatewayChainId, } = config;
|
|
1080
|
+
if (!kmsContractAddress || !ethers.isAddress(kmsContractAddress)) {
|
|
1081
|
+
throw new Error('KMS contract address is not valid or empty');
|
|
1082
|
+
}
|
|
1083
|
+
if (!verifyingContractAddressDecryption ||
|
|
1084
|
+
!ethers.isAddress(verifyingContractAddressDecryption)) {
|
|
1085
|
+
throw new Error('Verifying contract for Decryption address is not valid or empty');
|
|
1086
|
+
}
|
|
1087
|
+
if (!verifyingContractAddressInputVerification ||
|
|
1088
|
+
!ethers.isAddress(verifyingContractAddressInputVerification)) {
|
|
1089
|
+
throw new Error('Verifying contract for InputVerification address is not valid or empty');
|
|
1090
|
+
}
|
|
1091
|
+
if (!aclContractAddress || !ethers.isAddress(aclContractAddress)) {
|
|
1092
|
+
throw new Error('ACL contract address is not valid or empty');
|
|
1093
|
+
}
|
|
1094
|
+
if (publicKey && !(publicKey.data instanceof Uint8Array))
|
|
1095
|
+
throw new Error('publicKey must be a Uint8Array');
|
|
1096
|
+
const provider = getProvider(config);
|
|
1097
|
+
if (!provider) {
|
|
1098
|
+
throw new Error('No network has been provided!');
|
|
1099
|
+
}
|
|
1100
|
+
const chainId = await getChainId(provider, config);
|
|
1101
|
+
const publicKeyData = await getTfheCompactPublicKey(config);
|
|
1102
|
+
const publicParamsData = await getPublicParams(config);
|
|
1103
|
+
const kmsSigners = await getKMSSigners(provider, config);
|
|
1104
|
+
const thresholdKMSSigners = await getKMSSignersThreshold(provider, config);
|
|
1105
|
+
const coprocessorSigners = await getCoprocessorSigners(provider, config);
|
|
1106
|
+
const thresholdCoprocessorSigners = await getCoprocessorSignersThreshold(provider, config);
|
|
1107
|
+
return {
|
|
1108
|
+
createEncryptedInput: createRelayerEncryptedInput(aclContractAddress, verifyingContractAddressInputVerification, chainId, gatewayChainId, cleanURL(config.relayerUrl), publicKeyData.publicKey, publicParamsData, coprocessorSigners, thresholdCoprocessorSigners),
|
|
1109
|
+
generateKeypair,
|
|
1110
|
+
createEIP712: createEIP712(gatewayChainId, verifyingContractAddressDecryption, chainId),
|
|
1111
|
+
publicDecrypt: publicDecryptRequest(kmsSigners, thresholdKMSSigners, gatewayChainId, verifyingContractAddressDecryption, aclContractAddress, cleanURL(config.relayerUrl), provider),
|
|
1112
|
+
userDecrypt: userDecryptRequest(kmsSigners, gatewayChainId, chainId, verifyingContractAddressDecryption, aclContractAddress, cleanURL(config.relayerUrl), provider),
|
|
1113
|
+
getPublicKey: () => publicKeyData.publicKey
|
|
1114
|
+
? {
|
|
1115
|
+
publicKey: publicKeyData.publicKey.safe_serialize(SERIALIZED_SIZE_LIMIT_PK),
|
|
1116
|
+
publicKeyId: publicKeyData.publicKeyId,
|
|
1117
|
+
}
|
|
1118
|
+
: null,
|
|
1119
|
+
getPublicParams: (bits) => {
|
|
1120
|
+
if (publicParamsData[bits]) {
|
|
1121
|
+
return {
|
|
1122
|
+
publicParams: publicParamsData[bits].publicParams.safe_serialize(SERIALIZED_SIZE_LIMIT_CRS),
|
|
1123
|
+
publicParamsId: publicParamsData[bits].publicParamsId,
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
return null;
|
|
1127
|
+
},
|
|
1128
|
+
};
|
|
1129
|
+
};
|
|
1130
|
+
|
|
1131
|
+
const createTfheKeypair = () => {
|
|
1132
|
+
const block_params = new nodeTfhe.ShortintParameters(nodeTfhe.ShortintParametersName.PARAM_MESSAGE_2_CARRY_2_KS_PBS_TUNIFORM_2M128);
|
|
1133
|
+
const casting_params = new nodeTfhe.ShortintCompactPublicKeyEncryptionParameters(nodeTfhe.ShortintCompactPublicKeyEncryptionParametersName.V1_0_PARAM_PKE_MESSAGE_2_CARRY_2_KS_PBS_TUNIFORM_2M128);
|
|
1134
|
+
const config = nodeTfhe.TfheConfigBuilder.default()
|
|
1135
|
+
.use_custom_parameters(block_params)
|
|
1136
|
+
.use_dedicated_compact_public_key_parameters(casting_params)
|
|
1137
|
+
.build();
|
|
1138
|
+
let clientKey = nodeTfhe.TfheClientKey.generate(config);
|
|
1139
|
+
let publicKey = nodeTfhe.TfheCompactPublicKey.new(clientKey);
|
|
1140
|
+
const crs = nodeTfhe.CompactPkeCrs.from_config(config, 4 * 512);
|
|
1141
|
+
return { clientKey, publicKey, crs };
|
|
1142
|
+
};
|
|
1143
|
+
const createTfhePublicKey = () => {
|
|
1144
|
+
const { publicKey } = createTfheKeypair();
|
|
1145
|
+
return toHexString(publicKey.serialize());
|
|
1146
|
+
};
|
|
1147
|
+
|
|
1148
|
+
exports.createEIP712 = createEIP712;
|
|
1149
|
+
exports.createInstance = createInstance;
|
|
1150
|
+
exports.createTfheKeypair = createTfheKeypair;
|
|
1151
|
+
exports.createTfhePublicKey = createTfhePublicKey;
|
|
1152
|
+
exports.generateKeypair = generateKeypair;
|