commit
804ed42354
|
@ -55,7 +55,7 @@ module.exports = {
|
||||||
"import/no-extraneous-dependencies": ["error", {
|
"import/no-extraneous-dependencies": ["error", {
|
||||||
"devDependencies": false,
|
"devDependencies": false,
|
||||||
"optionalDependencies": false,
|
"optionalDependencies": false,
|
||||||
"peerDependencies": false
|
"peerDependencies": true
|
||||||
}],
|
}],
|
||||||
"func-call-spacing": "off",
|
"func-call-spacing": "off",
|
||||||
"max-len": ["error", {
|
"max-len": ["error", {
|
||||||
|
|
|
@ -1,18 +1,32 @@
|
||||||
name: CI Tests
|
name: Main
|
||||||
|
|
||||||
on: [pull_request, push]
|
on: [pull_request, push]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
tests:
|
||||||
name: Quick tests
|
name: Tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v2
|
||||||
- name: Bootstrap
|
- uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: "12.x"
|
||||||
|
registry-url: "https://registry.npmjs.org"
|
||||||
|
- name: Install deps
|
||||||
run: yarn
|
run: yarn
|
||||||
- name: Check types
|
- name: Check types
|
||||||
run: yarn check-types
|
run: yarn check-types
|
||||||
- name: Lint
|
- name: Lint
|
||||||
run: yarn lint
|
run: yarn lint
|
||||||
- name: Tests
|
- name: Test build
|
||||||
run: yarn test
|
run: yarn build
|
||||||
|
|
||||||
|
- name: Unit tests
|
||||||
|
run: yarn test:unit
|
||||||
|
- name: Spec tests
|
||||||
|
run: yarn test:spec
|
||||||
|
- name: Web tests
|
||||||
|
run: yarn test:web
|
||||||
|
|
||||||
|
- name: Benchmark
|
||||||
|
run: yarn benchmark
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
.eslintrc.js
|
82
README.md
82
README.md
|
@ -6,22 +6,90 @@
|
||||||
![ES Version](https://img.shields.io/badge/ES-2017-yellow)
|
![ES Version](https://img.shields.io/badge/ES-2017-yellow)
|
||||||
![Node Version](https://img.shields.io/badge/node-12.x-green)
|
![Node Version](https://img.shields.io/badge/node-12.x-green)
|
||||||
|
|
||||||
This is a Javascript library that implements BLS (Boneh-Lynn-Shacham) signatures and supports signature aggregation.
|
Javascript library for BLS (Boneh-Lynn-Shacham) signatures and signature aggregation.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn add @chainsafe/bls
|
||||||
|
```
|
||||||
|
|
||||||
|
To use native bindings you must install peer dependency `@chainsafe/blst`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn add @chainsafe/bls @chainsafe/blst
|
||||||
|
```
|
||||||
|
|
||||||
|
You must initialize the library once in your application before using it. The result is cached and use across all your imports
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import bls, {init} from "@chainsafe/bls";
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
await init("herumi");
|
||||||
|
|
||||||
|
const secretKey = bls.PrivateKey.fromKeygen();
|
||||||
|
const publicKey = secretKey.toPublicKey();
|
||||||
|
const message = new Uint8Array(32);
|
||||||
|
|
||||||
|
const signature = secretKey.sign(message);
|
||||||
|
console.log("Is valid: ", signature.verify(publicKey, message));
|
||||||
|
})();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Browser
|
||||||
|
|
||||||
|
If you are in the browser, import from `/browser` to import directly the WASM version
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import bls from "@chainsafe/bls/browser";
|
||||||
|
```
|
||||||
|
|
||||||
|
### Native bindings only
|
||||||
|
|
||||||
|
If you are in NodeJS, import from `/node` to skip browser specific code. Also install peer dependency `@chainsafe/blst` which has the native bindings
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn add @chainsafe/bls @chainsafe/blst
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import bls from "@chainsafe/bls/node";
|
||||||
|
```
|
||||||
|
|
||||||
|
### Native bindings + WASM fallback
|
||||||
|
|
||||||
|
If you want to offer a fallback in NodeJS, first try to load native bindings and then fallback to WASM. Also install peer dependency `@chainsafe/blst` which has the native bindings
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn add @chainsafe/bls @chainsafe/blst
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import bls from "@chainsafe/bls";
|
||||||
|
|
||||||
|
try {
|
||||||
|
await bls.init("blst-native");
|
||||||
|
} catch (e) {
|
||||||
|
await bls.init("herumi");
|
||||||
|
console.warn("Using WASM");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The API is identical for all implementations.
|
||||||
|
|
||||||
|
## Spec versioning
|
||||||
|
|
||||||
| Version | Bls spec version |
|
| Version | Bls spec version |
|
||||||
| ------- | :--------------: |
|
| ------- | :--------------: |
|
||||||
| 0.3.x | initial version |
|
|
||||||
| 1.x.x | draft #6 |
|
|
||||||
| 2.x.x | draft #7 |
|
| 2.x.x | draft #7 |
|
||||||
|
| 1.x.x | draft #6 |
|
||||||
|
| 0.3.x | initial version |
|
||||||
|
|
||||||
> [spec](https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/beacon-chain.md#bls-signatures)
|
> [spec](https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/beacon-chain.md#bls-signatures)
|
||||||
|
|
||||||
> [test vectors](https://github.com/ethereum/eth2.0-spec-tests/tree/master/tests/bls)
|
> [test vectors](https://github.com/ethereum/eth2.0-spec-tests/tree/master/tests/bls)
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
- `yarn add @chainsafe/bls`
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Apache-2.0
|
Apache-2.0
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "./lib/blst";
|
|
@ -0,0 +1 @@
|
||||||
|
module.exports = require("./lib/blst");
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "./lib/herumi";
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "./lib/herumi";
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "./lib/herumi";
|
|
@ -6,7 +6,7 @@ module.exports = function(config) {
|
||||||
|
|
||||||
basePath: "",
|
basePath: "",
|
||||||
frameworks: ["mocha", "chai"],
|
frameworks: ["mocha", "chai"],
|
||||||
files: ["test/unit/*.ts"],
|
files: ["test/unit/run-web-implementation.test.ts"],
|
||||||
exclude: [],
|
exclude: [],
|
||||||
preprocessors: {
|
preprocessors: {
|
||||||
"test/**/*.ts": ["webpack"]
|
"test/**/*.ts": ["webpack"]
|
||||||
|
|
38
package.json
38
package.json
|
@ -4,13 +4,19 @@
|
||||||
"description": "Implementation of bls signature verification for ethereum 2.0",
|
"description": "Implementation of bls signature verification for ethereum 2.0",
|
||||||
"main": "lib/index.js",
|
"main": "lib/index.js",
|
||||||
"types": "lib/index.d.ts",
|
"types": "lib/index.d.ts",
|
||||||
|
"module": "./browser",
|
||||||
|
"browser": "./browser",
|
||||||
"homepage": "https://github.com/chainsafe/bls",
|
"homepage": "https://github.com/chainsafe/bls",
|
||||||
"author": "ChainSafe Systems",
|
"author": "ChainSafe Systems",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"files": [
|
"files": [
|
||||||
"lib/**/*.js",
|
"lib/**/*.js",
|
||||||
"lib/**/*.js.map",
|
"lib/**/*.js.map",
|
||||||
"lib/**/*.d.ts"
|
"lib/**/*.d.ts",
|
||||||
|
"blst-native.*",
|
||||||
|
"browser.*",
|
||||||
|
"herumi.*",
|
||||||
|
"node."
|
||||||
],
|
],
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"ethereum",
|
"ethereum",
|
||||||
|
@ -30,18 +36,18 @@
|
||||||
"lint:fix": "yarn run lint --fix",
|
"lint:fix": "yarn run lint --fix",
|
||||||
"pretest": "yarn check-types",
|
"pretest": "yarn check-types",
|
||||||
"prepublishOnly": "yarn build",
|
"prepublishOnly": "yarn build",
|
||||||
"test:web:unit": "karma start",
|
"test:web": "karma start",
|
||||||
"test:node:unit": "nyc --cache-dir .nyc_output/.cache -r lcov -e .ts mocha --colors -r ts-node/register 'test/unit/**/*.test.ts' && nyc report",
|
"test:unit": "mocha --colors -r ts-node/register 'test/unit/**/*.test.ts'",
|
||||||
"test:unit": "yarn run test:node:unit && yarn run test:web:unit",
|
"test:coverage": "nyc --cache-dir .nyc_output/.cache -r lcov -e .ts mocha --colors -r ts-node/register 'test/unit/**/*.test.ts' && nyc report",
|
||||||
"test:spec": "mocha --colors -r ts-node/register 'test/spec/**/*.test.ts'",
|
"test:spec": "mocha --colors -r ts-node/register 'test/spec/**/*.test.ts'",
|
||||||
"test": "yarn run test:unit && yarn run test:spec",
|
"test": "yarn run test:unit && yarn run test:spec",
|
||||||
"coverage": "codecov -F bls",
|
"coverage": "codecov -F bls",
|
||||||
"benchmark": "node -r ./.babel-register test/benchmarks"
|
"benchmark": "ts-node test/benchmark"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chainsafe/bls-keygen": "^0.2.0",
|
"@chainsafe/bls-keygen": "^0.3.0",
|
||||||
"@chainsafe/eth2-bls-wasm": "^0.5.0",
|
"bls-eth-wasm": "^0.4.4",
|
||||||
"assert": "^1.4.1"
|
"randombytes": "^2.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/cli": "^7.8.4",
|
"@babel/cli": "^7.8.4",
|
||||||
|
@ -52,11 +58,12 @@
|
||||||
"@babel/preset-env": "^7.8.4",
|
"@babel/preset-env": "^7.8.4",
|
||||||
"@babel/preset-typescript": "^7.8.3",
|
"@babel/preset-typescript": "^7.8.3",
|
||||||
"@babel/register": "^7.8.3",
|
"@babel/register": "^7.8.3",
|
||||||
"@chainsafe/as-sha256": "0.2.0",
|
"@chainsafe/blst": "^0.1.3",
|
||||||
"@chainsafe/eth2-spec-tests": "0.12.0",
|
"@chainsafe/eth2-spec-tests": "0.12.0",
|
||||||
"@chainsafe/lodestar-spec-test-util": "^0.5.0",
|
"@chainsafe/lodestar-spec-test-util": "^0.11.0",
|
||||||
"@types/chai": "^4.2.9",
|
"@types/chai": "^4.2.9",
|
||||||
"@types/mocha": "^7.0.1",
|
"@types/mocha": "^8.0.4",
|
||||||
|
"@types/randombytes": "^2.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^2.20.0",
|
"@typescript-eslint/eslint-plugin": "^2.20.0",
|
||||||
"@typescript-eslint/parser": "^2.20.0",
|
"@typescript-eslint/parser": "^2.20.0",
|
||||||
"chai": "^4.2.0",
|
"chai": "^4.2.0",
|
||||||
|
@ -70,7 +77,7 @@
|
||||||
"karma-mocha": "^1.3.0",
|
"karma-mocha": "^1.3.0",
|
||||||
"karma-spec-reporter": "^0.0.32",
|
"karma-spec-reporter": "^0.0.32",
|
||||||
"karma-webpack": "^4.0.2",
|
"karma-webpack": "^4.0.2",
|
||||||
"mocha": "^6.2.0",
|
"mocha": "^8.2.1",
|
||||||
"nyc": "^15.0.0",
|
"nyc": "^15.0.0",
|
||||||
"prettier": "^2.1.2",
|
"prettier": "^2.1.2",
|
||||||
"ts-loader": "^6.2.1",
|
"ts-loader": "^6.2.1",
|
||||||
|
@ -78,5 +85,12 @@
|
||||||
"typescript": "^3.7.5",
|
"typescript": "^3.7.5",
|
||||||
"webpack": "^4.30.0",
|
"webpack": "^4.30.0",
|
||||||
"webpack-cli": "^3.3.2"
|
"webpack-cli": "^3.3.2"
|
||||||
|
},
|
||||||
|
"resolutions": {
|
||||||
|
"mocha": "^8.2.1",
|
||||||
|
"v8-profiler-next": "1.3.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@chainsafe/blst": "^0.1.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
import {PrivateKey} from "./privateKey";
|
||||||
|
import {PublicKey} from "./publicKey";
|
||||||
|
import {Signature} from "./signature";
|
||||||
|
import {IBls} from "../interface";
|
||||||
|
import {functionalInterfaceFactory} from "../functional";
|
||||||
|
export * from "../constants";
|
||||||
|
|
||||||
|
export {PrivateKey, PublicKey, Signature};
|
||||||
|
|
||||||
|
export async function init(): Promise<void> {
|
||||||
|
// Native bindings require no init() call
|
||||||
|
}
|
||||||
|
export function destroy(): void {
|
||||||
|
// Native bindings require no destroy() call
|
||||||
|
}
|
||||||
|
|
||||||
|
export const bls: IBls = {
|
||||||
|
PrivateKey,
|
||||||
|
PublicKey,
|
||||||
|
Signature,
|
||||||
|
|
||||||
|
...functionalInterfaceFactory({PrivateKey, PublicKey, Signature}),
|
||||||
|
init,
|
||||||
|
destroy,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default bls;
|
|
@ -0,0 +1,46 @@
|
||||||
|
import * as blst from "@chainsafe/blst";
|
||||||
|
import {bytesToHex, hexToBytes, randomBytes} from "../helpers";
|
||||||
|
import {SECRET_KEY_LENGTH} from "../constants";
|
||||||
|
import {IPrivateKey} from "../interface";
|
||||||
|
import {PublicKey} from "./publicKey";
|
||||||
|
import {Signature} from "./signature";
|
||||||
|
|
||||||
|
export class PrivateKey implements IPrivateKey {
|
||||||
|
readonly value: blst.SecretKey;
|
||||||
|
|
||||||
|
constructor(value: blst.SecretKey) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromBytes(bytes: Uint8Array): PrivateKey {
|
||||||
|
const sk = blst.SecretKey.fromBytes(bytes);
|
||||||
|
return new PrivateKey(sk);
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromHex(hex: string): PrivateKey {
|
||||||
|
return this.fromBytes(hexToBytes(hex));
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromKeygen(entropy?: Uint8Array): PrivateKey {
|
||||||
|
const sk = blst.SecretKey.fromKeygen(entropy || randomBytes(SECRET_KEY_LENGTH));
|
||||||
|
return new PrivateKey(sk);
|
||||||
|
}
|
||||||
|
|
||||||
|
sign(message: Uint8Array): Signature {
|
||||||
|
return new Signature(this.value.sign(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
toPublicKey(): PublicKey {
|
||||||
|
const jacobian = this.value.toAggregatePublicKey();
|
||||||
|
const affine = jacobian.toPublicKey();
|
||||||
|
return new PublicKey(affine, jacobian);
|
||||||
|
}
|
||||||
|
|
||||||
|
toBytes(): Uint8Array {
|
||||||
|
return this.value.toBytes();
|
||||||
|
}
|
||||||
|
|
||||||
|
toHex(): string {
|
||||||
|
return bytesToHex(this.toBytes());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
import * as blst from "@chainsafe/blst";
|
||||||
|
import {bytesToHex, hexToBytes} from "../helpers";
|
||||||
|
import {IPublicKey} from "../interface";
|
||||||
|
import {Signature} from "./signature";
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromBytes(bytes: Uint8Array): PublicKey {
|
||||||
|
const affine = blst.PublicKey.fromBytes(bytes);
|
||||||
|
const jacobian = blst.AggregatePublicKey.fromPublicKey(affine);
|
||||||
|
return new PublicKey(affine, jacobian);
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromHex(hex: string): PublicKey {
|
||||||
|
return this.fromBytes(hexToBytes(hex));
|
||||||
|
}
|
||||||
|
|
||||||
|
static aggregate(pubkeys: PublicKey[]): PublicKey {
|
||||||
|
const jacobian = blst.aggregatePubkeys(pubkeys.map((pk) => pk.jacobian));
|
||||||
|
const affine = jacobian.toPublicKey();
|
||||||
|
return new PublicKey(affine, jacobian);
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyMessage(signature: Signature, message: Uint8Array): boolean {
|
||||||
|
return signature.verify(this, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
toBytes(): Uint8Array {
|
||||||
|
return this.affine.toBytes();
|
||||||
|
}
|
||||||
|
|
||||||
|
toHex(): string {
|
||||||
|
return bytesToHex(this.toBytes());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
import * as blst from "@chainsafe/blst";
|
||||||
|
import {bytesToHex, hexToBytes} from "../helpers";
|
||||||
|
import {ISignature} from "../interface";
|
||||||
|
import {PublicKey} from "./publicKey";
|
||||||
|
|
||||||
|
export class Signature implements ISignature {
|
||||||
|
readonly affine: blst.Signature;
|
||||||
|
|
||||||
|
constructor(value: blst.Signature) {
|
||||||
|
this.affine = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromBytes(bytes: Uint8Array): Signature {
|
||||||
|
return new Signature(blst.Signature.fromBytes(bytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromHex(hex: string): Signature {
|
||||||
|
return this.fromBytes(hexToBytes(hex));
|
||||||
|
}
|
||||||
|
|
||||||
|
static aggregate(signatures: Signature[]): Signature {
|
||||||
|
const agg = blst.AggregateSignature.fromSignatures(signatures.map((sig) => sig.affine));
|
||||||
|
return new Signature(agg.toSignature());
|
||||||
|
}
|
||||||
|
|
||||||
|
verify(publicKey: PublicKey, message: Uint8Array): boolean {
|
||||||
|
return this.aggregateVerify([message], [publicKey.affine]);
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyAggregate(publicKeys: PublicKey[], message: Uint8Array): boolean {
|
||||||
|
const agg = PublicKey.aggregate(publicKeys);
|
||||||
|
return this.aggregateVerify([message], [agg.affine]);
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyMultiple(publicKeys: PublicKey[], messages: Uint8Array[]): boolean {
|
||||||
|
return this.aggregateVerify(
|
||||||
|
messages,
|
||||||
|
publicKeys.map((pk) => pk.affine)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
toBytes(): Uint8Array {
|
||||||
|
return this.affine.toBytes();
|
||||||
|
}
|
||||||
|
|
||||||
|
toHex(): string {
|
||||||
|
return bytesToHex(this.toBytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
private aggregateVerify(msgs: Uint8Array[], pks: blst.PublicKey[]): boolean {
|
||||||
|
// If this set is simply an infinity signature and infinity pubkey then skip verification.
|
||||||
|
// This has the effect of always declaring that this sig/pubkey combination is valid.
|
||||||
|
// for Eth2.0 specs tests
|
||||||
|
if (this.affine.value.is_inf() && pks.length === 1 && pks[0].value.is_inf()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return blst.aggregateVerify(msgs, pks, this.affine);
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,3 +3,5 @@ export const SIGNATURE_LENGTH = 96;
|
||||||
export const FP_POINT_LENGTH = 48;
|
export const FP_POINT_LENGTH = 48;
|
||||||
export const PUBLIC_KEY_LENGTH = FP_POINT_LENGTH;
|
export const PUBLIC_KEY_LENGTH = FP_POINT_LENGTH;
|
||||||
export const G2_HASH_PADDING = 16;
|
export const G2_HASH_PADDING = 16;
|
||||||
|
export const EMPTY_PUBLIC_KEY = new Uint8Array(PUBLIC_KEY_LENGTH);
|
||||||
|
export const EMPTY_SIGNATURE = new Uint8Array(SIGNATURE_LENGTH);
|
||||||
|
|
|
@ -0,0 +1,114 @@
|
||||||
|
import {validateBytes} from "./helpers";
|
||||||
|
import {IBls} from "./interface";
|
||||||
|
|
||||||
|
// Returned type is enforced at each implementation's index
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||||
|
export function functionalInterfaceFactory({
|
||||||
|
PrivateKey,
|
||||||
|
PublicKey,
|
||||||
|
Signature,
|
||||||
|
}: Pick<IBls, "PrivateKey" | "PublicKey" | "Signature">) {
|
||||||
|
/**
|
||||||
|
* Signs given message using secret key.
|
||||||
|
* @param secretKey
|
||||||
|
* @param message
|
||||||
|
*/
|
||||||
|
function sign(secretKey: Uint8Array, message: Uint8Array): Uint8Array {
|
||||||
|
validateBytes(secretKey, "secretKey");
|
||||||
|
validateBytes(message, "message");
|
||||||
|
|
||||||
|
const privateKey = PrivateKey.fromBytes(secretKey);
|
||||||
|
return privateKey.sign(message).toBytes();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compines all given signature into one.
|
||||||
|
* @param signatures
|
||||||
|
*/
|
||||||
|
function aggregateSignatures(signatures: Uint8Array[]): Uint8Array {
|
||||||
|
const agg = Signature.aggregate(signatures.map((p) => Signature.fromBytes(p)));
|
||||||
|
return agg.toBytes();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combines all given public keys into single one
|
||||||
|
* @param publicKeys
|
||||||
|
*/
|
||||||
|
function aggregatePubkeys(publicKeys: Uint8Array[]): Uint8Array {
|
||||||
|
const agg = PublicKey.aggregate(publicKeys.map((p) => PublicKey.fromBytes(p)));
|
||||||
|
return agg.toBytes();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies if signature is message signed with given public key.
|
||||||
|
* @param publicKey
|
||||||
|
* @param message
|
||||||
|
* @param signature
|
||||||
|
*/
|
||||||
|
function verify(publicKey: Uint8Array, message: Uint8Array, signature: Uint8Array): boolean {
|
||||||
|
validateBytes(publicKey, "publicKey");
|
||||||
|
validateBytes(message, "message");
|
||||||
|
validateBytes(signature, "signature");
|
||||||
|
|
||||||
|
try {
|
||||||
|
return Signature.fromBytes(signature).verify(PublicKey.fromBytes(publicKey), message);
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies if aggregated signature is same message signed with given public keys.
|
||||||
|
* @param publicKeys
|
||||||
|
* @param message
|
||||||
|
* @param signature
|
||||||
|
*/
|
||||||
|
function verifyAggregate(publicKeys: Uint8Array[], message: Uint8Array, signature: Uint8Array): boolean {
|
||||||
|
validateBytes(publicKeys, "publicKey");
|
||||||
|
validateBytes(message, "message");
|
||||||
|
validateBytes(signature, "signature");
|
||||||
|
|
||||||
|
try {
|
||||||
|
return Signature.fromBytes(signature).verifyAggregate(
|
||||||
|
publicKeys.map((pubkey) => PublicKey.fromBytes(pubkey)),
|
||||||
|
message
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies if signature is list of message signed with corresponding public key.
|
||||||
|
* @param publicKeys
|
||||||
|
* @param messages
|
||||||
|
* @param signature
|
||||||
|
* @param fast Check if all messages are different
|
||||||
|
*/
|
||||||
|
function verifyMultiple(publicKeys: Uint8Array[], messages: Uint8Array[], signature: Uint8Array): boolean {
|
||||||
|
validateBytes(publicKeys, "publicKey");
|
||||||
|
validateBytes(messages, "message");
|
||||||
|
validateBytes(signature, "signature");
|
||||||
|
|
||||||
|
if (publicKeys.length === 0 || publicKeys.length != messages.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Signature.fromBytes(signature).verifyMultiple(
|
||||||
|
publicKeys.map((publicKey) => PublicKey.fromBytes(publicKey)),
|
||||||
|
messages.map((msg) => msg)
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
sign,
|
||||||
|
aggregateSignatures,
|
||||||
|
aggregatePubkeys,
|
||||||
|
verify,
|
||||||
|
verifyAggregate,
|
||||||
|
verifyMultiple,
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
/**
|
||||||
|
* Browser compatible fromHex method
|
||||||
|
* From https://github.com/herumi/bls-eth-wasm/blob/04eedb77aa96e66b4f65a0ab477228adf8090c36/src/bls.js#L62
|
||||||
|
*/
|
||||||
|
export function hexToBytes(hex: string): Uint8Array {
|
||||||
|
if (hex.startsWith("0x")) {
|
||||||
|
hex = hex.slice(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hex.length & 1) {
|
||||||
|
throw Error("hexToBytes:length must be even " + hex.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
const n = hex.length / 2;
|
||||||
|
const a = new Uint8Array(n);
|
||||||
|
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
a[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Browser compatible toHex method
|
||||||
|
* From https://github.com/herumi/bls-eth-wasm/blob/04eedb77aa96e66b4f65a0ab477228adf8090c36/src/bls.js#L50
|
||||||
|
*/
|
||||||
|
export function bytesToHex(bytes: Uint8Array): string {
|
||||||
|
// return "0x" + Buffer.from(bytes).toString("hex");
|
||||||
|
let s = "";
|
||||||
|
const n = bytes.length;
|
||||||
|
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
s += ("0" + bytes[i].toString(16)).slice(-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
return "0x" + s;
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from "./hex";
|
||||||
|
export * from "./utils";
|
|
@ -1,18 +1,26 @@
|
||||||
import assert from "assert";
|
import randomBytes from "randombytes";
|
||||||
import {PUBLIC_KEY_LENGTH, SIGNATURE_LENGTH} from "../constants";
|
|
||||||
|
|
||||||
/**
|
// Single import to ease changing this lib if necessary
|
||||||
* Pads byte array with zeroes on left side up to desired length.
|
export {randomBytes};
|
||||||
* Throws if source is larger than desired result.
|
|
||||||
* @param source
|
export function isEqualBytes(a: Buffer | Uint8Array, b: Buffer | Uint8Array): boolean {
|
||||||
* @param length
|
return toBuffer(a).equals(toBuffer(b));
|
||||||
*/
|
|
||||||
export function padLeft(source: Uint8Array, length: number): Buffer {
|
|
||||||
assert(source.length <= length, "Given array must be smaller or equal to desired array size");
|
|
||||||
const result = Buffer.alloc(length, 0);
|
|
||||||
result.set(source, length - source.length);
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EMPTY_PUBLIC_KEY = Buffer.alloc(PUBLIC_KEY_LENGTH);
|
export function toBuffer(input: Uint8Array): Buffer {
|
||||||
export const EMPTY_SIGNATURE = Buffer.alloc(SIGNATURE_LENGTH);
|
return Buffer.from(input.buffer, input.byteOffset, input.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate bytes to prevent confusing WASM errors downstream if bytes is null
|
||||||
|
*/
|
||||||
|
export function validateBytes(
|
||||||
|
bytes: Uint8Array | Uint8Array[] | null,
|
||||||
|
argName?: string
|
||||||
|
): asserts bytes is NonNullable<typeof bytes> {
|
||||||
|
for (const item of Array.isArray(bytes) ? bytes : [bytes]) {
|
||||||
|
if (item == null) {
|
||||||
|
throw Error(`${argName || "bytes"} is null or undefined`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,20 +1,19 @@
|
||||||
/* eslint-disable require-atomic-updates */
|
/* eslint-disable require-atomic-updates */
|
||||||
import bls from "@chainsafe/eth2-bls-wasm";
|
import bls from "bls-eth-wasm";
|
||||||
|
|
||||||
type Bls = typeof bls;
|
type Bls = typeof bls;
|
||||||
let blsGlobal: Bls | null = null;
|
let blsGlobal: Bls | null = null;
|
||||||
let blsGlobalPromise: Promise<Bls> | null = null;
|
let blsGlobalPromise: Promise<void> | null = null;
|
||||||
|
|
||||||
export async function setupBls(): Promise<Bls> {
|
export async function setupBls(): Promise<void> {
|
||||||
if (!blsGlobal) {
|
if (!blsGlobal) {
|
||||||
await bls.init();
|
await bls.init(bls.BLS12_381);
|
||||||
blsGlobal = bls;
|
blsGlobal = bls;
|
||||||
}
|
}
|
||||||
return blsGlobal;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache a promise for Bls instead of Bls to make sure it is initialized only once
|
// Cache a promise for Bls instead of Bls to make sure it is initialized only once
|
||||||
export async function init(): Promise<Bls> {
|
export async function init(): Promise<void> {
|
||||||
if (!blsGlobalPromise) {
|
if (!blsGlobalPromise) {
|
||||||
blsGlobalPromise = setupBls();
|
blsGlobalPromise = setupBls();
|
||||||
}
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
import {PrivateKey} from "./privateKey";
|
||||||
|
import {PublicKey} from "./publicKey";
|
||||||
|
import {Signature} from "./signature";
|
||||||
|
import {init, destroy} from "./context";
|
||||||
|
import {IBls} from "../interface";
|
||||||
|
import {functionalInterfaceFactory} from "../functional";
|
||||||
|
export * from "../constants";
|
||||||
|
|
||||||
|
export {PrivateKey, PublicKey, Signature, init, destroy};
|
||||||
|
|
||||||
|
export const bls: IBls = {
|
||||||
|
PrivateKey,
|
||||||
|
PublicKey,
|
||||||
|
Signature,
|
||||||
|
|
||||||
|
...functionalInterfaceFactory({PrivateKey, PublicKey, Signature}),
|
||||||
|
init,
|
||||||
|
destroy,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default bls;
|
|
@ -0,0 +1,52 @@
|
||||||
|
import {SecretKeyType} from "bls-eth-wasm";
|
||||||
|
import {generateRandomSecretKey} from "@chainsafe/bls-keygen";
|
||||||
|
import {SECRET_KEY_LENGTH} from "../constants";
|
||||||
|
import {getContext} from "./context";
|
||||||
|
import {PublicKey} from "./publicKey";
|
||||||
|
import {Signature} from "./signature";
|
||||||
|
import {bytesToHex, hexToBytes} from "../helpers";
|
||||||
|
import {IPrivateKey} from "../interface";
|
||||||
|
|
||||||
|
export class PrivateKey implements IPrivateKey {
|
||||||
|
readonly value: SecretKeyType;
|
||||||
|
|
||||||
|
constructor(value: SecretKeyType) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromBytes(bytes: Uint8Array): PrivateKey {
|
||||||
|
if (bytes.length !== SECRET_KEY_LENGTH) {
|
||||||
|
throw Error(`Private key should have ${SECRET_KEY_LENGTH} bytes`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const context = getContext();
|
||||||
|
const secretKey = new context.SecretKey();
|
||||||
|
secretKey.deserialize(bytes);
|
||||||
|
return new PrivateKey(secretKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromHex(hex: string): PrivateKey {
|
||||||
|
return this.fromBytes(hexToBytes(hex));
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromKeygen(entropy?: Uint8Array): PrivateKey {
|
||||||
|
const sk = generateRandomSecretKey(entropy && Buffer.from(entropy));
|
||||||
|
return this.fromBytes(sk);
|
||||||
|
}
|
||||||
|
|
||||||
|
sign(message: Uint8Array): Signature {
|
||||||
|
return new Signature(this.value.sign(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
toPublicKey(): PublicKey {
|
||||||
|
return new PublicKey(this.value.getPublicKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
toBytes(): Uint8Array {
|
||||||
|
return this.value.serialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
toHex(): string {
|
||||||
|
return bytesToHex(this.toBytes());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
import {PublicKeyType} from "bls-eth-wasm";
|
||||||
|
import {getContext} from "./context";
|
||||||
|
import {EMPTY_PUBLIC_KEY, PUBLIC_KEY_LENGTH} from "../constants";
|
||||||
|
import {Signature} from "./signature";
|
||||||
|
import {bytesToHex, hexToBytes, isEqualBytes} from "../helpers";
|
||||||
|
import {IPublicKey} from "../interface";
|
||||||
|
|
||||||
|
export class PublicKey implements IPublicKey {
|
||||||
|
readonly value: PublicKeyType;
|
||||||
|
|
||||||
|
constructor(value: PublicKeyType) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromBytes(bytes: Uint8Array): PublicKey {
|
||||||
|
if (bytes.length !== PUBLIC_KEY_LENGTH) {
|
||||||
|
throw Error(`Public key must have ${PUBLIC_KEY_LENGTH} bytes`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const context = getContext();
|
||||||
|
const publicKey = new context.PublicKey();
|
||||||
|
if (!isEqualBytes(EMPTY_PUBLIC_KEY, bytes)) {
|
||||||
|
publicKey.deserialize(bytes);
|
||||||
|
}
|
||||||
|
return new PublicKey(publicKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromHex(hex: string): PublicKey {
|
||||||
|
return this.fromBytes(hexToBytes(hex));
|
||||||
|
}
|
||||||
|
|
||||||
|
static aggregate(pubkeys: PublicKey[]): PublicKey {
|
||||||
|
if (pubkeys.length === 0) {
|
||||||
|
throw Error("EMPTY_AGGREGATE_ARRAY");
|
||||||
|
}
|
||||||
|
|
||||||
|
const agg = new PublicKey(pubkeys[0].value.clone());
|
||||||
|
for (const pk of pubkeys.slice(1)) {
|
||||||
|
agg.value.add(pk.value);
|
||||||
|
}
|
||||||
|
return agg;
|
||||||
|
}
|
||||||
|
|
||||||
|
add(other: PublicKey): PublicKey {
|
||||||
|
const agg = new PublicKey(this.value.clone());
|
||||||
|
agg.value.add(other.value);
|
||||||
|
return agg;
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyMessage(signature: Signature, message: Uint8Array): boolean {
|
||||||
|
return this.value.verify(signature.value, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
toBytes(): Uint8Array {
|
||||||
|
return this.value.serialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
toHex(): string {
|
||||||
|
return bytesToHex(this.toBytes());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
import {SIGNATURE_LENGTH, EMPTY_SIGNATURE} from "../constants";
|
||||||
|
import {SignatureType} from "bls-eth-wasm";
|
||||||
|
import {getContext} from "./context";
|
||||||
|
import {PublicKey} from "./publicKey";
|
||||||
|
import {bytesToHex, hexToBytes, isEqualBytes} from "../helpers";
|
||||||
|
import {ISignature} from "../interface";
|
||||||
|
|
||||||
|
export class Signature implements ISignature {
|
||||||
|
readonly value: SignatureType;
|
||||||
|
|
||||||
|
constructor(value: SignatureType) {
|
||||||
|
if (!value.isValidOrder()) {
|
||||||
|
throw Error("Signature is not in valid order");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromBytes(bytes: Uint8Array): Signature {
|
||||||
|
if (bytes.length !== SIGNATURE_LENGTH) {
|
||||||
|
throw Error(`Signature must have ${SIGNATURE_LENGTH} bytes`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const context = getContext();
|
||||||
|
const signature = new context.Signature();
|
||||||
|
if (!isEqualBytes(EMPTY_SIGNATURE, bytes)) {
|
||||||
|
signature.deserialize(bytes);
|
||||||
|
}
|
||||||
|
return new Signature(signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromHex(hex: string): Signature {
|
||||||
|
return this.fromBytes(hexToBytes(hex));
|
||||||
|
}
|
||||||
|
|
||||||
|
static aggregate(signatures: Signature[]): Signature {
|
||||||
|
if (signatures.length === 0) {
|
||||||
|
throw Error("EMPTY_AGGREGATE_ARRAY");
|
||||||
|
}
|
||||||
|
|
||||||
|
const context = getContext();
|
||||||
|
const signature = new context.Signature();
|
||||||
|
signature.aggregate(signatures.map((sig) => sig.value));
|
||||||
|
return new Signature(signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
add(other: Signature): Signature {
|
||||||
|
const agg = this.value.clone();
|
||||||
|
agg.add(other.value);
|
||||||
|
return new Signature(agg);
|
||||||
|
}
|
||||||
|
|
||||||
|
verify(publicKey: PublicKey, message: Uint8Array): boolean {
|
||||||
|
return publicKey.value.verify(this.value, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyAggregate(publicKeys: PublicKey[], message: Uint8Array): boolean {
|
||||||
|
return this.value.fastAggregateVerify(
|
||||||
|
publicKeys.map((key) => key.value),
|
||||||
|
message
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyMultiple(publicKeys: PublicKey[], messages: Uint8Array[]): boolean {
|
||||||
|
const msgs = Buffer.concat(messages);
|
||||||
|
return this.value.aggregateVerifyNoCheck(
|
||||||
|
publicKeys.map((key) => key.value),
|
||||||
|
msgs
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
toBytes(): Uint8Array {
|
||||||
|
return this.value.serialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
toHex(): string {
|
||||||
|
return bytesToHex(this.toBytes());
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
import bls from "./index";
|
import {bls} from "./index";
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
(function (window: any) {
|
(function (window: any) {
|
166
src/index.ts
166
src/index.ts
|
@ -1,155 +1,31 @@
|
||||||
import {Keypair} from "./keypair";
|
import {IBls} from "./interface";
|
||||||
import {PrivateKey} from "./privateKey";
|
import {bls as blsHerumi} from "./herumi";
|
||||||
import {PublicKey} from "./publicKey";
|
|
||||||
import {Signature} from "./signature";
|
|
||||||
import {PUBLIC_KEY_LENGTH} from "./constants";
|
|
||||||
import assert from "assert";
|
|
||||||
|
|
||||||
export {Keypair, PrivateKey, PublicKey, Signature};
|
export type Implementation = "herumi" | "blst-native";
|
||||||
|
|
||||||
export {init as initBLS} from "./context";
|
// TODO: Use a Proxy for example to throw an error if it's not initialized yet
|
||||||
|
export let bls: IBls;
|
||||||
|
|
||||||
function toBuffer(input: Uint8Array): Buffer {
|
async function getImplementation(impl: Implementation = "herumi"): Promise<IBls> {
|
||||||
return Buffer.from(input.buffer, input.byteOffset, input.length);
|
switch (impl) {
|
||||||
|
case "herumi":
|
||||||
|
await blsHerumi.init();
|
||||||
|
return blsHerumi;
|
||||||
|
|
||||||
|
case "blst-native":
|
||||||
|
if (typeof require !== "function") {
|
||||||
|
throw Error("blst-native is only supported in NodeJS");
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||||
|
return require("./blst");
|
||||||
|
|
||||||
/**
|
default:
|
||||||
* Generates new secret and public key
|
throw new Error(`Unsupported implementation - ${impl}`);
|
||||||
*/
|
|
||||||
export function generateKeyPair(): Keypair {
|
|
||||||
return Keypair.generate();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates public key from given secret.
|
|
||||||
* @param {BLSSecretKey} secretKey
|
|
||||||
*/
|
|
||||||
export function generatePublicKey(secretKey: Uint8Array): Buffer {
|
|
||||||
assert(secretKey, "secretKey is null or undefined");
|
|
||||||
const keypair = new Keypair(PrivateKey.fromBytes(toBuffer(secretKey)));
|
|
||||||
return keypair.publicKey.toBytesCompressed();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Signs given message using secret key.
|
|
||||||
* @param secretKey
|
|
||||||
* @param messageHash
|
|
||||||
*/
|
|
||||||
export function sign(secretKey: Uint8Array, messageHash: Uint8Array): Buffer {
|
|
||||||
assert(secretKey, "secretKey is null or undefined");
|
|
||||||
assert(messageHash, "messageHash is null or undefined");
|
|
||||||
const privateKey = PrivateKey.fromBytes(toBuffer(secretKey));
|
|
||||||
return privateKey.signMessage(toBuffer(messageHash)).toBytesCompressed();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compines all given signature into one.
|
|
||||||
* @param signatures
|
|
||||||
*/
|
|
||||||
export function aggregateSignatures(signatures: Uint8Array[]): Buffer {
|
|
||||||
assert(signatures && signatures.length > 0, "signatures is null or undefined or empty array");
|
|
||||||
return Signature.aggregate(
|
|
||||||
signatures.map(
|
|
||||||
(signature): Signature => {
|
|
||||||
return Signature.fromCompressedBytes(signature);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
).toBytesCompressed();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Combines all given public keys into single one
|
|
||||||
* @param publicKeys
|
|
||||||
*/
|
|
||||||
export function aggregatePubkeys(publicKeys: Uint8Array[]): Buffer {
|
|
||||||
assert(publicKeys, "publicKeys is null or undefined");
|
|
||||||
if (publicKeys.length === 0) {
|
|
||||||
return Buffer.alloc(PUBLIC_KEY_LENGTH);
|
|
||||||
}
|
|
||||||
return publicKeys
|
|
||||||
.map((p) => PublicKey.fromBytes(toBuffer(p)))
|
|
||||||
.reduce((agg, pubKey) => agg.add(pubKey))
|
|
||||||
.toBytesCompressed();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verifies if signature is message signed with given public key.
|
|
||||||
* @param publicKey
|
|
||||||
* @param messageHash
|
|
||||||
* @param signature
|
|
||||||
*/
|
|
||||||
export function verify(publicKey: Uint8Array, messageHash: Uint8Array, signature: Uint8Array): boolean {
|
|
||||||
assert(publicKey, "publicKey is null or undefined");
|
|
||||||
assert(messageHash, "messageHash is null or undefined");
|
|
||||||
assert(signature, "signature is null or undefined");
|
|
||||||
try {
|
|
||||||
return PublicKey.fromBytes(publicKey).verifyMessage(
|
|
||||||
Signature.fromCompressedBytes(toBuffer(signature)),
|
|
||||||
toBuffer(messageHash)
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export async function init(impl: Implementation): Promise<void> {
|
||||||
* Verifies if aggregated signature is same message signed with given public keys.
|
bls = await getImplementation(impl);
|
||||||
* @param publicKeys
|
|
||||||
* @param messageHash
|
|
||||||
* @param signature
|
|
||||||
*/
|
|
||||||
export function verifyAggregate(publicKeys: Uint8Array[], messageHash: Uint8Array, signature: Uint8Array): boolean {
|
|
||||||
assert(publicKeys, "publicKey is null or undefined");
|
|
||||||
assert(messageHash, "messageHash is null or undefined");
|
|
||||||
assert(signature, "signature is null or undefined");
|
|
||||||
try {
|
|
||||||
return Signature.fromCompressedBytes(signature).verifyAggregate(
|
|
||||||
publicKeys.map((pubkey) => PublicKey.fromBytes(pubkey)),
|
|
||||||
messageHash
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export default bls;
|
||||||
* Verifies if signature is list of message signed with corresponding public key.
|
|
||||||
* @param publicKeys
|
|
||||||
* @param messageHashes
|
|
||||||
* @param signature
|
|
||||||
* @param fast Check if all messages are different
|
|
||||||
*/
|
|
||||||
export function verifyMultiple(
|
|
||||||
publicKeys: Uint8Array[],
|
|
||||||
messageHashes: Uint8Array[],
|
|
||||||
signature: Uint8Array,
|
|
||||||
fast = false
|
|
||||||
): boolean {
|
|
||||||
assert(publicKeys, "publicKey is null or undefined");
|
|
||||||
assert(messageHashes, "messageHash is null or undefined");
|
|
||||||
assert(signature, "signature is null or undefined");
|
|
||||||
|
|
||||||
if (publicKeys.length === 0 || publicKeys.length != messageHashes.length) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return Signature.fromCompressedBytes(toBuffer(signature)).verifyMultiple(
|
|
||||||
publicKeys.map((key) => PublicKey.fromBytes(toBuffer(key))),
|
|
||||||
messageHashes.map((m) => toBuffer(m)),
|
|
||||||
fast
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
generateKeyPair,
|
|
||||||
generatePublicKey,
|
|
||||||
sign,
|
|
||||||
aggregateSignatures,
|
|
||||||
aggregatePubkeys,
|
|
||||||
verify,
|
|
||||||
verifyAggregate,
|
|
||||||
verifyMultiple,
|
|
||||||
};
|
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
export interface IBls {
|
||||||
|
PrivateKey: {
|
||||||
|
fromBytes(bytes: Uint8Array): IPrivateKey;
|
||||||
|
fromHex(hex: string): IPrivateKey;
|
||||||
|
fromKeygen(): IPrivateKey;
|
||||||
|
};
|
||||||
|
PublicKey: {
|
||||||
|
fromBytes(bytes: Uint8Array): IPublicKey;
|
||||||
|
fromHex(hex: string): IPublicKey;
|
||||||
|
aggregate(pubkeys: IPublicKey[]): IPublicKey;
|
||||||
|
};
|
||||||
|
Signature: {
|
||||||
|
fromBytes(bytes: Uint8Array): ISignature;
|
||||||
|
fromHex(hex: string): ISignature;
|
||||||
|
aggregate(signatures: ISignature[]): ISignature;
|
||||||
|
};
|
||||||
|
|
||||||
|
sign(secretKey: Uint8Array, message: Uint8Array): Uint8Array;
|
||||||
|
aggregatePubkeys(publicKeys: Uint8Array[]): Uint8Array;
|
||||||
|
aggregateSignatures(signatures: Uint8Array[]): Uint8Array;
|
||||||
|
verify(publicKey: Uint8Array, message: Uint8Array, signature: Uint8Array): boolean;
|
||||||
|
verifyAggregate(publicKeys: Uint8Array[], message: Uint8Array, signature: Uint8Array): boolean;
|
||||||
|
verifyMultiple(publicKeys: Uint8Array[], messages: Uint8Array[], signature: Uint8Array): boolean;
|
||||||
|
|
||||||
|
init(): Promise<void>;
|
||||||
|
destroy(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IKeypair {
|
||||||
|
publicKey: IPublicKey;
|
||||||
|
privateKey: IPrivateKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPublicKey {
|
||||||
|
toBytes(): Uint8Array;
|
||||||
|
toHex(): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISignature {
|
||||||
|
toBytes(): Uint8Array;
|
||||||
|
toHex(): string;
|
||||||
|
verify(publicKey: IPublicKey, message: Uint8Array): boolean;
|
||||||
|
verifyAggregate(publicKeys: IPublicKey[], message: Uint8Array): boolean;
|
||||||
|
verifyMultiple(publicKeys: IPublicKey[], messages: Uint8Array[]): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPrivateKey {
|
||||||
|
toPublicKey(): IPublicKey;
|
||||||
|
sign(message: Uint8Array): ISignature;
|
||||||
|
toBytes(): Uint8Array;
|
||||||
|
toHex(): string;
|
||||||
|
}
|
|
@ -1,29 +0,0 @@
|
||||||
import {PublicKey} from "./publicKey";
|
|
||||||
import {PrivateKey} from "./privateKey";
|
|
||||||
|
|
||||||
export class Keypair {
|
|
||||||
private readonly _publicKey: PublicKey;
|
|
||||||
|
|
||||||
private readonly _privateKey: PrivateKey;
|
|
||||||
|
|
||||||
public constructor(privateKey: PrivateKey, publicKey?: PublicKey) {
|
|
||||||
this._privateKey = privateKey;
|
|
||||||
if (!publicKey) {
|
|
||||||
this._publicKey = PublicKey.fromPrivateKey(this._privateKey);
|
|
||||||
} else {
|
|
||||||
this._publicKey = publicKey;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public get publicKey(): PublicKey {
|
|
||||||
return this._publicKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get privateKey(): PrivateKey {
|
|
||||||
return this._privateKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static generate(): Keypair {
|
|
||||||
return new Keypair(PrivateKey.random());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,66 +0,0 @@
|
||||||
import {SECRET_KEY_LENGTH} from "./constants";
|
|
||||||
import assert from "assert";
|
|
||||||
import {SecretKeyType} from "@chainsafe/eth2-bls-wasm";
|
|
||||||
import {generateRandomSecretKey} from "@chainsafe/bls-keygen";
|
|
||||||
import {getContext} from "./context";
|
|
||||||
import {PublicKey} from "./publicKey";
|
|
||||||
import {Signature} from "./signature";
|
|
||||||
|
|
||||||
export class PrivateKey {
|
|
||||||
private value: SecretKeyType;
|
|
||||||
|
|
||||||
protected constructor(value: SecretKeyType) {
|
|
||||||
this.value = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static fromBytes(bytes: Uint8Array): PrivateKey {
|
|
||||||
assert(bytes.length === SECRET_KEY_LENGTH, "Private key should have 32 bytes");
|
|
||||||
const context = getContext();
|
|
||||||
const secretKey = new context.SecretKey();
|
|
||||||
secretKey.deserialize(Buffer.from(bytes));
|
|
||||||
return new PrivateKey(secretKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static fromHexString(value: string): PrivateKey {
|
|
||||||
value = value.replace("0x", "");
|
|
||||||
assert(value.length === SECRET_KEY_LENGTH * 2, "secret key must have 32 bytes");
|
|
||||||
const context = getContext();
|
|
||||||
return new PrivateKey(context.deserializeHexStrToSecretKey(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static fromInt(num: number): PrivateKey {
|
|
||||||
const context = getContext();
|
|
||||||
const secretKey = new context.SecretKey();
|
|
||||||
secretKey.setInt(num);
|
|
||||||
return new PrivateKey(secretKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static random(): PrivateKey {
|
|
||||||
const randomKey: Buffer = generateRandomSecretKey();
|
|
||||||
return this.fromBytes(randomKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
public getValue(): SecretKeyType {
|
|
||||||
return this.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// public sign(message: Uint8Array): Signature {
|
|
||||||
// return Signature.fromValue(this.value.sign(message));
|
|
||||||
// }
|
|
||||||
|
|
||||||
public signMessage(message: Uint8Array): Signature {
|
|
||||||
return Signature.fromValue(this.value.sign(message));
|
|
||||||
}
|
|
||||||
|
|
||||||
public toPublicKey(): PublicKey {
|
|
||||||
return PublicKey.fromPublicKeyType(this.value.getPublicKey());
|
|
||||||
}
|
|
||||||
|
|
||||||
public toBytes(): Buffer {
|
|
||||||
return Buffer.from(this.value.serialize());
|
|
||||||
}
|
|
||||||
|
|
||||||
public toHexString(): string {
|
|
||||||
return `0x${this.value.serializeToHexStr()}`;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,61 +0,0 @@
|
||||||
import {PrivateKey} from "./privateKey";
|
|
||||||
import {PublicKeyType} from "@chainsafe/eth2-bls-wasm";
|
|
||||||
import {getContext} from "./context";
|
|
||||||
import {PUBLIC_KEY_LENGTH} from "./constants";
|
|
||||||
import assert from "assert";
|
|
||||||
import {Signature} from "./signature";
|
|
||||||
import {EMPTY_PUBLIC_KEY} from "./helpers/utils";
|
|
||||||
|
|
||||||
export class PublicKey {
|
|
||||||
private value: PublicKeyType;
|
|
||||||
|
|
||||||
protected constructor(value: PublicKeyType) {
|
|
||||||
this.value = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static fromPrivateKey(privateKey: PrivateKey): PublicKey {
|
|
||||||
return privateKey.toPublicKey();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static fromBytes(bytes: Uint8Array): PublicKey {
|
|
||||||
const context = getContext();
|
|
||||||
const publicKey = new context.PublicKey();
|
|
||||||
if (!EMPTY_PUBLIC_KEY.equals(bytes)) {
|
|
||||||
publicKey.deserialize(bytes);
|
|
||||||
}
|
|
||||||
return new PublicKey(publicKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static fromHex(value: string): PublicKey {
|
|
||||||
value = value.replace("0x", "");
|
|
||||||
assert(value.length === PUBLIC_KEY_LENGTH * 2);
|
|
||||||
const context = getContext();
|
|
||||||
return new PublicKey(context.deserializeHexStrToPublicKey(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static fromPublicKeyType(value: PublicKeyType): PublicKey {
|
|
||||||
return new PublicKey(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public add(other: PublicKey): PublicKey {
|
|
||||||
const agg = new PublicKey(this.value.clone());
|
|
||||||
agg.value.add(other.value);
|
|
||||||
return agg;
|
|
||||||
}
|
|
||||||
|
|
||||||
public verifyMessage(signature: Signature, messageHash: Uint8Array): boolean {
|
|
||||||
return this.value.verify(signature.getValue(), messageHash);
|
|
||||||
}
|
|
||||||
|
|
||||||
public toBytesCompressed(): Buffer {
|
|
||||||
return Buffer.from(this.value.serialize());
|
|
||||||
}
|
|
||||||
|
|
||||||
public toHexString(): string {
|
|
||||||
return `0x${this.toBytesCompressed().toString("hex")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getValue(): PublicKeyType {
|
|
||||||
return this.value;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,72 +0,0 @@
|
||||||
import assert from "assert";
|
|
||||||
import {FP_POINT_LENGTH} from "./constants";
|
|
||||||
import {SignatureType} from "@chainsafe/eth2-bls-wasm";
|
|
||||||
import {getContext} from "./context";
|
|
||||||
import {PublicKey} from "./publicKey";
|
|
||||||
import {EMPTY_SIGNATURE} from "./helpers/utils";
|
|
||||||
|
|
||||||
export class Signature {
|
|
||||||
private value: SignatureType;
|
|
||||||
|
|
||||||
protected constructor(value: SignatureType) {
|
|
||||||
this.value = value;
|
|
||||||
assert(this.value.isValidOrder());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static fromCompressedBytes(value: Uint8Array): Signature {
|
|
||||||
assert(value.length === 2 * FP_POINT_LENGTH, `Signature must have ${2 * FP_POINT_LENGTH} bytes`);
|
|
||||||
const context = getContext();
|
|
||||||
const signature = new context.Signature();
|
|
||||||
if (!EMPTY_SIGNATURE.equals(value)) {
|
|
||||||
signature.deserialize(value);
|
|
||||||
}
|
|
||||||
return new Signature(signature);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static fromValue(signature: SignatureType): Signature {
|
|
||||||
return new Signature(signature);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static aggregate(signatures: Signature[]): Signature {
|
|
||||||
const context = getContext();
|
|
||||||
const signature = new context.Signature();
|
|
||||||
signature.aggregate(signatures.map((sig) => sig.getValue()));
|
|
||||||
return new Signature(signature);
|
|
||||||
}
|
|
||||||
|
|
||||||
public add(other: Signature): Signature {
|
|
||||||
const agg = this.value.clone();
|
|
||||||
agg.add(other.value);
|
|
||||||
return new Signature(agg);
|
|
||||||
}
|
|
||||||
|
|
||||||
public getValue(): SignatureType {
|
|
||||||
return this.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public verifyAggregate(publicKeys: PublicKey[], message: Uint8Array): boolean {
|
|
||||||
return this.value.fastAggregateVerify(
|
|
||||||
publicKeys.map((key) => key.getValue()),
|
|
||||||
message
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public verifyMultiple(publicKeys: PublicKey[], messages: Uint8Array[], fast = false): boolean {
|
|
||||||
const msgs = Buffer.concat(messages);
|
|
||||||
if (!fast && !getContext().areAllMsgDifferent(msgs)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return this.value.aggregateVerifyNoCheck(
|
|
||||||
publicKeys.map((key) => key.getValue()),
|
|
||||||
msgs
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public toBytesCompressed(): Buffer {
|
|
||||||
return Buffer.from(this.value.serialize());
|
|
||||||
}
|
|
||||||
|
|
||||||
public toHex(): string {
|
|
||||||
return "0x" + this.value.serializeToHexStr();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,102 @@
|
||||||
|
import {runBenchmark} from "./runner";
|
||||||
|
import {runForAllImplementations} from "../switch";
|
||||||
|
import {IPublicKey, ISignature} from "../../src/interface";
|
||||||
|
import {randomBytes} from "../../src/helpers";
|
||||||
|
|
||||||
|
runForAllImplementations(async (bls, implementation) => {
|
||||||
|
await bls.init();
|
||||||
|
|
||||||
|
const aggCount = 30;
|
||||||
|
|
||||||
|
// verify
|
||||||
|
|
||||||
|
runBenchmark<{pk: IPublicKey; msg: Uint8Array; sig: ISignature}, boolean>({
|
||||||
|
id: `${implementation} verify`,
|
||||||
|
|
||||||
|
prepareTest: () => {
|
||||||
|
const msg = randomMsg();
|
||||||
|
const sk = bls.PrivateKey.fromKeygen();
|
||||||
|
const pk = sk.toPublicKey();
|
||||||
|
const sig = sk.sign(msg);
|
||||||
|
return {
|
||||||
|
input: {pk, msg, sig},
|
||||||
|
resultCheck: (valid) => valid === true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
testRunner: ({pk, msg, sig}) => {
|
||||||
|
return sig.verify(pk, msg);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fast aggregate
|
||||||
|
|
||||||
|
runBenchmark<{pks: IPublicKey[]; msg: Uint8Array; sig: ISignature}, boolean>({
|
||||||
|
id: `${implementation} verifyAggregate`,
|
||||||
|
|
||||||
|
prepareTest: () => {
|
||||||
|
const msg = randomMsg();
|
||||||
|
const dataArr = range(aggCount).map(() => {
|
||||||
|
const sk = bls.PrivateKey.fromKeygen();
|
||||||
|
const pk = sk.toPublicKey();
|
||||||
|
const sig = sk.sign(msg);
|
||||||
|
return {pk, sig};
|
||||||
|
});
|
||||||
|
|
||||||
|
const pks = dataArr.map((data) => data.pk);
|
||||||
|
const sig = bls.Signature.aggregate(dataArr.map((data) => data.sig));
|
||||||
|
|
||||||
|
return {
|
||||||
|
input: {pks, msg, sig},
|
||||||
|
resultCheck: (valid) => valid === true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
testRunner: ({pks, msg, sig}) => {
|
||||||
|
return sig.verifyAggregate(pks, msg);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Aggregate pubkeys
|
||||||
|
|
||||||
|
runBenchmark<IPublicKey[], void>({
|
||||||
|
id: `${implementation} aggregate pubkeys (${aggCount})`,
|
||||||
|
|
||||||
|
prepareTest: () => {
|
||||||
|
return {
|
||||||
|
input: range(aggCount).map(() => bls.PrivateKey.fromKeygen().toPublicKey()),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
testRunner: (pks) => {
|
||||||
|
bls.PublicKey.aggregate(pks);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Aggregate sigs
|
||||||
|
|
||||||
|
runBenchmark<ISignature[], void>({
|
||||||
|
id: `${implementation} aggregate signatures (${aggCount})`,
|
||||||
|
|
||||||
|
prepareTest: () => {
|
||||||
|
const msg = randomMsg();
|
||||||
|
const sigs = range(aggCount).map(() => {
|
||||||
|
const sk = bls.PrivateKey.fromKeygen();
|
||||||
|
return sk.sign(msg);
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
input: sigs,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
testRunner: (sigs) => {
|
||||||
|
bls.Signature.aggregate(sigs);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function range(n: number): number[] {
|
||||||
|
const nums: number[] = [];
|
||||||
|
for (let i = 0; i < n; i++) nums.push(i);
|
||||||
|
return nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomMsg(): Uint8Array {
|
||||||
|
return randomBytes(32);
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
export function runBenchmark<T, R>({
|
||||||
|
prepareTest,
|
||||||
|
testRunner,
|
||||||
|
runs = 100,
|
||||||
|
id,
|
||||||
|
}: {
|
||||||
|
prepareTest: (i: number) => {input: T; resultCheck?: (result: R) => boolean};
|
||||||
|
testRunner: (input: T) => R;
|
||||||
|
runs?: number;
|
||||||
|
id: string;
|
||||||
|
}): void {
|
||||||
|
const diffsNanoSec: bigint[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < runs; i++) {
|
||||||
|
const {input, resultCheck} = prepareTest(i);
|
||||||
|
|
||||||
|
const start = process.hrtime.bigint();
|
||||||
|
const result = testRunner(input);
|
||||||
|
const end = process.hrtime.bigint();
|
||||||
|
|
||||||
|
if (resultCheck && !resultCheck(result)) throw Error("Result fails check test");
|
||||||
|
diffsNanoSec.push(end - start);
|
||||||
|
}
|
||||||
|
|
||||||
|
const average = averageBigint(diffsNanoSec);
|
||||||
|
const opsPerSec = 1e9 / Number(average);
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(`${id}: ${opsPerSec.toPrecision(5)} ops/sec (${runs} runs)`); // ±1.74%
|
||||||
|
}
|
||||||
|
|
||||||
|
function averageBigint(arr: bigint[]): bigint {
|
||||||
|
const total = arr.reduce((total, value) => total + value);
|
||||||
|
return total / BigInt(arr.length);
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
import path from "path";
|
||||||
|
export const SPEC_TESTS_DIR = path.join(__dirname, "../node_modules/@chainsafe/eth2-spec-tests/tests");
|
|
@ -1,6 +1,8 @@
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import bls, {initBLS} from "../../src";
|
|
||||||
import {describeDirectorySpecTest, InputType} from "@chainsafe/lodestar-spec-test-util";
|
import {describeDirectorySpecTest, InputType} from "@chainsafe/lodestar-spec-test-util";
|
||||||
|
import {bytesToHex, hexToBytes} from "../../src/helpers";
|
||||||
|
import {SPEC_TESTS_DIR} from "../params";
|
||||||
|
import {describeForAllImplementations} from "../switch";
|
||||||
|
|
||||||
interface IAggregateSigsTestCase {
|
interface IAggregateSigsTestCase {
|
||||||
data: {
|
data: {
|
||||||
|
@ -9,32 +11,23 @@ interface IAggregateSigsTestCase {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
before(async function f() {
|
describeForAllImplementations((bls) => {
|
||||||
await initBLS();
|
|
||||||
});
|
|
||||||
|
|
||||||
describeDirectorySpecTest<IAggregateSigsTestCase, string>(
|
describeDirectorySpecTest<IAggregateSigsTestCase, string>(
|
||||||
"BLS - aggregate sigs",
|
"bls/aggregate/small",
|
||||||
path.join(__dirname, "../../node_modules/@chainsafe/eth2-spec-tests/tests/general/phase0/bls/aggregate/small"),
|
path.join(SPEC_TESTS_DIR, "general/phase0/bls/aggregate/small"),
|
||||||
(testCase) => {
|
(testCase) => {
|
||||||
try {
|
try {
|
||||||
const result = bls.aggregateSignatures(
|
const signatures = testCase.data.input;
|
||||||
testCase.data.input.map((pubKey) => {
|
const agg = bls.aggregateSignatures(signatures.map(hexToBytes));
|
||||||
return Buffer.from(pubKey.replace("0x", ""), "hex");
|
return bytesToHex(agg);
|
||||||
})
|
|
||||||
);
|
|
||||||
return `0x${result.toString("hex")}`;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.message === "signatures is null or undefined or empty array") {
|
if (e.message === "EMPTY_AGGREGATE_ARRAY") return null;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
inputTypes: {
|
inputTypes: {data: InputType.YAML},
|
||||||
data: InputType.YAML,
|
|
||||||
},
|
|
||||||
getExpected: (testCase) => testCase.data.output,
|
getExpected: (testCase) => testCase.data.output,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import bls, {initBLS} from "../../src";
|
|
||||||
import {describeDirectorySpecTest, InputType} from "@chainsafe/lodestar-spec-test-util";
|
import {describeDirectorySpecTest, InputType} from "@chainsafe/lodestar-spec-test-util";
|
||||||
|
import {hexToBytes} from "../../src/helpers";
|
||||||
|
import {SPEC_TESTS_DIR} from "../params";
|
||||||
|
import {describeForAllImplementations} from "../switch";
|
||||||
|
|
||||||
interface IAggregateSigsVerifyTestCase {
|
interface IAggregateSigsVerifyTestCase {
|
||||||
data: {
|
data: {
|
||||||
|
@ -13,30 +15,17 @@ interface IAggregateSigsVerifyTestCase {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
before(async function f() {
|
describeForAllImplementations((bls) => {
|
||||||
try {
|
|
||||||
await initBLS();
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
describeDirectorySpecTest<IAggregateSigsVerifyTestCase, boolean>(
|
describeDirectorySpecTest<IAggregateSigsVerifyTestCase, boolean>(
|
||||||
"BLS - aggregate sigs verify",
|
"bls/aggregate_verify/small",
|
||||||
path.join(__dirname, "../../node_modules/@chainsafe/eth2-spec-tests/tests/general/phase0/bls/aggregate_verify/small"),
|
path.join(SPEC_TESTS_DIR, "general/phase0/bls/aggregate_verify/small"),
|
||||||
(testCase) => {
|
(testCase) => {
|
||||||
const pubkeys = testCase.data.input.pubkeys.map((pubkey) => {
|
const {pubkeys, messages, signature} = testCase.data.input;
|
||||||
return Buffer.from(pubkey.replace("0x", ""), "hex");
|
return bls.verifyMultiple(pubkeys.map(hexToBytes), messages.map(hexToBytes), hexToBytes(signature));
|
||||||
});
|
|
||||||
const messages = testCase.data.input.messages.map((msg) => {
|
|
||||||
return Buffer.from(msg.replace("0x", ""), "hex");
|
|
||||||
});
|
|
||||||
return bls.verifyMultiple(pubkeys, messages, Buffer.from(testCase.data.input.signature.replace("0x", ""), "hex"));
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
inputTypes: {
|
inputTypes: {data: InputType.YAML},
|
||||||
data: InputType.YAML,
|
|
||||||
},
|
|
||||||
getExpected: (testCase) => testCase.data.output,
|
getExpected: (testCase) => testCase.data.output,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import bls, {initBLS} from "../../src";
|
|
||||||
import {describeDirectorySpecTest, InputType} from "@chainsafe/lodestar-spec-test-util";
|
import {describeDirectorySpecTest, InputType} from "@chainsafe/lodestar-spec-test-util";
|
||||||
|
import {hexToBytes} from "../../src/helpers";
|
||||||
|
import {SPEC_TESTS_DIR} from "../params";
|
||||||
|
import {describeForAllImplementations} from "../switch";
|
||||||
|
|
||||||
interface IAggregateSigsVerifyTestCase {
|
interface IAggregateSigsVerifyTestCase {
|
||||||
data: {
|
data: {
|
||||||
|
@ -13,31 +15,17 @@ interface IAggregateSigsVerifyTestCase {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
before(async function f() {
|
describeForAllImplementations((bls) => {
|
||||||
try {
|
|
||||||
await initBLS();
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
describeDirectorySpecTest<IAggregateSigsVerifyTestCase, boolean>(
|
describeDirectorySpecTest<IAggregateSigsVerifyTestCase, boolean>(
|
||||||
"BLS - aggregate sigs verify",
|
"bls/fast_aggregate_verify/small",
|
||||||
path.join(
|
path.join(SPEC_TESTS_DIR, "general/phase0/bls/fast_aggregate_verify/small"),
|
||||||
__dirname,
|
|
||||||
"../../node_modules/@chainsafe/eth2-spec-tests/tests/general/phase0/bls/fast_aggregate_verify/small"
|
|
||||||
),
|
|
||||||
(testCase) => {
|
(testCase) => {
|
||||||
return bls.verifyAggregate(
|
const {pubkeys, message, signature} = testCase.data.input;
|
||||||
testCase.data.input.pubkeys.map((key) => Buffer.from(key.replace("0x", ""), "hex")),
|
return bls.verifyAggregate(pubkeys.map(hexToBytes), hexToBytes(message), hexToBytes(signature));
|
||||||
Buffer.from(testCase.data.input.message.replace("0x", ""), "hex"),
|
|
||||||
Buffer.from(testCase.data.input.signature.replace("0x", ""), "hex")
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
inputTypes: {
|
inputTypes: {data: InputType.YAML},
|
||||||
data: InputType.YAML,
|
|
||||||
},
|
|
||||||
getExpected: (testCase) => testCase.data.output,
|
getExpected: (testCase) => testCase.data.output,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import bls, {initBLS} from "../../src";
|
|
||||||
import {describeDirectorySpecTest, InputType} from "@chainsafe/lodestar-spec-test-util";
|
import {describeDirectorySpecTest, InputType} from "@chainsafe/lodestar-spec-test-util";
|
||||||
|
import {bytesToHex, hexToBytes} from "../../src/helpers";
|
||||||
|
import {SPEC_TESTS_DIR} from "../params";
|
||||||
|
import {describeForAllImplementations} from "../switch";
|
||||||
|
|
||||||
interface ISignMessageTestCase {
|
interface ISignMessageTestCase {
|
||||||
data: {
|
data: {
|
||||||
|
@ -12,24 +14,18 @@ interface ISignMessageTestCase {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
before(async function f() {
|
describeForAllImplementations((bls) => {
|
||||||
await initBLS();
|
|
||||||
});
|
|
||||||
|
|
||||||
describeDirectorySpecTest<ISignMessageTestCase, string>(
|
describeDirectorySpecTest<ISignMessageTestCase, string>(
|
||||||
"BLS - sign",
|
"bls/sign/small",
|
||||||
path.join(__dirname, "../../node_modules/@chainsafe/eth2-spec-tests/tests/general/phase0/bls/sign/small"),
|
path.join(SPEC_TESTS_DIR, "general/phase0/bls/sign/small"),
|
||||||
(testCase) => {
|
(testCase) => {
|
||||||
const signature = bls.sign(
|
const {privkey, message} = testCase.data.input;
|
||||||
Buffer.from(testCase.data.input.privkey.replace("0x", ""), "hex"),
|
const signature = bls.sign(hexToBytes(privkey), hexToBytes(message));
|
||||||
Buffer.from(testCase.data.input.message.replace("0x", ""), "hex")
|
return bytesToHex(signature);
|
||||||
);
|
|
||||||
return `0x${signature.toString("hex")}`;
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
inputTypes: {
|
inputTypes: {data: InputType.YAML},
|
||||||
data: InputType.YAML,
|
|
||||||
},
|
|
||||||
getExpected: (testCase) => testCase.data.output,
|
getExpected: (testCase) => testCase.data.output,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import bls, {initBLS} from "../../src";
|
|
||||||
import {describeDirectorySpecTest, InputType} from "@chainsafe/lodestar-spec-test-util";
|
import {describeDirectorySpecTest, InputType} from "@chainsafe/lodestar-spec-test-util";
|
||||||
|
import {hexToBytes} from "../../src/helpers";
|
||||||
|
import {SPEC_TESTS_DIR} from "../params";
|
||||||
|
import {describeForAllImplementations} from "../switch";
|
||||||
|
|
||||||
interface IVerifyTestCase {
|
interface IVerifyTestCase {
|
||||||
data: {
|
data: {
|
||||||
|
@ -13,24 +15,17 @@ interface IVerifyTestCase {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
before(async function f() {
|
describeForAllImplementations((bls) => {
|
||||||
await initBLS();
|
|
||||||
});
|
|
||||||
|
|
||||||
describeDirectorySpecTest<IVerifyTestCase, boolean>(
|
describeDirectorySpecTest<IVerifyTestCase, boolean>(
|
||||||
"BLS - verify",
|
"bls/verify/small",
|
||||||
path.join(__dirname, "../../node_modules/@chainsafe/eth2-spec-tests/tests/general/phase0/bls/verify/small"),
|
path.join(SPEC_TESTS_DIR, "general/phase0/bls/verify/small"),
|
||||||
(testCase) => {
|
(testCase) => {
|
||||||
return bls.verify(
|
const {pubkey, message, signature} = testCase.data.input;
|
||||||
Buffer.from(testCase.data.input.pubkey.replace("0x", ""), "hex"),
|
return bls.verify(hexToBytes(pubkey), hexToBytes(message), hexToBytes(signature));
|
||||||
Buffer.from(testCase.data.input.message.replace("0x", ""), "hex"),
|
|
||||||
Buffer.from(testCase.data.input.signature.replace("0x", ""), "hex")
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
inputTypes: {
|
inputTypes: {data: InputType.YAML},
|
||||||
data: InputType.YAML,
|
|
||||||
},
|
|
||||||
getExpected: (testCase) => testCase.data.output,
|
getExpected: (testCase) => testCase.data.output,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
import blst from "../src/blst";
|
||||||
|
import herumi from "../src/herumi";
|
||||||
|
import {IBls} from "../src/interface";
|
||||||
|
|
||||||
|
export type Implementation = "blst" | "herumi";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||||
|
export function getBls(implementation: Implementation): IBls {
|
||||||
|
switch (implementation) {
|
||||||
|
case "blst":
|
||||||
|
return blst;
|
||||||
|
case "herumi":
|
||||||
|
return herumi;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runForAllImplementations(
|
||||||
|
callback: (bls: IBls, implementation: Implementation) => Promise<void> | void
|
||||||
|
): Promise<void> {
|
||||||
|
for (const implementation of ["blst", "herumi"] as Implementation[]) {
|
||||||
|
const bls = getBls(implementation);
|
||||||
|
await callback(bls, implementation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function describeForAllImplementations(callback: (bls: IBls) => void): void {
|
||||||
|
runForAllImplementations((bls, implementation) => {
|
||||||
|
describe(implementation, () => {
|
||||||
|
before(async () => {
|
||||||
|
await bls.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
callback(bls);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,19 +0,0 @@
|
||||||
import {init, getContext, destroy} from "../../src/context";
|
|
||||||
import {expect} from "chai";
|
|
||||||
|
|
||||||
describe("bls wasm constext", function () {
|
|
||||||
afterEach(() => {
|
|
||||||
destroy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("initializes and works", async function () {
|
|
||||||
await init();
|
|
||||||
expect(getContext().getCurveOrder()).to.be.equal(
|
|
||||||
"52435875175126190479447740508185965837690552500527637822603658699938581184513"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("throws if context not initialized", async function () {
|
|
||||||
expect(() => getContext().getCurveOrder()).to.throw();
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
import {expect} from "chai";
|
||||||
|
import {hexToBytes, bytesToHex} from "../../../src/helpers/hex";
|
||||||
|
|
||||||
|
describe("helpers / hex", () => {
|
||||||
|
const testCases: {id: string; hex: string}[] = [
|
||||||
|
{
|
||||||
|
id: "pubkey",
|
||||||
|
hex: "0xb6f21199594b56d77670564bf422cb331d5281ca2c1f9a45588a56881d8287ef8619efa6456d6cd2ef61306aa5b21311",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const {id, hex} of testCases) {
|
||||||
|
it(`${id} hexToBytes`, () => {
|
||||||
|
const expectedBytes = hexToBytesNode(hex);
|
||||||
|
const bytes = hexToBytes(hex);
|
||||||
|
expect(expectedBytes.equals(bytes)).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`${id} bytesToHex`, () => {
|
||||||
|
const bytes = hexToBytesNode(hex);
|
||||||
|
const _hex = bytesToHex(bytes);
|
||||||
|
expect(_hex).to.equal(hex);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function hexToBytesNode(hex: string): Buffer {
|
||||||
|
return Buffer.from(hex.replace("0x", ""), "hex");
|
||||||
|
}
|
|
@ -1,226 +1,93 @@
|
||||||
import bls, {aggregatePubkeys, aggregateSignatures, initBLS, Keypair, verify, verifyMultiple} from "../../src";
|
|
||||||
import SHA256 from "@chainsafe/as-sha256";
|
|
||||||
import {expect} from "chai";
|
import {expect} from "chai";
|
||||||
import {destroy} from "../../src/context";
|
import {IBls} from "../../src/interface";
|
||||||
|
import {getN, randomMessage} from "../util";
|
||||||
|
|
||||||
describe("test bls", function () {
|
export function runIndexTests(bls: IBls): void {
|
||||||
before(async function () {
|
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||||
await initBLS();
|
function getRandomData() {
|
||||||
});
|
const sk = bls.PrivateKey.fromKeygen();
|
||||||
|
const pk = sk.toPublicKey();
|
||||||
|
const msg = randomMessage();
|
||||||
|
const sig = sk.sign(msg);
|
||||||
|
return {sk, pk, msg, sig};
|
||||||
|
}
|
||||||
|
|
||||||
after(function () {
|
describe("verify", () => {
|
||||||
destroy();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("aggregate pubkey", function () {
|
|
||||||
it("should aggregate empty array", function () {
|
|
||||||
expect(bls.aggregatePubkeys([])).to.not.throw;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("verify", function () {
|
|
||||||
it("should verify signature", () => {
|
it("should verify signature", () => {
|
||||||
const keypair = Keypair.generate();
|
const {pk, msg, sig} = getRandomData();
|
||||||
const messageHash = Buffer.from(SHA256.digest(Buffer.from("Test")));
|
const pkHex = pk.toHex();
|
||||||
const signature = keypair.privateKey.signMessage(messageHash);
|
const isValid = bls.verify(pk.toBytes(), msg, sig.toBytes());
|
||||||
const result = verify(keypair.publicKey.toBytesCompressed(), messageHash, signature.toBytesCompressed());
|
expect(isValid, "fail verify").to.be.true;
|
||||||
expect(result).to.be.true;
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not modify original pubkey when verifying", () => {
|
// Make sure to not modify original pubkey when verifying
|
||||||
const keypair = Keypair.generate();
|
expect(pk.toHex()).to.be.equal(pkHex, "pubkey modified when verifying");
|
||||||
const messageHash = Buffer.from(SHA256.digest(Buffer.from("Test")));
|
|
||||||
const signature = keypair.privateKey.signMessage(messageHash);
|
|
||||||
const pubKey = keypair.publicKey.toBytesCompressed();
|
|
||||||
verify(pubKey, messageHash, signature.toBytesCompressed());
|
|
||||||
expect("0x" + pubKey.toString("hex")).to.be.equal(keypair.publicKey.toHexString());
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should fail verify empty signature", () => {
|
it("should fail verify empty signature", () => {
|
||||||
const keypair = Keypair.generate();
|
const {pk, msg} = getRandomData();
|
||||||
const messageHash2 = Buffer.from(SHA256.digest(Buffer.from("Test message2")));
|
const emptySig = Buffer.alloc(96);
|
||||||
const signature = Buffer.alloc(96);
|
const isValid = bls.verify(pk.toBytes(), msg, emptySig);
|
||||||
const result = verify(keypair.publicKey.toBytesCompressed(), messageHash2, signature);
|
expect(isValid).to.be.false;
|
||||||
expect(result).to.be.false;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should fail verify signature of different message", () => {
|
it("should fail verify signature of different message", () => {
|
||||||
const keypair = Keypair.generate();
|
const {pk, sig} = getRandomData();
|
||||||
const messageHash = Buffer.from(SHA256.digest(Buffer.from("Test message")));
|
const msg2 = randomMessage();
|
||||||
const messageHash2 = Buffer.from(SHA256.digest(Buffer.from("Test message2")));
|
const isValid = bls.verify(pk.toBytes(), msg2, sig.toBytes());
|
||||||
const signature = keypair.privateKey.signMessage(messageHash);
|
expect(isValid).to.be.false;
|
||||||
const result = verify(keypair.publicKey.toBytesCompressed(), messageHash2, signature.toBytesCompressed());
|
|
||||||
expect(result).to.be.false;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should fail verify signature signed by different key", () => {
|
it("should fail verify signature signed by different key", () => {
|
||||||
const keypair = Keypair.generate();
|
const {msg, sig} = getRandomData();
|
||||||
const keypair2 = Keypair.generate();
|
const {pk: pk2} = getRandomData();
|
||||||
const messageHash = Buffer.from(SHA256.digest(Buffer.from("Test message")));
|
const isValid = bls.verify(pk2.toBytes(), msg, sig.toBytes());
|
||||||
const signature = keypair.privateKey.signMessage(messageHash);
|
expect(isValid).to.be.false;
|
||||||
const result = verify(keypair2.publicKey.toBytesCompressed(), messageHash, signature.toBytesCompressed());
|
|
||||||
expect(result).to.be.false;
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("verify multiple", function () {
|
describe("verify multiple", () => {
|
||||||
it("should verify aggregated signatures", function () {
|
it("should verify aggregated signatures", () => {
|
||||||
this.timeout(5000);
|
const sks = getN(4, () => bls.PrivateKey.fromKeygen());
|
||||||
|
const msgs = getN(2, () => randomMessage());
|
||||||
|
const pks = sks.map((sk) => sk.toPublicKey());
|
||||||
|
|
||||||
const keypair1 = Keypair.generate();
|
const sigs = [sks[0].sign(msgs[0]), sks[1].sign(msgs[0]), sks[2].sign(msgs[1]), sks[3].sign(msgs[1])];
|
||||||
const keypair2 = Keypair.generate();
|
|
||||||
const keypair3 = Keypair.generate();
|
|
||||||
const keypair4 = Keypair.generate();
|
|
||||||
|
|
||||||
const message1 = Buffer.from(SHA256.digest(Buffer.from("Test1")));
|
const aggPubkeys = [
|
||||||
const message2 = Buffer.from(SHA256.digest(Buffer.from("Test2")));
|
bls.aggregatePubkeys([pks[0], pks[1]].map((pk) => pk.toBytes())),
|
||||||
|
bls.aggregatePubkeys([pks[2], pks[3]].map((pk) => pk.toBytes())),
|
||||||
|
];
|
||||||
|
|
||||||
const signature1 = keypair1.privateKey.signMessage(message1);
|
const aggSig = bls.aggregateSignatures(sigs.map((sig) => sig.toBytes()));
|
||||||
const signature2 = keypair2.privateKey.signMessage(message1);
|
|
||||||
const signature3 = keypair3.privateKey.signMessage(message2);
|
|
||||||
const signature4 = keypair4.privateKey.signMessage(message2);
|
|
||||||
|
|
||||||
const aggregatePubKey12 = aggregatePubkeys([
|
expect(bls.verifyMultiple(aggPubkeys, msgs, aggSig), "should be valid").to.be.true;
|
||||||
keypair1.publicKey.toBytesCompressed(),
|
expect(bls.verifyMultiple(aggPubkeys.reverse(), msgs, aggSig), "should fail - swaped pubkeys").to.be.false;
|
||||||
keypair2.publicKey.toBytesCompressed(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const aggregatePubKey34 = aggregatePubkeys([
|
|
||||||
keypair3.publicKey.toBytesCompressed(),
|
|
||||||
keypair4.publicKey.toBytesCompressed(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const aggregateSignature = aggregateSignatures([
|
|
||||||
signature1.toBytesCompressed(),
|
|
||||||
signature2.toBytesCompressed(),
|
|
||||||
signature3.toBytesCompressed(),
|
|
||||||
signature4.toBytesCompressed(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const result = verifyMultiple([aggregatePubKey12, aggregatePubKey34], [message1, message2], aggregateSignature);
|
|
||||||
|
|
||||||
expect(result).to.be.true;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should verify aggregated signatures - same message", function () {
|
it("should verify aggregated signatures - same message", () => {
|
||||||
this.timeout(5000);
|
const n = 4;
|
||||||
|
const msg = randomMessage();
|
||||||
|
const sks = getN(n, () => bls.PrivateKey.fromKeygen());
|
||||||
|
const pks = sks.map((sk) => sk.toPublicKey());
|
||||||
|
const sigs = sks.map((sk) => sk.sign(msg));
|
||||||
|
|
||||||
const keypair1 = Keypair.generate();
|
const aggregateSignature = bls.aggregateSignatures(sigs.map((sig) => sig.toBytes()));
|
||||||
const keypair2 = Keypair.generate();
|
|
||||||
const keypair3 = Keypair.generate();
|
|
||||||
const keypair4 = Keypair.generate();
|
|
||||||
|
|
||||||
const message = Buffer.from(SHA256.digest(Buffer.from("Test1")));
|
const isValid = bls.verifyMultiple(
|
||||||
|
pks.map((pk) => pk.toBytes()),
|
||||||
const signature1 = keypair1.privateKey.signMessage(message);
|
getN(4, () => msg), // Same message n times
|
||||||
const signature2 = keypair2.privateKey.signMessage(message);
|
|
||||||
const signature3 = keypair3.privateKey.signMessage(message);
|
|
||||||
const signature4 = keypair4.privateKey.signMessage(message);
|
|
||||||
|
|
||||||
const aggregateSignature = aggregateSignatures([
|
|
||||||
signature1.toBytesCompressed(),
|
|
||||||
signature2.toBytesCompressed(),
|
|
||||||
signature3.toBytesCompressed(),
|
|
||||||
signature4.toBytesCompressed(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const result = verifyMultiple(
|
|
||||||
[
|
|
||||||
keypair1.publicKey.toBytesCompressed(),
|
|
||||||
keypair2.publicKey.toBytesCompressed(),
|
|
||||||
keypair3.publicKey.toBytesCompressed(),
|
|
||||||
keypair4.publicKey.toBytesCompressed(),
|
|
||||||
],
|
|
||||||
[message, message, message, message],
|
|
||||||
aggregateSignature,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result).to.be.true;
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should fail to verify aggregated signatures - swapped messages", function () {
|
|
||||||
this.timeout(5000);
|
|
||||||
|
|
||||||
const keypair1 = Keypair.generate();
|
|
||||||
const keypair2 = Keypair.generate();
|
|
||||||
const keypair3 = Keypair.generate();
|
|
||||||
const keypair4 = Keypair.generate();
|
|
||||||
|
|
||||||
const message1 = Buffer.from(SHA256.digest(Buffer.from("Test1")));
|
|
||||||
const message2 = Buffer.from(SHA256.digest(Buffer.from("Test2")));
|
|
||||||
|
|
||||||
const signature1 = keypair1.privateKey.signMessage(message1);
|
|
||||||
const signature2 = keypair2.privateKey.signMessage(message1);
|
|
||||||
const signature3 = keypair3.privateKey.signMessage(message2);
|
|
||||||
const signature4 = keypair4.privateKey.signMessage(message2);
|
|
||||||
|
|
||||||
const aggregatePubKey12 = bls.aggregatePubkeys([
|
|
||||||
keypair1.publicKey.toBytesCompressed(),
|
|
||||||
keypair2.publicKey.toBytesCompressed(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const aggregatePubKey34 = bls.aggregatePubkeys([
|
|
||||||
keypair3.publicKey.toBytesCompressed(),
|
|
||||||
keypair4.publicKey.toBytesCompressed(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const aggregateSignature = bls.aggregateSignatures([
|
|
||||||
signature1.toBytesCompressed(),
|
|
||||||
signature2.toBytesCompressed(),
|
|
||||||
signature3.toBytesCompressed(),
|
|
||||||
signature4.toBytesCompressed(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const result = bls.verifyMultiple(
|
|
||||||
[aggregatePubKey12, aggregatePubKey34],
|
|
||||||
[message2, message1],
|
|
||||||
aggregateSignature
|
aggregateSignature
|
||||||
);
|
);
|
||||||
|
expect(isValid).to.be.true;
|
||||||
expect(result).to.be.false;
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should fail to verify aggregated signatures - different pubkeys and messsages", () => {
|
|
||||||
const keypair1 = Keypair.generate();
|
|
||||||
const keypair2 = Keypair.generate();
|
|
||||||
const keypair3 = Keypair.generate();
|
|
||||||
const keypair4 = Keypair.generate();
|
|
||||||
|
|
||||||
const message1 = Buffer.from(SHA256.digest(Buffer.from("Test1")));
|
|
||||||
const message2 = Buffer.from(SHA256.digest(Buffer.from("Test2")));
|
|
||||||
|
|
||||||
const signature1 = keypair1.privateKey.signMessage(message1);
|
|
||||||
const signature2 = keypair2.privateKey.signMessage(message1);
|
|
||||||
const signature3 = keypair3.privateKey.signMessage(message2);
|
|
||||||
const signature4 = keypair4.privateKey.signMessage(message2);
|
|
||||||
|
|
||||||
const aggregatePubKey12 = bls.aggregatePubkeys([
|
|
||||||
keypair1.publicKey.toBytesCompressed(),
|
|
||||||
keypair2.publicKey.toBytesCompressed(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const aggregateSignature = bls.aggregateSignatures([
|
|
||||||
signature1.toBytesCompressed(),
|
|
||||||
signature2.toBytesCompressed(),
|
|
||||||
signature3.toBytesCompressed(),
|
|
||||||
signature4.toBytesCompressed(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const result = bls.verifyMultiple([aggregatePubKey12], [message2, message1], aggregateSignature);
|
|
||||||
|
|
||||||
expect(result).to.be.false;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should fail to verify aggregated signatures - no public keys", () => {
|
it("should fail to verify aggregated signatures - no public keys", () => {
|
||||||
const signature = Buffer.alloc(96);
|
const sig = Buffer.alloc(96);
|
||||||
|
const msg1 = randomMessage();
|
||||||
|
const msg2 = randomMessage();
|
||||||
|
|
||||||
const message1 = Buffer.from(SHA256.digest(Buffer.from("Test1")));
|
const isValid = bls.verifyMultiple([], [msg2, msg1], sig);
|
||||||
const message2 = Buffer.from(SHA256.digest(Buffer.from("Test2")));
|
expect(isValid).to.be.false;
|
||||||
|
|
||||||
const result = bls.verifyMultiple([], [message2, message1], signature);
|
|
||||||
|
|
||||||
expect(result).to.be.false;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
import {PrivateKey, PublicKey, Keypair} from "../../src";
|
|
||||||
import {expect} from "chai";
|
|
||||||
import {destroy, init} from "../../src/context";
|
|
||||||
|
|
||||||
describe("keypair", function () {
|
|
||||||
before(async function () {
|
|
||||||
await init();
|
|
||||||
});
|
|
||||||
|
|
||||||
after(function () {
|
|
||||||
destroy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should create from private and public key", () => {
|
|
||||||
const secret = PrivateKey.random();
|
|
||||||
const secret2 = PrivateKey.random();
|
|
||||||
const publicKey = PublicKey.fromBytes(PublicKey.fromPrivateKey(secret2).toBytesCompressed());
|
|
||||||
const keypair = new Keypair(secret, publicKey);
|
|
||||||
expect(keypair.publicKey).to.be.equal(publicKey);
|
|
||||||
expect(keypair.privateKey).to.be.equal(secret);
|
|
||||||
expect(keypair.privateKey).to.not.be.equal(secret2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should create from private", () => {
|
|
||||||
const secret = PrivateKey.random();
|
|
||||||
const publicKey = PublicKey.fromPrivateKey(secret);
|
|
||||||
const keypair = new Keypair(secret);
|
|
||||||
expect(keypair.publicKey.toBytesCompressed().toString("hex")).to.be.equal(
|
|
||||||
publicKey.toBytesCompressed().toString("hex")
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,38 +1,26 @@
|
||||||
import {PrivateKey} from "../../src";
|
|
||||||
import {expect} from "chai";
|
import {expect} from "chai";
|
||||||
import {SECRET_KEY_LENGTH} from "../../src/constants";
|
import {IBls} from "../../src/interface";
|
||||||
import {destroy, init} from "../../src/context";
|
|
||||||
|
|
||||||
describe("privateKey", function () {
|
export function runPrivateKeyTests(bls: IBls): void {
|
||||||
before(async function () {
|
describe("PrivateKey", () => {
|
||||||
await init();
|
it("should generate fromKeygen private key", () => {
|
||||||
|
const privateKey1 = bls.PrivateKey.fromKeygen();
|
||||||
|
const privateKey2 = bls.PrivateKey.fromKeygen();
|
||||||
|
expect(privateKey1.toHex()).to.not.be.equal(privateKey2.toHex());
|
||||||
});
|
});
|
||||||
|
|
||||||
after(function () {
|
|
||||||
destroy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should generate random private key", function () {
|
|
||||||
const privateKey1 = PrivateKey.random();
|
|
||||||
const privateKey2 = PrivateKey.random();
|
|
||||||
expect(privateKey1.toHexString()).to.not.be.equal(privateKey2.toHexString());
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should export private key to hex string", function () {
|
|
||||||
const privateKey = "0x07656fd676da43883d163f49566c72b9cbf0a5a294f26808c807700732456da7";
|
const privateKey = "0x07656fd676da43883d163f49566c72b9cbf0a5a294f26808c807700732456da7";
|
||||||
|
|
||||||
expect(PrivateKey.fromHexString(privateKey).toHexString()).to.be.equal(privateKey);
|
it("should export private key to hex string", () => {
|
||||||
|
expect(bls.PrivateKey.fromHex(privateKey).toHex()).to.be.equal(privateKey);
|
||||||
const privateKey2 = "07656fd676da43883d163f49566c72b9cbf0a5a294f26808c807700732456da7";
|
|
||||||
|
|
||||||
expect(PrivateKey.fromHexString(privateKey2).toHexString()).to.be.equal(privateKey);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should export private key to bytes", function () {
|
it("should export private key to hex string from non-prefixed hex", () => {
|
||||||
expect(PrivateKey.random().toBytes().length).to.be.equal(SECRET_KEY_LENGTH);
|
expect(bls.PrivateKey.fromHex(privateKey).toHex()).to.be.equal(privateKey);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not accept too short private key", function () {
|
it("should not accept too short private key", () => {
|
||||||
expect(() => PrivateKey.fromHexString("0x2123")).to.throw();
|
expect(() => bls.PrivateKey.fromHex("0x2123")).to.throw();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -1,29 +1,21 @@
|
||||||
import {destroy, init} from "../../src/context";
|
|
||||||
import {PublicKey, PrivateKey} from "../../src";
|
|
||||||
import {expect} from "chai";
|
import {expect} from "chai";
|
||||||
|
import {IBls} from "../../src/interface";
|
||||||
|
|
||||||
describe("public key", function () {
|
export function runPublicKeyTests(bls: IBls): void {
|
||||||
before(async function f() {
|
describe("PublicKey", () => {
|
||||||
await init();
|
|
||||||
});
|
|
||||||
|
|
||||||
after(function () {
|
|
||||||
destroy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("from hex", function () {
|
|
||||||
const publicKey =
|
const publicKey =
|
||||||
"0xb6f21199594b56d77670564bf422cb331d5281ca2c1f9a45588a56881d8287ef8619efa6456d6cd2ef61306aa5b21311";
|
"0xb6f21199594b56d77670564bf422cb331d5281ca2c1f9a45588a56881d8287ef8619efa6456d6cd2ef61306aa5b21311";
|
||||||
expect(PublicKey.fromHex(publicKey).toHexString()).to.be.equal(publicKey);
|
|
||||||
|
it("should export public key to hex string", () => {
|
||||||
|
expect(bls.PublicKey.fromHex(publicKey).toHex()).to.be.equal(publicKey);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("from bytes", function () {
|
it("should export public key to hex string from non-prefixed hex", () => {
|
||||||
const publicKey =
|
expect(bls.PublicKey.fromHex(publicKey).toHex()).to.be.equal(publicKey);
|
||||||
"b6f21199594b56d77670564bf422cb331d5281ca2c1f9a45588a56881d8287ef8619efa6456d6cd2ef61306aa5b21311";
|
|
||||||
expect(PublicKey.fromBytes(Buffer.from(publicKey, "hex")).toHexString()).to.be.equal(`0x${publicKey}`);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("from private key", function () {
|
it("from private key", () => {
|
||||||
PublicKey.fromPrivateKey(PrivateKey.random());
|
bls.PrivateKey.fromKeygen().toPublicKey();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
import {runPrivateKeyTests} from "./privateKey.test";
|
||||||
|
import {runPublicKeyTests} from "./publicKey.test";
|
||||||
|
// import {runKeypairTests} from "./keypair.test";
|
||||||
|
import {runIndexTests} from "./index.test";
|
||||||
|
import {describeForAllImplementations} from "../switch";
|
||||||
|
|
||||||
|
// Import test's bls lib lazily to prevent breaking test with Karma
|
||||||
|
describeForAllImplementations((bls) => {
|
||||||
|
runPrivateKeyTests(bls);
|
||||||
|
runPublicKeyTests(bls);
|
||||||
|
// runKeypairTests(bls);
|
||||||
|
runIndexTests(bls);
|
||||||
|
});
|
|
@ -0,0 +1,21 @@
|
||||||
|
import herumi from "../../src/herumi";
|
||||||
|
import {runPrivateKeyTests} from "./privateKey.test";
|
||||||
|
import {runPublicKeyTests} from "./publicKey.test";
|
||||||
|
// import {runKeypairTests} from "./keypair.test";
|
||||||
|
import {runIndexTests} from "./index.test";
|
||||||
|
|
||||||
|
// This file is intended to be compiled and run by Karma
|
||||||
|
// Do not import the node.bindings or it will break with:
|
||||||
|
// Error: BLST bindings loader should only run in a NodeJS context: process.platform
|
||||||
|
describe("herumi", () => {
|
||||||
|
before(async () => {
|
||||||
|
// For consistency with describeForAllImplementations
|
||||||
|
// eslint-disable-next-line import/no-named-as-default-member
|
||||||
|
await herumi.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
runPrivateKeyTests(herumi);
|
||||||
|
runPublicKeyTests(herumi);
|
||||||
|
// runKeypairTests(bls);
|
||||||
|
runIndexTests(herumi);
|
||||||
|
});
|
|
@ -0,0 +1,9 @@
|
||||||
|
import {randomBytes} from "../src/helpers";
|
||||||
|
|
||||||
|
export function randomMessage(): Uint8Array {
|
||||||
|
return randomBytes(32);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getN<T>(n: number, getter: () => T): T[] {
|
||||||
|
return Array.from({length: n}, () => getter());
|
||||||
|
}
|
|
@ -4,9 +4,8 @@
|
||||||
"target": "esnext",
|
"target": "esnext",
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"pretty": true,
|
"pretty": true,
|
||||||
"lib": [
|
"lib": ["esnext.bigint"],
|
||||||
"esnext.bigint"
|
"typeRoots": ["./node_modules/@types"],
|
||||||
],
|
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"strictNullChecks": false,
|
"strictNullChecks": false,
|
||||||
|
|
Reference in New Issue