UNPKG

@bug-fix/drand-client

Version:

A client to the drand randomness beacon network.

232 lines (194 loc) 8.2 kB
import HttpCachingChain from './http-caching-chain' import {HttpChain} from './http-caching-chain' import HttpChainClient from './http-chain-client' import FastestNodeClient from './fastest-node-client' import MultiBeaconNode from './multi-beacon-node' import {retryOnError, roundAt, roundTime, sleep} from './util' import {verifyBeacon} from './beacon-verification' import {defaultClient, fastnetClient, quicknetClient, testnetDefaultClient, testnetQuicknetClient} from './defaults' // functionality for inspecting a drand node export interface DrandNode { chains(): Promise<Array<Chain>> health(): Promise<HealthCheckResponse> } // functionality for a given chain hosted by a node export interface Chain { baseUrl: string info(): Promise<ChainInfo> } export type ChainOptions = { // setting this to true will skip validation of beacons signatures (not recommended) disableBeaconVerification: boolean // setting this will add a query param to requests to stop providers returning a cached version noCache: boolean // adding these params will verify that the chain info from the requested chain matches them, otherwise an error will be thrown. // Leaving them out assumes that you are sure the `baseUrl` you are using for the chain client is the correct chain chainVerificationParams?: ChainVerificationParams } export const defaultChainOptions: ChainOptions = { disableBeaconVerification: false, noCache: false, } // these should correspond to `hash` and `public_key` in the `ChainInfo` below export type ChainVerificationParams = { chainHash: string publicKey: string } // this is aggregation of information returned by the `/health` endpoint of a node export type HealthCheckResponse = { // the http status code returned from the node's healthcheck status: number // the current round this node has caught up to. -1 when the service cannot be contacted current: number // the expected current round. -1 when the service cannot be contacted expected: number } // functionality for fetching individual beacons for a given `Chain` // you can implement this yourself to support protocols other than HTTP export interface ChainClient { options: ChainOptions latest(): Promise<RandomnessBeacon> get(roundNumber: number): Promise<RandomnessBeacon> chain(): Chain } // fetch a beacon for a given `roundNumber` or get the latest beacon by omitting the `roundNumber` export async function fetchBeacon(client: ChainClient, roundNumber?: number): Promise<RandomnessBeacon> { if (!roundNumber) { roundNumber = roundAt(Date.now(), await client.chain().info()) } if (roundNumber < 1) { throw Error('Cannot request lower than round number 1') } const beacon = await client.get(roundNumber) return validatedBeacon(client, beacon, roundNumber) } // fetch the most recent beacon to have been emitted at a given `time` in epoch ms export async function fetchBeaconByTime(client: ChainClient, time: number): Promise<RandomnessBeacon> { const info = await client.chain().info() const roundNumber = roundAt(time, info) return fetchBeacon(client, roundNumber) } // an async generator emitting beacons from the latest round onwards export async function* watch( client: ChainClient, abortController: AbortController, options: WatchOptions = defaultWatchOptions, ): AsyncGenerator<RandomnessBeacon> { const info = await client.chain().info() let currentRound = roundAt(Date.now(), info) while (!abortController.signal.aborted) { const now = Date.now() await sleep(roundTime(info, currentRound) - now) const beacon = await retryOnError(async () => client.get(currentRound), options.retriesOnFailure) yield validatedBeacon(client, beacon, currentRound) currentRound = currentRound + 1 } } // options for the async generator `watch` function export type WatchOptions = { // the number of retries to attempt for a failed response // could be useful if e.g. the network does not generate a round perfectly on time retriesOnFailure: number } const defaultWatchOptions = { retriesOnFailure: 3 } // internal function for validating a beacon if validation has not been disabled in the client options async function validatedBeacon(client: ChainClient, beacon: RandomnessBeacon, expectedRound: number): Promise<RandomnessBeacon> { if (client.options.disableBeaconVerification) { return beacon } const info = await client.chain().info() if (!await verifyBeacon(info, beacon, expectedRound)) { throw Error('The beacon retrieved was not valid!') } return beacon } // `ChainInfo` is returned by a node's `/info` endpoint export type ChainInfo = { public_key: string // hex encoded BLS12-381 public key period: number // how often the network emits randomness (in seconds) genesis_time: number // the time of the round 0 of the network (in epoch seconds) hash: string // the hash identifying this specific chain of beacons groupHash: string // a hash of the group file containing details of all the nodes participating in the network schemeID: string // the version/format of cryptography metadata: { beaconID: string // the ID of the beacon chain this `ChainInfo` corresponds to } } // currently drand supports chained and unchained randomness - read more here: https://drand.love/docs/cryptography/#randomness export type RandomnessBeacon = G2ChainedBeacon | G2UnchainedBeacon | G1UnchainedBeacon | G1RFC9380Beacon export type G2ChainedBeacon = { round: number randomness: string signature: string previous_signature: string } export type G2UnchainedBeacon = { round: number randomness: string signature: string // this is needed to distinguish it from the other unchained beacons so the type guard works correctly _phantomg2?: never } export type G1UnchainedBeacon = { round: number randomness: string signature: string // this distinguishes it from the other unchained beacons so the type guard works correctly _phantomg1?: never } export type G1RFC9380Beacon = { round: number randomness: string signature: string // this distinguishes it from the other unchained beacons so the type guard works correctly _phantomg19380?: never } // eslint-disable-next-line @typescript-eslint/no-explicit-any export function isChainedBeacon(value: any, info: ChainInfo): value is G2ChainedBeacon { return info.schemeID === 'pedersen-bls-chained' && !!value.previous_signature && !!value.randomness && !!value.signature && value.round > 0 } // eslint-disable-next-line @typescript-eslint/no-explicit-any export function isUnchainedBeacon(value: any, info: ChainInfo): value is G2UnchainedBeacon { return info.schemeID === 'pedersen-bls-unchained' && !!value.randomness && !!value.signature && value.previous_signature === undefined && value.round > 0 } // eslint-disable-next-line @typescript-eslint/no-explicit-any export function isG1G2SwappedBeacon(value: any, info: ChainInfo): value is G1UnchainedBeacon { return info.schemeID === 'bls-unchained-on-g1' && !!value.randomness && !!value.signature && value.previous_signature === undefined && value.round > 0 } // eslint-disable-next-line @typescript-eslint/no-explicit-any export function isG1Rfc9380(value: any, info: ChainInfo): value is G1RFC9380Beacon { return info.schemeID === 'bls-unchained-g1-rfc9380' && !!value.randomness && !!value.signature && value.previous_signature === undefined && value.round > 0 } // exports some default implementations of the above interfaces and other utility functions that could be used with them export { HttpChain, HttpChainClient, HttpCachingChain, MultiBeaconNode, FastestNodeClient, roundAt, roundTime, defaultClient, quicknetClient, fastnetClient, testnetDefaultClient, testnetQuicknetClient, }