Skip to content

Commit

Permalink
Return strings for strings, verifyBatch signature, fix tests and readme
Browse files Browse the repository at this point in the history
  • Loading branch information
paulmillr committed Jan 21, 2021
1 parent c25d620 commit b1acd48
Show file tree
Hide file tree
Showing 6 changed files with 119 additions and 67 deletions.
66 changes: 41 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,34 +36,47 @@ Node.js and browser:
> npm install noble-bls12-381
```js
import * as bls from "bls12-381";
const bls = require('noble-bls12-381');

// Use hex or Uint8Arrays
// You can use Uint8Array, or hex string for readability
const privateKey = '67d53f170b908cabb9eb326c3c337762d59289a8fec79f7bc9254b584b73265c';
const msg = 'hello';
const privateKeys = [
'18f020b98eb798752a50ed0563b079c125b0db5dd0b1060d1c1b47d4a193e1e4',
'ed69a8c50cf8c9836be3b67c7eeff416612d45ba39a5c099d48fa668bf558c9c',
'16ae669f3be7a2121e17d0c68c05a8f3d6bef21ec0f2315f1d7aec12484e4cf5'
];
const message = '64726e3da8';
const messages = ['d2', '0d98', '05caf3'];

(async () => {
const publicKey = bls.getPublicKey(privateKey);
const signature1 = await bls.sign(msg, privateKey);
const isCorrect1 = await bls.verify(msg, publicKey, signature);
const publicKeys = privateKeys.map(bls.getPublicKey);

const signature = await bls.sign(message, privateKey);
const isCorrect = await bls.verify(signature, message, publicKey);
console.log('key', publicKey);
console.log('signature', signature);
console.log('is correct:', isCorrect);

// Sign 1 msg with 3 keys
const privateKeys = [
'18f020b98eb798752a50ed0563b079c125b0db5dd0b1060d1c1b47d4a193e1e4',
'ed69a8c50cf8c9836be3b67c7eeff416612d45ba39a5c099d48fa668bf558c9c',
'16ae669f3be7a2121e17d0c68c05a8f3d6bef21ec0f2315f1d7aec12484e4cf5'
];
const publicKeys = privateKeys.map(bls.getPublicKey);
const signatures = await Promise.all(privateKeys.map(p => bls.sign(msg, p)));
const aggPubKey = bls.aggregatePublicKeys(publicKeys);
const aggSignature = bls.aggregateSignatures(signatures);
const isCorrect2 = await bls.verify(signature, msg, aggPubKey);
const signatures2 = await Promise.all(privateKeys.map(p => bls.sign(message, p)));
const aggPubKey2 = bls.aggregatePublicKeys(publicKeys);
const aggSignature2 = bls.aggregateSignatures(signatures2);
const isCorrect2 = await bls.verify(aggSignature2, message, aggPubKey2);
console.log();
console.log('signatures are', signatures2);
console.log('merged to aggregated signature', aggSignature2);
console.log('is correct:', isCorrect2);

// Sign 3 msgs with 3 keys
const messages = ['whatsup', 'all good', 'thanks'];
const signatures2 = await Promise.all(privateKeys.map((p, i) => bls.sign(messages[i], p)));
const aggSignature2 = bls.aggregateSignatures(signatures);
const isCorrect3 = await bls.verifyBatch(signature, messages, publicKeys);
const signatures3 = await Promise.all(privateKeys.map((p, i) => bls.sign(messages[i], p)));
const aggSignature3 = bls.aggregateSignatures(signatures3);
const isCorrect3 = await bls.verifyBatch(aggSignature3, messages, publicKeys);
console.log();
console.log('keys', publicKeys);
console.log('signatures', signatures3);
console.log('merged to aggregate signature', aggSignature3);
console.log('is correct:', isCorrect3);
})();
```

Expand All @@ -79,7 +92,8 @@ const msg = 'hello';

##### `getPublicKey(privateKey)`
```typescript
function getPublicKey(privateKey: Uint8Array | string | bigint): Uint8Array;
function getPublicKey(privateKey: Uint8Array | bigint): Uint8Array;
function getPublicKey(privateKey: string): string;
```
- `privateKey: Uint8Array | string | bigint` will be used to generate public key.
Public key is generated by executing scalar multiplication of a base Point(x, y) by a fixed
Expand Down Expand Up @@ -122,15 +136,17 @@ function verify(

##### `aggregatePublicKeys(publicKeys)`
```typescript
function aggregatePublicKeys(publicKeys: (Uint8Array | string)[]): Uint8Array;
function aggregatePublicKeys(publicKeys: Uint8Array[]): Uint8Array;
function aggregatePublicKeys(publicKeys: string[]): string;
function aggregatePublicKeys(publicKeys: PointG1[]): PointG1;
```
- `publicKeys: (Uint8Array | string | PointG1)[]` - e.g. that have been generated from `privateKey` by `getPublicKey`
- Returns `Uint8Array | PointG1`: one aggregated public key which calculated from public keys

##### `aggregateSignatures(signatures)`
```typescript
function aggregateSignatures(signatures: (Uint8Array | string)[]): Uint8Array;
function aggregateSignatures(signatures: Uint8Array[]): Uint8Array;
function aggregateSignatures(signatures: string[]): string;
function aggregateSignatures(signatures: PointG2[]): PointG2;
```
- `signatures: (Uint8Array | string | PointG2)[]` - e.g. that have been generated by `sign`
Expand All @@ -139,14 +155,14 @@ function aggregateSignatures(signatures: PointG2[]): PointG2;
##### `verifyBatch(hashes, publicKeys, signature)`
```typescript
function verifyBatch(
signature: Uint8Array | string | PointG2,
hashes: (Uint8Array | string | PointG2)[],
publicKeys: (Uint8Array | string | PointG1)[],
signature: Uint8Array | string | PointG2
publicKeys: (Uint8Array | string | PointG1)[]
): Promise<boolean>
```
- `signature: Uint8Array | string | PointG2` - object returned by the `aggregateSignatures` function
- `hashes: (Uint8Array | string | PointG2)[]` - messages hashes that needs to be verified
- `publicKeys: (Uint8Array | string | PointG1)[]` - e.g. that were generated from `privateKeys` by `getPublicKey`
- `signature: Uint8Array | string | PointG2` - object returned by the `aggregateSignatures` function
- Returns `Promise<boolean>`: `true` / `false` whether the signature matches hashes

##### `pairing(G1Point, G2Point)`
Expand Down
10 changes: 6 additions & 4 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,15 @@ export declare class PointG2 extends ProjectivePoint<Fq2> {
export declare function pairing(P: PointG1, Q: PointG2, withFinalExponent?: boolean): Fq12;
declare type PB1 = Bytes | PointG1;
declare type PB2 = Bytes | PointG2;
export declare function getPublicKey(privateKey: PrivateKey): Uint8Array;
export declare function getPublicKey(privateKey: PrivateKey): Uint8Array | string;
export declare function sign(message: Uint8Array, privateKey: PrivateKey): Promise<Uint8Array>;
export declare function sign(message: string, privateKey: PrivateKey): Promise<string>;
export declare function sign(message: PointG2, privateKey: PrivateKey): Promise<PointG2>;
export declare function verify(signature: PB2, message: PB2, publicKey: PB1): Promise<boolean>;
export declare function aggregatePublicKeys(publicKeys: Bytes[]): Uint8Array;
export declare function aggregatePublicKeys(publicKeys: Uint8Array[]): Uint8Array;
export declare function aggregatePublicKeys(publicKeys: string[]): string;
export declare function aggregatePublicKeys(publicKeys: PointG1[]): PointG1;
export declare function aggregateSignatures(signatures: Bytes[]): Uint8Array;
export declare function aggregateSignatures(signatures: Uint8Array[]): Uint8Array;
export declare function aggregateSignatures(signatures: string[]): string;
export declare function aggregateSignatures(signatures: PointG2[]): PointG2;
export declare function verifyBatch(messages: PB2[], publicKeys: PB1[], signature: PB2): Promise<boolean>;
export declare function verifyBatch(signature: PB2, messages: PB2[], publicKeys: PB1[]): Promise<boolean>;
36 changes: 29 additions & 7 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ function hexToNumberBE(hex) {
}
function bytesToNumberBE(bytes) {
if (typeof bytes === 'string') {
if (!(/^[a-fA-F0-9]*$/.test(bytes)))
throw new Error('expected hex string or Uint8Array');
return hexToNumberBE(bytes);
}
let value = 0n;
Expand Down Expand Up @@ -90,7 +92,7 @@ function hexToBytes(hexOrNum, padding = 0) {
const str = hex[i] + hex[i + 1];
const byte = byteMap[str];
if (byte == null)
throw new Error(`Expected hex string or Uint8Array, got ${hex}`);
throw new Error(`expected hex string or Uint8Array, got ${hex}`);
u8[j] = byte;
}
return padStart(u8, padding, 0);
Expand Down Expand Up @@ -194,6 +196,10 @@ class PointG1 extends math_1.ProjectivePoint {
super(x, y, z, math_1.Fq);
}
static fromCompressedHex(hex) {
if ((typeof hex === 'string' && hex.length !== 96) ||
(hex instanceof Uint8Array && hex.length !== 48)) {
throw new Error('invalid public key, expected 48 bytes / 96 hex chars');
}
const compressedValue = bytesToNumberBE(hex);
const bflag = math_1.mod(compressedValue, POW_2_383) / POW_2_382;
if (bflag === 1n) {
Expand Down Expand Up @@ -259,8 +265,13 @@ class PointG2 extends math_1.ProjectivePoint {
super(x, y, z, math_1.Fq2);
}
static async hashToCurve(msg) {
if (typeof msg === 'string')
if (typeof msg === 'string') {
if (!(/^[a-fA-F0-9]*$/.test(msg)))
throw new Error('expected hex string or Uint8Array');
msg = hexToBytes(msg);
}
if (!(msg instanceof Uint8Array))
throw new Error('expected hex string or Uint8Array');
const u = await hash_to_field(msg, 2);
const Q0 = new PointG2(...math_1.isogenyMapG2(math_1.map_to_curve_SSWU_G2(u[0])));
const Q1 = new PointG2(...math_1.isogenyMapG2(math_1.map_to_curve_SSWU_G2(u[1])));
Expand All @@ -271,7 +282,7 @@ class PointG2 extends math_1.ProjectivePoint {
static fromSignature(hex) {
const half = hex.length / 2;
if (half !== 48 && half !== 96)
throw new Error('Invalid compressed signature length, must be 48/96');
throw new Error('invalid compressed signature length, must be 96 or 192');
const z1 = bytesToNumberBE(hex.slice(0, half));
const z2 = bytesToNumberBE(hex.slice(half));
const bflag1 = math_1.mod(z1, POW_2_383) / POW_2_382;
Expand Down Expand Up @@ -354,7 +365,8 @@ async function normP2H(point) {
return point instanceof PointG2 ? point : await PointG2.hashToCurve(point);
}
function getPublicKey(privateKey) {
return PointG1.fromPrivateKey(privateKey).toCompressedHex();
const bytes = PointG1.fromPrivateKey(privateKey).toCompressedHex();
return typeof privateKey === 'string' ? bytesToHex(bytes) : bytes;
}
exports.getPublicKey = getPublicKey;
async function sign(message, privateKey) {
Expand Down Expand Up @@ -383,7 +395,12 @@ function aggregatePublicKeys(publicKeys) {
const agg = publicKeys
.map(normP1)
.reduce((sum, p) => sum.add(p), PointG1.ZERO);
return publicKeys[0] instanceof PointG1 ? agg : agg.toCompressedHex();
if (publicKeys[0] instanceof PointG1)
return agg;
const bytes = agg.toCompressedHex();
if (publicKeys[0] instanceof Uint8Array)
return bytes;
return bytesToHex(bytes);
}
exports.aggregatePublicKeys = aggregatePublicKeys;
function aggregateSignatures(signatures) {
Expand All @@ -392,10 +409,15 @@ function aggregateSignatures(signatures) {
const agg = signatures
.map(normP2)
.reduce((sum, s) => sum.add(s), PointG2.ZERO);
return signatures[0] instanceof PointG2 ? agg : agg.toSignature();
if (signatures[0] instanceof PointG2)
return agg;
const bytes = agg.toSignature();
if (signatures[0] instanceof Uint8Array)
return bytes;
return bytesToHex(bytes);
}
exports.aggregateSignatures = aggregateSignatures;
async function verifyBatch(messages, publicKeys, signature) {
async function verifyBatch(signature, messages, publicKeys) {
if (!messages.length)
throw new Error('Expected non-empty messages array');
if (publicKeys.length !== messages.length)
Expand Down
47 changes: 34 additions & 13 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ function hexToNumberBE(hex: string) {

function bytesToNumberBE(bytes: Bytes) {
if (typeof bytes === 'string') {
if (!(/^[a-fA-F0-9]*$/.test(bytes))) throw new Error('expected hex string or Uint8Array');
return hexToNumberBE(bytes);
}
let value = 0n;
Expand Down Expand Up @@ -110,7 +111,7 @@ function hexToBytes(hexOrNum: string | number | bigint, padding: number = 0): Ui
for (let i = 0, j = 0; i < len - 1; i += 2, j++) {
const str = hex[i] + hex[i + 1];
const byte = byteMap[str];
if (byte == null) throw new Error(`Expected hex string or Uint8Array, got ${hex}`);
if (byte == null) throw new Error(`expected hex string or Uint8Array, got ${hex}`);
u8[j] = byte;
}
return padStart(u8, padding, 0);
Expand Down Expand Up @@ -238,6 +239,13 @@ export class PointG1 extends ProjectivePoint<Fq> {
}

static fromCompressedHex(hex: Bytes) {
if (
(typeof hex === 'string' && hex.length !== 96) ||
(hex instanceof Uint8Array && hex.length !== 48)
) {
throw new Error('invalid public key, expected 48 bytes / 96 hex chars')
}

const compressedValue = bytesToNumberBE(hex);
const bflag = mod(compressedValue, POW_2_383) / POW_2_382;
if (bflag === 1n) {
Expand Down Expand Up @@ -315,7 +323,11 @@ export class PointG2 extends ProjectivePoint<Fq2> {

// https://tools.ietf.org/html/draft-irtf-cfrg-hash-to-curve-07#section-3
static async hashToCurve(msg: Bytes) {
if (typeof msg === 'string') msg = hexToBytes(msg);
if (typeof msg === 'string') {
if (!(/^[a-fA-F0-9]*$/.test(msg))) throw new Error('expected hex string or Uint8Array');
msg = hexToBytes(msg);
}
if (!(msg instanceof Uint8Array)) throw new Error('expected hex string or Uint8Array');
const u = await hash_to_field(msg, 2);
//console.log(`hash_to_curve(msg}) u0=${new Fq2(u[0])} u1=${new Fq2(u[1])}`);
const Q0 = new PointG2(...isogenyMapG2(map_to_curve_SSWU_G2(u[0])));
Expand All @@ -330,7 +342,7 @@ export class PointG2 extends ProjectivePoint<Fq2> {
static fromSignature(hex: Bytes): PointG2 {
const half = hex.length / 2;
if (half !== 48 && half !== 96)
throw new Error('Invalid compressed signature length, must be 48/96');
throw new Error('invalid compressed signature length, must be 96 or 192');
const z1 = bytesToNumberBE(hex.slice(0, half));
const z2 = bytesToNumberBE(hex.slice(half));
// indicates the infinity point
Expand Down Expand Up @@ -419,8 +431,9 @@ async function normP2H(point: PB2): Promise<PointG2> {
}

// P = pk x G
export function getPublicKey(privateKey: PrivateKey) {
return PointG1.fromPrivateKey(privateKey).toCompressedHex();
export function getPublicKey(privateKey: PrivateKey): Uint8Array | string {
const bytes = PointG1.fromPrivateKey(privateKey).toCompressedHex();
return typeof privateKey === 'string' ? bytesToHex(bytes) : bytes;
}

// S = pk x H(m)
Expand Down Expand Up @@ -450,32 +463,40 @@ export async function verify(signature: PB2, message: PB2, publicKey: PB1): Prom
}

// pk1 + pk2 + pk3 = pkA
export function aggregatePublicKeys(publicKeys: Bytes[]): Uint8Array;
export function aggregatePublicKeys(publicKeys: Uint8Array[]): Uint8Array;
export function aggregatePublicKeys(publicKeys: string[]): string;
export function aggregatePublicKeys(publicKeys: PointG1[]): PointG1;
export function aggregatePublicKeys(publicKeys: PB1[]): Uint8Array | PointG1 {
export function aggregatePublicKeys(publicKeys: PB1[]): Uint8Array | string | PointG1 {
if (!publicKeys.length) throw new Error('Expected non-empty array');
const agg = publicKeys
.map(normP1)
.reduce((sum, p) => sum.add(p), PointG1.ZERO);
return publicKeys[0] instanceof PointG1 ? agg : agg.toCompressedHex();
if (publicKeys[0] instanceof PointG1) return agg;
const bytes = agg.toCompressedHex();
if (publicKeys[0] instanceof Uint8Array) return bytes;
return bytesToHex(bytes);
}

// e(G, S) = e(G, SUM(n)(Si)) = MUL(n)(e(G, Si))
export function aggregateSignatures(signatures: Bytes[]): Uint8Array;
export function aggregateSignatures(signatures: Uint8Array[]): Uint8Array;
export function aggregateSignatures(signatures: string[]): string;
export function aggregateSignatures(signatures: PointG2[]): PointG2;
export function aggregateSignatures(signatures: PB2[]): Uint8Array | PointG2 {
export function aggregateSignatures(signatures: PB2[]): Uint8Array | string | PointG2 {
if (!signatures.length) throw new Error('Expected non-empty array');
const agg = signatures
.map(normP2)
.reduce((sum, s) => sum.add(s), PointG2.ZERO);
return signatures[0] instanceof PointG2 ? agg : agg.toSignature();
if (signatures[0] instanceof PointG2) return agg;
const bytes = agg.toSignature();
if (signatures[0] instanceof Uint8Array) return bytes;
return bytesToHex(bytes);
}

// ethresear.ch/t/fast-verification-of-multiple-bls-signatures/5407
export async function verifyBatch(
signature: PB2,
messages: PB2[],
publicKeys: PB1[],
signature: PB2
publicKeys: PB1[]
): Promise<boolean> {
if (!messages.length) throw new Error('Expected non-empty messages array');
if (publicKeys.length !== messages.length) throw new Error('Pubkey count should equal msg count');
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@
},
"license": "MIT",
"devDependencies": {
"@types/jest": "^25",
"@types/jest": "^26",
"@types/node": "^14",
"benchmark": "^2.1.4",
"fast-check": "^1.24",
"jest": "^25",
"jest": "^26.5.4",
"micro-bmark": "^0.1.2",
"prettier": "^2.1.2",
"ts-jest": "^25",
"ts-jest": "^26",
"typescript": "^4"
},
"keywords": [
Expand Down
Loading

0 comments on commit b1acd48

Please sign in to comment.