Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: simplify interface #182

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [9.0.0] - TODO

### Features

- simplify interface [#182](https://github.com/ChainSafe/bls/pull/182)

## [7.1.1] - 2022-05-15

### Chores
Expand Down
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@ yarn add @chainsafe/bls @chainsafe/blst

By default, native bindings will be used if in NodeJS and they are installed. A WASM implementation ("herumi") is used as a fallback in case any error occurs.

The `blst-native` implementation offers a multi-threaded approach to verification and utilizes the libuv worker pool to verification. It is a more performant options synchronously and FAR better when utilized asynchronously. All verification functions provide sync and async versions. Both the `blst-native` and `herumi` implementations offer verification functions with `async` prefixes as free functions and also on their respective classes. This was done to preserve the isomorphic architecture of this library. In reality however, only the `blst-native` bindings have the ability to implement a promise based approach. In the `herumi` version the async version just proxies to the sync version under the hood.

```ts
import bls from "@chainsafe/bls";

Expand Down
1 change: 1 addition & 0 deletions karma.conf.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ module.exports = function (config) {
resolve: webpackConfig.resolve,
experiments: webpackConfig.experiments,
optimization: webpackConfig.optimization,
externals: webpackConfig.externals,
stats: {warnings:false},
},
reporters: ["spec"],
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@chainsafe/bls",
"version": "8.1.0",
"version": "9.0.0",
"description": "Implementation of bls signature verification for ethereum 2.0",
"engines": {
"node": ">=18"
Expand Down Expand Up @@ -83,7 +83,7 @@
"bls-eth-wasm": "^1.1.1"
},
"devDependencies": {
"@chainsafe/blst": "^1.0.0",
"@chainsafe/blst": "^2.0.1",
"@chainsafe/eslint-plugin-node": "^11.2.3",
"@chainsafe/threads": "^1.9.0",
"@lodestar/spec-test-util": "1.13.0",
Expand Down Expand Up @@ -121,7 +121,7 @@
"v8-profiler-next": "1.10.0"
},
"peerDependencies": {
"@chainsafe/blst": "^1.0.0"
"@chainsafe/blst": "^2.0.1"
},
"peerDependenciesMeta": {
"@chainsafe/blst": {
Expand Down
49 changes: 12 additions & 37 deletions src/blst-native/publicKey.ts
Original file line number Diff line number Diff line change
@@ -1,57 +1,32 @@
import blst from "@chainsafe/blst";
import {EmptyAggregateError} from "../errors.js";
import {bytesToHex, hexToBytes} from "../helpers/index.js";
import {CoordType, PointFormat, PublicKey as IPublicKey, PublicKeyArg} from "../types.js";
import {PublicKey as IPublicKey} from "../types.js";

export class PublicKey implements IPublicKey {
private constructor(private readonly value: blst.PublicKey) {}
constructor(private readonly value: blst.PublicKey) {}

/** @param type Defaults to `CoordType.jacobian` */
static fromBytes(bytes: Uint8Array, type?: CoordType, validate = true): PublicKey {
// need to hack the CoordType so @chainsafe/blst is not a required dep
const pk = blst.PublicKey.deserialize(bytes, (type as unknown) as blst.CoordType);
if (validate) pk.keyValidate();
return new PublicKey(pk);
static fromBytes(bytes: Uint8Array, validate = true): PublicKey {
return new PublicKey(blst.PublicKey.fromBytes(bytes, validate));
}

static fromHex(hex: string): PublicKey {
return this.fromBytes(hexToBytes(hex));
static fromHex(hex: string, validate = true): PublicKey {
return new PublicKey(blst.PublicKey.fromHex(hex, validate));
}

static aggregate(publicKeys: PublicKeyArg[]): PublicKey {
static aggregate(publicKeys: PublicKey[]): PublicKey {
if (publicKeys.length === 0) {
throw new EmptyAggregateError();
}

const pk = blst.aggregatePublicKeys(publicKeys.map(PublicKey.convertToBlstPublicKeyArg));
const pk = blst.aggregatePublicKeys(publicKeys.map((pk) => (pk as PublicKey).value));
return new PublicKey(pk);
}

static convertToBlstPublicKeyArg(publicKey: PublicKeyArg): blst.PublicKeyArg {
// need to cast to blst-native key instead of IPublicKey
return publicKey instanceof Uint8Array ? publicKey : (publicKey as PublicKey).value;
}

/**
* Implemented for SecretKey to be able to call .toPublicKey()
*/
private static friendBuild(key: blst.PublicKey): PublicKey {
return new PublicKey(key);
}

toBytes(format?: PointFormat): Uint8Array {
if (format === PointFormat.uncompressed) {
return this.value.serialize(false);
} else {
return this.value.serialize(true);
}
}

toHex(format?: PointFormat): string {
return bytesToHex(this.toBytes(format));
toBytes(compress = true): Uint8Array {
return this.value.toBytes(compress);
}

multiplyBy(bytes: Uint8Array): PublicKey {
return new PublicKey(this.value.multiplyBy(bytes));
toHex(compress = true): string {
return this.value.toHex(compress);
}
}
25 changes: 9 additions & 16 deletions src/blst-native/secretKey.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,43 @@
import blst from "@chainsafe/blst";
import crypto from "crypto";
import {bytesToHex, hexToBytes, isZeroUint8Array} from "../helpers/index.js";
import {SECRET_KEY_LENGTH} from "../constants.js";
import blst from "@chainsafe/blst";
import {SecretKey as ISecretKey} from "../types.js";
import {PublicKey} from "./publicKey.js";
import {Signature} from "./signature.js";
import {isZeroUint8Array} from "../helpers/utils.js";
import {ZeroSecretKeyError} from "../errors.js";

export class SecretKey implements ISecretKey {
constructor(private readonly value: blst.SecretKey) {}

static fromBytes(bytes: Uint8Array): SecretKey {
// draft-irtf-cfrg-bls-signature-04 does not allow SK == 0
if (isZeroUint8Array(bytes)) {
throw new ZeroSecretKeyError();
}

const sk = blst.SecretKey.deserialize(bytes);
return new SecretKey(sk);
return new SecretKey(blst.SecretKey.fromBytes(bytes));
}

static fromHex(hex: string): SecretKey {
return this.fromBytes(hexToBytes(hex));
return new SecretKey(blst.SecretKey.fromHex(hex));
}

static fromKeygen(entropy?: Uint8Array): SecretKey {
const sk = blst.SecretKey.fromKeygen(entropy || crypto.randomBytes(SECRET_KEY_LENGTH));
return new SecretKey(sk);
return new SecretKey(blst.SecretKey.fromKeygen(entropy ?? crypto.getRandomValues(new Uint8Array(32))));
}

sign(message: Uint8Array): Signature {
// @ts-expect-error Need to hack private constructor with static method
return Signature.friendBuild(this.value.sign(message));
return new Signature(this.value.sign(message));
}

toPublicKey(): PublicKey {
const pk = this.value.toPublicKey();
// @ts-expect-error Need to hack private constructor with static method
return PublicKey.friendBuild(pk);
return new PublicKey(this.value.toPublicKey());
}

toBytes(): Uint8Array {
return this.value.serialize();
return this.value.toBytes();
}

toHex(): string {
return bytesToHex(this.toBytes());
return this.value.toHex();
}
}
151 changes: 33 additions & 118 deletions src/blst-native/signature.ts
Original file line number Diff line number Diff line change
@@ -1,150 +1,65 @@
import blst from "@chainsafe/blst";
import {bytesToHex, hexToBytes} from "../helpers/index.js";
import {SignatureSet, CoordType, PointFormat, Signature as ISignature, PublicKeyArg, SignatureArg} from "../types.js";
import {SignatureSet, Signature as ISignature} from "../types.js";
import {PublicKey} from "./publicKey.js";
import {EmptyAggregateError, ZeroSignatureError} from "../errors.js";
import {EmptyAggregateError} from "../errors.js";

export class Signature implements ISignature {
private constructor(private readonly value: blst.Signature) {}
constructor(private readonly value: blst.Signature) {}

/** @param type Defaults to `CoordType.affine` */
static fromBytes(bytes: Uint8Array, type?: CoordType, validate = true): Signature {
// need to hack the CoordType so @chainsafe/blst is not a required dep
const sig = blst.Signature.deserialize(bytes, (type as unknown) as blst.CoordType);
if (validate) sig.sigValidate();
return new Signature(sig);
static fromBytes(bytes: Uint8Array, validate = true): Signature {
return new Signature(blst.Signature.fromBytes(bytes, validate, false));
}

static fromHex(hex: string): Signature {
return this.fromBytes(hexToBytes(hex));
static fromHex(hex: string, validate = true): Signature {
return new Signature(blst.Signature.fromHex(hex, validate, false));
}

static aggregate(signatures: SignatureArg[]): Signature {
static aggregate(signatures: Signature[]): Signature {
if (signatures.length === 0) {
throw new EmptyAggregateError();
}

const agg = blst.aggregateSignatures(signatures.map(Signature.convertToBlstSignatureArg));
const agg = blst.aggregateSignatures(signatures.map((sig) => sig.value));
return new Signature(agg);
}

static verifyMultipleSignatures(sets: SignatureSet[]): boolean {
return blst.verifyMultipleAggregateSignatures(
sets.map((set) => ({
message: set.message,
publicKey: PublicKey.convertToBlstPublicKeyArg(set.publicKey),
signature: Signature.convertToBlstSignatureArg(set.signature),
}))
msg: set.message,
pk: (set.publicKey as PublicKey)["value"],
sig: (set.signature as Signature)["value"],
})),
true,
true
);
}

static asyncVerifyMultipleSignatures(sets: SignatureSet[]): Promise<boolean> {
return blst.asyncVerifyMultipleAggregateSignatures(
sets.map((set) => ({
message: set.message,
publicKey: PublicKey.convertToBlstPublicKeyArg(set.publicKey),
signature: Signature.convertToBlstSignatureArg(set.signature),
}))
);
}

static convertToBlstSignatureArg(signature: SignatureArg): blst.SignatureArg {
// Need to cast to blst-native Signature instead of ISignature
return signature instanceof Uint8Array ? signature : (signature as Signature).value;
}

/**
* Implemented for SecretKey to be able to call .sign()
*/
private static friendBuild(sig: blst.Signature): Signature {
return new Signature(sig);
}

verify(publicKey: PublicKeyArg, message: Uint8Array): boolean {
// TODO (@matthewkeil) The note in aggregateVerify and the checks in this method
// do not seem to go together. Need to check the spec further.

// Individual infinity signatures are NOT okay. Aggregated signatures MAY be infinity
if (this.value.isInfinity()) {
throw new ZeroSignatureError();
}
return blst.verify(message, PublicKey.convertToBlstPublicKeyArg(publicKey), this.value);
}

verifyAggregate(publicKeys: PublicKeyArg[], message: Uint8Array): boolean {
return blst.fastAggregateVerify(message, publicKeys.map(PublicKey.convertToBlstPublicKeyArg), this.value);
}

verifyMultiple(publicKeys: PublicKeyArg[], messages: Uint8Array[]): boolean {
return this.aggregateVerify(publicKeys, messages, false);
verify(publicKey: PublicKey, message: Uint8Array): boolean {
return blst.verify(message, publicKey["value"], this.value);
}

async asyncVerify(publicKey: PublicKeyArg, message: Uint8Array): Promise<boolean> {
// TODO (@matthewkeil) The note in aggregateVerify and the checks in this method
// do not seem to go together. Need to check the spec further.

// Individual infinity signatures are NOT okay. Aggregated signatures MAY be infinity
if (this.value.isInfinity()) {
throw new ZeroSignatureError();
}
return blst.asyncVerify(message, PublicKey.convertToBlstPublicKeyArg(publicKey), this.value);
}

async asyncVerifyAggregate(publicKeys: PublicKeyArg[], message: Uint8Array): Promise<boolean> {
return blst.asyncFastAggregateVerify(message, publicKeys.map(PublicKey.convertToBlstPublicKeyArg), this.value);
}

async asyncVerifyMultiple(publicKeys: PublicKeyArg[], messages: Uint8Array[]): Promise<boolean> {
return this.aggregateVerify(publicKeys, messages, true);
}

toBytes(format?: PointFormat): Uint8Array {
if (format === PointFormat.uncompressed) {
return this.value.serialize(false);
} else {
return this.value.serialize(true);
}
verifyAggregate(publicKeys: PublicKey[], message: Uint8Array): boolean {
return blst.fastAggregateVerify(
message,
publicKeys.map((pk) => pk["value"]),
this.value
);
}

toHex(format?: PointFormat): string {
return bytesToHex(this.toBytes(format));
verifyMultiple(publicKeys: PublicKey[], messages: Uint8Array[]): boolean {
return blst.aggregateVerify(
messages,
publicKeys.map((pk) => pk["value"]),
this.value
);
}

multiplyBy(bytes: Uint8Array): Signature {
return new Signature(this.value.multiplyBy(bytes));
toBytes(compress = true): Uint8Array {
return this.value.toBytes(compress);
}

private aggregateVerify<T extends false>(publicKeys: PublicKeyArg[], messages: Uint8Array[], runAsync: T): boolean;
private aggregateVerify<T extends true>(
publicKeys: PublicKeyArg[],
messages: Uint8Array[],
runAsync: T
): Promise<boolean>;
private aggregateVerify<T extends boolean>(
publicKeys: PublicKeyArg[],
messages: Uint8Array[],
runAsync: T
): Promise<boolean> | boolean {
// TODO (@matthewkeil) The note in verify and the checks in this method
// do not seem to go together. Need to check the spec further.

// If this set is simply an infinity signature and infinity publicKey then skip verification.
// This has the effect of always declaring that this sig/publicKey combination is valid.
// for Eth2.0 specs tests
if (publicKeys.length === 1) {
const publicKey = publicKeys[0];
// eslint-disable-next-line prettier/prettier
const pk: PublicKey = publicKey instanceof Uint8Array
? PublicKey.fromBytes(publicKey)
: (publicKey as PublicKey); // need to cast to blst-native key instead of IPublicKey
// @ts-expect-error Need to hack type to get access to the private `value`
if (this.value.isInfinity() && pk.value.isInfinity()) {
return runAsync ? Promise.resolve(true) : true;
}
}

return runAsync
? blst.asyncAggregateVerify(messages, publicKeys.map(PublicKey.convertToBlstPublicKeyArg), this.value)
: blst.aggregateVerify(messages, publicKeys.map(PublicKey.convertToBlstPublicKeyArg), this.value);
toHex(compress = true): string {
return this.value.toHex(compress);
}
}
Loading
Loading