Merge pull request #85 from ChainSafe/dapplion/multi-threading

Enable multi-threading for all implementations
This commit is contained in:
Lion - dapplion 2021-04-05 20:54:20 +02:00 committed by GitHub
commit 0f895b4795
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 453 additions and 163 deletions

View File

@ -86,29 +86,25 @@ import {aggCount, runs} from "./params";
// Verify multiple signatures // Verify multiple signatures
await runBenchmark<{pks: PublicKey[]; msgs: Uint8Array[]; sigs: Signature[]}, boolean>({ await runBenchmark({
id: `${implementation} verifyMultipleSignatures (${aggCount})`, id: `${implementation} verifyMultipleSignatures (${aggCount})`,
prepareTest: () => { prepareTest: () => {
const dataArr = range(aggCount).map(() => { const sets = range(aggCount).map(() => {
const sk = bls.SecretKey.fromKeygen(); const sk = bls.SecretKey.fromKeygen();
const pk = sk.toPublicKey(); const pk = sk.toPublicKey();
const msg = randomMessage(); const msg = randomMessage();
const sig = sk.sign(msg); const sig = sk.sign(msg);
return {pk, msg, sig}; return {publicKey: pk, message: msg, signature: sig};
}); });
const pks = dataArr.map((data) => data.pk);
const msgs = dataArr.map((data) => data.msg);
const sigs = dataArr.map((data) => data.sig);
return { return {
input: {pks, msgs, sigs}, input: sets,
resultCheck: (valid) => valid === true, resultCheck: (valid) => valid === true,
}; };
}, },
testRunner: ({pks, msgs, sigs}) => { testRunner: (sets) => {
return bls.Signature.verifyMultipleSignatures(pks, msgs, sigs); return bls.Signature.verifyMultipleSignatures(sets);
}, },
runs, runs,
}); });

View File

@ -12,21 +12,17 @@ import {range, randomMessage} from "../test/util";
const pk = sk.toPublicKey(); const pk = sk.toPublicKey();
const msg = randomMessage(); const msg = randomMessage();
const sig = sk.sign(msg); const sig = sk.sign(msg);
return {pk, msg, sig}; return {publicKey: pk, message: msg, signature: sig};
}); });
const pks = dataArr.map((data) => data.pk);
const msgs = dataArr.map((data) => data.msg);
const sigs = dataArr.map((data) => data.sig);
const startMulti = process.hrtime.bigint(); const startMulti = process.hrtime.bigint();
bls.Signature.verifyMultipleSignatures(pks, msgs, sigs); bls.Signature.verifyMultipleSignatures(dataArr);
const endMulti = process.hrtime.bigint(); const endMulti = process.hrtime.bigint();
const diffMulti = endMulti - startMulti; const diffMulti = endMulti - startMulti;
const startSingle = process.hrtime.bigint(); const startSingle = process.hrtime.bigint();
for (const {pk, msg, sig} of dataArr) { for (const {publicKey, message, signature} of dataArr) {
sig.verify(pk, msg); signature.verify(publicKey, message);
} }
const endSingle = process.hrtime.bigint(); const endSingle = process.hrtime.bigint();
const diffSingle = endSingle - startSingle; const diffSingle = endSingle - startSingle;

View File

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

View File

@ -46,7 +46,7 @@
"randombytes": "^2.1.0" "randombytes": "^2.1.0"
}, },
"devDependencies": { "devDependencies": {
"@chainsafe/blst": "^0.1.6", "@chainsafe/blst": "^0.2.0",
"@chainsafe/lodestar-spec-test-util": "^0.18.0", "@chainsafe/lodestar-spec-test-util": "^0.18.0",
"@types/chai": "^4.2.9", "@types/chai": "^4.2.9",
"@types/mocha": "^8.0.4", "@types/mocha": "^8.0.4",
@ -67,6 +67,7 @@
"mocha": "^8.2.1", "mocha": "^8.2.1",
"nyc": "^15.0.0", "nyc": "^15.0.0",
"prettier": "^2.1.2", "prettier": "^2.1.2",
"threads": "^1.6.3",
"ts-loader": "^6.2.1", "ts-loader": "^6.2.1",
"ts-node": "^8.6.2", "ts-node": "^8.6.2",
"typescript": "^3.7.5", "typescript": "^3.7.5",
@ -78,6 +79,6 @@
"v8-profiler-next": "1.3.0" "v8-profiler-next": "1.3.0"
}, },
"peerDependencies": { "peerDependencies": {
"@chainsafe/blst": "^0.1.6" "@chainsafe/blst": "^0.2.0"
} }
} }

View File

@ -15,6 +15,7 @@ export function destroy(): void {
} }
export const bls: IBls = { export const bls: IBls = {
implementation: "blst-native",
SecretKey, SecretKey,
PublicKey, PublicKey,
Signature, Signature,

View File

@ -1,25 +1,21 @@
import * as blst from "@chainsafe/blst"; import * as blst from "@chainsafe/blst";
import {EmptyAggregateError, ZeroPublicKeyError} from "../errors"; import {EmptyAggregateError, ZeroPublicKeyError} from "../errors";
import {bytesToHex, hexToBytes} from "../helpers"; import {bytesToHex, hexToBytes} from "../helpers";
import {PublicKey as IPublicKey} from "../interface"; import {PointFormat, PublicKey as IPublicKey} from "../interface";
export class PublicKey implements IPublicKey { export class PublicKey extends blst.PublicKey implements IPublicKey {
readonly affine: blst.PublicKey; constructor(value: ConstructorParameters<typeof blst.PublicKey>[0]) {
readonly jacobian: blst.AggregatePublicKey; super(value);
constructor(affine: blst.PublicKey, jacobian: blst.AggregatePublicKey) {
this.affine = affine;
this.jacobian = jacobian;
} }
static fromBytes(bytes: Uint8Array): PublicKey { /** @param type Defaults to `CoordType.jacobian` */
const affine = blst.PublicKey.fromBytes(bytes); static fromBytes(bytes: Uint8Array, type?: blst.CoordType): PublicKey {
if (affine.value.is_inf()) { const pk = blst.PublicKey.fromBytes(bytes, type);
if (pk.value.is_inf()) {
throw new ZeroPublicKeyError(); throw new ZeroPublicKeyError();
} }
const jacobian = blst.AggregatePublicKey.fromPublicKey(affine); return new PublicKey(pk.value);
return new PublicKey(affine, jacobian);
} }
static fromHex(hex: string): PublicKey { static fromHex(hex: string): PublicKey {
@ -31,16 +27,19 @@ export class PublicKey implements IPublicKey {
throw new EmptyAggregateError(); throw new EmptyAggregateError();
} }
const jacobian = blst.aggregatePubkeys(publicKeys.map((pk) => pk.jacobian)); const pk = blst.aggregatePubkeys(publicKeys);
const affine = jacobian.toPublicKey(); return new PublicKey(pk.value);
return new PublicKey(affine, jacobian);
} }
toBytes(): Uint8Array { toBytes(format?: PointFormat): Uint8Array {
return this.affine.toBytes(); if (format === PointFormat.uncompressed) {
return this.value.serialize();
} else {
return this.value.compress();
}
} }
toHex(): string { toHex(format?: PointFormat): string {
return bytesToHex(this.toBytes()); return bytesToHex(this.toBytes(format));
} }
} }

View File

@ -8,7 +8,6 @@ import {ZeroSecretKeyError} from "../errors";
export class SecretKey implements ISecretKey { export class SecretKey implements ISecretKey {
readonly value: blst.SecretKey; readonly value: blst.SecretKey;
constructor(value: blst.SecretKey) { constructor(value: blst.SecretKey) {
this.value = value; this.value = value;
} }
@ -33,13 +32,12 @@ export class SecretKey implements ISecretKey {
} }
sign(message: Uint8Array): Signature { sign(message: Uint8Array): Signature {
return new Signature(this.value.sign(message)); return new Signature(this.value.sign(message).value);
} }
toPublicKey(): PublicKey { toPublicKey(): PublicKey {
const jacobian = this.value.toAggregatePublicKey(); const pk = this.value.toPublicKey();
const affine = jacobian.toPublicKey(); return new PublicKey(pk.value);
return new PublicKey(affine, jacobian);
} }
toBytes(): Uint8Array { toBytes(): Uint8Array {

View File

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

View File

@ -1,5 +1,5 @@
export const SECRET_KEY_LENGTH = 32; export const SECRET_KEY_LENGTH = 32;
export const SIGNATURE_LENGTH = 96; export const PUBLIC_KEY_LENGTH_COMPRESSED = 48;
export const FP_POINT_LENGTH = 48; export const PUBLIC_KEY_LENGTH_UNCOMPRESSED = 48 * 2;
export const PUBLIC_KEY_LENGTH = FP_POINT_LENGTH; export const SIGNATURE_LENGTH_COMPRESSED = 96;
export const G2_HASH_PADDING = 16; export const SIGNATURE_LENGTH_UNCOMPRESSED = 96 * 2;

View File

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

View File

@ -117,22 +117,17 @@ export function functionalInterfaceFactory({
* https://ethresear.ch/t/fast-verification-of-multiple-bls-signatures/5407 * https://ethresear.ch/t/fast-verification-of-multiple-bls-signatures/5407
*/ */
function verifyMultipleSignatures( function verifyMultipleSignatures(
publicKeys: Uint8Array[], sets: {publicKey: Uint8Array; message: Uint8Array; signature: Uint8Array}[]
messages: Uint8Array[],
signatures: Uint8Array[]
): boolean { ): boolean {
validateBytes(publicKeys, "publicKey"); if (!sets) throw Error("sets is null or undefined");
validateBytes(messages, "message");
validateBytes(signatures, "signatures");
if (publicKeys.length === 0 || publicKeys.length !== messages.length || publicKeys.length !== signatures.length) {
return false;
}
try { try {
return Signature.verifyMultipleSignatures( return Signature.verifyMultipleSignatures(
publicKeys.map((publicKey) => PublicKey.fromBytes(publicKey)), sets.map((s) => ({
messages.map((msg) => msg), publicKey: PublicKey.fromBytes(s.publicKey),
signatures.map((signature) => Signature.fromBytes(signature)) message: s.message,
signature: Signature.fromBytes(s.signature),
}))
); );
} catch (e) { } catch (e) {
if (e instanceof NotInitializedError) throw e; if (e instanceof NotInitializedError) throw e;

View File

@ -9,6 +9,7 @@ export * from "../constants";
export {SecretKey, PublicKey, Signature, init, destroy}; export {SecretKey, PublicKey, Signature, init, destroy};
export const bls: IBls = { export const bls: IBls = {
implementation: "herumi",
SecretKey, SecretKey,
PublicKey, PublicKey,
Signature, Signature,

View File

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

View File

@ -1,10 +1,10 @@
import {SIGNATURE_LENGTH} from "../constants";
import {SignatureType, multiVerify} from "bls-eth-wasm"; import {SignatureType, multiVerify} from "bls-eth-wasm";
import {getContext} from "./context"; import {getContext} from "./context";
import {PublicKey} from "./publicKey"; import {PublicKey} from "./publicKey";
import {bytesToHex, concatUint8Arrays, hexToBytes, isZeroUint8Array} from "../helpers"; import {bytesToHex, concatUint8Arrays, hexToBytes, isZeroUint8Array} from "../helpers";
import {Signature as ISignature} from "../interface"; import {PointFormat, Signature as ISignature} from "../interface";
import {EmptyAggregateError, InvalidLengthError, InvalidOrderError} from "../errors"; import {EmptyAggregateError, InvalidLengthError, InvalidOrderError} from "../errors";
import {SIGNATURE_LENGTH_COMPRESSED, SIGNATURE_LENGTH_UNCOMPRESSED} from "../constants";
export class Signature implements ISignature { export class Signature implements ISignature {
readonly value: SignatureType; readonly value: SignatureType;
@ -18,13 +18,16 @@ export class Signature implements ISignature {
} }
static fromBytes(bytes: Uint8Array): Signature { static fromBytes(bytes: Uint8Array): Signature {
if (bytes.length !== SIGNATURE_LENGTH) {
throw new InvalidLengthError("Signature", SIGNATURE_LENGTH);
}
const context = getContext(); const context = getContext();
const signature = new context.Signature(); const signature = new context.Signature();
if (!isZeroUint8Array(bytes)) { 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); signature.deserialize(bytes);
} }
return new Signature(signature); return new Signature(signature);
@ -45,11 +48,11 @@ export class Signature implements ISignature {
return new Signature(signature); return new Signature(signature);
} }
static verifyMultipleSignatures(publicKeys: PublicKey[], messages: Uint8Array[], signatures: Signature[]): boolean { static verifyMultipleSignatures(sets: {publicKey: PublicKey; message: Uint8Array; signature: Signature}[]): boolean {
return multiVerify( return multiVerify(
publicKeys.map((publicKey) => publicKey.value), sets.map((s) => s.publicKey.value),
signatures.map((signature) => signature.value), sets.map((s) => s.signature.value),
messages sets.map((s) => s.message)
); );
} }
@ -71,11 +74,15 @@ export class Signature implements ISignature {
); );
} }
toBytes(): Uint8Array { toBytes(format?: PointFormat): Uint8Array {
if (format === PointFormat.uncompressed) {
return this.value.serializeUncompressed();
} else {
return this.value.serialize(); return this.value.serialize();
} }
}
toHex(): string { toHex(format?: PointFormat): string {
return bytesToHex(this.toBytes()); return bytesToHex(this.toBytes(format));
} }
} }

View File

@ -1,20 +1,8 @@
export interface IBls { export interface IBls {
SecretKey: { implementation: Implementation;
fromBytes(bytes: Uint8Array): SecretKey; SecretKey: Omit<typeof SecretKey, "prototype">;
fromHex(hex: string): SecretKey; PublicKey: Omit<typeof PublicKey, "prototype">;
fromKeygen(ikm?: Uint8Array): SecretKey; Signature: Omit<typeof Signature, "prototype">;
};
PublicKey: {
fromBytes(bytes: Uint8Array): PublicKey;
fromHex(hex: string): PublicKey;
aggregate(publicKeys: PublicKey[]): PublicKey;
};
Signature: {
fromBytes(bytes: Uint8Array): Signature;
fromHex(hex: string): Signature;
aggregate(signatures: Signature[]): Signature;
verifyMultipleSignatures(publicKeys: PublicKey[], messages: Uint8Array[], signatures: Signature[]): boolean;
};
sign(secretKey: Uint8Array, message: Uint8Array): Uint8Array; sign(secretKey: Uint8Array, message: Uint8Array): Uint8Array;
aggregatePublicKeys(publicKeys: Uint8Array[]): Uint8Array; aggregatePublicKeys(publicKeys: Uint8Array[]): Uint8Array;
@ -22,7 +10,7 @@ export interface IBls {
verify(publicKey: Uint8Array, message: Uint8Array, signature: Uint8Array): boolean; verify(publicKey: Uint8Array, message: Uint8Array, signature: Uint8Array): boolean;
verifyAggregate(publicKeys: Uint8Array[], message: Uint8Array, signature: Uint8Array): boolean; verifyAggregate(publicKeys: Uint8Array[], message: Uint8Array, signature: Uint8Array): boolean;
verifyMultiple(publicKeys: Uint8Array[], messages: Uint8Array[], signature: Uint8Array): boolean; verifyMultiple(publicKeys: Uint8Array[], messages: Uint8Array[], signature: Uint8Array): boolean;
verifyMultipleSignatures(publicKeys: Uint8Array[], messages: Uint8Array[], signatures: Uint8Array[]): boolean; verifyMultipleSignatures(sets: {publicKey: Uint8Array; message: Uint8Array; signature: Uint8Array}[]): boolean;
secretKeyToPublicKey(secretKey: Uint8Array): Uint8Array; secretKeyToPublicKey(secretKey: Uint8Array): Uint8Array;
init(): Promise<void>; init(): Promise<void>;
@ -40,21 +28,37 @@ export declare class SecretKey {
} }
export declare class PublicKey { export declare class PublicKey {
static fromBytes(bytes: Uint8Array): PublicKey; /** @param type Only for impl `blst-native`. Defaults to `CoordType.jacobian` */
static fromBytes(bytes: Uint8Array, type?: CoordType): PublicKey;
static fromHex(hex: string): PublicKey; static fromHex(hex: string): PublicKey;
static aggregate(publicKeys: PublicKey[]): PublicKey; static aggregate(publicKeys: PublicKey[]): PublicKey;
toBytes(): Uint8Array; /** @param format Defaults to `PointFormat.compressed` */
toHex(): string; toBytes(format?: PointFormat): Uint8Array;
toHex(format?: PointFormat): string;
} }
export declare class Signature { export declare class Signature {
static fromBytes(bytes: Uint8Array): Signature; /** @param type Only for impl `blst-native`. Defaults to `CoordType.affine` */
static fromBytes(bytes: Uint8Array, type?: CoordType, validate?: boolean): Signature;
static fromHex(hex: string): Signature; static fromHex(hex: string): Signature;
static aggregate(signatures: Signature[]): Signature; static aggregate(signatures: Signature[]): Signature;
static verifyMultipleSignatures(publicKeys: PublicKey[], messages: Uint8Array[], signatures: Signature[]): boolean; static verifyMultipleSignatures(sets: {publicKey: PublicKey; message: Uint8Array; signature: Signature}[]): boolean;
verify(publicKey: PublicKey, message: Uint8Array): boolean; verify(publicKey: PublicKey, message: Uint8Array): boolean;
verifyAggregate(publicKeys: PublicKey[], message: Uint8Array): boolean; verifyAggregate(publicKeys: PublicKey[], message: Uint8Array): boolean;
verifyMultiple(publicKeys: PublicKey[], messages: Uint8Array[]): boolean; verifyMultiple(publicKeys: PublicKey[], messages: Uint8Array[]): boolean;
toBytes(): Uint8Array; /** @param format Defaults to `PointFormat.compressed` */
toHex(): string; toBytes(format?: PointFormat): Uint8Array;
toHex(format?: PointFormat): string;
}
export type Implementation = "herumi" | "blst-native";
export enum PointFormat {
compressed = "compressed",
uncompressed = "uncompressed",
}
export enum CoordType {
affine,
jacobian,
} }

View File

@ -1,6 +1,6 @@
import {expect} from "chai"; import {expect} from "chai";
import {IBls} from "../../src/interface"; import {IBls, PointFormat} from "../../src/interface";
import {getN, randomMessage} from "../util"; import {getN, randomMessage, hexToBytesNode} from "../util";
import {hexToBytes} from "../../src/helpers"; import {hexToBytes} from "../../src/helpers";
import {maliciousVerifyMultipleSignaturesData} from "../data/malicious-signature-test-data"; import {maliciousVerifyMultipleSignaturesData} from "../data/malicious-signature-test-data";
@ -96,24 +96,23 @@ export function runIndexTests(bls: IBls): void {
describe("verifyMultipleSignatures", () => { describe("verifyMultipleSignatures", () => {
it("Should verify multiple signatures", () => { it("Should verify multiple signatures", () => {
const n = 4; const n = 4;
const dataArr = getN(n, () => { const sets = getN(n, () => {
const sk = bls.SecretKey.fromKeygen(); const sk = bls.SecretKey.fromKeygen();
const pk = sk.toPublicKey(); const publicKey = sk.toPublicKey();
const msg = randomMessage(); const message = randomMessage();
const sig = sk.sign(msg); const signature = sk.sign(message);
return {pk, msg, sig}; return {publicKey, message, signature};
}); });
const pks = dataArr.map((data) => data.pk);
const msgs = dataArr.map((data) => data.msg);
const sigs = dataArr.map((data) => data.sig);
expect(bls.Signature.verifyMultipleSignatures(pks, msgs, sigs)).to.equal(true, "class interface failed"); expect(bls.Signature.verifyMultipleSignatures(sets)).to.equal(true, "class interface failed");
expect( expect(
bls.verifyMultipleSignatures( bls.verifyMultipleSignatures(
pks.map((pk) => pk.toBytes()), sets.map((s) => ({
msgs, publicKey: s.publicKey.toBytes(),
sigs.map((sig) => sig.toBytes()) message: s.message,
signature: s.signature.toBytes(),
}))
) )
).to.equal(true, "functional (bytes serialized) interface failed"); ).to.equal(true, "functional (bytes serialized) interface failed");
}); });
@ -138,11 +137,47 @@ export function runIndexTests(bls: IBls): void {
"Malicious signature should be validated with bls.verifyMultiple" "Malicious signature should be validated with bls.verifyMultiple"
); );
const maliciousSets = pks.map((_, i) => ({
publicKey: pks[i],
message: msgs[i],
signature: sigs[i],
}));
// This method is expected to catch the malicious signature and not verify // This method is expected to catch the malicious signature and not verify
expect(bls.Signature.verifyMultipleSignatures(pks, msgs, sigs)).to.equal( expect(bls.Signature.verifyMultipleSignatures(maliciousSets)).to.equal(
false, false,
"Malicous signature should not validate with bls.verifyMultipleSignatures" "Malicous signature should not validate with bls.verifyMultipleSignatures"
); );
}); });
}); });
describe("serialize deserialize", () => {
/* eslint-disable max-len */
const skHex = "0x0101010101010101010101010101010101010101010101010101010101010101";
const pkHexCompExpected =
"0xaa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac1";
const pkHexUncompExpected =
"0x0a1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac111392125a1c3750363c2c97d9650fb78696e6428db8ff9efaf0471cbfd20324916ab545746db83756d335e92f9e8c8b8";
it("Should serialize comp pubkey", () => {
const sk = bls.SecretKey.fromBytes(hexToBytesNode(skHex));
const pkHexComp = sk.toPublicKey().toHex(PointFormat.compressed);
expect(pkHexComp).to.equal(pkHexCompExpected, "Wrong pkHexComp");
});
it("Should serialize uncomp pubkey", () => {
const sk = bls.SecretKey.fromBytes(hexToBytesNode(skHex));
const pkHexUncomp = sk.toPublicKey().toHex(PointFormat.uncompressed);
expect(pkHexUncomp).to.equal(pkHexUncompExpected, "Wrong pkHexUncomp");
});
it("Should deserialize comp pubkey", () => {
bls.PublicKey.fromHex(pkHexCompExpected);
});
it("Should deserialize uncomp pubkey", () => {
bls.PublicKey.fromHex(pkHexUncompExpected);
});
});
} }

View File

@ -0,0 +1,60 @@
import {expect} from "chai";
import {chunkify} from "./utils";
describe("chunkify", () => {
const arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 13, 14, 15];
const results = {
0: [[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 13, 14, 15]],
1: [[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 13, 14, 15]],
2: [
[0, 1, 2, 3, 4, 5, 6, 7],
[8, 9, 10, 12, 13, 14, 15],
],
3: [
[0, 1, 2, 3, 4],
[5, 6, 7, 8, 9],
[10, 12, 13, 14, 15],
],
4: [
[0, 1, 2, 3],
[4, 5, 6, 7],
[8, 9, 10, 12],
[13, 14, 15],
],
5: [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[9, 10, 12],
[13, 14, 15],
],
6: [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[9, 10, 12],
[13, 14, 15],
],
7: [[0, 1], [2, 3], [4, 5], [6, 7], [8, 9], [10, 12], [13, 14], [15]],
8: [[0, 1], [2, 3], [4, 5], [6, 7], [8, 9], [10, 12], [13, 14], [15]],
};
const testCases: {
id: string;
n: number;
arr: number[];
expectArr: number[][];
}[] = Object.entries(results).map(([i, expectArr]) => ({
id: i,
n: parseInt(i),
arr,
expectArr,
}));
for (const {id, arr, n, expectArr} of testCases) {
it(id, () => {
expect(chunkify(arr, n)).to.deep.equal(expectArr);
});
}
});

View File

@ -0,0 +1,47 @@
import {spawn, Pool, Worker, Thread} from "threads";
import {Implementation, PointFormat, PublicKey, Signature} from "../../../../src";
import {WorkerApi} from "./worker";
type ThreadType = {
[K in keyof WorkerApi]: (...args: Parameters<WorkerApi[K]>) => Promise<ReturnType<WorkerApi[K]>>;
};
export class BlsMultiThreadNaive {
impl: Implementation;
pool: Pool<Thread & ThreadType>;
format: PointFormat;
constructor(impl: Implementation, workerCount?: number) {
this.impl = impl;
// Use compressed for herumi for now.
// THe worker is not able to deserialize from uncompressed
// `Error: err _wrapDeserialize`
this.format = impl === "blst-native" ? PointFormat.uncompressed : PointFormat.compressed;
this.pool = Pool(() => (spawn(new Worker("./worker")) as any) as Promise<Thread & ThreadType>, workerCount);
}
async destroy(): Promise<void> {
await this.pool.terminate(true);
}
async verify(pk: PublicKey, msg: Uint8Array, sig: Signature): Promise<boolean> {
return this.pool.queue((worker) =>
worker.verify(this.impl, pk.toBytes(PointFormat.uncompressed), msg, sig.toBytes(PointFormat.uncompressed))
);
}
async verifyMultipleAggregateSignatures(
sets: {publicKey: PublicKey; message: Uint8Array; signature: Signature}[]
): Promise<boolean> {
return this.pool.queue((worker) =>
worker.verifyMultipleAggregateSignatures(
this.impl,
sets.map((s) => ({
publicKey: s.publicKey.toBytes(PointFormat.uncompressed),
message: s.message,
signature: s.signature.toBytes(PointFormat.uncompressed),
}))
)
);
}
}

View File

@ -0,0 +1,64 @@
import {expect} from "chai";
import {IBls, PublicKey, Signature} from "../../../../src";
import {BlsMultiThreadNaive} from "./index";
export function runMultithreadTests(bls: IBls): void {
describe("bls pool naive", function () {
// Starting all threads may take a while due to ts-node compilation
this.timeout(20 * 1000);
const n = 16;
let pool: BlsMultiThreadNaive;
before("Create pool and warm-up wallets", async function () {
pool = new BlsMultiThreadNaive(bls.implementation);
});
after("Destroy pool", async function () {
await pool.destroy();
});
describe("1 msg, 1 pk", function () {
const msg = Buffer.from("sample-msg");
const sk = bls.SecretKey.fromKeygen(Buffer.alloc(32, 1));
const pk = sk.toPublicKey();
const sig = sk.sign(msg);
it("verify", async function () {
if (bls.implementation === "herumi") this.skip();
const validArr = await Promise.all(
Array.from({length: 32}, (i) => i).map(async () => pool.verify(pk, msg, sig))
);
for (const [i, valid] of validArr.entries()) {
expect(valid).to.equal(true, `Invalid ${i}`);
}
});
});
describe("N msgs, N pks", function () {
const sets: {publicKey: PublicKey; message: Uint8Array; signature: Signature}[] = [];
for (let i = 0; i < n; i++) {
const message = Buffer.alloc(32, i);
const sk = bls.SecretKey.fromKeygen(Buffer.alloc(32, i));
sets.push({message, publicKey: sk.toPublicKey(), signature: sk.sign(message)});
}
it("verify", async function () {
if (bls.implementation === "herumi") this.skip();
const validArr = await Promise.all(sets.map((s) => pool.verify(s.publicKey, s.message, s.signature)));
for (const [i, valid] of validArr.entries()) {
expect(valid).to.equal(true, `Invalid ${i}`);
}
});
it("verifyMultipleAggregateSignatures", async function () {
if (bls.implementation === "herumi") this.skip();
const valid = await pool.verifyMultipleAggregateSignatures(sets);
expect(valid).to.equal(true);
});
});
});
}

View File

@ -0,0 +1,10 @@
export function chunkify<T>(arr: T[], chunkCount: number): T[][] {
const chunkSize = Math.round(arr.length / chunkCount);
const arrArr: T[][] = [];
for (let i = 0, j = arr.length; i < j; i += chunkSize) {
arrArr.push(arr.slice(i, i + chunkSize));
}
return arrArr;
}

View File

@ -0,0 +1,28 @@
import {expose} from "threads/worker";
import {bls, init, CoordType, Implementation} from "../../../../src";
export type WorkerApi = typeof workerApi;
const workerApi = {
async verify(impl: Implementation, publicKey: Uint8Array, message: Uint8Array, signature: Uint8Array) {
await init(impl);
const pk = bls.PublicKey.fromBytes(publicKey, CoordType.affine);
const sig = bls.Signature.fromBytes(signature, CoordType.affine, true);
return sig.verify(pk, message);
},
async verifyMultipleAggregateSignatures(
impl: Implementation,
sets: {publicKey: Uint8Array; message: Uint8Array; signature: Uint8Array}[]
) {
await init(impl);
return bls.Signature.verifyMultipleSignatures(
sets.map((s) => ({
publicKey: bls.PublicKey.fromBytes(s.publicKey, CoordType.affine),
message: s.message,
signature: bls.Signature.fromBytes(s.signature, CoordType.affine, true),
}))
);
},
};
expose(workerApi);

View File

@ -1,6 +1,7 @@
import {runSecretKeyTests} from "./secretKey.test"; import {runSecretKeyTests} from "./secretKey.test";
import {runPublicKeyTests} from "./publicKey.test"; import {runPublicKeyTests} from "./publicKey.test";
import {runIndexTests} from "./index.test"; import {runIndexTests} from "./index.test";
import {runMultithreadTests} from "./multithread/naive/naive.test";
import {describeForAllImplementations} from "../switch"; import {describeForAllImplementations} from "../switch";
// Import test's bls lib lazily to prevent breaking test with Karma // Import test's bls lib lazily to prevent breaking test with Karma
@ -8,4 +9,5 @@ describeForAllImplementations((bls) => {
runSecretKeyTests(bls); runSecretKeyTests(bls);
runPublicKeyTests(bls); runPublicKeyTests(bls);
runIndexTests(bls); runIndexTests(bls);
runMultithreadTests(bls);
}); });

View File

@ -161,10 +161,10 @@
bls-eth-wasm "^0.4.4" bls-eth-wasm "^0.4.4"
randombytes "^2.1.0" randombytes "^2.1.0"
"@chainsafe/blst@^0.1.6": "@chainsafe/blst@^0.2.0":
version "0.1.6" version "0.2.0"
resolved "https://registry.yarnpkg.com/@chainsafe/blst/-/blst-0.1.6.tgz#d933a1568d9e781cd13673d80ff1eaf40c955107" resolved "https://registry.yarnpkg.com/@chainsafe/blst/-/blst-0.2.0.tgz#5e2d2707c2c0d56ff077a00179a5255eaca14099"
integrity sha512-iv1CASFce9T1QQB+pVznMXadEYZFw3x/YOgstjw14OoKGZSYvjupA7h7247u/ZHQguw7aO3jmVTEzHAwO8yQqw== integrity sha512-eyyLm4C+Zhl18YwFa93J+xRSHj0NrBZodBO+z+aaREf71RnA7/EvOcAPVLpEW2CI7PsInhVne/ufb+A7gfHQrg==
dependencies: dependencies:
node-fetch "^2.6.1" node-fetch "^2.6.1"
node-gyp "^7.1.2" node-gyp "^7.1.2"
@ -1171,7 +1171,7 @@ callsite@1.0.0:
resolved "https://registry.yarnpkg.com/callsite/-/callsite-1.0.0.tgz#280398e5d664bd74038b6f0905153e6e8af1bc20" resolved "https://registry.yarnpkg.com/callsite/-/callsite-1.0.0.tgz#280398e5d664bd74038b6f0905153e6e8af1bc20"
integrity sha1-KAOY5dZkvXQDi28JBRU+borxvCA= integrity sha1-KAOY5dZkvXQDi28JBRU+borxvCA=
callsites@^3.0.0: callsites@^3.0.0, callsites@^3.1.0:
version "3.1.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
@ -2133,6 +2133,11 @@ eslint@^6.8.0:
text-table "^0.2.0" text-table "^0.2.0"
v8-compile-cache "^2.0.3" v8-compile-cache "^2.0.3"
esm@^3.2.25:
version "3.2.25"
resolved "https://registry.yarnpkg.com/esm/-/esm-3.2.25.tgz#342c18c29d56157688ba5ce31f8431fbb795cc10"
integrity sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==
espree@^6.1.2: espree@^6.1.2:
version "6.1.2" version "6.1.2"
resolved "https://registry.yarnpkg.com/espree/-/espree-6.1.2.tgz#6c272650932b4f91c3714e5e7b5f5e2ecf47262d" resolved "https://registry.yarnpkg.com/espree/-/espree-6.1.2.tgz#6c272650932b4f91c3714e5e7b5f5e2ecf47262d"
@ -3152,6 +3157,13 @@ is-number@^7.0.0:
resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
is-observable@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-observable/-/is-observable-1.1.0.tgz#b3e986c8f44de950867cab5403f5a3465005975e"
integrity sha512-NqCa4Sa2d+u7BWc6CukaObG3Fh+CU9bvixbpcXYhy2VvYS7vVGIdAgnIS5Ks3A/cqk4rebLJ9s8zBstT2aKnIA==
dependencies:
symbol-observable "^1.1.0"
is-plain-obj@^2.1.0: is-plain-obj@^2.1.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287"
@ -4216,6 +4228,11 @@ object.values@^1.1.0:
function-bind "^1.1.1" function-bind "^1.1.1"
has "^1.0.3" has "^1.0.3"
observable-fns@^0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/observable-fns/-/observable-fns-0.5.1.tgz#9b56478690dd0fa8603e3a7e7d2975d88bca0904"
integrity sha512-wf7g4Jpo1Wt2KIqZKLGeiuLOEMqpaOZ5gJn7DmSdqXgTdxRwSdBhWegQQpPteQ2gZvzCKqNNpwb853wcpA0j7A==
on-finished@~2.3.0: on-finished@~2.3.0:
version "2.3.0" version "2.3.0"
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
@ -5518,6 +5535,11 @@ supports-color@^7.1.0:
dependencies: dependencies:
has-flag "^4.0.0" has-flag "^4.0.0"
symbol-observable@^1.1.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==
table@^5.2.3: table@^5.2.3:
version "5.4.6" version "5.4.6"
resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e" resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e"
@ -5588,6 +5610,18 @@ text-table@^0.2.0:
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=
threads@^1.6.3:
version "1.6.3"
resolved "https://registry.yarnpkg.com/threads/-/threads-1.6.3.tgz#89324a93509403c90a169344023151ae1fe4986b"
integrity sha512-tKwFIWRgfAT85KGkrpDt2jWPO8IVH0sLNfB/pXad/VW9eUIY2Zlz+QyeizypXhPHv9IHfqRzvk2t3mPw+imhWw==
dependencies:
callsites "^3.1.0"
debug "^4.1.1"
is-observable "^1.1.0"
observable-fns "^0.5.1"
optionalDependencies:
tiny-worker ">= 2"
through2@^2.0.0: through2@^2.0.0:
version "2.0.5" version "2.0.5"
resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd"
@ -5608,6 +5642,13 @@ timers-browserify@^2.0.4:
dependencies: dependencies:
setimmediate "^1.0.4" setimmediate "^1.0.4"
"tiny-worker@>= 2":
version "2.3.0"
resolved "https://registry.yarnpkg.com/tiny-worker/-/tiny-worker-2.3.0.tgz#715ae34304c757a9af573ae9a8e3967177e6011e"
integrity sha512-pJ70wq5EAqTAEl9IkGzA+fN0836rycEuz2Cn6yeZ6FRzlVS5IDOkFHpIoEsksPRQV34GDqXm65+OlnZqUSyK2g==
dependencies:
esm "^3.2.25"
tmp@0.0.33, tmp@0.0.x, tmp@^0.0.33: tmp@0.0.33, tmp@0.0.x, tmp@^0.0.33:
version "0.0.33" version "0.0.33"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"