-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathblocklock.ts
More file actions
314 lines (267 loc) · 11.7 KB
/
blocklock.ts
File metadata and controls
314 lines (267 loc) · 11.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
import {getBytes, Signer, Provider, BigNumberish} from "ethers"
import {
decodeCondition,
encodeCiphertextToSolidity, encodeCondition,
extractSingleLog, parseSolidityCiphertext,
} from "./ethers-utils"
import {Ciphertext, decrypt_g1_with_preprocess, encrypt_towards_identity_g1, G2} from "./crypto/ibe-bn254"
import {BlocklockSender, BlocklockSender__factory} from "./generated"
import {TypesLib} from "./generated/BlocklockSender"
import {
configForChainId,
NetworkConfig,
ARBITRUM_SEPOLIA,
AVALANCHE_C_CHAIN,
BASE_SEPOLIA,
FILECOIN_CALIBNET,
FILECOIN_MAINNET,
OPTIMISM_SEPOLIA,
POLYGON_POS, SEI_TESTNET
} from "./networks"
const BLOCKLOCK_MAX_MSG_LEN: number = 256
const iface = BlocklockSender__factory.createInterface()
export class Blocklock {
private blocklockSender: BlocklockSender
private signer: Signer | Provider
constructor(signer: Signer | Provider, private networkConfig: NetworkConfig) {
this.blocklockSender = BlocklockSender__factory.connect(networkConfig.contractAddress, signer)
this.signer = signer
}
// you can create a Blocklock client using a chainId or any of the static convenience functions at the bottom
static createFromChainId(rpc: Signer | Provider, chainId: BigNumberish): Blocklock {
return new Blocklock(rpc, configForChainId(chainId))
}
/**
* Request a blocklock decryption at block number blockHeight.
* @param blockHeight time at which the decryption should key should be released
* @param ciphertext encrypted message to store on chain
* @param callbackGasLimit the maximum amount of gas the dcipher network should spend on the callback
* @param gasMultiplier a multiplier to use on the gas price for the chain
* @returns blocklock request id as a string
*/
async requestBlocklock(
blockHeight: bigint,
ciphertext: TypesLib.CiphertextStruct,
callbackGasLimit: bigint = this.networkConfig.callbackGasLimitDefault,
): Promise<bigint> {
if (this.signer.provider == null) {
throw new Error("you must configure an RPC provider")
}
const conditionBytes = encodeCondition(blockHeight);
// 1. Estimate request price using the selected txGasPrice
// with chain ID and fee data
const feeData = await this.signer.provider!.getFeeData();
// feeData.maxFeePerGas: Max total gas price we're willing to pay (base + priority), used in EIP-1559
const maxFeePerGas = feeData.maxFeePerGas!;
// feeData.maxPriorityFeePerGas: Tip to incentivize validators (goes directly to them)
const maxPriorityFeePerGas = feeData.maxPriorityFeePerGas!;
// 2. Get network gas price
const txGasPrice = (maxFeePerGas + maxPriorityFeePerGas) * 10n;
// 3. Estimate request price using the network txGasPrice
const requestPrice = await this.blocklockSender.estimateRequestPriceNative(
callbackGasLimit,
txGasPrice
);
// 4. Apply buffer e.g. 100% = 2x total
const valueToSend = requestPrice + (requestPrice * this.networkConfig.gasBufferPercent) / 100n;
// 5. Estimate gas
const estimatedGas = await this.blocklockSender.requestBlocklock.estimateGas(
callbackGasLimit,
conditionBytes,
ciphertext,
{
value: valueToSend,
gasPrice: txGasPrice,
}
);
// 6. Send transaction
const tx = await this.blocklockSender.requestBlocklock(
callbackGasLimit,
conditionBytes,
ciphertext,
{
value: valueToSend,
gasPrice: txGasPrice,
gasLimit: estimatedGas,
}
);
const receipt = await tx.wait();
if (!receipt) {
throw new Error("Transaction was not mined");
}
// 7. Extract request ID from log
const [requestID] = extractSingleLog(
iface,
receipt,
this.networkConfig.contractAddress,
iface.getEvent("BlocklockRequested")
);
return requestID;
}
/**
* Calculates the request price for a blocklock request given the callbackGasLimit.
* @param callbackGasLimit The callbackGasLimit to use when fulfilling the request with a decryption key.
* @returns The estimated request price and the transaction gas price used
*/
async calculateRequestPriceNative(callbackGasLimit: bigint): Promise<[bigint,bigint]> {
// 1. Estimate request price using the selected txGasPrice
// with chain ID and fee data
const feeData = await this.signer.provider!.getFeeData();
// feeData.maxFeePerGas: Max total gas price we're willing to pay (base + priority), used in EIP-1559
const maxFeePerGas = feeData.maxFeePerGas!;
// feeData.maxPriorityFeePerGas: Tip to incentivize validators (goes directly to them)
const maxPriorityFeePerGas = feeData.maxPriorityFeePerGas!;
// 2. Get network gas price
const txGasPrice = (maxFeePerGas + maxPriorityFeePerGas) * 10n;
// 3. Estimate request price using the network txGasPrice
const requestPrice = await this.blocklockSender.estimateRequestPriceNative(
callbackGasLimit,
txGasPrice
);
// 4. Apply buffer e.g. 100% = 2x total
const valueToSend = requestPrice + (requestPrice * this.networkConfig.gasBufferPercent) / 100n;
return [valueToSend, txGasPrice];
}
/**
* Fetch the details of a blocklock request, decryption key / signature excluded.
* This function should be called to fetch pending blocklock requests.
* @param requestId blocklock request id
* @returns details of the blocklock request, undefined if not found
*/
async fetchBlocklockRequest(requestId: bigint): Promise<BlocklockRequest | undefined> {
const request = await this.blocklockSender.getRequest.staticCall(requestId)
const blockHeight = decodeCondition(request.condition)
return {
id: request.decryptionRequestId,
blockHeight: blockHeight,
ciphertext: parseSolidityCiphertext(request.ciphertext)
}
}
/**
* Fetch all blocklock requests, decryption keys / signatures excluded.
* @returns a map with the details of each blocklock request
*/
async fetchAllBlocklockRequests(): Promise<Map<bigint, BlocklockRequest>> {
const requestFilter = this.blocklockSender.filters.BlocklockRequested()
const requests = await this.blocklockSender.queryFilter(requestFilter)
return new Map(Array.from(
requests.map((event) => {
const id = event.args.requestId
const blockHeight = decodeCondition(event.args.condition)
return [id, {
id,
blockHeight,
ciphertext: parseSolidityCiphertext(event.args.ciphertext),
}]
})
))
}
/**
* Fetch the status of a blocklock request, including the decryption key / signature if available.
* This function should be called to fetch blocklock requests that have been fulfilled, or to check
* whether it has been fulfilled or not.
* @param requestId blocklock request id
* @returns details of the blocklock request, undefined if not found
*/
async fetchBlocklockStatus(requestId: bigint): Promise<BlocklockStatus> {
const {condition, ciphertext, decryptionKey} = await this.blocklockSender.getRequest.staticCall(requestId)
const isPending = await this.blocklockSender.isInFlight.staticCall(requestId)
return {
id: requestId,
blockHeight: decodeCondition(condition),
decryptionKey: getBytes(decryptionKey),
ciphertext: parseSolidityCiphertext(ciphertext),
pending: isPending
}
}
/**
* Encrypt a message that can be decrypted once a certain blockHeight is reached.
* @param message plaintext to encrypt
* @param blockHeight time at which the decryption key should be released
* @param pk public key of the scheme
* @returns encrypted message
*/
encrypt(message: Uint8Array, blockHeight: bigint, pk: G2 = this.networkConfig.publicKey): Ciphertext {
if (message.length > BLOCKLOCK_MAX_MSG_LEN) {
throw new Error(`cannot encrypt messages larger than ${BLOCKLOCK_MAX_MSG_LEN} bytes.`)
}
const identity = encodeCondition(blockHeight)
return encrypt_towards_identity_g1(message, identity, pk, this.networkConfig.ibeOpts)
}
/**
* Decrypt a ciphertext using a decryption key.
* @param ciphertext the ciphertext to decrypt
* @param key decryption key
* @returns plaintext
*/
decrypt(ciphertext: Ciphertext, key: Uint8Array): Uint8Array {
if (ciphertext.W.length > BLOCKLOCK_MAX_MSG_LEN) {
throw new Error(`cannot decrypt messages larger than ${BLOCKLOCK_MAX_MSG_LEN} bytes.`)
}
return decrypt_g1_with_preprocess(ciphertext, key, this.networkConfig.ibeOpts)
}
/**
* Encrypt a message that can be decrypted once a certain blockHeight is reached.
* @param message plaintext to encrypt
* @param blockHeight time at which the decryption key should be released
* @param pk public key of the scheme
* @returns the identifier of the blocklock request, and the ciphertext
*/
async encryptAndRegister(message: Uint8Array, blockHeight: bigint, pk: G2 = this.networkConfig.publicKey): Promise<{
id: bigint,
ciphertext: Ciphertext
}> {
const ciphertext = this.encrypt(message, blockHeight, pk)
const id = await this.requestBlocklock(blockHeight, encodeCiphertextToSolidity(ciphertext))
return {id, ciphertext}
}
/**
* Try to decrypt a ciphertext with a specific blocklock id.
* @param requestId blocklock id of the ciphertext to decrypt
* @returns the plaintext if the decryption key is available, undefined otherwise
*/
async decryptWithId(requestId: bigint): Promise<Uint8Array> {
const status = await this.fetchBlocklockStatus(requestId)
if (!status) {
throw new Error("cannot find a request with this identifier")
}
// Decryption key has not been delivered yet, return
if (status.decryptionKey.length === 0) {
return new Uint8Array(0)
}
return this.decrypt(status.ciphertext, status.decryptionKey)
}
static createFilecoinMainnet(rpc: Signer | Provider): Blocklock {
return new Blocklock(rpc, FILECOIN_MAINNET)
}
static createFilecoinCalibnet(rpc: Signer | Provider): Blocklock {
return new Blocklock(rpc, FILECOIN_CALIBNET)
}
static createBaseSepolia(rpc: Signer | Provider): Blocklock {
return new Blocklock(rpc, BASE_SEPOLIA)
}
static createPolygonPos(rpc: Signer | Provider): Blocklock {
return new Blocklock(rpc, POLYGON_POS)
}
static createAvalancheCChain(rpc: Signer | Provider): Blocklock {
return new Blocklock(rpc, AVALANCHE_C_CHAIN)
}
static createOptimismSepolia(rpc: Signer | Provider): Blocklock {
return new Blocklock(rpc, OPTIMISM_SEPOLIA)
}
static createArbitrumSepolia(rpc: Signer | Provider): Blocklock {
return new Blocklock(rpc, ARBITRUM_SEPOLIA)
}
static createSeiTestnet(rpc: Signer | Provider): Blocklock {
return new Blocklock(rpc, SEI_TESTNET)
}
}
export type BlocklockRequest = {
id: bigint,
blockHeight: bigint,
ciphertext: Ciphertext,
}
export type BlocklockStatus = BlocklockRequest & {
decryptionKey: Uint8Array,
pending: boolean,
}