blocklock-js
Version:
A library for encrypting and decrypting data for the future
333 lines (281 loc) • 12.8 kB
text/typescript
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,
ARBITRUM_MAINNET,
AVALANCHE_C_CHAIN,
BASE_SEPOLIA,
BASE_MAINNET,
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,
bufferPercent?: bigint
): 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. Determine buffer (use provided one or fallback)
const effectiveBuffer = bufferPercent ?? this.networkConfig.gasBufferPercent;
// 5. Apply buffer e.g. 100% = 2x total
const valueToSend = requestPrice + (requestPrice * effectiveBuffer) / 100n;
// 6. Estimate gas
const estimatedGas = await this.blocklockSender.requestBlocklock.estimateGas(
callbackGasLimit,
conditionBytes,
ciphertext,
{
value: valueToSend,
gasPrice: txGasPrice,
}
);
// 7. 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");
}
// 8. 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.
* @param bufferPercent Optional buffer percent to apply on top of the estimated request price. If not provided, the default from the network config will be used.
* @returns The estimated request price and the transaction gas price used
*/
async calculateRequestPriceNative(callbackGasLimit: bigint, bufferPercent?: 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. Determine buffer (use provided one or fallback)
const effectiveBuffer = bufferPercent ?? this.networkConfig.gasBufferPercent;
// 5. Apply buffer e.g. 100% = 2x total
const valueToSend = requestPrice + (requestPrice * effectiveBuffer) / 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 createBaseMainnet(rpc: Signer | Provider): Blocklock {
return new Blocklock(rpc, BASE_MAINNET)
}
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 createArbitrumMainnet(rpc: Signer | Provider): Blocklock {
return new Blocklock(rpc, ARBITRUM_MAINNET)
}
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,
}