Compare commits

..

2 Commits

Author SHA1 Message Date
dapplion f544c289da Remove browser specific code 2020-12-06 11:35:17 +00:00
dapplion de53915150 Bump bls-eth-wasm 2020-12-06 11:28:50 +00:00
119 changed files with 5066 additions and 4370 deletions

15
.babel-register Normal file
View File

@ -0,0 +1,15 @@
/*
See
https://github.com/babel/babel/issues/8652
https://github.com/babel/babel/pull/6027
Babel isn't currently configured by default to read .ts files and
can only be configured to do so via cli or configuration below.
This file is used by mocha to interpret test files using a properly
configured babel.
This can (probably) be removed in babel 8.x.
*/
require('@babel/register')({
extensions: ['.ts'],
})

22
.babelrc Normal file
View File

@ -0,0 +1,22 @@
{
"presets": [
[
"@babel/env",
{
"targets": {
"node": "10.4",
"browsers": ">1%, not ie 11"
},
"exclude": [
"transform-regenerator"
]
}
],
"@babel/typescript"
],
"plugins": [
"@babel/proposal-class-properties",
"@babel/proposal-object-rest-spread",
"@babel/plugin-syntax-bigint"
]
}

View File

@ -13,15 +13,13 @@ module.exports = {
},
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaVersion: "latest",
project: "./tsconfig.json",
sourceType: "module",
ecmaVersion: 10,
project: "./tsconfig.json"
},
plugins: [
"@typescript-eslint",
"eslint-plugin-import",
"prettier",
"@chainsafe/eslint-plugin-node"
"prettier"
],
extends: [
"eslint:recommended",
@ -35,21 +33,20 @@ module.exports = {
"prettier/prettier": "error",
//doesnt work, it reports false errors
"constructor-super": "off",
//"@typescript-eslint/class-name-casing": "error",
"@typescript-eslint/class-name-casing": "error",
"@typescript-eslint/explicit-function-return-type": ["error", {
"allowExpressions": true
}],
"@typescript-eslint/func-call-spacing": "error",
"@typescript-eslint/indent": ["error", 2],
//"@typescript-eslint/interface-name-prefix": ["error", "always"],
"@typescript-eslint/interface-name-prefix": ["error", "always"],
"@typescript-eslint/member-ordering": "error",
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-require-imports": "error",
"@typescript-eslint/no-unused-vars": ["error", {
"varsIgnorePattern": "^_",
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_"
}],
"@typescript-eslint/ban-ts-comment": "warn",
"@typescript-eslint/ban-ts-ignore": "warn",
"@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/semi": "error",
"@typescript-eslint/type-annotation-spacing": "error",
@ -78,15 +75,7 @@ module.exports = {
"no-prototype-builtins": 0,
"prefer-const": "error",
"quotes": ["error", "double"],
"semi": "off",
"@chainsafe/node/file-extension-in-import": [
"error",
"always",
{
"esm": true
}
],
"import/no-unresolved": "off",
"semi": "off"
},
"overrides": [
{

11
.github/CODEOWNERS vendored
View File

@ -1,11 +0,0 @@
# These owners will be the default owners for everything in
# the repo. Unless a later match takes precedence,
# They will be requested for
# review when someone opens a pull request.
* @ChainSafe/lodestar
# Order is important; the last matching pattern takes the most
# precedence. When someone opens a pull request that only
# modifies md files, only md owners and not the global
# owner(s) will be requested for a review.
*.md @ChainSafe/lodestar

2
.github/FUNDING.yml vendored
View File

@ -1 +1 @@
custom: https://gitcoin.co/grants/6034/lodestar-typescript-ethereum-consensus-client
custom: https://gitcoin.co/grants/79/lodestar-eth20-client

View File

@ -1,31 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
---
<!--NOTE: -->
<!--- General questions should go to the discord chat instead of the issue tracker.-->
**Describe the bug**
<!--A clear and concise description of what the bug is and steps to reproduce it.-->
**Expected behavior**
<!--A clear and concise description of what you expected to happen.-->
**Steps to Reproduce**
<!--Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
-->
**Screenshots**
<!--If applicable, add screenshots to help explain your problem.-->
**Desktop (please complete the following information):**
- OS: <!--[e.g. ubuntu, OSX High Siera]-->
- Version: <!--[e.g. 22]-->
- Branch: <!--[Master]-->
- Commit hash: <!--[e8232]-->

View File

@ -1,21 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
---
<!--NOTE:
- General questions should go to the discord chat instead of the issue tracker.
-->
**Is your feature request related to a problem? Please describe.**
<!--A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]-->
**Describe the solution you'd like**
<!--A clear and concise description of what you want to happen.-->
**Describe alternatives you've considered**
<!--A clear and concise description of any alternative solutions or features you've considered.-->
**Additional context**
<!--Add any other context or screenshots about the feature request here.-->

View File

@ -1,13 +0,0 @@
---
name: Architecture/Planning Question
about: Suggest an idea for this project
---
<!--
NOTE:
- General questions should go to the discord chat instead of the issue tracker.
-->
**What is your question?**
<!--A clear and concise description of what the problem is.-->

69
.github/stale.yml vendored
View File

@ -1,69 +0,0 @@
# Configuration for probot-stale - https://github.com/probot/stale
# Number of days of inactivity before an Issue or Pull Request becomes stale
daysUntilStale: 60
# Number of days of inactivity before an Issue or Pull Request with the stale label is closed.
# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
daysUntilClose: 15
# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled)
onlyLabels: []
# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
exemptLabels:
- "Epic"
- "meta-good-first-issue"
- "meta-help-wanted"
- "meta-discussion"
- "meta-pm"
- "prio-critical"
- "prio-high"
- "prio-medium"
- "prio-low"
- "status-blocked"
- "status-do-not-merge"
# Set to true to ignore issues in a project (defaults to false)
exemptProjects: false
# Set to true to ignore issues in a milestone (defaults to false)
exemptMilestones: true
# Set to true to ignore issues with an assignee (defaults to false)
exemptAssignees: true
# Label to use when marking as stale
staleLabel: meta-stale
# Comment to post when marking as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed in 15 days if no further activity occurs. Thank you
for your contributions.
# Comment to post when removing the stale label.
# unmarkComment: >
# Your comment here
# Comment to post when closing a stale Issue or Pull Request.
closeComment: >
This issue or pull request has been automatically been closed due to inactivity.
# Limit the number of actions per hour, from 1-30. Default is 30
limitPerRun: 30
# Limit to only `issues` or `pulls`
# only: issues
# Optionally, specify configuration settings that are specific to just 'issues' or 'pulls':
# pulls:
# daysUntilStale: 30
# markComment: >
# This pull request has been automatically marked as stale because it has not had
# recent activity. It will be closed if no further activity occurs. Thank you
# for your contributions.
# issues:
# exemptLabels:
# - confirmed

View File

@ -34,7 +34,7 @@ jobs:
- name: Setup Nodejs
uses: actions/setup-node@v1
with:
node-version: "14.x"
node-version: "12.x"
registry-url: "https://registry.npmjs.org"
- name: Install dependencies
run: yarn install --non-interactive --frozen-lockfile
@ -62,4 +62,4 @@ jobs:
tag: ${{ needs.tag.outputs.tag }}
delete_orphan_tag: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -6,17 +6,16 @@ jobs:
tests:
name: Tests
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node: [14, 16]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: ${{matrix.node}}
node-version: "12.x"
registry-url: "https://registry.npmjs.org"
- name: Install deps
run: yarn --non-interactive --frozen-lockfile
run: yarn
- name: Check types
run: yarn check-types
- name: Lint
run: yarn lint
- name: Test build

View File

@ -1,6 +0,0 @@
extension: ["ts"]
colors: true
node-option:
- "experimental-specifier-resolution=node"
- "loader=ts-node/esm"

View File

@ -5,56 +5,7 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [7.1.1] - 2022-05-15
### Chores
- bump blst peer dependency [#130](https://github.com/ChainSafe/bls/pull/130)
## [7.1.0] - 2022-05-09
### Features
- add errors and constants to exports [#128](https://github.com/ChainSafe/bls/pull/128)
## [7.0.0] - 2022-05-05
### BREAKING CHANGES
- ESM Support [#121](https://github.com/ChainSafe/bls/pull/121)
## [6.0.3] - 2021-09-25
- bls-eth-wasm (herumi) package update to 0.4.8 for invalidating signature not in G2 [#106](https://github.com/ChainSafe/bls/pull/106)
- Signature.fromBytes now has default verification on [#106](https://github.com/ChainSafe/bls/pull/106)
## [6.0.2] - 2021-08-23
- Add register script [#102](https://github.com/ChainSafe/bls/pull/102)
## [6.0.1] - 2021-04-05
- Add validate key option to PublicKey.fromBytes() [#90](https://github.com/ChainSafe/bls/pull/90)
## [6.0.0] - 2021-04-05
- Allow to export points compressed and uncompressed [#85](https://github.com/ChainSafe/bls/pull/85)
- For BLST impl allow to choose what point coordinates to deserialize to [#85](https://github.com/ChainSafe/bls/pull/85)
- Update function signature of `verifyMultipleSignatures()` to use grouped signature sets [#85](https://github.com/ChainSafe/bls/pull/85)
- Bump BLST implementation to fix multi-thread issues [#85](https://github.com/ChainSafe/bls/pull/85)
## [5.1.1] - 2020-12-18
- Enable worker-threads support for blst [#76](https://github.com/ChainSafe/bls/pull/76)
## [5.1.0] - 2020-11-30
- Bump @chainsafe/lodestar-spec-test-util [#56](https://github.com/ChainSafe/bls/pull/56)
- Add benchmark results [#57](https://github.com/ChainSafe/bls/pull/57)
- Add verifyMultipleSignatures method [#58](https://github.com/ChainSafe/bls/pull/58)
- Set strictNullChecks to true [#67](https://github.com/ChainSafe/bls/pull/67)
- Simplify build setup with tsc [#68](https://github.com/ChainSafe/bls/pull/68)
## [dev]
## [5.0.1] - 2020-11-30

View File

@ -1,9 +1,10 @@
# bls
[![Build Status](https://travis-ci.org/ChainSafe/lodestar.svg?branch=master)](https://travis-ci.org/ChainSafe/lodestar)
[![codecov](https://codecov.io/gh/ChainSafe/lodestar/branch/master/graph/badge.svg)](https://codecov.io/gh/ChainSafe/lodestar)
![ETH2.0_Spec_Version 1.0.0](https://img.shields.io/badge/ETH2.0_Spec_Version-1.0.0-2e86c1.svg)
![ES Version](https://img.shields.io/badge/ES-2022-yellow)
![Node Version](https://img.shields.io/badge/node-14.8-green)
![ES Version](https://img.shields.io/badge/ES-2017-yellow)
![Node Version](https://img.shields.io/badge/node-12.x-green)
Javascript library for BLS (Boneh-Lynn-Shacham) signatures and signature aggregation, tailored for use in Eth2.
@ -19,94 +20,91 @@ To use native bindings you must install peer dependency `@chainsafe/blst`
yarn add @chainsafe/bls @chainsafe/blst
```
By default, native bindings will be used if in NodeJS and they are installed. A WASM implementation ("herumi") is used as a fallback in case any error occurs.
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 from "@chainsafe/bls";
import {init, SecretKey, secretKeyToPublicKey, sign, verify} from "@chainsafe/bls";
(async () => {
// class-based interface
const secretKey = bls.SecretKey.fromKeygen();
const publicKey = secretKey.toPublicKey();
const message = new Uint8Array(32);
await init("herumi");
const signature = secretKey.sign(message);
console.log("Is valid: ", signature.verify(publicKey, message));
// class-based interface
const secretKey = SecretKey.fromKeygen();
const publicKey = secretKey.toPublicKey();
const message = new Uint8Array(32);
// functional interface
const sk = secretKey.toBytes();
const pk = bls.secretKeyToPublicKey(sk);
const sig = bls.sign(sk, message);
console.log("Is valid: ", bls.verify(pk, message, sig));
const signature = secretKey.sign(message);
console.log("Is valid: ", signature.verify(publicKey, message));
// functional interface
const sk = secretKey.toBytes();
const pk = secretKeyToPublicKey(sk);
const sig = sign(sk, message);
console.log("Is valid: ", verify(pk, message, sig));
})();
```
### Browser
If you are in the browser, import from `/herumi` to explicitly import the WASM version
If you are in the browser, import from `/browser` to import directly the WASM version
```ts
import bls from "@chainsafe/bls/herumi";
import bls from "@chainsafe/bls/browser";
```
### Native bindings only
If you are in NodeJS, import from `/blst-native` to explicitly import the native bindings. Also install peer dependency `@chainsafe/blst` which has the native bindings
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/blst-native";
import bls from "@chainsafe/bls/node";
```
### Get implementation at runtime
### Native bindings + WASM fallback
If you need to get a bls implementation at runtime, import from `/getImplementation`.
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
```ts
import {getImplementation} from "@chainsafe/bls/getImplementation";
const bls = await getImplementation("herumi");
```bash
yarn add @chainsafe/bls @chainsafe/blst
```
### Switchable singleton
If you need a singleton that is switchable at runtime (the default behavior in <=v6), import from `/switchable`.
```ts
import bls, {init} from "@chainsafe/bls/switchable";
import {init} from "@chainsafe/bls";
// here `bls` is uninitialized
await init("herumi");
// here `bls` is initialized
// now other modules can `import bls from "@chainsafe/bls/switchable"` and it will be initialized
try {
await init("blst-native");
} catch (e) {
await init("herumi");
console.warn("Using WASM");
}
```
The API is identical for all implementations.
## Benchmarks
- `blst`: [src/blst-native](src/blst-native) (node.js-only, bindings to C via node-gyp)
- `herumi`: [src/herumi](src/herumi) (node.js & browser, wasm)
- `blst`: [src/blst](src/blst) (node.js-only, bindings to C via node-gyp)
- `herumi`: [src/herumi](src/herumi) (node.js & browser, wasm)
- `noble`: [noble-bls12-381](https://github.com/paulmillr/noble-bls12-381) (node.js & browser, pure JS)
Results are in `ops/sec (x times slower)`, where `x times slower` = times slower than fastest implementation (`blst`).
| Function - `ops/sec` | `blst` | `herumi` | `noble` |
| -------------------------------- | :----: | :----------: | :-----------: |
| `verify` | 326.38 | 47.674 (x7) | 17.906 (x18) |
| `verifyAggregate` (30) | 453.29 | 51.151 (x9) | 18.372 (x25) |
| `verifyMultiple` (30) | 34.497 | 3.5233 (x10) | 2.0286 (x17) |
| `verifyMultipleSignatures` (30) | 26.381 | 3.1633 (x8) | - |
| `aggregate` (pubkeys, 30) | 15686 | 2898.9 (x5) | 1875.0 (x8) |
| `aggregate` (sigs, 30) | 6373.4 | 1033.0 (x6) | 526.25 (x12) |
| `sign` | 925.49 | 108.81 (x9) | 10.246 (x90) |
| Function - `ops/sec` | `blst` | `herumi` | `noble`\* |
| ------------------------- | :----: | :----------: | :-----------: |
| `verify` | 443.75 | 46.658 (x9) | 12.355 (x36) |
| `verifyAggregate` (30) | 438.68 | 46.615 (x9) | 9.8803 (x44) |
| `verifyMultiple` (30) | 35.138 | 3.4332 (x10) | 0.9217 (x38) |
| `aggregate` (pubkeys, 30) | 15761 | 2603.5 (x6) | 42.956 (x366) |
| `aggregate` (sigs, 30) | 6587.8 | 1018.7 (x6) | - |
\* `blst` and `herumi` performed 100 runs each, `noble` 10 runs.
\*`noble` methods include serialization and deserialization to bytes, which may impact all benchmarks specially `aggregate`.
\*\* `blst` and `herumi` performed 100 runs each, `noble` 10 runs.
Results from CI run https://github.com/ChainSafe/bls/runs/1513710175?check_suite_focus=true#step:12:13
Results from CI run https://github.com/ChainSafe/bls/runs/1488856560?check_suite_focus=true#step:12:13
## Spec versioning

View File

@ -1,84 +1,170 @@
import {runBenchmark} from "./runner.js";
import {runForAllImplementations} from "../test/switch.js";
import {PublicKey, Signature} from "../src/types.js";
import {aggCount} from "./params.js";
import {runBenchmark} from "./runner";
import {runForAllImplementations} from "../test/switch";
import {PublicKey, Signature, SecretKey} from "../src/interface";
import {range, randomMessage} from "../test/util";
import {aggCount, runs} from "./params";
(async function () {
await runForAllImplementations(async (bls, implementation) => {
const msgSame = Buffer.alloc(32, 255);
const sameMessage: {pk: PublicKey; msg: Uint8Array; sig: Signature}[] = [];
const diffMessage: {pk: PublicKey; msg: Uint8Array; sig: Signature}[] = [];
for (let i = 0; i < aggCount; i++) {
const msg = Buffer.alloc(32, i + 1);
const sk = bls.SecretKey.fromBytes(Buffer.alloc(32, i + 1));
const pk = sk.toPublicKey();
sameMessage.push({pk, msg: msgSame, sig: sk.sign(msgSame)});
diffMessage.push({pk, msg, sig: sk.sign(msg)});
}
const {pk, msg, sig} = diffMessage[0];
const sameMessageSig = bls.Signature.aggregate(sameMessage.map((s) => s.sig));
const diffMessageSig = bls.Signature.aggregate(diffMessage.map((s) => s.sig));
// verify
await runBenchmark({
await runBenchmark<{pk: PublicKey; msg: Uint8Array; sig: Signature}, boolean>({
id: `${implementation} verify`,
prepareTest: () => ({pk, msg, sig}),
testRunner: ({pk, msg, sig}) => sig.verify(pk, msg),
prepareTest: () => {
const msg = randomMessage();
const sk = bls.SecretKey.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);
},
runs,
});
// Fast aggregate
await runBenchmark({
await runBenchmark<{pks: PublicKey[]; msg: Uint8Array; sig: Signature}, boolean>({
id: `${implementation} verifyAggregate (${aggCount})`,
prepareTest: () => ({pks: sameMessage.map((s) => s.pk), msg: msgSame, sig: sameMessageSig}),
testRunner: ({pks, msg, sig}) => sig.verifyAggregate(pks, msg),
prepareTest: () => {
const msg = randomMessage();
const dataArr = range(aggCount).map(() => {
const sk = bls.SecretKey.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);
},
runs,
});
// Verify multiple
await runBenchmark({
await runBenchmark<{pks: PublicKey[]; msgs: Uint8Array[]; sig: Signature}, boolean>({
id: `${implementation} verifyMultiple (${aggCount})`,
prepareTest: () => ({
pks: diffMessage.map((s) => s.pk),
msgs: diffMessage.map((s) => s.msg),
sig: diffMessageSig,
}),
testRunner: ({pks, msgs, sig}) => sig.verifyMultiple(pks, msgs),
prepareTest: () => {
const dataArr = range(aggCount).map(() => {
const sk = bls.SecretKey.fromKeygen();
const pk = sk.toPublicKey();
const msg = randomMessage();
const sig = sk.sign(msg);
return {pk, msg, sig};
});
const pks = dataArr.map((data) => data.pk);
const msgs = dataArr.map((data) => data.msg);
const sig = bls.Signature.aggregate(dataArr.map((data) => data.sig));
return {
input: {pks, msgs, sig},
resultCheck: (valid) => valid === true,
};
},
testRunner: ({pks, msgs, sig}) => {
return sig.verifyMultiple(pks, msgs);
},
runs,
});
// Verify multiple signatures
await runBenchmark({
await runBenchmark<{pks: PublicKey[]; msgs: Uint8Array[]; sigs: Signature[]}, boolean>({
id: `${implementation} verifyMultipleSignatures (${aggCount})`,
prepareTest: () => diffMessage,
testRunner: (sets) =>
bls.Signature.verifyMultipleSignatures(sets.map((s) => ({publicKey: s.pk, message: s.msg, signature: s.sig}))),
prepareTest: () => {
const dataArr = range(aggCount).map(() => {
const sk = bls.SecretKey.fromKeygen();
const pk = sk.toPublicKey();
const msg = randomMessage();
const sig = sk.sign(msg);
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);
return {
input: {pks, msgs, sigs},
resultCheck: (valid) => valid === true,
};
},
testRunner: ({pks, msgs, sigs}) => {
return bls.Signature.verifyMultipleSignatures(pks, msgs, sigs);
},
runs,
});
// Aggregate pubkeys
await runBenchmark({
await runBenchmark<PublicKey[], void>({
id: `${implementation} aggregate pubkeys (${aggCount})`,
prepareTest: () => diffMessage.map((s) => s.pk),
testRunner: (pks) => bls.PublicKey.aggregate(pks),
prepareTest: () => {
return {
input: range(aggCount).map(() => bls.SecretKey.fromKeygen().toPublicKey()),
};
},
testRunner: (pks) => {
bls.PublicKey.aggregate(pks);
},
runs,
});
// Aggregate sigs
await runBenchmark({
await runBenchmark<Signature[], void>({
id: `${implementation} aggregate signatures (${aggCount})`,
prepareTest: () => diffMessage.map((s) => s.sig),
testRunner: (sigs) => bls.Signature.aggregate(sigs),
prepareTest: () => {
const msg = randomMessage();
const sigs = range(aggCount).map(() => {
const sk = bls.SecretKey.fromKeygen();
return sk.sign(msg);
});
return {
input: sigs,
};
},
testRunner: (sigs) => {
bls.Signature.aggregate(sigs);
},
runs,
});
// Sign
await runBenchmark({
await runBenchmark<{sk: SecretKey; msg: Uint8Array}, void>({
id: `${implementation} sign`,
prepareTest: () => ({sk: bls.SecretKey.fromKeygen(), msg: msgSame}),
testRunner: ({sk, msg}) => sk.sign(msg),
prepareTest: () => ({
input: {
sk: bls.SecretKey.fromKeygen(),
msg: randomMessage(),
},
}),
testRunner: ({sk, msg}) => {
sk.sign(msg);
},
runs,
});
});
})();

View File

@ -1,109 +1,124 @@
import {runBenchmark} from "./runner.js";
import {range, randomMessage} from "../test/util.js";
import {runBenchmark} from "./runner";
import {range, randomMessage} from "../test/util";
import {generateRandomSecretKey} from "@chainsafe/bls-keygen";
import * as noble from "noble-bls12-381";
import {aggCount, runsNoble} from "./params.js";
import {aggCount, runsNoble} from "./params";
(async function () {
{
// verify
// verify
const priv = generateRandomSecretKey();
const msg = randomMessage();
const pk = noble.PointG1.fromPrivateKey(priv);
const sig = noble.PointG2.fromSignature(await noble.sign(msg, priv));
await runBenchmark<{pk: Uint8Array; msg: Uint8Array; sig: Uint8Array}, boolean>({
id: `noble verify`,
await runBenchmark<{pk: noble.PointG1; msg: noble.PointG2; sig: noble.PointG2}, boolean>({
id: `noble verify`,
prepareTest: async () => ({pk, msg: await noble.PointG2.hashToCurve(msg), sig}),
testRunner: async ({pk, msg, sig}) => await noble.verify(sig, msg, pk),
runs: runsNoble,
});
}
prepareTest: async () => {
const priv = generateRandomSecretKey();
const msg = randomMessage();
const sig = await noble.sign(msg, priv);
const pk = noble.getPublicKey(priv);
{
// Fast aggregate
const msg = randomMessage();
const dataArr = await Promise.all(
range(aggCount).map(async () => {
const sk = generateRandomSecretKey();
const pk = noble.PointG1.fromPrivateKey(sk);
const sig = noble.PointG2.fromSignature(await noble.sign(msg, sk));
return {pk, sig};
})
);
const pks = dataArr.map((data) => data.pk);
const sig = (noble.aggregateSignatures(dataArr.map((data) => data.sig)) as any) as noble.PointG2;
await runBenchmark({
id: `noble verifyAggregate (${aggCount})`,
prepareTest: async () => ({pks, msg: await noble.PointG2.hashToCurve(msg), sig}),
testRunner: async ({pks, msg, sig}) =>
await noble.verify(sig, msg, (noble.aggregatePublicKeys(pks) as any) as noble.PointG1),
runs: runsNoble,
});
}
{
// Verify multiple
const dataArr = await Promise.all(
range(aggCount).map(async () => {
const sk = generateRandomSecretKey();
const pk = noble.PointG1.fromPrivateKey(sk);
const msg = randomMessage();
const sig = noble.PointG2.fromSignature(await noble.sign(msg, sk));
return {pk, msg: await noble.PointG2.hashToCurve(msg), sig};
})
);
const pks = dataArr.map((data) => data.pk);
const msgs = dataArr.map((data) => data.msg);
const sig = (noble.aggregateSignatures(dataArr.map((data) => data.sig)) as any) as noble.PointG2;
await runBenchmark({
id: `noble verifyMultiple (${aggCount})`,
prepareTest: async () => ({pks, msgs, sig}),
testRunner: async ({pks, msgs, sig}) => await noble.verifyBatch(msgs, pks, sig),
runs: runsNoble,
});
}
{
// Aggregate pubkeys
const pubkeys = range(aggCount).map(() => noble.PointG1.fromPrivateKey(generateRandomSecretKey()));
await runBenchmark({
id: `noble aggregate pubkeys (${aggCount})`,
prepareTest: () => pubkeys,
testRunner: async (pks) => noble.aggregatePublicKeys(pks),
runs: runsNoble,
});
}
const hashes = await Promise.all(
range(aggCount)
.map(() => generateRandomSecretKey())
.map(noble.PointG2.hashToCurve)
);
await runBenchmark({
id: `noble aggregate signatures (${aggCount})`,
prepareTest: () => hashes,
testRunner: async (sigs) => noble.aggregateSignatures(sigs),
return {
input: {pk, msg, sig},
resultCheck: (valid: boolean) => valid === true,
};
},
testRunner: async ({pk, msg, sig}) => {
return await noble.verify(sig, msg, pk);
},
runs: runsNoble,
});
const sk = generateRandomSecretKey();
const msg = await noble.PointG2.hashToCurve(randomMessage());
// Fast aggregate
await runBenchmark({
await runBenchmark<{pks: Uint8Array[]; msg: Uint8Array; sig: Uint8Array}, boolean>({
id: `noble verifyAggregate (${aggCount})`,
prepareTest: async () => {
const msg = randomMessage();
const dataArr = await Promise.all(
range(aggCount).map(async () => {
const sk = generateRandomSecretKey();
const pk = noble.getPublicKey(sk);
const sig = await noble.sign(msg, sk);
return {pk, sig};
})
);
const pks = dataArr.map((data) => data.pk);
const sig = noble.aggregateSignatures(dataArr.map((data) => data.sig));
return {
input: {pks, msg, sig},
resultCheck: (valid: boolean) => valid === true,
};
},
testRunner: async ({pks, msg, sig}) => {
const pk = noble.aggregatePublicKeys(pks);
return await noble.verify(sig, msg, pk);
},
runs: runsNoble,
});
// Verify multiple
await runBenchmark<{pks: Uint8Array[]; msgs: Uint8Array[]; sig: Uint8Array}, boolean>({
id: `noble verifyMultiple (${aggCount})`,
prepareTest: async () => {
const dataArr = await Promise.all(
range(aggCount).map(async () => {
const sk = generateRandomSecretKey();
const pk = noble.getPublicKey(sk);
const msg = randomMessage();
const sig = await noble.sign(msg, sk);
return {pk, msg, sig};
})
);
const pks = dataArr.map((data) => data.pk);
const msgs = dataArr.map((data) => data.msg);
const sig = noble.aggregateSignatures(dataArr.map((data) => data.sig));
return {
input: {pks, msgs, sig},
resultCheck: (valid: boolean) => valid === true,
};
},
testRunner: async ({pks, msgs, sig}) => {
return await noble.verifyBatch(msgs, pks, sig);
},
runs: runsNoble,
});
// Aggregate pubkeys
await runBenchmark<Uint8Array[], void>({
id: `noble aggregate pubkeys (${aggCount})`,
prepareTest: () => {
return {
input: range(aggCount).map(() => noble.getPublicKey(generateRandomSecretKey())),
};
},
testRunner: async (pks) => {
noble.aggregatePublicKeys(pks);
},
runs: runsNoble,
});
// Sign
await runBenchmark<{sk: Uint8Array; msg: Uint8Array}, void>({
id: `noble sign`,
prepareTest: () => ({sk, msg}),
testRunner: async ({sk, msg}) => await noble.sign(msg, sk),
prepareTest: async () => ({
input: {
sk: generateRandomSecretKey(),
msg: randomMessage(),
},
}),
testRunner: async ({sk, msg}) => {
await noble.sign(msg, sk);
},
runs: runsNoble,
});
})();

View File

@ -1,14 +1,13 @@
{
"name": "bls-libs-benchmark",
"version": "1.0.0",
"type": "module",
"exports": "./index.js",
"main": "index.js",
"license": "MIT",
"scripts": {
"benchmark": "ts-node-esm index",
"benchmark:all": "ts-node-esm index && ts-node-esm noble && ts-node-esm verifyMultipleSignaturesSavings"
"benchmark": "ts-node index",
"benchmark:all": "ts-node index && ts-node noble"
},
"dependencies": {
"noble-bls12-381": "^0.7.2"
"noble-bls12-381": "^0.6.1"
}
}

View File

@ -6,7 +6,7 @@ export async function runBenchmark<T, R>({
runs = 100,
id,
}: {
prepareTest: (i: number) => PromiseOptional<T>;
prepareTest: (i: number) => PromiseOptional<{input: T; resultCheck?: (result: R) => boolean}>;
testRunner: (input: T) => PromiseOptional<R>;
runs?: number;
id: string;
@ -14,12 +14,13 @@ export async function runBenchmark<T, R>({
const diffsNanoSec: bigint[] = [];
for (let i = 0; i < runs; i++) {
const input = await prepareTest(i);
const {input, resultCheck} = await prepareTest(i);
const start = process.hrtime.bigint();
const result = await testRunner(input);
const end = process.hrtime.bigint();
if (resultCheck && !resultCheck(result)) throw Error("Result fails check test");
diffsNanoSec.push(end - start);
}

View File

@ -1,34 +0,0 @@
import {runForAllImplementations} from "../test/switch.js";
import {range, randomMessage} from "../test/util.js";
(async function () {
console.log("verifyMultipleSignatures savings");
console.log(["Impl", "# sigs", "ratio multi/single"].join("\t"));
await runForAllImplementations(async (bls, implementation) => {
for (const aggCount of [2, 5, 10, 20, 50, 100]) {
const dataArr = range(aggCount).map(() => {
const sk = bls.SecretKey.fromKeygen();
const pk = sk.toPublicKey();
const msg = randomMessage();
const sig = sk.sign(msg);
return {publicKey: pk, message: msg, signature: sig};
});
const startMulti = process.hrtime.bigint();
bls.Signature.verifyMultipleSignatures(dataArr);
const endMulti = process.hrtime.bigint();
const diffMulti = endMulti - startMulti;
const startSingle = process.hrtime.bigint();
for (const {publicKey, message, signature} of dataArr) {
signature.verify(publicKey, message);
}
const endSingle = process.hrtime.bigint();
const diffSingle = endSingle - startSingle;
const ratio = Number(diffMulti) / Number(diffSingle);
console.log([implementation, aggCount, ratio.toPrecision(2)].join("\t"));
}
});
})();

View File

@ -2,7 +2,7 @@
# yarn lockfile v1
noble-bls12-381@^0.7.2:
version "0.7.2"
resolved "https://registry.yarnpkg.com/noble-bls12-381/-/noble-bls12-381-0.7.2.tgz#9a9384891569ba32785d6e4ff8588b783487eae4"
integrity sha512-Z5isbU6opuWPL3dxsGqO5BdOE8WP1XUM7HFIn/xeE5pATTnml/PEIy4MFQQrktHiitkuJdsCDtzEOnS9eIpC3Q==
noble-bls12-381@^0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/noble-bls12-381/-/noble-bls12-381-0.6.1.tgz#b44bb5443b4b5c409723f19a8288155f0b3ad126"
integrity sha512-Dt0lq24ez75HqOqNIsxxbzfY7YOuwArtE3H6Clp1XbwnY4Ga1OjFbTaXq5aDBE3+ab1wLK11s0b3yR3+RiWWqw==

1
blst-native.d.ts vendored Normal file
View File

@ -0,0 +1 @@
export * from "./lib/blst";

1
blst-native.js Normal file
View File

@ -0,0 +1 @@
module.exports = require("./lib/blst");

1
browser.d.ts vendored Normal file
View File

@ -0,0 +1 @@
export * from "./lib/herumi";

1
browser.js Normal file
View File

@ -0,0 +1 @@
export * from "./lib/herumi";

1
herumi.d.ts vendored Normal file
View File

@ -0,0 +1 @@
export * from "./lib/herumi";

1
herumi.js Normal file
View File

@ -0,0 +1 @@
module.exports = require("./lib/herumi");

View File

@ -1,29 +1,20 @@
// eslint-disable-next-line @typescript-eslint/no-require-imports
const webpackConfig = require("./webpack.config.cjs");
const webpackConfig = require("./webpack.config");
module.exports = function (config) {
config.set({
basePath: "",
frameworks: [
"webpack",
"mocha",
"chai",
],
files: [
"test/unit-web/run-web-implementation.test.ts",
"test/unit/index-named-exports.test.ts",
],
frameworks: ["mocha", "chai"],
files: ["test/unit-web/run-web-implementation.test.ts", "test/unit/index-named-exports.test.ts"],
exclude: [],
preprocessors: {
"test/**/*.ts": ["webpack"],
},
webpack: {
mode: "production",
node: webpackConfig.node,
module: webpackConfig.module,
resolve: webpackConfig.resolve,
experiments: webpackConfig.experiments,
optimization: webpackConfig.optimization,
stats: {warnings:false},
},
reporters: ["spec"],

View File

@ -1,8 +0,0 @@
import { SecretKey } from "./secretKey.js";
import { PublicKey } from "./publicKey.js";
import { Signature } from "./signature.js";
import { IBls } from "../types.js";
export * from "../constants.js";
export { SecretKey, PublicKey, Signature };
export declare const bls: IBls;
export default bls;

View File

@ -1,14 +0,0 @@
import { SecretKey } from "./secretKey.js";
import { PublicKey } from "./publicKey.js";
import { Signature } from "./signature.js";
import { functionalInterfaceFactory } from "../functional.js";
export * from "../constants.js";
export { SecretKey, PublicKey, Signature };
export const bls = {
implementation: "blst-native",
SecretKey,
PublicKey,
Signature,
...functionalInterfaceFactory({ SecretKey, PublicKey, Signature }),
};
export default bls;

View File

@ -1,11 +0,0 @@
import * as blst from "@chainsafe/blst";
import { PointFormat, PublicKey as IPublicKey } from "../types.js";
export declare class PublicKey extends blst.PublicKey implements IPublicKey {
constructor(value: ConstructorParameters<typeof blst.PublicKey>[0]);
/** @param type Defaults to `CoordType.jacobian` */
static fromBytes(bytes: Uint8Array, type?: blst.CoordType, validate?: boolean): PublicKey;
static fromHex(hex: string): PublicKey;
static aggregate(publicKeys: PublicKey[]): PublicKey;
toBytes(format?: PointFormat): Uint8Array;
toHex(format?: PointFormat): string;
}

View File

@ -1,37 +0,0 @@
import * as blst from "@chainsafe/blst";
import { EmptyAggregateError } from "../errors.js";
import { bytesToHex, hexToBytes } from "../helpers/index.js";
import { PointFormat } from "../types.js";
export class PublicKey extends blst.PublicKey {
constructor(value) {
super(value);
}
/** @param type Defaults to `CoordType.jacobian` */
static fromBytes(bytes, type, validate) {
const pk = blst.PublicKey.fromBytes(bytes, type);
if (validate)
pk.keyValidate();
return new PublicKey(pk.value);
}
static fromHex(hex) {
return this.fromBytes(hexToBytes(hex));
}
static aggregate(publicKeys) {
if (publicKeys.length === 0) {
throw new EmptyAggregateError();
}
const pk = blst.aggregatePubkeys(publicKeys);
return new PublicKey(pk.value);
}
toBytes(format) {
if (format === PointFormat.uncompressed) {
return this.value.serialize();
}
else {
return this.value.compress();
}
}
toHex(format) {
return bytesToHex(this.toBytes(format));
}
}

View File

@ -1,15 +0,0 @@
import * as blst from "@chainsafe/blst";
import { SecretKey as ISecretKey } from "../types.js";
import { PublicKey } from "./publicKey.js";
import { Signature } from "./signature.js";
export declare class SecretKey implements ISecretKey {
readonly value: blst.SecretKey;
constructor(value: blst.SecretKey);
static fromBytes(bytes: Uint8Array): SecretKey;
static fromHex(hex: string): SecretKey;
static fromKeygen(entropy?: Uint8Array): SecretKey;
sign(message: Uint8Array): Signature;
toPublicKey(): PublicKey;
toBytes(): Uint8Array;
toHex(): string;
}

View File

@ -1,39 +0,0 @@
import * as blst from "@chainsafe/blst";
import { bytesToHex, hexToBytes, isZeroUint8Array, randomBytes } from "../helpers/index.js";
import { SECRET_KEY_LENGTH } from "../constants.js";
import { PublicKey } from "./publicKey.js";
import { Signature } from "./signature.js";
import { ZeroSecretKeyError } from "../errors.js";
export class SecretKey {
constructor(value) {
this.value = value;
}
static fromBytes(bytes) {
// draft-irtf-cfrg-bls-signature-04 does not allow SK == 0
if (isZeroUint8Array(bytes)) {
throw new ZeroSecretKeyError();
}
const sk = blst.SecretKey.fromBytes(bytes);
return new SecretKey(sk);
}
static fromHex(hex) {
return this.fromBytes(hexToBytes(hex));
}
static fromKeygen(entropy) {
const sk = blst.SecretKey.fromKeygen(entropy || randomBytes(SECRET_KEY_LENGTH));
return new SecretKey(sk);
}
sign(message) {
return new Signature(this.value.sign(message).value);
}
toPublicKey() {
const pk = this.value.toPublicKey();
return new PublicKey(pk.value);
}
toBytes() {
return this.value.toBytes();
}
toHex() {
return bytesToHex(this.toBytes());
}
}

View File

@ -1,21 +0,0 @@
import * as blst from "@chainsafe/blst";
import { PointFormat, Signature as ISignature } from "../types.js";
import { PublicKey } from "./publicKey.js";
export declare class Signature extends blst.Signature implements ISignature {
constructor(value: ConstructorParameters<typeof blst.Signature>[0]);
/** @param type Defaults to `CoordType.affine` */
static fromBytes(bytes: Uint8Array, type?: blst.CoordType, validate?: boolean): Signature;
static fromHex(hex: string): Signature;
static aggregate(signatures: Signature[]): Signature;
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(format?: PointFormat): Uint8Array;
toHex(format?: PointFormat): string;
private aggregateVerify;
}

View File

@ -1,62 +0,0 @@
import * as blst from "@chainsafe/blst";
import { bytesToHex, hexToBytes } from "../helpers/index.js";
import { PointFormat } from "../types.js";
import { EmptyAggregateError, ZeroSignatureError } from "../errors.js";
export class Signature extends blst.Signature {
constructor(value) {
super(value);
}
/** @param type Defaults to `CoordType.affine` */
static fromBytes(bytes, type, validate = true) {
const sig = blst.Signature.fromBytes(bytes, type);
if (validate)
sig.sigValidate();
return new Signature(sig.value);
}
static fromHex(hex) {
return this.fromBytes(hexToBytes(hex));
}
static aggregate(signatures) {
if (signatures.length === 0) {
throw new EmptyAggregateError();
}
const agg = blst.aggregateSignatures(signatures);
return new Signature(agg.value);
}
static verifyMultipleSignatures(sets) {
return blst.verifyMultipleAggregateSignatures(sets.map((s) => ({ msg: s.message, pk: s.publicKey, sig: s.signature })));
}
verify(publicKey, message) {
// Individual infinity signatures are NOT okay. Aggregated signatures MAY be infinity
if (this.value.is_inf()) {
throw new ZeroSignatureError();
}
return blst.verify(message, publicKey, this);
}
verifyAggregate(publicKeys, message) {
return blst.fastAggregateVerify(message, publicKeys, this);
}
verifyMultiple(publicKeys, messages) {
return blst.aggregateVerify(messages, publicKeys, this);
}
toBytes(format) {
if (format === PointFormat.uncompressed) {
return this.value.serialize();
}
else {
return this.value.compress();
}
}
toHex(format) {
return bytesToHex(this.toBytes(format));
}
aggregateVerify(msgs, pks) {
// If this set is simply an infinity signature and infinity publicKey then skip verification.
// This has the effect of always declaring that this sig/publicKey combination is valid.
// for Eth2.0 specs tests
if (this.value.is_inf() && pks.length === 1 && pks[0].value.is_inf()) {
return true;
}
return blst.aggregateVerify(msgs, pks, this);
}
}

5
lib/constants.d.ts vendored
View File

@ -1,5 +0,0 @@
export declare const SECRET_KEY_LENGTH = 32;
export declare const PUBLIC_KEY_LENGTH_COMPRESSED = 48;
export declare const PUBLIC_KEY_LENGTH_UNCOMPRESSED: number;
export declare const SIGNATURE_LENGTH_COMPRESSED = 96;
export declare const SIGNATURE_LENGTH_UNCOMPRESSED: number;

View File

@ -1,5 +0,0 @@
export const SECRET_KEY_LENGTH = 32;
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;

25
lib/errors.d.ts vendored
View File

@ -1,25 +0,0 @@
/**
* This error should not be ignored by the functional interface
* try / catch blocks, to prevent false negatives
*/
export declare class NotInitializedError extends Error {
constructor(implementation: string);
}
export declare class ZeroSecretKeyError extends Error {
constructor();
}
export declare class ZeroPublicKeyError extends Error {
constructor();
}
export declare class ZeroSignatureError extends Error {
constructor();
}
export declare class EmptyAggregateError extends Error {
constructor();
}
export declare class InvalidOrderError extends Error {
constructor();
}
export declare class InvalidLengthError extends Error {
constructor(arg: string, length: number);
}

View File

@ -1,39 +0,0 @@
/**
* This error should not be ignored by the functional interface
* try / catch blocks, to prevent false negatives
*/
export class NotInitializedError extends Error {
constructor(implementation) {
super(`NOT_INITIALIZED: ${implementation}`);
}
}
export class ZeroSecretKeyError extends Error {
constructor() {
super("ZERO_SECRET_KEY");
}
}
export class ZeroPublicKeyError extends Error {
constructor() {
super("ZERO_PUBLIC_KEY");
}
}
export class ZeroSignatureError extends Error {
constructor() {
super("ZERO_SIGNATURE");
}
}
export class EmptyAggregateError extends Error {
constructor() {
super("EMPTY_AGGREGATE_ARRAY");
}
}
export class InvalidOrderError extends Error {
constructor() {
super("INVALID_ORDER");
}
}
export class InvalidLengthError extends Error {
constructor(arg, length) {
super(`INVALID_LENGTH: ${arg} - ${length} bytes`);
}
}

15
lib/functional.d.ts vendored
View File

@ -1,15 +0,0 @@
import { IBls } from "./types.js";
export declare function functionalInterfaceFactory({ SecretKey, PublicKey, Signature, }: Pick<IBls, "SecretKey" | "PublicKey" | "Signature">): {
sign: (secretKey: Uint8Array, message: Uint8Array) => Uint8Array;
aggregateSignatures: (signatures: Uint8Array[]) => Uint8Array;
aggregatePublicKeys: (publicKeys: 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;
verifyMultipleSignatures: (sets: {
publicKey: Uint8Array;
message: Uint8Array;
signature: Uint8Array;
}[]) => boolean;
secretKeyToPublicKey: (secretKey: Uint8Array) => Uint8Array;
};

View File

@ -1,137 +0,0 @@
import { validateBytes } from "./helpers/index.js";
import { NotInitializedError } from "./errors.js";
// Returned type is enforced at each implementation's index
// eslint-disable-next-line max-len
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type,@typescript-eslint/explicit-module-boundary-types
export function functionalInterfaceFactory({ SecretKey, PublicKey, Signature, }) {
/**
* Signs given message using secret key.
* @param secretKey
* @param message
*/
function sign(secretKey, message) {
validateBytes(secretKey, "secretKey");
validateBytes(message, "message");
return SecretKey.fromBytes(secretKey).sign(message).toBytes();
}
/**
* Compines all given signature into one.
* @param signatures
*/
function aggregateSignatures(signatures) {
const agg = Signature.aggregate(signatures.map((p) => Signature.fromBytes(p)));
return agg.toBytes();
}
/**
* Combines all given public keys into single one
* @param publicKeys
*/
function aggregatePublicKeys(publicKeys) {
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, message, signature) {
validateBytes(publicKey, "publicKey");
validateBytes(message, "message");
validateBytes(signature, "signature");
try {
return Signature.fromBytes(signature).verify(PublicKey.fromBytes(publicKey), message);
}
catch (e) {
if (e instanceof NotInitializedError)
throw e;
return false;
}
}
/**
* Verifies if aggregated signature is same message signed with given public keys.
* @param publicKeys
* @param message
* @param signature
*/
function verifyAggregate(publicKeys, message, signature) {
validateBytes(publicKeys, "publicKey");
validateBytes(message, "message");
validateBytes(signature, "signature");
try {
return Signature.fromBytes(signature).verifyAggregate(publicKeys.map((publicKey) => PublicKey.fromBytes(publicKey)), message);
}
catch (e) {
if (e instanceof NotInitializedError)
throw 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, messages, signature) {
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) {
if (e instanceof NotInitializedError)
throw e;
return false;
}
}
/**
* Verifies multiple signatures at once returning true if all valid or false
* if at least one is not. Optimization useful when knowing which signature is
* wrong is not relevant, i.e. verifying an entire Eth2.0 block.
*
* This method provides a safe way to do so by multiplying each signature by
* a random number so an attacker cannot craft a malicious signature that won't
* verify on its own but will if it's added to a specific predictable signature
* https://ethresear.ch/t/fast-verification-of-multiple-bls-signatures/5407
*/
function verifyMultipleSignatures(sets) {
if (!sets)
throw Error("sets is null or undefined");
try {
return Signature.verifyMultipleSignatures(sets.map((s) => ({
publicKey: PublicKey.fromBytes(s.publicKey),
message: s.message,
signature: Signature.fromBytes(s.signature),
})));
}
catch (e) {
if (e instanceof NotInitializedError)
throw e;
return false;
}
}
/**
* Computes a public key from a secret key
*/
function secretKeyToPublicKey(secretKey) {
validateBytes(secretKey, "secretKey");
return SecretKey.fromBytes(secretKey).toPublicKey().toBytes();
}
return {
sign,
aggregateSignatures,
aggregatePublicKeys,
verify,
verifyAggregate,
verifyMultiple,
verifyMultipleSignatures,
secretKeyToPublicKey,
};
}

View File

@ -1,2 +0,0 @@
import type { IBls, Implementation } from "./types.js";
export declare function getImplementation(impl?: Implementation): Promise<IBls>;

View File

@ -1,17 +0,0 @@
// Thanks https://github.com/iliakan/detect-node/blob/master/index.esm.js
const isNode = Object.prototype.toString.call(typeof process !== "undefined" ? process : 0) === "[object process]";
export async function getImplementation(impl = "herumi") {
switch (impl) {
case "herumi": {
return await (await import("./herumi/index.js")).bls();
}
case "blst-native":
// Lazy import native bindings to prevent automatically importing binding.node files
if (!isNode) {
throw Error("blst-native is only supported in NodeJS");
}
return (await import("./blst-native/index.js")).bls;
default:
throw new Error(`Unsupported implementation - ${impl}`);
}
}

10
lib/helpers/hex.d.ts vendored
View File

@ -1,10 +0,0 @@
/**
* Browser compatible fromHex method
* From https://github.com/herumi/bls-eth-wasm/blob/04eedb77aa96e66b4f65a0ab477228adf8090c36/src/bls.js#L62
*/
export declare function hexToBytes(hex: string): Uint8Array;
/**
* Browser compatible toHex method
* From https://github.com/herumi/bls-eth-wasm/blob/04eedb77aa96e66b4f65a0ab477228adf8090c36/src/bls.js#L50
*/
export declare function bytesToHex(bytes: Uint8Array): string;

View File

@ -1,30 +0,0 @@
/**
* Browser compatible fromHex method
* From https://github.com/herumi/bls-eth-wasm/blob/04eedb77aa96e66b4f65a0ab477228adf8090c36/src/bls.js#L62
*/
export function hexToBytes(hex) {
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) {
let s = "";
const n = bytes.length;
for (let i = 0; i < n; i++) {
s += ("0" + bytes[i].toString(16)).slice(-2);
}
return "0x" + s;
}

View File

@ -1,2 +0,0 @@
export * from "./hex.js";
export * from "./utils.js";

View File

@ -1,2 +0,0 @@
export * from "./hex.js";
export * from "./utils.js";

View File

@ -1,8 +0,0 @@
import randomBytes from "randombytes";
export { randomBytes };
/**
* Validate bytes to prevent confusing WASM errors downstream if bytes is null
*/
export declare function validateBytes(bytes: Uint8Array | Uint8Array[] | null, argName?: string): asserts bytes is NonNullable<typeof bytes>;
export declare function isZeroUint8Array(bytes: Uint8Array): boolean;
export declare function concatUint8Arrays(bytesArr: Uint8Array[]): Uint8Array;

View File

@ -1,26 +0,0 @@
import randomBytes from "randombytes";
// Single import to ease changing this lib if necessary
export { randomBytes };
/**
* Validate bytes to prevent confusing WASM errors downstream if bytes is null
*/
export function validateBytes(bytes, argName) {
for (const item of Array.isArray(bytes) ? bytes : [bytes]) {
if (item == null) {
throw Error(`${argName || "bytes"} is null or undefined`);
}
}
}
export function isZeroUint8Array(bytes) {
return bytes.every((byte) => byte === 0);
}
export function concatUint8Arrays(bytesArr) {
const totalLen = bytesArr.reduce((total, bytes) => total + bytes.length, 0);
const merged = new Uint8Array(totalLen);
let mergedLen = 0;
for (const bytes of bytesArr) {
merged.set(bytes, mergedLen);
mergedLen += bytes.length;
}
return merged;
}

View File

@ -1,12 +0,0 @@
import bls from "bls-eth-wasm";
declare type Bls = typeof bls;
declare global {
interface Window {
msCrypto: typeof window["crypto"];
}
}
export declare function setupBls(): Promise<void>;
export declare function init(): Promise<void>;
export declare function destroy(): void;
export declare function getContext(): Bls;
export {};

View File

@ -1,35 +0,0 @@
/* eslint-disable require-atomic-updates */
import bls from "bls-eth-wasm";
import { NotInitializedError } from "../errors.js";
let blsGlobal = null;
let blsGlobalPromise = null;
export async function setupBls() {
if (!blsGlobal) {
await bls.init(bls.BLS12_381);
// Patch to fix multiVerify() calls on a browser with polyfilled NodeJS crypto
if (typeof window === "object") {
const crypto = window.crypto || window.msCrypto;
// getRandomValues is not typed in `bls-eth-wasm` because it's not meant to be exposed
// @ts-ignore
bls.getRandomValues = (x) => crypto.getRandomValues(x);
}
blsGlobal = bls;
}
}
// Cache a promise for Bls instead of Bls to make sure it is initialized only once
export async function init() {
if (!blsGlobalPromise) {
blsGlobalPromise = setupBls();
}
return blsGlobalPromise;
}
export function destroy() {
blsGlobal = null;
blsGlobalPromise = null;
}
export function getContext() {
if (!blsGlobal) {
throw new NotInitializedError("herumi");
}
return blsGlobal;
}

View File

@ -1,9 +0,0 @@
import { SecretKey } from "./secretKey.js";
import { PublicKey } from "./publicKey.js";
import { Signature } from "./signature.js";
import { init, destroy } from "./context.js";
import { IBls } from "../types.js";
export * from "../constants.js";
export { SecretKey, PublicKey, Signature, init, destroy };
export declare const bls: () => Promise<IBls>;
export default bls;

View File

@ -1,18 +0,0 @@
import { SecretKey } from "./secretKey.js";
import { PublicKey } from "./publicKey.js";
import { Signature } from "./signature.js";
import { init, destroy } from "./context.js";
import { functionalInterfaceFactory } from "../functional.js";
export * from "../constants.js";
export { SecretKey, PublicKey, Signature, init, destroy };
export const bls = async () => {
await init();
return {
implementation: "herumi",
SecretKey,
PublicKey,
Signature,
...functionalInterfaceFactory({ SecretKey, PublicKey, Signature }),
};
};
export default bls;

View File

@ -1,11 +0,0 @@
import type { PublicKeyType } from "bls-eth-wasm";
import { PointFormat, PublicKey as IPublicKey } from "../types.js";
export declare class PublicKey implements IPublicKey {
readonly value: PublicKeyType;
constructor(value: PublicKeyType);
static fromBytes(bytes: Uint8Array): PublicKey;
static fromHex(hex: string): PublicKey;
static aggregate(publicKeys: PublicKey[]): PublicKey;
toBytes(format?: PointFormat): Uint8Array;
toHex(format?: PointFormat): string;
}

View File

@ -1,53 +0,0 @@
import { getContext } from "./context.js";
import { bytesToHex, hexToBytes, isZeroUint8Array } from "../helpers/index.js";
import { PointFormat } from "../types.js";
import { EmptyAggregateError, InvalidLengthError, ZeroPublicKeyError } from "../errors.js";
import { PUBLIC_KEY_LENGTH_COMPRESSED, PUBLIC_KEY_LENGTH_UNCOMPRESSED } from "../constants.js";
export class PublicKey {
constructor(value) {
if (value.isZero()) {
throw new ZeroPublicKeyError();
}
this.value = value;
}
static fromBytes(bytes) {
const context = getContext();
const publicKey = new context.PublicKey();
if (!isZeroUint8Array(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);
}
static fromHex(hex) {
return this.fromBytes(hexToBytes(hex));
}
static aggregate(publicKeys) {
if (publicKeys.length === 0) {
throw new EmptyAggregateError();
}
const agg = new PublicKey(publicKeys[0].value.clone());
for (const pk of publicKeys.slice(1)) {
agg.value.add(pk.value);
}
return agg;
}
toBytes(format) {
if (format === PointFormat.uncompressed) {
return this.value.serializeUncompressed();
}
else {
return this.value.serialize();
}
}
toHex(format) {
return bytesToHex(this.toBytes(format));
}
}

View File

@ -1,15 +0,0 @@
import type { SecretKeyType } from "bls-eth-wasm";
import { PublicKey } from "./publicKey.js";
import { Signature } from "./signature.js";
import { SecretKey as ISecretKey } from "../types.js";
export declare class SecretKey implements ISecretKey {
readonly value: SecretKeyType;
constructor(value: SecretKeyType);
static fromBytes(bytes: Uint8Array): SecretKey;
static fromHex(hex: string): SecretKey;
static fromKeygen(entropy?: Uint8Array): SecretKey;
sign(message: Uint8Array): Signature;
toPublicKey(): PublicKey;
toBytes(): Uint8Array;
toHex(): string;
}

View File

@ -1,43 +0,0 @@
import { generateRandomSecretKey } from "@chainsafe/bls-keygen";
import { SECRET_KEY_LENGTH } from "../constants.js";
import { getContext } from "./context.js";
import { PublicKey } from "./publicKey.js";
import { Signature } from "./signature.js";
import { bytesToHex, hexToBytes } from "../helpers/index.js";
import { InvalidLengthError, ZeroSecretKeyError } from "../errors.js";
export class SecretKey {
constructor(value) {
if (value.isZero()) {
throw new ZeroSecretKeyError();
}
this.value = value;
}
static fromBytes(bytes) {
if (bytes.length !== SECRET_KEY_LENGTH) {
throw new InvalidLengthError("SecretKey", SECRET_KEY_LENGTH);
}
const context = getContext();
const secretKey = new context.SecretKey();
secretKey.deserialize(bytes);
return new SecretKey(secretKey);
}
static fromHex(hex) {
return this.fromBytes(hexToBytes(hex));
}
static fromKeygen(entropy) {
const sk = generateRandomSecretKey(entropy);
return this.fromBytes(sk);
}
sign(message) {
return new Signature(this.value.sign(message));
}
toPublicKey() {
return new PublicKey(this.value.getPublicKey());
}
toBytes() {
return this.value.serialize();
}
toHex() {
return bytesToHex(this.toBytes());
}
}

View File

@ -1,24 +0,0 @@
import type { SignatureType } from "bls-eth-wasm";
import { PublicKey } from "./publicKey.js";
import { PointFormat, Signature as ISignature, CoordType } from "../types.js";
export declare class Signature implements ISignature {
readonly value: SignatureType;
constructor(value: SignatureType);
/**
* @param type Does not affect `herumi` implementation, always de-serializes to `jacobian`
* @param validate With `herumi` implementation signature validation is always on regardless of this flag.
*/
static fromBytes(bytes: Uint8Array, _type?: CoordType, _validate?: boolean): Signature;
static fromHex(hex: string): Signature;
static aggregate(signatures: Signature[]): Signature;
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(format?: PointFormat): Uint8Array;
toHex(format?: PointFormat): string;
}

View File

@ -1,70 +0,0 @@
import { getContext } from "./context.js";
import { bytesToHex, concatUint8Arrays, hexToBytes, isZeroUint8Array } from "../helpers/index.js";
import { PointFormat } from "../types.js";
import { EmptyAggregateError, InvalidLengthError, InvalidOrderError } from "../errors.js";
import { SIGNATURE_LENGTH_COMPRESSED, SIGNATURE_LENGTH_UNCOMPRESSED } from "../constants.js";
export class Signature {
constructor(value) {
if (!value.isValidOrder()) {
throw new InvalidOrderError();
}
this.value = value;
}
/**
* @param type Does not affect `herumi` implementation, always de-serializes to `jacobian`
* @param validate With `herumi` implementation signature validation is always on regardless of this flag.
*/
static fromBytes(bytes, _type, _validate = true) {
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);
}
static fromHex(hex) {
return this.fromBytes(hexToBytes(hex));
}
static aggregate(signatures) {
if (signatures.length === 0) {
throw new EmptyAggregateError();
}
const context = getContext();
const signature = new context.Signature();
signature.aggregate(signatures.map((sig) => sig.value));
return new Signature(signature);
}
static verifyMultipleSignatures(sets) {
const context = getContext();
return context.multiVerify(sets.map((s) => s.publicKey.value), sets.map((s) => s.signature.value), sets.map((s) => s.message));
}
verify(publicKey, message) {
return publicKey.value.verify(this.value, message);
}
verifyAggregate(publicKeys, message) {
return this.value.fastAggregateVerify(publicKeys.map((key) => key.value), message);
}
verifyMultiple(publicKeys, messages) {
return this.value.aggregateVerifyNoCheck(publicKeys.map((key) => key.value), concatUint8Arrays(messages));
}
toBytes(format) {
if (format === PointFormat.uncompressed) {
return this.value.serializeUncompressed();
}
else {
return this.value.serialize();
}
}
toHex(format) {
return bytesToHex(this.toBytes(format));
}
}

1
lib/herumi/web.d.ts vendored
View File

@ -1 +0,0 @@
export {};

View File

@ -1,6 +0,0 @@
import { bls } from "./index.js";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(function (window) {
window.bls = bls;
// @ts-ignore
})(window);

3
lib/index.d.ts vendored
View File

@ -1,3 +0,0 @@
import type { IBls } from "./types.js";
export declare const bls: () => Promise<IBls>;
export default bls;

View File

@ -1,14 +0,0 @@
import { getImplementation } from "./getImplementation.js";
// Thanks https://github.com/iliakan/detect-node/blob/master/index.esm.js
const isNode = Object.prototype.toString.call(typeof process !== "undefined" ? process : 0) === "[object process]";
export const bls = async () => {
let bls;
try {
bls = await getImplementation(isNode ? "blst-native" : "herumi");
}
catch (e) {
bls = await getImplementation("herumi");
}
return bls;
};
export default bls;

4
lib/switchable.d.ts vendored
View File

@ -1,4 +0,0 @@
import type { IBls, Implementation } from "./types.js";
declare const bls: IBls;
export default bls;
export declare function init(impl: Implementation): Promise<void>;

View File

@ -1,11 +0,0 @@
import { getImplementation } from "./getImplementation.js";
// TODO: Use a Proxy for example to throw an error if it's not initialized yet
const bls = {};
export default bls;
export async function init(impl) {
// Using Object.assign instead of just bls = getImplementation()
// because otherwise the default import breaks. The reference is lost
// and the imported object is still undefined after calling init()
const blsImpl = await getImplementation(impl);
Object.assign(bls, blsImpl);
}

File diff suppressed because one or more lines are too long

66
lib/types.d.ts vendored
View File

@ -1,66 +0,0 @@
export interface IBls {
implementation: Implementation;
SecretKey: typeof SecretKey;
PublicKey: typeof PublicKey;
Signature: typeof Signature;
sign(secretKey: Uint8Array, message: Uint8Array): Uint8Array;
aggregatePublicKeys(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;
verifyMultipleSignatures(sets: {
publicKey: Uint8Array;
message: Uint8Array;
signature: Uint8Array;
}[]): boolean;
secretKeyToPublicKey(secretKey: Uint8Array): Uint8Array;
}
export declare class SecretKey {
private constructor();
static fromBytes(bytes: Uint8Array): SecretKey;
static fromHex(hex: string): SecretKey;
static fromKeygen(entropy?: Uint8Array): SecretKey;
sign(message: Uint8Array): Signature;
toPublicKey(): PublicKey;
toBytes(): Uint8Array;
toHex(): string;
}
export declare class PublicKey {
private constructor();
/** @param type Only for impl `blst-native`. Defaults to `CoordType.jacobian` */
static fromBytes(bytes: Uint8Array, type?: CoordType, validate?: boolean): PublicKey;
static fromHex(hex: string): PublicKey;
static aggregate(publicKeys: PublicKey[]): PublicKey;
/** @param format Defaults to `PointFormat.compressed` */
toBytes(format?: PointFormat): Uint8Array;
toHex(format?: PointFormat): string;
}
export declare class Signature {
private constructor();
/** @param type Only for impl `blst-native`. Defaults to `CoordType.affine`
* @param validate When using `herumi` implementation, signature validation is always on regardless of this flag. */
static fromBytes(bytes: Uint8Array, type?: CoordType, validate?: boolean): Signature;
static fromHex(hex: string): Signature;
static aggregate(signatures: Signature[]): Signature;
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;
/** @param format Defaults to `PointFormat.compressed` */
toBytes(format?: PointFormat): Uint8Array;
toHex(format?: PointFormat): string;
}
export declare type Implementation = "herumi" | "blst-native";
export declare enum PointFormat {
compressed = "compressed",
uncompressed = "uncompressed"
}
export declare enum CoordType {
affine = 0,
jacobian = 1
}

View File

@ -1,10 +0,0 @@
export var PointFormat;
(function (PointFormat) {
PointFormat["compressed"] = "compressed";
PointFormat["uncompressed"] = "uncompressed";
})(PointFormat || (PointFormat = {}));
export var CoordType;
(function (CoordType) {
CoordType[CoordType["affine"] = 0] = "affine";
CoordType[CoordType["jacobian"] = 1] = "jacobian";
})(CoordType || (CoordType = {}));

1
node.d.ts vendored Normal file
View File

@ -0,0 +1 @@
export * from "./lib/blst";

1
node.js Normal file
View File

@ -0,0 +1 @@
module.exports = require("./lib/blst");

View File

@ -1,49 +1,11 @@
{
"name": "@chainsafe/bls",
"version": "7.1.1",
"version": "5.0.1",
"description": "Implementation of bls signature verification for ethereum 2.0",
"engines": {
"node": ">=14.8.0"
},
"type": "module",
"exports": {
".": {
"import": "./lib/index.js"
},
"./types": {
"import": "./lib/types.js"
},
"./errors": {
"import": "./lib/errors.js"
},
"./constants": {
"import": "./lib/constants.js"
},
"./getImplementation": {
"import": "./lib/getImplementation.js"
},
"./switchable": {
"import": "./lib/switchable.js"
},
"./blst-native": {
"import": "./lib/blst-native/index.js"
},
"./herumi": {
"import": "./lib/herumi/index.js"
}
},
"main": "lib/index.js",
"types": "lib/index.d.ts",
"typesVersions": {
"*": {
"*": [
"*",
"lib/*",
"lib/*/index"
]
}
},
"module": "./lib/index.js",
"browser": "./lib/herumi.js",
"module": "./browser",
"browser": "./browser",
"homepage": "https://github.com/chainsafe/bls",
"author": "ChainSafe Systems",
"license": "Apache-2.0",
@ -51,8 +13,10 @@
"lib/**/*.js",
"lib/**/*.js.map",
"lib/**/*.d.ts",
"*.d.ts",
"*.js"
"blst-native.*",
"browser.*",
"herumi.*",
"node."
],
"keywords": [
"ethereum",
@ -62,63 +26,72 @@
],
"scripts": {
"clean": "rm -rf lib && rm -rf dist && rm -f tsconfig.tsbuildinfo",
"check-build": "node -e \"(async function() { await import('./lib/index.js') })()\"",
"build": "tsc --incremental --project tsconfig.build.json",
"build": "yarn build-lib && yarn build-types",
"build:release": "yarn clean && yarn build && yarn build-web",
"build-lib": "babel src -x .ts -d lib",
"build-types": "tsc --declaration --incremental --outDir lib --project tsconfig.build.json --emitDeclarationOnly",
"build-web": "webpack --mode production --entry ./lib/web.js --output ./dist/bls.min.js",
"check-types": "tsc --noEmit",
"lint": "eslint --color --ext .ts src/ test/",
"lint:fix": "yarn run lint --fix",
"pretest": "yarn check-types",
"prepublishOnly": "yarn build",
"test:web": "karma start karma.conf.cjs",
"test:unit": "mocha 'test/unit/**/*.test.ts'",
"test:coverage": "nyc --cache-dir .nyc_output/.cache -r lcov -e .ts mocha 'test/unit/**/*.test.ts' && nyc report",
"test:spec": "mocha 'test/spec/**/*.test.ts'",
"test:web": "karma start",
"test:unit": "mocha --colors -r ts-node/register 'test/unit/**/*.test.ts'",
"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": "yarn run test:unit && yarn run test:spec",
"download-test-cases": "ts-node-esm test/downloadSpecTests.ts",
"download-test-cases": "ts-node test/downloadSpecTests.ts",
"coverage": "codecov -F bls",
"benchmark": "ts-node-esm benchmark",
"benchmark": "ts-node benchmark",
"benchmark:all": "cd benchmark && yarn install && yarn benchmark:all"
},
"dependencies": {
"@chainsafe/bls-keygen": "^0.4.0",
"bls-eth-wasm": "^0.4.8",
"@chainsafe/bls-keygen": "^0.3.0",
"bls-eth-wasm": "^0.4.6",
"randombytes": "^2.1.0"
},
"devDependencies": {
"@chainsafe/blst": "^0.2.4",
"@chainsafe/eslint-plugin-node": "^11.2.3",
"@chainsafe/lodestar-spec-test-util": "^0.18.0",
"@chainsafe/threads": "^1.9.0",
"@babel/cli": "^7.8.4",
"@babel/core": "^7.8.4",
"@babel/plugin-proposal-class-properties": "^7.8.3",
"@babel/plugin-proposal-object-rest-spread": "^7.8.3",
"@babel/plugin-syntax-bigint": "^7.8.3",
"@babel/preset-env": "^7.8.4",
"@babel/preset-typescript": "^7.8.3",
"@babel/register": "^7.8.3",
"@chainsafe/blst": "^0.1.3",
"@chainsafe/lodestar-spec-test-util": "^0.12.0",
"@types/chai": "^4.2.9",
"@types/mocha": "^10.0.0",
"@types/mocha": "^8.0.4",
"@types/randombytes": "^2.0.0",
"@typescript-eslint/eslint-plugin": "^4.31.1",
"@typescript-eslint/parser": "^4.31.1",
"buffer": "^6.0.3",
"chai": "^4.3.6",
"eslint": "^7.14.0",
"@typescript-eslint/eslint-plugin": "^2.20.0",
"@typescript-eslint/parser": "^2.20.0",
"chai": "^4.2.0",
"eslint": "^6.8.0",
"eslint-plugin-import": "^2.20.1",
"eslint-plugin-prettier": "^3.1.4",
"karma": "^6.3.18",
"karma": "^4.4.1",
"karma-chai": "^0.1.0",
"karma-chrome-launcher": "^3.1.0",
"karma-cli": "^2.0.0",
"karma-mocha": "^2.0.1",
"karma-mocha": "^1.3.0",
"karma-spec-reporter": "^0.0.32",
"karma-webpack": "^5.0.0",
"mocha": "^10.0.0",
"karma-webpack": "^4.0.2",
"mocha": "^8.2.1",
"nyc": "^15.0.0",
"prettier": "^2.1.2",
"resolve-typescript-plugin": "^1.2.0",
"ts-loader": "^9.3.1",
"ts-node": "^10.9.1",
"typescript": "4.7.4",
"webpack": "^5.72.0",
"webpack-cli": "^4.9.2"
"ts-loader": "^6.2.1",
"ts-node": "^8.6.2",
"typescript": "^3.7.5",
"webpack": "^4.30.0",
"webpack-cli": "^3.3.2"
},
"resolutions": {
"mocha": "^9.2.2",
"mocha": "^8.2.1",
"v8-profiler-next": "1.3.0"
},
"peerDependencies": {
"@chainsafe/blst": "^0.2.4"
"@chainsafe/blst": "^0.1.3"
}
}

View File

@ -1,19 +0,0 @@
import {SecretKey} from "./secretKey.js";
import {PublicKey} from "./publicKey.js";
import {Signature} from "./signature.js";
import {IBls} from "../types.js";
import {functionalInterfaceFactory} from "../functional.js";
export * from "../constants.js";
export {SecretKey, PublicKey, Signature};
export const bls: IBls = {
implementation: "blst-native",
SecretKey,
PublicKey,
Signature,
...functionalInterfaceFactory({SecretKey, PublicKey, Signature}),
};
export default bls;

View File

@ -1,42 +0,0 @@
import * as blst from "@chainsafe/blst";
import {EmptyAggregateError} from "../errors.js";
import {bytesToHex, hexToBytes} from "../helpers/index.js";
import {PointFormat, PublicKey as IPublicKey} from "../types.js";
export class PublicKey extends blst.PublicKey implements IPublicKey {
constructor(value: ConstructorParameters<typeof blst.PublicKey>[0]) {
super(value);
}
/** @param type Defaults to `CoordType.jacobian` */
static fromBytes(bytes: Uint8Array, type?: blst.CoordType, validate?: boolean): PublicKey {
const pk = blst.PublicKey.fromBytes(bytes, type);
if (validate) pk.keyValidate();
return new PublicKey(pk.value);
}
static fromHex(hex: string): PublicKey {
return this.fromBytes(hexToBytes(hex));
}
static aggregate(publicKeys: PublicKey[]): PublicKey {
if (publicKeys.length === 0) {
throw new EmptyAggregateError();
}
const pk = blst.aggregatePubkeys(publicKeys);
return new PublicKey(pk.value);
}
toBytes(format?: PointFormat): Uint8Array {
if (format === PointFormat.uncompressed) {
return this.value.serialize();
} else {
return this.value.compress();
}
}
toHex(format?: PointFormat): string {
return bytesToHex(this.toBytes(format));
}
}

View File

@ -1,77 +0,0 @@
import * as blst from "@chainsafe/blst";
import {bytesToHex, hexToBytes} from "../helpers/index.js";
import {PointFormat, Signature as ISignature} from "../types.js";
import {PublicKey} from "./publicKey.js";
import {EmptyAggregateError, ZeroSignatureError} from "../errors.js";
export class Signature extends blst.Signature implements ISignature {
constructor(value: ConstructorParameters<typeof blst.Signature>[0]) {
super(value);
}
/** @param type Defaults to `CoordType.affine` */
static fromBytes(bytes: Uint8Array, type?: blst.CoordType, validate = true): Signature {
const sig = blst.Signature.fromBytes(bytes, type);
if (validate) sig.sigValidate();
return new Signature(sig.value);
}
static fromHex(hex: string): Signature {
return this.fromBytes(hexToBytes(hex));
}
static aggregate(signatures: Signature[]): Signature {
if (signatures.length === 0) {
throw new EmptyAggregateError();
}
const agg = blst.aggregateSignatures(signatures);
return new Signature(agg.value);
}
static verifyMultipleSignatures(sets: {publicKey: PublicKey; message: Uint8Array; signature: Signature}[]): boolean {
return blst.verifyMultipleAggregateSignatures(
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.value.is_inf()) {
throw new ZeroSignatureError();
}
return blst.verify(message, publicKey, this);
}
verifyAggregate(publicKeys: PublicKey[], message: Uint8Array): boolean {
return blst.fastAggregateVerify(message, publicKeys, this);
}
verifyMultiple(publicKeys: PublicKey[], messages: Uint8Array[]): boolean {
return blst.aggregateVerify(messages, publicKeys, this);
}
toBytes(format?: PointFormat): Uint8Array {
if (format === PointFormat.uncompressed) {
return this.value.serialize();
} else {
return this.value.compress();
}
}
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.value.is_inf() && pks.length === 1 && pks[0].value.is_inf()) {
return true;
}
return blst.aggregateVerify(msgs, pks, this);
}
}

27
src/blst/index.ts Normal file
View File

@ -0,0 +1,27 @@
import {SecretKey} from "./secretKey";
import {PublicKey} from "./publicKey";
import {Signature} from "./signature";
import {IBls} from "../interface";
import {functionalInterfaceFactory} from "../functional";
export * from "../constants";
export {SecretKey, 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 = {
SecretKey,
PublicKey,
Signature,
...functionalInterfaceFactory({SecretKey, PublicKey, Signature}),
init,
destroy,
};
export default bls;

46
src/blst/publicKey.ts Normal file
View File

@ -0,0 +1,46 @@
import * as blst from "@chainsafe/blst";
import {EmptyAggregateError, ZeroPublicKeyError} from "../errors";
import {bytesToHex, hexToBytes} from "../helpers";
import {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;
}
static fromBytes(bytes: Uint8Array): PublicKey {
const affine = blst.PublicKey.fromBytes(bytes);
if (affine.value.is_inf()) {
throw new ZeroPublicKeyError();
}
const jacobian = blst.AggregatePublicKey.fromPublicKey(affine);
return new PublicKey(affine, jacobian);
}
static fromHex(hex: string): PublicKey {
return this.fromBytes(hexToBytes(hex));
}
static aggregate(publicKeys: PublicKey[]): PublicKey {
if (publicKeys.length === 0) {
throw new EmptyAggregateError();
}
const jacobian = blst.aggregatePubkeys(publicKeys.map((pk) => pk.jacobian));
const affine = jacobian.toPublicKey();
return new PublicKey(affine, jacobian);
}
toBytes(): Uint8Array {
return this.affine.toBytes();
}
toHex(): string {
return bytesToHex(this.toBytes());
}
}

View File

@ -1,13 +1,14 @@
import * as blst from "@chainsafe/blst";
import {bytesToHex, hexToBytes, isZeroUint8Array, randomBytes} from "../helpers/index.js";
import {SECRET_KEY_LENGTH} from "../constants.js";
import {SecretKey as ISecretKey} from "../types.js";
import {PublicKey} from "./publicKey.js";
import {Signature} from "./signature.js";
import {ZeroSecretKeyError} from "../errors.js";
import {bytesToHex, hexToBytes, isZeroUint8Array, randomBytes} from "../helpers";
import {SECRET_KEY_LENGTH} from "../constants";
import {SecretKey as ISecretKey} from "../interface";
import {PublicKey} from "./publicKey";
import {Signature} from "./signature";
import {ZeroSecretKeyError} from "../errors";
export class SecretKey implements ISecretKey {
readonly value: blst.SecretKey;
constructor(value: blst.SecretKey) {
this.value = value;
}
@ -32,12 +33,13 @@ export class SecretKey implements ISecretKey {
}
sign(message: Uint8Array): Signature {
return new Signature(this.value.sign(message).value);
return new Signature(this.value.sign(message));
}
toPublicKey(): PublicKey {
const pk = this.value.toPublicKey();
return new PublicKey(pk.value);
const jacobian = this.value.toAggregatePublicKey();
const affine = jacobian.toPublicKey();
return new PublicKey(affine, jacobian);
}
toBytes(): Uint8Array {

78
src/blst/signature.ts Normal file
View File

@ -0,0 +1,78 @@
import * as blst from "@chainsafe/blst";
import {bytesToHex, hexToBytes} from "../helpers";
import {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;
}
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 {
if (signatures.length === 0) {
throw new EmptyAggregateError();
}
const agg = blst.AggregateSignature.fromSignatures(signatures.map((sig) => sig.affine));
return new Signature(agg.toSignature());
}
static verifyMultipleSignatures(publicKeys: PublicKey[], messages: Uint8Array[], signatures: Signature[]): boolean {
return blst.verifyMultipleAggregateSignatures(
messages,
publicKeys.map((publicKey) => publicKey.affine),
signatures.map((signature) => signature.affine)
);
}
verify(publicKey: PublicKey, message: Uint8Array): boolean {
// Individual infinity signatures are NOT okay. Aggregated signatures MAY be infinity
if (this.affine.value.is_inf()) {
throw new ZeroSignatureError();
}
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 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()) {
return true;
}
return blst.aggregateVerify(msgs, pks, this.affine);
}
}

View File

@ -1,5 +1,5 @@
export const SECRET_KEY_LENGTH = 32;
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;
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;

View File

@ -40,6 +40,6 @@ export class InvalidOrderError extends Error {
export class InvalidLengthError extends Error {
constructor(arg: string, length: number) {
super(`INVALID_LENGTH: ${arg} - ${length} bytes`);
super(`INVALID_LENGTH: ${arg} must have ${length} bytes`);
}
}

View File

@ -1,10 +1,9 @@
import {IBls} from "./types.js";
import {validateBytes} from "./helpers/index.js";
import {NotInitializedError} from "./errors.js";
import {IBls} from "./interface";
import {validateBytes} from "./helpers";
import {NotInitializedError} from "./errors";
// Returned type is enforced at each implementation's index
// eslint-disable-next-line max-len
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type,@typescript-eslint/explicit-module-boundary-types
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function functionalInterfaceFactory({
SecretKey,
PublicKey,
@ -118,17 +117,22 @@ export function functionalInterfaceFactory({
* https://ethresear.ch/t/fast-verification-of-multiple-bls-signatures/5407
*/
function verifyMultipleSignatures(
sets: {publicKey: Uint8Array; message: Uint8Array; signature: Uint8Array}[]
publicKeys: Uint8Array[],
messages: Uint8Array[],
signatures: Uint8Array[]
): boolean {
if (!sets) throw Error("sets is null or undefined");
validateBytes(publicKeys, "publicKey");
validateBytes(messages, "message");
validateBytes(signatures, "signatures");
if (publicKeys.length === 0 || publicKeys.length !== messages.length || publicKeys.length !== signatures.length) {
return false;
}
try {
return Signature.verifyMultipleSignatures(
sets.map((s) => ({
publicKey: PublicKey.fromBytes(s.publicKey),
message: s.message,
signature: Signature.fromBytes(s.signature),
}))
publicKeys.map((publicKey) => PublicKey.fromBytes(publicKey)),
messages.map((msg) => msg),
signatures.map((signature) => Signature.fromBytes(signature))
);
} catch (e) {
if (e instanceof NotInitializedError) throw e;

View File

@ -1,22 +0,0 @@
import type {IBls, Implementation} from "./types.js";
// Thanks https://github.com/iliakan/detect-node/blob/master/index.esm.js
const isNode = Object.prototype.toString.call(typeof process !== "undefined" ? process : 0) === "[object process]";
export async function getImplementation(impl: Implementation = "herumi"): Promise<IBls> {
switch (impl) {
case "herumi": {
return await (await import("./herumi/index.js")).bls();
}
case "blst-native":
// Lazy import native bindings to prevent automatically importing binding.node files
if (!isNode) {
throw Error("blst-native is only supported in NodeJS");
}
return (await import("./blst-native/index.js")).bls;
default:
throw new Error(`Unsupported implementation - ${impl}`);
}
}

View File

@ -1,2 +1,2 @@
export * from "./hex.js";
export * from "./utils.js";
export * from "./hex";
export * from "./utils";

View File

@ -1,30 +1,14 @@
/* eslint-disable require-atomic-updates */
import bls from "bls-eth-wasm";
import {NotInitializedError} from "../errors.js";
import {NotInitializedError} from "../errors";
type Bls = typeof bls;
let blsGlobal: Bls | null = null;
let blsGlobalPromise: Promise<void> | null = null;
// Patch to fix multiVerify() calls on a browser with polyfilled NodeJS crypto
declare global {
interface Window {
msCrypto: typeof window["crypto"];
}
}
export async function setupBls(): Promise<void> {
if (!blsGlobal) {
await bls.init(bls.BLS12_381);
// Patch to fix multiVerify() calls on a browser with polyfilled NodeJS crypto
if (typeof window === "object") {
const crypto = window.crypto || window.msCrypto;
// getRandomValues is not typed in `bls-eth-wasm` because it's not meant to be exposed
// @ts-ignore
bls.getRandomValues = (x) => crypto.getRandomValues(x);
}
blsGlobal = bls;
}
}

View File

@ -1,23 +1,21 @@
import {SecretKey} from "./secretKey.js";
import {PublicKey} from "./publicKey.js";
import {Signature} from "./signature.js";
import {init, destroy} from "./context.js";
import {IBls} from "../types.js";
import {functionalInterfaceFactory} from "../functional.js";
export * from "../constants.js";
import {SecretKey} from "./secretKey";
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 {SecretKey, PublicKey, Signature, init, destroy};
export const bls = async (): Promise<IBls> => {
await init();
return {
implementation: "herumi",
SecretKey,
PublicKey,
Signature,
export const bls: IBls = {
SecretKey,
PublicKey,
Signature,
...functionalInterfaceFactory({SecretKey, PublicKey, Signature}),
};
...functionalInterfaceFactory({SecretKey, PublicKey, Signature}),
init,
destroy,
};
export default bls;

View File

@ -1,9 +1,9 @@
import type {PublicKeyType} from "bls-eth-wasm";
import {getContext} from "./context.js";
import {bytesToHex, hexToBytes, isZeroUint8Array} from "../helpers/index.js";
import {PointFormat, PublicKey as IPublicKey} from "../types.js";
import {EmptyAggregateError, InvalidLengthError, ZeroPublicKeyError} from "../errors.js";
import {PUBLIC_KEY_LENGTH_COMPRESSED, PUBLIC_KEY_LENGTH_UNCOMPRESSED} from "../constants.js";
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 {EmptyAggregateError, InvalidLengthError, ZeroPublicKeyError} from "../errors";
export class PublicKey implements IPublicKey {
readonly value: PublicKeyType;
@ -17,16 +17,14 @@ 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)) {
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);
}
publicKey.deserialize(bytes);
}
return new PublicKey(publicKey);
}
@ -47,15 +45,11 @@ export class PublicKey implements IPublicKey {
return agg;
}
toBytes(format?: PointFormat): Uint8Array {
if (format === PointFormat.uncompressed) {
return this.value.serializeUncompressed();
} else {
return this.value.serialize();
}
toBytes(): Uint8Array {
return this.value.serialize();
}
toHex(format?: PointFormat): string {
return bytesToHex(this.toBytes(format));
toHex(): string {
return bytesToHex(this.toBytes());
}
}

View File

@ -1,12 +1,12 @@
import type {SecretKeyType} from "bls-eth-wasm";
import {SecretKeyType} from "bls-eth-wasm";
import {generateRandomSecretKey} from "@chainsafe/bls-keygen";
import {SECRET_KEY_LENGTH} from "../constants.js";
import {getContext} from "./context.js";
import {PublicKey} from "./publicKey.js";
import {Signature} from "./signature.js";
import {bytesToHex, hexToBytes} from "../helpers/index.js";
import {SecretKey as ISecretKey} from "../types.js";
import {InvalidLengthError, ZeroSecretKeyError} from "../errors.js";
import {SECRET_KEY_LENGTH} from "../constants";
import {getContext} from "./context";
import {PublicKey} from "./publicKey";
import {Signature} from "./signature";
import {bytesToHex, hexToBytes} from "../helpers";
import {SecretKey as ISecretKey} from "../interface";
import {InvalidLengthError, ZeroSecretKeyError} from "../errors";
export class SecretKey implements ISecretKey {
readonly value: SecretKeyType;
@ -35,7 +35,7 @@ export class SecretKey implements ISecretKey {
}
static fromKeygen(entropy?: Uint8Array): SecretKey {
const sk = generateRandomSecretKey(entropy);
const sk = generateRandomSecretKey(entropy && Buffer.from(entropy));
return this.fromBytes(sk);
}

View File

@ -1,10 +1,10 @@
import type {SignatureType} from "bls-eth-wasm";
import {getContext} from "./context.js";
import {PublicKey} from "./publicKey.js";
import {bytesToHex, concatUint8Arrays, hexToBytes, isZeroUint8Array} from "../helpers/index.js";
import {PointFormat, Signature as ISignature, CoordType} from "../types.js";
import {EmptyAggregateError, InvalidLengthError, InvalidOrderError} from "../errors.js";
import {SIGNATURE_LENGTH_COMPRESSED, SIGNATURE_LENGTH_UNCOMPRESSED} from "../constants.js";
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 {EmptyAggregateError, InvalidLengthError, InvalidOrderError} from "../errors";
export class Signature implements ISignature {
readonly value: SignatureType;
@ -17,21 +17,14 @@ export class Signature implements ISignature {
this.value = value;
}
/**
* @param type Does not affect `herumi` implementation, always de-serializes to `jacobian`
* @param validate With `herumi` implementation signature validation is always on regardless of this flag.
*/
static fromBytes(bytes: Uint8Array, _type?: CoordType, _validate = true): Signature {
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);
@ -52,12 +45,11 @@ export class Signature implements ISignature {
return new Signature(signature);
}
static verifyMultipleSignatures(sets: {publicKey: PublicKey; message: Uint8Array; signature: Signature}[]): boolean {
const context = getContext();
return context.multiVerify(
sets.map((s) => s.publicKey.value),
sets.map((s) => s.signature.value),
sets.map((s) => s.message)
static verifyMultipleSignatures(publicKeys: PublicKey[], messages: Uint8Array[], signatures: Signature[]): boolean {
return multiVerify(
publicKeys.map((publicKey) => publicKey.value),
signatures.map((signature) => signature.value),
messages
);
}
@ -79,15 +71,11 @@ export class Signature implements ISignature {
);
}
toBytes(format?: PointFormat): Uint8Array {
if (format === PointFormat.uncompressed) {
return this.value.serializeUncompressed();
} else {
return this.value.serialize();
}
toBytes(): Uint8Array {
return this.value.serialize();
}
toHex(format?: PointFormat): string {
return bytesToHex(this.toBytes(format));
toHex(): string {
return bytesToHex(this.toBytes());
}
}

View File

@ -1,4 +1,4 @@
import {bls} from "./index.js";
import {bls} from "./index";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(function (window: any) {

View File

@ -1,19 +1,47 @@
import type {IBls} from "./types.js";
import {getImplementation} from "./getImplementation.js";
import {IBls} from "./interface";
import {bls as blsHerumi} from "./herumi";
// Thanks https://github.com/iliakan/detect-node/blob/master/index.esm.js
const isNode = Object.prototype.toString.call(typeof process !== "undefined" ? process : 0) === "[object process]";
export type Implementation = "herumi" | "blst-native";
export const bls = async (): Promise<IBls> => {
let bls: IBls;
try {
bls = await getImplementation(isNode ? "blst-native" : "herumi");
} catch (e) {
bls = await getImplementation("herumi");
}
return bls;
};
export * from "./interface";
// TODO: Use a Proxy for example to throw an error if it's not initialized yet
export const bls: IBls = {} as IBls;
export default bls;
async function getImplementation(impl: Implementation = "herumi"): Promise<IBls> {
switch (impl) {
case "herumi":
await blsHerumi.init();
return blsHerumi;
case "blst-native":
// Lazy import native bindings to prevent automatically importing binding.node files
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").bls;
default:
throw new Error(`Unsupported implementation - ${impl}`);
}
}
export async function init(impl: Implementation): Promise<void> {
// Using Object.assign instead of just bls = getImplementation()
// because otherwise the default import breaks. The reference is lost
// and the imported object is still undefined after calling init()
const blsImpl = await getImplementation(impl);
Object.assign(bls, blsImpl);
Object.assign(exports, blsImpl);
}
// Proxy named exports, will get set by `Object.assign(exports, blsImpl)`
export declare let sign: IBls["sign"];
export declare let aggregateSignatures: IBls["aggregateSignatures"];
export declare let aggregatePublicKeys: IBls["aggregatePublicKeys"];
export declare let verify: IBls["verify"];
export declare let verifyAggregate: IBls["verifyAggregate"];
export declare let verifyMultiple: IBls["verifyMultiple"];
export declare let secretKeyToPublicKey: IBls["secretKeyToPublicKey"];

60
src/interface.ts Normal file
View File

@ -0,0 +1,60 @@
export interface IBls {
SecretKey: {
fromBytes(bytes: Uint8Array): SecretKey;
fromHex(hex: string): SecretKey;
fromKeygen(ikm?: Uint8Array): SecretKey;
};
PublicKey: {
fromBytes(bytes: Uint8Array): PublicKey;
fromHex(hex: string): PublicKey;
aggregate(publicKeys: PublicKey[]): PublicKey;
};
Signature: {
fromBytes(bytes: Uint8Array): Signature;
fromHex(hex: string): Signature;
aggregate(signatures: Signature[]): Signature;
verifyMultipleSignatures(publicKeys: PublicKey[], messages: Uint8Array[], signatures: Signature[]): boolean;
};
sign(secretKey: Uint8Array, message: Uint8Array): Uint8Array;
aggregatePublicKeys(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;
verifyMultipleSignatures(publicKeys: Uint8Array[], messages: Uint8Array[], signatures: Uint8Array[]): boolean;
secretKeyToPublicKey(secretKey: Uint8Array): Uint8Array;
init(): Promise<void>;
destroy(): void;
}
export declare class SecretKey {
static fromBytes(bytes: Uint8Array): SecretKey;
static fromHex(hex: string): SecretKey;
static fromKeygen(entropy?: Uint8Array): SecretKey;
sign(message: Uint8Array): Signature;
toPublicKey(): PublicKey;
toBytes(): Uint8Array;
toHex(): string;
}
export declare class PublicKey {
static fromBytes(bytes: Uint8Array): PublicKey;
static fromHex(hex: string): PublicKey;
static aggregate(publicKeys: PublicKey[]): PublicKey;
toBytes(): Uint8Array;
toHex(): string;
}
export declare class Signature {
static fromBytes(bytes: Uint8Array): Signature;
static fromHex(hex: string): Signature;
static aggregate(signatures: Signature[]): Signature;
static verifyMultipleSignatures(publicKeys: PublicKey[], messages: Uint8Array[], signatures: Signature[]): boolean;
verify(publicKey: PublicKey, message: Uint8Array): boolean;
verifyAggregate(publicKeys: PublicKey[], message: Uint8Array): boolean;
verifyMultiple(publicKeys: PublicKey[], messages: Uint8Array[]): boolean;
toBytes(): Uint8Array;
toHex(): string;
}

View File

@ -1,14 +0,0 @@
import type {IBls, Implementation} from "./types.js";
import {getImplementation} from "./getImplementation.js";
// TODO: Use a Proxy for example to throw an error if it's not initialized yet
const bls: IBls = {} as IBls;
export default bls;
export async function init(impl: Implementation): Promise<void> {
// Using Object.assign instead of just bls = getImplementation()
// because otherwise the default import breaks. The reference is lost
// and the imported object is still undefined after calling init()
const blsImpl = await getImplementation(impl);
Object.assign(bls, blsImpl);
}

View File

@ -1,68 +0,0 @@
export interface IBls {
implementation: Implementation;
SecretKey: typeof SecretKey;
PublicKey: typeof PublicKey;
Signature: typeof Signature;
sign(secretKey: Uint8Array, message: Uint8Array): Uint8Array;
aggregatePublicKeys(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;
verifyMultipleSignatures(sets: {publicKey: Uint8Array; message: Uint8Array; signature: Uint8Array}[]): boolean;
secretKeyToPublicKey(secretKey: Uint8Array): Uint8Array;
}
export declare class SecretKey {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private constructor(...value: any);
static fromBytes(bytes: Uint8Array): SecretKey;
static fromHex(hex: string): SecretKey;
static fromKeygen(entropy?: Uint8Array): SecretKey;
sign(message: Uint8Array): Signature;
toPublicKey(): PublicKey;
toBytes(): Uint8Array;
toHex(): string;
}
export declare class PublicKey {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private constructor(...value: any);
/** @param type Only for impl `blst-native`. Defaults to `CoordType.jacobian` */
static fromBytes(bytes: Uint8Array, type?: CoordType, validate?: boolean): PublicKey;
static fromHex(hex: string): PublicKey;
static aggregate(publicKeys: PublicKey[]): PublicKey;
/** @param format Defaults to `PointFormat.compressed` */
toBytes(format?: PointFormat): Uint8Array;
toHex(format?: PointFormat): string;
}
export declare class Signature {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private constructor(...value: any);
/** @param type Only for impl `blst-native`. Defaults to `CoordType.affine`
* @param validate When using `herumi` implementation, signature validation is always on regardless of this flag. */
static fromBytes(bytes: Uint8Array, type?: CoordType, validate?: boolean): Signature;
static fromHex(hex: string): Signature;
static aggregate(signatures: Signature[]): Signature;
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;
/** @param format Defaults to `PointFormat.compressed` */
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,
}

View File

@ -1,5 +1,5 @@
import {downloadTests} from "@chainsafe/lodestar-spec-test-util";
import {SPEC_TEST_VERSION, SPEC_TESTS_DIR, SPEC_TEST_TO_DOWNLOAD} from "./params.js";
import {SPEC_TEST_VERSION, SPEC_TESTS_DIR, SPEC_TEST_TO_DOWNLOAD} from "./params";
/* eslint-disable no-console */

View File

@ -1,8 +1,5 @@
import path from "path";
import {fileURLToPath} from "url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
export const SPEC_TEST_VERSION = "v1.0.0";
export const SPEC_TEST_TO_DOWNLOAD = ["general" as const];
export const SPEC_TEST_TO_DOWNLOAD = ["general" as "general"];
export const SPEC_TESTS_DIR = path.join(__dirname, "spec-tests");

View File

@ -1,9 +1,9 @@
import path from "path";
import {describeDirectorySpecTest, InputType} from "@chainsafe/lodestar-spec-test-util";
import {bytesToHex, hexToBytes} from "../../src/helpers/index.js";
import {SPEC_TESTS_DIR} from "../params.js";
import {describeForAllImplementations} from "../switch.js";
import {EmptyAggregateError} from "../../src/errors.js";
import {bytesToHex, hexToBytes} from "../../src/helpers";
import {SPEC_TESTS_DIR} from "../params";
import {describeForAllImplementations} from "../switch";
import {EmptyAggregateError} from "../../src/errors";
interface IAggregateSigsTestCase {
data: {
@ -13,7 +13,7 @@ interface IAggregateSigsTestCase {
}
describeForAllImplementations((bls) => {
describeDirectorySpecTest<IAggregateSigsTestCase, string | null>(
describeDirectorySpecTest<IAggregateSigsTestCase, string>(
"bls/aggregate/small",
path.join(SPEC_TESTS_DIR, "tests/general/phase0/bls/aggregate/small"),
(testCase) => {

View File

@ -1,8 +1,8 @@
import path from "path";
import {describeDirectorySpecTest, InputType} from "@chainsafe/lodestar-spec-test-util";
import {hexToBytes} from "../../src/helpers/index.js";
import {SPEC_TESTS_DIR} from "../params.js";
import {describeForAllImplementations} from "../switch.js";
import {hexToBytes} from "../../src/helpers";
import {SPEC_TESTS_DIR} from "../params";
import {describeForAllImplementations} from "../switch";
interface IAggregateSigsVerifyTestCase {
data: {

View File

@ -1,9 +1,8 @@
import path from "path";
import {describeDirectorySpecTest, InputType} from "@chainsafe/lodestar-spec-test-util";
import {hexToBytes} from "../../src/helpers/index.js";
import {SPEC_TESTS_DIR} from "../params.js";
import {describeForAllImplementations} from "../switch.js";
import {CoordType} from "@chainsafe/blst";
import {hexToBytes} from "../../src/helpers";
import {SPEC_TESTS_DIR} from "../params";
import {describeForAllImplementations} from "../switch";
interface IAggregateSigsVerifyTestCase {
data: {
@ -22,14 +21,7 @@ describeForAllImplementations((bls) => {
path.join(SPEC_TESTS_DIR, "tests/general/phase0/bls/fast_aggregate_verify/small"),
(testCase) => {
const {pubkeys, message, signature} = testCase.data.input;
try {
return bls.Signature.fromBytes(hexToBytes(signature)).verifyAggregate(
pubkeys.map((hex) => bls.PublicKey.fromBytes(hexToBytes(hex), CoordType.jacobian, true)),
hexToBytes(message)
);
} catch (e) {
return false;
}
return bls.verifyAggregate(pubkeys.map(hexToBytes), hexToBytes(message), hexToBytes(signature));
},
{
inputTypes: {data: InputType.YAML},

View File

@ -1,9 +1,9 @@
import path from "path";
import {describeDirectorySpecTest, InputType} from "@chainsafe/lodestar-spec-test-util";
import {bytesToHex, hexToBytes} from "../../src/helpers/index.js";
import {SPEC_TESTS_DIR} from "../params.js";
import {describeForAllImplementations} from "../switch.js";
import {ZeroSecretKeyError} from "../../src/errors.js";
import {bytesToHex, hexToBytes} from "../../src/helpers";
import {SPEC_TESTS_DIR} from "../params";
import {describeForAllImplementations} from "../switch";
import {ZeroSecretKeyError} from "../../src/errors";
interface ISignMessageTestCase {
data: {
@ -16,7 +16,7 @@ interface ISignMessageTestCase {
}
describeForAllImplementations((bls) => {
describeDirectorySpecTest<ISignMessageTestCase, string | null>(
describeDirectorySpecTest<ISignMessageTestCase, string>(
"bls/sign/small",
path.join(SPEC_TESTS_DIR, "tests/general/phase0/bls/sign/small"),
(testCase) => {

View File

@ -1,8 +1,8 @@
import path from "path";
import {describeDirectorySpecTest, InputType} from "@chainsafe/lodestar-spec-test-util";
import {hexToBytes} from "../../src/helpers/index.js";
import {SPEC_TESTS_DIR} from "../params.js";
import {describeForAllImplementations} from "../switch.js";
import {hexToBytes} from "../../src/helpers";
import {SPEC_TESTS_DIR} from "../params";
import {describeForAllImplementations} from "../switch";
interface IVerifyTestCase {
data: {

Some files were not shown because too many files have changed in this diff Show More