Compare commits

..

4 Commits

Author SHA1 Message Date
Marin Petrunić 5d92af17fc
Merge remote-tracking branch 'origin/dev' into mpetrunic/abstract-hermu 2020-11-24 21:08:46 +01:00
Marin Petrunić c153de6e24
fix lint 2020-11-23 14:42:52 +01:00
Marin Petrunić 135625ccc0
fix context backing name 2020-11-23 14:42:04 +01:00
Marin Petrunić 0d273a7d36
abstract herumi 2020-11-23 14:35:20 +01:00
127 changed files with 5168 additions and 5744 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",
@ -58,7 +55,7 @@ module.exports = {
"import/no-extraneous-dependencies": ["error", {
"devDependencies": false,
"optionalDependencies": false,
"peerDependencies": true
"peerDependencies": false
}],
"func-call-spacing": "off",
"max-len": ["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

@ -1,65 +0,0 @@
name: Release
on:
push:
branches:
- 'master'
jobs:
tag:
name: Check and Tag
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Create tag
id: tag
uses: butlerlogic/action-autotag@1.1.1
with:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
strategy: package # Optional, since "package" is the default strategy
tag_prefix: "v"
outputs:
tag: ${{ steps.tag.outputs.tagname }}
version: ${{ steps.tag.outputs.version }}
release:
name: Publish
runs-on: ubuntu-latest
needs: tag
if: needs.tag.outputs.tag != ''
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup Nodejs
uses: actions/setup-node@v1
with:
node-version: "14.x"
registry-url: "https://registry.npmjs.org"
- name: Install dependencies
run: yarn install --non-interactive --frozen-lockfile
- name: Build
run: yarn run build
- name: Publish to npm registry
run: yarn publish --no-git-tag-version --no-commit-hooks --non-interactive
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ needs.tag.outputs.tag }}
body: ""
release_name: Release ${{ needs.tag.outputs.tag }}
#in case of failure
- name: Rollback on failure
if: failure()
uses: author/action-rollback@9ec72a6af74774e00343c6de3e946b0901c23013
with:
id: ${{ steps.create_release.outputs.id }}
tag: ${{ needs.tag.outputs.tag }}
delete_orphan_tag: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -1,35 +1,18 @@
name: Main
name: CI Tests
on: [pull_request, push]
jobs:
tests:
name: Tests
lint:
name: Quick 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}}
- name: Install deps
run: yarn --non-interactive --frozen-lockfile
- name: Lint
run: yarn lint
- name: Test build
run: yarn build
- name: Unit tests
run: yarn test:unit
- name: Download spec tests
run: yarn download-test-cases
- name: Spec tests
run: yarn test:spec
- name: Web tests
run: yarn test:web
- name: Benchmark
run: yarn benchmark:all
- uses: actions/checkout@v1
- name: Bootstrap
run: yarn
- name: Check types
run: yarn check-types
- name: Lint
run: yarn lint
- name: Tests
run: yarn test

5
.gitignore vendored
View File

@ -65,8 +65,3 @@ typings/
dist/
lib/
benchmark-reports
.vscode/
# Eth2.0 spec tests data
test/spec-tests

View File

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

View File

@ -1 +0,0 @@
.eslintrc.js

View File

@ -5,71 +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)
## [5.0.1] - 2020-11-30
- Remove foreign property breaking types ([ccd870](https://github.com/chainsafe/bls/commit/ccd870))
- Deduplicate interface ([0bf6e9](https://github.com/chainsafe/bls/commit/0bf6e9))
- Deprecate IKeypair ([293286](https://github.com/chainsafe/bls/commit/293286))
## [5.0.0] - 2020-11-30
### BREAKING CHANGES
- Compatible with [Eth2 spec 1.0.0](https://github.com/ethereum/eth2.0-specs/blob/v1.0.0/specs/phase0/beacon-chain.md#bls-signatures)
- Update bls-keygen to latest EIP-2333 standard
- Refactored class-based interface, minor functional interface changes
- BLST support
## [dev]
## [4.0.0] - 2020-08-31

131
README.md
View File

@ -1,125 +1,26 @@
# 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)
![ETH2.0_Spec_Version 0.12.0](https://img.shields.io/badge/ETH2.0_Spec_Version-0.12.0-2e86c1.svg)
![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.
This is a Javascript library that implements BLS (Boneh-Lynn-Shacham) signatures and supports signature aggregation.
| Version | Bls spec version |
| ------- | :--------------: |
| 0.3.x | initial version |
| 1.x.x | draft #6 |
| 2.x.x | draft #7 |
> [spec](https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/beacon-chain.md#bls-signatures)
> [test vectors](https://github.com/ethereum/eth2.0-spec-tests/tree/master/tests/bls)
## Usage
```bash
yarn add @chainsafe/bls
```
To use native bindings you must install peer dependency `@chainsafe/blst`
```bash
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.
```ts
import bls from "@chainsafe/bls";
(async () => {
// class-based interface
const secretKey = bls.SecretKey.fromKeygen();
const publicKey = secretKey.toPublicKey();
const message = new Uint8Array(32);
const signature = secretKey.sign(message);
console.log("Is valid: ", signature.verify(publicKey, message));
// 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));
})();
```
### Browser
If you are in the browser, import from `/herumi` to explicitly import the WASM version
```ts
import bls from "@chainsafe/bls/herumi";
```
### 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
```bash
yarn add @chainsafe/bls @chainsafe/blst
```
```ts
import bls from "@chainsafe/bls/blst-native";
```
### Get implementation at runtime
If you need to get a bls implementation at runtime, import from `/getImplementation`.
```ts
import {getImplementation} from "@chainsafe/bls/getImplementation";
const bls = await getImplementation("herumi");
```
### 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";
// 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
```
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)
- `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) |
\* `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
## Spec versioning
| Version | Bls spec hash-to-curve version |
| ------- | :----------------------------: |
| 5.x.x | draft #9 |
| 2.x.x | draft #7 |
| 1.x.x | draft #6 |
| 0.3.x | initial version |
> [spec](https://github.com/ethereum/eth2.0-specs/blob/v1.0.0/specs/phase0/beacon-chain.md#bls-signatures)
> [test vectors](https://github.com/ethereum/eth2.0-spec-tests/tree/master/tests/bls)
- `yarn add @chainsafe/bls`
## License

View File

@ -1,84 +0,0 @@
import {runBenchmark} from "./runner.js";
import {runForAllImplementations} from "../test/switch.js";
import {PublicKey, Signature} from "../src/types.js";
import {aggCount} from "./params.js";
(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({
id: `${implementation} verify`,
prepareTest: () => ({pk, msg, sig}),
testRunner: ({pk, msg, sig}) => sig.verify(pk, msg),
});
// Fast aggregate
await runBenchmark({
id: `${implementation} verifyAggregate (${aggCount})`,
prepareTest: () => ({pks: sameMessage.map((s) => s.pk), msg: msgSame, sig: sameMessageSig}),
testRunner: ({pks, msg, sig}) => sig.verifyAggregate(pks, msg),
});
// Verify multiple
await runBenchmark({
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),
});
// Verify multiple signatures
await runBenchmark({
id: `${implementation} verifyMultipleSignatures (${aggCount})`,
prepareTest: () => diffMessage,
testRunner: (sets) =>
bls.Signature.verifyMultipleSignatures(sets.map((s) => ({publicKey: s.pk, message: s.msg, signature: s.sig}))),
});
// Aggregate pubkeys
await runBenchmark({
id: `${implementation} aggregate pubkeys (${aggCount})`,
prepareTest: () => diffMessage.map((s) => s.pk),
testRunner: (pks) => bls.PublicKey.aggregate(pks),
});
// Aggregate sigs
await runBenchmark({
id: `${implementation} aggregate signatures (${aggCount})`,
prepareTest: () => diffMessage.map((s) => s.sig),
testRunner: (sigs) => bls.Signature.aggregate(sigs),
});
// Sign
await runBenchmark({
id: `${implementation} sign`,
prepareTest: () => ({sk: bls.SecretKey.fromKeygen(), msg: msgSame}),
testRunner: ({sk, msg}) => sk.sign(msg),
});
});
})();

View File

@ -1,109 +0,0 @@
import {runBenchmark} from "./runner.js";
import {range, randomMessage} from "../test/util.js";
import {generateRandomSecretKey} from "@chainsafe/bls-keygen";
import * as noble from "noble-bls12-381";
import {aggCount, runsNoble} from "./params.js";
(async function () {
{
// 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: 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,
});
}
{
// 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),
runs: runsNoble,
});
const sk = generateRandomSecretKey();
const msg = await noble.PointG2.hashToCurve(randomMessage());
await runBenchmark({
id: `noble sign`,
prepareTest: () => ({sk, msg}),
testRunner: async ({sk, msg}) => await noble.sign(msg, sk),
runs: runsNoble,
});
})();

View File

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

View File

@ -1,3 +0,0 @@
export const aggCount = 30;
export const runs = 100;
export const runsNoble = 10;

View File

@ -1,35 +0,0 @@
type PromiseOptional<T> = T | Promise<T>;
export async function runBenchmark<T, R>({
prepareTest,
testRunner,
runs = 100,
id,
}: {
prepareTest: (i: number) => PromiseOptional<T>;
testRunner: (input: T) => PromiseOptional<R>;
runs?: number;
id: string;
}): Promise<void> {
const diffsNanoSec: bigint[] = [];
for (let i = 0; i < runs; i++) {
const input = await prepareTest(i);
const start = process.hrtime.bigint();
const result = await testRunner(input);
const end = process.hrtime.bigint();
diffsNanoSec.push(end - start);
}
const average = averageBigint(diffsNanoSec);
const opsPerSec = 1e9 / Number(average);
// eslint-disable-next-line no-console
console.log(`${id}: ${opsPerSec.toPrecision(5)} ops/sec (${runs} runs)`); // ±1.74%
}
function averageBigint(arr: bigint[]): bigint {
const total = arr.reduce((total, value) => total + value);
return total / BigInt(arr.length);
}

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

@ -1,8 +0,0 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# 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==

View File

@ -1,34 +0,0 @@
// eslint-disable-next-line @typescript-eslint/no-require-imports
const webpackConfig = require("./webpack.config.cjs");
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",
],
exclude: [],
preprocessors: {
"test/**/*.ts": ["webpack"],
},
webpack: {
mode: "production",
module: webpackConfig.module,
resolve: webpackConfig.resolve,
experiments: webpackConfig.experiments,
optimization: webpackConfig.optimization,
stats: {warnings:false},
},
reporters: ["spec"],
browsers: ["ChromeHeadless"],
singleRun: true,
});
};

26
karma.conf.js Normal file
View File

@ -0,0 +1,26 @@
// eslint-disable-next-line @typescript-eslint/no-require-imports
const webpackConfig = require("./webpack.config");
module.exports = function(config) {
config.set({
basePath: "",
frameworks: ["mocha", "chai"],
files: ["test/unit/*.ts"],
exclude: [],
preprocessors: {
"test/**/*.ts": ["webpack"]
},
webpack: {
mode: "production",
node: webpackConfig.node,
module: webpackConfig.module,
resolve: webpackConfig.resolve
},
reporters: ["spec"],
browsers: ["ChromeHeadless"],
singleRun: true
});
};

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 = {}));

View File

@ -1,58 +1,16 @@
{
"name": "@chainsafe/bls",
"version": "7.1.1",
"version": "4.0.0",
"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",
"homepage": "https://github.com/chainsafe/bls",
"author": "ChainSafe Systems",
"license": "Apache-2.0",
"files": [
"lib/**/*.js",
"lib/**/*.js.map",
"lib/**/*.d.ts",
"*.d.ts",
"*.js"
"lib/**/*.d.ts"
],
"keywords": [
"ethereum",
@ -62,63 +20,66 @@
],
"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:unit": "karma start",
"test:node:unit": "nyc --cache-dir .nyc_output/.cache -r lcov -e .ts mocha --colors -r ts-node/register 'test/unit/**/*.test.ts' && nyc report",
"test:unit": "yarn run test:node:unit && yarn run test:web:unit",
"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",
"coverage": "codecov -F bls",
"benchmark": "ts-node-esm benchmark",
"benchmark:all": "cd benchmark && yarn install && yarn benchmark:all"
"benchmark": "node -r ./.babel-register test/benchmarks"
},
"dependencies": {
"@chainsafe/bls-keygen": "^0.4.0",
"bls-eth-wasm": "^0.4.8",
"randombytes": "^2.1.0"
"@chainsafe/bls-keygen": "^0.3.0",
"bls-eth-wasm": "^0.4.1"
},
"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/as-sha256": "0.2.0",
"@chainsafe/eth2-spec-tests": "0.12.0",
"@chainsafe/lodestar-spec-test-util": "^0.11.0",
"@types/chai": "^4.2.9",
"@types/mocha": "^10.0.0",
"@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",
"@types/mocha": "^8.0.4",
"@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"
}
}

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,50 +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 {SecretKey as ISecretKey} from "../types.js";
import {PublicKey} from "./publicKey.js";
import {Signature} from "./signature.js";
import {ZeroSecretKeyError} from "../errors.js";
export class SecretKey implements ISecretKey {
readonly value: blst.SecretKey;
constructor(value: blst.SecretKey) {
this.value = value;
}
static fromBytes(bytes: Uint8Array): SecretKey {
// draft-irtf-cfrg-bls-signature-04 does not allow SK == 0
if (isZeroUint8Array(bytes)) {
throw new ZeroSecretKeyError();
}
const sk = blst.SecretKey.fromBytes(bytes);
return new SecretKey(sk);
}
static fromHex(hex: string): SecretKey {
return this.fromBytes(hexToBytes(hex));
}
static fromKeygen(entropy?: Uint8Array): SecretKey {
const sk = blst.SecretKey.fromKeygen(entropy || randomBytes(SECRET_KEY_LENGTH));
return new SecretKey(sk);
}
sign(message: Uint8Array): Signature {
return new Signature(this.value.sign(message).value);
}
toPublicKey(): PublicKey {
const pk = this.value.toPublicKey();
return new PublicKey(pk.value);
}
toBytes(): Uint8Array {
return this.value.toBytes();
}
toHex(): string {
return bytesToHex(this.toBytes());
}
}

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);
}
}

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;

47
src/context.ts Normal file
View File

@ -0,0 +1,47 @@
import {herumiToIBls} from "./herumi";
import {IBls} from "./interface";
export type Backing = "herumi" | "blst-native" | "blst-wasm";
let contextBacking: Backing | undefined = undefined;
let context: IBls | undefined = undefined;
//to maintain api compatible, add all backing context to return type
export async function init(backing: Backing = "herumi"): Promise<IBls> {
if (!context) {
switch (backing) {
case "herumi":
{
context = await herumiToIBls();
contextBacking = backing;
}
break;
default:
throw new Error(`Unsupported backing - ${backing}`);
}
}
await context.init();
return context;
}
export function destroy(): void {
if (context) {
context.destroy();
}
context = undefined;
contextBacking = undefined;
}
export function getContext(): IBls {
if (!context) {
throw new Error("BLS not initialized");
}
return context;
}
export function getContextBacking(): Backing {
if (!context) {
throw new Error("BLS not initialized");
}
return contextBacking;
}

View File

@ -1,45 +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: string) {
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: string, length: number) {
super(`INVALID_LENGTH: ${arg} - ${length} bytes`);
}
}

View File

@ -1,157 +0,0 @@
import {IBls} from "./types.js";
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,
}: Pick<IBls, "SecretKey" | "PublicKey" | "Signature">) {
/**
* Signs given message using secret key.
* @param secretKey
* @param message
*/
function sign(secretKey: Uint8Array, message: Uint8Array): Uint8Array {
validateBytes(secretKey, "secretKey");
validateBytes(message, "message");
return SecretKey.fromBytes(secretKey).sign(message).toBytes();
}
/**
* Compines all given signature into one.
* @param signatures
*/
function aggregateSignatures(signatures: Uint8Array[]): Uint8Array {
const agg = Signature.aggregate(signatures.map((p) => Signature.fromBytes(p)));
return agg.toBytes();
}
/**
* Combines all given public keys into single one
* @param publicKeys
*/
function aggregatePublicKeys(publicKeys: Uint8Array[]): Uint8Array {
const agg = PublicKey.aggregate(publicKeys.map((p) => PublicKey.fromBytes(p)));
return agg.toBytes();
}
/**
* Verifies if signature is message signed with given public key.
* @param publicKey
* @param message
* @param signature
*/
function verify(publicKey: Uint8Array, message: Uint8Array, signature: Uint8Array): boolean {
validateBytes(publicKey, "publicKey");
validateBytes(message, "message");
validateBytes(signature, "signature");
try {
return Signature.fromBytes(signature).verify(PublicKey.fromBytes(publicKey), message);
} catch (e) {
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: Uint8Array[], message: Uint8Array, signature: Uint8Array): boolean {
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: Uint8Array[], messages: Uint8Array[], signature: Uint8Array): boolean {
validateBytes(publicKeys, "publicKey");
validateBytes(messages, "message");
validateBytes(signature, "signature");
if (publicKeys.length === 0 || publicKeys.length != messages.length) {
return false;
}
try {
return Signature.fromBytes(signature).verifyMultiple(
publicKeys.map((publicKey) => PublicKey.fromBytes(publicKey)),
messages.map((msg) => msg)
);
} catch (e) {
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: {publicKey: Uint8Array; message: Uint8Array; signature: Uint8Array}[]
): boolean {
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: Uint8Array): Uint8Array {
validateBytes(secretKey, "secretKey");
return SecretKey.fromBytes(secretKey).toPublicKey().toBytes();
}
return {
sign,
aggregateSignatures,
aggregatePublicKeys,
verify,
verifyAggregate,
verifyMultiple,
verifyMultipleSignatures,
secretKeyToPublicKey,
};
}

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,37 +0,0 @@
/**
* Browser compatible fromHex method
* From https://github.com/herumi/bls-eth-wasm/blob/04eedb77aa96e66b4f65a0ab477228adf8090c36/src/bls.js#L62
*/
export function hexToBytes(hex: string): Uint8Array {
if (hex.startsWith("0x")) {
hex = hex.slice(2);
}
if (hex.length & 1) {
throw Error("hexToBytes:length must be even " + hex.length);
}
const n = hex.length / 2;
const a = new Uint8Array(n);
for (let i = 0; i < n; i++) {
a[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
}
return a;
}
/**
* Browser compatible toHex method
* From https://github.com/herumi/bls-eth-wasm/blob/04eedb77aa96e66b4f65a0ab477228adf8090c36/src/bls.js#L50
*/
export function bytesToHex(bytes: Uint8Array): string {
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 +1 @@
export * from "./hex.js";
export * from "./utils.js";
export * from "./utils";

View File

@ -1,36 +1,10 @@
import randomBytes from "randombytes";
import {PUBLIC_KEY_LENGTH, SIGNATURE_LENGTH} from "../constants";
// 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: Uint8Array | Uint8Array[] | null,
argName?: string
): asserts bytes is NonNullable<typeof bytes> {
for (const item of Array.isArray(bytes) ? bytes : [bytes]) {
if (item == null) {
throw Error(`${argName || "bytes"} is null or undefined`);
}
export function assert(condition: unknown, message = "Assertion failed"): asserts condition {
if (!condition) {
throw new Error(message);
}
}
export function isZeroUint8Array(bytes: Uint8Array): boolean {
return bytes.every((byte) => byte === 0);
}
export function concatUint8Arrays(bytesArr: Uint8Array[]): Uint8Array {
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;
}
export const EMPTY_PUBLIC_KEY = Buffer.alloc(PUBLIC_KEY_LENGTH);
export const EMPTY_SIGNATURE = Buffer.alloc(SIGNATURE_LENGTH);

33
src/herumi/adapter.ts Normal file
View File

@ -0,0 +1,33 @@
import herumi from "bls-eth-wasm";
import {init as initHerumi, destroy, getContext} from "./context";
import {IBls, IPrivateKeyValue, IPublicKeyValue, ISignatureValue} from "../interface";
export async function herumiToIBls(): Promise<IBls> {
await initHerumi();
const context: IBls = {
SecretKey: getContext().SecretKey as IPrivateKeyValue,
PublicKey: getContext().PublicKey as IPublicKeyValue,
Signature: getContext().Signature as ISignatureValue,
toHex: herumi.toHex,
toHexStr: herumi.toHexStr,
fromHexStr: herumi.fromHexStr,
getCurveOrder: herumi.getCurveOrder,
getFieldOrder: herumi.getFieldOrder,
verifySignatureOrder: herumi.verifySignatureOrder,
verifyPublicKeyOrder: herumi.verifyPublicKeyOrder,
areAllMsgDifferent: herumi.areAllMsgDifferent,
shouldVerifyBlsSignatureOrder: herumi.shouldVerifyBlsSignatureOrder,
shouldVerifyBlsPublicKeyOrder: herumi.shouldVerifyBlsPublicKeyOrder,
deserializeHexStrToSecretKey: herumi.deserializeHexStrToSecretKey as IBls["deserializeHexStrToSecretKey"],
deserializeHexStrToPublicKey: herumi.deserializeHexStrToPublicKey as IBls["deserializeHexStrToPublicKey"],
deserializeHexStrToSignature: herumi.deserializeHexStrToSignature as IBls["deserializeHexStrToSignature"],
init: async () => {
return context;
},
destroy: () => {
destroy();
},
};
return context;
}

View File

@ -1,36 +1,20 @@
/* eslint-disable require-atomic-updates */
import bls from "bls-eth-wasm";
import {NotInitializedError} from "../errors.js";
type Bls = typeof bls;
let blsGlobal: Bls | null = null;
let blsGlobalPromise: Promise<void> | null = null;
let blsGlobalPromise: Promise<Bls> | 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> {
export async function setupBls(): Promise<Bls> {
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);
}
await bls.init();
blsGlobal = bls;
}
return blsGlobal;
}
// Cache a promise for Bls instead of Bls to make sure it is initialized only once
export async function init(): Promise<void> {
export async function init(): Promise<Bls> {
if (!blsGlobalPromise) {
blsGlobalPromise = setupBls();
}
@ -44,7 +28,7 @@ export function destroy(): void {
export function getContext(): Bls {
if (!blsGlobal) {
throw new NotInitializedError("herumi");
throw new Error("BLS not initialized");
}
return blsGlobal;
}

View File

@ -1,23 +1 @@
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";
export {SecretKey, PublicKey, Signature, init, destroy};
export const bls = async (): Promise<IBls> => {
await init();
return {
implementation: "herumi",
SecretKey,
PublicKey,
Signature,
...functionalInterfaceFactory({SecretKey, PublicKey, Signature}),
};
};
export default bls;
export {herumiToIBls} from "./adapter";

View File

@ -1,61 +0,0 @@
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";
export class PublicKey implements IPublicKey {
readonly value: PublicKeyType;
constructor(value: PublicKeyType) {
if (value.isZero()) {
throw new ZeroPublicKeyError();
}
this.value = value;
}
static fromBytes(bytes: Uint8Array): PublicKey {
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: string): PublicKey {
return this.fromBytes(hexToBytes(hex));
}
static aggregate(publicKeys: PublicKey[]): PublicKey {
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?: PointFormat): Uint8Array {
if (format === PointFormat.uncompressed) {
return this.value.serializeUncompressed();
} else {
return this.value.serialize();
}
}
toHex(format?: PointFormat): string {
return bytesToHex(this.toBytes(format));
}
}

View File

@ -1,57 +0,0 @@
import type {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";
export class SecretKey implements ISecretKey {
readonly value: SecretKeyType;
constructor(value: SecretKeyType) {
if (value.isZero()) {
throw new ZeroSecretKeyError();
}
this.value = value;
}
static fromBytes(bytes: Uint8Array): SecretKey {
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: string): SecretKey {
return this.fromBytes(hexToBytes(hex));
}
static fromKeygen(entropy?: Uint8Array): SecretKey {
const sk = generateRandomSecretKey(entropy);
return this.fromBytes(sk);
}
sign(message: Uint8Array): Signature {
return new Signature(this.value.sign(message));
}
toPublicKey(): PublicKey {
return new PublicKey(this.value.getPublicKey());
}
toBytes(): Uint8Array {
return this.value.serialize();
}
toHex(): string {
return bytesToHex(this.toBytes());
}
}

View File

@ -1,93 +0,0 @@
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";
export class Signature implements ISignature {
readonly value: SignatureType;
constructor(value: SignatureType) {
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: Uint8Array, _type?: CoordType, _validate = true): Signature {
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: string): Signature {
return this.fromBytes(hexToBytes(hex));
}
static aggregate(signatures: Signature[]): Signature {
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: {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)
);
}
verify(publicKey: PublicKey, message: Uint8Array): boolean {
return publicKey.value.verify(this.value, message);
}
verifyAggregate(publicKeys: PublicKey[], message: Uint8Array): boolean {
return this.value.fastAggregateVerify(
publicKeys.map((key) => key.value),
message
);
}
verifyMultiple(publicKeys: PublicKey[], messages: Uint8Array[]): boolean {
return this.value.aggregateVerifyNoCheck(
publicKeys.map((key) => key.value),
concatUint8Arrays(messages)
);
}
toBytes(format?: PointFormat): Uint8Array {
if (format === PointFormat.uncompressed) {
return this.value.serializeUncompressed();
} else {
return this.value.serialize();
}
}
toHex(format?: PointFormat): string {
return bytesToHex(this.toBytes(format));
}
}

View File

@ -1,19 +1,155 @@
import type {IBls} from "./types.js";
import {getImplementation} from "./getImplementation.js";
import {PUBLIC_KEY_LENGTH} from "./constants";
import {Keypair} from "./keypair";
import {PrivateKey} from "./privateKey";
import {PublicKey} from "./publicKey";
import {Signature} from "./signature";
import {assert} from "./helpers";
// 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 {Keypair, PrivateKey, PublicKey, Signature};
export const bls = async (): Promise<IBls> => {
let bls: IBls;
export {init as initBLS} from "./context";
try {
bls = await getImplementation(isNode ? "blst-native" : "herumi");
} catch (e) {
bls = await getImplementation("herumi");
function toBuffer(input: Uint8Array): Buffer {
return Buffer.from(input.buffer, input.byteOffset, input.length);
}
/**
* Generates new secret and public key
*/
export function generateKeyPair(): Keypair {
return Keypair.generate();
}
/**
* Generates public key from given secret.
* @param {BLSSecretKey} secretKey
*/
export function generatePublicKey(secretKey: Uint8Array): Buffer {
assert(secretKey, "secretKey is null or undefined");
const keypair = new Keypair(PrivateKey.fromBytes(toBuffer(secretKey)));
return keypair.publicKey.toBytesCompressed();
}
/**
* Signs given message using secret key.
* @param secretKey
* @param messageHash
*/
export function sign(secretKey: Uint8Array, messageHash: Uint8Array): Buffer {
assert(secretKey, "secretKey is null or undefined");
assert(messageHash, "messageHash is null or undefined");
const privateKey = PrivateKey.fromBytes(toBuffer(secretKey));
return privateKey.signMessage(toBuffer(messageHash)).toBytesCompressed();
}
/**
* Compines all given signature into one.
* @param signatures
*/
export function aggregateSignatures(signatures: Uint8Array[]): Buffer {
assert(signatures && signatures.length > 0, "signatures is null or undefined or empty array");
return Signature.aggregate(
signatures.map(
(signature): Signature => {
return Signature.fromCompressedBytes(signature);
}
)
).toBytesCompressed();
}
/**
* Combines all given public keys into single one
* @param publicKeys
*/
export function aggregatePubkeys(publicKeys: Uint8Array[]): Buffer {
assert(publicKeys, "publicKeys is null or undefined");
if (publicKeys.length === 0) {
return Buffer.alloc(PUBLIC_KEY_LENGTH);
}
return publicKeys
.map((p) => PublicKey.fromBytes(toBuffer(p)))
.reduce((agg, pubKey) => agg.add(pubKey))
.toBytesCompressed();
}
return bls;
/**
* Verifies if signature is message signed with given public key.
* @param publicKey
* @param messageHash
* @param signature
*/
export function verify(publicKey: Uint8Array, messageHash: Uint8Array, signature: Uint8Array): boolean {
assert(publicKey, "publicKey is null or undefined");
assert(messageHash, "messageHash is null or undefined");
assert(signature, "signature is null or undefined");
try {
return PublicKey.fromBytes(publicKey).verifyMessage(
Signature.fromCompressedBytes(toBuffer(signature)),
toBuffer(messageHash)
);
} catch (e) {
return false;
}
}
/**
* Verifies if aggregated signature is same message signed with given public keys.
* @param publicKeys
* @param messageHash
* @param signature
*/
export function verifyAggregate(publicKeys: Uint8Array[], messageHash: Uint8Array, signature: Uint8Array): boolean {
assert(publicKeys, "publicKey is null or undefined");
assert(messageHash, "messageHash is null or undefined");
assert(signature, "signature is null or undefined");
try {
return Signature.fromCompressedBytes(signature).verifyAggregate(
publicKeys.map((pubkey) => PublicKey.fromBytes(pubkey)),
messageHash
);
} catch (e) {
return false;
}
}
/**
* Verifies if signature is list of message signed with corresponding public key.
* @param publicKeys
* @param messageHashes
* @param signature
* @param fast Check if all messages are different
*/
export function verifyMultiple(
publicKeys: Uint8Array[],
messageHashes: Uint8Array[],
signature: Uint8Array,
fast = false
): boolean {
assert(publicKeys, "publicKey is null or undefined");
assert(messageHashes, "messageHash is null or undefined");
assert(signature, "signature is null or undefined");
if (publicKeys.length === 0 || publicKeys.length != messageHashes.length) {
return false;
}
try {
return Signature.fromCompressedBytes(toBuffer(signature)).verifyMultiple(
publicKeys.map((key) => PublicKey.fromBytes(toBuffer(key))),
messageHashes.map((m) => toBuffer(m)),
fast
);
} catch (e) {
return false;
}
}
export default {
generateKeyPair,
generatePublicKey,
sign,
aggregateSignatures,
aggregatePubkeys,
verify,
verifyAggregate,
verifyMultiple,
};
export default bls;

85
src/interface.ts Normal file
View File

@ -0,0 +1,85 @@
interface ICommon {
new (): this;
deserializeHexStr(s: string): void;
serializeToHexStr(): string;
isEqual(rhs: this): boolean;
deserialize(v: Uint8Array): void;
serialize(): Uint8Array;
add(rhs: this): void;
dump(msg?: string): string;
clear(): void;
clone(): this;
}
export interface IPrivateKeyValue extends ICommon {
setInt(x: number): void;
setHashOf(a: Uint8Array): void;
setLittleEndian(a: Uint8Array): void;
setByCSPRNG(): void;
getPublicKey(): IPublicKeyValue;
sign(m: string | Uint8Array): ISignatureValue;
}
export interface IPublicKeyValue extends ICommon {
verify(signature: ISignatureValue, m: Uint8Array | string): boolean;
isValidOrder(): boolean;
deserializeUncompressed(s: Uint8Array): void;
serializeUncompressed(): Uint8Array;
deserializeUncompressedHexStr(s: string): void;
serializeUncompressedToHexStr(): string;
}
export interface ISignatureValue extends ICommon {
deserializeUncompressed(s: Uint8Array): void;
serializeUncompressed(): Uint8Array;
deserializeUncompressedHexStr(s: string): void;
serializeUncompressedToHexStr(): string;
isValidOrder(): boolean;
aggregate(others: ISignatureValue[]): boolean;
aggregateVerifyNoCheck(publicKeys: IPublicKeyValue[], messages: Uint8Array): boolean;
fastAggregateVerify(publicKeys: IPublicKeyValue[], message: Uint8Array): boolean;
}
export interface IBls {
//property names are like that for api compatibility
SecretKey: InstanceType<IPrivateKeyValue>;
PublicKey: InstanceType<IPublicKeyValue>;
Signature: InstanceType<ISignatureValue>;
toHex(a: Uint8Array, start: number, length: number): string;
toHexStr(a: Uint8Array): string;
fromHexStr(s: string): Uint8Array;
getCurveOrder(): string;
getFieldOrder(): string;
verifySignatureOrder(doVerify: boolean): void;
verifyPublicKeyOrder(doVerify: boolean): void;
/**
*
* @param msgs single array with concatenated messages
* @param msgSize defaults to MSG_SIZE
*/
areAllMsgDifferent(msgs: Uint8Array, msgSize?: number): boolean;
shouldVerifyBlsSignatureOrder(b: string): void;
shouldVerifyBlsPublicKeyOrder(b: string): void;
deserializeHexStrToSecretKey(s: string): IPrivateKeyValue;
deserializeHexStrToPublicKey(s: string): IPublicKeyValue;
deserializeHexStrToSignature(s: string): ISignatureValue;
init(): Promise<this>;
destroy(): void;
}

29
src/keypair.ts Normal file
View File

@ -0,0 +1,29 @@
import {PublicKey} from "./publicKey";
import {PrivateKey} from "./privateKey";
export class Keypair {
private readonly _publicKey: PublicKey;
private readonly _privateKey: PrivateKey;
public constructor(privateKey: PrivateKey, publicKey?: PublicKey) {
this._privateKey = privateKey;
if (!publicKey) {
this._publicKey = PublicKey.fromPrivateKey(this._privateKey);
} else {
this._publicKey = publicKey;
}
}
public get publicKey(): PublicKey {
return this._publicKey;
}
public get privateKey(): PrivateKey {
return this._privateKey;
}
public static generate(): Keypair {
return new Keypair(PrivateKey.random());
}
}

67
src/privateKey.ts Normal file
View File

@ -0,0 +1,67 @@
import {SECRET_KEY_LENGTH} from "./constants";
import {assert} from "./helpers";
import {generateRandomSecretKey} from "@chainsafe/bls-keygen";
import {IPrivateKeyValue} from "./interface";
import {getContext} from "./context";
import {PublicKey, Signature} from ".";
export class PrivateKey {
private value: IPrivateKeyValue;
protected constructor(value: IPrivateKeyValue) {
this.value = value;
}
public static fromBytes(bytes: Uint8Array): PrivateKey {
assert(bytes.length === SECRET_KEY_LENGTH, "Private key should have 32 bytes");
const context = getContext();
const secretKey = new context.SecretKey();
secretKey.deserialize(Buffer.from(bytes));
return new PrivateKey(secretKey);
}
public static fromHexString(value: string): PrivateKey {
value = value.replace("0x", "");
assert(value.length === SECRET_KEY_LENGTH * 2, "secret key must have 32 bytes");
const context = getContext();
const secretKeyValue = new context.SecretKey();
secretKeyValue.deserializeHexStr(value);
return new PrivateKey(secretKeyValue);
}
public static fromInt(num: number): PrivateKey {
const context = getContext();
const secretKey = new context.SecretKey();
secretKey.setInt(num);
return new PrivateKey(secretKey);
}
public static random(): PrivateKey {
const randomKey: Buffer = generateRandomSecretKey();
return this.fromBytes(randomKey);
}
public getValue(): IPrivateKeyValue {
return this.value;
}
// public sign(message: Uint8Array): Signature {
// return Signature.fromValue(this.value.sign(message));
// }
public signMessage(message: Uint8Array): Signature {
return Signature.fromValue(this.value.sign(message));
}
public toPublicKey(): PublicKey {
return PublicKey.fromPublicKeyType(this.value.getPublicKey());
}
public toBytes(): Buffer {
return Buffer.from(this.value.serialize());
}
public toHexString(): string {
return `0x${this.value.serializeToHexStr()}`;
}
}

63
src/publicKey.ts Normal file
View File

@ -0,0 +1,63 @@
import {PrivateKey} from "./privateKey";
import {PUBLIC_KEY_LENGTH} from "./constants";
import {assert} from "./helpers";
import {Signature} from "./signature";
import {EMPTY_PUBLIC_KEY} from "./helpers/utils";
import {IPublicKeyValue} from "./interface";
import {getContext} from "./context";
export class PublicKey {
private value: IPublicKeyValue;
protected constructor(value: IPublicKeyValue) {
this.value = value;
}
public static fromPrivateKey(privateKey: PrivateKey): PublicKey {
return privateKey.toPublicKey();
}
public static fromBytes(bytes: Uint8Array): PublicKey {
const context = getContext();
const publicKey = new context.PublicKey();
if (!EMPTY_PUBLIC_KEY.equals(bytes)) {
publicKey.deserialize(bytes);
}
return new PublicKey(publicKey);
}
public static fromHex(value: string): PublicKey {
value = value.replace("0x", "");
assert(value.length === PUBLIC_KEY_LENGTH * 2);
const context = getContext();
const pubkeyValue = new context.PublicKey();
pubkeyValue.deserializeHexStr(value);
return new PublicKey(pubkeyValue);
}
public static fromPublicKeyType(value: IPublicKeyValue): PublicKey {
return new PublicKey(value);
}
public add(other: PublicKey): PublicKey {
const agg = new PublicKey(this.value.clone());
agg.value.add(other.value);
return agg;
}
public verifyMessage(signature: Signature, messageHash: Uint8Array): boolean {
return this.value.verify(signature.getValue(), messageHash);
}
public toBytesCompressed(): Buffer {
return Buffer.from(this.value.serialize());
}
public toHexString(): string {
return `0x${this.toBytesCompressed().toString("hex")}`;
}
public getValue(): IPublicKeyValue {
return this.value;
}
}

72
src/signature.ts Normal file
View File

@ -0,0 +1,72 @@
import {assert} from "./helpers";
import {FP_POINT_LENGTH} from "./constants";
import {getContext} from "./context";
import {EMPTY_SIGNATURE} from "./helpers/utils";
import {ISignatureValue} from "./interface";
import {PublicKey} from "./publicKey";
export class Signature {
private value: ISignatureValue;
protected constructor(value: ISignatureValue) {
this.value = value;
assert(this.value.isValidOrder());
}
public static fromCompressedBytes(value: Uint8Array): Signature {
assert(value.length === 2 * FP_POINT_LENGTH, `Signature must have ${2 * FP_POINT_LENGTH} bytes`);
const context = getContext();
const signature = new context.Signature();
if (!EMPTY_SIGNATURE.equals(value)) {
signature.deserialize(value);
}
return new Signature(signature);
}
public static fromValue(signature: ISignatureValue): Signature {
return new Signature(signature);
}
public static aggregate(signatures: Signature[]): Signature {
const context = getContext();
const signature = new context.Signature();
signature.aggregate(signatures.map((sig) => sig.getValue()));
return new Signature(signature);
}
public add(other: Signature): Signature {
const agg = this.value.clone();
agg.add(other.value);
return new Signature(agg);
}
public getValue(): ISignatureValue {
return this.value;
}
public verifyAggregate(publicKeys: PublicKey[], message: Uint8Array): boolean {
return this.value.fastAggregateVerify(
publicKeys.map((key) => key.getValue()),
message
);
}
public verifyMultiple(publicKeys: PublicKey[], messages: Uint8Array[], fast = false): boolean {
const msgs = Buffer.concat(messages);
if (!fast && !getContext().areAllMsgDifferent(msgs, messages[0].length)) {
return false;
}
return this.value.aggregateVerifyNoCheck(
publicKeys.map((key) => key.getValue()),
msgs
);
}
public toBytesCompressed(): Buffer {
return Buffer.from(this.value.serialize());
}
public toHex(): string {
return "0x" + this.value.serializeToHexStr();
}
}

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,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,32 +0,0 @@
/* eslint-disable max-len */
/**
* Data computed with lower level BLS primitives
* https://github.com/dapplion/eth2-bls-wasm/blob/2d2f3e6a0487e96706bfd8a1b8039c7d6c79f71f/verifyMultipleSignatures.test.js#L40
* It creates N valid signatures
* It messes with the last 2 signatures, modifying their values
* such that they wqould not fail in aggregate signature verification.
* Creates a random G2 point with is added to the last signature
* and substracted to the second to last signature
*/
export const maliciousVerifyMultipleSignaturesData = {
pks: [
"b836ccf44fa01e46745ccc3a47855e959783ef5df5cdcc607354b98d52c16b6613761339bfb833fd525cdca7c8071c6b",
"a317ce36dcf2bf6fd262dbad80427f890bc166152682cb6c600a66eb7d525f200839ab798ca4877c3143a31201905de4",
"b9b7b4f4a88d98f34b4c9ba8ae10e935ba51164ddc045d6ae26b403c87a6934e6c75f9fb5cc4b3b29a1255b316d08de5",
"a386a2bc7e9d13cf9b4ad3c819547534c768aeae6a2414bfcebee50f38aaf85a9d610974db931278c08fe86a91eb2999",
],
msgs: [
"690a91fc0a7a49bbc5afe9516c1831ca8845f281ef2e414f7dfeb71b5e91a902",
"3829d4fc2332afc2634079823b89598f3674be5da324b1092b3d8aeb7af5e164",
"9a9406647ed6af16b5ce3e828c5f5ef35f1221ed10476209476c12776ce417ac",
"3e8e4bcb78fda59a43ebfb90970cc6036ce18dc3d3a1b714cc4c1bfc00b8258e",
],
sigs: [
"864ed65f224cf4e49e9bbf313d3dc243649885d9bd432a15e6c1259f2e4c29fcefa7a4c3aafaac01519f7c92239702d7096df2971b1801cd26d0ca0d5e7743ccb0abe79d8c383f9bb04ebe553a3094e84d55bc79be7eff5ffdb9b322205acfd1",
"90efd8c82c356956fc170bec2aed874d14cea079625dfe69d8bc375e10fcd96e2c0348dfeb713f1889629ccb9ec95fee0e0c9cc7a728d8a7068701a04192ed585ec761edf6e2c1e44ceaaa61732052af81a6033fa7d375d7f7157909549322da",
"9023f43cc8e05a3e842b242b9f6781a9e2eadbfcbebd1242563e56bb47cd273ef20fc0c5099e05e83093581907bfd02915b5ef8c553918d4524c274a8856950c87c6314a2c003a2ed28e5fb56ddfdb233a2b895c2397bd15629325d95ca43b83",
"82c8fedc6ad43e945bbf7529d55b73d7ce593bc9ea94dfaf91d720b2ab0e51ce551f7fcda96d428b627ff776c94d6f360af425fe7fb4e4469b893071149db747f27a8bd488af7ba7f0edf86c7e551af89d7a55d4fc86968e10f91ed76e68e373",
],
manipulated: [false, false, true, true],
};

View File

@ -1,16 +0,0 @@
import {downloadTests} from "@chainsafe/lodestar-spec-test-util";
import {SPEC_TEST_VERSION, SPEC_TESTS_DIR, SPEC_TEST_TO_DOWNLOAD} from "./params.js";
/* eslint-disable no-console */
downloadTests(
{
specVersion: SPEC_TEST_VERSION,
outputDir: SPEC_TESTS_DIR,
testsToDownload: SPEC_TEST_TO_DOWNLOAD,
},
console.log
).catch((e) => {
console.error(e);
process.exit(1);
});

View File

@ -1,8 +0,0 @@
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_TESTS_DIR = path.join(__dirname, "spec-tests");

View File

@ -1,34 +0,0 @@
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";
interface IAggregateSigsTestCase {
data: {
input: string[];
output: string;
};
}
describeForAllImplementations((bls) => {
describeDirectorySpecTest<IAggregateSigsTestCase, string | null>(
"bls/aggregate/small",
path.join(SPEC_TESTS_DIR, "tests/general/phase0/bls/aggregate/small"),
(testCase) => {
try {
const signatures = testCase.data.input;
const agg = bls.aggregateSignatures(signatures.map(hexToBytes));
return bytesToHex(agg);
} catch (e) {
if (e instanceof EmptyAggregateError) return null;
throw e;
}
},
{
inputTypes: {data: InputType.YAML},
getExpected: (testCase) => testCase.data.output,
}
);
});

View File

@ -0,0 +1,40 @@
import path from "path";
import bls, {initBLS} from "../../src";
import {describeDirectorySpecTest, InputType} from "@chainsafe/lodestar-spec-test-util";
interface IAggregateSigsTestCase {
data: {
input: string[];
output: string;
};
}
before(async function f() {
await initBLS();
});
describeDirectorySpecTest<IAggregateSigsTestCase, string>(
"BLS - aggregate sigs",
path.join(__dirname, "../../node_modules/@chainsafe/eth2-spec-tests/tests/general/phase0/bls/aggregate/small"),
(testCase) => {
try {
const result = bls.aggregateSignatures(
testCase.data.input.map((pubKey) => {
return Buffer.from(pubKey.replace("0x", ""), "hex");
})
);
return `0x${result.toString("hex")}`;
} catch (e) {
if (e.message === "signatures is null or undefined or empty array") {
return null;
}
throw e;
}
},
{
inputTypes: {
data: InputType.YAML,
},
getExpected: (testCase) => testCase.data.output,
}
);

View File

@ -0,0 +1,42 @@
import path from "path";
import bls, {initBLS} from "../../src";
import {describeDirectorySpecTest, InputType} from "@chainsafe/lodestar-spec-test-util";
interface IAggregateSigsVerifyTestCase {
data: {
input: {
pubkeys: string[];
messages: string[];
signature: string;
};
output: boolean;
};
}
before(async function f() {
try {
await initBLS();
} catch (e) {
console.log(e);
}
});
describeDirectorySpecTest<IAggregateSigsVerifyTestCase, boolean>(
"BLS - aggregate sigs verify",
path.join(__dirname, "../../node_modules/@chainsafe/eth2-spec-tests/tests/general/phase0/bls/aggregate_verify/small"),
(testCase) => {
const pubkeys = testCase.data.input.pubkeys.map((pubkey) => {
return Buffer.from(pubkey.replace("0x", ""), "hex");
});
const messages = testCase.data.input.messages.map((msg) => {
return Buffer.from(msg.replace("0x", ""), "hex");
});
return bls.verifyMultiple(pubkeys, messages, Buffer.from(testCase.data.input.signature.replace("0x", ""), "hex"));
},
{
inputTypes: {
data: InputType.YAML,
},
getExpected: (testCase) => testCase.data.output,
}
);

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