Skip to content

Commit

Permalink
beacon verification for g1/g2 swap network (#63)
Browse files Browse the repository at this point in the history
unit and integration tests for the new scheme
  • Loading branch information
CluEleSsUK authored Mar 7, 2023
1 parent e5cee71 commit 73dfcfe
Show file tree
Hide file tree
Showing 5 changed files with 175 additions and 52 deletions.
74 changes: 57 additions & 17 deletions lib/beacon-verification.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,79 @@
import * as bls from '@noble/bls12-381'
import {ChainedBeacon, ChainInfo, isChainedBeacon, isUnchainedBeacon, RandomnessBeacon, UnchainedBeacon} from './index'
import {PointG1, PointG2, Fp12, pairing} from '@noble/bls12-381';

import {
G2ChainedBeacon,
ChainInfo,
isChainedBeacon,
isUnchainedBeacon,
RandomnessBeacon,
G2UnchainedBeacon,
isG1G2SwappedBeacon,
G1UnchainedBeacon
} from './index'

async function verifyBeacon(chainInfo: ChainInfo, beacon: RandomnessBeacon): Promise<boolean> {
const publicKey = chainInfo.public_key

let message: Uint8Array
if (!await randomnessIsValid(beacon)) {
return false
}

if (isChainedBeacon(beacon, chainInfo)) {
message = await chainedBeaconMessage(beacon)
return bls.verify(beacon.signature, await chainedBeaconMessage(beacon), publicKey)
}

} else if (isUnchainedBeacon(beacon, chainInfo)) {
message = await unchainedBeaconMessage(beacon)
} else {
console.error(`Beacon type ${chainInfo.schemeID} was not supported`)
return false
if (isUnchainedBeacon(beacon, chainInfo)) {
return bls.verify(beacon.signature, await unchainedBeaconMessage(beacon), publicKey)
}

const signatureVerifies = await bls.verify(beacon.signature, message, publicKey)
if (!(signatureVerifies && await randomnessIsValid(beacon))) {
console.error('Beacon returned was invalid')
return false
if (isG1G2SwappedBeacon(beacon, chainInfo)) {
return verifySigOnG1(beacon.signature, await unchainedBeaconMessage(beacon), publicKey)
}
return true

console.error(`Beacon type ${chainInfo.schemeID} was not supported`)
return false

}

// @noble/bls12-381 does everything on G2, so we've implemented a manual verification for beacons on G1
type G1Hex = Uint8Array | string | PointG1;
type G2Hex = Uint8Array | string | PointG2;

function normP1(point: G1Hex): PointG1 {
return point instanceof PointG1 ? point : PointG1.fromHex(point);
}

function normP2(point: G2Hex): PointG2 {
return point instanceof PointG2 ? point : PointG2.fromHex(point);
}

async function normP1Hash(point: G1Hex): Promise<PointG1> {
return point instanceof PointG1 ? point : PointG1.hashToCurve(point);
}

export async function verifySigOnG1(signature: G1Hex, message: G1Hex, publicKey: G2Hex): Promise<boolean> {
const P = normP2(publicKey);
const Hm = await normP1Hash(message);
const G = PointG2.BASE;
const S = normP1(signature);
const ePHm = pairing(Hm, P.negate(), false);
const eGS = pairing(S, G, false);
const exp = eGS.multiply(ePHm).finalExponentiate();
return exp.equals(Fp12.ONE);
}

async function chainedBeaconMessage(beacon: ChainedBeacon): Promise<Uint8Array> {
async function chainedBeaconMessage(beacon: G2ChainedBeacon): Promise<Uint8Array> {
const message = Buffer.concat([
signatureBuffer(beacon.previous_signature),
roundBuffer(beacon.round)
])

return await bls.utils.sha256(message)
return bls.utils.sha256(message)
}

async function unchainedBeaconMessage(beacon: UnchainedBeacon): Promise<Uint8Array> {
return await bls.utils.sha256(roundBuffer(beacon.round))
async function unchainedBeaconMessage(beacon: G2UnchainedBeacon | G1UnchainedBeacon): Promise<Uint8Array> {
return bls.utils.sha256(roundBuffer(beacon.round))
}

function signatureBuffer(sig: string) {
Expand Down
29 changes: 24 additions & 5 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,23 +144,33 @@ export type ChainInfo = {
}

// currently drand supports chained and unchained randomness - read more here: https://drand.love/docs/cryptography/#randomness
export type RandomnessBeacon = ChainedBeacon | UnchainedBeacon
export type RandomnessBeacon = G2ChainedBeacon | G2UnchainedBeacon | G1UnchainedBeacon

export type ChainedBeacon = {
export type G2ChainedBeacon = {
round: number
randomness: string
signature: string
previous_signature: string
}

export type UnchainedBeacon = {
export type G2UnchainedBeacon = {
round: number
randomness: string
signature: string
// this is needed to distinguish it from the `G1UnchainedBeacon` so the type guard works correctly
_phantomg2?: never
}

export type G1UnchainedBeacon = {
round: number
randomness: string
signature: string
// this distinguishes it from the `G2UnchainedBeacon` so the type guard works correctly
_phantomg1?: never
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function isChainedBeacon(value: any, info: ChainInfo): value is ChainedBeacon {
export function isChainedBeacon(value: any, info: ChainInfo): value is G2ChainedBeacon {
return info.schemeID === 'pedersen-bls-chained' &&
!!value.previous_signature &&
!!value.randomness &&
Expand All @@ -170,13 +180,22 @@ export function isChainedBeacon(value: any, info: ChainInfo): value is ChainedBe


// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function isUnchainedBeacon(value: any, info: ChainInfo): value is UnchainedBeacon {
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
}

// 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}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "drand-client",
"version": "1.0.0-pre.10",
"version": "1.0.0-pre.11",
"description": "A client to the drand randomness beacon network.",
"main": "index.js",
"types": "index.d.ts",
Expand Down
25 changes: 25 additions & 0 deletions test/beacon-verification.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,4 +152,29 @@ describe('verifyBeacon', () => {

expect(await verifyBeacon(chainInfo, beacon)).toBeFalsy()
})
describe('signatures on G1', () => {
const validBeacon = {
round: 3,
randomness: 'a4eb0ed6c4132da066843c3bfdce732ce5013eda86e74c136ab8ccc387b798dd',
signature: '8176555f90d71aa49ceb37739683749491c2bab15a46094b255289ed25cf8f01cdfb1fe8bd9cd5a19eb09448a3e53186'
}
const chainInfo = {
public_key: 'a0b862a7527fee3a731bcb59280ab6abd62d5c0b6ea03dc4ddf6612fdfc9d01f01c31542541771903475eb1ec6615f8d0df0b8b6dce385811d6dcf8cbefb8759e5e616a3dfd054c928940766d9a5b9db91e3b697e5d70a975181e007f87fca5e',
period: 3,
genesis_time: 1677685200,
hash: 'dbd506d6ef76e5f386f41c651dcb808c5bcbd75471cc4eafa3f4df7ad4e4c493',
groupHash: 'a81e9d63f614ccdb144b8ff79fbd4d5a2d22055c0bfe4ee9a8092003dab1c6c0',
schemeID: 'bls-unchained-on-g1',
metadata: {'beaconID': 'fastnet'}
}

it('should verify a signature on G1', async () => {
await expect(verifyBeacon(chainInfo, validBeacon)).resolves.toEqual(true)
})

it('should not verify a signature on G1 for the wrong round', async () => {
const invalidBeacon = {...validBeacon, round: 55}
await expect(verifyBeacon(chainInfo, invalidBeacon)).resolves.toEqual(false)
})
})
})
97 changes: 68 additions & 29 deletions test/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,76 @@ import {fetchBeacon, fetchBeaconByTime, HttpCachingChain, HttpChainClient, watch
import 'jest-fetch-mock'

describe('randomness client', () => {
const testnetUnchainedUrl = 'https://pl-eu.testnet.drand.sh/7672797f548f3f4748ac4bf3352fc6c6b6468c9ad40ad456a397545c6e2df5bf';
const chain = new HttpCachingChain(testnetUnchainedUrl)
const client = new HttpChainClient(chain)

it('can consume randomness', async () => {
const beacon = await fetchBeaconByTime(client, Date.now())
expect(beacon.round).toBeGreaterThan(0)
})
it('can consume some round', async () => {
const beacon = await fetchBeacon(client, 7456110)
expect(beacon.round).toBeGreaterThan(0)
describe('testnet default network', () => {
const testnetUnchainedUrl = 'https://pl-eu.testnet.drand.sh/7672797f548f3f4748ac4bf3352fc6c6b6468c9ad40ad456a397545c6e2df5bf';
const chain = new HttpCachingChain(testnetUnchainedUrl)
const client = new HttpChainClient(chain)

it('can consume randomness', async () => {
const beacon = await fetchBeaconByTime(client, Date.now())
expect(beacon.round).toBeGreaterThan(0)
})
it('can consume some round', async () => {
const beacon = await fetchBeacon(client, 7456110)
expect(beacon.round).toBeGreaterThan(0)
})
it('watch returns successive rounds', async () => {
const generator = watch(client, new AbortController())

const nextIsExpected = async (expectedRound: number) => {
const beacon = await generator.next()
expect(beacon.done).toBeFalsy()
expect(beacon.value).toBeDefined()
expect(beacon.value.round).toEqual(expectedRound)
}

const beacon = await generator.next()
expect(beacon.value).toBeDefined()

const round = beacon.value.round
await nextIsExpected(round + 1)
await nextIsExpected(round + 2)
await nextIsExpected(round + 3)
await nextIsExpected(round + 4)
await nextIsExpected(round + 5)
await nextIsExpected(round + 6)
}, 30000) // test should be longer than the frequency
})
it('watch returns successive rounds', async () => {
const generator = watch(client, new AbortController())

const nextIsExpected = async (expectedRound: number) => {
describe('testnet g1/g2 swapped network', () => {
const testnetUnchainedUrl = 'https://pl-eu.testnet.drand.sh/f3827d772c155f95a9fda8901ddd59591a082df5ac6efe3a479ddb1f5eeb202c';
const chain = new HttpCachingChain(testnetUnchainedUrl)
const client = new HttpChainClient(chain)

it('can consume randomness', async () => {
const beacon = await fetchBeaconByTime(client, Date.now())
expect(beacon.round).toBeGreaterThan(0)
})
it('can consume some round', async () => {
const beacon = await fetchBeacon(client, 100)
expect(beacon.round).toBeGreaterThan(0)
})
it('watch returns successive rounds', async () => {
const generator = watch(client, new AbortController())

const nextIsExpected = async (expectedRound: number) => {
const beacon = await generator.next()
expect(beacon.done).toBeFalsy()
expect(beacon.value).toBeDefined()
expect(beacon.value.round).toEqual(expectedRound)
}

const beacon = await generator.next()
expect(beacon.done).toBeFalsy()
expect(beacon.value).toBeDefined()
expect(beacon.value.round).toEqual(expectedRound)
}

const beacon = await generator.next()
expect(beacon.value).toBeDefined()

const round = beacon.value.round
await nextIsExpected(round + 1)
await nextIsExpected(round + 2)
await nextIsExpected(round + 3)
await nextIsExpected(round + 4)
await nextIsExpected(round + 5)
await nextIsExpected(round + 6)
}, 30000) // test should be longer than the frequency
})

const round = beacon.value.round
await nextIsExpected(round + 1)
await nextIsExpected(round + 2)
await nextIsExpected(round + 3)
await nextIsExpected(round + 4)
await nextIsExpected(round + 5)
await nextIsExpected(round + 6)
}, 30000) // test should be longer than the frequency
})
})

0 comments on commit 73dfcfe

Please sign in to comment.