randomness-js
Version:
A library for consuming, verifying and using randomness from the dcipher network
222 lines (189 loc) • 8.63 kB
text/typescript
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})
}
}