diff --git a/benchmark/verifyMultipleSignaturesSavings.ts b/benchmark/verifyMultipleSignaturesSavings.ts index 31b5245..16357da 100644 --- a/benchmark/verifyMultipleSignaturesSavings.ts +++ b/benchmark/verifyMultipleSignaturesSavings.ts @@ -15,12 +15,8 @@ import {range, randomMessage} from "../test/util"; return {pk, msg, sig}; }); - const pks = dataArr.map((data) => data.pk); - const msgs = dataArr.map((data) => data.msg); - const sigs = dataArr.map((data) => data.sig); - const startMulti = process.hrtime.bigint(); - bls.Signature.verifyMultipleSignatures(pks, msgs, sigs); + bls.Signature.verifyMultipleSignatures(dataArr); const endMulti = process.hrtime.bigint(); const diffMulti = endMulti - startMulti; diff --git a/package.json b/package.json index 09ecb42..12f4058 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "randombytes": "^2.1.0" }, "devDependencies": { - "@chainsafe/blst": "^0.1.6", + "@chainsafe/blst": "^0.2.0", "@chainsafe/lodestar-spec-test-util": "^0.18.0", "@types/chai": "^4.2.9", "@types/mocha": "^8.0.4", @@ -67,6 +67,7 @@ "mocha": "^8.2.1", "nyc": "^15.0.0", "prettier": "^2.1.2", + "threads": "^1.6.3", "ts-loader": "^6.2.1", "ts-node": "^8.6.2", "typescript": "^3.7.5", @@ -78,6 +79,6 @@ "v8-profiler-next": "1.3.0" }, "peerDependencies": { - "@chainsafe/blst": "^0.1.6" + "@chainsafe/blst": "^0.2.0" } } diff --git a/src/blst/index.ts b/src/blst/index.ts index 478804c..e4027ac 100644 --- a/src/blst/index.ts +++ b/src/blst/index.ts @@ -15,6 +15,7 @@ export function destroy(): void { } export const bls: IBls = { + implementation: "blst-native", SecretKey, PublicKey, Signature, diff --git a/src/blst/publicKey.ts b/src/blst/publicKey.ts index c6a7de4..641c1b9 100644 --- a/src/blst/publicKey.ts +++ b/src/blst/publicKey.ts @@ -1,25 +1,20 @@ import * as blst from "@chainsafe/blst"; import {EmptyAggregateError, ZeroPublicKeyError} from "../errors"; import {bytesToHex, hexToBytes} from "../helpers"; -import {PublicKey as IPublicKey} from "../interface"; +import {CoordType, PointFormat, PublicKey as IPublicKey} from "../interface"; -export class PublicKey implements IPublicKey { - readonly affine: blst.PublicKey; - readonly jacobian: blst.AggregatePublicKey; - - constructor(affine: blst.PublicKey, jacobian: blst.AggregatePublicKey) { - this.affine = affine; - this.jacobian = jacobian; +export class PublicKey extends blst.PublicKey implements IPublicKey { + constructor(value: ConstructorParameters[0]) { + super(value); } - static fromBytes(bytes: Uint8Array): PublicKey { - const affine = blst.PublicKey.fromBytes(bytes); - if (affine.value.is_inf()) { + static fromBytes(bytes: Uint8Array, type?: blst.CoordType): PublicKey { + const pk = blst.PublicKey.fromBytes(bytes, type); + if (pk.value.is_inf()) { throw new ZeroPublicKeyError(); } - const jacobian = blst.AggregatePublicKey.fromPublicKey(affine); - return new PublicKey(affine, jacobian); + return new PublicKey(pk.value); } static fromHex(hex: string): PublicKey { @@ -31,16 +26,19 @@ export class PublicKey implements IPublicKey { throw new EmptyAggregateError(); } - const jacobian = blst.aggregatePubkeys(publicKeys.map((pk) => pk.jacobian)); - const affine = jacobian.toPublicKey(); - return new PublicKey(affine, jacobian); + const pk = blst.aggregatePubkeys(publicKeys); + return new PublicKey(pk.value); } - toBytes(): Uint8Array { - return this.affine.toBytes(); + toBytes(format?: PointFormat): Uint8Array { + if (format === PointFormat.uncompressed) { + return this.value.serialize(); + } else { + return this.value.compress(); + } } - toHex(): string { - return bytesToHex(this.toBytes()); + toHex(format?: PointFormat): string { + return bytesToHex(this.toBytes(format)); } } diff --git a/src/blst/secretKey.ts b/src/blst/secretKey.ts index 1c11de5..f25104b 100644 --- a/src/blst/secretKey.ts +++ b/src/blst/secretKey.ts @@ -8,7 +8,6 @@ import {ZeroSecretKeyError} from "../errors"; export class SecretKey implements ISecretKey { readonly value: blst.SecretKey; - constructor(value: blst.SecretKey) { this.value = value; } @@ -33,13 +32,12 @@ export class SecretKey implements ISecretKey { } sign(message: Uint8Array): Signature { - return new Signature(this.value.sign(message)); + return new Signature(this.value.sign(message).value); } toPublicKey(): PublicKey { - const jacobian = this.value.toAggregatePublicKey(); - const affine = jacobian.toPublicKey(); - return new PublicKey(affine, jacobian); + const pk = this.value.toPublicKey(); + return new PublicKey(pk.value); } toBytes(): Uint8Array { diff --git a/src/blst/signature.ts b/src/blst/signature.ts index 69b0e0f..ae38270 100644 --- a/src/blst/signature.ts +++ b/src/blst/signature.ts @@ -1,18 +1,18 @@ import * as blst from "@chainsafe/blst"; import {bytesToHex, hexToBytes} from "../helpers"; -import {Signature as ISignature} from "../interface"; +import {PointFormat, Signature as ISignature} from "../interface"; import {PublicKey} from "./publicKey"; import {EmptyAggregateError, ZeroSignatureError} from "../errors"; -export class Signature implements ISignature { - readonly affine: blst.Signature; - - constructor(value: blst.Signature) { - this.affine = value; +export class Signature extends blst.Signature implements ISignature { + constructor(value: ConstructorParameters[0]) { + super(value); } - static fromBytes(bytes: Uint8Array): Signature { - return new Signature(blst.Signature.fromBytes(bytes)); + static fromBytes(bytes: Uint8Array, type?: blst.CoordType, validate?: boolean): Signature { + const sig = blst.Signature.fromBytes(bytes, type); + if (validate) sig.sigValidate(); + return new Signature(sig.value); } static fromHex(hex: string): Signature { @@ -24,55 +24,53 @@ export class Signature implements ISignature { throw new EmptyAggregateError(); } - const agg = blst.AggregateSignature.fromSignatures(signatures.map((sig) => sig.affine)); - return new Signature(agg.toSignature()); + const agg = blst.aggregateSignatures(signatures); + return new Signature(agg.value); } - static verifyMultipleSignatures(publicKeys: PublicKey[], messages: Uint8Array[], signatures: Signature[]): boolean { + static verifyMultipleSignatures(sets: {publicKey: PublicKey; message: Uint8Array; signature: Signature}[]): boolean { return blst.verifyMultipleAggregateSignatures( - messages, - publicKeys.map((publicKey) => publicKey.affine), - signatures.map((signature) => signature.affine) + sets.map((s) => ({msg: s.message, pk: s.publicKey, sig: s.signature})) ); } verify(publicKey: PublicKey, message: Uint8Array): boolean { // Individual infinity signatures are NOT okay. Aggregated signatures MAY be infinity - if (this.affine.value.is_inf()) { + if (this.value.is_inf()) { throw new ZeroSignatureError(); } - return this.aggregateVerify([message], [publicKey.affine]); + return blst.verify(message, publicKey, this); } verifyAggregate(publicKeys: PublicKey[], message: Uint8Array): boolean { - const agg = PublicKey.aggregate(publicKeys); - return this.aggregateVerify([message], [agg.affine]); + return blst.fastAggregateVerify(message, publicKeys, this); } verifyMultiple(publicKeys: PublicKey[], messages: Uint8Array[]): boolean { - return this.aggregateVerify( - messages, - publicKeys.map((pk) => pk.affine) - ); + return blst.aggregateVerify(messages, publicKeys, this); } - toBytes(): Uint8Array { - return this.affine.toBytes(); + toBytes(format?: PointFormat): Uint8Array { + if (format === PointFormat.uncompressed) { + return this.value.serialize(); + } else { + return this.value.compress(); + } } - toHex(): string { - return bytesToHex(this.toBytes()); + toHex(format?: PointFormat): string { + return bytesToHex(this.toBytes(format)); } private aggregateVerify(msgs: Uint8Array[], pks: blst.PublicKey[]): boolean { // 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.affine.value.is_inf() && pks.length === 1 && pks[0].value.is_inf()) { + if (this.value.is_inf() && pks.length === 1 && pks[0].value.is_inf()) { return true; } - return blst.aggregateVerify(msgs, pks, this.affine); + return blst.aggregateVerify(msgs, pks, this); } } diff --git a/src/constants.ts b/src/constants.ts index 2243a76..a495623 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,5 +1,5 @@ export const SECRET_KEY_LENGTH = 32; -export const SIGNATURE_LENGTH = 96; -export const FP_POINT_LENGTH = 48; -export const PUBLIC_KEY_LENGTH = FP_POINT_LENGTH; -export const G2_HASH_PADDING = 16; +export const PUBLIC_KEY_LENGTH_COMPRESSED = 48; +export const PUBLIC_KEY_LENGTH_UNCOMPRESSED = 48 * 2; +export const SIGNATURE_LENGTH_COMPRESSED = 96; +export const SIGNATURE_LENGTH_UNCOMPRESSED = 96 * 2; diff --git a/src/errors.ts b/src/errors.ts index d95c3be..267e22a 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -40,6 +40,6 @@ export class InvalidOrderError extends Error { export class InvalidLengthError extends Error { constructor(arg: string, length: number) { - super(`INVALID_LENGTH: ${arg} must have ${length} bytes`); + super(`INVALID_LENGTH: ${arg} - ${length} bytes`); } } diff --git a/src/functional.ts b/src/functional.ts index e59d0b4..d07ee6d 100644 --- a/src/functional.ts +++ b/src/functional.ts @@ -117,22 +117,17 @@ export function functionalInterfaceFactory({ * https://ethresear.ch/t/fast-verification-of-multiple-bls-signatures/5407 */ function verifyMultipleSignatures( - publicKeys: Uint8Array[], - messages: Uint8Array[], - signatures: Uint8Array[] + sets: {publicKey: Uint8Array; message: Uint8Array; signature: Uint8Array}[] ): boolean { - validateBytes(publicKeys, "publicKey"); - validateBytes(messages, "message"); - validateBytes(signatures, "signatures"); + if (!sets) throw Error("sets is null or undefined"); - if (publicKeys.length === 0 || publicKeys.length !== messages.length || publicKeys.length !== signatures.length) { - return false; - } try { return Signature.verifyMultipleSignatures( - publicKeys.map((publicKey) => PublicKey.fromBytes(publicKey)), - messages.map((msg) => msg), - signatures.map((signature) => Signature.fromBytes(signature)) + sets.map((s) => ({ + publicKey: PublicKey.fromBytes(s.publicKey), + message: s.message, + signature: Signature.fromBytes(s.signature), + })) ); } catch (e) { if (e instanceof NotInitializedError) throw e; diff --git a/src/herumi/index.ts b/src/herumi/index.ts index bad6872..e79f56d 100644 --- a/src/herumi/index.ts +++ b/src/herumi/index.ts @@ -9,6 +9,7 @@ export * from "../constants"; export {SecretKey, PublicKey, Signature, init, destroy}; export const bls: IBls = { + implementation: "herumi", SecretKey, PublicKey, Signature, diff --git a/src/herumi/publicKey.ts b/src/herumi/publicKey.ts index f61d836..805e62f 100644 --- a/src/herumi/publicKey.ts +++ b/src/herumi/publicKey.ts @@ -1,9 +1,9 @@ import {PublicKeyType} from "bls-eth-wasm"; import {getContext} from "./context"; -import {PUBLIC_KEY_LENGTH} from "../constants"; import {bytesToHex, hexToBytes, isZeroUint8Array} from "../helpers"; -import {PublicKey as IPublicKey} from "../interface"; +import {PointFormat, PublicKey as IPublicKey} from "../interface"; import {EmptyAggregateError, InvalidLengthError, ZeroPublicKeyError} from "../errors"; +import {PUBLIC_KEY_LENGTH_COMPRESSED, PUBLIC_KEY_LENGTH_UNCOMPRESSED} from "../constants"; export class PublicKey implements IPublicKey { readonly value: PublicKeyType; @@ -17,14 +17,16 @@ export class PublicKey implements IPublicKey { } static fromBytes(bytes: Uint8Array): PublicKey { - if (bytes.length !== PUBLIC_KEY_LENGTH) { - throw new InvalidLengthError("PublicKey", PUBLIC_KEY_LENGTH); - } - const context = getContext(); const publicKey = new context.PublicKey(); if (!isZeroUint8Array(bytes)) { - publicKey.deserialize(bytes); + if (bytes.length === PUBLIC_KEY_LENGTH_COMPRESSED) { + publicKey.deserialize(bytes); + } else if (bytes.length === PUBLIC_KEY_LENGTH_UNCOMPRESSED) { + publicKey.deserializeUncompressed(bytes); + } else { + throw new InvalidLengthError("PublicKey", bytes.length); + } } return new PublicKey(publicKey); } @@ -45,11 +47,15 @@ export class PublicKey implements IPublicKey { return agg; } - toBytes(): Uint8Array { - return this.value.serialize(); + toBytes(format?: PointFormat): Uint8Array { + if (format === PointFormat.uncompressed) { + return this.value.serializeUncompressed(); + } else { + return this.value.serialize(); + } } - toHex(): string { - return bytesToHex(this.toBytes()); + toHex(format?: PointFormat): string { + return bytesToHex(this.toBytes(format)); } } diff --git a/src/herumi/signature.ts b/src/herumi/signature.ts index e8c41be..a07e253 100644 --- a/src/herumi/signature.ts +++ b/src/herumi/signature.ts @@ -1,10 +1,10 @@ -import {SIGNATURE_LENGTH} from "../constants"; import {SignatureType, multiVerify} from "bls-eth-wasm"; import {getContext} from "./context"; import {PublicKey} from "./publicKey"; import {bytesToHex, concatUint8Arrays, hexToBytes, isZeroUint8Array} from "../helpers"; -import {Signature as ISignature} from "../interface"; +import {PointFormat, Signature as ISignature} from "../interface"; import {EmptyAggregateError, InvalidLengthError, InvalidOrderError} from "../errors"; +import {SIGNATURE_LENGTH_COMPRESSED, SIGNATURE_LENGTH_UNCOMPRESSED} from "../constants"; export class Signature implements ISignature { readonly value: SignatureType; @@ -18,13 +18,16 @@ export class Signature implements ISignature { } static fromBytes(bytes: Uint8Array): Signature { - if (bytes.length !== SIGNATURE_LENGTH) { - throw new InvalidLengthError("Signature", SIGNATURE_LENGTH); - } - const context = getContext(); const signature = new context.Signature(); if (!isZeroUint8Array(bytes)) { + if (bytes.length === SIGNATURE_LENGTH_COMPRESSED) { + signature.deserialize(bytes); + } else if (bytes.length === SIGNATURE_LENGTH_UNCOMPRESSED) { + signature.deserializeUncompressed(bytes); + } else { + throw new InvalidLengthError("Signature", bytes.length); + } signature.deserialize(bytes); } return new Signature(signature); @@ -45,11 +48,11 @@ export class Signature implements ISignature { return new Signature(signature); } - static verifyMultipleSignatures(publicKeys: PublicKey[], messages: Uint8Array[], signatures: Signature[]): boolean { + static verifyMultipleSignatures(sets: {publicKey: PublicKey; message: Uint8Array; signature: Signature}[]): boolean { return multiVerify( - publicKeys.map((publicKey) => publicKey.value), - signatures.map((signature) => signature.value), - messages + sets.map((s) => s.publicKey.value), + sets.map((s) => s.signature.value), + sets.map((s) => s.message) ); } @@ -71,11 +74,15 @@ export class Signature implements ISignature { ); } - toBytes(): Uint8Array { - return this.value.serialize(); + toBytes(format?: PointFormat): Uint8Array { + if (format === PointFormat.uncompressed) { + return this.value.serializeUncompressed(); + } else { + return this.value.serialize(); + } } - toHex(): string { - return bytesToHex(this.toBytes()); + toHex(format?: PointFormat): string { + return bytesToHex(this.toBytes(format)); } } diff --git a/src/interface.ts b/src/interface.ts index 822e657..2687776 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -1,19 +1,20 @@ export interface IBls { + implementation: Implementation; SecretKey: { fromBytes(bytes: Uint8Array): SecretKey; fromHex(hex: string): SecretKey; fromKeygen(ikm?: Uint8Array): SecretKey; }; PublicKey: { - fromBytes(bytes: Uint8Array): PublicKey; + fromBytes(bytes: Uint8Array, type?: CoordType): PublicKey; fromHex(hex: string): PublicKey; aggregate(publicKeys: PublicKey[]): PublicKey; }; Signature: { - fromBytes(bytes: Uint8Array): Signature; + fromBytes(bytes: Uint8Array, type?: CoordType, validate?: boolean): Signature; fromHex(hex: string): Signature; aggregate(signatures: Signature[]): Signature; - verifyMultipleSignatures(publicKeys: PublicKey[], messages: Uint8Array[], signatures: Signature[]): boolean; + verifyMultipleSignatures(sets: {publicKey: PublicKey; message: Uint8Array; signature: Signature}[]): boolean; }; sign(secretKey: Uint8Array, message: Uint8Array): Uint8Array; @@ -22,7 +23,7 @@ export interface IBls { verify(publicKey: Uint8Array, message: Uint8Array, signature: Uint8Array): boolean; verifyAggregate(publicKeys: Uint8Array[], message: Uint8Array, signature: Uint8Array): boolean; verifyMultiple(publicKeys: Uint8Array[], messages: Uint8Array[], signature: Uint8Array): boolean; - verifyMultipleSignatures(publicKeys: Uint8Array[], messages: Uint8Array[], signatures: Uint8Array[]): boolean; + verifyMultipleSignatures(sets: {publicKey: Uint8Array; message: Uint8Array; signature: Uint8Array}[]): boolean; secretKeyToPublicKey(secretKey: Uint8Array): Uint8Array; init(): Promise; @@ -40,21 +41,33 @@ export declare class SecretKey { } export declare class PublicKey { - static fromBytes(bytes: Uint8Array): PublicKey; + static fromBytes(bytes: Uint8Array, type?: CoordType): PublicKey; static fromHex(hex: string): PublicKey; static aggregate(publicKeys: PublicKey[]): PublicKey; - toBytes(): Uint8Array; - toHex(): string; + toBytes(format?: PointFormat): Uint8Array; + toHex(format?: PointFormat): string; } export declare class Signature { - static fromBytes(bytes: Uint8Array): Signature; + static fromBytes(bytes: Uint8Array, type?: CoordType, validate?: boolean): Signature; static fromHex(hex: string): Signature; static aggregate(signatures: Signature[]): Signature; - static verifyMultipleSignatures(publicKeys: PublicKey[], messages: Uint8Array[], signatures: Signature[]): boolean; + static verifyMultipleSignatures(sets: {publicKey: PublicKey; message: Uint8Array; signature: Signature}[]): boolean; verify(publicKey: PublicKey, message: Uint8Array): boolean; verifyAggregate(publicKeys: PublicKey[], message: Uint8Array): boolean; verifyMultiple(publicKeys: PublicKey[], messages: Uint8Array[]): boolean; - toBytes(): Uint8Array; - toHex(): string; + toBytes(format?: PointFormat): Uint8Array; + toHex(format?: PointFormat): string; +} + +export type Implementation = "herumi" | "blst-native"; + +export enum PointFormat { + compressed = "compressed", + uncompressed = "uncompressed", +} + +export enum CoordType { + affine, + jacobian, } diff --git a/test/unit/index.test.ts b/test/unit/index.test.ts index b9e16a0..223eee7 100644 --- a/test/unit/index.test.ts +++ b/test/unit/index.test.ts @@ -1,6 +1,6 @@ import {expect} from "chai"; -import {IBls} from "../../src/interface"; -import {getN, randomMessage} from "../util"; +import {IBls, PointFormat} from "../../src/interface"; +import {getN, randomMessage, hexToBytesNode} from "../util"; import {hexToBytes} from "../../src/helpers"; import {maliciousVerifyMultipleSignaturesData} from "../data/malicious-signature-test-data"; @@ -96,24 +96,23 @@ export function runIndexTests(bls: IBls): void { describe("verifyMultipleSignatures", () => { it("Should verify multiple signatures", () => { const n = 4; - const dataArr = getN(n, () => { + const sets = getN(n, () => { const sk = bls.SecretKey.fromKeygen(); - const pk = sk.toPublicKey(); - const msg = randomMessage(); - const sig = sk.sign(msg); - return {pk, msg, sig}; + const publicKey = sk.toPublicKey(); + const message = randomMessage(); + const signature = sk.sign(message); + return {publicKey, message, signature}; }); - const pks = dataArr.map((data) => data.pk); - const msgs = dataArr.map((data) => data.msg); - const sigs = dataArr.map((data) => data.sig); - expect(bls.Signature.verifyMultipleSignatures(pks, msgs, sigs)).to.equal(true, "class interface failed"); + expect(bls.Signature.verifyMultipleSignatures(sets)).to.equal(true, "class interface failed"); expect( bls.verifyMultipleSignatures( - pks.map((pk) => pk.toBytes()), - msgs, - sigs.map((sig) => sig.toBytes()) + sets.map((s) => ({ + publicKey: s.publicKey.toBytes(), + message: s.message, + signature: s.signature.toBytes(), + })) ) ).to.equal(true, "functional (bytes serialized) interface failed"); }); @@ -138,11 +137,47 @@ export function runIndexTests(bls: IBls): void { "Malicious signature should be validated with bls.verifyMultiple" ); + const maliciousSets = pks.map((_, i) => ({ + publicKey: pks[i], + message: msgs[i], + signature: sigs[i], + })); + // This method is expected to catch the malicious signature and not verify - expect(bls.Signature.verifyMultipleSignatures(pks, msgs, sigs)).to.equal( + expect(bls.Signature.verifyMultipleSignatures(maliciousSets)).to.equal( false, "Malicous signature should not validate with bls.verifyMultipleSignatures" ); }); }); + + describe("serialize deserialize", () => { + /* eslint-disable max-len */ + + const skHex = "0x0101010101010101010101010101010101010101010101010101010101010101"; + const pkHexCompExpected = + "0xaa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac1"; + const pkHexUncompExpected = + "0x0a1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac111392125a1c3750363c2c97d9650fb78696e6428db8ff9efaf0471cbfd20324916ab545746db83756d335e92f9e8c8b8"; + + it("Should serialize comp pubkey", () => { + const sk = bls.SecretKey.fromBytes(hexToBytesNode(skHex)); + const pkHexComp = sk.toPublicKey().toHex(PointFormat.compressed); + expect(pkHexComp).to.equal(pkHexCompExpected, "Wrong pkHexComp"); + }); + + it("Should serialize uncomp pubkey", () => { + const sk = bls.SecretKey.fromBytes(hexToBytesNode(skHex)); + const pkHexUncomp = sk.toPublicKey().toHex(PointFormat.uncompressed); + expect(pkHexUncomp).to.equal(pkHexUncompExpected, "Wrong pkHexUncomp"); + }); + + it("Should deserialize comp pubkey", () => { + bls.PublicKey.fromHex(pkHexCompExpected); + }); + + it("Should deserialize uncomp pubkey", () => { + bls.PublicKey.fromHex(pkHexUncompExpected); + }); + }); } diff --git a/test/unit/multithread/naive/chunkify.test.ts b/test/unit/multithread/naive/chunkify.test.ts new file mode 100644 index 0000000..5e53530 --- /dev/null +++ b/test/unit/multithread/naive/chunkify.test.ts @@ -0,0 +1,60 @@ +import {expect} from "chai"; +import {chunkify} from "./utils"; + +describe("chunkify", () => { + const arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 13, 14, 15]; + + const results = { + 0: [[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 13, 14, 15]], + 1: [[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 13, 14, 15]], + 2: [ + [0, 1, 2, 3, 4, 5, 6, 7], + [8, 9, 10, 12, 13, 14, 15], + ], + 3: [ + [0, 1, 2, 3, 4], + [5, 6, 7, 8, 9], + [10, 12, 13, 14, 15], + ], + 4: [ + [0, 1, 2, 3], + [4, 5, 6, 7], + [8, 9, 10, 12], + [13, 14, 15], + ], + 5: [ + [0, 1, 2], + [3, 4, 5], + [6, 7, 8], + [9, 10, 12], + [13, 14, 15], + ], + 6: [ + [0, 1, 2], + [3, 4, 5], + [6, 7, 8], + [9, 10, 12], + [13, 14, 15], + ], + 7: [[0, 1], [2, 3], [4, 5], [6, 7], [8, 9], [10, 12], [13, 14], [15]], + 8: [[0, 1], [2, 3], [4, 5], [6, 7], [8, 9], [10, 12], [13, 14], [15]], + }; + + const testCases: { + id: string; + n: number; + arr: number[]; + expectArr: number[][]; + }[] = Object.entries(results).map(([i, expectArr]) => ({ + id: i, + n: parseInt(i), + arr, + expectArr, + })); + + for (const {id, arr, n, expectArr} of testCases) { + it(id, () => { + expect(chunkify(arr, n)).to.deep.equal(expectArr); + }); + } +}); diff --git a/test/unit/multithread/naive/index.ts b/test/unit/multithread/naive/index.ts new file mode 100644 index 0000000..c671e19 --- /dev/null +++ b/test/unit/multithread/naive/index.ts @@ -0,0 +1,47 @@ +import {spawn, Pool, Worker, Thread} from "threads"; +import {Implementation, PointFormat, PublicKey, Signature} from "../../../../src"; +import {WorkerApi} from "./worker"; + +type ThreadType = { + [K in keyof WorkerApi]: (...args: Parameters) => Promise>; +}; + +export class BlsMultiThreadNaive { + impl: Implementation; + pool: Pool; + format: PointFormat; + + constructor(impl: Implementation, workerCount?: number) { + this.impl = impl; + // Use compressed for herumi for now. + // THe worker is not able to deserialize from uncompressed + // `Error: err _wrapDeserialize` + this.format = impl === "blst-native" ? PointFormat.uncompressed : PointFormat.compressed; + this.pool = Pool(() => (spawn(new Worker("./worker")) as any) as Promise, workerCount); + } + + async destroy(): Promise { + await this.pool.terminate(true); + } + + async verify(pk: PublicKey, msg: Uint8Array, sig: Signature): Promise { + return this.pool.queue((worker) => + worker.verify(this.impl, pk.toBytes(PointFormat.uncompressed), msg, sig.toBytes(PointFormat.uncompressed)) + ); + } + + async verifyMultipleAggregateSignatures( + sets: {publicKey: PublicKey; message: Uint8Array; signature: Signature}[] + ): Promise { + return this.pool.queue((worker) => + worker.verifyMultipleAggregateSignatures( + this.impl, + sets.map((s) => ({ + publicKey: s.publicKey.toBytes(PointFormat.uncompressed), + message: s.message, + signature: s.signature.toBytes(PointFormat.uncompressed), + })) + ) + ); + } +} diff --git a/test/unit/multithread/naive/naive.test.ts b/test/unit/multithread/naive/naive.test.ts new file mode 100644 index 0000000..93e4276 --- /dev/null +++ b/test/unit/multithread/naive/naive.test.ts @@ -0,0 +1,65 @@ +import {expect} from "chai"; +import {IBls, PublicKey, Signature} from "../../../../src"; +import {BlsMultiThreadNaive} from "./index"; +import {warmUpWorkers} from "./utils"; + +export function runMultithreadTests(bls: IBls): void { + describe("bls pool naive", function () { + const nodeJsSemver = process.versions.node; + const nodeJsMajorVer = parseInt(nodeJsSemver.split(".")[0]); + if (!nodeJsMajorVer) { + throw Error(`Error parsing NodeJS version: ${nodeJsSemver}`); + } + if (nodeJsMajorVer < 12) { + return; // Skip everything + } + + const n = 16; + let pool: BlsMultiThreadNaive; + + before("Create pool and warm-up wallets", async function () { + // Starting all threads may take a while due to ts-node compilation + this.timeout(20 * 1000); + pool = new BlsMultiThreadNaive(bls.implementation); + await warmUpWorkers(bls, pool); + }); + + after("Destroy pool", async function () { + this.timeout(20 * 1000); + await pool.destroy(); + }); + + describe("1 msg, 1 pk", () => { + const msg = Buffer.from("sample-msg"); + const sk = bls.SecretKey.fromKeygen(Buffer.alloc(32, 1)); + const pk = sk.toPublicKey(); + const sig = sk.sign(msg); + + it("verify", async () => { + const valid = await pool.verify(pk, msg, sig); + expect(valid).to.equal(true); + }); + }); + + describe("N msgs, N pks", () => { + const sets: {publicKey: PublicKey; message: Uint8Array; signature: Signature}[] = []; + for (let i = 0; i < n; i++) { + const message = Buffer.alloc(32, i); + const sk = bls.SecretKey.fromKeygen(Buffer.alloc(32, i)); + sets.push({message, publicKey: sk.toPublicKey(), signature: sk.sign(message)}); + } + + it("verify", async () => { + const validArr = await Promise.all(sets.map((s) => pool.verify(s.publicKey, s.message, s.signature))); + for (const [i, valid] of validArr.entries()) { + expect(valid).to.equal(true, `Invalid ${i}`); + } + }); + + it("verifyMultipleAggregateSignatures", async () => { + const valid = await pool.verifyMultipleAggregateSignatures(sets); + expect(valid).to.equal(true); + }); + }); + }); +} diff --git a/test/unit/multithread/naive/utils.ts b/test/unit/multithread/naive/utils.ts new file mode 100644 index 0000000..9d8e223 --- /dev/null +++ b/test/unit/multithread/naive/utils.ts @@ -0,0 +1,23 @@ +import os from "os"; +import {IBls} from "../../../../src"; +import {BlsMultiThreadNaive} from "./index"; + +export async function warmUpWorkers(bls: IBls, pool: BlsMultiThreadNaive): Promise { + const msg = Buffer.alloc(32, 1); + const sk = bls.SecretKey.fromKeygen(Buffer.alloc(32, 1)); + const pk = sk.toPublicKey(); + const sig = sk.sign(msg); + + await Promise.all(Array.from({length: os.cpus().length}, (_, i) => i).map(() => pool.verify(pk, msg, sig))); +} + +export function chunkify(arr: T[], chunkCount: number): T[][] { + const chunkSize = Math.round(arr.length / chunkCount); + const arrArr: T[][] = []; + + for (let i = 0, j = arr.length; i < j; i += chunkSize) { + arrArr.push(arr.slice(i, i + chunkSize)); + } + + return arrArr; +} diff --git a/test/unit/multithread/naive/worker.ts b/test/unit/multithread/naive/worker.ts new file mode 100644 index 0000000..d5c0d48 --- /dev/null +++ b/test/unit/multithread/naive/worker.ts @@ -0,0 +1,28 @@ +import {expose} from "threads/worker"; +import {bls, init, CoordType, Implementation} from "../../../../src"; + +export type WorkerApi = typeof workerApi; + +const workerApi = { + async verify(impl: Implementation, publicKey: Uint8Array, message: Uint8Array, signature: Uint8Array) { + await init(impl); + const pk = bls.PublicKey.fromBytes(publicKey, CoordType.affine); + const sig = bls.Signature.fromBytes(signature, CoordType.affine, true); + return sig.verify(pk, message); + }, + async verifyMultipleAggregateSignatures( + impl: Implementation, + sets: {publicKey: Uint8Array; message: Uint8Array; signature: Uint8Array}[] + ) { + await init(impl); + return bls.Signature.verifyMultipleSignatures( + sets.map((s) => ({ + publicKey: bls.PublicKey.fromBytes(s.publicKey, CoordType.affine), + message: s.message, + signature: bls.Signature.fromBytes(s.signature, CoordType.affine, true), + })) + ); + }, +}; + +expose(workerApi); diff --git a/test/unit/run-all-implementations.test.ts b/test/unit/run-all-implementations.test.ts index c8898e0..24752ab 100644 --- a/test/unit/run-all-implementations.test.ts +++ b/test/unit/run-all-implementations.test.ts @@ -1,6 +1,7 @@ import {runSecretKeyTests} from "./secretKey.test"; import {runPublicKeyTests} from "./publicKey.test"; import {runIndexTests} from "./index.test"; +import {runMultithreadTests} from "./multithread/naive/naive.test"; import {describeForAllImplementations} from "../switch"; // Import test's bls lib lazily to prevent breaking test with Karma @@ -8,4 +9,5 @@ describeForAllImplementations((bls) => { runSecretKeyTests(bls); runPublicKeyTests(bls); runIndexTests(bls); + runMultithreadTests(bls); }); diff --git a/yarn.lock b/yarn.lock index 42cf455..0aaf837 100644 --- a/yarn.lock +++ b/yarn.lock @@ -161,10 +161,10 @@ bls-eth-wasm "^0.4.4" randombytes "^2.1.0" -"@chainsafe/blst@^0.1.6": - version "0.1.6" - resolved "https://registry.yarnpkg.com/@chainsafe/blst/-/blst-0.1.6.tgz#d933a1568d9e781cd13673d80ff1eaf40c955107" - integrity sha512-iv1CASFce9T1QQB+pVznMXadEYZFw3x/YOgstjw14OoKGZSYvjupA7h7247u/ZHQguw7aO3jmVTEzHAwO8yQqw== +"@chainsafe/blst@^0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@chainsafe/blst/-/blst-0.2.0.tgz#5e2d2707c2c0d56ff077a00179a5255eaca14099" + integrity sha512-eyyLm4C+Zhl18YwFa93J+xRSHj0NrBZodBO+z+aaREf71RnA7/EvOcAPVLpEW2CI7PsInhVne/ufb+A7gfHQrg== dependencies: node-fetch "^2.6.1" node-gyp "^7.1.2" @@ -1171,7 +1171,7 @@ callsite@1.0.0: resolved "https://registry.yarnpkg.com/callsite/-/callsite-1.0.0.tgz#280398e5d664bd74038b6f0905153e6e8af1bc20" integrity sha1-KAOY5dZkvXQDi28JBRU+borxvCA= -callsites@^3.0.0: +callsites@^3.0.0, callsites@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== @@ -2133,6 +2133,11 @@ eslint@^6.8.0: text-table "^0.2.0" v8-compile-cache "^2.0.3" +esm@^3.2.25: + version "3.2.25" + resolved "https://registry.yarnpkg.com/esm/-/esm-3.2.25.tgz#342c18c29d56157688ba5ce31f8431fbb795cc10" + integrity sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA== + espree@^6.1.2: version "6.1.2" resolved "https://registry.yarnpkg.com/espree/-/espree-6.1.2.tgz#6c272650932b4f91c3714e5e7b5f5e2ecf47262d" @@ -3152,6 +3157,13 @@ is-number@^7.0.0: resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== +is-observable@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-observable/-/is-observable-1.1.0.tgz#b3e986c8f44de950867cab5403f5a3465005975e" + integrity sha512-NqCa4Sa2d+u7BWc6CukaObG3Fh+CU9bvixbpcXYhy2VvYS7vVGIdAgnIS5Ks3A/cqk4rebLJ9s8zBstT2aKnIA== + dependencies: + symbol-observable "^1.1.0" + is-plain-obj@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" @@ -4216,6 +4228,11 @@ object.values@^1.1.0: function-bind "^1.1.1" has "^1.0.3" +observable-fns@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/observable-fns/-/observable-fns-0.5.1.tgz#9b56478690dd0fa8603e3a7e7d2975d88bca0904" + integrity sha512-wf7g4Jpo1Wt2KIqZKLGeiuLOEMqpaOZ5gJn7DmSdqXgTdxRwSdBhWegQQpPteQ2gZvzCKqNNpwb853wcpA0j7A== + on-finished@~2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" @@ -5518,6 +5535,11 @@ supports-color@^7.1.0: dependencies: has-flag "^4.0.0" +symbol-observable@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" + integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== + table@^5.2.3: version "5.4.6" resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e" @@ -5588,6 +5610,18 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= +threads@^1.6.3: + version "1.6.3" + resolved "https://registry.yarnpkg.com/threads/-/threads-1.6.3.tgz#89324a93509403c90a169344023151ae1fe4986b" + integrity sha512-tKwFIWRgfAT85KGkrpDt2jWPO8IVH0sLNfB/pXad/VW9eUIY2Zlz+QyeizypXhPHv9IHfqRzvk2t3mPw+imhWw== + dependencies: + callsites "^3.1.0" + debug "^4.1.1" + is-observable "^1.1.0" + observable-fns "^0.5.1" + optionalDependencies: + tiny-worker ">= 2" + through2@^2.0.0: version "2.0.5" resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" @@ -5608,6 +5642,13 @@ timers-browserify@^2.0.4: dependencies: setimmediate "^1.0.4" +"tiny-worker@>= 2": + version "2.3.0" + resolved "https://registry.yarnpkg.com/tiny-worker/-/tiny-worker-2.3.0.tgz#715ae34304c757a9af573ae9a8e3967177e6011e" + integrity sha512-pJ70wq5EAqTAEl9IkGzA+fN0836rycEuz2Cn6yeZ6FRzlVS5IDOkFHpIoEsksPRQV34GDqXm65+OlnZqUSyK2g== + dependencies: + esm "^3.2.25" + tmp@0.0.33, tmp@0.0.x, tmp@^0.0.33: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"