blocklock-js
Version:
A library for encrypting and decrypting data for the future
401 lines (345 loc) • 15.3 kB
text/typescript
import { getBytes, ethers, Signer, Provider, BigNumberish, BytesLike } from "ethers"
import { keccak_256 } from "@noble/hashes/sha3"
import {
encodeCiphertextToSolidity,
encodeParams,
extractSingleLog, parseSolidityCiphertext,
} from "./ethers-utils"
import { Ciphertext, decrypt_g1_with_preprocess, encrypt_towards_identity_g1, G2, IbeOpts } from "./crypto/ibe-bn254"
import { BlocklockSender, BlocklockSender__factory } from "./generated"
import { TypesLib } from "./generated/BlocklockSender"
export type BigIntPair = {
c0: bigint;
c1: bigint;
};
export type BlockLockPublicKey = {
x: BigIntPair;
y: BigIntPair;
};
const BLOCKLOCK_MAX_MSG_LEN: number = 256
const createBlocklockIbeOpts = (chainId: bigint): IbeOpts => ({
hash: keccak_256,
k: 128,
expand_fn: "xmd",
dsts: {
H1_G1: Buffer.from(`BLOCKLOCK_BN254G1_XMD:KECCAK-256_SVDW_RO_H1_${encodeParams(["uint256"], [chainId])}_`),
H2: Buffer.from(`BLOCKLOCK_BN254_XMD:KECCAK-256_H2_${encodeParams(["uint256"], [chainId])}_`),
H3: Buffer.from(`BLOCKLOCK_BN254_XMD:KECCAK-256_H3_${encodeParams(["uint256"], [chainId])}_`),
H4: Buffer.from(`BLOCKLOCK_BN254_XMD:KECCAK-256_H4_${encodeParams(["uint256"], [chainId])}_`),
},
})
export const BLOCKLOCK_TESTNET_PUBLIC_KEY: BlockLockPublicKey = {
x: {
c0: BigInt("0x2691d39ecc380bfa873911a0b848c77556ee948fb8ab649137d3d3e78153f6ca"),
c1: BigInt("0x2863e20a5125b098108a5061b31f405e16a069e9ebff60022f57f4c4fd0237bf"),
},
y: {
c0: BigInt("0x193513dbe180d700b189c529754f650b7b7882122c8a1e242a938d23ea9f765c"),
c1: BigInt("0x11c939ea560caf31f552c9c4879b15865d38ba1dfb0f7a7d2ac46a4f0cae25ba"),
},
};
export const BLOCKLOCK_MAINNET_PUBLIC_KEY : BlockLockPublicKey = {
x: {
c0: BigInt("0x2b0985484a2503404d6c2b183d8be1e38aeb548f0435a935e4058499980c22a4"),
c1: BigInt("0x2eb81c7b1bb618894ad337b68d7ddf1d19f9b786d20b9de7735da410a98e458c"),
},
y: {
c0: BigInt("0x10c1587f14b640331d0dfd0edf9817e2c650c15e70c7bf408124d58e24b0ff3c"),
c1: BigInt("0xbd0526dce7136f54cf574cfd26e8d2d8c61c5218afb9d4466f9177674f436f4"),
},
};
type GasParams = {
gasLimit: number
maxFeePerGas: bigint
maxPriorityFeePerGas: bigint
}
const defaultGasParams: GasParams = {
gasLimit: 100_000,
maxFeePerGas: ethers.parseUnits("0.2", "gwei"),
maxPriorityFeePerGas: ethers.parseUnits("0.2", "gwei"),
}
const filecoinGasParams: GasParams = {
gasLimit: defaultGasParams.gasLimit * 500,
maxFeePerGas: ethers.parseUnits("0.2", "gwei"),
maxPriorityFeePerGas: ethers.parseUnits("0.2", "gwei"),
}
/* addresses of the deployed blocklockSender contracts */
export const FURNACE_TESTNET_CONTRACT_ADDRESS = "0xEd925F96790F11678972b0F2c250498D782DDec9"
export const FILECOIN_CALIBNET_CONTRACT_ADDRESS = "0xF00aB3B64c81b6Ce51f8220EB2bFaa2D469cf702"
export const FILECOIN_MAINNET_CONTRACT_ADDRESS = "0x34092470CC59A097d770523931E3bC179370B44b"
export const BASE_SEPOLIA_CONTRACT_ADDRESS = "0x82Fed730CbdeC5A2D8724F2e3b316a70A565e27e"
export const POLYGON_POS_CONTRACT_ADDRESS = "0x82Fed730CbdeC5A2D8724F2e3b316a70A565e27e"
const iface = BlocklockSender__factory.createInterface()
export class Blocklock {
private blocklockSender: BlocklockSender
private blocklockPublicKey: BlockLockPublicKey
private ibeOpts: IbeOpts
private gasParams: GasParams
private signer: Signer | Provider
constructor(
signer: Signer | Provider,
private readonly blocklockSenderContractAddress: string,
chainId: bigint,
gasParams: GasParams = defaultGasParams,
blocklockPublicKey: BlockLockPublicKey = BLOCKLOCK_TESTNET_PUBLIC_KEY,
) {
this.blocklockSender = BlocklockSender__factory.connect(blocklockSenderContractAddress, signer)
this.blocklockPublicKey = blocklockPublicKey
this.gasParams = gasParams
this.ibeOpts = createBlocklockIbeOpts(chainId)
this.signer = signer
}
static createFilecoinMainnet(rpc: Signer | Provider): Blocklock {
return new Blocklock(rpc, FILECOIN_MAINNET_CONTRACT_ADDRESS, 314n, filecoinGasParams, BLOCKLOCK_MAINNET_PUBLIC_KEY)
}
static createFilecoinCalibnet(rpc: Signer | Provider): Blocklock {
return new Blocklock(rpc, FILECOIN_CALIBNET_CONTRACT_ADDRESS, 314159n, filecoinGasParams)
}
static createFurnace(rpc: Signer | Provider): Blocklock {
return new Blocklock(rpc, FURNACE_TESTNET_CONTRACT_ADDRESS, 64630n)
}
static createBaseSepolia(rpc: Signer | Provider): Blocklock {
return new Blocklock(rpc, BASE_SEPOLIA_CONTRACT_ADDRESS, 84532n)
}
static createPolygonPos(rpc: Signer | Provider): Blocklock {
return new Blocklock(rpc, POLYGON_POS_CONTRACT_ADDRESS, 137n)
}
static createFromChainId(rpc: Signer | Provider, chainId: BigNumberish): Blocklock {
switch (chainId.toString().toLowerCase()) {
case "314159":
case "314159n":
case "0x4cb2f":
return Blocklock.createFilecoinCalibnet(rpc)
case "314":
case "314n":
case "0x13a":
return Blocklock.createFilecoinMainnet(rpc)
case "64630":
case "64630n":
case "0xfc76":
return Blocklock.createFurnace(rpc)
case "84532":
case "84532n":
case "0x14a34":
return Blocklock.createBaseSepolia(rpc)
case "137":
case "137n":
case "0x89":
return Blocklock.createPolygonPos(rpc)
default:
throw new Error("unsupported 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
* @returns blocklock request id as a string
*/
async requestBlocklock(blockHeight: bigint, ciphertext: TypesLib.CiphertextStruct): Promise<bigint> {
const conditionBytes = encodeCondition(blockHeight);
// 1. Get chain ID and fee data
const network = await this.signer.provider!.getNetwork();
const chainId = network.chainId;
const feeData = await this.signer.provider!.getFeeData();
// feeData.gasPrice: Legacy flat gas price (used on non-EIP-1559 chains like Filecoin or older EVMs)
const gasPrice = feeData.gasPrice!;
// 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!;
// latestblock.baseFeePerGas: Minimum gas price required by the network (burned), set by latest block
// const latestBlock = await this.signer.provider!.getBlock("latest");
// const baseFeePerGas = latestBlock!.baseFeePerGas; // BigNumber (v5) or bigint (v6)
// 2. Determine whether to use legacy or EIP-1559 pricing
const isFilecoin = Number(chainId) === 314 || Number(chainId) === 314159;
let txGasPrice: bigint;
if (isFilecoin) {
// Use legacy gasPrice directly
txGasPrice = gasPrice > 0? gasPrice * 10n : (maxFeePerGas + maxPriorityFeePerGas) * 10n;
} else {
// Use effective gas price based on EIP-1559
txGasPrice = maxFeePerGas + maxPriorityFeePerGas;
}
// 3. Estimate request price using the selected txGasPrice
const requestPrice = await this.blocklockSender.estimateRequestPriceNative(
this.gasParams.gasLimit,
txGasPrice
);
// 4. Apply buffer (e.g. 100% = 2× total)
const bufferPercent = isFilecoin? 300n: 100n;
const valueToSend = requestPrice + (requestPrice * bufferPercent) / 100n;
// 5. Estimate gas
const estimatedGas = await this.blocklockSender.requestBlocklock.estimateGas(
this.gasParams.gasLimit,
conditionBytes,
ciphertext,
isFilecoin
? { value: valueToSend, gasPrice: txGasPrice }
: {
value: valueToSend,
maxFeePerGas,
maxPriorityFeePerGas,
}
);
// 6. Send transaction
const tx = await this.blocklockSender.requestBlocklock(
this.gasParams.gasLimit,
conditionBytes,
ciphertext,
isFilecoin
? {
value: valueToSend,
gasLimit: estimatedGas,
gasPrice: txGasPrice,
}
: {
value: valueToSend,
gasLimit: estimatedGas,
maxFeePerGas,
maxPriorityFeePerGas,
}
);
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.blocklockSenderContractAddress,
iface.getEvent("BlocklockRequested")
);
return requestID;
}
/**
* 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)
return {
id: requestId,
blockHeight: decodeCondition(condition),
decryptionKey: getBytes(decryptionKey),
ciphertext: parseSolidityCiphertext(ciphertext),
}
}
/**
* 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.blocklockPublicKey): 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.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.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.blocklockPublicKey): 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)
}
}
export type BlocklockRequest = {
id: bigint,
blockHeight: bigint,
ciphertext: Ciphertext,
}
export type BlocklockStatus = BlocklockRequest & {
decryptionKey: Uint8Array,
}
// encodes a block height condition with the correct prefix
export function encodeCondition(blockHeight: bigint): Uint8Array {
const blockHeightBytes = getBytes(encodeParams(["uint256"], [blockHeight]))
// 0x42 is the magic 'B' tag for the `blockHeight` condition
return new Uint8Array([0x42, ...blockHeightBytes])
}
export function decodeCondition(bytes: BytesLike): bigint {
const b = getBytes(bytes)
if (b[0] !== 0x42) {
throw new Error("unexpected condition tag: expected `b` for blocklock!")
}
const [round] = ethers.AbiCoder.defaultAbiCoder().decode(["uint256"], b.slice(1))
return round
}