UNPKG

randomness-js

Version:

A library for consuming, verifying and using randomness from the dcipher network

222 lines (189 loc) 8.63 kB
import { BigNumberish, BytesLike, getBytes, keccak256, Provider, Signer, } from "ethers" import {bn254} from "@kevincharm/noble-bn254-drand" import {equalBytes} from "@noble/curves/abstract/utils" import {encodeParams, extractSingleLog} from "./ethers-helpers" import {withTimeout} from "./misc" import {RandomnessSender__factory} from "./generated" import {TypedContractEvent, TypedListener} from "./generated/common" import {RandomnessCallbackSuccessEvent, RandomnessSender} from "./generated/RandomnessSender" /* addresses of the deployed contracts */ export const FURNACE_TESTNET_CONTRACT_ADDRESS = "0x8192aF4ce49f473fCa7e3e5a8d819B0763Def048" export const FILECOIN_CALIBNET_CONTRACT_ADDRESS = "0x91c7774C7476F3832919adE7690467DF91bfd919" export const BASE_SEPOLIA_CONTRACT_ADDRESS = "0x455bfe4B1B4393b458d413E2B0778A95F9B84B82" export const POLYGON_POS_CONTRACT_ADDRESS = "0x455bfe4B1B4393b458d413E2B0778A95F9B84B82" const iface = RandomnessSender__factory.createInterface() export function createBlsDst(chainId: bigint): string { if (chainId <= 0n) { throw new Error("cannot create a BLS domain separator for an invalid chainId") } return `dcipher-randomness-v01-BN254G1_XMD:KECCAK-256_SVDW_RO_${encodeParams(["uint256"], [chainId])}_` } export type RandomnessVerificationParameters = { requestID: bigint, nonce: bigint, randomness: BytesLike signature: BytesLike } export type RandomnessVerificationConfig = { shouldBlowUp: boolean // determines whether the verification function silently returns a boolean on failure or explodes } export class Randomness { private readonly contract: RandomnessSender // any human who can find the right hex format to parse this point shall be crowned the [king|queen|catgirl] of England private readonly pk = new bn254.G2.ProjectivePoint( { c0: 17445541620214498517833872661220947475697073327136585274784354247720096233162n, c1: 18268991875563357240413244408004758684187086817233527689475815128036446189503n }, { c0: 11401601170172090472795479479864222172123705188644469125048759621824127399516n, c1: 8044854403167346152897273335539146380878155193886184396711544300199836788154n }, { c0: 1n, c1: 0n } ) constructor( private readonly rpc: Signer | Provider, private readonly contractAddress: string = FURNACE_TESTNET_CONTRACT_ADDRESS, private readonly chainId: bigint, private readonly defaultRequestTimeoutMs: number = 15_000, ) { console.log(`created randomness-js client with address ${contractAddress}`) this.contract = RandomnessSender__factory.connect(contractAddress, rpc) } static createFilecoinCalibnet(rpc: Signer | Provider): Randomness { // filecoin block time is 30s, so give a longer default timeout return new Randomness(rpc, FILECOIN_CALIBNET_CONTRACT_ADDRESS, 314159n, 90_000) } static createFurnace(rpc: Signer | Provider): Randomness { return new Randomness(rpc, FURNACE_TESTNET_CONTRACT_ADDRESS, 64630n) } static createBaseSepolia(rpc: Signer | Provider): Randomness { return new Randomness(rpc, BASE_SEPOLIA_CONTRACT_ADDRESS, 84532n) } static createPolygonPos(rpc: Signer | Provider): Randomness { return new Randomness(rpc, POLYGON_POS_CONTRACT_ADDRESS, 137n) } static createFromChainId(rpc: Signer | Provider, chainId: BigNumberish): Randomness { switch (chainId.toString().toLowerCase()) { case "314159": case "314159n": case "0x4cb2f": return Randomness.createFilecoinCalibnet(rpc) case "64630": case "64630n": case "0xfc76": return Randomness.createFurnace(rpc) case "84532": case "84532n": case "0x14a34": return Randomness.createBaseSepolia(rpc) case "137": case "137n": case "0x89": return Randomness.createPolygonPos(rpc) default: throw new Error("unsupported chainId :(") } } async requestRandomness(confirmations = 1, timeoutMs = this.defaultRequestTimeoutMs): Promise<RandomnessVerificationParameters> { if (this.rpc.provider == null) { throw Error("RPC requires a provider to request randomness") } const receipt = await withTimeout( this.contract.requestRandomness().then(tx => tx.wait(confirmations)), timeoutMs ) if (!receipt) { throw Error("no receipt because confirmations were 0") } const [requestID, nonce] = extractSingleLog(iface, receipt, this.contractAddress, iface.getEvent("RandomnessRequested")) return new Promise((resolve, reject) => { // then we have to both check the past and listen to the future for emitted events // lest we miss our fulfilled randomness. We set the listeners first... in case // by some magic our request is fulfilled _between_ the lines const successFilter = this.contract.filters.RandomnessCallbackSuccess(requestID) // how many blocks we're willing to look back - don't really expect it to be more than 1 const blockLookBack = 3 // eslint-disable-next-line @typescript-eslint/no-explicit-any let timeout: any = null // cleanup to do once we've managed all the events const cleanup = () => { this.contract.off(successFilter) clearTimeout(timeout) } const randomnessCallback = (result: RandomnessVerificationParameters) => { resolve(result) cleanup() } const randomnessListener = createRandomnessListener(nonce, randomnessCallback) this.contract.once(successFilter, randomnessListener) const randomnessFilter = createRandomnessLogListener(nonce, randomnessCallback) this.contract.queryFilter(successFilter, -blockLookBack) .then(randomnessFilter) .catch(reject) timeout = setTimeout(() => { cleanup() reject(new Error("timed out requesting randomness")) }, timeoutMs) }) } async verify( parameters: RandomnessVerificationParameters, config: RandomnessVerificationConfig = {shouldBlowUp: true} ): Promise<boolean> { const {randomness, signature, nonce} = parameters const signatureBytes = getBytes(signature) if (!equalBytes(getBytes(keccak256(signatureBytes)), getBytes(randomness))) { throw Error("randomness did not match the signature provided") } // we go through these hoops to give callers the option of using the boolean or // using exceptions for control flow let verifies = false let errorDuringVerification = false try { const m = getBytes(keccak256(encodeParams(["uint256"], [nonce]))) verifies = bn254.verifyShortSignature(signatureBytes, m, this.pk, {DST: createBlsDst(this.chainId)}) // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (_) { errorDuringVerification = true } if (verifies) { return true } if (!config.shouldBlowUp) { return false } if (!errorDuringVerification) { throw Error("signature failed to verify") } throw Error("error during signature verification: was your signature formatted correctly?") } } function createRandomnessListener(nonce: bigint, cb: (arg: RandomnessVerificationParameters) => void): TypedListener<TypedContractEvent> { return (log: RandomnessCallbackSuccessEvent.Log) => { const [requestID, randomness, signature] = log.args cb({requestID, nonce, randomness, signature}) } } function createRandomnessLogListener(nonce: bigint, cb: (arg: RandomnessVerificationParameters) => void) { return (logs: RandomnessCallbackSuccessEvent.Log[]) => { if (logs.length === 0) { return } if (!logs[0].args) { console.error("got a log without args somehow...") return } const [requestID, randomness, signature] = logs[0].args cb({requestID, randomness, signature, nonce}) } }