Move setup handle to C in node bindings (#177)

* feat(node-bindings): move KzgSettings to c

* fix(node-bindings): typo in comment

* fix(node-bindings): remove unnecessary SetInstanceData

* feat(node-bindings): use C for setting instance data

* docs(node-bindings): fix comment on struct

* refactor(node-bindings): revert export order to minimize diff
This commit is contained in:
Matthew Keil 2023-03-09 08:00:15 -06:00 committed by GitHub
parent 599ae2fe21
commit 87a3e4148d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 154 additions and 230 deletions

View File

@ -9,55 +9,67 @@
#include "c_kzg_4844.h"
#include "blst.h"
Napi::Value throw_invalid_arguments_count(
const unsigned int expected,
const unsigned int actual,
const Napi::Env env
) {
Napi::RangeError::New(
env,
"Wrong number of arguments. Expected: "
+ std::to_string(expected)
+ ", received " + std::to_string(actual)
).ThrowAsJavaScriptException();
/**
* Structure containing information needed for the lifetime of the bindings
* instance. It is not safe to use global static data with worker instances.
* Native node addons are loaded as a dll's once no matter how many node
* instances are using the library. Each node instance will initialize an
* instance of the bindings and workers share memory space. In addition
* the worker JS thread will be independent of the main JS thread. Global
* statics are not thread safe and have the potential for initialization and
* clean-up overwrites which results in segfault or undefined behavior.
*
* An instance of this struct will get created during initialization and it
* will be available from the runtime. It can be retrieved via
* `napi_get_instance_data` or `Napi::Env::GetInstanceData`.
*/
typedef struct {
bool is_setup;
KZGSettings settings;
} KzgAddonData;
return env.Null();
}
Napi::Value throw_invalid_argument_type(const Napi::Env env, std::string name, std::string expectedType) {
Napi::TypeError::New(
env,
"Invalid argument type: " + name + ". Expected " + expectedType
).ThrowAsJavaScriptException();
return env.Null();
/**
* This cleanup function follows the `napi_finalize` interface and will be
* called by the runtime when the exports object is garbage collected. Is
* passed with napi_set_instance_data call when data is set.
*
* @remark This function should not be called, only the runtime should do
* the cleanup.
*
* @param[in] env (unused)
* @param[in] data Pointer KzgAddonData stored by the runtime
* @param[in] hint (unused)
*/
void delete_kzg_addon_data(napi_env /*env*/, void *data, void* /*hint*/) {
if (((KzgAddonData*)data)->is_setup) {
free_trusted_setup(&((KzgAddonData*)data)->settings);
}
free(data);
}
/**
* Get kzg_settings from a Napi::External
* Get kzg_settings from bindings instance data
*
* Checks for:
* - arg IsExternal
*
* Built to pass in a raw Napi::Value so it can be used like
* `get_kzg_settings(env, info[0])`.
* - loadTrustedSetup has been run
*
* 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.
* exception was raised or a valid KZGSettings was returned.
*
* @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;
KZGSettings *get_kzg_settings(Napi::Env &env, const Napi::CallbackInfo &info) {
KzgAddonData *data = env.GetInstanceData<KzgAddonData>();
if (!data->is_setup) {
Napi::Error::New(env, "Must run loadTrustedSetup before running any other c-kzg functions").ThrowAsJavaScriptException();
return nullptr;
}
return val.As<Napi::External<KZGSettings>>().Data();
return &(data->settings);
}
/**
@ -118,62 +130,25 @@ inline Bytes48 *get_bytes48(const Napi::Env &env, const Napi::Value &val, std::s
return reinterpret_cast<Bytes48 *>(get_bytes(env, val, BYTES_PER_COMMITMENT, name));
}
// loadTrustedSetup: (filePath: string) => SetupHandle;
Napi::Value LoadTrustedSetup(const Napi::CallbackInfo& info) {
auto env = info.Env();
size_t argument_count = info.Length();
size_t expected_argument_count = 1;
if (argument_count != expected_argument_count) {
return throw_invalid_arguments_count(expected_argument_count, argument_count, env);
Napi::Env env = info.Env();
KzgAddonData *data = env.GetInstanceData<KzgAddonData>();
if (data->is_setup) {
Napi::Error::New(env, "kzg bindings are already setup").ThrowAsJavaScriptException();
return env.Undefined();
}
if (!info[0].IsString()) {
return throw_invalid_argument_type(env, "filePath", "string");
// the validation checks for this happen in JS
const std::string file_path = info[0].As<Napi::String>().Utf8Value();
FILE *file_handle = fopen(file_path.c_str(), "r");
if (file_handle == NULL) {
Napi::Error::New(env, "Error opening trusted setup file: " + file_path).ThrowAsJavaScriptException();
return env.Undefined();
}
const std::string file_path = info[0].ToString().Utf8Value();
KZGSettings* kzg_settings = (KZGSettings*)malloc(sizeof(KZGSettings));
if (kzg_settings == NULL) {
Napi::Error::New(env, "Error while allocating memory for KZG settings").ThrowAsJavaScriptException();
return env.Null();
if (load_trusted_setup_file(&(data->settings), file_handle) != C_KZG_OK) {
Napi::Error::New(env, "Error loading trusted setup file: " + file_path).ThrowAsJavaScriptException();
return env.Undefined();
}
FILE* f = fopen(file_path.c_str(), "r");
if (f == NULL) {
free(kzg_settings);
Napi::Error::New(env, "Error opening trusted setup file: " + file_path).ThrowAsJavaScriptException();
return env.Null();
}
if (load_trusted_setup_file(kzg_settings, f) != C_KZG_OK) {
free(kzg_settings);
Napi::Error::New(env, "Error loading trusted setup file").ThrowAsJavaScriptException();
return env.Null();
}
return Napi::External<KZGSettings>::New(info.Env(), kzg_settings);
}
// freeTrustedSetup: (setupHandle: SetupHandle) => void;
Napi::Value FreeTrustedSetup(const Napi::CallbackInfo& info) {
auto env = info.Env();
size_t argument_count = info.Length();
size_t expected_argument_count = 1;
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();
}
free_trusted_setup(kzg_settings);
free(kzg_settings);
data->is_setup = true;
return env.Undefined();
}
@ -188,16 +163,11 @@ Napi::Value FreeTrustedSetup(const Napi::CallbackInfo& info) {
*/
Napi::Value BlobToKzgCommitment(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);
}
Blob *blob = get_blob(env, info[0]);
if (blob == nullptr) {
return env.Null();
}
KZGSettings *kzg_settings = get_kzg_settings(env, info[1]);
KZGSettings *kzg_settings = get_kzg_settings(env, info);
if (kzg_settings == nullptr) {
return env.Null();
}
@ -225,11 +195,6 @@ Napi::Value BlobToKzgCommitment(const Napi::CallbackInfo& info) {
*/
Napi::Value ComputeKzgProof(const Napi::CallbackInfo& info) {
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);
}
Blob *blob = get_blob(env, info[0]);
if (blob == nullptr) {
return env.Null();
@ -238,7 +203,7 @@ Napi::Value ComputeKzgProof(const Napi::CallbackInfo& info) {
if (z_bytes == nullptr) {
return env.Null();
}
KZGSettings *kzg_settings = get_kzg_settings(env, info[2]);
KZGSettings *kzg_settings = get_kzg_settings(env, info);
if (kzg_settings == nullptr) {
return env.Null();
}
@ -273,16 +238,11 @@ Napi::Value ComputeKzgProof(const Napi::CallbackInfo& info) {
*/
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);
}
Blob *blob = get_blob(env, info[0]);
if (blob == nullptr) {
return env.Null();
}
KZGSettings *kzg_settings = get_kzg_settings(env, info[1]);
KZGSettings *kzg_settings = get_kzg_settings(env, info);
if (kzg_settings == nullptr) {
return env.Null();
}
@ -317,11 +277,6 @@ Napi::Value ComputeBlobKzgProof(const Napi::CallbackInfo& info) {
*/
Napi::Value VerifyKzgProof(const Napi::CallbackInfo& info) {
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);
}
Bytes48 *commitment_bytes = get_bytes48(env, info[0], "commitmentBytes");
if (commitment_bytes == nullptr) {
return env.Null();
@ -338,7 +293,7 @@ Napi::Value VerifyKzgProof(const Napi::CallbackInfo& info) {
if (proof_bytes == nullptr) {
return env.Null();
}
KZGSettings *kzg_settings = get_kzg_settings(env, info[4]);
KZGSettings *kzg_settings = get_kzg_settings(env, info);
if (kzg_settings == nullptr) {
return env.Null();
}
@ -375,11 +330,6 @@ Napi::Value VerifyKzgProof(const Napi::CallbackInfo& info) {
*/
Napi::Value VerifyBlobKzgProof(const Napi::CallbackInfo& info) {
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);
}
Blob *blob_bytes = get_blob(env, info[0]);
if (blob_bytes == nullptr) {
return env.Null();
@ -392,7 +342,7 @@ Napi::Value VerifyBlobKzgProof(const Napi::CallbackInfo& info) {
if (proof_bytes == nullptr) {
return env.Null();
}
KZGSettings *kzg_settings = get_kzg_settings(env, info[3]);
KZGSettings *kzg_settings = get_kzg_settings(env, info);
if (kzg_settings == nullptr) {
return env.Null();
}
@ -429,11 +379,6 @@ Napi::Value VerifyBlobKzgProof(const Napi::CallbackInfo& info) {
*/
Napi::Value VerifyBlobKzgProofBatch(const Napi::CallbackInfo& info) {
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);
}
C_KZG_RET ret;
Blob *blobs = NULL;
Bytes48 *commitments = NULL;
@ -446,7 +391,7 @@ Napi::Value VerifyBlobKzgProofBatch(const Napi::CallbackInfo& info) {
Napi::Array blobs_param = info[0].As<Napi::Array>();
Napi::Array commitments_param = info[1].As<Napi::Array>();
Napi::Array proofs_param = info[2].As<Napi::Array>();
KZGSettings *kzg_settings = get_kzg_settings(env, info[3]);
KZGSettings *kzg_settings = get_kzg_settings(env, info);
if (kzg_settings == nullptr) {
return env.Null();
}
@ -516,16 +461,28 @@ out:
return result;
}
Napi::Object Init(Napi::Env env, Napi::Object exports) {
Napi::Object Init(Napi::Env env, Napi::Object exports) {
KzgAddonData* data = (KzgAddonData*)malloc(sizeof(KzgAddonData));
if (data == nullptr) {
Napi::Error::New(env, "error allocating memory for kzg setup handle").ThrowAsJavaScriptException();
return exports;
}
data->is_setup = false;
napi_status status = napi_set_instance_data(env, data, delete_kzg_addon_data, NULL);
if (status != napi_ok) {
Napi::Error::New(env, "error setting kzg bindings instance data").ThrowAsJavaScriptException();
return exports;
}
// Functions
exports["loadTrustedSetup"] = Napi::Function::New(env, LoadTrustedSetup);
exports["freeTrustedSetup"] = Napi::Function::New(env, FreeTrustedSetup);
exports["blobToKzgCommitment"] = Napi::Function::New(env, BlobToKzgCommitment);
exports["computeKzgProof"] = Napi::Function::New(env, ComputeKzgProof);
exports["computeBlobKzgProof"] = Napi::Function::New(env, ComputeBlobKzgProof);
exports["verifyKzgProof"] = Napi::Function::New(env, VerifyKzgProof);
exports["verifyBlobKzgProof"] = Napi::Function::New(env, VerifyBlobKzgProof);
exports["verifyBlobKzgProofBatch"] = Napi::Function::New(env, VerifyBlobKzgProofBatch);
exports["loadTrustedSetup"] = Napi::Function::New(env, LoadTrustedSetup, "setup");
exports["blobToKzgCommitment"] = Napi::Function::New(env, BlobToKzgCommitment, "blobToKzgCommitment");
exports["computeKzgProof"] = Napi::Function::New(env, ComputeKzgProof, "computeKzgProof");
exports["computeBlobKzgProof"] = Napi::Function::New(env, ComputeBlobKzgProof, "computeBlobKzgProof");
exports["verifyKzgProof"] = Napi::Function::New(env, VerifyKzgProof, "verifyKzgProof");
exports["verifyBlobKzgProof"] = Napi::Function::New(env, VerifyBlobKzgProof, "verifyBlobKzgProof");
exports["verifyBlobKzgProofBatch"] = Napi::Function::New(env, VerifyBlobKzgProofBatch, "verifyBlobKzgProofBatch");
// Constants
exports["BYTES_PER_BLOB"] = Napi::Number::New(env, BYTES_PER_BLOB);
@ -533,8 +490,7 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
exports["BYTES_PER_FIELD_ELEMENT"] = Napi::Number::New(env, BYTES_PER_FIELD_ELEMENT);
exports["BYTES_PER_PROOF"] = Napi::Number::New(env, BYTES_PER_PROOF);
exports["FIELD_ELEMENTS_PER_BLOB"] = Napi::Number::New(env, FIELD_ELEMENTS_PER_BLOB);
return exports;
}
NODE_API_MODULE(addon, Init)
NODE_API_MODULE(addon, Init)

View File

@ -3,67 +3,55 @@
* https://github.com/ethereum/consensus-specs/blob/dev/specs/eip4844/polynomial-commitments.md#kzg
*/
const kzg: KZG = require("./kzg.node");
const fs = require("fs");
import * as fs from "fs";
import * as path from "path";
export type Bytes32 = Uint8Array; // 32 bytes
export type Bytes48 = 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;
export interface TrustedSetupJson {
setup_G1: string[];
setup_G2: string[];
setup_G1_lagrange: string[];
roots_of_unity: string[];
}
// The C++ native addon interface
type KZG = {
interface KZG {
BYTES_PER_BLOB: number;
BYTES_PER_COMMITMENT: number;
BYTES_PER_FIELD_ELEMENT: number;
BYTES_PER_PROOF: number;
FIELD_ELEMENTS_PER_BLOB: number;
loadTrustedSetup: (filePath: string) => SetupHandle;
loadTrustedSetup: (filePath: string) => void;
freeTrustedSetup: (setupHandle: SetupHandle) => void;
blobToKzgCommitment: (blob: Blob) => KZGCommitment;
blobToKzgCommitment: (blob: Blob, setupHandle: SetupHandle) => KZGCommitment;
computeKzgProof: (blob: Blob, zBytes: Bytes32) => KZGProof;
computeKzgProof: (
blob: Blob,
zBytes: Bytes32,
setupHandle: SetupHandle,
) => KZGProof;
computeBlobKzgProof: (blob: Blob, setupHandle: SetupHandle) => KZGProof;
computeBlobKzgProof: (blob: Blob) => KZGProof;
verifyKzgProof: (
commitmentBytes: Bytes48,
zBytes: Bytes32,
yBytes: Bytes32,
proofBytes: Bytes48,
setupHandle: SetupHandle,
) => boolean;
verifyBlobKzgProof: (
blob: Blob,
commitmentBytes: Bytes48,
proofBytes: Bytes48,
setupHandle: SetupHandle,
) => boolean;
verifyBlobKzgProofBatch: (
blobs: Blob[],
commitmentsBytes: Bytes48[],
proofsBytes: Bytes48[],
setupHandle: SetupHandle,
) => boolean;
};
type TrustedSetupJSON = {
setup_G1: string[];
setup_G2: string[];
setup_G1_lagrange: string[];
roots_of_unity: string[];
};
}
export const BYTES_PER_BLOB = kzg.BYTES_PER_BLOB;
export const BYTES_PER_COMMITMENT = kzg.BYTES_PER_COMMITMENT;
@ -71,55 +59,58 @@ export const BYTES_PER_FIELD_ELEMENT = kzg.BYTES_PER_FIELD_ELEMENT;
export const BYTES_PER_PROOF = kzg.BYTES_PER_PROOF;
export const FIELD_ELEMENTS_PER_BLOB = kzg.FIELD_ELEMENTS_PER_BLOB;
// Stored as internal state
let setupHandle: SetupHandle | undefined;
function requireSetupHandle(): SetupHandle {
if (!setupHandle) {
throw new Error("You must call loadTrustedSetup to initialize KZG.");
}
return setupHandle;
}
export async function transformTrustedSetupJSON(
filePath: string,
): Promise<string> {
const data: TrustedSetupJSON = JSON.parse(fs.readFileSync(filePath));
const textFilePath = filePath.replace(".json", "") + ".txt";
try {
fs.unlinkSync(textFilePath);
} catch {}
const file = fs.createWriteStream(textFilePath);
file.write(`${FIELD_ELEMENTS_PER_BLOB}\n65\n`);
file.write(data.setup_G1.map((p) => p.replace("0x", "")).join("\n"));
file.write("\n");
file.write(data.setup_G2.map((p) => p.replace("0x", "")).join("\n"));
file.end();
const p = new Promise((resolve) => {
file.close(resolve);
});
await p;
/**
* Converts JSON formatted trusted setup into the native format that
* the native library requires. Returns the absolute file path to the
* the formatted file. The path will be the same as the origin
* file but with a ".txt" extension.
*
* @param {string} filePath - The absolute path of JSON formatted trusted setup
*
* @return {string} - The absolute path of the re-formatted trusted setup
*
* @throws {Error} - For invalid file operations
*/
function transformTrustedSetupJson(filePath: string): string {
const data: TrustedSetupJson = JSON.parse(fs.readFileSync(filePath, "utf8"));
const textFilePath = filePath.replace(".json", ".txt");
const setupText =
kzg.FIELD_ELEMENTS_PER_BLOB +
"\n65\n" +
data.setup_G1.map((p) => p.substring(2)).join("\n") +
"\n" +
data.setup_G2.map((p) => p.substring(2)).join("\n");
fs.writeFileSync(textFilePath, setupText);
return textFilePath;
}
/**
* Sets up the c-kzg library. Pass in a properly formatted trusted setup file
* to configure the library. File must be in json format, see or {@link TrustedSetupJson}
* interface for more details, or as a properly formatted utf-8 encoded file.
*
* @remark This function must be run before any other functions in this
* library can be run.
*
* @param {string} filePath - The absolute path of the trusted setup
*
* @return {void}
*
* @throws {Error} - For invalid file operations
*/
export function loadTrustedSetup(filePath: string): void {
if (setupHandle) {
throw new Error(
"Call freeTrustedSetup before loading a new trusted setup.",
if (!(filePath && typeof filePath === "string")) {
throw new TypeError(
"must initialize kzg with the filePath to a txt/json trusted setup",
);
}
setupHandle = kzg.loadTrustedSetup(filePath);
}
export function freeTrustedSetup(): void {
kzg.freeTrustedSetup(requireSetupHandle());
setupHandle = undefined;
if (!fs.existsSync(filePath)) {
throw new Error(`no trusted setup found: ${filePath}`);
}
if (path.parse(filePath).ext === ".json") {
filePath = transformTrustedSetupJson(filePath);
}
return kzg.loadTrustedSetup(filePath);
}
/**
@ -132,7 +123,7 @@ export function freeTrustedSetup(): void {
* @throws {TypeError} - For invalid arguments or failure of the native library
*/
export function blobToKzgCommitment(blob: Blob): KZGCommitment {
return kzg.blobToKzgCommitment(blob, requireSetupHandle());
return kzg.blobToKzgCommitment(blob);
}
/**
@ -146,7 +137,7 @@ export function blobToKzgCommitment(blob: Blob): KZGCommitment {
* @throws {TypeError} - For invalid arguments or failure of the native library
*/
export function computeKzgProof(blob: Blob, zBytes: Bytes32): KZGProof {
return kzg.computeKzgProof(blob, zBytes, requireSetupHandle());
return kzg.computeKzgProof(blob, zBytes);
}
/**
@ -160,7 +151,7 @@ export function computeKzgProof(blob: Blob, zBytes: Bytes32): KZGProof {
* @throws {TypeError} - For invalid arguments or failure of the native library
*/
export function computeBlobKzgProof(blob: Blob): KZGProof {
return kzg.computeBlobKzgProof(blob, requireSetupHandle());
return kzg.computeBlobKzgProof(blob);
}
/**
@ -181,13 +172,7 @@ export function verifyKzgProof(
yBytes: Bytes32,
proofBytes: Bytes48,
): boolean {
return kzg.verifyKzgProof(
commitmentBytes,
zBytes,
yBytes,
proofBytes,
requireSetupHandle(),
);
return kzg.verifyKzgProof(commitmentBytes, zBytes, yBytes, proofBytes);
}
/**
@ -207,12 +192,7 @@ export function verifyBlobKzgProof(
commitmentBytes: Bytes48,
proofBytes: Bytes48,
): boolean {
return kzg.verifyBlobKzgProof(
blob,
commitmentBytes,
proofBytes,
requireSetupHandle(),
);
return kzg.verifyBlobKzgProof(blob, commitmentBytes, proofBytes);
}
/**
@ -234,10 +214,5 @@ export function verifyBlobKzgProofBatch(
commitmentsBytes: Bytes48[],
proofsBytes: Bytes48[],
): boolean {
return kzg.verifyBlobKzgProofBatch(
blobs,
commitmentsBytes,
proofsBytes,
requireSetupHandle(),
);
return kzg.verifyBlobKzgProofBatch(blobs, commitmentsBytes, proofsBytes);
}

View File

@ -7,7 +7,6 @@ const yaml = require("js-yaml");
import {
loadTrustedSetup,
freeTrustedSetup,
blobToKzgCommitment,
computeKzgProof,
computeBlobKzgProof,
@ -18,7 +17,6 @@ import {
BYTES_PER_COMMITMENT,
BYTES_PER_PROOF,
BYTES_PER_FIELD_ELEMENT,
transformTrustedSetupJSON,
} from "./kzg";
const setupFileName = "testing_trusted_setups.json";
@ -79,12 +77,7 @@ function bytesFromHex(hexString: string): Buffer {
describe("C-KZG", () => {
beforeAll(async () => {
const file = await transformTrustedSetupJSON(SETUP_FILE_PATH);
loadTrustedSetup(file);
});
afterAll(() => {
freeTrustedSetup();
loadTrustedSetup(SETUP_FILE_PATH);
});
describe("reference tests should pass", () => {