Skip to content

Commit

Permalink
Merge pull request #179 from ChainSafe/mkeil/fix-async-implementation
Browse files Browse the repository at this point in the history
fix: async implementation
  • Loading branch information
matthewkeil authored Apr 22, 2024
2 parents a5aa94c + 2c78014 commit 3b4db7d
Show file tree
Hide file tree
Showing 9 changed files with 314 additions and 177 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ 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 Expand Up @@ -106,7 +108,7 @@ Results are in `ops/sec (x times slower)`, where `x times slower` = times slower

\* `blst` and `herumi` performed 100 runs each, `noble` 10 runs.

Results from CI run https://github.com/ChainSafe/bls/runs/1513710175?check_suite_focus=true#step:12:13
Results from CI run <https://github.com/ChainSafe/bls/runs/1513710175?check_suite_focus=true#step:12:13>

## Spec versioning

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@chainsafe/bls",
"version": "8.0.0",
"version": "8.1.0",
"description": "Implementation of bls signature verification for ethereum 2.0",
"engines": {
"node": ">=18"
Expand Down
11 changes: 8 additions & 3 deletions src/blst-native/publicKey.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import blst from "@chainsafe/blst";
import {EmptyAggregateError} from "../errors.js";
import {bytesToHex, hexToBytes} from "../helpers/index.js";
import {CoordType, PointFormat, PublicKey as IPublicKey} from "../types.js";
import {CoordType, PointFormat, PublicKey as IPublicKey, PublicKeyArg} from "../types.js";

export class PublicKey implements IPublicKey {
private constructor(private readonly value: blst.PublicKey) {}
Expand All @@ -18,15 +18,20 @@ export class PublicKey implements IPublicKey {
return this.fromBytes(hexToBytes(hex));
}

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

const pk = blst.aggregatePublicKeys(publicKeys.map(({value}) => value));
const pk = blst.aggregatePublicKeys(publicKeys.map(PublicKey.convertToBlstPublicKeyArg));
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()
*/
Expand Down
104 changes: 78 additions & 26 deletions src/blst-native/signature.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import blst from "@chainsafe/blst";
import {bytesToHex, hexToBytes} from "../helpers/index.js";
import {CoordType, PointFormat, Signature as ISignature} from "../types.js";
import {SignatureSet, CoordType, PointFormat, Signature as ISignature, PublicKeyArg, SignatureArg} from "../types.js";
import {PublicKey} from "./publicKey.js";
import {EmptyAggregateError, ZeroSignatureError} from "../errors.js";

Expand All @@ -19,54 +19,83 @@ export class Signature implements ISignature {
return this.fromBytes(hexToBytes(hex));
}

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

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

static verifyMultipleSignatures(sets: {publicKey: PublicKey; message: Uint8Array; signature: Signature}[]): boolean {
static verifyMultipleSignatures(sets: SignatureSet[]): boolean {
return blst.verifyMultipleAggregateSignatures(
// @ts-expect-error Need to hack type to get access to the private `value`
sets.map((s) => ({message: s.message, publicKey: s.publicKey.value, signature: s.signature.value}))
sets.map((set) => ({
message: set.message,
publicKey: PublicKey.convertToBlstPublicKeyArg(set.publicKey),
signature: Signature.convertToBlstSignatureArg(set.signature),
}))
);
}

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: PublicKey, message: Uint8Array): boolean {
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);
}

// @ts-expect-error Need to hack type to get access to the private `value`
return blst.verify(message, publicKey.value, this.value);
verifyAggregate(publicKeys: PublicKeyArg[], message: Uint8Array): boolean {
return blst.fastAggregateVerify(message, publicKeys.map(PublicKey.convertToBlstPublicKeyArg), this.value);
}

verifyAggregate(publicKeys: PublicKey[], message: Uint8Array): boolean {
return blst.fastAggregateVerify(
message,
// @ts-expect-error Need to hack type to get access to the private `value`
publicKeys.map((pk) => pk.value),
this.value
);
verifyMultiple(publicKeys: PublicKeyArg[], messages: Uint8Array[]): boolean {
return this.aggregateVerify(publicKeys, messages, false);
}

verifyMultiple(publicKeys: PublicKey[], messages: Uint8Array[]): boolean {
return this.aggregateVerify(
messages,
// @ts-expect-error Need to hack type to get access to the private `value`
publicKeys.map((pk) => pk.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 {
Expand All @@ -85,14 +114,37 @@ export class Signature implements ISignature {
return new Signature(this.value.multiplyBy(bytes));
}

private aggregateVerify(msgs: Uint8Array[], pks: blst.PublicKey[]): boolean {
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 (this.value.isInfinity() && pks.length === 1 && pks[0].isInfinity()) {
return true;
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 blst.aggregateVerify(msgs, pks, this.value);
return runAsync
? blst.asyncAggregateVerify(messages, publicKeys.map(PublicKey.convertToBlstPublicKeyArg), this.value)
: blst.aggregateVerify(messages, publicKeys.map(PublicKey.convertToBlstPublicKeyArg), this.value);
}
}
Loading

0 comments on commit 3b4db7d

Please # to comment.