From ecc668bbe7bd3cf5e5b3398a754088a1b2a778e9 Mon Sep 17 00:00:00 2001 From: Matthew Keil Date: Mon, 6 Mar 2023 19:28:30 -0600 Subject: [PATCH] Node binding argument and return type updates (#170) --- bindings/node.js/kzg.cxx | 450 +++++++++++++++++++++++++-------------- bindings/node.js/kzg.ts | 141 ++++++------ bindings/node.js/test.ts | 140 ++++++++++-- 3 files changed, 484 insertions(+), 247 deletions(-) diff --git a/bindings/node.js/kzg.cxx b/bindings/node.js/kzg.cxx index 80559da..9c4bbeb 100644 --- a/bindings/node.js/kzg.cxx +++ b/bindings/node.js/kzg.cxx @@ -4,6 +4,7 @@ #include // std::ostringstream #include // std::copy #include // std::ostream_iterator +#include #include #include "c_kzg_4844.h" #include "blst.h" @@ -32,41 +33,90 @@ Napi::Value throw_invalid_argument_type(const Napi::Env env, std::string name, s return env.Null(); } -Napi::TypedArrayOf napi_typed_array_from_bytes(uint8_t* array, size_t length, Napi::Env env) { - // Create std::vector out of array. - // We allocate it on the heap to allow wrapping it up into ArrayBuffer. - std::unique_ptr> vector = - std::make_unique>(length, 0); - - for (size_t i = 0; i < length; ++i) { - (*vector)[i] = array[i]; +/** + * Get kzg_settings from a Napi::External + * + * Checks for: + * - arg IsExternal + * + * Built to pass in a raw Napi::Value so it can be used like + * `get_kzg_settings(env, info[0])`. + * + * Designed to raise the correct javascript exception and return a + * valid pointer to the calling context to avoid native stack-frame + * unwinds. Calling context can check for `nullptr` to see if an + * exception was raised or a valid pointer was returned from V8. + * + * @param[in] env Passed from calling context + * @param[in] val Napi::Value to validate and get pointer from + * + * @return - Pointer to the KZGSettings + */ +KZGSettings *get_kzg_settings(const Napi::Env &env, const Napi::Value &val) { + if (!val.IsExternal()) { + Napi::TypeError::New(env, "Must pass setupHandle as the last function argument").ThrowAsJavaScriptException(); + return nullptr; } - - // Wrap up the std::vector into the ArrayBuffer. - Napi::ArrayBuffer buffer = Napi::ArrayBuffer::New( - env, - vector->data(), - length /* size in bytes */, - [](Napi::Env /*env*/, void* /*data*/, std::vector* hint) { - std::unique_ptr> vectorPtrToDelete(hint); - }, - vector.get()); - - // The finalizer is responsible for deleting the vector: release the - // unique_ptr ownership. - vector.release(); - - return Napi::Uint8Array::New(env, length, buffer, 0); + return val.As>().Data(); } -const uint8_t * extract_byte_array_from_param(const Napi::CallbackInfo& info, const int index, const std::string name) { - auto param = info[index].As(); - if (!param.IsTypedArray() || param.TypedArrayType() != napi_uint8_array) { - throw_invalid_argument_type(info.Env(), name, "UInt8Array"); +/** + * Checks for: + * - arg is Uint8Array or Buffer (inherits from Uint8Array) + * - underlying ArrayBuffer length is correct + * + * Internal function for argument validation. Prefer to use + * the helpers below that already have the reinterpreted casts: + * - get_blob + * - get_bytes32 + * - get_bytes48 + * + * Built to pass in a raw Napi::Value so it can be used like + * `get_bytes(env, info[0])` or can also be used to pull props from + * arrays like `get_bytes(env, passed_napi_array[2])`. + * + * Designed to raise the correct javascript exception and return a + * valid pointer to the calling context to avoid native stack-frame + * unwinds. Calling context can check for `nullptr` to see if an + * exception was raised or a valid pointer was returned from V8. + * + * @param[in] env Passed from calling context + * @param[in] val Napi::Value to validate and get pointer from + * @param[in] length Byte length to validate Uint8Array data against + * @param[in] name Name of prop being validated for error reporting + * + * @return - native pointer to first byte in ArrayBuffer + */ +inline uint8_t *get_bytes( + const Napi::Env &env, + const Napi::Value &val, + size_t length, + std::string_view name) +{ + if (!val.IsTypedArray() || val.As().TypedArrayType() != napi_uint8_array) { + std::ostringstream msg; + msg << "Expected " << name << " to be a Uint8Array"; + Napi::TypeError::New(env, msg.str()).ThrowAsJavaScriptException(); + return nullptr; } - return param.As().Data(); + Napi::Uint8Array array = val.As(); + if (array.ByteLength() != length) { + std::ostringstream msg; + msg << "Expected " << name << " to be " << length << " bytes"; + Napi::TypeError::New(env, msg.str()).ThrowAsJavaScriptException(); + return nullptr; + } + return array.Data(); +} +inline Blob *get_blob(const Napi::Env &env, const Napi::Value &val) { + return reinterpret_cast(get_bytes(env, val, BYTES_PER_BLOB, "blob")); +} +inline Bytes32 *get_bytes32(const Napi::Env &env, const Napi::Value &val, std::string_view name) { + return reinterpret_cast(get_bytes(env, val, BYTES_PER_FIELD_ELEMENT, name)); +} +inline Bytes48 *get_bytes48(const Napi::Env &env, const Napi::Value &val, std::string_view name) { + return reinterpret_cast(get_bytes(env, val, BYTES_PER_COMMITMENT, name)); } - // loadTrustedSetup: (filePath: string) => SetupHandle; Napi::Value LoadTrustedSetup(const Napi::CallbackInfo& info) { @@ -89,7 +139,7 @@ Napi::Value LoadTrustedSetup(const Napi::CallbackInfo& info) { if (kzg_settings == NULL) { Napi::Error::New(env, "Error while allocating memory for KZG settings").ThrowAsJavaScriptException(); return env.Null(); - }; + } FILE* f = fopen(file_path.c_str(), "r"); @@ -117,136 +167,189 @@ Napi::Value FreeTrustedSetup(const Napi::CallbackInfo& info) { if (argument_count != expected_argument_count) { return throw_invalid_arguments_count(expected_argument_count, argument_count, env); } + KZGSettings *kzg_settings = get_kzg_settings(env, info[0]); + if (kzg_settings == nullptr) { + return env.Null(); + } - auto kzg_settings = info[0].As>().Data(); free_trusted_setup(kzg_settings); free(kzg_settings); return env.Undefined(); } -// blobToKzgCommitment: (blob: Blob, setupHandle: SetupHandle) => KZGCommitment; +/** + * Convert a blob to a KZG commitment. + * + * @param[in] {Blob} blob - The blob representing the polynomial to be committed to + * + * @return {KZGCommitment} - The resulting commitment + * + * @throws {TypeError} - For invalid arguments or failure of the native library + */ Napi::Value BlobToKzgCommitment(const Napi::CallbackInfo& info) { - auto env = info.Env(); - + Napi::Env env = info.Env(); size_t argument_count = info.Length(); size_t expected_argument_count = 2; if (argument_count != expected_argument_count) { return throw_invalid_arguments_count(expected_argument_count, argument_count, env); } - - Blob *blob = (Blob *)extract_byte_array_from_param(info, 0, "blob"); - if (env.IsExceptionPending()) { + Blob *blob = get_blob(env, info[0]); + if (blob == nullptr) { + return env.Null(); + } + KZGSettings *kzg_settings = get_kzg_settings(env, info[1]); + if (kzg_settings == nullptr) { return env.Null(); } - - auto kzg_settings = info[1].As>().Data(); KZGCommitment commitment; C_KZG_RET ret = blob_to_kzg_commitment(&commitment, blob, kzg_settings); if (ret != C_KZG_OK) { Napi::Error::New(env, "Failed to convert blob to commitment") .ThrowAsJavaScriptException(); - return env.Undefined(); - }; + return env.Null(); + } - return napi_typed_array_from_bytes((uint8_t *)(&commitment), BYTES_PER_COMMITMENT, env); + return Napi::Buffer::Copy(env, reinterpret_cast(&commitment), BYTES_PER_COMMITMENT); } -// computeKzgProof: (blob: Blob, zBytes: Bytes32, setupHandle: SetupHandle) => KZGProof; +/** + * Compute KZG proof for polynomial in Lagrange form at position z. + * + * @param[in] {Blob} blob - The blob (polynomial) to generate a proof for + * @param[in] {Bytes32} zBytes - The generator z-value for the evaluation points + * + * @return {KZGProof} - The resulting proof + * + * @throws {TypeError} - for invalid arguments or failure of the native library + */ Napi::Value ComputeKzgProof(const Napi::CallbackInfo& info) { - auto env = info.Env(); - + Napi::Env env = info.Env(); size_t argument_count = info.Length(); size_t expected_argument_count = 3; if (argument_count != expected_argument_count) { return throw_invalid_arguments_count(expected_argument_count, argument_count, env); } - - auto blob = extract_byte_array_from_param(info, 0, "blob"); - auto z_bytes = extract_byte_array_from_param(info, 1, "zBytes"); - auto kzg_settings = info[2].As>().Data(); - - if (env.IsExceptionPending()) { + Blob *blob = get_blob(env, info[0]); + if (blob == nullptr) { + return env.Null(); + } + Bytes32 *z_bytes = get_bytes32(env, info[1], "zBytes"); + if (z_bytes == nullptr) { + return env.Null(); + } + KZGSettings *kzg_settings = get_kzg_settings(env, info[2]); + if (kzg_settings == nullptr) { return env.Null(); } KZGProof proof; C_KZG_RET ret = compute_kzg_proof( &proof, - (Blob *)blob, - (Bytes32 *)z_bytes, + blob, + z_bytes, kzg_settings ); if (ret != C_KZG_OK) { Napi::Error::New(env, "Failed to compute proof") .ThrowAsJavaScriptException(); - return env.Undefined(); - }; + return env.Null(); + } - return napi_typed_array_from_bytes((uint8_t *)(&proof), BYTES_PER_PROOF, env); + return Napi::Buffer::Copy(env, reinterpret_cast(&proof), BYTES_PER_PROOF); } -// computeBlobKzgProof: (blob: Blob, setupHandle: SetupHandle) => KZGProof; -Napi::Value ComputeBlobKzgProof(const Napi::CallbackInfo& info) { - auto env = info.Env(); +/** + * Given a blob, return the KZG proof that is used to verify it against the + * commitment. + * + * @param[in] {Blob} blob - The blob (polynomial) to generate a proof for + * + * @return {KZGProof} - The resulting proof + * + * @throws {TypeError} - for invalid arguments or failure of the native library + */ +Napi::Value ComputeBlobKzgProof(const Napi::CallbackInfo& info) { + Napi::Env env = info.Env(); size_t argument_count = info.Length(); size_t expected_argument_count = 2; if (argument_count != expected_argument_count) { return throw_invalid_arguments_count(expected_argument_count, argument_count, env); } - - auto blob = extract_byte_array_from_param(info, 0, "blob"); - auto kzg_settings = info[1].As>().Data(); - - if (env.IsExceptionPending()) { + Blob *blob = get_blob(env, info[0]); + if (blob == nullptr) { + return env.Null(); + } + KZGSettings *kzg_settings = get_kzg_settings(env, info[1]); + if (kzg_settings == nullptr) { return env.Null(); } KZGProof proof; C_KZG_RET ret = compute_blob_kzg_proof( &proof, - (Blob *)blob, + blob, kzg_settings ); if (ret != C_KZG_OK) { Napi::Error::New(env, "Error in computeBlobKzgProof") .ThrowAsJavaScriptException(); - return env.Undefined(); - }; + return env.Null(); + } - return napi_typed_array_from_bytes((uint8_t *)(&proof), BYTES_PER_PROOF, env); + return Napi::Buffer::Copy(env, reinterpret_cast(&proof), BYTES_PER_PROOF); } -// verifyKzgProof: (commitmentBytes: Bytes48, zBytes: Bytes32, yBytes: Bytes32, proofBytes: Bytes48, setupHandle: SetupHandle) => boolean; +/** + * Verify a KZG poof claiming that `p(z) == y`. + * + * @param[in] {Bytes48} commitmentBytes - The serialized commitment corresponding to polynomial p(x) + * @param[in] {Bytes32} zBytes - The serialized evaluation point + * @param[in] {Bytes32} yBytes - The serialized claimed evaluation result + * @param[in] {Bytes48} proofBytes - The serialized KZG proof + * + * @return {boolean} - true/false depending on proof validity + * + * @throws {TypeError} - for invalid arguments or failure of the native library + */ Napi::Value VerifyKzgProof(const Napi::CallbackInfo& info) { - auto env = info.Env(); - + Napi::Env env = info.Env(); size_t argument_count = info.Length(); size_t expected_argument_count = 5; if (argument_count != expected_argument_count) { return throw_invalid_arguments_count(expected_argument_count, argument_count, env); } - - auto commitment_bytes = extract_byte_array_from_param(info, 0, "commitmentBytes"); - auto z_bytes = extract_byte_array_from_param(info, 1, "zBytes"); - auto y_bytes = extract_byte_array_from_param(info, 2, "yBytes"); - auto proof_bytes = extract_byte_array_from_param(info, 3, "proofBytes"); - auto kzg_settings = info[4].As>().Data(); - - if (env.IsExceptionPending()) { + Bytes48 *commitment_bytes = get_bytes48(env, info[0], "commitmentBytes"); + if (commitment_bytes == nullptr) { + return env.Null(); + } + Bytes32 *z_bytes = get_bytes32(env, info[1], "zBytes"); + if (z_bytes == nullptr) { + return env.Null(); + } + Bytes32 *y_bytes = get_bytes32(env, info[2], "yBytes"); + if (y_bytes == nullptr) { + return env.Null(); + } + Bytes48 *proof_bytes = get_bytes48(env, info[3], "proofBytes"); + if (proof_bytes == nullptr) { + return env.Null(); + } + KZGSettings *kzg_settings = get_kzg_settings(env, info[4]); + if (kzg_settings == nullptr) { return env.Null(); } bool out; C_KZG_RET ret = verify_kzg_proof( &out, - (Bytes48 *)commitment_bytes, - (Bytes32 *)z_bytes, - (Bytes32 *)y_bytes, - (Bytes48 *)proof_bytes, + commitment_bytes, + z_bytes, + y_bytes, + proof_bytes, kzg_settings ); @@ -258,33 +361,49 @@ Napi::Value VerifyKzgProof(const Napi::CallbackInfo& info) { return Napi::Boolean::New(env, out); } -// verifyBlobKzgProof: (blob: Blob, commitmentBytes: Bytes48, proofBytes: Bytes48, setupHandle: SetupHandle) => boolean; +/** + * Given a blob and its proof, verify that it corresponds to the provided + * commitment. + * + * @param[in] {Blob} blob - The serialized blob to verify + * @param[in] {Bytes48} commitmentBytes - The serialized commitment to verify + * @param[in] {Bytes48} proofBytes - The serialized KZG proof for verification + * + * @return {boolean} - true/false depending on proof validity + * + * @throws {TypeError} - for invalid arguments or failure of the native library + */ Napi::Value VerifyBlobKzgProof(const Napi::CallbackInfo& info) { - auto env = info.Env(); - + Napi::Env env = info.Env(); size_t argument_count = info.Length(); size_t expected_argument_count = 4; if (argument_count != expected_argument_count) { return throw_invalid_arguments_count(expected_argument_count, argument_count, env); } - - auto blob_bytes = extract_byte_array_from_param(info, 0, "blob"); - auto commitment_bytes = extract_byte_array_from_param(info, 1, "commitmentBytes"); - auto proof_bytes = extract_byte_array_from_param(info, 2, "proofBytes"); - auto kzg_settings = info[3].As>().Data(); - - if (env.IsExceptionPending()) { + Blob *blob_bytes = get_blob(env, info[0]); + if (blob_bytes == nullptr) { return env.Null(); } - + Bytes48 *commitment_bytes = get_bytes48(env, info[1], "commitmentBytes"); + if (commitment_bytes == nullptr) { + return env.Null(); + } + Bytes48 *proof_bytes = get_bytes48(env, info[2], "proofBytes"); + if (proof_bytes == nullptr) { + return env.Null(); + } + KZGSettings *kzg_settings = get_kzg_settings(env, info[3]); + if (kzg_settings == nullptr) { + return env.Null(); + } + bool out; C_KZG_RET ret = verify_blob_kzg_proof( &out, - (Blob *)blob_bytes, - (Bytes48 *)commitment_bytes, - (Bytes48 *)proof_bytes, - kzg_settings - ); + blob_bytes, + commitment_bytes, + proof_bytes, + kzg_settings); if (ret != C_KZG_OK) { Napi::TypeError::New(env, "Error in verifyBlobKzgProof").ThrowAsJavaScriptException(); @@ -294,75 +413,83 @@ Napi::Value VerifyBlobKzgProof(const Napi::CallbackInfo& info) { return Napi::Boolean::New(env, out); } -// verifyBlobKzgProofBatch: (blobs: Blob[], commitmentsBytes: Bytes48[], proofsBytes: Bytes48[], setupHandle: SetupHandle) => boolean; +/** + * Given an array of blobs and their proofs, verify that they corresponds to their + * provided commitment. + * + * @remark blobs[0] relates to commitmentBytes[0] and proofBytes[0] + * + * @param[in] {Blob} blobs - An array of serialized blobs to verify + * @param[in] {Bytes48} commitmentBytes - An array of serialized commitments to verify + * @param[in] {Bytes48} proofBytes - An array of serialized KZG proofs for verification + * + * @return {boolean} - true/false depending on batch validity + * + * @throws {TypeError} - for invalid arguments or failure of the native library + */ Napi::Value VerifyBlobKzgProofBatch(const Napi::CallbackInfo& info) { - auto env = info.Env(); - - C_KZG_RET ret; - Blob *blobs = NULL; - Bytes48 *commitments = NULL; - Bytes48 *proofs = NULL; - Napi::Value result = env.Null(); - + Napi::Env env = info.Env(); size_t argument_count = info.Length(); size_t expected_argument_count = 4; if (argument_count != expected_argument_count) { return throw_invalid_arguments_count(expected_argument_count, argument_count, env); } - - auto blobs_param = info[0].As(); - auto commitments_param = info[1].As(); - auto proofs_param = info[2].As(); - auto kzg_settings = info[3].As>().Data(); - - auto blobs_count = blobs_param.Length(); - auto commitments_count = commitments_param.Length(); - auto proofs_count = proofs_param.Length(); - - if (blobs_count != commitments_count || blobs_count != proofs_count) { - Napi::Error::New(env, "verifyBlobKzgProofBatch requires equal number of blobs/commitments/proofs") - .ThrowAsJavaScriptException(); - result = env.Null(); + C_KZG_RET ret; + Blob *blobs = NULL; + Bytes48 *commitments = NULL; + Bytes48 *proofs = NULL; + Napi::Value result = env.Null(); + if (!(info[0].IsArray() && info[1].IsArray() && info[2].IsArray())) { + Napi::Error::New(env, "blobs, commitments, and proofs must all be arrays").ThrowAsJavaScriptException(); + return result; + } + Napi::Array blobs_param = info[0].As(); + Napi::Array commitments_param = info[1].As(); + Napi::Array proofs_param = info[2].As(); + KZGSettings *kzg_settings = get_kzg_settings(env, info[3]); + if (kzg_settings == nullptr) { + return env.Null(); + } + uint32_t count = blobs_param.Length(); + if (count != commitments_param.Length() || count != proofs_param.Length()) { + Napi::Error::New(env, "requires equal number of blobs/commitments/proofs").ThrowAsJavaScriptException(); + return result; + } + blobs = (Blob *)calloc(count, sizeof(Blob)); + if (blobs == nullptr) { + Napi::Error::New(env, "Error while allocating memory for blobs").ThrowAsJavaScriptException(); + goto out; + } + commitments = (Bytes48 *)calloc(count, sizeof(Bytes48)); + if (commitments == nullptr) { + Napi::Error::New(env, "Error while allocating memory for commitments").ThrowAsJavaScriptException(); + goto out; + } + proofs = (Bytes48 *)calloc(count, sizeof(Bytes48)); + if (proofs == nullptr) { + Napi::Error::New(env, "Error while allocating memory for proofs").ThrowAsJavaScriptException(); goto out; } - blobs = (Blob *)calloc(blobs_count, sizeof(Blob)); - if (blobs == NULL) { - Napi::Error::New(env, "Error while allocating memory for blobs").ThrowAsJavaScriptException(); - result = env.Null(); - goto out; - }; - - commitments = (Bytes48 *)calloc(commitments_count, sizeof(Bytes48)); - if (commitments == NULL) { - free(blobs); - Napi::Error::New(env, "Error while allocating memory for commitments").ThrowAsJavaScriptException(); - result = env.Null(); - goto out; - }; - - proofs = (Bytes48 *)calloc(proofs_count, sizeof(Bytes48)); - if (proofs == NULL) { - Napi::Error::New(env, "Error while allocating memory for proofs").ThrowAsJavaScriptException(); - result = env.Null(); - goto out; - }; - - for (uint32_t index = 0; index < blobs_count; index++) { - // Extract blob bytes from parameter - Napi::Value blob = blobs_param[index]; - auto blob_bytes = blob.As().Data(); - memcpy(blobs[index].bytes, blob_bytes, BYTES_PER_BLOB); - - // Extract commitment from parameter - Napi::Value commitment = commitments_param[index]; - auto commitment_bytes = commitment.As().Data(); - memcpy(&commitments[index], commitment_bytes, BYTES_PER_COMMITMENT); - - // Extract proof from parameter - Napi::Value proof = proofs_param[index]; - auto proof_bytes = proof.As().Data(); - memcpy(&proofs[index], proof_bytes, BYTES_PER_PROOF); + for (uint32_t index = 0; index < count; index++) { + // add HandleScope here to release reference to temp values + // after each iteration since data is being memcpy + Napi::HandleScope scope{env}; + Blob *blob = get_blob(env, blobs_param[index]); + if (blob == nullptr) { + goto out; + } + memcpy(&blobs[index], blob, BYTES_PER_BLOB); + Bytes48 *commitment = get_bytes48(env, commitments_param[index], "commitmentBytes"); + if (commitment == nullptr) { + goto out; + } + memcpy(&commitments[index], commitment, BYTES_PER_COMMITMENT); + Bytes48 *proof = get_bytes48(env, proofs_param[index], "proofBytes"); + if (proof == nullptr) { + goto out; + } + memcpy(&proofs[index], proof, BYTES_PER_PROOF); } bool out; @@ -371,13 +498,12 @@ Napi::Value VerifyBlobKzgProofBatch(const Napi::CallbackInfo& info) { blobs, commitments, proofs, - blobs_count, + count, kzg_settings ); if (ret != C_KZG_OK) { Napi::TypeError::New(env, "Error in verifyBlobKzgProofBatch").ThrowAsJavaScriptException(); - result = env.Null(); goto out; } diff --git a/bindings/node.js/kzg.ts b/bindings/node.js/kzg.ts index cbfb5a1..5b2a57c 100644 --- a/bindings/node.js/kzg.ts +++ b/bindings/node.js/kzg.ts @@ -7,8 +7,8 @@ const fs = require("fs"); export type Bytes32 = Uint8Array; // 32 bytes export type Bytes48 = Uint8Array; // 48 bytes -export type KZGProof = Uint8Array; // 48 bytes -export type KZGCommitment = Uint8Array; // 48 bytes +export type KZGProof = Buffer; // 48 bytes +export type KZGCommitment = Buffer; // 48 bytes export type Blob = Uint8Array; // 4096 * 32 bytes type SetupHandle = Object; @@ -81,62 +81,6 @@ function requireSetupHandle(): SetupHandle { return setupHandle; } -function checkBlob(blob: Blob) { - if (!(blob instanceof Uint8Array)) { - throw new Error("Expected blob to be a UInt8Array."); - } - if (blob.length != BYTES_PER_BLOB) { - throw new Error(`Expected blob to be ${BYTES_PER_BLOB} bytes.`); - } -} - -function checkBlobs(blobs: Blob[]) { - for (let blob of blobs) { - checkBlob(blob); - } -} - -function checkCommitment(commitment: KZGCommitment) { - if (!(commitment instanceof Uint8Array)) { - throw new Error("Expected commitment to be a UInt8Array."); - } - if (commitment.length != BYTES_PER_COMMITMENT) { - throw new Error(`Expected commitment to be ${BYTES_PER_COMMITMENT} bytes.`); - } -} - -function checkCommitments(commitments: KZGCommitment[]) { - for (let commitment of commitments) { - checkCommitment(commitment); - } -} - -function checkProof(proof: KZGProof) { - if (!(proof instanceof Uint8Array)) { - throw new Error("Expected proof to be a UInt8Array."); - } - if (proof.length != BYTES_PER_PROOF) { - throw new Error(`Expected proof to be ${BYTES_PER_PROOF} bytes.`); - } -} - -function checkProofs(proofs: KZGProof[]) { - for (let proof of proofs) { - checkProof(proof); - } -} - -function checkFieldElement(field: Bytes32) { - if (!(field instanceof Uint8Array)) { - throw new Error("Expected field element to be a UInt8Array."); - } - if (field.length != BYTES_PER_FIELD_ELEMENT) { - throw new Error( - `Expected field element to be ${BYTES_PER_FIELD_ELEMENT} bytes.`, - ); - } -} - export async function transformTrustedSetupJSON( filePath: string, ): Promise { @@ -178,32 +122,65 @@ export function freeTrustedSetup(): void { setupHandle = undefined; } +/** + * Convert a blob to a KZG commitment. + * + * @param {Blob} blob - The blob representing the polynomial to be committed to + * + * @return {KZGCommitment} - The resulting commitment + * + * @throws {TypeError} - For invalid arguments or failure of the native library + */ export function blobToKzgCommitment(blob: Blob): KZGCommitment { - checkBlob(blob); return kzg.blobToKzgCommitment(blob, requireSetupHandle()); } +/** + * Compute KZG proof for polynomial in Lagrange form at position z. + * + * @param {Blob} blob - The blob (polynomial) to generate a proof for + * @param {Bytes32} zBytes - The generator z-value for the evaluation points + * + * @return {KZGProof} - The resulting proof + * + * @throws {TypeError} - For invalid arguments or failure of the native library + */ export function computeKzgProof(blob: Blob, zBytes: Bytes32): KZGProof { - checkBlob(blob); - checkFieldElement(zBytes); return kzg.computeKzgProof(blob, zBytes, requireSetupHandle()); } +/** + * Given a blob, return the KZG proof that is used to verify it against the + * commitment. + * + * @param {Blob} blob - The blob (polynomial) to generate a proof for + * + * @return {KZGProof} - The resulting proof + * + * @throws {TypeError} - For invalid arguments or failure of the native library + */ export function computeBlobKzgProof(blob: Blob): KZGProof { - checkBlob(blob); return kzg.computeBlobKzgProof(blob, requireSetupHandle()); } +/** + * Verify a KZG poof claiming that `p(z) == y`. + * + * @param {Bytes48} commitmentBytes - The serialized commitment corresponding to polynomial p(x) + * @param {Bytes32} zBytes - The serialized evaluation point + * @param {Bytes32} yBytes - The serialized claimed evaluation result + * @param {Bytes48} proofBytes - The serialized KZG proof + * + * @return {boolean} - true/false depending on proof validity + * + * @throws {TypeError} - For invalid arguments or failure of the native library + */ export function verifyKzgProof( commitmentBytes: Bytes48, zBytes: Bytes32, yBytes: Bytes32, proofBytes: Bytes48, ): boolean { - checkCommitment(commitmentBytes); - checkFieldElement(zBytes); - checkFieldElement(yBytes); - checkProof(proofBytes); return kzg.verifyKzgProof( commitmentBytes, zBytes, @@ -213,14 +190,23 @@ export function verifyKzgProof( ); } +/** + * Given a blob and its proof, verify that it corresponds to the provided + * commitment. + * + * @param {Blob} blob - The serialized blob to verify + * @param {Bytes48} commitmentBytes - The serialized commitment to verify + * @param {Bytes48} proofBytes - The serialized KZG proof for verification + * + * @return {boolean} - true/false depending on proof validity + * + * @throws {TypeError} - For invalid arguments or failure of the native library + */ export function verifyBlobKzgProof( blob: Blob, commitmentBytes: Bytes48, proofBytes: Bytes48, ): boolean { - checkBlob(blob); - checkCommitment(commitmentBytes); - checkProof(proofBytes); return kzg.verifyBlobKzgProof( blob, commitmentBytes, @@ -229,14 +215,25 @@ export function verifyBlobKzgProof( ); } +/** + * Given an array of blobs and their proofs, verify that they corresponds to their + * provided commitment. + * + * Note: blobs[0] relates to commitmentBytes[0] and proofBytes[0] + * + * @param {Blob} blobs - An array of serialized blobs to verify + * @param {Bytes48} commitmentBytes - An array of serialized commitments to verify + * @param {Bytes48} proofBytes - An array of serialized KZG proofs for verification + * + * @return {boolean} - true/false depending on batch validity + * + * @throws {TypeError} - For invalid arguments or failure of the native library + */ export function verifyBlobKzgProofBatch( blobs: Blob[], commitmentsBytes: Bytes48[], proofsBytes: Bytes48[], ): boolean { - checkBlobs(blobs); - checkCommitments(commitmentsBytes); - checkProofs(proofsBytes); return kzg.verifyBlobKzgProofBatch( blobs, commitmentsBytes, diff --git a/bindings/node.js/test.ts b/bindings/node.js/test.ts index 704abcf..c5cd86c 100644 --- a/bindings/node.js/test.ts +++ b/bindings/node.js/test.ts @@ -64,8 +64,17 @@ const generateRandomBlob = () => { ); }; -function bytesFromHex(hexstring: string): Uint8Array { - return Uint8Array.from(Buffer.from(hexstring.slice(2), "hex")); +const blobValidLength = randomBytes(BYTES_PER_BLOB); +const blobBadLength = randomBytes(BYTES_PER_BLOB - 1); +const commitmentValidLength = randomBytes(BYTES_PER_COMMITMENT); +const commitmentBadLength = randomBytes(BYTES_PER_COMMITMENT - 1); +const proofValidLength = randomBytes(BYTES_PER_PROOF); +const proofBadLength = randomBytes(BYTES_PER_PROOF - 1); +const fieldElementValidLength = randomBytes(BYTES_PER_FIELD_ELEMENT); +const fieldElementBadLength = randomBytes(BYTES_PER_FIELD_ELEMENT - 1); + +function bytesFromHex(hexString: string): Buffer { + return Buffer.from(hexString.slice(2), "hex"); } describe("C-KZG", () => { @@ -84,7 +93,7 @@ describe("C-KZG", () => { tests.forEach((testFile: string) => { const test = yaml.load(readFileSync(testFile, "ascii")); - let commitment = new Uint8Array(); + let commitment: Buffer; let blob = bytesFromHex(test.input.blob); try { @@ -105,7 +114,7 @@ describe("C-KZG", () => { tests.forEach((testFile: string) => { const test = yaml.load(readFileSync(testFile, "ascii")); - let proof = new Uint8Array(); + let proof: Buffer; let blob = bytesFromHex(test.input.blob); let z = bytesFromHex(test.input.z); @@ -127,7 +136,7 @@ describe("C-KZG", () => { tests.forEach((testFile: string) => { const test = yaml.load(readFileSync(testFile, "ascii")); - let proof = new Uint8Array(); + let proof: Buffer; let blob = bytesFromHex(test.input.blob); try { @@ -212,14 +221,14 @@ describe("C-KZG", () => { it("throws as expected when given an argument of invalid type", () => { // @ts-expect-error expect(() => blobToKzgCommitment("wrong type")).toThrowError( - "Expected blob to be a UInt8Array", + "Expected blob to be a Uint8Array", ); }); it("throws as expected when given an argument of invalid length", () => { - expect(() => - blobToKzgCommitment(randomBytes(BYTES_PER_BLOB - 1)), - ).toThrowError("Expected blob to be 131072 bytes"); + expect(() => blobToKzgCommitment(blobBadLength)).toThrowError( + "Expected blob to be 131072 bytes", + ); }); }); @@ -230,6 +239,14 @@ describe("C-KZG", () => { const zBytes = new Uint8Array(BYTES_PER_FIELD_ELEMENT).fill(0); computeKzgProof(blob, zBytes); }); + it("throws as expected when given an argument of invalid length", () => { + expect(() => + computeKzgProof(blobBadLength, fieldElementValidLength), + ).toThrowError("Expected blob to be 131072 bytes"); + expect(() => + computeKzgProof(blobValidLength, fieldElementBadLength), + ).toThrowError("Expected zBytes to be 32 bytes"); + }); }); // TODO: add more tests for this function. @@ -238,6 +255,11 @@ describe("C-KZG", () => { let blob = generateRandomBlob(); computeBlobKzgProof(blob); }); + it("throws as expected when given an argument of invalid length", () => { + expect(() => computeBlobKzgProof(blobBadLength)).toThrowError( + "Expected blob to be 131072 bytes", + ); + }); }); describe("edge cases for verifyKzgProof", () => { @@ -262,6 +284,40 @@ describe("C-KZG", () => { expect(verifyKzgProof(commitment, z, y, proof)).toBe(false); }); + it("throws as expected when given an argument of invalid length", () => { + expect(() => + verifyKzgProof( + commitmentBadLength, + fieldElementValidLength, + fieldElementValidLength, + proofValidLength, + ), + ).toThrowError("Expected commitmentBytes to be 48 bytes"); + expect(() => + verifyKzgProof( + commitmentValidLength, + fieldElementBadLength, + fieldElementValidLength, + proofValidLength, + ), + ).toThrowError("Expected zBytes to be 32 bytes"); + expect(() => + verifyKzgProof( + commitmentValidLength, + fieldElementValidLength, + fieldElementBadLength, + proofValidLength, + ), + ).toThrowError("Expected yBytes to be 32 bytes"); + expect(() => + verifyKzgProof( + commitmentValidLength, + fieldElementValidLength, + fieldElementValidLength, + proofBadLength, + ), + ).toThrowError("Expected proofBytes to be 48 bytes"); + }); }); describe("edge cases for verifyBlobKzgProof", () => { @@ -285,14 +341,72 @@ describe("C-KZG", () => { let proof = computeBlobKzgProof(generateRandomBlob()); expect(verifyBlobKzgProof(blob, commitment, proof)).toBe(false); }); + it("throws as expected when given an argument of invalid length", () => { + expect(() => + verifyBlobKzgProof( + blobBadLength, + commitmentValidLength, + proofValidLength, + ), + ).toThrowError("Expected blob to be 131072 bytes"); + expect(() => + verifyBlobKzgProof( + blobValidLength, + commitmentBadLength, + proofValidLength, + ), + ).toThrowError("Expected commitmentBytes to be 48 bytes"); + expect(() => + verifyBlobKzgProof( + blobValidLength, + commitmentValidLength, + proofBadLength, + ), + ).toThrowError("Expected proofBytes to be 48 bytes"); + }); }); describe("edge cases for verifyBlobKzgProofBatch", () => { + it("should reject non-array args", () => { + expect(() => + verifyBlobKzgProofBatch( + 2 as unknown as Uint8Array[], + [commitmentValidLength, commitmentValidLength], + [proofValidLength, proofValidLength], + ), + ).toThrowError("blobs, commitments, and proofs must all be arrays"); + }); it("should reject non-bytearray blob", () => { expect(() => - // @ts-expect-error - verifyBlobKzgProofBatch(["foo", "bar"], [], []), - ).toThrowError("Expected blob to be a UInt8Array"); + verifyBlobKzgProofBatch( + ["foo", "bar"] as unknown as Uint8Array[], + [commitmentValidLength, commitmentValidLength], + [proofValidLength, proofValidLength], + ), + ).toThrowError("Expected blob to be a Uint8Array"); + }); + it("throws as expected when given an argument of invalid length", () => { + expect(() => + verifyBlobKzgProofBatch( + [blobBadLength, blobValidLength], + [commitmentValidLength, commitmentValidLength], + [proofValidLength, proofValidLength], + ), + ).toThrowError("Expected blob to be 131072 bytes"); + expect(() => + verifyBlobKzgProofBatch( + [blobValidLength, blobValidLength], + [commitmentBadLength, commitmentValidLength], + [proofValidLength, proofValidLength], + ), + ).toThrowError("Expected commitmentBytes to be 48 bytes"); + expect(() => + verifyBlobKzgProofBatch( + [blobValidLength, blobValidLength], + [commitmentValidLength, commitmentValidLength], + [proofValidLength, proofBadLength], + ), + ).toThrowError("Expected proofBytes to be 48 bytes"); }); it("zero blobs/commitments/proofs should verify as true", () => { @@ -305,7 +419,7 @@ describe("C-KZG", () => { let commitments = new Array(count); let proofs = new Array(count); - for (let [i, _] of blobs.entries()) { + for (let [i] of blobs.entries()) { blobs[i] = generateRandomBlob(); commitments[i] = blobToKzgCommitment(blobs[i]); proofs[i] = computeBlobKzgProof(blobs[i]);