diff --git a/.github/workflows/main.yml b/.github/no_run_workflows/main.yml similarity index 100% rename from .github/workflows/main.yml rename to .github/no_run_workflows/main.yml diff --git a/rebuild/.eslintrc.js b/rebuild/.eslintrc.cjs similarity index 90% rename from rebuild/.eslintrc.js rename to rebuild/.eslintrc.cjs index 20c17dfe..e52f1323 100644 --- a/rebuild/.eslintrc.js +++ b/rebuild/.eslintrc.cjs @@ -11,8 +11,12 @@ module.exports = { }, parser: "@typescript-eslint/parser", parserOptions: { - ecmaVersion: 10, + ecmaVersion: 12, project: "./tsconfig.json", + sourceType: "module", + ecmaFeatures:{ + modules: true + } }, plugins: ["@typescript-eslint", "eslint-plugin-import", "eslint-plugin-node", "prettier"], extends: [ @@ -51,6 +55,7 @@ module.exports = { "@typescript-eslint/explicit-member-accessibility": ["error", {accessibility: "no-public"}], "@typescript-eslint/no-unsafe-call": "error", "@typescript-eslint/no-unsafe-return": "error", + "import/no-unresolved": "off", "import/no-extraneous-dependencies": [ "error", { @@ -79,6 +84,12 @@ module.exports = { }, settings: { "import/core-modules": ["node:child_process", "node:crypto", "node:fs", "node:os", "node:path", "node:util"], + 'import/resolver': { + node: { + extensions: [".js", ".ts"], + moduleDirectory: ["lib/", "test/", "node_modules/"] + } + }, }, overrides: [ { diff --git a/rebuild/.mocharc.cjs b/rebuild/.mocharc.cjs new file mode 100644 index 00000000..9213d09b --- /dev/null +++ b/rebuild/.mocharc.cjs @@ -0,0 +1,6 @@ +module.exports = { + colors: true, + require: "ts-node/register", + exit: true, + loader: "ts-node/esm" +} diff --git a/rebuild/.mocharc.yaml b/rebuild/.mocharc.yaml deleted file mode 100644 index 8be2be3f..00000000 --- a/rebuild/.mocharc.yaml +++ /dev/null @@ -1,3 +0,0 @@ -colors: true -require: ts-node/register -exit: true diff --git a/rebuild/binding.gyp b/rebuild/binding.gyp index 6defe7f3..27a29439 100644 --- a/rebuild/binding.gyp +++ b/rebuild/binding.gyp @@ -9,7 +9,7 @@ 'src/secret_key.cc', 'src/public_key.cc', 'src/signature.cc', - # 'src/functions.cc', + 'src/functions.cc', ], 'include_dirs': [ 'deps/blst/bindings', diff --git a/rebuild/lib/index.d.ts b/rebuild/lib/index.d.ts index 17414785..b7cf1aea 100644 --- a/rebuild/lib/index.d.ts +++ b/rebuild/lib/index.d.ts @@ -16,7 +16,7 @@ export type BlstBuffer = Uint8Array; export type PublicKeyArg = BlstBuffer | PublicKey; export type SignatureArg = BlstBuffer | Signature; export interface SignatureSet { - msg: BlstBuffer; + message: BlstBuffer; publicKey: PublicKeyArg; signature: SignatureArg; } @@ -71,6 +71,7 @@ export class SecretKey implements Serializable { * Serialize a secret key into a Buffer. */ serialize(): Buffer; + toHex(): string; toPublicKey(): PublicKey; sign(msg: BlstBuffer): Signature; } @@ -79,6 +80,7 @@ export class PublicKey implements Serializable { private constructor(); static deserialize(skBytes: BlstBuffer, coordType?: CoordType): PublicKey; serialize(compress?: boolean): Buffer; + toHex(compress?: boolean): string; keyValidate(): void; } @@ -86,5 +88,152 @@ export class Signature implements Serializable { private constructor(); static deserialize(skBytes: BlstBuffer, coordType?: CoordType): Signature; serialize(compress?: boolean): Buffer; + toHex(compress?: boolean): string; sigValidate(): void; } + +/** + * Aggregates an array of PublicKeyArgs. Can pass mixed deserialized PublicKey + * objects and serialized Uint8Array in the `keys` array. Passing serialized + * objects requires deserialization of the blst::P1 + * + * @param {PublicKeyArg} keys - Array of public keys to aggregate + * + * @return {PublicKey} - Aggregated jacobian public key + * + * @throw {TypeError} - Invalid input + * @throw {Error} - Invalid aggregation + */ +export function aggregatePublicKeys(keys: PublicKeyArg[]): PublicKey; + +/** + * Aggregates an array of SignatureArgs. Can pass mixed deserialized Signature + * objects and serialized Uint8Array in the `signatures` array. Passing serialized + * objects requires deserialization of the blst::P2 + * + * @param {SignatureArg} signatures - Array of signatures to aggregate + * + * @return {Signature} - Aggregated jacobian signature + * + * @throw {TypeError} - Invalid input + * @throw {Error} - Invalid aggregation + */ +export function aggregateSignatures(signatures: SignatureArg[]): Signature; + +/** + * Bls verification of a message against a public key and signature. + * + * @param {BlstBuffer} msg - Message to verify + * @param {PublicKeyArg} publicKey - Public key to verify against + * @param {SignatureArg} signature - Signature of the message + * + * @return {boolean} - True if the signature is valid, false otherwise + * + * @throw {TypeError} - Invalid input + */ +export function verify(msg: BlstBuffer, publicKey: PublicKeyArg, signature: SignatureArg): boolean; + +/** + * Bls verification of a message against a set of public keys and an aggregated signature. + * + * @param {BlstBuffer} msg - Message to verify + * @param {PublicKeyArg} publicKeys - Public keys to aggregate and verify against + * @param {SignatureArg} signature - Aggregated signature of the message + * + * @return {boolean} - True if the signature is valid, false otherwise + * + * @throw {TypeError} - Invalid input + * @throw {Error} - Invalid aggregation + */ +export function fastAggregateVerify(msg: BlstBuffer, publicKeys: PublicKeyArg[], signature: SignatureArg): boolean; + +/** + * Bls verification of a set of messages, with corresponding public keys, and a single + * aggregated signature. + * + * @param {BlstBuffer} msgs - Messages to verify + * @param {PublicKeyArg} publicKeys - Corresponding public keys to verify against + * @param {SignatureArg} signature - Aggregated signature of the message + * + * @return {boolean} - True if the signature is valid, false otherwise + * + * @throw {TypeError} - Invalid input + * @throw {Error} - Invalid aggregation + */ +export function aggregateVerify(msgs: BlstBuffer[], publicKeys: PublicKeyArg[], signature: SignatureArg): boolean; + +/** + * Bls batch verification for groups with a message and corresponding public key + * and signature. Only returns true if all signatures are valid. + * + * @param {SignatureSet} signatureSets - Array of SignatureSet objects to batch verify + * + * @return {boolean} - True if all signatures are valid, false otherwise + * + * @throw {TypeError} - Invalid input + * @throw {Error} - Invalid aggregation + */ +export function verifyMultipleAggregateSignatures(signatureSets: SignatureSet[]): boolean; + +/** + * Bls verification of a message against a public key and signature. + * + * @param {BlstBuffer} msg - Message to verify + * @param {PublicKeyArg} publicKey - Public key to verify against + * @param {SignatureArg} signature - Signature of the message + * + * @return {Promise} - True if the signature is valid, false otherwise + * + * @throw {TypeError} - Invalid input + */ +export function asyncVerify(msg: BlstBuffer, publicKey: PublicKeyArg, signature: SignatureArg): Promise; + +/** + * Bls verification of a message against a set of public keys and an aggregated signature. + * + * @param {BlstBuffer} msg - Message to verify + * @param {PublicKeyArg} publicKeys - Public keys to aggregate and verify against + * @param {SignatureArg} signature - Aggregated signature of the message + * + * @return {Promise} - True if the signature is valid, false otherwise + * + * @throw {TypeError} - Invalid input + * @throw {Error} - Invalid aggregation + */ +export function asyncFastAggregateVerify( + msg: BlstBuffer, + publicKey: PublicKeyArg[], + signature: SignatureArg +): Promise; + +/** + * Bls verification of a set of messages, with corresponding public keys, and a single + * aggregated signature. + * + * @param {BlstBuffer} msgs - Messages to verify + * @param {PublicKeyArg} publicKeys - Corresponding public keys to verify against + * @param {SignatureArg} signature - Aggregated signature of the message + * + * @return {Promise} - True if the signature is valid, false otherwise + * + * @throw {TypeError} - Invalid input + * @throw {Error} - Invalid aggregation + */ +export function asyncAggregateVerify( + msg: BlstBuffer[], + publicKey: PublicKeyArg[], + signature: SignatureArg +): Promise; + +/** + * Bls batch verification for groups with a message and corresponding public key + * and signature. Only returns true if all signatures are valid. + * + * @param {SignatureSet} signatureSets - Array of SignatureSet objects to batch verify + * + * @return {Promise} - True if all signatures are valid, false otherwise + * + * @throw {TypeError} - Invalid input + * @throw {Error} - Invalid aggregation + */ +export function asyncVerifyMultipleAggregateSignatures(signatureSets: SignatureSet[]): Promise; diff --git a/rebuild/lib/index.js b/rebuild/lib/index.js index f12a763d..42d5b0c2 100644 --- a/rebuild/lib/index.js +++ b/rebuild/lib/index.js @@ -3,11 +3,96 @@ /* eslint-disable @typescript-eslint/no-require-imports */ /* eslint-disable @typescript-eslint/no-var-requires */ /* eslint-disable @typescript-eslint/no-unsafe-call */ -const bindings = require("bindings")("blst_ts_addon"); +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +const {default: getBindings} = await import("bindings"); +const bindings = getBindings("blst_ts_addon"); +const { + BLST_CONSTANTS, + SecretKey, + PublicKey, + Signature, + aggregatePublicKeys, + aggregateSignatures, + aggregateVerify, + verifyMultipleAggregateSignatures, + asyncAggregateVerify, + asyncVerifyMultipleAggregateSignatures, +} = bindings; -bindings.CoordType = { +SecretKey.prototype.toHex = function () { + return `0x${this.serialize().toString("hex")}`; +}; + +PublicKey.prototype.toHex = function (compress) { + return `0x${this.serialize(compress).toString("hex")}`; +}; + +Signature.prototype.toHex = function (compress) { + return `0x${this.serialize(compress).toString("hex")}`; +}; + +export { + BLST_CONSTANTS, + SecretKey, + PublicKey, + Signature, + aggregatePublicKeys, + aggregateSignatures, + aggregateVerify, + verifyMultipleAggregateSignatures, + asyncAggregateVerify, + asyncVerifyMultipleAggregateSignatures, +}; + +export function verify(msg, pk, sig) { + return aggregateVerify([msg], [pk], sig); +} + +export function asyncVerify(msg, pk, sig) { + return asyncAggregateVerify([msg], [pk], sig); +} + +export function fastAggregateVerify(msg, pks, sig) { + let key; + try { + // this throws for invalid key, catch and return false + key = aggregatePublicKeys(pks); + } catch { + return false; + } + return aggregateVerify([msg], [key], sig); +} + +export function asyncFastAggregateVerify(msg, pks, sig) { + let key; + try { + // this throws for invalid key, catch and return false + key = aggregatePublicKeys(pks); + } catch { + return false; + } + return asyncAggregateVerify([msg], [key], sig); +} + +export const CoordType = { affine: 0, jacobian: 1, }; -module.exports = exports = bindings; +export default { + CoordType, + BLST_CONSTANTS, + SecretKey, + PublicKey, + Signature, + aggregatePublicKeys, + aggregateSignatures, + verify, + aggregateVerify, + fastAggregateVerify, + verifyMultipleAggregateSignatures, + asyncVerify, + asyncAggregateVerify, + asyncFastAggregateVerify, + asyncVerifyMultipleAggregateSignatures, +}; diff --git a/rebuild/package.json b/rebuild/package.json index 1a843aea..0f3a745a 100644 --- a/rebuild/package.json +++ b/rebuild/package.json @@ -2,8 +2,6 @@ "name": "@chainsafe/blst-ts", "version": "1.0.0", "description": "Typescript wrapper for supranational/blst native bindings, a highly performant BLS12-381 signature library", - "main": "lib/index.js", - "types": "lib/index.d.ts", "gypfile": true, "private": false, "files": [ @@ -12,6 +10,26 @@ "lib", "src" ], + "type": "module", + "main": "./import/index.js", + "module": "./import/index.js", + "types": "./import/index.d.ts", + "exports": { + ".": "./import/index.js" + }, + "typesVersions": { + "*": { + "*": [ + "./import/index.d.ts" + ] + } + }, + "publishConfig": { + "exports": { + ".": "./import/index.js" + }, + "types": "./import/index.d.ts" + }, "scripts": { "preinstall": "mkdir -p deps && cp -r ../blst deps", "clean": "npm run clean:dist; npm run clean:build", @@ -24,7 +42,7 @@ "lint:ts": "eslint --color --ext .ts lib/ test/", "lint:c": "clang-format -i ./src/*", "lint": "npm run lint:c && npm run lint:ts", - "test": "yarn test:unit", + "test": "yarn test:unit && yarn test:spec", "test:unit": "mocha test/unit/**/*.test.ts", "test:spec": "mocha 'test/spec/**/*.test.ts'", "download-spec-tests": "node -r ts-node/register test/spec/downloadTests.ts" diff --git a/rebuild/src/addon.cc b/rebuild/src/addon.cc index c7e4d78c..02497216 100644 --- a/rebuild/src/addon.cc +++ b/rebuild/src/addon.cc @@ -31,19 +31,19 @@ bool is_valid_length( }; error_out.append(" bytes long"); return false; -}; +} BlstTsAddon::BlstTsAddon(Napi::Env env, Napi::Object exports) : _dst{"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_"}, _blst_error_strings{ "BLST_SUCCESS", - "BLST_BAD_ENCODING", - "BLST_POINT_NOT_ON_CURVE", - "BLST_POINT_NOT_IN_GROUP", - "BLST_AGGR_TYPE_MISMATCH", - "BLST_VERIFY_FAIL", - "BLST_PK_IS_INFINITY", - "BLST_BAD_SCALAR", + "BLST_ERROR::BLST_BAD_ENCODING", + "BLST_ERROR::BLST_POINT_NOT_ON_CURVE", + "BLST_ERROR::BLST_POINT_NOT_IN_GROUP", + "BLST_ERROR::BLST_AGGR_TYPE_MISMATCH", + "BLST_ERROR::BLST_VERIFY_FAIL", + "BLST_ERROR::BLST_PK_IS_INFINITY", + "BLST_ERROR::BLST_BAD_SCALAR", } { Napi::Object js_constants = Napi::Object::New(env); js_constants.Set( @@ -56,12 +56,36 @@ BlstTsAddon::BlstTsAddon(Napi::Env env, Napi::Object exports) SecretKey::Init(env, exports, this); PublicKey::Init(env, exports, this); Signature::Init(env, exports, this); - // Functions::Init(env, exports); + Functions::Init(env, exports); env.SetInstanceData(this); + + // Check that openssl PRNG is seeded + blst::byte seed{0}; + if (!this->GetRandomBytes(&seed, 0)) { + Napi::Error::New( + env, "BLST_ERROR: Error seeding pseudo-random number generator") + .ThrowAsJavaScriptException(); + } } std::string BlstTsAddon::GetBlstErrorString(const blst::BLST_ERROR &err) { return _blst_error_strings[err]; } +bool BlstTsAddon::GetRandomBytes(blst::byte *bytes, size_t length) { + // [randomBytes](https://github.com/nodejs/node/blob/4166d40d0873b6d8a0c7291872c8d20dc680b1d7/lib/internal/crypto/random.js#L98) + // [RandomBytesJob](https://github.com/nodejs/node/blob/4166d40d0873b6d8a0c7291872c8d20dc680b1d7/lib/internal/crypto/random.js#L139) + // [RandomBytesTraits::DeriveBits](https://github.com/nodejs/node/blob/4166d40d0873b6d8a0c7291872c8d20dc680b1d7/src/crypto/crypto_random.cc#L65) + // [CSPRNG](https://github.com/nodejs/node/blob/4166d40d0873b6d8a0c7291872c8d20dc680b1d7/src/crypto/crypto_util.cc#L63) + do { + if (1 == RAND_status()) { + if (1 == RAND_bytes(bytes, length)) { + return true; + } + } + } while (1 == RAND_poll()); + + return false; +} + NODE_API_ADDON(BlstTsAddon) diff --git a/rebuild/src/addon.h b/rebuild/src/addon.h index 80745bc8..5775098b 100644 --- a/rebuild/src/addon.h +++ b/rebuild/src/addon.h @@ -18,20 +18,22 @@ using std::endl; #define BLST_TS_RANDOM_BYTES_LENGTH 8U -#define BLST_TS_FUNCTION_PREAMBLE \ +#define BLST_TS_FUNCTION_PREAMBLE(info, env, module) \ Napi::Env env = info.Env(); \ Napi::EscapableHandleScope scope(env); \ BlstTsAddon *module = env.GetInstanceData(); #define BLST_TS_UNWRAP_UINT_8_ARRAY(value_name, arr_name, js_name, ret_val) \ if (!value_name.IsTypedArray()) { \ - Napi::TypeError::New(env, js_name " must be a BlstBuffer") \ + Napi::TypeError::New( \ + env, "BLST_ERROR: " js_name " must be a BlstBuffer") \ .ThrowAsJavaScriptException(); \ return ret_val; \ } \ Napi::TypedArray arr_name##_array = value_name.As(); \ if (arr_name##_array.TypedArrayType() != napi_uint8_array) { \ - Napi::TypeError::New(env, js_name " must be a BlstBuffer") \ + Napi::TypeError::New( \ + env, "BLST_ERROR: " js_name " must be a BlstBuffer") \ .ThrowAsJavaScriptException(); \ return ret_val; \ } \ @@ -69,13 +71,118 @@ using std::endl; : _affine->serialize(serialized.Data()); \ } else { \ Napi::Error::New( \ - env, class_name " cannot be serialized. No point found!") \ + env, \ + "BLST_ERROR: " class_name \ + " cannot be serialized. No point found!") \ .ThrowAsJavaScriptException(); \ return scope.Escape(env.Undefined()); \ } \ \ return scope.Escape(serialized); +#define BLST_TS_UNWRAP_POINT_ARG( \ + env, \ + module, \ + val_name, \ + ptr_group, \ + has_error, \ + snake_case_name, \ + pascal_case_name, \ + capital_case_name, \ + pascal_case_string, \ + blst_point, \ + group_num, \ + coord_type, \ + member_name) \ + /* Arg is a serialized point */ \ + if (val_name.IsTypedArray()) { \ + Napi::TypedArray untyped = val_name.As(); \ + if (untyped.TypedArrayType() != napi_uint8_array) { \ + Napi::TypeError::New( \ + env, \ + "BLST_ERROR: " pascal_case_string "Arg must be a BlstBuffer") \ + .ThrowAsJavaScriptException(); \ + has_error = true; \ + } \ + Napi::Uint8Array typed = untyped.As(); \ + std::string err_out{"BLST_ERROR: " pascal_case_string "Arg"}; \ + if (!is_valid_length( \ + err_out, \ + typed.ByteLength(), \ + BLST_TS_##capital_case_name##_LENGTH_COMPRESSED, \ + BLST_TS_##capital_case_name##_LENGTH_UNCOMPRESSED)) { \ + Napi::TypeError::New(env, err_out).ThrowAsJavaScriptException(); \ + has_error = true; \ + } \ + if (pascal_case_string[0] == 'P' && \ + is_zero_bytes(typed.Data(), 0, typed.ByteLength())) { \ + Napi::TypeError::New( \ + env, "BLST_ERROR: PublicKeyArg must not be zero key") \ + .ThrowAsJavaScriptException(); \ + has_error = true; \ + } \ + /** this can potentially throw. macro must be in try/catch. Leave in \ + * outer context so that loop counter can be used in error message \ + * \ + * Only need to create this ptr to hold the blst::point and make sure \ + * its deleted. Deserialized objects have a member smart pointer \ + */ \ + ptr_group.unique_ptr.reset( \ + new blst_point{typed.Data(), typed.ByteLength()}); \ + ptr_group.raw_pointer = ptr_group.unique_ptr.get(); \ + \ + /* Arg is a deserialized point */ \ + } else if (val_name.IsObject()) { \ + Napi::Object wrapped = val_name.As(); \ + if (!wrapped.CheckTypeTag(&module->_##snake_case_name##_tag)) { \ + Napi::TypeError::New( \ + env, \ + "BLST_ERROR: " pascal_case_string \ + " must be a " pascal_case_string "Arg") \ + .ThrowAsJavaScriptException(); \ + has_error = true; \ + } \ + pascal_case_name *snake_case_name = pascal_case_name::Unwrap(wrapped); \ + /* Check that the required point type has been created */ \ + if (coord_type == CoordType::Jacobian) { \ + if (!snake_case_name->_has_jacobian) { \ + if (!snake_case_name->_has_affine) { \ + Napi::Error::New( \ + env, \ + "BLST_ERROR: " pascal_case_string " not initialized") \ + .ThrowAsJavaScriptException(); \ + has_error = true; \ + } \ + snake_case_name->_jacobian.reset(new blst::P##group_num{ \ + snake_case_name->_affine->to_jacobian()}); \ + snake_case_name->_has_jacobian = true; \ + } \ + } else { \ + if (!snake_case_name->_has_affine) { \ + if (!snake_case_name->_has_jacobian) { \ + Napi::Error::New( \ + env, \ + "BLST_ERROR: " pascal_case_string " not initialized") \ + .ThrowAsJavaScriptException(); \ + has_error = true; \ + } \ + snake_case_name->_affine.reset( \ + new blst::P##group_num##_Affine{ \ + snake_case_name->_jacobian->to_affine()}); \ + snake_case_name->_has_affine = true; \ + } \ + } \ + /* copy raw_pointer to context outside of macro */ \ + ptr_group.raw_pointer = snake_case_name->member_name.get(); \ + } else { \ + Napi::TypeError::New( \ + env, \ + "BLST_ERROR: " pascal_case_string " must be a " pascal_case_string \ + "Arg") \ + .ThrowAsJavaScriptException(); \ + has_error = true; \ + } + class BlstTsAddon; typedef enum { Affine, Jacobian } CoordType; @@ -114,6 +221,7 @@ bool is_valid_length( /** * Circular dependency if these are moved up to the top of the file. */ +#include "functions.h" #include "public_key.h" #include "secret_key.h" #include "signature.h" @@ -159,6 +267,19 @@ class BlstTsAddon : public Napi::Addon { * Converts a blst error to an error string */ std::string GetBlstErrorString(const blst::BLST_ERROR &err); + + /** + * Uses the same openssl method as node to generate random bytes + * + * Either succeeds with exactly |length| bytes of cryptographically + * strong pseudo-random data, or fails. This function may block. + * Don't assume anything about the contents of |buffer| on error. + * MUST check the return value for success! + * + * As a special case, |length == 0| can be used to check if the + * GetRandomBytes is properly seeded without consuming entropy. + */ + bool GetRandomBytes(blst::byte *ikm, size_t length); }; #endif /* BLST_TS_ADDON_H__ */ diff --git a/rebuild/src/functions.cc b/rebuild/src/functions.cc new file mode 100644 index 00000000..ecdb2d0c --- /dev/null +++ b/rebuild/src/functions.cc @@ -0,0 +1,684 @@ +#include "functions.h" + +namespace { +Napi::Value AggregatePublicKeys(const Napi::CallbackInfo &info) { + BLST_TS_FUNCTION_PREAMBLE(info, env, module) + if (!info[0].IsArray()) { + Napi::TypeError::New( + env, "BLST_ERROR: publicKeys must be of type PublicKeyArg[]") + .ThrowAsJavaScriptException(); + return scope.Escape(env.Undefined()); + } + Napi::Array arr = info[0].As(); + uint32_t length = arr.Length(); + if (length == 0) { + Napi::TypeError::New( + env, "BLST_ERROR: PublicKeyArg[] must have length > 0") + .ThrowAsJavaScriptException(); + return scope.Escape(env.Undefined()); + } + + BLST_TS_CREATE_JHEAP_OBJECT(wrapped, public_key, PublicKey, result) + result->_has_jacobian = true; + result->_jacobian.reset(new blst::P1); + + bool has_error = false; + for (uint32_t i = 0; i < length; i++) { + Napi::Value val = arr[i]; + PointerGroup ptr_group; + try { + BLST_TS_UNWRAP_POINT_ARG( + env, + module, + val, + ptr_group, + has_error, + public_key, + PublicKey, + PUBLIC_KEY, + "PublicKey", + blst::P1, + 1, + CoordType::Jacobian, + _jacobian) + if (has_error) { + return scope.Escape(env.Undefined()); + } + result->_jacobian->add(*ptr_group.raw_pointer); + } catch (const blst::BLST_ERROR &err) { + std::ostringstream msg; + msg << "BLST_ERROR::" << module->GetBlstErrorString(err) + << ": Invalid key at index " << i; + Napi::Error::New(env, msg.str()).ThrowAsJavaScriptException(); + return env.Undefined(); + } + } + + return scope.Escape(wrapped); +} + +Napi::Value AggregateSignatures(const Napi::CallbackInfo &info) { + BLST_TS_FUNCTION_PREAMBLE(info, env, module) + if (!info[0].IsArray()) { + Napi::TypeError::New( + env, "BLST_ERROR: signatures must be of type SignatureArg[]") + .ThrowAsJavaScriptException(); + return scope.Escape(env.Undefined()); + } + Napi::Array arr = info[0].As(); + uint32_t length = arr.Length(); + if (length == 0) { + Napi::TypeError::New( + env, "BLST_ERROR: SignatureArg[] must have length > 0") + .ThrowAsJavaScriptException(); + return scope.Escape(env.Undefined()); + } + + BLST_TS_CREATE_JHEAP_OBJECT(wrapped, signature, Signature, result) + result->_has_jacobian = true; + result->_jacobian.reset(new blst::P2); + + bool has_error = false; + for (uint32_t i = 0; i < arr.Length(); i++) { + Napi::Value val = arr[i]; + PointerGroup ptr_group; + try { + BLST_TS_UNWRAP_POINT_ARG( + env, + module, + val, + ptr_group, + has_error, + signature, + Signature, + SIGNATURE, + "Signature", + blst::P2, + 2, + CoordType::Jacobian, + _jacobian) + if (has_error) { + return scope.Escape(env.Undefined()); + } + result->_jacobian->add(*ptr_group.raw_pointer); + } catch (const blst::BLST_ERROR &err) { + std::ostringstream msg; + msg << "BLST_ERROR::" << module->GetBlstErrorString(err) + << " - Invalid signature at index " << i; + Napi::Error::New(env, msg.str()).ThrowAsJavaScriptException(); + return env.Undefined(); + } + } + + return scope.Escape(wrapped); +} + +Napi::Value AggregateVerify(const Napi::CallbackInfo &info) { + Napi::Env env = info.Env(); + Napi::EscapableHandleScope scope(env); + try { + BlstTsAddon *module = env.GetInstanceData(); + bool has_error = false; + + if (!info[0].IsArray()) { + Napi::TypeError::New( + env, "BLST_ERROR: msgs must be of type BlstBuffer[]") + .ThrowAsJavaScriptException(); + return scope.Escape(env.Undefined()); + } + Napi::Array msgs_array = info[0].As(); + uint32_t msgs_array_length = msgs_array.Length(); + + if (!info[1].IsArray()) { + Napi::TypeError::New( + env, "publicKeys must be of type PublicKeyArg[]") + .ThrowAsJavaScriptException(); + return scope.Escape(env.Undefined()); + } + Napi::Array pk_array = info[1].As(); + uint32_t pk_array_length = pk_array.Length(); + + Napi::Value sig_val = info[2]; + PointerGroup sig_ptr_group; + BLST_TS_UNWRAP_POINT_ARG( + env, + module, + sig_val, + sig_ptr_group, + has_error, + signature, + Signature, + SIGNATURE, + "Signature", + blst::P2_Affine, + 2, + CoordType::Affine, + _affine) + if (has_error) { + return scope.Escape(env.Undefined()); + } + + if (pk_array_length == 0) { + if (sig_ptr_group.raw_pointer->is_inf()) { + return scope.Escape(Napi::Boolean::New(env, false)); + } + Napi::TypeError::New( + env, "BLST_ERROR: publicKeys must have length > 0") + .ThrowAsJavaScriptException(); + return scope.Escape(env.Undefined()); + } + if (msgs_array_length == 0) { + Napi::TypeError::New(env, "BLST_ERROR: msgs must have length > 0") + .ThrowAsJavaScriptException(); + return scope.Escape(env.Undefined()); + } + if (msgs_array_length != pk_array_length) { + Napi::TypeError::New( + env, "BLST_ERROR: msgs and publicKeys must be the same length") + .ThrowAsJavaScriptException(); + return scope.Escape(env.Undefined()); + } + + std::unique_ptr ctx{ + new blst::Pairing(true, module->_dst)}; + + for (uint32_t i = 0; i < pk_array_length; i++) { + Napi::Value msg_value = msgs_array[i]; + BLST_TS_UNWRAP_UINT_8_ARRAY( + msg_value, msg, "msg", scope.Escape(env.Undefined())) + + Napi::Value val = pk_array[i]; + PointerGroup pk_ptr_group; + BLST_TS_UNWRAP_POINT_ARG( + env, + module, + val, + pk_ptr_group, + has_error, + public_key, + PublicKey, + PUBLIC_KEY, + "PublicKey", + blst::P1_Affine, + 1, + CoordType::Affine, + _affine) + if (has_error) { + return scope.Escape(env.Undefined()); + } + + blst::BLST_ERROR err = ctx->aggregate( + pk_ptr_group.raw_pointer, + sig_ptr_group.raw_pointer, + msg.Data(), + msg.ByteLength()); + if (err != blst::BLST_ERROR::BLST_SUCCESS) { + std::ostringstream msg; + msg << "BLST_ERROR::" << module->GetBlstErrorString(err) + << ": Invalid verification aggregate at index " << i; + Napi::Error::New(env, msg.str()).ThrowAsJavaScriptException(); + return scope.Escape(env.Undefined()); + } + } + + ctx->commit(); + blst::PT pt{*sig_ptr_group.raw_pointer}; + return Napi::Boolean::New(env, ctx->finalverify(&pt)); + } catch (...) { + return Napi::Boolean::New(env, false); + } +} + +Napi::Value VerifyMultipleAggregateSignatures(const Napi::CallbackInfo &info) { + Napi::Env env = info.Env(); + Napi::EscapableHandleScope scope(env); + try { + BlstTsAddon *module = env.GetInstanceData(); + bool has_error = false; + + if (!info[0].IsArray()) { + Napi::TypeError::New( + env, "BLST_ERROR: signatureSets must be of type SignatureSet[]") + .ThrowAsJavaScriptException(); + return scope.Escape(env.Undefined()); + } + Napi::Array sets_array = info[0].As(); + uint32_t sets_array_length = sets_array.Length(); + std::unique_ptr ctx{ + new blst::Pairing(true, module->_dst)}; + + for (uint32_t i = 0; i < sets_array_length; i++) { + blst::byte rand[BLST_TS_RANDOM_BYTES_LENGTH]; + if (!module->GetRandomBytes(rand, BLST_TS_RANDOM_BYTES_LENGTH)) { + Napi::Error::New( + env, "BLST_ERROR: Failed to generate random bytes") + .ThrowAsJavaScriptException(); + return scope.Escape(env.Undefined()); + } + + Napi::Value set_value = sets_array[i]; + if (!set_value.IsObject()) { + Napi::TypeError::New( + env, "BLST_ERROR: signatureSet must be an object") + .ThrowAsJavaScriptException(); + return scope.Escape(env.Undefined()); + } + Napi::Object set = set_value.As(); + + Napi::Value msg_value = set.Get("message"); + BLST_TS_UNWRAP_UINT_8_ARRAY( + msg_value, msg, "message", scope.Escape(env.Undefined())) + + Napi::Value pk_val = set.Get("publicKey"); + PointerGroup pk_ptr_group; + BLST_TS_UNWRAP_POINT_ARG( + env, + module, + pk_val, + pk_ptr_group, + has_error, + public_key, + PublicKey, + PUBLIC_KEY, + "PublicKey", + blst::P1_Affine, + 1, + CoordType::Affine, + _affine) + if (has_error) { + return scope.Escape(env.Undefined()); + } + + Napi::Value sig_val = set.Get("signature"); + PointerGroup sig_ptr_group; + BLST_TS_UNWRAP_POINT_ARG( + env, + module, + sig_val, + sig_ptr_group, + has_error, + signature, + Signature, + SIGNATURE, + "Signature", + blst::P2_Affine, + 2, + CoordType::Affine, + _affine) + if (has_error) { + return scope.Escape(env.Undefined()); + } + + blst::BLST_ERROR err = ctx->mul_n_aggregate( + pk_ptr_group.raw_pointer, + sig_ptr_group.raw_pointer, + rand, + BLST_TS_RANDOM_BYTES_LENGTH, + msg.Data(), + msg.ByteLength()); + if (err != blst::BLST_ERROR::BLST_SUCCESS) { + std::ostringstream msg; + msg << module->GetBlstErrorString(err) + << ": Invalid batch aggregation at index " << i; + Napi::Error::New(env, msg.str()).ThrowAsJavaScriptException(); + return scope.Escape(env.Undefined()); + } + } + ctx->commit(); + return Napi::Boolean::New(env, ctx->finalverify()); + } catch (...) { + return Napi::Boolean::New(env, false); + } +} + +typedef struct { + PointerGroup pk_ptr_group; + PointerGroup sig_ptr_group; + uint8_t *msg; + size_t msg_len; +} SignatureSet; + +class VerifyMultipleAggregateSignaturesWorker : public Napi::AsyncWorker { + public: + VerifyMultipleAggregateSignaturesWorker(const Napi::CallbackInfo &info) + : Napi:: + AsyncWorker{info.Env(), "VerifyMultipleAggregateSignaturesWorker"}, + m_deferred{Env()}, + m_has_error{false}, + m_module{Env().GetInstanceData()}, + m_ctx{new blst::Pairing(true, m_module->_dst)}, + m_sets{}, + m_result{false} { + Napi::Env env = Env(); + if (!info[0].IsArray()) { + Napi::Error::New( + env, "BLST_ERROR: signatureSets must be of type SignatureSet[]") + .ThrowAsJavaScriptException(); + m_has_error = true; + return; + } + Napi::Array sets_array = info[0].As(); + uint32_t sets_array_length = sets_array.Length(); + m_sets.reserve(sets_array_length); + + try { + for (uint32_t i = 0; i < sets_array_length; i++) { + Napi::Value set_value = sets_array[i]; + if (!set_value.IsObject()) { + Napi::Error::New( + env, "BLST_ERROR: signatureSet must be an object") + .ThrowAsJavaScriptException(); + m_has_error = true; + return; + } + Napi::Object set = set_value.As(); + + Napi::Value msg_value = set.Get("message"); + BLST_TS_UNWRAP_UINT_8_ARRAY(msg_value, msg, "message", ) + + m_sets.push_back( + {PointerGroup(), + PointerGroup(), + msg.Data(), + msg.ByteLength()}); + + Napi::Value pk_val = set.Get("publicKey"); + BLST_TS_UNWRAP_POINT_ARG( + env, + m_module, + pk_val, + m_sets[i].pk_ptr_group, + m_has_error, + public_key, + PublicKey, + PUBLIC_KEY, + "PublicKey", + blst::P1_Affine, + 1, + CoordType::Affine, + _affine) + if (m_has_error) { + return; + } + + Napi::Value sig_val = set.Get("signature"); + BLST_TS_UNWRAP_POINT_ARG( + env, + m_module, + sig_val, + m_sets[i].sig_ptr_group, + m_has_error, + signature, + Signature, + SIGNATURE, + "Signature", + blst::P2_Affine, + 2, + CoordType::Affine, + _affine) + if (m_has_error) { + return; + } + } + } catch (const blst::BLST_ERROR &err) { + Napi::Error::New(env, m_module->GetBlstErrorString(err)) + .ThrowAsJavaScriptException(); + m_has_error = true; + } + } + + /** + * GetPromise associated with _deferred for return to JS + */ + Napi::Promise GetPromise() { return m_deferred.Promise(); } + + protected: + void Execute() { + for (uint32_t i = 0; i < m_sets.size(); i++) { + blst::byte rand[BLST_TS_RANDOM_BYTES_LENGTH]; + if (!m_module->GetRandomBytes(rand, BLST_TS_RANDOM_BYTES_LENGTH)) { + SetError("BLST_ERROR: Failed to generate random bytes"); + return; + } + + blst::BLST_ERROR err = m_ctx->mul_n_aggregate( + m_sets[i].pk_ptr_group.raw_pointer, + m_sets[i].sig_ptr_group.raw_pointer, + rand, + BLST_TS_RANDOM_BYTES_LENGTH, + m_sets[i].msg, + m_sets[i].msg_len); + if (err != blst::BLST_ERROR::BLST_SUCCESS) { + std::ostringstream msg; + msg << m_module->GetBlstErrorString(err) + << ": Invalid batch aggregation at index " << i; + SetError(msg.str()); + return; + } + } + m_ctx->commit(); + m_result = m_ctx->finalverify(); + } + void OnOK() { m_deferred.Resolve(Napi::Boolean::New(Env(), m_result)); } + void OnError(const Napi::Error &err) { m_deferred.Reject(err.Value()); } + + public: + Napi::Promise::Deferred m_deferred; + bool m_has_error; + + private: + BlstTsAddon *m_module; + std::unique_ptr m_ctx; + std::vector m_sets; + bool m_result; +}; + +Napi::Value AsyncVerifyMultipleAggregateSignatures( + const Napi::CallbackInfo &info) { + VerifyMultipleAggregateSignaturesWorker *worker = + new VerifyMultipleAggregateSignaturesWorker(info); + if (worker->m_has_error) { + delete worker; + return info.Env().Undefined(); + } + worker->Queue(); + return worker->GetPromise(); +} + +typedef struct { + PointerGroup pk_ptr_group; + uint8_t *msg; + size_t msg_len; +} AggregateVerifySet; + +class AggregateVerifyWorker : public Napi::AsyncWorker { + public: + AggregateVerifyWorker(const Napi::CallbackInfo &info) + : Napi::AsyncWorker{info.Env(), "AggregateVerifyWorker"}, + m_deferred{Env()}, + m_has_error{false}, + m_module{Env().GetInstanceData()}, + m_ctx{new blst::Pairing(true, m_module->_dst)}, + m_sig_ptr_group(), + m_sets{}, + m_is_invalid{false}, + m_result{false} { + Napi::Env env = Env(); + try { + if (!info[0].IsArray()) { + Napi::TypeError::New( + env, "BLST_ERROR: msgs must be of type BlstBuffer[]") + .ThrowAsJavaScriptException(); + m_has_error = true; + return; + } + Napi::Array msgs_array = info[0].As(); + uint32_t msgs_array_length = msgs_array.Length(); + + if (!info[1].IsArray()) { + Napi::TypeError::New( + env, + "BLST_ERROR: publicKeys must be of type PublicKeyArg[]") + .ThrowAsJavaScriptException(); + m_has_error = true; + return; + } + Napi::Array pk_array = info[1].As(); + uint32_t pk_array_length = pk_array.Length(); + + Napi::Value sig_val = info[2]; + BLST_TS_UNWRAP_POINT_ARG( + env, + m_module, + sig_val, + m_sig_ptr_group, + m_has_error, + signature, + Signature, + SIGNATURE, + "Signature", + blst::P2_Affine, + 2, + CoordType::Affine, + _affine) + if (m_has_error) { + return; + } + + if (pk_array_length == 0) { + if (m_sig_ptr_group.raw_pointer->is_inf()) { + m_is_invalid = true; + return; + } + Napi::TypeError::New( + env, "BLST_ERROR: publicKeys must have length > 0") + .ThrowAsJavaScriptException(); + m_has_error = true; + return; + } + if (msgs_array_length == 0) { + Napi::TypeError::New( + env, "BLST_ERROR: msgs must have length > 0") + .ThrowAsJavaScriptException(); + m_has_error = true; + return; + } + if (msgs_array_length != pk_array_length) { + Napi::TypeError::New( + env, + "BLST_ERROR: msgs and publicKeys must be the same length") + .ThrowAsJavaScriptException(); + m_has_error = true; + return; + } + + m_sets.reserve(pk_array_length); + for (uint32_t i = 0; i < pk_array_length; i++) { + m_sets.push_back({PointerGroup(), nullptr, 0}); + + Napi::Value msg_value = msgs_array[i]; + BLST_TS_UNWRAP_UINT_8_ARRAY(msg_value, msg, "msg", ) + m_sets[i].msg = msg.Data(); + m_sets[i].msg_len = msg.ByteLength(); + + Napi::Value pk_val = pk_array[i]; + BLST_TS_UNWRAP_POINT_ARG( + env, + m_module, + pk_val, + m_sets[i].pk_ptr_group, + m_has_error, + public_key, + PublicKey, + PUBLIC_KEY, + "PublicKey", + blst::P1_Affine, + 1, + CoordType::Affine, + _affine) + if (m_has_error) { + return; + } + } + } catch (...) { + m_is_invalid = true; + } + } + + /** + * GetPromise associated with _deferred for return to JS + */ + Napi::Promise GetPromise() { return m_deferred.Promise(); } + + protected: + void Execute() { + if (m_is_invalid) { + return; + } + for (uint32_t i = 0; i < m_sets.size(); i++) { + blst::BLST_ERROR err = m_ctx->aggregate( + m_sets[i].pk_ptr_group.raw_pointer, + m_sig_ptr_group.raw_pointer, + m_sets[i].msg, + m_sets[i].msg_len); + if (err != blst::BLST_ERROR::BLST_SUCCESS) { + std::ostringstream msg; + msg << "BLST_ERROR::" << m_module->GetBlstErrorString(err) + << ": Invalid verification aggregate at index " << i; + SetError(msg.str()); + return; + } + } + m_ctx->commit(); + blst::PT pt{*m_sig_ptr_group.raw_pointer}; + m_result = m_ctx->finalverify(&pt); + } + void OnOK() { m_deferred.Resolve(Napi::Boolean::New(Env(), m_result)); } + void OnError(const Napi::Error &err) { m_deferred.Reject(err.Value()); } + + public: + Napi::Promise::Deferred m_deferred; + bool m_has_error; + + private: + BlstTsAddon *m_module; + std::unique_ptr m_ctx; + PointerGroup m_sig_ptr_group; + std::vector m_sets; + bool m_is_invalid; + bool m_result; +}; + +Napi::Value AsyncAggregateVerify(const Napi::CallbackInfo &info) { + AggregateVerifyWorker *worker = new AggregateVerifyWorker(info); + if (worker->m_has_error) { + delete worker; + return info.Env().Undefined(); + } + worker->Queue(); + return worker->GetPromise(); +} +} // anonymous namespace + +namespace Functions { +void Init(const Napi::Env &env, Napi::Object &exports) { + exports.Set( + Napi::String::New(env, "aggregatePublicKeys"), + Napi::Function::New(env, AggregatePublicKeys)); + exports.Set( + Napi::String::New(env, "aggregateSignatures"), + Napi::Function::New(env, AggregateSignatures)); + exports.Set( + Napi::String::New(env, "aggregateVerify"), + Napi::Function::New(env, AggregateVerify)); + exports.Set( + Napi::String::New(env, "asyncAggregateVerify"), + Napi::Function::New(env, AsyncAggregateVerify)); + exports.Set( + Napi::String::New(env, "verifyMultipleAggregateSignatures"), + Napi::Function::New(env, VerifyMultipleAggregateSignatures)); + exports.Set( + Napi::String::New(env, "asyncVerifyMultipleAggregateSignatures"), + Napi::Function::New(env, AsyncVerifyMultipleAggregateSignatures)); +} +} // namespace Functions diff --git a/rebuild/src/functions.h b/rebuild/src/functions.h new file mode 100644 index 00000000..ab11750c --- /dev/null +++ b/rebuild/src/functions.h @@ -0,0 +1,22 @@ +#ifndef BLST_TS_FUNCTIONS_H__ +#define BLST_TS_FUNCTIONS_H__ + +#include "addon.h" +#include "blst.hpp" +#include "napi.h" + +/** + * @note The unique_ptr is used to hold the blst::P* object if a Uint8Array is + * being converted instead of a PublicKey or Signature object. + */ +template +struct PointerGroup { + std::unique_ptr unique_ptr = std::make_unique(); + T *raw_pointer = nullptr; +}; + +namespace Functions { +void Init(const Napi::Env &env, Napi::Object &exports); +} + +#endif /* BLST_TS_FUNCTIONS_H__ */ diff --git a/rebuild/src/public_key.cc b/rebuild/src/public_key.cc index 57791524..19454e22 100644 --- a/rebuild/src/public_key.cc +++ b/rebuild/src/public_key.cc @@ -36,12 +36,12 @@ void PublicKey::Init( } Napi::Value PublicKey::Deserialize(const Napi::CallbackInfo &info) { - BLST_TS_FUNCTION_PREAMBLE + BLST_TS_FUNCTION_PREAMBLE(info, env, module) Napi::Value pk_bytes_value = info[0]; BLST_TS_UNWRAP_UINT_8_ARRAY( pk_bytes_value, pk_bytes, "pkBytes", scope.Escape(env.Undefined())) - std::string err_out{"pkBytes"}; + std::string err_out{"BLST_ERROR: pkBytes"}; if (!is_valid_length( err_out, pk_bytes.ByteLength(), @@ -59,7 +59,8 @@ Napi::Value PublicKey::Deserialize(const Napi::CallbackInfo &info) { if (!info[1].IsUndefined()) { Napi::Value type_val = info[1].As(); if (!type_val.IsNumber()) { - Napi::TypeError::New(env, "type must be of enum CoordType (number)") + Napi::TypeError::New( + env, "BLST_ERROR: type must be of enum CoordType (number)") .ThrowAsJavaScriptException(); return scope.Escape(env.Undefined()); } @@ -105,7 +106,7 @@ PublicKey::PublicKey(const Napi::CallbackInfo &info) .ThrowAsJavaScriptException(); return; } -}; +} Napi::Value PublicKey::Serialize(const Napi::CallbackInfo &info) { BLST_TS_SERIALIZE_POINT(PUBLIC_KEY, "PublicKey"); @@ -117,22 +118,22 @@ Napi::Value PublicKey::KeyValidate(const Napi::CallbackInfo &info) { if (_has_jacobian) { if (_jacobian->is_inf()) { - Napi::Error::New(env, "blst::BLST_PK_IS_INFINITY") + Napi::Error::New(env, "BLST_ERROR::BLST_PK_IS_INFINITY") .ThrowAsJavaScriptException(); } else if (!_jacobian->in_group()) { - Napi::Error::New(env, "blst::BLST_POINT_NOT_IN_GROUP") + Napi::Error::New(env, "BLST_ERROR::BLST_POINT_NOT_IN_GROUP") .ThrowAsJavaScriptException(); } } else if (_has_affine) { if (_affine->is_inf()) { - Napi::Error::New(env, "blst::BLST_PK_IS_INFINITY") + Napi::Error::New(env, "BLST_ERROR::BLST_PK_IS_INFINITY") .ThrowAsJavaScriptException(); } else if (!_affine->in_group()) { - Napi::Error::New(env, "blst::BLST_POINT_NOT_IN_GROUP") + Napi::Error::New(env, "BLST_ERROR::BLST_POINT_NOT_IN_GROUP") .ThrowAsJavaScriptException(); } } else { - Napi::Error::New(env, "blst::BLST_PK_IS_INFINITY") + Napi::Error::New(env, "BLST_ERROR::BLST_PK_IS_INFINITY") .ThrowAsJavaScriptException(); } diff --git a/rebuild/src/secret_key.cc b/rebuild/src/secret_key.cc index fb72f8d8..6ca03b97 100644 --- a/rebuild/src/secret_key.cc +++ b/rebuild/src/secret_key.cc @@ -43,7 +43,7 @@ void SecretKey::Init( } Napi::Value SecretKey::FromKeygen(const Napi::CallbackInfo &info) { - BLST_TS_FUNCTION_PREAMBLE + BLST_TS_FUNCTION_PREAMBLE(info, env, module) Napi::Value ikm_value = info[0]; BLST_TS_UNWRAP_UINT_8_ARRAY( @@ -61,7 +61,7 @@ Napi::Value SecretKey::FromKeygen(const Napi::CallbackInfo &info) { // optional parameter from blst library. if (!info[1].IsUndefined()) { if (!info[1].IsString()) { - Napi::TypeError::New(env, "info must be a string") + Napi::TypeError::New(env, "BLST_ERROR: info must be a string") .ThrowAsJavaScriptException(); return env.Undefined(); } @@ -85,12 +85,12 @@ Napi::Value SecretKey::FromKeygen(const Napi::CallbackInfo &info) { } Napi::Value SecretKey::Deserialize(const Napi::CallbackInfo &info) { - BLST_TS_FUNCTION_PREAMBLE + BLST_TS_FUNCTION_PREAMBLE(info, env, module) Napi::Value sk_bytes_value = info[0]; BLST_TS_UNWRAP_UINT_8_ARRAY( sk_bytes_value, sk_bytes, "skBytes", scope.Escape(env.Undefined())) - std::string err_out{"skBytes"}; + std::string err_out{"BLST_ERROR: skBytes"}; if (!is_valid_length( err_out, sk_bytes.ByteLength(), BLST_TS_SECRET_KEY_LENGTH)) { Napi::TypeError::New(env, err_out).ThrowAsJavaScriptException(); @@ -125,7 +125,7 @@ SecretKey::SecretKey(const Napi::CallbackInfo &info) .ThrowAsJavaScriptException(); return; } -}; +} Napi::Value SecretKey::Serialize(const Napi::CallbackInfo &info) { Napi::Env env = info.Env(); @@ -139,7 +139,7 @@ Napi::Value SecretKey::Serialize(const Napi::CallbackInfo &info) { } Napi::Value SecretKey::ToPublicKey(const Napi::CallbackInfo &info) { - BLST_TS_FUNCTION_PREAMBLE + BLST_TS_FUNCTION_PREAMBLE(info, env, module) BLST_TS_CREATE_JHEAP_OBJECT(wrapped, public_key, PublicKey, pk) // Derive public key from secret key. Default to jacobian coordinates @@ -150,11 +150,12 @@ Napi::Value SecretKey::ToPublicKey(const Napi::CallbackInfo &info) { } Napi::Value SecretKey::Sign(const Napi::CallbackInfo &info) { - BLST_TS_FUNCTION_PREAMBLE + BLST_TS_FUNCTION_PREAMBLE(info, env, module) // Check for zero key and throw error to meet spec requirements if (_is_zero_key) { - Napi::TypeError::New(env, "cannot sign message with zero private key") + Napi::TypeError::New( + env, "BLST_ERROR: cannot sign message with zero private key") .ThrowAsJavaScriptException(); return scope.Escape(info.Env().Undefined()); } diff --git a/rebuild/src/signature.cc b/rebuild/src/signature.cc index 8c9bfef4..2328e361 100644 --- a/rebuild/src/signature.cc +++ b/rebuild/src/signature.cc @@ -36,12 +36,12 @@ void Signature::Init( } Napi::Value Signature::Deserialize(const Napi::CallbackInfo &info) { - BLST_TS_FUNCTION_PREAMBLE + BLST_TS_FUNCTION_PREAMBLE(info, env, module) Napi::Value sig_bytes_value = info[0]; BLST_TS_UNWRAP_UINT_8_ARRAY( sig_bytes_value, sig_bytes, "sigBytes", scope.Escape(env.Undefined())) - std::string err_out{"sigBytes"}; + std::string err_out{"BLST_ERROR: sigBytes"}; if (!is_valid_length( err_out, sig_bytes.ByteLength(), @@ -59,7 +59,8 @@ Napi::Value Signature::Deserialize(const Napi::CallbackInfo &info) { if (!info[1].IsUndefined()) { Napi::Value type_val = info[1].As(); if (!type_val.IsNumber()) { - Napi::TypeError::New(env, "type must be of enum CoordType (number)") + Napi::TypeError::New( + env, "BLST_ERROR: type must be of enum CoordType (number)") .ThrowAsJavaScriptException(); return scope.Escape(env.Undefined()); } @@ -101,7 +102,7 @@ Signature::Signature(const Napi::CallbackInfo &info) .ThrowAsJavaScriptException(); return; } -}; +} Napi::Value Signature::Serialize(const Napi::CallbackInfo &info){ BLST_TS_SERIALIZE_POINT(SIGNATURE, "Signature")} @@ -111,13 +112,13 @@ Napi::Value Signature::SigValidate(const Napi::CallbackInfo &info) { Napi::EscapableHandleScope scope(env); if (!_has_jacobian && !_has_affine) { - Napi::Error::New(env, "Signature not initialized") + Napi::Error::New(env, "BLST_ERROR: Signature not initialized") .ThrowAsJavaScriptException(); } else if (_has_jacobian && !_jacobian->in_group()) { - Napi::Error::New(env, "blst::BLST_POINT_NOT_IN_GROUP") + Napi::Error::New(env, "BLST_ERROR::BLST_POINT_NOT_IN_GROUP") .ThrowAsJavaScriptException(); } else if (_has_affine && !_affine->in_group()) { - Napi::Error::New(env, "blst::BLST_POINT_NOT_IN_GROUP") + Napi::Error::New(env, "BLST_ERROR::BLST_POINT_NOT_IN_GROUP") .ThrowAsJavaScriptException(); } diff --git a/rebuild/test/__fixtures__/index.ts b/rebuild/test/__fixtures__/index.ts index b5e2fafb..b3a88047 100644 --- a/rebuild/test/__fixtures__/index.ts +++ b/rebuild/test/__fixtures__/index.ts @@ -1,4 +1,4 @@ -import {fromHex, getFilledUint8, makeNapiTestSet, makeNapiTestSets, sullyUint8Array} from "../utils"; +import {fromHex, getFilledUint8, makeNapiTestSet, makeNapiTestSets, sullyUint8Array} from "../utils.js"; export const invalidInputs: [string, any][] = [ ["boolean", true], @@ -61,9 +61,9 @@ export const validSignature = { export const badSignature = sullyUint8Array(makeNapiTestSet().signature.serialize(false)); export const validSignatureSet = makeNapiTestSets(1).map((set) => { - const {msg, secretKey, publicKey, signature} = set; + const {message, secretKey, publicKey, signature} = set; return { - msg, + message, secretKey, publicKey, signature, diff --git a/rebuild/test/spec/downloadTests.ts b/rebuild/test/spec/downloadTests.ts new file mode 100644 index 00000000..ab7ec340 --- /dev/null +++ b/rebuild/test/spec/downloadTests.ts @@ -0,0 +1,72 @@ +/* eslint-disable no-console */ +import fs from "node:fs"; +import path from "node:path"; +import {execSync} from "node:child_process"; +import tar from "tar"; +import fetch from "node-fetch"; +import { + SPEC_TEST_LOCATION, + SPEC_TEST_VERSION, + SPEC_TEST_REPO_URL, + SPEC_TEST_TO_DOWNLOAD, +} from "./specTestVersioning.js"; + +const specVersion = SPEC_TEST_VERSION; +const outputDir = SPEC_TEST_LOCATION; +const specTestsRepoUrl = SPEC_TEST_REPO_URL; + +const versionFile = path.join(outputDir, "version.txt"); +const existingVersion = fs.existsSync(versionFile) ? fs.readFileSync(versionFile, "utf8").trim() : "none"; + +if (existingVersion === specVersion) { + console.log(`version ${specVersion} already downloaded`); + process.exit(0); +} else { + console.log(`Downloading new version: ${specVersion} existingVersion: ${existingVersion}`); +} + +if (fs.existsSync(outputDir)) { + console.log(`Cleaning existing version ${existingVersion} at ${outputDir}`); + shell(`rm -rf ${outputDir}`); +} + +fs.mkdirSync(outputDir, {recursive: true}); + +const urls = SPEC_TEST_TO_DOWNLOAD.map((test) => `${specTestsRepoUrl}/releases/download/${specVersion}/${test}.tar.gz`); + +downloadAndExtract(urls, outputDir) + .then(() => { + console.log("Downloads and extractions complete."); + fs.writeFileSync(versionFile, specVersion); + }) + .catch((error) => { + console.error(`Error downloading test files: ${error}`); + process.exit(1); + }); + +function shell(cmd: string): string { + try { + return execSync(cmd, {encoding: "utf8"}).trim(); + } catch (error) { + console.error(`Error executing shell command: ${cmd}`); + throw error; + } +} + +async function downloadAndExtract(urls: string[], outputDir: string): Promise { + for (const url of urls) { + const fileName = url.split("/").pop(); + const filePath = path.resolve(outputDir, String(fileName)); + const response = await fetch(url); + if (!response.ok || !response.body) { + throw new Error(`Failed to download ${url}`); + } + + await fs.promises.writeFile(filePath, response.body); + + await tar.x({ + file: filePath, + cwd: outputDir, + }); + } +} diff --git a/rebuild/test/spec/index.test.ts b/rebuild/test/spec/index.test.ts new file mode 100644 index 00000000..762dc6cb --- /dev/null +++ b/rebuild/test/spec/index.test.ts @@ -0,0 +1,234 @@ +import {expect} from "chai"; +import fs from "fs"; +import path from "path"; +import jsYaml from "js-yaml"; +import {SPEC_TEST_LOCATION} from "./specTestVersioning.js"; +import { + PublicKey, + SecretKey, + Signature, + aggregatePublicKeys, + aggregateSignatures, + verify as VERIFY, + aggregateVerify, + fastAggregateVerify, +} from "../../lib/index.js"; +import {fromHex, toHex} from "../utils.js"; + +// Example full path +// blst-ts/spec-tests/tests/general/altair/bls/eth_aggregate_pubkeys/small/eth_aggregate_pubkeys_empty_list + +const G2_POINT_AT_INFINITY = + "0xc00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; +const G1_POINT_AT_INFINITY = + "0xc00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; + +const testRootDirByFork = path.join(SPEC_TEST_LOCATION, "tests/general"); +for (const fork of fs.readdirSync(testRootDirByFork)) { + // fork = "phase0" | "altair" + const testRootDirFork = path.join(testRootDirByFork, fork, "bls"); + + if (!fs.existsSync(testRootDirFork)) continue; + + for (const testType of fs.readdirSync(testRootDirFork)) { + // testType = "eth_aggregate_pubkeys" | "fast_aggregate_verify" | ... + const testTypeDir = path.join(testRootDirFork, testType); + describe(path.join(fork, testType), () => { + const testFnByType: Record any> = { + aggregate, + aggregate_verify, + eth_aggregate_pubkeys, + eth_fast_aggregate_verify, + fast_aggregate_verify, + sign, + verify, + }; + const testFn = testFnByType[testType]; + + before("Known testFn", () => { + if (!testFn) throw Error(`Unknown testFn ${testType}`); + }); + + for (const testCaseGroup of fs.readdirSync(testTypeDir)) { + // testCaseGroup = "small" + const testCaseGroupDir = path.join(testTypeDir, testCaseGroup); + + for (const testCase of fs.readdirSync(testCaseGroupDir)) { + // testCase = "eth_aggregate_pubkeys_empty_list" + const testCaseDir = path.join(testCaseGroupDir, testCase); + + it(testCase, () => { + // Ensure there are no unknown files + const files = fs.readdirSync(testCaseDir); + expect(files).to.deep.equal(["data.yaml"], `Unknown files in ${testCaseDir}`); + + // Examples of parsed YAML + // { + // input: [ + // '0x91347bccf740d859038fcdcaf233eeceb2a436bcaaee9b2aa3bfb70efe29dfb2677562ccbea1c8e061fb9971b0753c240622fab78489ce96768259fc01360346da5b9f579e5da0d941e4c6ba18a0e64906082375394f337fa1af2b7127b0d121', + // '0x9674e2228034527f4c083206032b020310face156d4a4685e2fcaec2f6f3665aa635d90347b6ce124eb879266b1e801d185de36a0a289b85e9039662634f2eea1e02e670bc7ab849d006a70b2f93b84597558a05b879c8d445f387a5d5b653df', + // '0xae82747ddeefe4fd64cf9cedb9b04ae3e8a43420cd255e3c7cd06a8d88b7c7f8638543719981c5d16fa3527c468c25f0026704a6951bde891360c7e8d12ddee0559004ccdbe6046b55bae1b257ee97f7cdb955773d7cf29adf3ccbb9975e4eb9' + // ], + // output: '0x9712c3edd73a209c742b8250759db12549b3eaf43b5ca61376d9f30e2747dbcf842d8b2ac0901d2a093713e20284a7670fcf6954e9ab93de991bb9b313e664785a075fc285806fa5224c82bde146561b446ccfc706a64b8579513cfc4ff1d930' + // } + // + // { + // input: ['0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'], + // output: null + // } + // + // { + // input: ..., + // output: false + // } + + const testData = jsYaml.load(fs.readFileSync(path.join(testCaseDir, "data.yaml"), "utf8")) as { + input: unknown; + output: unknown; + }; + + if (process.env.DEBUG) { + // eslint-disable-next-line no-console + console.log(testData); + } + + try { + expect(testFn(testData.input)).to.deep.equal(testData.output); + } catch (e) { + // spec test expect a boolean even for invalid inputs + if (!isBlstError(e)) throw e; + + expect(false).to.deep.equal(Boolean(testData.output)); + } + }); + } + } + }); + } +} + +/** + * ``` + * input: List[BLS Signature] -- list of input BLS signatures + * output: BLS Signature -- expected output, single BLS signature or empty. + * ``` + */ +function aggregate(input: string[]): string | null { + const agg = aggregateSignatures(input.map((hex) => Signature.deserialize(fromHex(hex)))); + return toHex(agg.serialize()); +} + +/** + * ``` + * input: + * pubkeys: List[BLS Pubkey] -- the pubkeys + * messages: List[bytes32] -- the messages + * signature: BLS Signature -- the signature to verify against pubkeys and messages + * output: bool -- true (VALID) or false (INVALID) + * ``` + */ +function aggregate_verify(input: {pubkeys: string[]; messages: string[]; signature: string}): boolean { + const {pubkeys, messages, signature} = input; + return aggregateVerify( + messages.map(fromHex), + pubkeys.map((hex) => PublicKey.deserialize(fromHex(hex))), + Signature.deserialize(fromHex(signature)) + ); +} + +/** + * ``` + * input: List[BLS Signature] -- list of input BLS signatures + * output: BLS Signature -- expected output, single BLS signature or empty. + * ``` + */ +function eth_aggregate_pubkeys(input: string[]): string | null { + // Don't add this checks in the source as beacon nodes check the pubkeys for inf when onboarding + for (const pk of input) { + if (pk === G1_POINT_AT_INFINITY) return null; + } + + const agg = aggregatePublicKeys(input.map((hex) => PublicKey.deserialize(fromHex(hex)))); + return toHex(agg.serialize()); +} + +/** + * ``` + * input: + * pubkeys: List[BLS Pubkey] -- list of input BLS pubkeys + * message: bytes32 -- the message + * signature: BLS Signature -- the signature to verify against pubkeys and message + * output: bool -- true (VALID) or false (INVALID) + * ``` + */ +function eth_fast_aggregate_verify(input: {pubkeys: string[]; message: string; signature: string}): boolean { + const {pubkeys, message, signature} = input; + + if (pubkeys.length === 0 && signature === G2_POINT_AT_INFINITY) { + return true; + } + + // Don't add this checks in the source as beacon nodes check the pubkeys for inf when onboarding + for (const pk of pubkeys) { + if (pk === G1_POINT_AT_INFINITY) return false; + } + + return fastAggregateVerify( + fromHex(message), + pubkeys.map((hex) => PublicKey.deserialize(fromHex(hex))), + Signature.deserialize(fromHex(signature)) + ); +} + +/** + * ``` + * input: + * pubkeys: List[BLS Pubkey] -- list of input BLS pubkeys + * message: bytes32 -- the message + * signature: BLS Signature -- the signature to verify against pubkeys and message + * output: bool -- true (VALID) or false (INVALID) + * ``` + */ +function fast_aggregate_verify(input: {pubkeys: string[]; message: string; signature: string}): boolean | null { + const {pubkeys, message, signature} = input; + + // Don't add this checks in the source as beacon nodes check the pubkeys for inf when onboarding + for (const pk of pubkeys) { + if (pk === G1_POINT_AT_INFINITY) return false; + } + + return fastAggregateVerify( + fromHex(message), + pubkeys.map((hex) => PublicKey.deserialize(fromHex(hex))), + Signature.deserialize(fromHex(signature)) + ); +} + +/** + * input: + * privkey: bytes32 -- the private key used for signing + * message: bytes32 -- input message to sign (a hash) + * output: BLS Signature -- expected output, single BLS signature or empty. + */ +function sign(input: {privkey: string; message: string}): string | null { + const {privkey, message} = input; + const sk = SecretKey.deserialize(fromHex(privkey)); + const signature = sk.sign(fromHex(message)); + return toHex(signature.serialize()); +} + +/** + * input: + * pubkey: bytes48 -- the pubkey + * message: bytes32 -- the message + * signature: bytes96 -- the signature to verify against pubkey and message + * output: bool -- VALID or INVALID + */ +function verify(input: {pubkey: string; message: string; signature: string}): boolean { + const {pubkey, message, signature} = input; + return VERIFY(fromHex(message), PublicKey.deserialize(fromHex(pubkey)), Signature.deserialize(fromHex(signature))); +} + +function isBlstError(e: unknown): boolean { + return (e as Error).message.includes("BLST_ERROR"); +} diff --git a/rebuild/test/spec/specTestVersioning.ts b/rebuild/test/spec/specTestVersioning.ts new file mode 100644 index 00000000..336e2b13 --- /dev/null +++ b/rebuild/test/spec/specTestVersioning.ts @@ -0,0 +1,17 @@ +import {join, dirname} from "path"; +import {fileURLToPath} from "url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +// WARNING! Don't move or rename this file !!! +// +// This file is used to generate the cache ID for spec tests download in Github Actions CI +// It's path is hardcoded in: `.github/workflows/test-spec.yml` +// +// The contents of this file MUST include the URL, version and target path, and nothing else. + +export const SPEC_TEST_REPO_URL = "https://github.com/ethereum/consensus-spec-tests"; +export const SPEC_TEST_VERSION = "v1.3.0"; +export const SPEC_TEST_TO_DOWNLOAD = ["general" as const]; +// Target directory is the host package root: '/spec-tests' +export const SPEC_TEST_LOCATION = join(__dirname, "../../spec-tests"); diff --git a/rebuild/test/types.ts b/rebuild/test/types.ts index 96c484fd..6cf6079c 100644 --- a/rebuild/test/types.ts +++ b/rebuild/test/types.ts @@ -1,9 +1,9 @@ -import * as bindings from "../lib"; +import * as bindings from "../lib/index.js"; export type BufferLike = string | bindings.BlstBuffer; export interface NapiTestSet { - msg: Uint8Array; + message: Uint8Array; secretKey: bindings.SecretKey; publicKey: bindings.PublicKey; signature: bindings.Signature; diff --git a/rebuild/test/unit/PublicKey.test.ts b/rebuild/test/unit/PublicKey.test.ts index c5239481..9bc0f779 100644 --- a/rebuild/test/unit/PublicKey.test.ts +++ b/rebuild/test/unit/PublicKey.test.ts @@ -1,7 +1,13 @@ import {expect} from "chai"; -import {BLST_CONSTANTS, CoordType, PublicKey, SecretKey} from "../../lib"; -import {expectEqualHex, expectNotEqualHex, sullyUint8Array} from "../utils"; -import {validPublicKey, SECRET_KEY_BYTES, invalidInputs, badPublicKey, G1_POINT_AT_INFINITY} from "../__fixtures__"; +import {BLST_CONSTANTS, CoordType, PublicKey, SecretKey} from "../../lib/index.js"; +import {expectEqualHex, expectNotEqualHex, sullyUint8Array} from "../utils.js"; +import { + validPublicKey, + SECRET_KEY_BYTES, + invalidInputs, + badPublicKey, + G1_POINT_AT_INFINITY, +} from "../__fixtures__/index.js"; describe("PublicKey", () => { it("should exist", () => { diff --git a/rebuild/test/unit/SecretKey.test.ts b/rebuild/test/unit/SecretKey.test.ts index e695c5be..6ab66474 100644 --- a/rebuild/test/unit/SecretKey.test.ts +++ b/rebuild/test/unit/SecretKey.test.ts @@ -1,7 +1,7 @@ import {expect} from "chai"; -import {PublicKey, SecretKey, Signature, BLST_CONSTANTS} from "../../lib"; -import {KEY_MATERIAL, SECRET_KEY_BYTES, invalidInputs} from "../__fixtures__"; -import {expectEqualHex, expectNotEqualHex} from "../utils"; +import {PublicKey, SecretKey, Signature, BLST_CONSTANTS} from "../../lib/index.js"; +import {KEY_MATERIAL, SECRET_KEY_BYTES, invalidInputs} from "../__fixtures__/index.js"; +import {expectEqualHex, expectNotEqualHex} from "../utils.js"; describe("SecretKey", () => { it("should exist", () => { diff --git a/rebuild/test/unit/Signature.test.ts b/rebuild/test/unit/Signature.test.ts index fb5c9309..908f6095 100644 --- a/rebuild/test/unit/Signature.test.ts +++ b/rebuild/test/unit/Signature.test.ts @@ -1,7 +1,7 @@ import {expect} from "chai"; -import {BLST_CONSTANTS, CoordType, SecretKey, Signature} from "../../lib"; -import {expectEqualHex, expectNotEqualHex, sullyUint8Array} from "../utils"; -import {KEY_MATERIAL, invalidInputs, validSignature} from "../__fixtures__"; +import {BLST_CONSTANTS, CoordType, SecretKey, Signature} from "../../lib/index.js"; +import {expectEqualHex, expectNotEqualHex, sullyUint8Array} from "../utils.js"; +import {KEY_MATERIAL, invalidInputs, validSignature} from "../__fixtures__/index.js"; describe("Signature", () => { it("should exist", () => { @@ -48,38 +48,38 @@ describe("Signature", () => { expect(() => Signature.deserialize(sullyUint8Array(validSignature.compressed))).to.throw("BLST_BAD_ENCODING"); }); }); - describe("methods", () => { - describe("serialize", () => { - const sig = SecretKey.fromKeygen(KEY_MATERIAL).sign(Buffer.from("some fancy message")); - it("should default to compressed serialization", () => { - expectEqualHex(sig.serialize(), sig.serialize(true)); - expectNotEqualHex(sig.serialize(), sig.serialize(false)); - }); - it("should serialize compressed to the correct length", () => { - expect(sig.serialize(true)).to.have.lengthOf(BLST_CONSTANTS.SIGNATURE_LENGTH_COMPRESSED); - }); - it("should serialize uncompressed to the correct length", () => { - expect(sig.serialize(false)).to.have.lengthOf(BLST_CONSTANTS.SIGNATURE_LENGTH_UNCOMPRESSED); - }); - it("should serialize affine and jacobian points to the same value", () => { - const jacobian = Signature.deserialize(sig.serialize(), CoordType.jacobian); - const affine = Signature.deserialize(sig.serialize(), CoordType.affine); - expectEqualHex(jacobian.serialize(true), affine.serialize(true)); - expectEqualHex(jacobian.serialize(false), affine.serialize(false)); - }); + }); + describe("methods", () => { + describe("serialize", () => { + const sig = SecretKey.fromKeygen(KEY_MATERIAL).sign(Buffer.from("some fancy message")); + it("should default to compressed serialization", () => { + expectEqualHex(sig.serialize(), sig.serialize(true)); + expectNotEqualHex(sig.serialize(), sig.serialize(false)); }); - describe("sigValidate()", () => { - it("should return undefined for valid", () => { - const sig = Signature.deserialize(validSignature.compressed); - expect(sig.sigValidate()).to.be.undefined; - }); - it("should throw for invalid", () => { - const pkSeed = Signature.deserialize(validSignature.compressed); - const sig = Signature.deserialize( - Uint8Array.from([...pkSeed.serialize().subarray(0, 94), ...Buffer.from("a1")]) - ); - expect(() => sig.sigValidate()).to.throw("blst::BLST_POINT_NOT_IN_GROUP"); - }); + it("should serialize compressed to the correct length", () => { + expect(sig.serialize(true)).to.have.lengthOf(BLST_CONSTANTS.SIGNATURE_LENGTH_COMPRESSED); + }); + it("should serialize uncompressed to the correct length", () => { + expect(sig.serialize(false)).to.have.lengthOf(BLST_CONSTANTS.SIGNATURE_LENGTH_UNCOMPRESSED); + }); + it("should serialize affine and jacobian points to the same value", () => { + const jacobian = Signature.deserialize(sig.serialize(), CoordType.jacobian); + const affine = Signature.deserialize(sig.serialize(), CoordType.affine); + expectEqualHex(jacobian.serialize(true), affine.serialize(true)); + expectEqualHex(jacobian.serialize(false), affine.serialize(false)); + }); + }); + describe("sigValidate()", () => { + it("should return undefined for valid", () => { + const sig = Signature.deserialize(validSignature.compressed); + expect(sig.sigValidate()).to.be.undefined; + }); + it("should throw for invalid", () => { + const pkSeed = Signature.deserialize(validSignature.compressed); + const sig = Signature.deserialize( + Uint8Array.from([...pkSeed.serialize().subarray(0, 94), ...Buffer.from("a1")]) + ); + expect(() => sig.sigValidate()).to.throw("BLST_ERROR::BLST_POINT_NOT_IN_GROUP"); }); }); }); diff --git a/rebuild/test/unit/aggregatePublicKeys.test.ts b/rebuild/test/unit/aggregatePublicKeys.test.ts new file mode 100644 index 00000000..84387d9b --- /dev/null +++ b/rebuild/test/unit/aggregatePublicKeys.test.ts @@ -0,0 +1,30 @@ +import {expect} from "chai"; +import {aggregatePublicKeys, PublicKey} from "../../lib/index.js"; +import {isEqualBytes, makeNapiTestSets} from "../utils.js"; +import {badPublicKey} from "../__fixtures__/index.js"; + +describe("Aggregate Public Keys", () => { + const sets = makeNapiTestSets(10); + const keys = sets.map(({publicKey}) => publicKey); + + describe("aggregatePublicKeys()", () => { + it("should return the promise of a PublicKey", () => { + const agg = aggregatePublicKeys(keys); + expect(agg).to.be.instanceOf(PublicKey); + }); + it("should be able to keyValidate PublicKey", () => { + const agg = aggregatePublicKeys(keys); + expect(agg.keyValidate()).to.be.undefined; + }); + it("should throw for invalid PublicKey", () => { + expect(() => aggregatePublicKeys(keys.concat(badPublicKey as unknown as PublicKey))).to.throw( + "BLST_ERROR::BLST_BAD_ENCODING: Invalid key at index 10" + ); + }); + it("should return a key that is not in the keys array", () => { + const agg = aggregatePublicKeys(keys); + const serialized = agg.serialize(); + expect(keys.find((key) => isEqualBytes(key.serialize(), serialized))).to.be.undefined; + }); + }); +}); diff --git a/rebuild/test/unit/aggregateSignatures.test.ts b/rebuild/test/unit/aggregateSignatures.test.ts new file mode 100644 index 00000000..cff9f29a --- /dev/null +++ b/rebuild/test/unit/aggregateSignatures.test.ts @@ -0,0 +1,30 @@ +import {expect} from "chai"; +import {aggregateSignatures, Signature} from "../../lib/index.js"; +import {isEqualBytes, makeNapiTestSets} from "../utils.js"; +import {badSignature} from "../__fixtures__/index.js"; + +describe("Aggregate Signatures", () => { + const sets = makeNapiTestSets(10); + const signatures = sets.map(({signature}) => signature); + + describe("aggregateSignatures()", () => { + it("should return a Signature", () => { + const agg = aggregateSignatures(signatures); + expect(agg).to.be.instanceOf(Signature); + }); + it("should be able to keyValidate Signature", () => { + const agg = aggregateSignatures(signatures); + expect(agg.sigValidate()).to.be.undefined; + }); + it("should throw for invalid Signature", () => { + expect(() => aggregateSignatures(signatures.concat(badSignature as unknown as Signature))).to.throw( + "BLST_ERROR::BLST_BAD_ENCODING - Invalid signature at index 10" + ); + }); + it("should return a key that is not in the keys array", () => { + const agg = aggregateSignatures(signatures); + const serialized = agg.serialize(); + expect(signatures.find((sig) => isEqualBytes(sig.serialize(), serialized))).to.be.undefined; + }); + }); +}); diff --git a/rebuild/test/unit/bindings.test.ts b/rebuild/test/unit/bindings.test.ts index b92d1b12..00cf5129 100644 --- a/rebuild/test/unit/bindings.test.ts +++ b/rebuild/test/unit/bindings.test.ts @@ -1,7 +1,54 @@ import {expect} from "chai"; -import * as bindings from "../../lib"; +import * as bindings from "../../lib/index.js"; describe("bindings", () => { + describe("exports", () => { + const exports = new Set(Object.keys(bindings)); + exports.delete("path"); + exports.delete("default"); + + const expectedFunctions = [ + "aggregatePublicKeys", + "aggregateSignatures", + "verify", + "asyncVerify", + "fastAggregateVerify", + "asyncFastAggregateVerify", + "aggregateVerify", + "asyncAggregateVerify", + "verifyMultipleAggregateSignatures", + "asyncVerifyMultipleAggregateSignatures", + ]; + const expectedClasses = ["PublicKey", "SecretKey", "Signature"]; + const expectedConstants = ["CoordType", "BLST_CONSTANTS"]; + after(() => { + expect(exports.size).to.equal(0); + }); + it("should export all the expected functions", () => { + for (const expected of expectedFunctions) { + if (!exports.has(expected)) { + throw new Error(`Missing export: ${expected}`); + } + exports.delete(expected); + } + }); + it("should export all the expected classes", () => { + for (const expected of expectedClasses) { + if (!exports.has(expected)) { + throw new Error(`Missing export: ${expected}`); + } + exports.delete(expected); + } + }); + it("should export all the expected constants", () => { + for (const expected of expectedConstants) { + if (!exports.has(expected)) { + throw new Error(`Missing export: ${expected}`); + } + exports.delete(expected); + } + }); + }); describe("constants", () => { const { DST, diff --git a/rebuild/test/unit/verify.test.ts b/rebuild/test/unit/verify.test.ts new file mode 100644 index 00000000..9330fc14 --- /dev/null +++ b/rebuild/test/unit/verify.test.ts @@ -0,0 +1,151 @@ +import {expect} from "chai"; +import { + aggregateVerify, + asyncAggregateVerify, + asyncFastAggregateVerify, + asyncVerify, + fastAggregateVerify, + verify, +} from "../../lib/index.js"; +import {sullyUint8Array, makeNapiTestSets} from "../utils.js"; +import {NapiTestSet} from "../types.js"; + +describe("Verify", () => { + let testSet: NapiTestSet; + before(() => { + testSet = makeNapiTestSets(1)[0]; + }); + describe("verify", () => { + it("should return a boolean", () => { + expect(verify(testSet.message, testSet.publicKey, testSet.signature)).to.be.a("boolean"); + }); + it("should default to false", () => { + expect(verify(sullyUint8Array(testSet.message), testSet.publicKey, testSet.signature)).to.be.false; + expect(verify(testSet.message, sullyUint8Array(testSet.publicKey.serialize()), testSet.signature)).to.be.false; + expect(verify(testSet.message, testSet.publicKey, sullyUint8Array(testSet.signature.serialize()))).to.be.false; + }); + it("should return true for valid sets", () => { + expect(verify(testSet.message, testSet.publicKey, testSet.signature)).to.be.true; + }); + }); + describe("asyncVerify", () => { + it("should return Promise", async () => { + const resPromise = asyncVerify(testSet.message, testSet.publicKey, testSet.signature); + expect(resPromise).to.be.instanceOf(Promise); + const res = await resPromise; + expect(res).to.be.a("boolean"); + }); + it("should default to Promise", async () => { + expect(await asyncVerify(sullyUint8Array(testSet.message), testSet.publicKey, testSet.signature)).to.be.false; + expect(await asyncVerify(testSet.message, sullyUint8Array(testSet.publicKey.serialize()), testSet.signature)).to + .be.false; + expect(await asyncVerify(testSet.message, testSet.publicKey, sullyUint8Array(testSet.signature.serialize()))).to + .be.false; + }); + it("should return true for valid sets", async () => { + expect(await asyncVerify(testSet.message, testSet.publicKey, testSet.signature)).to.be.true; + }); + }); +}); + +describe("Aggregate Verify", () => { + let testSet: NapiTestSet; + before(() => { + testSet = makeNapiTestSets(1)[0]; + }); + describe("aggregateVerify", () => { + it("should return a boolean", () => { + expect(aggregateVerify([testSet.message], [testSet.publicKey], testSet.signature)).to.be.a("boolean"); + }); + it("should default to false", () => { + expect(aggregateVerify([sullyUint8Array(testSet.message)], [testSet.publicKey], testSet.signature)).to.be.false; + expect(aggregateVerify([testSet.message], [sullyUint8Array(testSet.publicKey.serialize())], testSet.signature)).to + .be.false; + expect(aggregateVerify([testSet.message], [testSet.publicKey], sullyUint8Array(testSet.signature.serialize()))).to + .be.false; + }); + it("should return true for valid sets", () => { + expect(aggregateVerify([testSet.message], [testSet.publicKey], testSet.signature)).to.be.true; + }); + }); + describe("asyncAggregateVerify", () => { + it("should return Promise", async () => { + const resPromise = asyncAggregateVerify([testSet.message], [testSet.publicKey], testSet.signature); + expect(resPromise).to.be.instanceOf(Promise); + const res = await resPromise; + expect(res).to.be.a("boolean"); + }); + it("should default to Promise", async () => { + expect(await asyncAggregateVerify([sullyUint8Array(testSet.message)], [testSet.publicKey], testSet.signature)).to + .be.false; + expect( + await asyncAggregateVerify( + [testSet.message], + [sullyUint8Array(testSet.publicKey.serialize())], + testSet.signature + ) + ).to.be.false; + expect( + await asyncAggregateVerify( + [testSet.message], + [testSet.publicKey], + sullyUint8Array(testSet.signature.serialize()) + ) + ).to.be.false; + }); + it("should return true for valid sets", async () => { + expect(await asyncAggregateVerify([testSet.message], [testSet.publicKey], testSet.signature)).to.be.true; + }); + }); +}); + +describe("Fast Aggregate Verify", () => { + let testSet: NapiTestSet; + before(() => { + testSet = makeNapiTestSets(1)[0]; + }); + describe("fastAggregateVerify", () => { + it("should return a boolean", () => { + expect(fastAggregateVerify(testSet.message, [testSet.publicKey], testSet.signature)).to.be.a("boolean"); + }); + it("should default to false", () => { + expect(fastAggregateVerify(sullyUint8Array(testSet.message), [testSet.publicKey], testSet.signature)).to.be.false; + expect(fastAggregateVerify(testSet.message, [sullyUint8Array(testSet.publicKey.serialize())], testSet.signature)) + .to.be.false; + expect(fastAggregateVerify(testSet.message, [testSet.publicKey], sullyUint8Array(testSet.signature.serialize()))) + .to.be.false; + }); + it("should return true for valid sets", () => { + expect(fastAggregateVerify(testSet.message, [testSet.publicKey], testSet.signature)).to.be.true; + }); + }); + describe("asyncFastAggregateVerify", () => { + it("should return Promise", async () => { + const resPromise = asyncFastAggregateVerify(testSet.message, [testSet.publicKey], testSet.signature); + expect(resPromise).to.be.instanceOf(Promise); + const res = await resPromise; + expect(res).to.be.a("boolean"); + }); + it("should default to Promise", async () => { + expect(await asyncFastAggregateVerify(sullyUint8Array(testSet.message), [testSet.publicKey], testSet.signature)) + .to.be.false; + expect( + await asyncFastAggregateVerify( + testSet.message, + [sullyUint8Array(testSet.publicKey.serialize())], + testSet.signature + ) + ).to.be.false; + expect( + await asyncFastAggregateVerify( + testSet.message, + [testSet.publicKey], + sullyUint8Array(testSet.signature.serialize()) + ) + ).to.be.false; + }); + it("should return true for valid sets", async () => { + expect(await asyncFastAggregateVerify(testSet.message, [testSet.publicKey], testSet.signature)).to.be.true; + }); + }); +}); diff --git a/rebuild/test/unit/verifyMultipleAggregateSignatures.test.ts b/rebuild/test/unit/verifyMultipleAggregateSignatures.test.ts new file mode 100644 index 00000000..f0235b12 --- /dev/null +++ b/rebuild/test/unit/verifyMultipleAggregateSignatures.test.ts @@ -0,0 +1,31 @@ +import {expect} from "chai"; +import {asyncVerifyMultipleAggregateSignatures, verifyMultipleAggregateSignatures} from "../../lib/index.js"; +import {makeNapiTestSets} from "../utils.js"; + +describe("Verify Multiple Aggregate Signatures", () => { + describe("verifyMultipleAggregateSignatures", () => { + it("should return a boolean", () => { + expect(verifyMultipleAggregateSignatures([])).to.be.a("boolean"); + }); + it("should default to false", () => { + expect(verifyMultipleAggregateSignatures([])).to.be.false; + }); + it("should return true for valid sets", () => { + expect(verifyMultipleAggregateSignatures(makeNapiTestSets(6))).to.be.true; + }); + }); + describe("asyncVerifyMultipleAggregateSignatures", () => { + it("should return Promise", async () => { + const resPromise = asyncVerifyMultipleAggregateSignatures([]); + expect(resPromise).to.be.instanceOf(Promise); + const res = await resPromise; + expect(res).to.be.a("boolean"); + }); + it("should default to Promise", async () => { + expect(await asyncVerifyMultipleAggregateSignatures([])).to.be.false; + }); + it("should return true for valid sets", async () => { + expect(await asyncVerifyMultipleAggregateSignatures(makeNapiTestSets(6))).to.be.true; + }); + }); +}); diff --git a/rebuild/test/utils.ts b/rebuild/test/utils.ts index bb418b5f..d739f217 100644 --- a/rebuild/test/utils.ts +++ b/rebuild/test/utils.ts @@ -1,7 +1,7 @@ import {expect} from "chai"; import {randomFillSync} from "crypto"; -import * as bindings from "../lib"; -import {BufferLike, NapiTestSet} from "./types"; +import * as bindings from "../lib/index.js"; +import {BufferLike, NapiTestSet} from "./types.js"; function toHexString(bytes: BufferLike): string { if (typeof bytes === "string") return bytes; @@ -10,18 +10,22 @@ function toHexString(bytes: BufferLike): string { throw Error("toHexString only accepts BufferLike types"); } -export function normalizeHex(bytes: BufferLike): string { +export function toHex(bytes: BufferLike): string { const hex = toHexString(bytes); if (hex.startsWith("0x")) return hex; return "0x" + hex; } +export function isEqualBytes(value: BufferLike, expected: BufferLike): boolean { + return toHex(value) === toHex(expected); +} + export function expectEqualHex(value: BufferLike, expected: BufferLike): void { - expect(normalizeHex(value)).to.equal(normalizeHex(expected)); + expect(toHex(value)).to.equal(toHex(expected)); } export function expectNotEqualHex(value: BufferLike, expected: BufferLike): void { - expect(normalizeHex(value)).to.not.equal(normalizeHex(expected)); + expect(toHex(value)).to.not.equal(toHex(expected)); } export function fromHex(hexString: string): Uint8Array { @@ -41,22 +45,22 @@ export function sullyUint8Array(bytes: Uint8Array): Uint8Array { const DEFAULT_TEST_MESSAGE = Uint8Array.from(Buffer.from("test-message")); -export function makeNapiTestSet(msg: Uint8Array = DEFAULT_TEST_MESSAGE): NapiTestSet { +export function makeNapiTestSet(message: Uint8Array = DEFAULT_TEST_MESSAGE): NapiTestSet { const secretKey = bindings.SecretKey.fromKeygen(randomFillSync(Buffer.alloc(32))); const publicKey = secretKey.toPublicKey(); - const signature = secretKey.sign(msg); + const signature = secretKey.sign(message); return { - msg, + message, secretKey, publicKey, signature, }; } -export function makeNapiTestSets(numSets: number, msg = DEFAULT_TEST_MESSAGE): NapiTestSet[] { +export function makeNapiTestSets(numSets: number, message = DEFAULT_TEST_MESSAGE): NapiTestSet[] { const sets: NapiTestSet[] = []; for (let i = 0; i < numSets; i++) { - sets.push(makeNapiTestSet(msg)); + sets.push(makeNapiTestSet(message)); } return sets; } diff --git a/rebuild/tsconfig.json b/rebuild/tsconfig.json index d4fb53f1..e107e6cf 100644 --- a/rebuild/tsconfig.json +++ b/rebuild/tsconfig.json @@ -3,7 +3,8 @@ "compilerOptions": { "noEmit": true, "target": "esnext", - "module": "commonjs", + "module": "esnext", + "moduleResolution": "NodeNext", "lib": ["ESNext"], "strict": true,