From a48baa2edacdae47ff1ee384ac57d952c50b7a06 Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Thu, 2 Apr 2026 11:33:49 +0200 Subject: [PATCH 1/9] chore: update the zapi version --- build.zig | 5 +++-- build.zig.zon | 4 ++-- zbuild.zon | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/build.zig b/build.zig index 6a683170b..2a17b9239 100644 --- a/build.zig +++ b/build.zig @@ -146,7 +146,6 @@ pub fn build(b: *std.Build) void { .target = target, .optimize = optimize, }); - module_bls.linkLibrary(dep_blst.artifact("blst")); b.modules.put(b.dupe("bls"), module_bls) catch @panic("OOM"); const module_state_transition = b.createModule(.{ @@ -1161,7 +1160,7 @@ pub fn build(b: *std.Build) void { module_bindings.addImport("config", module_config); module_bindings.addImport("fork_types", module_fork_types); module_bindings.addImport("state_transition", module_state_transition); - module_bindings.addImport("zapi:napi", dep_zapi.module("napi")); + module_bindings.addImport("zapi", dep_zapi.module("zapi")); module_int.addImport("config", module_config); module_int.addImport("download_era_options", options_module_download_era_options); @@ -1199,4 +1198,6 @@ pub fn build(b: *std.Build) void { module_bls_spec_tests.addImport("hex", module_hex); module_bls_spec_tests.addImport("yaml", dep_yaml.module("yaml")); module_bls_spec_tests.addImport("spec_test_options", options_module_spec_test_options); + + _ = dep_blst; } diff --git a/build.zig.zon b/build.zig.zon index 4560ddf28..b5abaf9c7 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -23,8 +23,8 @@ .hash = "zig_yaml-0.1.0-C1161m2NAgDhth5OvjG1o1UNKcdo-XNfO82m10J1g4Cl", }, .zapi = .{ - .url = "git+https://github.com/chainsafe/zapi#cf013457cdf0612a62a6d008d65505ff79e85596", - .hash = "napi_z-0.1.0-iqJb7-4oAgBkIQFWFEDazD0-G9NRBQZaEY0fwTB7Aqbu", + .url = "git+https://github.com/chainsafe/zapi#e40f8d1e022435f68bc76159f15db2c1c6cb6856", + .hash = "zapi-0.1.0-rIqzUbuhAwDHKpdfh4b3zS27O26yg4FtKtIbWz3ueRw9", }, .zbench = .{ .url = "git+https://github.com/hendriknielaender/zBench#d8c7dd485306b88b757d52005614ebcb0a336942", diff --git a/zbuild.zon b/zbuild.zon index 86da67b60..74eee5354 100644 --- a/zbuild.zon +++ b/zbuild.zon @@ -22,7 +22,7 @@ .url = "git+https://github.com/chainsafe/zig-yaml#e0e5962579e990a66c21424416c7ac092b20b772", }, .zapi = .{ - .url = "git+https://github.com/chainsafe/zapi#cf013457cdf0612a62a6d008d65505ff79e85596", + .url = "git+https://github.com/chainsafe/zapi#e40f8d1e022435f68bc76159f15db2c1c6cb6856", }, .zbench = .{ .url = "git+https://github.com/hendriknielaender/zBench#d8c7dd485306b88b757d52005614ebcb0a336942", @@ -253,7 +253,7 @@ .config, .fork_types, .state_transition, - "zapi:napi", + .zapi, }, }, .linkage = .dynamic, From 887a03e364ceab8411a64a520bf15220e61e06aa Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Thu, 2 Apr 2026 11:36:43 +0200 Subject: [PATCH 2/9] feat: use new zapi dsl --- bindings/napi/BeaconStateView.zig | 3 +- bindings/napi/blst.zig | 8 +- bindings/napi/config.zig | 35 ++---- bindings/napi/metrics.zig | 19 +-- bindings/napi/napi_property_descriptor.zig | 2 +- bindings/napi/pool.zig | 25 ++-- bindings/napi/pubkeys.zig | 136 ++++++++++----------- bindings/napi/root.zig | 72 +++++------ bindings/napi/shuffle.zig | 39 ++---- bindings/napi/state_transition.zig | 2 +- bindings/napi/to_napi_value.zig | 2 +- bindings/src/pubkeys.d.ts | 2 +- bindings/test/pubkeys.test.ts | 4 +- bindings/test/shuffle.test.ts | 2 +- 14 files changed, 144 insertions(+), 207 deletions(-) diff --git a/bindings/napi/BeaconStateView.zig b/bindings/napi/BeaconStateView.zig index a92dd04cd..53d061dd6 100644 --- a/bindings/napi/BeaconStateView.zig +++ b/bindings/napi/BeaconStateView.zig @@ -1,5 +1,5 @@ const std = @import("std"); -const napi = @import("zapi:napi"); +const napi = @import("zapi").napi; const c = @import("config"); const fork_types = @import("fork_types"); const st = @import("state_transition"); @@ -37,6 +37,7 @@ pub fn BeaconStateView_ctor(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value cached_state, BeaconStateView_finalize, null, + null, ); return cb.this(); diff --git a/bindings/napi/blst.zig b/bindings/napi/blst.zig index 2321041d7..08cc638df 100644 --- a/bindings/napi/blst.zig +++ b/bindings/napi/blst.zig @@ -1,6 +1,6 @@ //! Contains the necessary bindings for blst operations in lodestar-ts. const std = @import("std"); -const napi = @import("zapi:napi"); +const napi = @import("zapi").napi; const bls = @import("bls"); const builtin = @import("builtin"); const getter = @import("napi_property_descriptor.zig").getter; @@ -108,7 +108,7 @@ pub fn PublicKey_finalize(_: napi.Env, pk: *PublicKey, _: ?*anyopaque) void { pub fn PublicKey_ctor(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { const pk = try allocator.create(PublicKey); errdefer allocator.destroy(pk); - _ = try env.wrap(cb.this(), PublicKey, pk, PublicKey_finalize, null); + _ = try env.wrap(cb.this(), PublicKey, pk, PublicKey_finalize, null, null); return cb.this(); } @@ -218,7 +218,7 @@ pub fn Signature_finalize(_: napi.Env, sig: *Signature, _: ?*anyopaque) void { pub fn Signature_ctor(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { const sig = try allocator.create(Signature); errdefer allocator.destroy(sig); - _ = try env.wrap(cb.this(), Signature, sig, Signature_finalize, null); + _ = try env.wrap(cb.this(), Signature, sig, Signature_finalize, null, null); return cb.this(); } @@ -327,7 +327,7 @@ pub fn SecretKey_finalize(_: napi.Env, sk: *SecretKey, _: ?*anyopaque) void { pub fn SecretKey_ctor(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { const sk = try allocator.create(SecretKey); errdefer allocator.destroy(sk); - _ = try env.wrap(cb.this(), SecretKey, sk, SecretKey_finalize, null); + _ = try env.wrap(cb.this(), SecretKey, sk, SecretKey_finalize, null, null); return cb.this(); } diff --git a/bindings/napi/config.zig b/bindings/napi/config.zig index e6a42e96d..c5e216e02 100644 --- a/bindings/napi/config.zig +++ b/bindings/napi/config.zig @@ -1,5 +1,6 @@ const std = @import("std"); -const napi = @import("zapi:napi"); +const napi = @import("zapi").napi; +const js = @import("zapi").js; const active_preset = @import("preset").active_preset; const c = @import("config"); const BeaconConfig = @import("config").BeaconConfig; @@ -41,29 +42,31 @@ pub const State = struct { pub var state: State = .{}; -pub fn Config_set(env: napi.Env, cb: napi.CallbackInfo(2)) !napi.Value { +/// JS: config.set(chainConfigObj, genesisValidatorsRoot) +pub fn set(obj: js.Value, genesis_root: js.Uint8Array) !void { if (!state.initialized) { return error.ConfigNotInitialized; } - const obj = try cb.arg(0).coerceToObject(); - const chain_config = try chainConfigFromObject(env, obj); - const genesis_validators_root_info = try cb.arg(1).getTypedarrayInfo(); - if (genesis_validators_root_info.data.len != 32) { + // Drop to low-level for the complex object parsing + const e = js.env(); + const raw_obj = try obj.toValue().coerceToObject(); + const chain_config = try chainConfigFromObject(e, raw_obj); + + const root_slice = try genesis_root.toSlice(); + if (root_slice.len != 32) { return error.InvalidGenesisValidatorsRootLength; } state.config = BeaconConfig.init( chain_config, - genesis_validators_root_info.data[0..32].*, + root_slice[0..32].*, ); state.initialized = true; - - return env.getUndefined(); } -pub fn chainConfigFromObject(env: napi.Env, obj: napi.Value) !ChainConfig { +fn chainConfigFromObject(env: napi.Env, obj: napi.Value) !ChainConfig { var chain_config: ChainConfig = undefined; inline for (std.meta.fields(ChainConfig)) |field| { const field_value = obj.getNamedProperty(field.name) catch |err| { @@ -143,15 +146,3 @@ pub fn chainConfigFromObject(env: napi.Env, obj: napi.Value) !ChainConfig { } return chain_config; } - -pub fn register(env: napi.Env, exports: napi.Value) !void { - const config_obj = try env.createObject(); - try config_obj.setNamedProperty("set", try env.createFunction( - "set", - 2, - Config_set, - null, - )); - - try exports.setNamedProperty("config", config_obj); -} diff --git a/bindings/napi/metrics.zig b/bindings/napi/metrics.zig index 95a92c27e..b370ec604 100644 --- a/bindings/napi/metrics.zig +++ b/bindings/napi/metrics.zig @@ -1,6 +1,6 @@ const std = @import("std"); const builtin = @import("builtin"); -const napi = @import("zapi:napi"); +const js = @import("zapi").js; const state_transition = @import("state_transition"); var gpa: std.heap.DebugAllocator(.{}) = .init; @@ -11,11 +11,12 @@ else var initialized: bool = false; -pub fn Metrics_scrapeMetrics(env: napi.Env, _: napi.CallbackInfo(0)) !napi.Value { +/// JS: metrics.scrapeMetrics() → string +pub fn scrapeMetrics() !js.String { var buf = std.ArrayList(u8).init(allocator); defer buf.deinit(); try state_transition.metrics.write(buf.writer()); - return env.createStringUtf8(buf.items); + return js.String.from(buf.items); } pub fn deinit() void { @@ -23,15 +24,3 @@ pub fn deinit() void { state_transition.metrics.state_transition.deinit(); initialized = false; } - -pub fn register(env: napi.Env, exports: napi.Value) !void { - const metrics_obj = try env.createObject(); - - try metrics_obj.setNamedProperty("scrapeMetrics", try env.createFunction( - "scrapeMetrics", - 0, - Metrics_scrapeMetrics, - null, - )); - try exports.setNamedProperty("metrics", metrics_obj); -} diff --git a/bindings/napi/napi_property_descriptor.zig b/bindings/napi/napi_property_descriptor.zig index e0f61263c..f289d6663 100644 --- a/bindings/napi/napi_property_descriptor.zig +++ b/bindings/napi/napi_property_descriptor.zig @@ -1,5 +1,5 @@ const std = @import("std"); -const napi = @import("zapi:napi"); +const napi = @import("zapi").napi; /// Extracts a function name, without prefix, from a napi binding function. /// diff --git a/bindings/napi/pool.zig b/bindings/napi/pool.zig index 542346e05..8e5b644e9 100644 --- a/bindings/napi/pool.zig +++ b/bindings/napi/pool.zig @@ -1,5 +1,5 @@ const std = @import("std"); -const napi = @import("zapi:napi"); +const js = @import("zapi").js; const Node = @import("persistent_merkle_tree").Node; /// Pool uses page allocator for internal allocations. @@ -27,27 +27,16 @@ pub const State = struct { pub var state: State = .{}; -pub fn Pool_ensureCapacity(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { +/// JS: pool.ensureCapacity(newSize) +pub fn ensureCapacity(new_size: js.Number) !void { if (!state.initialized) { return error.PoolNotInitialized; } + const requested = new_size.assertU32(); const old_size = state.pool.nodes.capacity; - const new_size = try cb.arg(0).getValueUint32(); - if (new_size <= old_size) { - return env.getUndefined(); + if (requested <= old_size) { + return; } - try state.pool.preheat(@intCast(new_size - state.pool.nodes.capacity)); - return env.getUndefined(); -} - -pub fn register(env: napi.Env, exports: napi.Value) !void { - const pool_obj = try env.createObject(); - try pool_obj.setNamedProperty("ensureCapacity", try env.createFunction( - "ensureCapacity", - 1, - Pool_ensureCapacity, - null, - )); - try exports.setNamedProperty("pool", pool_obj); + try state.pool.preheat(@intCast(requested - state.pool.nodes.capacity)); } diff --git a/bindings/napi/pubkeys.zig b/bindings/napi/pubkeys.zig index 7b1a6ddae..9869eb368 100644 --- a/bindings/napi/pubkeys.zig +++ b/bindings/napi/pubkeys.zig @@ -1,11 +1,10 @@ const std = @import("std"); -const napi = @import("zapi:napi"); +const js = @import("zapi").js; +const napi = @import("zapi").napi; const bls = @import("bls"); const blst_bindings = @import("./blst.zig"); const PubkeyIndexMap = @import("state_transition").PubkeyIndexMap; const Index2PubkeyCache = @import("state_transition").Index2PubkeyCache; -const getter = @import("napi_property_descriptor.zig").getter; -const method = @import("napi_property_descriptor.zig").method; /// Uses page allocator for internal allocations. /// It's recommended to never reallocate the pubkey2index after initialization. @@ -70,15 +69,14 @@ fn pubkey2indexWrittenSize() usize { return total_size - @sizeOf(Header); } -pub fn pubkeys_save(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { +/// JS: pubkeys.save(filePath) +pub fn save(file_path: js.String) !void { var file_path_buf: [1024]u8 = undefined; - const file_path = try cb.arg(0).getValueStringUtf8(&file_path_buf); - var file = try std.fs.cwd().createFile(file_path, .{ .truncate = true }); + const path = try file_path.toSlice(&file_path_buf); + var file = try std.fs.cwd().createFile(path, .{ .truncate = true }); defer file.close(); - // Write header - // Magic "PKIX" + len + capacity - + // Write header: Magic "PKIX" + len + capacity var header: [12]u8 = [_]u8{ 'P', 'K', 'I', 'X', 0, 0, 0, 0, 0, 0, 0, 0 }; std.mem.writeInt(u32, header[4..8], @intCast(state.index2pubkey.items.len), .little); std.mem.writeInt(u32, header[8..12], @intCast(state.index2pubkey.capacity), .little); @@ -87,19 +85,17 @@ pub fn pubkeys_save(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { // Write pubkey2index entries const p2i_size = pubkey2indexWrittenSize(); const ptr: [*]u8 = @ptrCast(state.pubkey2index.unmanaged.metadata.?); - const slice = ptr[0..p2i_size]; - try file.writeAll(slice); + try file.writeAll(ptr[0..p2i_size]); // Write index2pubkey entries try file.writeAll(std.mem.sliceAsBytes(state.index2pubkey.items)); - - return env.getUndefined(); } -pub fn pubkeys_load(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { +/// JS: pubkeys.load(filePath) +pub fn load(file_path: js.String) !void { var file_path_buf: [1024]u8 = undefined; - const file_path = try cb.arg(0).getValueStringUtf8(&file_path_buf); - var file = try std.fs.cwd().openFile(file_path, .{}); + const path = try file_path.toSlice(&file_path_buf); + var file = try std.fs.cwd().openFile(path, .{}); defer file.close(); if (state.initialized) { @@ -137,119 +133,115 @@ pub fn pubkeys_load(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { // Read pubkey2index entries const ptr: [*]u8 = @ptrCast(state.pubkey2index.unmanaged.metadata.?); - const slice = ptr[0..p2i_size]; - _ = try file.readAll(slice); + _ = try file.readAll(ptr[0..p2i_size]); state.pubkey2index.unmanaged.size = len; state.pubkey2index.unmanaged.available = capacity - len; // Read index2pubkey entries - const index2pubkey_bytes = std.mem.sliceAsBytes(state.index2pubkey.items); - _ = try file.readAll(index2pubkey_bytes); + _ = try file.readAll(std.mem.sliceAsBytes(state.index2pubkey.items)); state.initialized = true; - return env.getUndefined(); } -pub fn pubkeys_getIndex(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { +/// JS: pubkeys.getIndex(pubkeyBytes) → number | null +/// Returns the index for a 48-byte pubkey, or undefined if not found. +pub fn getIndex(pubkey: js.Uint8Array) !js.Value { if (!state.initialized) { return error.PubkeyIndexNotInitialized; } - const pubkey_info = try cb.arg(0).getTypedarrayInfo(); - if (pubkey_info.data.len != 48) { + const pubkey_slice = try pubkey.toSlice(); + if (pubkey_slice.len != 48) { return error.InvalidPubkeyLength; } - const index = state.pubkey2index.get(pubkey_info.data[0..48].*) orelse return env.getNull(); - return try env.createUint32(@intCast(index)); + const e = js.env(); + if (state.pubkey2index.get(pubkey_slice[0..48].*)) |index| { + const val = try e.createUint32(@intCast(index)); + return .{ .val = val }; + } else { + const val = try e.getNull(); + return .{ .val = val }; + } } -pub fn pubkeys_get(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { +/// JS: pubkeys.get(index) → PublicKey | undefined +/// Returns the PublicKey at the given index, or undefined if out of bounds. +/// Uses low-level N-API for PublicKey class interop. +pub fn get(index: js.Number) !js.Value { if (!state.initialized) { return error.PubkeyIndexNotInitialized; } - const index = try cb.arg(0).getValueUint32(); - if (index >= state.index2pubkey.items.len) { - return env.getUndefined(); + const e = js.env(); + const idx = index.assertU32(); + if (idx >= state.index2pubkey.items.len) { + const undef = try e.getUndefined(); + return .{ .val = undef }; } - const out = try blst_bindings.newPublicKeyInstance(env); - const out_pubkey = try env.unwrap(bls.PublicKey, out); - out_pubkey.* = state.index2pubkey.items[@intCast(index)]; - return out; + // Drop to low-level for PublicKey class interop + const out = try blst_bindings.newPublicKeyInstance(e); + const out_pubkey = try e.unwrap(bls.PublicKey, out); + out_pubkey.* = state.index2pubkey.items[@intCast(idx)]; + return .{ .val = out }; } -pub fn pubkeys_set(env: napi.Env, cb: napi.CallbackInfo(2)) !napi.Value { +/// JS: pubkeys.set(index, pubkeyBytes) +pub fn set(index: js.Number, pubkey: js.Uint8Array) !void { if (!state.initialized) { return error.PubkeyIndexNotInitialized; } - const index = try cb.arg(0).getValueUint32(); - const pubkey_info = try cb.arg(1).getTypedarrayInfo(); - if (pubkey_info.data.len != 48) { + const idx = index.assertU32(); + const pubkey_slice = try pubkey.toSlice(); + if (pubkey_slice.len != 48) { return error.InvalidPubkeyLength; } - const pubkey_bytes = pubkey_info.data[0..48]; + const pubkey_bytes = pubkey_slice[0..48]; // Ensure capacity if needed - if (index >= state.index2pubkey.capacity) { - const new_cap: u32 = @intCast(@max(index + 1, state.index2pubkey.capacity * 2)); + if (idx >= state.index2pubkey.capacity) { + const new_cap: u32 = @intCast(@max(idx + 1, state.index2pubkey.capacity * 2)); try state.pubkey2index.ensureTotalCapacity(new_cap); try state.index2pubkey.ensureTotalCapacity(new_cap); } // Extend length if needed - if (index >= state.index2pubkey.items.len) { - state.index2pubkey.items.len = index + 1; + if (idx >= state.index2pubkey.items.len) { + state.index2pubkey.items.len = idx + 1; } // Set pubkey2index - state.pubkey2index.put(pubkey_bytes.*, @intCast(index)) catch return error.PubkeyIndexInsertFailed; + state.pubkey2index.put(pubkey_bytes.*, @intCast(idx)) catch return error.PubkeyIndexInsertFailed; // Deserialize and set index2pubkey - state.index2pubkey.items[@intCast(index)] = try bls.PublicKey.uncompress(pubkey_bytes); - - return env.getUndefined(); + state.index2pubkey.items[@intCast(idx)] = try bls.PublicKey.uncompress(pubkey_bytes); } -pub fn pubkeys_size(env: napi.Env, _: napi.CallbackInfo(0)) !napi.Value { +/// JS: pubkeys.size → number (getter) +/// Note: DSL doesn't support getters yet, so this is a function. +pub fn size() !js.Number { if (!state.initialized) { return error.PubkeyIndexNotInitialized; } - return try env.createUint32(@intCast(state.index2pubkey.items.len)); + return js.Number.from(@as(u32, @intCast(state.index2pubkey.items.len))); } -pub fn pubkeys_ensureCapacity(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { +/// JS: pubkeys.ensureCapacity(newSize) +pub fn ensureCapacity(new_size: js.Number) !void { if (!state.initialized) { return error.PubkeyIndexNotInitialized; } + const requested = new_size.assertU32(); const old_size = state.index2pubkey.capacity; - const new_size = try cb.arg(0).getValueUint32(); - if (new_size <= old_size) { - return env.getUndefined(); + if (requested <= old_size) { + return; } - try state.pubkey2index.ensureTotalCapacity(new_size); - try state.index2pubkey.ensureTotalCapacity(new_size); - return env.getUndefined(); -} - -pub fn register(env: napi.Env, exports: napi.Value) !void { - const pubkeys_obj = try env.createObject(); - - try pubkeys_obj.defineProperties(&[_]napi.c.napi_property_descriptor{ - method(1, pubkeys_load), - method(1, pubkeys_save), - method(1, pubkeys_ensureCapacity), - method(1, pubkeys_get), - method(1, pubkeys_getIndex), - method(2, pubkeys_set), - getter(pubkeys_size), - }); - - try exports.setNamedProperty("pubkeys", pubkeys_obj); + try state.pubkey2index.ensureTotalCapacity(requested); + try state.index2pubkey.ensureTotalCapacity(requested); } diff --git a/bindings/napi/root.zig b/bindings/napi/root.zig index 99a3c2a5f..93676baea 100644 --- a/bindings/napi/root.zig +++ b/bindings/napi/root.zig @@ -1,53 +1,47 @@ const std = @import("std"); -const napi = @import("zapi:napi"); -const pool = @import("./pool.zig"); -const pubkeys = @import("./pubkeys.zig"); -const config = @import("./config.zig"); -const shuffle = @import("./shuffle.zig"); -const metrics = @import("./metrics.zig"); +const zapi = @import("zapi"); +const napi = zapi.napi; +const js = zapi.js; +pub const pool = @import("./pool.zig"); +pub const shuffle = @import("./shuffle.zig"); +pub const config = @import("./config.zig"); +pub const pubkeys = @import("./pubkeys.zig"); +pub const metrics = @import("./metrics.zig"); + const BeaconStateView = @import("./BeaconStateView.zig"); -const blst = @import("./blst.zig"); const state_transition = @import("./state_transition.zig"); +const blst = @import("./blst.zig"); -comptime { - napi.module.register(register); -} - -/// Tracks how many NAPI environments reference the shared module state. -/// Shared state (pool, pubkeys, config) is initialized on the first register -/// and torn down only when the last environment exits. -var env_refcount: std.atomic.Value(u32) = std.atomic.Value(u32).init(0); - -const EnvCleanup = struct { - fn hook(_: *EnvCleanup) void { - if (env_refcount.fetchSub(1, .acq_rel) == 1) { - // Last environment — tear down shared state. - config.state.deinit(); - pubkeys.state.deinit(); - pool.state.deinit(); - metrics.deinit(); - } - } -}; - -var env_cleanup: EnvCleanup = .{}; - -fn register(env: napi.Env, exports: napi.Value) !void { - if (env_refcount.fetchAdd(1, .monotonic) == 0) { +fn init(old_ref_count: u32) !void { + if (old_ref_count == 0) { // First environment — initialize shared state. try pool.state.init(); try pubkeys.state.init(); config.state.init(); } +} - try env.addEnvCleanupHook(EnvCleanup, &env_cleanup, EnvCleanup.hook); +fn cleanup(new_ref_count: u32) void { + if (new_ref_count == 0) { + // Last environment — tear down shared state. + config.state.deinit(); + pubkeys.state.deinit(); + pool.state.deinit(); + metrics.deinit(); + } +} - try pool.register(env, exports); - try pubkeys.register(env, exports); - try config.register(env, exports); - try shuffle.register(env, exports); - try BeaconStateView.register(env, exports); +/// Manual registration for modules not yet converted to DSL. +fn register(env: napi.Env, exports: napi.Value) !void { try blst.register(env, exports); + try BeaconStateView.register(env, exports); try state_transition.register(env, exports); - try metrics.register(env, exports); +} + +comptime { + js.exportModule(@This(), .{ + .init = init, + .cleanup = cleanup, + .register = register, + }); } diff --git a/bindings/napi/shuffle.zig b/bindings/napi/shuffle.zig index 43279241b..39d8674c8 100644 --- a/bindings/napi/shuffle.zig +++ b/bindings/napi/shuffle.zig @@ -1,35 +1,16 @@ -const std = @import("std"); -const napi = @import("zapi:napi"); -const innerShuffleList = @import("state_transition").shuffle.innerShuffleList; +const js = @import("zapi").js; +const stInnerShuffleList = @import("state_transition").shuffle.innerShuffleList; -pub fn Shuffle_shuffleList(env: napi.Env, cb: napi.CallbackInfo(4)) !napi.Value { - const list_info = try cb.arg(0).getTypedarrayInfo(); - if (list_info.array_type != .uint32) { - return error.InvalidShuffleListType; - } - const list: []u32 = @alignCast(std.mem.bytesAsSlice(u32, list_info.data)); - const seed_info = try cb.arg(1).getTypedarrayInfo(); - const seed = seed_info.data; +pub fn innerShuffleList(list: js.Uint32Array, seed: js.Uint8Array, rounds: js.Number, forwards: js.Boolean) !void { + const list_u32 = try list.toSlice(); + const seed_slice = try seed.toSlice(); - const rounds_u32 = try cb.arg(2).getValueUint32(); - if (rounds_u32 > 255) { + const rounds_i32 = rounds.assertI32(); + if (rounds_i32 < 0 or rounds_i32 > 255) { return error.InvalidRoundsSize; } - const rounds: u8 = @intCast(rounds_u32); - const forwards = try cb.arg(3).getValueBool(); - - try innerShuffleList(u32, list, seed, rounds, forwards); - - return env.getUndefined(); -} + const rounds_u8: u8 = @intCast(@as(u32, @intCast(rounds_i32))); + const fwd = forwards.assertBool(); -pub fn register(env: napi.Env, exports: napi.Value) !void { - const shuffle_obj = try env.createObject(); - try shuffle_obj.setNamedProperty("innerShuffleList", try env.createFunction( - "innerShuffleList", - 4, - Shuffle_shuffleList, - null, - )); - try exports.setNamedProperty("shuffle", shuffle_obj); + try stInnerShuffleList(u32, list_u32, seed_slice, rounds_u8, fwd); } diff --git a/bindings/napi/state_transition.zig b/bindings/napi/state_transition.zig index 2d8e8ff13..8e234b287 100644 --- a/bindings/napi/state_transition.zig +++ b/bindings/napi/state_transition.zig @@ -1,5 +1,5 @@ const std = @import("std"); -const napi = @import("zapi:napi"); +const napi = @import("zapi").napi; const builtin = @import("builtin"); const fork_types = @import("fork_types"); const state_transition = @import("state_transition"); diff --git a/bindings/napi/to_napi_value.zig b/bindings/napi/to_napi_value.zig index 1fd7c028a..c333749e0 100644 --- a/bindings/napi/to_napi_value.zig +++ b/bindings/napi/to_napi_value.zig @@ -1,6 +1,6 @@ const std = @import("std"); const ssz = @import("ssz"); -const napi = @import("zapi:napi"); +const napi = @import("zapi").napi; const constants = @import("constants"); pub fn sszValueToNapiValue(env: napi.Env, comptime ST: type, value: *const ST.Type) !napi.Value { diff --git a/bindings/src/pubkeys.d.ts b/bindings/src/pubkeys.d.ts index 27069a370..1ddeda59c 100644 --- a/bindings/src/pubkeys.d.ts +++ b/bindings/src/pubkeys.d.ts @@ -10,7 +10,7 @@ export interface PubkeyCache { /** Set both directions atomically — impl owns the PublicKey.fromBytes() deserialization */ set(index: number, pubkey: Uint8Array): void; /** Number of entries */ - readonly size: number; + size(): number; /** Load cache from a PKIX file (clears JS-level cache) */ load(filepath: string): void; /** Save cache to a PKIX file */ diff --git a/bindings/test/pubkeys.test.ts b/bindings/test/pubkeys.test.ts index c285921b9..8e32c0260 100644 --- a/bindings/test/pubkeys.test.ts +++ b/bindings/test/pubkeys.test.ts @@ -29,7 +29,7 @@ describe("pubkeys", () => { for (const {index, pubkeyBytes} of keypairs) { pubkeyCache.set(index, pubkeyBytes); } - expect(pubkeyCache.size).toBe(keypairs.length); + expect(pubkeyCache.size()).toBe(keypairs.length); for (const {index, pubkeyBytes} of keypairs) { expect(pubkeyCache.get(index)).toBeDefined(); @@ -66,7 +66,7 @@ describe("pubkeys", () => { pubkeyCache.save(tempPkixPath); pubkeyCache.load(tempPkixPath); - expect(pubkeyCache.size).toBe(keypairs.length); + expect(pubkeyCache.size()).toBe(keypairs.length); for (const {index, pubkeyBytes} of keypairs) { expect(pubkeyCache.getIndex(pubkeyBytes)).toBe(index); expect(pubkeyCache.get(index)).toBeDefined(); diff --git a/bindings/test/shuffle.test.ts b/bindings/test/shuffle.test.ts index e15e8f1d1..cfbe0745a 100644 --- a/bindings/test/shuffle.test.ts +++ b/bindings/test/shuffle.test.ts @@ -43,7 +43,7 @@ describe("innerShuffleList", () => { const forwards = false; expect(() => { innerShuffleList(invalidInput as any, seed, rounds, forwards); - }).toThrow("Invalid argument"); + }).toThrow("InvalidArg"); }); it("should fail with invalid rounds", async () => { From f1f10976a07f9ae755d45fbf13ce5301bf7e4f7c Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Thu, 2 Apr 2026 11:41:59 +0200 Subject: [PATCH 3/9] fix: sync zbuild file --- build.zig | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/build.zig b/build.zig index 2a17b9239..b48ee98a0 100644 --- a/build.zig +++ b/build.zig @@ -146,6 +146,7 @@ pub fn build(b: *std.Build) void { .target = target, .optimize = optimize, }); + module_bls.linkLibrary(dep_blst.artifact("blst")); b.modules.put(b.dupe("bls"), module_bls) catch @panic("OOM"); const module_state_transition = b.createModule(.{ @@ -1198,6 +1199,4 @@ pub fn build(b: *std.Build) void { module_bls_spec_tests.addImport("hex", module_hex); module_bls_spec_tests.addImport("yaml", dep_yaml.module("yaml")); module_bls_spec_tests.addImport("spec_test_options", options_module_spec_test_options); - - _ = dep_blst; } From 6e542df60bdd66ebcc7935d7ab7ebaaa9a70b72b Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Thu, 2 Apr 2026 11:51:47 +0200 Subject: [PATCH 4/9] fix: update code as per feedback --- bindings/napi/config.zig | 4 +--- bindings/napi/shuffle.zig | 6 +++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/bindings/napi/config.zig b/bindings/napi/config.zig index c5e216e02..c0ee6fdf8 100644 --- a/bindings/napi/config.zig +++ b/bindings/napi/config.zig @@ -49,9 +49,7 @@ pub fn set(obj: js.Value, genesis_root: js.Uint8Array) !void { } // Drop to low-level for the complex object parsing - const e = js.env(); - const raw_obj = try obj.toValue().coerceToObject(); - const chain_config = try chainConfigFromObject(e, raw_obj); + const chain_config = try chainConfigFromObject(js.env(), try obj.toValue().coerceToObject()); const root_slice = try genesis_root.toSlice(); if (root_slice.len != 32) { diff --git a/bindings/napi/shuffle.zig b/bindings/napi/shuffle.zig index 39d8674c8..f8378634a 100644 --- a/bindings/napi/shuffle.zig +++ b/bindings/napi/shuffle.zig @@ -9,8 +9,8 @@ pub fn innerShuffleList(list: js.Uint32Array, seed: js.Uint8Array, rounds: js.Nu if (rounds_i32 < 0 or rounds_i32 > 255) { return error.InvalidRoundsSize; } - const rounds_u8: u8 = @intCast(@as(u32, @intCast(rounds_i32))); - const fwd = forwards.assertBool(); + const rounds_u8: u8 = @intCast(rounds_i32); + const is_forwards = forwards.assertBool(); - try stInnerShuffleList(u32, list_u32, seed_slice, rounds_u8, fwd); + try stInnerShuffleList(u32, list_u32, seed_slice, rounds_u8, is_forwards); } From 10db6b729248197008d58ff17ffb3266aef3430d Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Thu, 2 Apr 2026 11:55:40 +0200 Subject: [PATCH 5/9] fix: assert the arguments --- bindings/napi/config.zig | 2 ++ bindings/napi/pubkeys.zig | 2 ++ 2 files changed, 4 insertions(+) diff --git a/bindings/napi/config.zig b/bindings/napi/config.zig index c0ee6fdf8..5198449d6 100644 --- a/bindings/napi/config.zig +++ b/bindings/napi/config.zig @@ -44,6 +44,8 @@ pub var state: State = .{}; /// JS: config.set(chainConfigObj, genesisValidatorsRoot) pub fn set(obj: js.Value, genesis_root: js.Uint8Array) !void { + std.debug.assert(genesis_root.len == 32); + if (!state.initialized) { return error.ConfigNotInitialized; } diff --git a/bindings/napi/pubkeys.zig b/bindings/napi/pubkeys.zig index 9869eb368..e99c5186f 100644 --- a/bindings/napi/pubkeys.zig +++ b/bindings/napi/pubkeys.zig @@ -147,6 +147,8 @@ pub fn load(file_path: js.String) !void { /// JS: pubkeys.getIndex(pubkeyBytes) → number | null /// Returns the index for a 48-byte pubkey, or undefined if not found. pub fn getIndex(pubkey: js.Uint8Array) !js.Value { + std.debug.assert(pubkey.len == 48); + if (!state.initialized) { return error.PubkeyIndexNotInitialized; } From cab9a5fe93f1551e78595c619ec0dd435e1c3060 Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Thu, 2 Apr 2026 12:00:41 +0200 Subject: [PATCH 6/9] chore; revert the assertions --- bindings/napi/config.zig | 2 -- bindings/napi/pubkeys.zig | 2 -- 2 files changed, 4 deletions(-) diff --git a/bindings/napi/config.zig b/bindings/napi/config.zig index 5198449d6..c0ee6fdf8 100644 --- a/bindings/napi/config.zig +++ b/bindings/napi/config.zig @@ -44,8 +44,6 @@ pub var state: State = .{}; /// JS: config.set(chainConfigObj, genesisValidatorsRoot) pub fn set(obj: js.Value, genesis_root: js.Uint8Array) !void { - std.debug.assert(genesis_root.len == 32); - if (!state.initialized) { return error.ConfigNotInitialized; } diff --git a/bindings/napi/pubkeys.zig b/bindings/napi/pubkeys.zig index e99c5186f..9869eb368 100644 --- a/bindings/napi/pubkeys.zig +++ b/bindings/napi/pubkeys.zig @@ -147,8 +147,6 @@ pub fn load(file_path: js.String) !void { /// JS: pubkeys.getIndex(pubkeyBytes) → number | null /// Returns the index for a 48-byte pubkey, or undefined if not found. pub fn getIndex(pubkey: js.Uint8Array) !js.Value { - std.debug.assert(pubkey.len == 48); - if (!state.initialized) { return error.PubkeyIndexNotInitialized; } From b273436238ce5b508ab3b5398f72fa1866ac8129 Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Wed, 8 Apr 2026 21:07:04 +0200 Subject: [PATCH 7/9] feat: use new zapi dsl for blst and beacon-state-view --- bindings/napi/BeaconStateView.zig | 1664 ++++++++++++---------------- bindings/napi/blst.zig | 1128 ++++++------------- bindings/napi/pubkeys.zig | 13 +- bindings/napi/root.zig | 16 +- bindings/napi/state_transition.zig | 102 +- build.zig.zon | 4 +- zbuild.zon | 2 +- 7 files changed, 1114 insertions(+), 1815 deletions(-) diff --git a/bindings/napi/BeaconStateView.zig b/bindings/napi/BeaconStateView.zig index 53d061dd6..4f2f8d112 100644 --- a/bindings/napi/BeaconStateView.zig +++ b/bindings/napi/BeaconStateView.zig @@ -1,5 +1,7 @@ const std = @import("std"); -const napi = @import("zapi").napi; +const zapi = @import("zapi"); +const js = zapi.js; +const napi = zapi.napi; const c = @import("config"); const fork_types = @import("fork_types"); const st = @import("state_transition"); @@ -10,1088 +12,866 @@ const AnySignedBeaconBlock = fork_types.AnySignedBeaconBlock; const preset = @import("preset").preset; const ct = @import("consensus_types"); const pool = @import("./pool.zig"); -const config = @import("./config.zig"); +const config_bindings = @import("./config.zig"); const pubkey = @import("./pubkeys.zig"); const sszValueToNapiValue = @import("./to_napi_value.zig").sszValueToNapiValue; const numberSliceToNapiValue = @import("./to_napi_value.zig").numberSliceToNapiValue; -const getter = @import("napi_property_descriptor.zig").getter; -const method = @import("napi_property_descriptor.zig").method; - -/// Allocator used for all BeaconStateView instances. var gpa: std.heap.DebugAllocator(.{}) = .init; const allocator = gpa.allocator(); -pub fn BeaconStateView_finalize(_: napi.Env, cached_state: *CachedBeaconState, _: ?*anyopaque) void { - cached_state.deinit(); - allocator.destroy(cached_state); +fn asJsValue(value: napi.Value) js.Value { + return .{ .val = value }; } -pub fn BeaconStateView_ctor(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { - const cached_state = try allocator.create(CachedBeaconState); - errdefer allocator.destroy(cached_state); - - _ = try env.wrap( - cb.this(), - CachedBeaconState, - cached_state, - BeaconStateView_finalize, - null, - null, - ); +fn jsNull() !js.Value { + return asJsValue(try js.env().getNull()); +} - return cb.this(); +fn throwNull(code: [:0]const u8, message: [:0]const u8) !js.Value { + const env = js.env(); + try env.throwError(code, message); + return jsNull(); } -pub fn BeaconStateView_createFromBytes(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { - const ctor = cb.this(); +fn optionalBool(options: ?js.Value, name: [:0]const u8, default_value: bool) !bool { + if (options) |value| { + const raw = value.toValue(); + if (try raw.typeof() == .object and try raw.hasNamedProperty(name)) { + return try (try raw.getNamedProperty(name)).getValueBool(); + } + } + return default_value; +} + +pub const BeaconStateView = struct { + pub const js_meta = js.class(.{ + .properties = .{ + .slot = true, + .fork = true, + .epoch = true, + .genesisTime = true, + .genesisValidatorsRoot = true, + .eth1Data = true, + .latestBlockHeader = true, + .previousJustifiedCheckpoint = true, + .currentJustifiedCheckpoint = true, + .finalizedCheckpoint = true, + .previousEpochParticipation = true, + .currentEpochParticipation = true, + .latestExecutionPayloadHeader = true, + .historicalSummaries = true, + .pendingDeposits = true, + .pendingDepositsCount = true, + .pendingPartialWithdrawals = true, + .pendingPartialWithdrawalsCount = true, + .pendingConsolidations = true, + .pendingConsolidationsCount = true, + .proposerLookahead = true, + .previousDecisionRoot = true, + .currentDecisionRoot = true, + .nextDecisionRoot = true, + .currentProposers = true, + .nextProposers = true, + .previousProposers = true, + .currentSyncCommittee = true, + .nextSyncCommittee = true, + .currentSyncCommitteeIndexed = true, + .syncProposerReward = true, + .effectiveBalanceIncrements = true, + .validatorCount = true, + .activeValidatorCount = true, + .isMergeTransitionComplete = true, + .isExecutionStateType = true, + .proposerRewards = true, + .clonedCount = true, + .clonedCountWithTransferCache = true, + .createdWithTransferCache = true, + }, + }); - const bytes_info = try cb.arg(0).getTypedarrayInfo(); - const state = try allocator.create(AnyBeaconState); - errdefer allocator.destroy(state); + cached_state: ?*CachedBeaconState = null, - const slot = fork_types.readSlotFromAnyBeaconStateBytes(bytes_info.data); - const fork = config.state.config.forkSeq(slot); - state.* = try AnyBeaconState.deserialize(allocator, &pool.state.pool, fork, bytes_info.data); - errdefer state.deinit(); + const Self = @This(); - const cached_state_value = try env.newInstance(ctor, .{}); + pub fn init() Self { + return .{}; + } - const cached_state = try env.unwrap(CachedBeaconState, cached_state_value); + pub fn deinit(self: *Self) void { + if (self.cached_state) |cached_state| { + cached_state.deinit(); + allocator.destroy(cached_state); + self.cached_state = null; + } + } - try cached_state.init( - allocator, - state, - .{ - .config = &config.state.config, - .index_to_pubkey = &pubkey.state.index2pubkey, - .pubkey_to_index = &pubkey.state.pubkey2index, - }, - null, - ); + fn requireState(self: *const Self) !*CachedBeaconState { + return self.cached_state orelse error.InvalidState; + } - return cached_state_value; -} + pub fn createFromBytes(bytes: js.Uint8Array) !Self { + const state = try allocator.create(AnyBeaconState); + errdefer allocator.destroy(state); + + const byte_slice = try bytes.toSlice(); + const slot_value = fork_types.readSlotFromAnyBeaconStateBytes(byte_slice); + const fork_seq = config_bindings.state.config.forkSeq(slot_value); + state.* = try AnyBeaconState.deserialize(allocator, &pool.state.pool, fork_seq, byte_slice); + errdefer state.deinit(); + + const cached_state = try allocator.create(CachedBeaconState); + errdefer allocator.destroy(cached_state); + + try cached_state.init( + allocator, + state, + .{ + .config = &config_bindings.state.config, + .index_to_pubkey = &pubkey.state.index2pubkey, + .pubkey_to_index = &pubkey.state.pubkey2index, + }, + null, + ); -pub fn BeaconStateView_slot(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - const slot = try cached_state.state.slot(); - return try env.createInt64(@intCast(slot)); -} + return .{ .cached_state = cached_state }; + } -pub fn BeaconStateView_fork(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - var fork_view = try cached_state.state.fork(); - var fork: ct.phase0.Fork.Type = undefined; - try fork_view.toValue(allocator, &fork); - return try sszValueToNapiValue(env, ct.phase0.Fork, &fork); -} + pub fn slot(self: *const Self) !js.Number { + const slot_value = try (try self.requireState()).state.slot(); + return js.Number.from(slot_value); + } -pub fn BeaconStateView_epoch(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - const slot = try cached_state.state.slot(); - return try env.createInt64(@intCast(slot / preset.SLOTS_PER_EPOCH)); -} + pub fn fork(self: *const Self) !js.Value { + const env = js.env(); + var fork_view = try (try self.requireState()).state.fork(); + var fork_value: ct.phase0.Fork.Type = undefined; + try fork_view.toValue(allocator, &fork_value); + return asJsValue(try sszValueToNapiValue(env, ct.phase0.Fork, &fork_value)); + } -pub fn BeaconStateView_genesisTime(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - return try env.createInt64(@intCast(try cached_state.state.genesisTime())); -} + pub fn epoch(self: *const Self) !js.Number { + const slot_value = try (try self.requireState()).state.slot(); + return js.Number.from(slot_value / preset.SLOTS_PER_EPOCH); + } -pub fn BeaconStateView_genesisValidatorsRoot(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - return sszValueToNapiValue(env, ct.primitive.Root, try cached_state.state.genesisValidatorsRoot()); -} + pub fn genesisTime(self: *const Self) !js.Number { + return js.Number.from(try (try self.requireState()).state.genesisTime()); + } -pub fn BeaconStateView_eth1Data(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - var eth1_data_view = try cached_state.state.eth1Data(); - var eth1_data: ct.phase0.Eth1Data.Type = undefined; - try eth1_data_view.toValue(allocator, ð1_data); - return try sszValueToNapiValue(env, ct.phase0.Eth1Data, ð1_data); -} + pub fn genesisValidatorsRoot(self: *const Self) !js.Value { + const env = js.env(); + return asJsValue(try sszValueToNapiValue(env, ct.primitive.Root, try (try self.requireState()).state.genesisValidatorsRoot())); + } -pub fn BeaconStateView_latestBlockHeader(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - var header_view = try cached_state.state.latestBlockHeader(); - var header: ct.phase0.BeaconBlockHeader.Type = undefined; - try header_view.toValue(allocator, &header); - return try sszValueToNapiValue(env, ct.phase0.BeaconBlockHeader, &header); -} + pub fn eth1Data(self: *const Self) !js.Value { + const env = js.env(); + var eth1_data_view = try (try self.requireState()).state.eth1Data(); + var eth1_data: ct.phase0.Eth1Data.Type = undefined; + try eth1_data_view.toValue(allocator, ð1_data); + return asJsValue(try sszValueToNapiValue(env, ct.phase0.Eth1Data, ð1_data)); + } -pub fn BeaconStateView_previousJustifiedCheckpoint(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - var cp: ct.phase0.Checkpoint.Type = undefined; - try cached_state.state.previousJustifiedCheckpoint(&cp); - return try sszValueToNapiValue(env, ct.phase0.Checkpoint, &cp); -} + pub fn latestBlockHeader(self: *const Self) !js.Value { + const env = js.env(); + var header_view = try (try self.requireState()).state.latestBlockHeader(); + var header: ct.phase0.BeaconBlockHeader.Type = undefined; + try header_view.toValue(allocator, &header); + return asJsValue(try sszValueToNapiValue(env, ct.phase0.BeaconBlockHeader, &header)); + } -pub fn BeaconStateView_currentJustifiedCheckpoint(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - var cp: ct.phase0.Checkpoint.Type = undefined; - try cached_state.state.currentJustifiedCheckpoint(&cp); - return try sszValueToNapiValue(env, ct.phase0.Checkpoint, &cp); -} + pub fn previousJustifiedCheckpoint(self: *const Self) !js.Value { + const env = js.env(); + var cp: ct.phase0.Checkpoint.Type = undefined; + try (try self.requireState()).state.previousJustifiedCheckpoint(&cp); + return asJsValue(try sszValueToNapiValue(env, ct.phase0.Checkpoint, &cp)); + } -pub fn BeaconStateView_finalizedCheckpoint(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - var cp: ct.phase0.Checkpoint.Type = undefined; - try cached_state.state.finalizedCheckpoint(&cp); - return try sszValueToNapiValue(env, ct.phase0.Checkpoint, &cp); -} + pub fn currentJustifiedCheckpoint(self: *const Self) !js.Value { + const env = js.env(); + var cp: ct.phase0.Checkpoint.Type = undefined; + try (try self.requireState()).state.currentJustifiedCheckpoint(&cp); + return asJsValue(try sszValueToNapiValue(env, ct.phase0.Checkpoint, &cp)); + } -/// Get the block root at a given slot. -/// Arguments: -/// - arg 0: slot (number) -/// Returns: Root (Uint8Array) -pub fn BeaconStateView_getBlockRoot(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - const slot: u64 = @intCast(try cb.arg(0).getValueInt64()); - - const result = switch (cached_state.state.forkSeq()) { - inline else => |f| st.getBlockRootAtSlot(f, cached_state.state.castToFork(f), slot), - }; - const root = result catch |err| { - const msg = switch (err) { - error.SlotTooBig => "Can only get block root in the past", - error.SlotTooSmall => "Cannot get block root more than SLOTS_PER_HISTORICAL_ROOT in the past", - else => "Failed to get block root", + pub fn finalizedCheckpoint(self: *const Self) !js.Value { + const env = js.env(); + var cp: ct.phase0.Checkpoint.Type = undefined; + try (try self.requireState()).state.finalizedCheckpoint(&cp); + return asJsValue(try sszValueToNapiValue(env, ct.phase0.Checkpoint, &cp)); + } + + pub fn getBlockRoot(self: *const Self, slot_arg: js.Number) !js.Value { + const env = js.env(); + const cached_state = try self.requireState(); + const slot_value: u64 = @intCast(try slot_arg.toI64()); + + const result = switch (cached_state.state.forkSeq()) { + inline else => |f| st.getBlockRootAtSlot(f, cached_state.state.castToFork(f), slot_value), + }; + const root = result catch |err| { + const msg = switch (err) { + error.SlotTooBig => "Can only get block root in the past", + error.SlotTooSmall => "Cannot get block root more than SLOTS_PER_HISTORICAL_ROOT in the past", + else => "Failed to get block root", + }; + return throwNull("INVALID_SLOT", msg); }; - try env.throwError("INVALID_SLOT", msg); - return env.getNull(); - }; - return sszValueToNapiValue(env, ct.primitive.Root, root); -} + return asJsValue(try sszValueToNapiValue(env, ct.primitive.Root, root)); + } -/// Get the randao mix at a given epoch. -/// Arguments: -/// - arg 0: epoch (number) -/// Returns: Bytes32 (Uint8Array) -pub fn BeaconStateView_getRandaoMix(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - const epoch: u64 = @intCast(try cb.arg(0).getValueInt64()); - - const result = switch (cached_state.state.forkSeq()) { - inline else => |f| st.getRandaoMix(f, cached_state.state.castToFork(f), epoch), - }; - const mix = result catch { - try env.throwError("INVALID_EPOCH", "Failed to get randao mix for epoch"); - return env.getNull(); - }; - - return sszValueToNapiValue(env, ct.primitive.Bytes32, mix); -} + pub fn getRandaoMix(self: *const Self, epoch_arg: js.Number) !js.Value { + const env = js.env(); + const cached_state = try self.requireState(); + const epoch_value: u64 = @intCast(try epoch_arg.toI64()); -pub fn BeaconStateView_previousEpochParticipation(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - var view = try cached_state.state.previousEpochParticipation(); + const result = switch (cached_state.state.forkSeq()) { + inline else => |f| st.getRandaoMix(f, cached_state.state.castToFork(f), epoch_value), + }; + const mix = result catch { + return throwNull("INVALID_EPOCH", "Failed to get randao mix for epoch"); + }; - const size = try view.serializedSize(); - var bytes: [*]u8 = undefined; - const buf = try env.createArrayBuffer(size, &bytes); - _ = try view.serializeIntoBytes(bytes[0..size]); + return asJsValue(try sszValueToNapiValue(env, ct.primitive.Bytes32, mix)); + } - return try env.createTypedarray(.uint8, size, buf, 0); -} + pub fn previousEpochParticipation(self: *const Self) !js.Uint8Array { + const env = js.env(); + var view = try (try self.requireState()).state.previousEpochParticipation(); -pub fn BeaconStateView_currentEpochParticipation(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - var view = try cached_state.state.currentEpochParticipation(); + const size = try view.serializedSize(); + var bytes: [*]u8 = undefined; + const buf = try env.createArrayBuffer(size, &bytes); + _ = try view.serializeIntoBytes(bytes[0..size]); - const size = try view.serializedSize(); - var bytes: [*]u8 = undefined; - const buf = try env.createArrayBuffer(size, &bytes); - _ = try view.serializeIntoBytes(bytes[0..size]); + return .{ .val = try env.createTypedarray(.uint8, size, buf, 0) }; + } - return try env.createTypedarray(.uint8, size, buf, 0); -} + pub fn currentEpochParticipation(self: *const Self) !js.Uint8Array { + const env = js.env(); + var view = try (try self.requireState()).state.currentEpochParticipation(); -pub fn BeaconStateView_latestExecutionPayloadHeader(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - var header: AnyExecutionPayloadHeader = undefined; - try cached_state.state.latestExecutionPayloadHeader(allocator, &header); - defer header.deinit(allocator); - - return switch (header) { - .bellatrix => |*h| try sszValueToNapiValue(env, ct.bellatrix.ExecutionPayloadHeader, h), - .capella => |*h| try sszValueToNapiValue(env, ct.capella.ExecutionPayloadHeader, h), - .deneb => |*h| try sszValueToNapiValue(env, ct.deneb.ExecutionPayloadHeader, h), - }; -} + const size = try view.serializedSize(); + var bytes: [*]u8 = undefined; + const buf = try env.createArrayBuffer(size, &bytes); + _ = try view.serializeIntoBytes(bytes[0..size]); -/// Get the historical summaries from the state (Capella+). -/// Returns: array of {blockSummaryRoot: Uint8Array, stateSummaryRoot: Uint8Array} -pub fn BeaconStateView_historicalSummaries(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + return .{ .val = try env.createTypedarray(.uint8, size, buf, 0) }; + } - var historical_summaries_view = try cached_state.state.historicalSummaries(); - var historical_summaries = ct.capella.HistoricalSummaries.default_value; - try historical_summaries_view.toValue(allocator, &historical_summaries); - defer historical_summaries.deinit(allocator); + pub fn latestExecutionPayloadHeader(self: *const Self) !js.Value { + const env = js.env(); + var header: AnyExecutionPayloadHeader = undefined; + try (try self.requireState()).state.latestExecutionPayloadHeader(allocator, &header); + defer header.deinit(allocator); - return try sszValueToNapiValue(env, ct.capella.HistoricalSummaries, &historical_summaries); -} + const value = switch (header) { + .bellatrix => |*h| try sszValueToNapiValue(env, ct.bellatrix.ExecutionPayloadHeader, h), + .capella => |*h| try sszValueToNapiValue(env, ct.capella.ExecutionPayloadHeader, h), + .deneb => |*h| try sszValueToNapiValue(env, ct.deneb.ExecutionPayloadHeader, h), + }; + return asJsValue(value); + } -/// Get the pending deposits from the state (Electra+). -/// Returns: Uint8Array of SSZ serialized PendingDeposits list -pub fn BeaconStateView_pendingDeposits(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - - var pending_deposits = cached_state.state.pendingDeposits() catch { - try env.throwError("STATE_ERROR", "Failed to get pendingDeposits"); - return env.getNull(); - }; - - const size = pending_deposits.serializedSize() catch { - try env.throwError("STATE_ERROR", "Failed to get pendingDeposits size"); - return env.getNull(); - }; - - var bytes: [*]u8 = undefined; - const buf = try env.createArrayBuffer(size, &bytes); - _ = pending_deposits.serializeIntoBytes(bytes[0..size]) catch { - try env.throwError("STATE_ERROR", "Failed to serialize pendingDeposits"); - return env.getNull(); - }; - - return try env.createTypedarray(.uint8, size, buf, 0); -} + pub fn historicalSummaries(self: *const Self) !js.Value { + const env = js.env(); + var historical_summaries_view = try (try self.requireState()).state.historicalSummaries(); + var historical_summaries = ct.capella.HistoricalSummaries.default_value; + try historical_summaries_view.toValue(allocator, &historical_summaries); + defer historical_summaries.deinit(allocator); + return asJsValue(try sszValueToNapiValue(env, ct.capella.HistoricalSummaries, &historical_summaries)); + } -pub fn BeaconStateView_pendingDepositsCount(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - var pending_deposits = try cached_state.state.pendingDeposits(); - return try env.createInt64(@intCast(try pending_deposits.length())); -} + pub fn pendingDeposits(self: *const Self) !js.Value { + const env = js.env(); + const cached_state = try self.requireState(); -/// Get the pending partial withdrawals from the state (Electra+). -/// Returns: Uint8Array of SSZ serialized PendingPartialWithdrawals list -pub fn BeaconStateView_pendingPartialWithdrawals(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - - var pending_partial_withdrawals = cached_state.state.pendingPartialWithdrawals() catch { - try env.throwError("STATE_ERROR", "Failed to get pendingPartialWithdrawals"); - return env.getNull(); - }; - - const size = pending_partial_withdrawals.serializedSize() catch { - try env.throwError("STATE_ERROR", "Failed to get pendingPartialWithdrawals size"); - return env.getNull(); - }; - - var bytes: [*]u8 = undefined; - const buf = try env.createArrayBuffer(size, &bytes); - _ = pending_partial_withdrawals.serializeIntoBytes(bytes[0..size]) catch { - try env.throwError("STATE_ERROR", "Failed to serialize pendingPartialWithdrawals"); - return env.getNull(); - }; - - return try env.createTypedarray(.uint8, size, buf, 0); -} + var pending_deposits = cached_state.state.pendingDeposits() catch { + return throwNull("STATE_ERROR", "Failed to get pendingDeposits"); + }; -pub fn BeaconStateView_pendingPartialWithdrawalsCount(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - var pending_partial_withdrawals = try cached_state.state.pendingPartialWithdrawals(); - return try env.createInt64(@intCast(try pending_partial_withdrawals.length())); -} + const size = pending_deposits.serializedSize() catch { + return throwNull("STATE_ERROR", "Failed to get pendingDeposits size"); + }; -/// Get the pending consolidations from the state -pub fn BeaconStateView_pendingConsolidations(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + var bytes: [*]u8 = undefined; + const buf = try env.createArrayBuffer(size, &bytes); + _ = pending_deposits.serializeIntoBytes(bytes[0..size]) catch { + return throwNull("STATE_ERROR", "Failed to serialize pendingDeposits"); + }; - var pending_consolidations = cached_state.state.pendingConsolidations() catch { - try env.throwError("STATE_ERROR", "Failed to get pendingConsolidations"); - return env.getNull(); - }; + return asJsValue(try env.createTypedarray(.uint8, size, buf, 0)); + } - const size = pending_consolidations.serializedSize() catch { - try env.throwError("STATE_ERROR", "Failed to get pendingConsolidations size"); - return env.getNull(); - }; + pub fn pendingDepositsCount(self: *const Self) !js.Number { + var pending_deposits = try (try self.requireState()).state.pendingDeposits(); + return js.Number.from(try pending_deposits.length()); + } - var bytes: [*]u8 = undefined; - const buf = try env.createArrayBuffer(size, &bytes); - _ = pending_consolidations.serializeIntoBytes(bytes[0..size]) catch { - try env.throwError("STATE_ERROR", "Failed to serialize pendingConsolidations"); - return env.getNull(); - }; + pub fn pendingPartialWithdrawals(self: *const Self) !js.Value { + const env = js.env(); + const cached_state = try self.requireState(); - return try env.createTypedarray(.uint8, size, buf, 0); -} + var pending_partial_withdrawals = cached_state.state.pendingPartialWithdrawals() catch { + return throwNull("STATE_ERROR", "Failed to get pendingPartialWithdrawals"); + }; + const size = pending_partial_withdrawals.serializedSize() catch { + return throwNull("STATE_ERROR", "Failed to get pendingPartialWithdrawals size"); + }; -pub fn BeaconStateView_pendingConsolidationsCount(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - var pending_consolidations = try cached_state.state.pendingConsolidations(); - return try env.createInt64(@intCast(try pending_consolidations.length())); -} + var bytes: [*]u8 = undefined; + const buf = try env.createArrayBuffer(size, &bytes); + _ = pending_partial_withdrawals.serializeIntoBytes(bytes[0..size]) catch { + return throwNull("STATE_ERROR", "Failed to serialize pendingPartialWithdrawals"); + }; + return asJsValue(try env.createTypedarray(.uint8, size, buf, 0)); + } -/// Get the proposer lookahead from the state (Fulu+). -pub fn BeaconStateView_proposerLookahead(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + pub fn pendingPartialWithdrawalsCount(self: *const Self) !js.Number { + var pending_partial_withdrawals = try (try self.requireState()).state.pendingPartialWithdrawals(); + return js.Number.from(try pending_partial_withdrawals.length()); + } - var proposer_lookahead = cached_state.state.proposerLookahead() catch { - try env.throwError("STATE_ERROR", "Failed to get proposerLookahead"); - return env.getNull(); - }; + pub fn pendingConsolidations(self: *const Self) !js.Value { + const env = js.env(); + const cached_state = try self.requireState(); - const lookahead = proposer_lookahead.getAll(allocator) catch { - try env.throwError("STATE_ERROR", "Failed to get proposerLookahead values"); - return env.getNull(); - }; - defer allocator.free(lookahead); + var pending_consolidations = cached_state.state.pendingConsolidations() catch { + return throwNull("STATE_ERROR", "Failed to get pendingConsolidations"); + }; + const size = pending_consolidations.serializedSize() catch { + return throwNull("STATE_ERROR", "Failed to get pendingConsolidations size"); + }; - return try numberSliceToNapiValue(env, u64, lookahead, .{ .typed_array = .uint32 }); -} + var bytes: [*]u8 = undefined; + const buf = try env.createArrayBuffer(size, &bytes); + _ = pending_consolidations.serializeIntoBytes(bytes[0..size]) catch { + return throwNull("STATE_ERROR", "Failed to serialize pendingConsolidations"); + }; -// pub fn BeaconStateView_executionPayloadAvailability + return asJsValue(try env.createTypedarray(.uint8, size, buf, 0)); + } -// pub fn BeaconStateView_getShufflingAtEpoch + pub fn pendingConsolidationsCount(self: *const Self) !js.Number { + var pending_consolidations = try (try self.requireState()).state.pendingConsolidations(); + return js.Number.from(try pending_consolidations.length()); + } -/// Get the previous decision root for the state. -pub fn BeaconStateView_previousDecisionRoot(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - const root = cached_state.previousDecisionRoot(); - return sszValueToNapiValue(env, ct.primitive.Root, &root); -} + pub fn proposerLookahead(self: *const Self) !js.Value { + const env = js.env(); + const cached_state = try self.requireState(); -/// Get the current decision root for the state. -pub fn BeaconStateView_currentDecisionRoot(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - const root = cached_state.currentDecisionRoot(); - return sszValueToNapiValue(env, ct.primitive.Root, &root); -} + var proposer_lookahead = cached_state.state.proposerLookahead() catch { + return throwNull("STATE_ERROR", "Failed to get proposerLookahead"); + }; -/// Get the next decision root for the state. -pub fn BeaconStateView_nextDecisionRoot(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - const root = cached_state.nextDecisionRoot(); - return sszValueToNapiValue(env, ct.primitive.Root, &root); -} + const lookahead = proposer_lookahead.getAll(allocator) catch { + return throwNull("STATE_ERROR", "Failed to get proposerLookahead values"); + }; + defer allocator.free(lookahead); -/// Get the shuffling decision root for a given epoch. -pub fn BeaconStateView_getShufflingDecisionRoot(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - const epoch: u64 = @intCast(try cb.arg(0).getValueInt64()); - const root = st.calculateShufflingDecisionRoot(cached_state.state, epoch) catch { - try env.throwError("STATE_ERROR", "Failed to calculate shuffling decision root"); - return env.getNull(); - }; - return sszValueToNapiValue(env, ct.primitive.Root, &root); -} + return asJsValue(try numberSliceToNapiValue(env, u64, lookahead, .{ .typed_array = .uint32 })); + } -pub fn BeaconStateView_previousProposers(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - if (cached_state.epoch_cache.proposers_prev_epoch) |*proposers| { - return numberSliceToNapiValue( - env, - u64, - proposers, - .{}, - ); + pub fn previousDecisionRoot(self: *const Self) !js.Value { + const env = js.env(); + const root = (try self.requireState()).previousDecisionRoot(); + return asJsValue(try sszValueToNapiValue(env, ct.primitive.Root, &root)); } - return env.getNull(); -} -pub fn BeaconStateView_currentProposers(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - return numberSliceToNapiValue( - env, - u64, - &cached_state.epoch_cache.proposers, - .{}, - ); -} -pub fn BeaconStateView_nextProposers(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - if (cached_state.epoch_cache.proposers_next_epoch) |*proposers| { - return numberSliceToNapiValue( - env, - u64, - proposers, - .{}, - ); + pub fn currentDecisionRoot(self: *const Self) !js.Value { + const env = js.env(); + const root = (try self.requireState()).currentDecisionRoot(); + return asJsValue(try sszValueToNapiValue(env, ct.primitive.Root, &root)); } - return env.getNull(); -} -/// Get the beacon proposer for a given slot. -/// Arguments: -/// - arg 0: slot (number) -/// Returns: validator index of the proposer -pub fn BeaconStateView_getBeaconProposer(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - const slot: u64 = @intCast(try cb.arg(0).getValueInt64()); - const proposer = try cached_state.epoch_cache.getBeaconProposer(slot); - return try env.createInt64(@intCast(proposer)); -} + pub fn nextDecisionRoot(self: *const Self) !js.Value { + const env = js.env(); + const root = (try self.requireState()).nextDecisionRoot(); + return asJsValue(try sszValueToNapiValue(env, ct.primitive.Root, &root)); + } -pub fn BeaconStateView_currentSyncCommittee(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - var current_sync_committee = try cached_state.state.currentSyncCommittee(); - var result: ct.altair.SyncCommittee.Type = undefined; - try current_sync_committee.toValue(allocator, &result); - return try sszValueToNapiValue(env, ct.altair.SyncCommittee, &result); -} + pub fn getShufflingDecisionRoot(self: *const Self, epoch_arg: js.Number) !js.Value { + const env = js.env(); + const epoch_value: u64 = @intCast(try epoch_arg.toI64()); + const root = st.calculateShufflingDecisionRoot((try self.requireState()).state, epoch_value) catch { + return throwNull("STATE_ERROR", "Failed to calculate shuffling decision root"); + }; + return asJsValue(try sszValueToNapiValue(env, ct.primitive.Root, &root)); + } -pub fn BeaconStateView_nextSyncCommittee(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - var next_sync_committee = try cached_state.state.nextSyncCommittee(); - var result: ct.altair.SyncCommittee.Type = undefined; - try next_sync_committee.toValue(allocator, &result); - return try sszValueToNapiValue(env, ct.altair.SyncCommittee, &result); -} + pub fn previousProposers(self: *const Self) !?js.Array { + const env = js.env(); + const cached_state = try self.requireState(); + if (cached_state.epoch_cache.proposers_prev_epoch) |*proposers| { + return .{ .val = try numberSliceToNapiValue(env, u64, proposers, .{}) }; + } + return null; + } -pub fn BeaconStateView_currentSyncCommitteeIndexed(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - const sync_committee_cache = cached_state.epoch_cache.current_sync_committee_indexed.get(); - const validator_indices = sync_committee_cache.getValidatorIndices(); - const validator_index_map = sync_committee_cache.getValidatorIndexMap(); - const obj = try env.createObject(); - try obj.setNamedProperty( - "validatorIndices", - try numberSliceToNapiValue( - env, - u32, - @as([]const u32, @ptrCast(validator_indices)), - .{ .typed_array = null }, - ), - ); + pub fn currentProposers(self: *const Self) !js.Array { + const env = js.env(); + const cached_state = try self.requireState(); + return .{ .val = try numberSliceToNapiValue(env, u64, &cached_state.epoch_cache.proposers, .{}) }; + } - const global = try env.getGlobal(); - const map_ctor = try global.getNamedProperty("Map"); - const map = try env.newInstance(map_ctor, .{}); - const set_fn = try map.getNamedProperty("set"); + pub fn nextProposers(self: *const Self) !?js.Array { + const env = js.env(); + const cached_state = try self.requireState(); + if (cached_state.epoch_cache.proposers_next_epoch) |*proposers| { + return .{ .val = try numberSliceToNapiValue(env, u64, proposers, .{}) }; + } + return null; + } - // TODO: might need to check the perf here; another way is to send a array instead and convert them in js side. - var iterator = validator_index_map.iterator(); - while (iterator.next()) |entry| { - const idx = entry.key_ptr.*; - const positions = entry.value_ptr; + pub fn getBeaconProposer(self: *const Self, slot_arg: js.Number) !js.Number { + const slot_value: u64 = @intCast(try slot_arg.toI64()); + const proposer = try (try self.requireState()).epoch_cache.getBeaconProposer(slot_value); + return js.Number.from(proposer); + } - const key_value_napi = try env.createInt64(@intCast(idx)); - const positions_napi = try numberSliceToNapiValue(env, u32, @as([]const u32, @ptrCast(positions.items)), .{ .typed_array = .uint32 }); + pub fn currentSyncCommittee(self: *const Self) !js.Value { + const env = js.env(); + var current_sync_committee = try (try self.requireState()).state.currentSyncCommittee(); + var result: ct.altair.SyncCommittee.Type = undefined; + try current_sync_committee.toValue(allocator, &result); + return asJsValue(try sszValueToNapiValue(env, ct.altair.SyncCommittee, &result)); + } - _ = try env.callFunction(set_fn, map, .{ key_value_napi, positions_napi }); + pub fn nextSyncCommittee(self: *const Self) !js.Value { + const env = js.env(); + var next_sync_committee = try (try self.requireState()).state.nextSyncCommittee(); + var result: ct.altair.SyncCommittee.Type = undefined; + try next_sync_committee.toValue(allocator, &result); + return asJsValue(try sszValueToNapiValue(env, ct.altair.SyncCommittee, &result)); } - try obj.setNamedProperty("validatorIndexMap", map); + pub fn currentSyncCommitteeIndexed(self: *const Self) !js.Value { + const env = js.env(); + const cached_state = try self.requireState(); + const sync_committee_cache = cached_state.epoch_cache.current_sync_committee_indexed.get(); + const validator_indices = sync_committee_cache.getValidatorIndices(); + const validator_index_map = sync_committee_cache.getValidatorIndexMap(); + + const obj = try env.createObject(); + try obj.setNamedProperty( + "validatorIndices", + try numberSliceToNapiValue( + env, + u32, + @as([]const u32, @ptrCast(validator_indices)), + .{ .typed_array = null }, + ), + ); - return obj; -} + const global = try env.getGlobal(); + const map_ctor = try global.getNamedProperty("Map"); + const map = try env.newInstance(map_ctor, .{}); + const set_fn = try map.getNamedProperty("set"); + + var iterator = validator_index_map.iterator(); + while (iterator.next()) |entry| { + const idx = entry.key_ptr.*; + const positions = entry.value_ptr; + + const key_value_napi = try env.createInt64(@intCast(idx)); + const positions_napi = try numberSliceToNapiValue( + env, + u32, + @as([]const u32, @ptrCast(positions.items)), + .{ .typed_array = .uint32 }, + ); + + _ = try env.callFunction(set_fn, map, .{ key_value_napi, positions_napi }); + } -pub fn BeaconStateView_syncProposerReward(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - const sync_proposer_reward = cached_state.epoch_cache.sync_proposer_reward; - return try env.createInt64(@intCast(sync_proposer_reward)); -} + try obj.setNamedProperty("validatorIndexMap", map); + return asJsValue(obj); + } -/// Get the indexed sync committee at a given epoch. -/// Arguments: -/// - arg 0: epoch (number) -/// Returns: object with validatorIndices (number[]) -pub fn BeaconStateView_getIndexedSyncCommitteeAtEpoch(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - const epoch: u64 = @intCast(try cb.arg(0).getValueInt64()); - - const sync_committee = cached_state.epoch_cache.getIndexedSyncCommitteeAtEpoch(epoch) catch { - try env.throwError("NO_SYNC_COMMITTEE", "Sync committee not available for requested epoch"); - return env.getNull(); - }; - - const obj = try env.createObject(); - try obj.setNamedProperty( - "validatorIndices", - try numberSliceToNapiValue(env, u64, sync_committee.getValidatorIndices(), .{}), - ); - return obj; -} + pub fn syncProposerReward(self: *const Self) !js.Number { + const sync_proposer_reward = (try self.requireState()).epoch_cache.sync_proposer_reward; + return js.Number.from(sync_proposer_reward); + } -pub fn BeaconStateView_effectiveBalanceIncrements(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - const increments = cached_state.epoch_cache.getEffectiveBalanceIncrements(); - return try numberSliceToNapiValue(env, u16, increments.items, .{ .typed_array = .uint16 }); -} + pub fn getIndexedSyncCommitteeAtEpoch(self: *const Self, epoch_arg: js.Number) !js.Value { + const env = js.env(); + const cached_state = try self.requireState(); + const epoch_value: u64 = @intCast(try epoch_arg.toI64()); -pub fn BeaconStateView_getEffectiveBalanceIncrementsZeroInactive(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + const sync_committee = cached_state.epoch_cache.getIndexedSyncCommitteeAtEpoch(epoch_value) catch { + return throwNull("NO_SYNC_COMMITTEE", "Sync committee not available for requested epoch"); + }; - var result = try st.getEffectiveBalanceIncrementsZeroInactive(allocator, cached_state); - defer result.deinit(); + const obj = try env.createObject(); + try obj.setNamedProperty( + "validatorIndices", + try numberSliceToNapiValue(env, u64, sync_committee.getValidatorIndices(), .{}), + ); + return asJsValue(obj); + } - return try numberSliceToNapiValue(env, u16, result.items, .{ .typed_array = .uint16 }); -} + pub fn effectiveBalanceIncrements(self: *const Self) !js.Uint16Array { + const env = js.env(); + const increments = (try self.requireState()).epoch_cache.getEffectiveBalanceIncrements(); + return .{ .val = try numberSliceToNapiValue(env, u16, increments.items, .{ .typed_array = .uint16 }) }; + } -pub fn BeaconStateView_getBalance(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - const index: u64 = @intCast(try cb.arg(0).getValueInt64()); - var balances = try cached_state.state.balances(); - const balance = try balances.get(index); - return try env.createBigintUint64(balance); -} + pub fn getEffectiveBalanceIncrementsZeroInactive(self: *const Self) !js.Uint16Array { + const env = js.env(); + var result = try st.getEffectiveBalanceIncrementsZeroInactive(allocator, try self.requireState()); + defer result.deinit(); + return .{ .val = try numberSliceToNapiValue(env, u16, result.items, .{ .typed_array = .uint16 }) }; + } -/// Get a validator by index. -pub fn BeaconStateView_getValidator(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - const index: u64 = @intCast(try cb.arg(0).getValueInt64()); + pub fn getBalance(self: *const Self, index_arg: js.Number) !js.BigInt { + const index_value: u64 = @intCast(try index_arg.toI64()); + var balances = try (try self.requireState()).state.balances(); + const balance = try balances.get(index_value); + return js.BigInt.from(balance); + } - var validators = try cached_state.state.validators(); - var validator_view = try validators.get(index); - var validator: ct.phase0.Validator.Type = undefined; - try validator_view.toValue(allocator, &validator); + pub fn getValidator(self: *const Self, index_arg: js.Number) !js.Value { + const env = js.env(); + const index_value: u64 = @intCast(try index_arg.toI64()); - return try sszValueToNapiValue(env, ct.phase0.Validator, &validator); -} + var validators = try (try self.requireState()).state.validators(); + var validator_view = try validators.get(index_value); + var validator: ct.phase0.Validator.Type = undefined; + try validator_view.toValue(allocator, &validator); -/// Get the status of a validator by index. -/// Arguments: -/// - arg 0: validator index (number) -/// Returns: status string -pub fn BeaconStateView_getValidatorStatus(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - const index: u64 = @intCast(try cb.arg(0).getValueInt64()); - const current_epoch = cached_state.epoch_cache.epoch; - - var validators = try cached_state.state.validators(); - var validator_view = try validators.get(index); - var validator: ct.phase0.Validator.Type = undefined; - try validator_view.toValue(allocator, &validator); - - const status = st.getValidatorStatus(&validator, current_epoch); - return try env.createStringUtf8(status.toString()); -} + return asJsValue(try sszValueToNapiValue(env, ct.phase0.Validator, &validator)); + } -/// Get the total number of validators in the registry. -pub fn BeaconStateView_validatorCount(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - const count = try cached_state.state.validatorsCount(); - return try env.createInt64(@intCast(count)); -} + pub fn getValidatorStatus(self: *const Self, index_arg: js.Number) !js.String { + const cached_state = try self.requireState(); + const index_value: u64 = @intCast(try index_arg.toI64()); + const current_epoch = cached_state.epoch_cache.epoch; -/// Get the number of active validators at the current epoch. -pub fn BeaconStateView_activeValidatorCount(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - const epoch_cache = cached_state.epoch_cache; - const count = epoch_cache.current_shuffling.get().active_indices.len; - return try env.createInt64(@intCast(count)); -} + var validators = try cached_state.state.validators(); + var validator_view = try validators.get(index_value); + var validator: ct.phase0.Validator.Type = undefined; + try validator_view.toValue(allocator, &validator); -pub fn BeaconStateView_isExecutionStateType(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - const fork_seq = cached_state.state.forkSeq(); - return try env.getBoolean(fork_seq.gte(.bellatrix)); -} + const status = st.getValidatorStatus(&validator, current_epoch); + return js.String.from(status.toString()); + } -pub fn BeaconStateView_isExecutionEnabled(env: napi.Env, cb: napi.CallbackInfo(2)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - - // arg 0: fork name - var fork_name_buf: [16]u8 = undefined; - const fork_name = try cb.arg(0).getValueStringUtf8(&fork_name_buf); - const fork = c.ForkSeq.fromName(fork_name); - - // arg 1: signed block bytes - const bytes_info = try cb.arg(1).getTypedarrayInfo(); - - // Deserialize the signed block - const signed_block = try AnySignedBeaconBlock.deserialize( - allocator, - .full, - fork, - bytes_info.data, - ); - defer signed_block.deinit(allocator); - - if (signed_block.forkSeq() != cached_state.state.forkSeq()) { - try env.throwError("FORK_MISMATCH", "Fork of signed block does not match state fork"); - return env.getNull(); - } - const result = switch (cached_state.state.forkSeq()) { - inline else => |f| switch (signed_block.blockType()) { - inline else => |bt| if (comptime bt == .blinded and f.lt(.bellatrix)) { - return error.InvalidBlockTypeForFork; - } else st.isExecutionEnabled( - f, - cached_state.state.castToFork(f), - bt, - signed_block.beaconBlock().castToFork(bt, f), - ), - }, - }; - return try env.getBoolean(result); -} + pub fn validatorCount(self: *const Self) !js.Number { + const count = try (try self.requireState()).state.validatorsCount(); + return js.Number.from(count); + } -/// Check if the merge transition is complete. -/// Returns: boolean -pub fn BeaconStateView_isMergeTransitionComplete(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - const result = switch (cached_state.state.forkSeq()) { - inline else => |f| st.isMergeTransitionComplete(f, cached_state.state.castToFork(f)), - }; - return env.getBoolean(result); -} + pub fn activeValidatorCount(self: *const Self) !js.Number { + const count = (try self.requireState()).epoch_cache.current_shuffling.get().active_indices.len; + return js.Number.from(count); + } -// pub fn BeaconStateView_getExpectedWithdrawals - -/// Get the proposer rewards for the state. -pub fn BeaconStateView_proposerRewards(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - const rewards = cached_state.getProposerRewards(); - - const obj = try env.createObject(); - try obj.setNamedProperty( - "attestations", - try env.createBigintUint64(rewards.attestations), - ); - try obj.setNamedProperty( - "syncAggregate", - try env.createBigintUint64(rewards.sync_aggregate), - ); - try obj.setNamedProperty( - "slashing", - try env.createBigintUint64(rewards.slashing), - ); - return obj; -} + pub fn isExecutionStateType(self: *const Self) !js.Boolean { + const fork_seq = (try self.requireState()).state.forkSeq(); + return js.Boolean.from(fork_seq.gte(.bellatrix)); + } -// pub fn BeaconStateView_computeBlockRewards + pub fn isExecutionEnabled(self: *const Self, fork_name_value: js.String, signed_block_bytes: js.Uint8Array) !js.Value { + const env = js.env(); + const cached_state = try self.requireState(); -// pub fn BeaconStateView_computeAttestationRewards + var fork_name_buf: [16]u8 = undefined; + const fork_name = try fork_name_value.toSlice(&fork_name_buf); + const fork_seq = c.ForkSeq.fromName(fork_name); -// pub fn BeaconStateView_computeSyncCommitteeRewards + const bytes = try signed_block_bytes.toSlice(); + const signed_block = try AnySignedBeaconBlock.deserialize( + allocator, + .full, + fork_seq, + bytes, + ); + defer signed_block.deinit(allocator); -// pub fn BeaconStateView_getLatestWeakSubjectivityCheckpointEpoch + if (signed_block.forkSeq() != cached_state.state.forkSeq()) { + return throwNull("FORK_MISMATCH", "Fork of signed block does not match state fork"); + } -/// Get the validity status of a signed voluntary exit. -pub fn BeaconStateView_getVoluntaryExitValidity(env: napi.Env, cb: napi.CallbackInfo(2)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - const verify_signature = try cb.arg(1).getValueBool(); + const result = switch (cached_state.state.forkSeq()) { + inline else => |f| switch (signed_block.blockType()) { + inline else => |bt| if (comptime bt == .blinded and f.lt(.bellatrix)) { + return error.InvalidBlockTypeForFork; + } else st.isExecutionEnabled( + f, + cached_state.state.castToFork(f), + bt, + signed_block.beaconBlock().castToFork(bt, f), + ), + }, + }; + return asJsValue(try env.getBoolean(result)); + } - const bytes_info = try cb.arg(0).getTypedarrayInfo(); + pub fn isMergeTransitionComplete(self: *const Self) !js.Boolean { + const cached_state = try self.requireState(); + const result = switch (cached_state.state.forkSeq()) { + inline else => |f| st.isMergeTransitionComplete(f, cached_state.state.castToFork(f)), + }; + return js.Boolean.from(result); + } - var signed_voluntary_exit: ct.phase0.SignedVoluntaryExit.Type = ct.phase0.SignedVoluntaryExit.default_value; - ct.phase0.SignedVoluntaryExit.deserializeFromBytes(bytes_info.data, &signed_voluntary_exit) catch { - try env.throwError("DESERIALIZE_ERROR", "Failed to deserialize SignedVoluntaryExit"); - return env.getNull(); - }; + pub fn proposerRewards(self: *const Self) !js.Value { + const env = js.env(); + const rewards = (try self.requireState()).getProposerRewards(); - const result = switch (cached_state.state.forkSeq()) { - inline else => |f| st.getVoluntaryExitValidity( - f, - cached_state.config, - cached_state.epoch_cache, - cached_state.state.castToFork(f), - &signed_voluntary_exit, - verify_signature, - ), - }; - const validity = result catch { - try env.throwError("VALIDATION_ERROR", "Failed to get voluntary exit validity"); - return env.getNull(); - }; + const obj = try env.createObject(); + try obj.setNamedProperty("attestations", try env.createBigintUint64(rewards.attestations)); + try obj.setNamedProperty("syncAggregate", try env.createBigintUint64(rewards.sync_aggregate)); + try obj.setNamedProperty("slashing", try env.createBigintUint64(rewards.slashing)); + return asJsValue(obj); + } - return env.createStringUtf8(@tagName(validity)); -} + pub fn getVoluntaryExitValidity(self: *const Self, signed_exit_bytes: js.Uint8Array, verify_signature_value: js.Boolean) !js.Value { + const env = js.env(); + const cached_state = try self.requireState(); + const verify_signature = verify_signature_value.assertBool(); + const bytes = try signed_exit_bytes.toSlice(); -/// Check if a signed voluntary exit is valid. -pub fn BeaconStateView_isValidVoluntaryExit(env: napi.Env, cb: napi.CallbackInfo(2)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - const verify_signature = try cb.arg(1).getValueBool(); - - const bytes_info = try cb.arg(0).getTypedarrayInfo(); - - var signed_voluntary_exit: ct.phase0.SignedVoluntaryExit.Type = ct.phase0.SignedVoluntaryExit.default_value; - ct.phase0.SignedVoluntaryExit.deserializeFromBytes(bytes_info.data, &signed_voluntary_exit) catch { - try env.throwError("DESERIALIZE_ERROR", "Failed to deserialize SignedVoluntaryExit"); - return env.getNull(); - }; - - const result = switch (cached_state.state.forkSeq()) { - inline else => |f| st.isValidVoluntaryExit( - f, - cached_state.config, - cached_state.epoch_cache, - cached_state.state.castToFork(f), - &signed_voluntary_exit, - verify_signature, - ), - }; - const is_valid = result catch { - try env.throwError("VALIDATION_ERROR", "Failed to validate voluntary exit"); - return env.getNull(); - }; - - return env.getBoolean(is_valid); -} + var signed_voluntary_exit: ct.phase0.SignedVoluntaryExit.Type = ct.phase0.SignedVoluntaryExit.default_value; + ct.phase0.SignedVoluntaryExit.deserializeFromBytes(bytes, &signed_voluntary_exit) catch { + return throwNull("DESERIALIZE_ERROR", "Failed to deserialize SignedVoluntaryExit"); + }; -pub fn BeaconStateView_getFinalizedRootProof(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - var proof = try cached_state.state.getFinalizedRootProof(allocator); - defer proof.deinit(allocator); - - const witnesses = std.ArrayListUnmanaged([32]u8).fromOwnedSlice(proof.witnesses); - return try sszValueToNapiValue( - env, - // a compatible type for "a list of roots" - ct.phase0.HistoricalRoots, - &witnesses, - ); -} + const result = switch (cached_state.state.forkSeq()) { + inline else => |f| st.getVoluntaryExitValidity( + f, + cached_state.config, + cached_state.epoch_cache, + cached_state.state.castToFork(f), + &signed_voluntary_exit, + verify_signature, + ), + }; + const validity = result catch { + return throwNull("VALIDATION_ERROR", "Failed to get voluntary exit validity"); + }; + + return asJsValue(try env.createStringUtf8(@tagName(validity))); + } -// pub fn BeaconStateView_getSyncCommitteesWitness + pub fn isValidVoluntaryExit(self: *const Self, signed_exit_bytes: js.Uint8Array, verify_signature_value: js.Boolean) !js.Value { + const env = js.env(); + const cached_state = try self.requireState(); + const verify_signature = verify_signature_value.assertBool(); + const bytes = try signed_exit_bytes.toSlice(); -/// Get a single Merkle proof for a node at the given generalized index. -pub fn BeaconStateView_getSingleProof(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - const gindex: u64 = @intCast(try cb.arg(0).getValueInt64()); + var signed_voluntary_exit: ct.phase0.SignedVoluntaryExit.Type = ct.phase0.SignedVoluntaryExit.default_value; + ct.phase0.SignedVoluntaryExit.deserializeFromBytes(bytes, &signed_voluntary_exit) catch { + return throwNull("DESERIALIZE_ERROR", "Failed to deserialize SignedVoluntaryExit"); + }; - var proof = cached_state.state.getSingleProof(allocator, gindex) catch { - try env.throwError("STATE_ERROR", "Failed to get single proof"); - return env.getNull(); - }; - defer proof.deinit(allocator); + const result = switch (cached_state.state.forkSeq()) { + inline else => |f| st.isValidVoluntaryExit( + f, + cached_state.config, + cached_state.epoch_cache, + cached_state.state.castToFork(f), + &signed_voluntary_exit, + verify_signature, + ), + }; + const is_valid = result catch { + return throwNull("VALIDATION_ERROR", "Failed to validate voluntary exit"); + }; - const result = try env.createArray(); - for (proof.witnesses, 0..) |witness, i| { - var witness_bytes: [*]u8 = undefined; - const witness_buf = try env.createArrayBuffer(32, &witness_bytes); - @memcpy(witness_bytes[0..32], &witness); - try result.setElement(@intCast(i), try env.createTypedarray(.uint8, 32, witness_buf, 0)); + return asJsValue(try env.getBoolean(is_valid)); } - return result; -} + pub fn getFinalizedRootProof(self: *const Self) !js.Value { + const env = js.env(); + var proof = try (try self.requireState()).state.getFinalizedRootProof(allocator); + defer proof.deinit(allocator); -/// Create a compact multi-proof from a descriptor. -/// Arguments: -/// - arg 0: descriptor (Uint8Array) -/// Returns: {type: string, leaves: Uint8Array[], descriptor: Uint8Array} -pub fn BeaconStateView_createMultiProof(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { - const persistent_merkle_tree = @import("persistent_merkle_tree"); - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - - const descriptor_info = try cb.arg(0).getTypedarrayInfo(); - if (descriptor_info.array_type != .uint8) { - try env.throwTypeError("STATE_ERROR", "Expected descriptor to be a Uint8Array"); - return env.getNull(); - } - const descriptor = descriptor_info.data; - - // Get the root node from the state - try cached_state.state.commit(); - const root_node = switch (cached_state.state.*) { - inline else => |state| state.root, - }; - - // Create proof input for compact multi-proof - const proof_input = persistent_merkle_tree.proof.ProofInput{ - .compactMulti = .{ .descriptor = descriptor }, - }; - - var proof = persistent_merkle_tree.proof.createProof( - allocator, - &pool.state.pool, - root_node, - proof_input, - ) catch { - try env.throwError("STATE_ERROR", "Failed to create proof"); - return env.getNull(); - }; - defer proof.deinit(allocator); - - // Create result object matching Proof interface - const result = try env.createObject(); - - // Add type field - const proof_type_str = switch (proof) { - inline else => |_, tag| @tagName(tag), - }; - try result.setNamedProperty("type", try env.createStringUtf8(proof_type_str)); - - // Extract data based on proof type - switch (proof) { - .compactMulti => |compact| { - // Create leaves array - const leaves_array = try env.createArray(); - for (compact.leaves, 0..) |leaf, i| { - var leaf_bytes: [*]u8 = undefined; - const leaf_buf = try env.createArrayBuffer(32, &leaf_bytes); - @memcpy(leaf_bytes[0..32], &leaf); - try leaves_array.setElement(@intCast(i), try env.createTypedarray(.uint8, 32, leaf_buf, 0)); - } - try result.setNamedProperty("leaves", leaves_array); - - // Create descriptor Uint8Array - var descriptor_bytes: [*]u8 = undefined; - const descriptor_buf = try env.createArrayBuffer(compact.descriptor.len, &descriptor_bytes); - @memcpy(descriptor_bytes[0..compact.descriptor.len], compact.descriptor); - try result.setNamedProperty("descriptor", try env.createTypedarray(.uint8, compact.descriptor.len, descriptor_buf, 0)); - }, - else => { - try env.throwError("STATE_ERROR", "Unexpected proof type"); - return env.getNull(); - }, + const witnesses = std.ArrayListUnmanaged([32]u8).fromOwnedSlice(proof.witnesses); + return asJsValue(try sszValueToNapiValue( + env, + ct.phase0.HistoricalRoots, + &witnesses, + )); } - return result; -} - -pub fn BeaconStateView_computeUnrealizedCheckpoints(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - const result = try st.computeUnrealizedCheckpoints(cached_state, allocator); - - const obj = try env.createObject(); - try obj.setNamedProperty( - "justifiedCheckpoint", - try sszValueToNapiValue(env, ct.phase0.Checkpoint, &result.justified_checkpoint), - ); - try obj.setNamedProperty( - "finalizedCheckpoint", - try sszValueToNapiValue(env, ct.phase0.Checkpoint, &result.finalized_checkpoint), - ); - return obj; -} + pub fn getSingleProof(self: *const Self, gindex_arg: js.Number) !js.Value { + const env = js.env(); + const gindex: u64 = @intCast(try gindex_arg.toI64()); -pub fn BeaconStateView_clonedCount(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - return try env.createInt64(@intCast(cached_state.cloned_count)); -} + var proof = (try self.requireState()).state.getSingleProof(allocator, gindex) catch { + return throwNull("STATE_ERROR", "Failed to get single proof"); + }; + defer proof.deinit(allocator); + + const result = try env.createArray(); + for (proof.witnesses, 0..) |witness, i| { + var witness_bytes: [*]u8 = undefined; + const witness_buf = try env.createArrayBuffer(32, &witness_bytes); + @memcpy(witness_bytes[0..32], &witness); + try result.setElement(@intCast(i), try env.createTypedarray(.uint8, 32, witness_buf, 0)); + } -pub fn BeaconStateView_clonedCountWithTransferCache(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - return try env.createInt64(@intCast(cached_state.cloned_count_with_transfer_cache)); -} + return asJsValue(result); + } -pub fn BeaconStateView_createdWithTransferCache(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - return try env.getBoolean(cached_state.created_with_transfer_cache); -} + pub fn createMultiProof(self: *const Self, descriptor: js.Uint8Array) !js.Value { + const persistent_merkle_tree = @import("persistent_merkle_tree"); + const env = js.env(); + const cached_state = try self.requireState(); + const descriptor_bytes = try descriptor.toSlice(); -// pub fn BeaconStateView_isStateValidatorsNodesPopulated + try cached_state.state.commit(); + const root_node = switch (cached_state.state.*) { + inline else => |state| state.root, + }; -// pub fn BeaconStateView_loadOtherState + const proof_input = persistent_merkle_tree.proof.ProofInput{ + .compactMulti = .{ .descriptor = descriptor_bytes }, + }; -pub fn BeaconStateView_serialize(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - const result = try cached_state.state.serialize(allocator); - defer allocator.free(result); + var proof = persistent_merkle_tree.proof.createProof( + allocator, + &pool.state.pool, + root_node, + proof_input, + ) catch { + return throwNull("STATE_ERROR", "Failed to create proof"); + }; + defer proof.deinit(allocator); - return try numberSliceToNapiValue(env, u8, result, .{ .typed_array = .uint8 }); -} + const result = try env.createObject(); + const proof_type_str = switch (proof) { + inline else => |_, tag| @tagName(tag), + }; + try result.setNamedProperty("type", try env.createStringUtf8(proof_type_str)); + + switch (proof) { + .compactMulti => |compact| { + const leaves_array = try env.createArray(); + for (compact.leaves, 0..) |leaf, i| { + var leaf_bytes: [*]u8 = undefined; + const leaf_buf = try env.createArrayBuffer(32, &leaf_bytes); + @memcpy(leaf_bytes[0..32], &leaf); + try leaves_array.setElement(@intCast(i), try env.createTypedarray(.uint8, 32, leaf_buf, 0)); + } + try result.setNamedProperty("leaves", leaves_array); + + var descriptor_buf_bytes: [*]u8 = undefined; + const descriptor_buf = try env.createArrayBuffer(compact.descriptor.len, &descriptor_buf_bytes); + @memcpy(descriptor_buf_bytes[0..compact.descriptor.len], compact.descriptor); + try result.setNamedProperty( + "descriptor", + try env.createTypedarray(.uint8, compact.descriptor.len, descriptor_buf, 0), + ); + }, + else => return throwNull("STATE_ERROR", "Unexpected proof type"), + } -pub fn BeaconStateView_serializedSize(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - const size = switch (cached_state.state.*) { - inline else => |state| try state.serializedSize(), - }; - return try env.createInt64(@intCast(size)); -} + return asJsValue(result); + } -/// arg 0: output: preallocated Uint8Array buffer -/// arg 1: offset: offset of buffer where serialization should start -pub fn BeaconStateView_serializeToBytes(env: napi.Env, cb: napi.CallbackInfo(2)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + pub fn computeUnrealizedCheckpoints(self: *const Self) !js.Value { + const env = js.env(); + const result = try st.computeUnrealizedCheckpoints(try self.requireState(), allocator); - const output_info = try cb.arg(0).getTypedarrayInfo(); - if (output_info.array_type != .uint8) { - return error.InvalidOutputBufferType; + const obj = try env.createObject(); + try obj.setNamedProperty( + "justifiedCheckpoint", + try sszValueToNapiValue(env, ct.phase0.Checkpoint, &result.justified_checkpoint), + ); + try obj.setNamedProperty( + "finalizedCheckpoint", + try sszValueToNapiValue(env, ct.phase0.Checkpoint, &result.finalized_checkpoint), + ); + return asJsValue(obj); } - const offset = try cb.arg(1).getValueUint32(); - if (offset > output_info.length) { - return error.InvalidOffset; + pub fn clonedCount(self: *const Self) !js.Number { + return js.Number.from((try self.requireState()).cloned_count); } - const output_slice = output_info.data[offset..]; - const bytes_written = switch (cached_state.state.*) { - inline else => |state| try state.serializeIntoBytes(output_slice), - }; + pub fn clonedCountWithTransferCache(self: *const Self) !js.Number { + return js.Number.from((try self.requireState()).cloned_count_with_transfer_cache); + } - return try env.createInt64(@intCast(bytes_written)); -} + pub fn createdWithTransferCache(self: *const Self) !js.Boolean { + return js.Boolean.from((try self.requireState()).created_with_transfer_cache); + } -pub fn BeaconStateView_serializeValidators(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - var validators_view = try cached_state.state.validators(); + pub fn serialize(self: *const Self) !js.Uint8Array { + const env = js.env(); + const result = try (try self.requireState()).state.serialize(allocator); + defer allocator.free(result); + return .{ .val = try numberSliceToNapiValue(env, u8, result, .{ .typed_array = .uint8 }) }; + } - const size = try validators_view.serializedSize(); - var arraybuffer_bytes: [*]u8 = undefined; - const arraybuffer = try env.createArrayBuffer(size, &arraybuffer_bytes); - _ = try validators_view.serializeIntoBytes(arraybuffer_bytes[0..size]); - return try env.createTypedarray(.uint8, size, arraybuffer, 0); -} + pub fn serializedSize(self: *const Self) !js.Number { + const cached_state = try self.requireState(); + const size = switch (cached_state.state.*) { + inline else => |state| try state.serializedSize(), + }; + return js.Number.from(size); + } -pub fn BeaconStateView_serializedValidatorsSize(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - var validators_view = try cached_state.state.validators(); - const size = try validators_view.serializedSize(); - return try env.createInt64(@intCast(size)); -} + pub fn serializeToBytes(self: *const Self, output: js.Uint8Array, offset: js.Number) !js.Number { + const output_slice = try output.toSlice(); + const off = try offset.toU32(); + if (off > output_slice.len) return error.InvalidOffset; -pub fn BeaconStateView_serializeValidatorsToBytes(env: napi.Env, cb: napi.CallbackInfo(2)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + const cached_state = try self.requireState(); + const bytes_written = switch (cached_state.state.*) { + inline else => |state| try state.serializeIntoBytes(output_slice[off..]), + }; - // arg 0: output - const output_info = try cb.arg(0).getTypedarrayInfo(); - if (output_info.array_type != .uint8) { - return error.InvalidOutputBufferType; + return js.Number.from(bytes_written); } - // arg 1: offset - const offset = try cb.arg(1).getValueUint32(); - if (offset > output_info.length) { - return error.InvalidOffset; + pub fn serializeValidators(self: *const Self) !js.Uint8Array { + const env = js.env(); + var validators_view = try (try self.requireState()).state.validators(); + + const size = try validators_view.serializedSize(); + var arraybuffer_bytes: [*]u8 = undefined; + const arraybuffer = try env.createArrayBuffer(size, &arraybuffer_bytes); + _ = try validators_view.serializeIntoBytes(arraybuffer_bytes[0..size]); + return .{ .val = try env.createTypedarray(.uint8, size, arraybuffer, 0) }; } - var validators_view = try cached_state.state.validators(); - const output_slice = output_info.data[offset..]; - const bytes_written = try validators_view.serializeIntoBytes(output_slice); - return try env.createInt64(@intCast(bytes_written)); -} + pub fn serializedValidatorsSize(self: *const Self) !js.Number { + var validators_view = try (try self.requireState()).state.validators(); + const size = try validators_view.serializedSize(); + return js.Number.from(size); + } -pub fn BeaconStateView_hashTreeRoot(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - const root = try cached_state.state.hashTreeRoot(); - return try numberSliceToNapiValue(env, u8, root, .{ .typed_array = .uint8 }); -} + pub fn serializeValidatorsToBytes(self: *const Self, output: js.Uint8Array, offset: js.Number) !js.Number { + const output_slice = try output.toSlice(); + const off = try offset.toU32(); + if (off > output_slice.len) return error.InvalidOffset; -// pub fn BeaconStateView_stateTransition - -/// Process slots from current state slot to target slot, returning a new BeaconStateView. -/// -/// Arguments: -/// - arg 0: target slot (number) -/// - arg 1: options object (optional) with `transferCache` boolean -pub fn BeaconStateView_processSlots(env: napi.Env, cb: napi.CallbackInfo(2)) !napi.Value { - const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - const slot: u64 = @intCast(try cb.arg(0).getValueInt64()); - - var transfer_cache = false; - if (cb.getArg(1)) |options_arg| { - if (try options_arg.typeof() == .object) { - if (try options_arg.hasNamedProperty("transferCache")) { - transfer_cache = try (try options_arg.getNamedProperty("transferCache")).getValueBool(); - std.debug.print("has transferCache: {}\n", .{transfer_cache}); - } - } - } - const post_state = try cached_state.clone(allocator, .{ .transfer_cache = transfer_cache }); - errdefer { - post_state.deinit(); - allocator.destroy(post_state); + var validators_view = try (try self.requireState()).state.validators(); + const bytes_written = try validators_view.serializeIntoBytes(output_slice[off..]); + return js.Number.from(bytes_written); } - try st.processSlots(allocator, post_state, slot, .{}); - - const ctor = try cb.this().getNamedProperty("constructor"); - const new_state_value = try env.newInstance(ctor, .{}); - const dummy_state = try env.unwrap(CachedBeaconState, new_state_value); - dummy_state.* = post_state.*; - allocator.destroy(post_state); + pub fn hashTreeRoot(self: *const Self) !js.Uint8Array { + const env = js.env(); + const root = try (try self.requireState()).state.hashTreeRoot(); + return .{ .val = try numberSliceToNapiValue(env, u8, root, .{ .typed_array = .uint8 }) }; + } - return new_state_value; -} + pub fn processSlots(self: *const Self, slot_arg: js.Number, options: ?js.Value) !Self { + const slot_value: u64 = @intCast(try slot_arg.toI64()); + const transfer_cache = try optionalBool(options, "transferCache", false); + const post_state = try (try self.requireState()).clone(allocator, .{ .transfer_cache = transfer_cache }); + errdefer { + post_state.deinit(); + allocator.destroy(post_state); + } -pub fn register(env: napi.Env, exports: napi.Value) !void { - const beacon_state_view_ctor = try env.defineClass( - "BeaconStateView", - 0, - BeaconStateView_ctor, - null, - &[_]napi.c.napi_property_descriptor{ - getter(BeaconStateView_slot), - getter(BeaconStateView_fork), - getter(BeaconStateView_epoch), - getter(BeaconStateView_genesisTime), - getter(BeaconStateView_genesisValidatorsRoot), - getter(BeaconStateView_eth1Data), - getter(BeaconStateView_latestBlockHeader), - getter(BeaconStateView_previousJustifiedCheckpoint), - getter(BeaconStateView_currentJustifiedCheckpoint), - getter(BeaconStateView_finalizedCheckpoint), - method(1, BeaconStateView_getBlockRoot), - method(1, BeaconStateView_getRandaoMix), - getter(BeaconStateView_previousEpochParticipation), - getter(BeaconStateView_currentEpochParticipation), - getter(BeaconStateView_latestExecutionPayloadHeader), - getter(BeaconStateView_historicalSummaries), - getter(BeaconStateView_pendingDeposits), - getter(BeaconStateView_pendingDepositsCount), - getter(BeaconStateView_pendingPartialWithdrawals), - getter(BeaconStateView_pendingPartialWithdrawalsCount), - getter(BeaconStateView_pendingConsolidations), - getter(BeaconStateView_pendingConsolidationsCount), - getter(BeaconStateView_proposerLookahead), - // getter(BeaconStateView_executionPayloadAvailability), - - // method(1, BeaconStateView_getShufflingAtEpoch), - getter(BeaconStateView_previousDecisionRoot), - getter(BeaconStateView_currentDecisionRoot), - getter(BeaconStateView_nextDecisionRoot), - method(1, BeaconStateView_getShufflingDecisionRoot), - getter(BeaconStateView_currentProposers), - getter(BeaconStateView_nextProposers), - getter(BeaconStateView_previousProposers), - method(1, BeaconStateView_getBeaconProposer), - getter(BeaconStateView_currentSyncCommittee), - getter(BeaconStateView_nextSyncCommittee), - getter(BeaconStateView_currentSyncCommitteeIndexed), - getter(BeaconStateView_syncProposerReward), - method(1, BeaconStateView_getIndexedSyncCommitteeAtEpoch), - - getter(BeaconStateView_effectiveBalanceIncrements), - method(0, BeaconStateView_getEffectiveBalanceIncrementsZeroInactive), - method(1, BeaconStateView_getBalance), - method(1, BeaconStateView_getValidator), - method(1, BeaconStateView_getValidatorStatus), - getter(BeaconStateView_validatorCount), - getter(BeaconStateView_activeValidatorCount), - - getter(BeaconStateView_isMergeTransitionComplete), - getter(BeaconStateView_isExecutionStateType), - method(2, BeaconStateView_isExecutionEnabled), - - // method(0, BeaconStateView_getExpectedWithdrawals), - - getter(BeaconStateView_proposerRewards), - // method(2, BeaconStateView_computeBlockRewards), - // method(2, BeaconStateView_computeAttestationRewards), - // method(2, BeaconStateView_computeSyncCommitteeRewards), - // method(0, BeaconStateView_getLatestWeakSubjectivityCheckpointEpoch), - - method(2, BeaconStateView_getVoluntaryExitValidity), - method(2, BeaconStateView_isValidVoluntaryExit), - - method(0, BeaconStateView_getFinalizedRootProof), - // method(1, BeaconStateView_getSyncCommitteesWitness), - method(1, BeaconStateView_getSingleProof), - method(1, BeaconStateView_createMultiProof), - - method(0, BeaconStateView_computeUnrealizedCheckpoints), - - getter(BeaconStateView_clonedCount), - getter(BeaconStateView_clonedCountWithTransferCache), - getter(BeaconStateView_createdWithTransferCache), - // getter(BeaconStateView_isStateValidatorsNodesPopulated), - - // method(2, BeaconStateView_loadOtherState), - method(0, BeaconStateView_serialize), - method(0, BeaconStateView_serializedSize), - method(2, BeaconStateView_serializeToBytes), - method(0, BeaconStateView_serializeValidators), - method(0, BeaconStateView_serializedValidatorsSize), - method(2, BeaconStateView_serializeValidatorsToBytes), - - method(0, BeaconStateView_hashTreeRoot), - - // method(2, BeaconStateView_stateTransition), - method(2, BeaconStateView_processSlots), - }, - ); - // Static method on constructor - try beacon_state_view_ctor.defineProperties(&[_]napi.c.napi_property_descriptor{.{ - .utf8name = "createFromBytes", - .method = napi.wrapCallback(1, BeaconStateView_createFromBytes), - }}); - - try exports.setNamedProperty("BeaconStateView", beacon_state_view_ctor); -} + try st.processSlots(allocator, post_state, slot_value, .{}); + return .{ .cached_state = post_state }; + } +}; diff --git a/bindings/napi/blst.zig b/bindings/napi/blst.zig index 08cc638df..d8acc3251 100644 --- a/bindings/napi/blst.zig +++ b/bindings/napi/blst.zig @@ -1,641 +1,357 @@ //! Contains the necessary bindings for blst operations in lodestar-ts. const std = @import("std"); -const napi = @import("zapi").napi; +const zapi = @import("zapi"); +const js = zapi.js; +const napi = zapi.napi; const bls = @import("bls"); const builtin = @import("builtin"); -const getter = @import("napi_property_descriptor.zig").getter; -const method = @import("napi_property_descriptor.zig").method; -const PublicKey = bls.PublicKey; -const Signature = bls.Signature; -const SecretKey = bls.SecretKey; +const NativePublicKey = bls.PublicKey; +const NativeSignature = bls.Signature; +const NativeSecretKey = bls.SecretKey; const Pairing = bls.Pairing; const AggregatePublicKey = bls.AggregatePublicKey; const AggregateSignature = bls.AggregateSignature; const DST = bls.DST; +const VerifySet = struct { + msg: js.Value, + pk: js.Value, + sig: js.Value, +}; + +const AsyncAggregateSet = struct { + pk: js.Value, + sig: js.Value, +}; + var gpa: std.heap.DebugAllocator(.{}) = .init; const allocator = if (builtin.mode == .Debug) gpa.allocator() else std.heap.c_allocator; -/// Per-context (per-thread) instance data for constructor references -const InstanceData = struct { - public_key_ctor_ref: ?napi.c.napi_ref = null, - signature_ctor_ref: ?napi.c.napi_ref = null, - - fn init(env: napi.Env) !*InstanceData { - const self = try allocator.create(InstanceData); - errdefer allocator.destroy(self); - - self.* = .{}; - try napi.status.check(napi.c.napi_set_instance_data( - env.env, - @ptrCast(self), - InstanceData.finalize, - null, - )); - return self; - } - - fn finalize(env: napi.c.napi_env, data: ?*anyopaque, _: ?*anyopaque) callconv(.C) void { - const self: *InstanceData = @ptrCast(@alignCast(data orelse return)); - self.clearRefs(env); - allocator.destroy(self); - } - - fn get(env: napi.Env) !*InstanceData { - var raw: ?*anyopaque = null; - try napi.status.check(napi.c.napi_get_instance_data(env.env, &raw)); - return @ptrCast(@alignCast(raw orelse return error.InstanceDataNotInitialized)); - } - - fn clearRefs(self: *InstanceData, env: napi.c.napi_env) void { - if (self.public_key_ctor_ref) |ref| { - napi.status.check(napi.c.napi_delete_reference(env, ref)) catch {}; - self.public_key_ctor_ref = null; - } - if (self.signature_ctor_ref) |ref| { - napi.status.check(napi.c.napi_delete_reference(env, ref)) catch {}; - self.signature_ctor_ref = null; - } - } -}; - -fn setRef(env: napi.Env, ctor: napi.Value, slot: *?napi.c.napi_ref) !void { - if (slot.*) |ref| { - try napi.status.check(napi.c.napi_delete_reference(env.env, ref)); - } - - var ref: napi.c.napi_ref = undefined; - try napi.status.check(napi.c.napi_create_reference(env.env, ctor.value, 1, &ref)); - slot.* = ref; +fn boolOrDefault(value: ?js.Boolean, default: bool) bool { + return if (value) |v| v.assertBool() else default; } -fn getFromRef(env: napi.Env, slot: ?napi.c.napi_ref) !napi.Value { - const ref_ = slot orelse return error.RefNotInitialized; - - var value: napi.c.napi_value = undefined; - try napi.status.check(napi.c.napi_get_reference_value(env.env, ref_, &value)); - return .{ - .env = env.env, - .value = value, - }; +fn hexFromString(hex_string: js.String, buf: []u8) ![]const u8 { + const full = try hex_string.toSlice(buf); + return if (full.len >= 2 and full[0] == '0' and full[1] == 'x') full[2..] else full; } -pub fn newPublicKeyInstance(env: napi.Env) !napi.Value { - const state = try InstanceData.get(env); - const ctor = try getFromRef(env, state.public_key_ctor_ref); - return try env.newInstance(ctor, .{}); +fn formatHex(bytes: []const u8, buf: []u8) !js.String { + const written = try std.fmt.bufPrint(buf, "0x{x}", .{std.fmt.fmtSliceHexLower(bytes)}); + return js.String.from(written); } -pub fn newSignatureInstance(env: napi.Env) !napi.Value { - const state = try InstanceData.get(env); - const ctor = try getFromRef(env, state.signature_ctor_ref); - return try env.newInstance(ctor, .{}); +fn unwrapClass(comptime T: type, value: js.Value) !*T { + return js.env().unwrap(T, value.toValue()); } -fn coerceToBool(boolish: napi.Value) napi.status.NapiError!bool { - const b = try boolish.coerceToBool(); - return b.getValueBool(); -} - -pub fn PublicKey_finalize(_: napi.Env, pk: *PublicKey, _: ?*anyopaque) void { - allocator.destroy(pk); -} - -pub fn PublicKey_ctor(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { - const pk = try allocator.create(PublicKey); - errdefer allocator.destroy(pk); - _ = try env.wrap(cb.this(), PublicKey, pk, PublicKey_finalize, null, null); - return cb.this(); -} - -/// Converts given array of bytes to a `PublicKey`. -/// 1) bytes: Uint8Array -/// 2) pk_validate: ?bool -pub fn PublicKey_fromBytes(env: napi.Env, cb: napi.CallbackInfo(2)) !napi.Value { - const ctor = cb.this(); - const bytes_info = try cb.arg(0).getTypedarrayInfo(); - const pk_validate: bool = if (cb.getArg(1)) |sgc| - try coerceToBool(sgc) - else - false; - - const pk_value = try env.newInstance(ctor, .{}); - const pk = try env.unwrap(PublicKey, pk_value); +pub const PublicKey = struct { + pub const js_meta = js.class(.{}); - pk.* = try PublicKey.deserialize(bytes_info.data[0..]); + raw: NativePublicKey = .{}, - if (pk_validate) { - try pk.validate(); + pub fn init() PublicKey { + return .{}; } - return pk_value; -} - -/// Converts given hex string to a `PublicKey`. -/// -/// 1) bytes: Uint8Array -/// 2) pk_validate: ?bool -pub fn PublicKey_fromHex(env: napi.Env, cb: napi.CallbackInfo(2)) !napi.Value { - const ctor = cb.this(); - var hex_buf: [PublicKey.SERIALIZE_SIZE * 2 + 2]u8 = undefined; - const hex = try hexFromValue(cb.arg(0), &hex_buf); - const pk_validate: bool = if (cb.getArg(1)) |sgc| - try coerceToBool(sgc) - else - false; - - const pk_value = try env.newInstance(ctor, .{}); - const pk = try env.unwrap(PublicKey, pk_value); - - var buf: [PublicKey.SERIALIZE_SIZE]u8 = undefined; - const bytes = try std.fmt.hexToBytes(&buf, hex); - pk.* = try PublicKey.deserialize(bytes); - - if (pk_validate) try pk.validate(); - - return pk_value; -} - -/// Converts given array of bytes to a `PublicKey`. -pub fn PublicKey_validate(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { - const pk = try env.unwrap(PublicKey, cb.this()); - try pk.validate(); - - return try env.getUndefined(); -} - -/// Serializes this public key to bytes. -pub fn PublicKey_toBytes(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { - const pk = try env.unwrap(PublicKey, cb.this()); - const compress = try if (cb.getArg(0)) |c| coerceToBool(c) else true; - - if (compress) { - const bytes = pk.compress(); - - var arraybuffer_bytes: [*]u8 = undefined; - const arraybuffer = try env.createArrayBuffer(PublicKey.COMPRESS_SIZE, &arraybuffer_bytes); - @memcpy(arraybuffer_bytes[0..PublicKey.COMPRESS_SIZE], &bytes); - return try env.createTypedarray(.uint8, PublicKey.COMPRESS_SIZE, arraybuffer, 0); - } else { - const bytes = pk.serialize(); - - var arraybuffer_bytes: [*]u8 = undefined; - const arraybuffer = try env.createArrayBuffer(PublicKey.SERIALIZE_SIZE, &arraybuffer_bytes); - @memcpy(arraybuffer_bytes[0..PublicKey.SERIALIZE_SIZE], &bytes); - return try env.createTypedarray(.uint8, PublicKey.SERIALIZE_SIZE, arraybuffer, 0); + pub fn fromBytes(bytes: js.Uint8Array, pk_validate: ?js.Boolean) !PublicKey { + const slice = try bytes.toSlice(); + var pk = try NativePublicKey.deserialize(slice); + if (boolOrDefault(pk_validate, false)) { + try pk.validate(); + } + return .{ .raw = pk }; } -} - -pub fn PublicKey_toHex(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { - const pk = try env.unwrap(PublicKey, cb.this()); - const compress = try if (cb.getArg(0)) |c| coerceToBool(c) else true; - if (compress) { - const bytes = pk.compress(); + pub fn fromHex(hex_string: js.String, pk_validate: ?js.Boolean) !PublicKey { + var hex_buf: [NativePublicKey.SERIALIZE_SIZE * 2 + 2]u8 = undefined; + const hex = try hexFromString(hex_string, &hex_buf); - const hex = try std.fmt.allocPrint(allocator, "0x{x}", .{std.fmt.fmtSliceHexLower(&bytes)}); - defer allocator.free(hex); + var bytes_buf: [NativePublicKey.SERIALIZE_SIZE]u8 = undefined; + const bytes = try std.fmt.hexToBytes(&bytes_buf, hex); - return try env.createStringUtf8(hex); - } else { - const bytes = pk.serialize(); - - const hex = try std.fmt.allocPrint(allocator, "0x{x}", .{std.fmt.fmtSliceHexLower(&bytes)}); - defer allocator.free(hex); - - return try env.createStringUtf8(hex); + var pk = try NativePublicKey.deserialize(bytes); + if (boolOrDefault(pk_validate, false)) { + try pk.validate(); + } + return .{ .raw = pk }; } -} - -pub fn Signature_finalize(_: napi.Env, sig: *Signature, _: ?*anyopaque) void { - allocator.destroy(sig); -} -pub fn Signature_ctor(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { - const sig = try allocator.create(Signature); - errdefer allocator.destroy(sig); - _ = try env.wrap(cb.this(), Signature, sig, Signature_finalize, null, null); - return cb.this(); -} - -/// Converts given array of bytes to a `Signature`. -pub fn Signature_fromBytes(env: napi.Env, cb: napi.CallbackInfo(3)) !napi.Value { - const ctor = cb.this(); - const bytes_info = try cb.arg(0).getTypedarrayInfo(); - const sig_validate: bool = if (cb.getArg(1)) |sgc| - try coerceToBool(sgc) - else - false; - const sig_infcheck: bool = if (cb.getArg(2)) |v| - try coerceToBool(v) - else - false; - - const sig_value = try env.newInstance(ctor, .{}); - const sig = try env.unwrap(Signature, sig_value); - - sig.* = Signature.deserialize(bytes_info.data[0..]) catch return error.DeserializationFailed; - - if (sig_validate) { - try sig.validate(sig_infcheck); + pub fn validate(self: *const PublicKey) !void { + try self.raw.validate(); } - return sig_value; -} - -/// Converts given hex string to a `Signature`. -/// -/// If `sig_validate` is `true`, the public key will be infinity and group checked. -/// If `sig_infcheck` is `false`, the infinity check will be skipped. -pub fn Signature_fromHex(env: napi.Env, cb: napi.CallbackInfo(3)) !napi.Value { - const ctor = cb.this(); - - var hex_buf: [Signature.SERIALIZE_SIZE * 2 + 2]u8 = undefined; - const hex = try hexFromValue(cb.arg(0), &hex_buf); - const sig_validate: bool = if (cb.getArg(1)) |sgc| - try coerceToBool(sgc) - else - false; - const sig_infcheck: bool = if (cb.getArg(2)) |v| - try coerceToBool(v) - else - false; - - const sig_value = try env.newInstance(ctor, .{}); - const sig = try env.unwrap(Signature, sig_value); - - var buf: [Signature.SERIALIZE_SIZE]u8 = undefined; - const bytes = try std.fmt.hexToBytes(&buf, hex); - sig.* = Signature.deserialize(bytes) catch return error.DeserializationFailed; - - if (sig_validate) try sig.validate(sig_infcheck); - - return sig_value; -} + pub fn toBytes(self: *const PublicKey, compress: ?js.Boolean) js.Uint8Array { + if (boolOrDefault(compress, true)) { + const bytes = self.raw.compress(); + return js.Uint8Array.from(bytes[0..]); + } -/// Serializes this signature to bytes. -pub fn Signature_toBytes(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { - const sig = try env.unwrap(Signature, cb.this()); - const compress = try if (cb.getArg(0)) |c| coerceToBool(c) else true; - - if (compress) { - const bytes = sig.compress(); - - var arraybuffer_bytes: [*]u8 = undefined; - const arraybuffer = try env.createArrayBuffer(Signature.COMPRESS_SIZE, &arraybuffer_bytes); - @memcpy(arraybuffer_bytes[0..Signature.COMPRESS_SIZE], &bytes); - return try env.createTypedarray(.uint8, Signature.COMPRESS_SIZE, arraybuffer, 0); - } else { - const bytes = sig.serialize(); - - var arraybuffer_bytes: [*]u8 = undefined; - const arraybuffer = try env.createArrayBuffer(Signature.SERIALIZE_SIZE, &arraybuffer_bytes); - @memcpy(arraybuffer_bytes[0..Signature.SERIALIZE_SIZE], &bytes); - return try env.createTypedarray(.uint8, Signature.SERIALIZE_SIZE, arraybuffer, 0); + const bytes = self.raw.serialize(); + return js.Uint8Array.from(bytes[0..]); } -} - -pub fn Signature_toHex(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { - const sig = try env.unwrap(Signature, cb.this()); - const compress = try if (cb.getArg(0)) |c| coerceToBool(c) else true; - - if (compress) { - const bytes = sig.compress(); - const hex = try std.fmt.allocPrint(allocator, "0x{x}", .{std.fmt.fmtSliceHexLower(&bytes)}); - defer allocator.free(hex); - - return try env.createStringUtf8(hex); - } else { - const bytes = sig.serialize(); - - const hex = try std.fmt.allocPrint(allocator, "0x{x}", .{std.fmt.fmtSliceHexLower(&bytes)}); - defer allocator.free(hex); + pub fn toHex(self: *const PublicKey, compress: ?js.Boolean) !js.String { + var hex_buf: [NativePublicKey.SERIALIZE_SIZE * 2 + 2]u8 = undefined; + if (boolOrDefault(compress, true)) { + const bytes = self.raw.compress(); + return formatHex(bytes[0..], &hex_buf); + } - return try env.createStringUtf8(hex); + const bytes = self.raw.serialize(); + return formatHex(bytes[0..], &hex_buf); } -} +}; -pub fn SecretKey_finalize(_: napi.Env, sk: *SecretKey, _: ?*anyopaque) void { - allocator.destroy(sk); -} +pub const Signature = struct { + pub const js_meta = js.class(.{}); -pub fn SecretKey_ctor(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { - const sk = try allocator.create(SecretKey); - errdefer allocator.destroy(sk); - _ = try env.wrap(cb.this(), SecretKey, sk, SecretKey_finalize, null, null); - return cb.this(); -} + raw: NativeSignature = .{}, -/// Creates a `SecretKey` from raw bytes. -pub fn SecretKey_fromBytes(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { - const ctor = cb.this(); - const bytes_info = try cb.arg(0).getTypedarrayInfo(); - - if (bytes_info.data.len != SecretKey.serialize_size) { - return error.InvalidSecretKeyLength; + pub fn init() Signature { + return .{}; } - const sk_value = try env.newInstance(ctor, .{}); - const sk = try env.unwrap(SecretKey, sk_value); - sk.* = SecretKey.deserialize(bytes_info.data[0..SecretKey.serialize_size]) catch return error.DeserializationFailed; + pub fn fromBytes(bytes: js.Uint8Array, sig_validate: ?js.Boolean, sig_infcheck: ?js.Boolean) !Signature { + const slice = try bytes.toSlice(); + var sig = NativeSignature.deserialize(slice) catch return error.DeserializationFailed; - return sk_value; -} + if (boolOrDefault(sig_validate, false)) { + try sig.validate(boolOrDefault(sig_infcheck, false)); + } -/// Creates a `SecretKey` from a hex string. -pub fn SecretKey_fromHex(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { - const ctor = cb.this(); + return .{ .raw = sig }; + } - var hex_buf: [SecretKey.serialize_size * 2 + 3]u8 = undefined; - const hex = try hexFromValue(cb.arg(0), &hex_buf); - const sk_value = try env.newInstance(ctor, .{}); - const sk = try env.unwrap(SecretKey, sk_value); + pub fn fromHex(hex_string: js.String, sig_validate: ?js.Boolean, sig_infcheck: ?js.Boolean) !Signature { + var hex_buf: [NativeSignature.SERIALIZE_SIZE * 2 + 2]u8 = undefined; + const hex = try hexFromString(hex_string, &hex_buf); - var buf: [SecretKey.serialize_size]u8 = undefined; - const bytes = try std.fmt.hexToBytes(&buf, hex); - sk.* = SecretKey.deserialize(bytes[0..SecretKey.serialize_size]) catch return error.DeserializationFailed; + var bytes_buf: [NativeSignature.SERIALIZE_SIZE]u8 = undefined; + const bytes = try std.fmt.hexToBytes(&bytes_buf, hex); - return sk_value; -} + var sig = NativeSignature.deserialize(bytes) catch return error.DeserializationFailed; + if (boolOrDefault(sig_validate, false)) { + try sig.validate(boolOrDefault(sig_infcheck, false)); + } -pub fn SecretKey_toHex(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { - const sk = try env.unwrap(SecretKey, cb.this()); - const bytes = sk.serialize(); + return .{ .raw = sig }; + } - const hex = try std.fmt.allocPrint(allocator, "0x{x}", .{std.fmt.fmtSliceHexLower(&bytes)}); - defer allocator.free(hex); + pub fn aggregate(signatures: js.Array, sigs_groupcheck: ?js.Boolean) !Signature { + const signatures_len = try signatures.length(); + if (signatures_len == 0) return error.EmptySignatureArray; - return try env.createStringUtf8(hex); -} + const sigs = try allocator.alloc(NativeSignature, signatures_len); + defer allocator.free(sigs); -/// Generates a `SecretKey` from a seed (IKM) using key derivation. -/// -/// Seed must be at least 32 bytes. -pub fn SecretKey_fromKeygen(env: napi.Env, cb: napi.CallbackInfo(2)) !napi.Value { - const ctor = cb.this(); - const bytes_info = try cb.arg(0).getTypedarrayInfo(); + for (0..signatures_len) |i| { + const wrapped = try unwrapClass(Signature, try signatures.get(@intCast(i))); + sigs[i] = wrapped.raw; + } - const key_info_data: ?[]const u8 = if (cb.getArg(1)) |ki| blk: { - const typeof = try ki.typeof(); - if (typeof == .undefined or typeof == .null) break :blk null; - const info = try ki.getTypedarrayInfo(); - if (info.array_type != .uint8) return error.InvalidArgument; - break :blk info.data; - } else null; + const agg_sig = AggregateSignature.aggregate(sigs, boolOrDefault(sigs_groupcheck, false)) catch { + return error.AggregationFailed; + }; + return .{ .raw = agg_sig.toSignature() }; + } - if (bytes_info.data.len < 32) return error.InvalidSeedLength; + pub fn toBytes(self: *const Signature, compress: ?js.Boolean) js.Uint8Array { + if (boolOrDefault(compress, true)) { + const bytes = self.raw.compress(); + return js.Uint8Array.from(bytes[0..]); + } - const sk_value = try env.newInstance(ctor, .{}); - const sk = try env.unwrap(SecretKey, sk_value); - sk.* = SecretKey.keyGen(bytes_info.data, key_info_data) catch return error.KeyGenFailed; + const bytes = self.raw.serialize(); + return js.Uint8Array.from(bytes[0..]); + } - return sk_value; -} + pub fn toHex(self: *const Signature, compress: ?js.Boolean) !js.String { + var hex_buf: [NativeSignature.SERIALIZE_SIZE * 2 + 2]u8 = undefined; + if (boolOrDefault(compress, true)) { + const bytes = self.raw.compress(); + return formatHex(bytes[0..], &hex_buf); + } -/// Signs a message with this `SecretKey`, returns a `Signature`. -pub fn SecretKey_sign(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { - const sk = try env.unwrap(SecretKey, cb.this()); - const msg = try cb.arg(0).getTypedarrayInfo(); + const bytes = self.raw.serialize(); + return formatHex(bytes[0..], &hex_buf); + } - const sig_value = try newSignatureInstance(env); - const sig = try env.unwrap(Signature, sig_value); - sig.* = sk.sign(msg.data, DST, null); + pub fn validate(self: *const Signature, sig_infcheck: js.Boolean) !void { + self.raw.validate(sig_infcheck.assertBool()) catch return error.InvalidSignature; + } +}; - return sig_value; -} +pub const SecretKey = struct { + pub const js_meta = js.class(.{}); -/// Derives the PublicKey from this SecretKey. -pub fn SecretKey_toPublicKey(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { - const sk = try env.unwrap(SecretKey, cb.this()); + raw: NativeSecretKey = .{}, - const pk_value = try newPublicKeyInstance(env); - const pk = try env.unwrap(PublicKey, pk_value); - pk.* = sk.toPublicKey(); + pub fn init() SecretKey { + return .{}; + } - return pk_value; -} + pub fn fromBytes(bytes: js.Uint8Array) !SecretKey { + const slice = try bytes.toSlice(); + if (slice.len != NativeSecretKey.serialize_size) { + return error.InvalidSecretKeyLength; + } -/// Serializes the SecretKey to bytes (32 bytes). -pub fn SecretKey_toBytes(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { - const sk = try env.unwrap(SecretKey, cb.this()); - const bytes = sk.serialize(); + const sk = NativeSecretKey.deserialize(slice[0..NativeSecretKey.serialize_size]) catch { + return error.DeserializationFailed; + }; + return .{ .raw = sk }; + } - var arraybuffer_bytes: [*]u8 = undefined; - const arraybuffer = try env.createArrayBuffer(SecretKey.serialize_size, &arraybuffer_bytes); - @memcpy(arraybuffer_bytes[0..SecretKey.serialize_size], &bytes); - return try env.createTypedarray(.uint8, SecretKey.serialize_size, arraybuffer, 0); -} + pub fn fromHex(hex_string: js.String) !SecretKey { + var hex_buf: [NativeSecretKey.serialize_size * 2 + 3]u8 = undefined; + const hex = try hexFromString(hex_string, &hex_buf); -/// Aggregates multiple Signature objects into one. -/// -/// 1) sigs_array: []Signature -/// 2) sigs_groupcheck: bool -pub fn Signature_aggregate(env: napi.Env, cb: napi.CallbackInfo(2)) !napi.Value { - const ctor = cb.this(); - const sigs_array = cb.arg(0); - const sigs_groupcheck = try coerceToBool(cb.arg(1)); + var bytes_buf: [NativeSecretKey.serialize_size]u8 = undefined; + const bytes = try std.fmt.hexToBytes(&bytes_buf, hex); + const sk = NativeSecretKey.deserialize(bytes[0..NativeSecretKey.serialize_size]) catch { + return error.DeserializationFailed; + }; + return .{ .raw = sk }; + } - const sigs_len = try sigs_array.getArrayLength(); - if (sigs_len == 0) return error.EmptySignatureArray; + pub fn fromKeygen(seed: js.Uint8Array, key_info: ?js.Value) !SecretKey { + const seed_slice = try seed.toSlice(); + if (seed_slice.len < 32) return error.InvalidSeedLength; - const sigs = try allocator.alloc(Signature, sigs_len); - defer allocator.free(sigs); + const key_info_slice: ?[]const u8 = if (key_info) |value| blk: { + if (value.isUndefined() or value.isNull()) break :blk null; + const typed = try value.asUint8Array(); + break :blk try typed.toSlice(); + } else null; - for (0..sigs_len) |i| { - const sig_value = try sigs_array.getElement(@intCast(i)); - const sig = try env.unwrap(Signature, sig_value); - sigs[i] = sig.*; + const sk = NativeSecretKey.keyGen(seed_slice, key_info_slice) catch return error.KeyGenFailed; + return .{ .raw = sk }; } - const agg_sig = AggregateSignature.aggregate(sigs, sigs_groupcheck) catch return error.AggregationFailed; - - const sig_value = try env.newInstance(ctor, .{}); - const sig = try env.unwrap(Signature, sig_value); - sig.* = agg_sig.toSignature(); - - return sig_value; -} + pub fn sign(self: *const SecretKey, msg: js.Uint8Array) !Signature { + const slice = try msg.toSlice(); + return .{ .raw = self.raw.sign(slice, DST, null) }; + } -/// Validates the signature. -/// Throws an error if the signature is invalid. -pub fn Signature_validate(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { - const sig = try env.unwrap(Signature, cb.this()); - const sig_infcheck = try coerceToBool(cb.arg(0)); + pub fn toPublicKey(self: *const SecretKey) !PublicKey { + return .{ .raw = self.raw.toPublicKey() }; + } - sig.validate(sig_infcheck) catch return error.InvalidSignature; + pub fn toBytes(self: *const SecretKey) js.Uint8Array { + const bytes = self.raw.serialize(); + return js.Uint8Array.from(bytes[0..]); + } - return try env.getUndefined(); -} + pub fn toHex(self: *const SecretKey) !js.String { + const bytes = self.raw.serialize(); + var hex_buf: [NativeSecretKey.serialize_size * 2 + 2]u8 = undefined; + return formatHex(bytes[0..], &hex_buf); + } +}; /// Verifies a given `msg` against a `Signature` and a `PublicKey`. -/// -/// Returns `true` if signature is valid, `false` otherwise. -/// -/// Arguments: -/// 1) msg: Uint8Array -/// 2) pk: PublicKey -/// 3) sig: Signature -/// 4) pk_validate: ?bool -/// 5) sig_groupcheck: ?bool -pub fn blst_verify(env: napi.Env, cb: napi.CallbackInfo(5)) !napi.Value { - const msg_info = try cb.arg(0).getTypedarrayInfo(); - const pk = try env.unwrap(PublicKey, cb.arg(1)); - const sig = try env.unwrap(Signature, cb.arg(2)); - const pk_validate: bool = if (cb.getArg(3)) |sgc| - try coerceToBool(sgc) - else - false; - const sig_groupcheck: bool = if (cb.getArg(4)) |v| - try coerceToBool(v) - else - false; - - sig.verify(sig_groupcheck, msg_info.data, DST, null, pk, pk_validate) catch { - return try env.getBoolean(false); - }; +pub fn verify(msg: js.Uint8Array, pk: PublicKey, sig: Signature, pk_validate: ?js.Boolean, sig_groupcheck: ?js.Boolean) !js.Boolean { + const msg_slice = try msg.toSlice(); - return try env.getBoolean(true); + sig.raw.verify( + boolOrDefault(sig_groupcheck, false), + msg_slice, + DST, + null, + &pk.raw, + boolOrDefault(pk_validate, false), + ) catch return js.Boolean.from(false); + + return js.Boolean.from(true); } -/// Verify an aggregated signature against multiple messages and multiple public keys. -/// 1) msgs: Uint8Array[] -/// 2) pks: PublicKey[] -/// 3) sig: Signature -/// 4) pks_validate: ?bool -/// 5) sig_groupcheck: ?bool -pub fn blst_aggregateVerify( - env: napi.Env, - cb: napi.CallbackInfo(5), -) !napi.Value { - const msgs_array = cb.arg(0); - const pks_array = cb.arg(1); - const sig = try env.unwrap(Signature, cb.arg(2)); - const pks_validate: bool = if (cb.getArg(3)) |sgc| - try coerceToBool(sgc) - else - false; - const sig_groupcheck: bool = if (cb.getArg(4)) |v| - try coerceToBool(v) - else - false; - - const msgs_len = try msgs_array.getArrayLength(); - const pks_len = try pks_array.getArrayLength(); +/// Verifies an aggregated signature against multiple messages and public keys. +pub fn aggregateVerify(msgs: js.Array, pks: js.Array, sig: Signature, pks_validate: ?js.Boolean, sig_groupcheck: ?js.Boolean) !js.Boolean { + const msgs_len = try msgs.length(); + const pks_len = try pks.length(); if (msgs_len == 0 or pks_len == 0 or msgs_len != pks_len) { return error.InvalidAggregateVerifyInput; } - const msgs = try allocator.alloc([32]u8, msgs_len); - defer allocator.free(msgs); - const pks = try allocator.alloc(PublicKey, pks_len); - defer allocator.free(pks); + const msg_bufs = try allocator.alloc([32]u8, msgs_len); + defer allocator.free(msg_bufs); + + const native_pks = try allocator.alloc(NativePublicKey, pks_len); + defer allocator.free(native_pks); for (0..msgs_len) |i| { - const msg_value = try msgs_array.getElement(@intCast(i)); - const msg_info = try msg_value.getTypedarrayInfo(); - if (msg_info.data.len != 32) return error.InvalidMessageLength; - @memcpy(&msgs[i], msg_info.data[0..32]); - - const pk_value = try pks_array.getElement(@intCast(i)); - const pk = try env.unwrap(PublicKey, pk_value); - pks[i] = pk.*; + const msg_value = try msgs.get(@intCast(i)); + const msg_bytes = try (try msg_value.asUint8Array()).toSlice(); + if (msg_bytes.len != 32) return error.InvalidMessageLength; + @memcpy(&msg_bufs[i], msg_bytes[0..32]); + + const wrapped_pk = try unwrapClass(PublicKey, try pks.get(@intCast(i))); + native_pks[i] = wrapped_pk.raw; } var pairing_buf: [Pairing.sizeOf()]u8 align(Pairing.buf_align) = undefined; - - const result = sig.aggregateVerify( - sig_groupcheck, + const result = sig.raw.aggregateVerify( + boolOrDefault(sig_groupcheck, false), &pairing_buf, - msgs, + msg_bufs, DST, - pks, - pks_validate, - ) catch { - return try env.getBoolean(false); - }; + native_pks, + boolOrDefault(pks_validate, false), + ) catch return js.Boolean.from(false); - return try env.getBoolean(result); + return js.Boolean.from(result); } -/// Aggregate and verify an array of `PublicKey`s. Returns `false` if pks array is empty or if signature is invalid. -/// -/// `msg` (signing root) must be exactly 32 bytes. -/// -/// Arguments: -/// 1) msg: Uint8Array -/// 2) pks: PublicKey[] -/// 3) sig: Signature -/// 4) sigs_groupcheck: ?bool -pub fn blst_fastAggregateVerify(env: napi.Env, cb: napi.CallbackInfo(4)) !napi.Value { - const msg_info = try cb.arg(0).getTypedarrayInfo(); - if (msg_info.data.len != 32) return error.InvalidMessageLength; - - const pks_array = cb.arg(1); - const sig = try env.unwrap(Signature, cb.arg(2)); - const sigs_groupcheck = if (cb.getArg(3)) |sgc| - try coerceToBool(sgc) - else - false; - - const pks_len = try pks_array.getArrayLength(); +/// Aggregates and verifies multiple public keys against one 32-byte message. +pub fn fastAggregateVerify(msg: js.Uint8Array, pks: js.Array, sig: Signature, sigs_groupcheck: ?js.Boolean) !js.Boolean { + const msg_slice = try msg.toSlice(); + if (msg_slice.len != 32) return error.InvalidMessageLength; + + const pks_len = try pks.length(); if (pks_len == 0) { - return try env.getBoolean(false); + return js.Boolean.from(false); } - const pks = try allocator.alloc(PublicKey, pks_len); - defer allocator.free(pks); + const native_pks = try allocator.alloc(NativePublicKey, pks_len); + defer allocator.free(native_pks); for (0..pks_len) |i| { - const pk_value = try pks_array.getElement(@intCast(i)); - const pk = try env.unwrap(PublicKey, pk_value); - pks[i] = pk.*; + const wrapped_pk = try unwrapClass(PublicKey, try pks.get(@intCast(i))); + native_pks[i] = wrapped_pk.raw; } var pairing_buf: [Pairing.sizeOf()]u8 align(Pairing.buf_align) = undefined; - // `pks_validate` is always false here since we assume proof of possession for public keys. - const result = sig.fastAggregateVerify(sigs_groupcheck, &pairing_buf, msg_info.data[0..32], DST, pks, false) catch { - return try env.getBoolean(false); - }; + const result = sig.raw.fastAggregateVerify( + boolOrDefault(sigs_groupcheck, false), + &pairing_buf, + msg_slice[0..32], + DST, + native_pks, + false, + ) catch return js.Boolean.from(false); - return try env.getBoolean(result); + return js.Boolean.from(result); } -/// Batch verify multiple signature sets. -/// Returns `false` if verification fails. -/// -/// Arguments: -/// 1) sets: Array of { msg: Uint8Array, pk: PublicKey, sig: Signature } -/// 2) pks_validate: ?bool -/// 3) sigs_groupcheck: ?bool -pub fn blst_verifyMultipleAggregateSignatures(env: napi.Env, cb: napi.CallbackInfo(3)) !napi.Value { - const sets = cb.arg(0); - const n_elems = try sets.getArrayLength(); - - const pks_validate: bool = if (cb.getArg(1)) |v| - try coerceToBool(v) - else - false; - const sigs_groupcheck: bool = if (cb.getArg(2)) |sgc| - try coerceToBool(sgc) - else - false; - +/// Batch verify multiple aggregate signature sets. +pub fn verifyMultipleAggregateSignatures(sets: js.Array, pks_validate: ?js.Boolean, sigs_groupcheck: ?js.Boolean) !js.Boolean { + const n_elems = try sets.length(); if (n_elems == 0) { - return try env.getBoolean(false); + return js.Boolean.from(false); } const msgs = try allocator.alloc([32]u8, n_elems); defer allocator.free(msgs); - const pks = try allocator.alloc(*PublicKey, n_elems); + const pks = try allocator.alloc(*NativePublicKey, n_elems); defer allocator.free(pks); - const sigs = try allocator.alloc(*Signature, n_elems); + const sigs = try allocator.alloc(*NativeSignature, n_elems); defer allocator.free(sigs); const rands = try allocator.alloc([32]u8, n_elems); @@ -645,19 +361,18 @@ pub fn blst_verifyMultipleAggregateSignatures(env: napi.Env, cb: napi.CallbackIn const rand = prng.random(); for (0..n_elems) |i| { - const set_value = try sets.getElement(@intCast(i)); + const set_obj = try (try sets.get(@intCast(i))).asObject(VerifySet); + const set = try set_obj.get(); - const msg_value = try set_value.getNamedProperty("msg"); - const msg = try msg_value.getTypedarrayInfo(); - if (msg.data.len != 32) return error.InvalidMessageLength; - @memcpy(&msgs[i], msg.data[0..32]); + const msg_bytes = try (try set.msg.asUint8Array()).toSlice(); + if (msg_bytes.len != 32) return error.InvalidMessageLength; + @memcpy(&msgs[i], msg_bytes[0..32]); - // Use unwrapped pointers directly - no copy needed - const pk_value = try set_value.getNamedProperty("pk"); - pks[i] = try env.unwrap(PublicKey, pk_value); + const wrapped_pk = try unwrapClass(PublicKey, set.pk); + pks[i] = &wrapped_pk.raw; - const sig_value = try set_value.getNamedProperty("sig"); - sigs[i] = try env.unwrap(Signature, sig_value); + const wrapped_sig = try unwrapClass(Signature, set.sig); + sigs[i] = &wrapped_sig.raw; rand.bytes(&rands[i]); } @@ -669,147 +384,135 @@ pub fn blst_verifyMultipleAggregateSignatures(env: napi.Env, cb: napi.CallbackIn msgs, DST, pks, - pks_validate, + boolOrDefault(pks_validate, false), sigs, - sigs_groupcheck, + boolOrDefault(sigs_groupcheck, false), rands, - ) catch { - return try env.getBoolean(false); - }; + ) catch return js.Boolean.from(false); - return try env.getBoolean(result); + return js.Boolean.from(result); } -/// Aggregate multiple Signature objects into one. -/// Validates each signature if `sigs_groupcheck` is true. -/// -/// Arguments: -/// 1) signatures: Signature[] -/// 2) sigs_groupcheck: ?bool -pub fn blst_aggregateSignatures(env: napi.Env, cb: napi.CallbackInfo(2)) !napi.Value { - const sigs_array = cb.arg(0); - - const sigs_groupcheck: bool = if (cb.getArg(1)) |sgc| - try coerceToBool(sgc) - else - false; - - const sigs_len = try sigs_array.getArrayLength(); +/// Aggregates multiple `Signature` objects into one. +pub fn aggregateSignatures(signatures: js.Array, sigs_groupcheck: ?js.Boolean) !Signature { + const signatures_len = try signatures.length(); + if (signatures_len == 0) return error.EmptySignatureArray; - if (sigs_len == 0) return error.EmptySignatureArray; - - const sigs = try allocator.alloc(Signature, sigs_len); + const sigs = try allocator.alloc(NativeSignature, signatures_len); defer allocator.free(sigs); - for (0..sigs_len) |i| { - const sig_value = try sigs_array.getElement(@intCast(i)); - const sig = try env.unwrap(Signature, sig_value); - sigs[i] = sig.*; + for (0..signatures_len) |i| { + const wrapped = try unwrapClass(Signature, try signatures.get(@intCast(i))); + sigs[i] = wrapped.raw; } - const agg_sig = AggregateSignature.aggregate(sigs, sigs_groupcheck) catch return error.AggregationFailed; - const result_sig = agg_sig.toSignature(); - - const sig_value = try newSignatureInstance(env); - const sig = try env.unwrap(Signature, sig_value); - sig.* = result_sig; + const agg_sig = AggregateSignature.aggregate(sigs, boolOrDefault(sigs_groupcheck, false)) catch { + return error.AggregationFailed; + }; - return sig_value; + return .{ .raw = agg_sig.toSignature() }; } -/// Aggregate multiple `PublicKey` objects into one. -/// -/// Arguments: -/// 1) pks: PublicKey[] -/// 2) pks_validate: ?bool -pub fn blst_aggregatePublicKeys(env: napi.Env, cb: napi.CallbackInfo(2)) !napi.Value { - const pks_array = cb.arg(0); - const pks_len = try pks_array.getArrayLength(); +/// Aggregates multiple `PublicKey` objects into one. +pub fn aggregatePublicKeys(pks: js.Array, pks_validate: ?js.Boolean) !PublicKey { + const pks_len = try pks.length(); + if (pks_len == 0) return error.EmptyPublicKeyArray; - const pks_validate: bool = if (cb.getArg(1)) |v| - try coerceToBool(v) - else - false; + const native_pks = try allocator.alloc(NativePublicKey, pks_len); + defer allocator.free(native_pks); - if (pks_len == 0) { - return error.EmptyPublicKeyArray; + for (0..pks_len) |i| { + const wrapped = try unwrapClass(PublicKey, try pks.get(@intCast(i))); + native_pks[i] = wrapped.raw; } - const pks = try allocator.alloc(PublicKey, pks_len); - defer allocator.free(pks); + const agg_pk = AggregatePublicKey.aggregate(native_pks, boolOrDefault(pks_validate, false)) catch { + return error.AggregationFailed; + }; + + return .{ .raw = agg_pk.toPublicKey() }; +} + +/// Aggregates public keys from serialized bytes. +pub fn aggregateSerializedPublicKeys(serialized_public_keys: js.Array, pks_validate: ?js.Boolean) !PublicKey { + const pks_len = try serialized_public_keys.length(); + if (pks_len == 0) return error.EmptyPublicKeyArray; + + const native_pks = try allocator.alloc(NativePublicKey, pks_len); + defer allocator.free(native_pks); for (0..pks_len) |i| { - const pk_value = try pks_array.getElement(@intCast(i)); - const pk = try env.unwrap(PublicKey, pk_value); - pks[i] = pk.*; + const bytes = try (try (try serialized_public_keys.get(@intCast(i))).asUint8Array()).toSlice(); + native_pks[i] = NativePublicKey.deserialize(bytes) catch return error.DeserializationFailed; } - const agg_pk = AggregatePublicKey.aggregate(pks, pks_validate) catch return error.AggregationFailed; - const result_pk = agg_pk.toPublicKey(); - - const pk_value = try newPublicKeyInstance(env); - const pk = try env.unwrap(PublicKey, pk_value); - pk.* = result_pk; + const agg_pk = AggregatePublicKey.aggregate(native_pks, boolOrDefault(pks_validate, false)) catch { + return error.AggregationFailed; + }; - return pk_value; + return .{ .raw = agg_pk.toPublicKey() }; } -/// Aggregate public keys from serialized bytes. -/// -/// Arguments: -/// 1) serializedPublicKeys: Uint8Array[] - array of serialized (96-bytes each) `PublicKey`s. -pub fn blst_aggregateSerializedPublicKeys(env: napi.Env, cb: napi.CallbackInfo(2)) !napi.Value { - const pks_array = cb.arg(0); - const pks_len = try pks_array.getArrayLength(); - const pks_validate: bool = if (cb.getArg(1)) |v| - try coerceToBool(v) - else - false; +/// Asynchronously aggregates public keys and signatures with randomness. +pub fn asyncAggregateWithRandomness(sets: js.Array) !js.Promise(js.Value) { + const n = try sets.length(); + if (n == 0) return error.EmptyArray; + if (n > MAX_AGGREGATE_PER_JOB) return error.TooManySets; - if (pks_len == 0) return error.EmptyPublicKeyArray; + const pks = try allocator.alloc(NativePublicKey, n); + errdefer allocator.free(pks); - const pks = try allocator.alloc(PublicKey, pks_len); - defer allocator.free(pks); + const sigs = try allocator.alloc(NativeSignature, n); + errdefer allocator.free(sigs); - for (0..pks_len) |i| { - const pk_bytes_value = try pks_array.getElement(@intCast(i)); - const bytes_info = try pk_bytes_value.getTypedarrayInfo(); + for (0..n) |i| { + const set_obj = try (try sets.get(@intCast(i))).asObject(AsyncAggregateSet); + const set = try set_obj.get(); - pks[i] = PublicKey.deserialize(bytes_info.data) catch - return error.DeserializationFailed; + const wrapped_pk = try unwrapClass(PublicKey, set.pk); + pks[i] = wrapped_pk.raw; + + const sig_bytes = try (try set.sig.asUint8Array()).toSlice(); + sigs[i] = NativeSignature.deserialize(sig_bytes[0..]) catch return error.DeserializationFailed; + sigs[i].validate(true) catch return error.InvalidSignature; } - const agg_pk = AggregatePublicKey.aggregate(pks, pks_validate) catch return error.AggregationFailed; - const result_pk = agg_pk.toPublicKey(); + const promise = try js.createPromise(js.Value); + const data = try allocator.create(AsyncAggregateData); + errdefer allocator.destroy(data); - const pk_value = try newPublicKeyInstance(env); - const pk = try env.unwrap(PublicKey, pk_value); - pk.* = result_pk; + data.* = .{ + .pks = pks, + .sigs = sigs, + .n = n, + .deferred = promise.deferred, + }; - return pk_value; -} + const resource_name = try js.env().createStringUtf8("asyncAggregateWithRandomness"); + data.work = try napi.AsyncWork(AsyncAggregateData).create( + js.env(), + null, + resource_name, + asyncAggregateExecute, + asyncAggregateComplete, + data, + ); + try data.work.queue(); -/// Unpacks a hex string from a `napi.Value`. Returns the slice representing the hex string. -fn hexFromValue(value: napi.Value, buf: []u8) ![]const u8 { - const hex_str = try value.getValueStringUtf8(buf); - const hex = if (hex_str.len >= 2 and hex_str[0] == '0' and hex_str[1] == 'x') hex_str[2..] else hex_str; - return hex; + return promise; } const MAX_AGGREGATE_PER_JOB = bls.MAX_AGGREGATE_PER_JOB; const AsyncAggregateData = struct { - // Inputs (copied on main thread, freed in complete) - pks: []PublicKey, - sigs: []Signature, + pks: []NativePublicKey, + sigs: []NativeSignature, n: usize, - // Outputs (set in execute) - result_pk: PublicKey = .{}, - result_sig: Signature = .{}, + result_pk: NativePublicKey = .{}, + result_sig: NativeSignature = .{}, err: bool = false, - // NAPI handles deferred: napi.Deferred, work: napi.AsyncWork(AsyncAggregateData) = undefined, }; @@ -817,19 +520,16 @@ const AsyncAggregateData = struct { fn asyncAggregateExecute(_: napi.Env, data: *AsyncAggregateData) void { const n = data.n; - // Generate 32 bytes of randomness per element, 64 meaningful bits (nbits=64) var rands: [32 * MAX_AGGREGATE_PER_JOB]u8 = undefined; std.crypto.random.bytes(rands[0 .. n * 32]); - // Build pointer arrays (stack-allocated, MAX_AGGREGATE_PER_JOB is 128) - var pk_refs: [MAX_AGGREGATE_PER_JOB]*const PublicKey = undefined; - var sig_refs: [MAX_AGGREGATE_PER_JOB]*const Signature = undefined; + var pk_refs: [MAX_AGGREGATE_PER_JOB]*const NativePublicKey = undefined; + var sig_refs: [MAX_AGGREGATE_PER_JOB]*const NativeSignature = undefined; for (0..n) |i| { pk_refs[i] = &data.pks[i]; sig_refs[i] = &data.sigs[i]; } - // Per-call scratch allocation (safe for worker threads) const p1_scratch_size = bls.c.blst_p1s_mult_pippenger_scratch_sizeof(n); const p2_scratch_size = bls.c.blst_p2s_mult_pippenger_scratch_sizeof(n); const scratch_size = @max(p1_scratch_size, p2_scratch_size); @@ -839,22 +539,20 @@ fn asyncAggregateExecute(_: napi.Env, data: *AsyncAggregateData) void { }; defer allocator.free(scratch); - // Pippenger multi-scalar multiplication on G1 (pubkeys) const agg_pk = AggregatePublicKey.aggregateWithRandomness( pk_refs[0..n], rands[0 .. n * 32], - false, // already validated + false, scratch, ) catch { data.err = true; return; }; - // Pippenger multi-scalar multiplication on G2 (signatures) const agg_sig = AggregateSignature.aggregateWithRandomness( sig_refs[0..n], rands[0 .. n * 32], - false, // already validated during deserialization + false, scratch, ) catch { data.err = true; @@ -879,154 +577,12 @@ fn asyncAggregateComplete(env: napi.Env, _: napi.status.Status, data: *AsyncAggr return; } - // Wrap results as NAPI PublicKey/Signature instances - const pk_value = newPublicKeyInstance(env) catch return; - const pk = env.unwrap(PublicKey, pk_value) catch return; - pk.* = data.result_pk; - - const sig_value = newSignatureInstance(env) catch return; - const sig = env.unwrap(Signature, sig_value) catch return; - sig.* = data.result_sig; + const pk_value = napi.Value{ .env = env.env, .value = js.convertReturn(PublicKey, .{ .raw = data.result_pk }, env.env) }; + const sig_value = napi.Value{ .env = env.env, .value = js.convertReturn(Signature, .{ .raw = data.result_sig }, env.env) }; - // Create {pk, sig} JS object and resolve promise const result = env.createObject() catch return; result.setNamedProperty("pk", pk_value) catch return; result.setNamedProperty("sig", sig_value) catch return; data.deferred.resolve(result) catch return; } - -/// Asynchronously aggregates public keys and signatures with randomness using -/// Pippenger multi-scalar multiplication. Heavy math runs on the libuv thread pool. -/// -/// Arguments: -/// 1) sets: Array of {pk: PublicKey, sig: Uint8Array} -/// -/// Returns: Promise<{pk: PublicKey, sig: Signature}> -pub fn blst_asyncAggregateWithRandomness(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { - const sets = cb.arg(0); - const n = try sets.getArrayLength(); - - if (n == 0) return error.EmptyArray; - // Max set size enforced at MAX_AGGREGATE_PER_JOB (128) to match blst-z internal limits - if (n > MAX_AGGREGATE_PER_JOB) return error.TooManySets; - - const pks = try allocator.alloc(PublicKey, n); - errdefer allocator.free(pks); - - const sigs = try allocator.alloc(Signature, n); - errdefer allocator.free(sigs); - - for (0..n) |i| { - const set_value = try sets.getElement(@intCast(i)); - - // Unwrap PublicKey (already validated when created via fromBytes) - const pk_value = try set_value.getNamedProperty("pk"); - const unwrapped_pk = try env.unwrap(PublicKey, pk_value); - pks[i] = unwrapped_pk.*; - - // Deserialize signature from Uint8Array with validation (infinity + group check), - // matching blst-ts Rust behavior - const sig_value = try set_value.getNamedProperty("sig"); - const sig_bytes = try sig_value.getTypedarrayInfo(); - sigs[i] = Signature.deserialize(sig_bytes.data[0..]) catch return error.DeserializationFailed; - sigs[i].validate(true) catch return error.InvalidSignature; - } - - const data = try allocator.create(AsyncAggregateData); - errdefer allocator.destroy(data); - - data.* = .{ - .pks = pks, - .sigs = sigs, - .n = n, - .deferred = try napi.Deferred.create(env.env), - }; - - const resource_name = try env.createStringUtf8("asyncAggregateWithRandomness"); - data.work = try napi.AsyncWork(AsyncAggregateData).create( - env, - null, - resource_name, - asyncAggregateExecute, - asyncAggregateComplete, - data, - ); - try data.work.queue(); - - return data.deferred.getPromise(); -} - -pub fn register(env: napi.Env, exports: napi.Value) !void { - const blst_obj = try env.createObject(); - - const sk_ctor = try env.defineClass( - "SecretKey", - 0, - SecretKey_ctor, - null, - &[_]napi.c.napi_property_descriptor{ - method(1, SecretKey_sign), - method(0, SecretKey_toPublicKey), - method(0, SecretKey_toBytes), - method(0, SecretKey_toHex), - }, - ); - try sk_ctor.defineProperties(&[_]napi.c.napi_property_descriptor{ - method(1, SecretKey_fromBytes), - method(1, SecretKey_fromHex), - method(2, SecretKey_fromKeygen), - }); - - const pk_ctor = try env.defineClass( - "PublicKey", - 0, - PublicKey_ctor, - null, - &[_]napi.c.napi_property_descriptor{ - method(1, PublicKey_toBytes), - method(1, PublicKey_toHex), - method(0, PublicKey_validate), - }, - ); - try pk_ctor.defineProperties(&[_]napi.c.napi_property_descriptor{ - method(2, PublicKey_fromBytes), - method(2, PublicKey_fromHex), - }); - - const sig_ctor = try env.defineClass( - "Signature", - 0, - Signature_ctor, - null, - &[_]napi.c.napi_property_descriptor{ - method(1, Signature_toBytes), - method(1, Signature_toHex), - method(1, Signature_validate), - }, - ); - try sig_ctor.defineProperties(&[_]napi.c.napi_property_descriptor{ - method(3, Signature_fromBytes), - method(3, Signature_fromHex), - method(2, Signature_aggregate), - }); - - const state = try InstanceData.init(env); - try setRef(env, pk_ctor, &state.public_key_ctor_ref); - try setRef(env, sig_ctor, &state.signature_ctor_ref); - - try blst_obj.setNamedProperty("SecretKey", sk_ctor); - try blst_obj.setNamedProperty("PublicKey", pk_ctor); - try blst_obj.setNamedProperty("Signature", sig_ctor); - - try blst_obj.setNamedProperty("verify", try env.createFunction("verify", 5, blst_verify, null)); - try blst_obj.setNamedProperty("aggregateVerify", try env.createFunction("aggregateVerify", 5, blst_aggregateVerify, null)); - try blst_obj.setNamedProperty("fastAggregateVerify", try env.createFunction("fastAggregateVerify", 4, blst_fastAggregateVerify, null)); - try blst_obj.setNamedProperty("verifyMultipleAggregateSignatures", try env.createFunction("verifyMultipleAggregateSignatures", 3, blst_verifyMultipleAggregateSignatures, null)); - try blst_obj.setNamedProperty("aggregateSignatures", try env.createFunction("aggregateSignatures", 2, blst_aggregateSignatures, null)); - try blst_obj.setNamedProperty("aggregatePublicKeys", try env.createFunction("aggregatePublicKeys", 2, blst_aggregatePublicKeys, null)); - try blst_obj.setNamedProperty("aggregateSerializedPublicKeys", try env.createFunction("aggregateSerializedPublicKeys", 2, blst_aggregateSerializedPublicKeys, null)); - try blst_obj.setNamedProperty("asyncAggregateWithRandomness", try env.createFunction("asyncAggregateWithRandomness", 1, blst_asyncAggregateWithRandomness, null)); - - try exports.setNamedProperty("blst", blst_obj); -} diff --git a/bindings/napi/pubkeys.zig b/bindings/napi/pubkeys.zig index 9869eb368..06246d540 100644 --- a/bindings/napi/pubkeys.zig +++ b/bindings/napi/pubkeys.zig @@ -168,24 +168,17 @@ pub fn getIndex(pubkey: js.Uint8Array) !js.Value { /// JS: pubkeys.get(index) → PublicKey | undefined /// Returns the PublicKey at the given index, or undefined if out of bounds. -/// Uses low-level N-API for PublicKey class interop. -pub fn get(index: js.Number) !js.Value { +pub fn get(index: js.Number) !?blst_bindings.PublicKey { if (!state.initialized) { return error.PubkeyIndexNotInitialized; } - const e = js.env(); const idx = index.assertU32(); if (idx >= state.index2pubkey.items.len) { - const undef = try e.getUndefined(); - return .{ .val = undef }; + return null; } - // Drop to low-level for PublicKey class interop - const out = try blst_bindings.newPublicKeyInstance(e); - const out_pubkey = try e.unwrap(bls.PublicKey, out); - out_pubkey.* = state.index2pubkey.items[@intCast(idx)]; - return .{ .val = out }; + return .{ .raw = state.index2pubkey.items[@intCast(idx)] }; } /// JS: pubkeys.set(index, pubkeyBytes) diff --git a/bindings/napi/root.zig b/bindings/napi/root.zig index 93676baea..53e64ec80 100644 --- a/bindings/napi/root.zig +++ b/bindings/napi/root.zig @@ -1,16 +1,14 @@ const std = @import("std"); const zapi = @import("zapi"); -const napi = zapi.napi; const js = zapi.js; pub const pool = @import("./pool.zig"); pub const shuffle = @import("./shuffle.zig"); pub const config = @import("./config.zig"); pub const pubkeys = @import("./pubkeys.zig"); pub const metrics = @import("./metrics.zig"); - -const BeaconStateView = @import("./BeaconStateView.zig"); -const state_transition = @import("./state_transition.zig"); -const blst = @import("./blst.zig"); +pub const blst = @import("./blst.zig"); +pub const BeaconStateView = @import("./BeaconStateView.zig").BeaconStateView; +pub const stateTransition = @import("./state_transition.zig"); fn init(old_ref_count: u32) !void { if (old_ref_count == 0) { @@ -31,17 +29,9 @@ fn cleanup(new_ref_count: u32) void { } } -/// Manual registration for modules not yet converted to DSL. -fn register(env: napi.Env, exports: napi.Value) !void { - try blst.register(env, exports); - try BeaconStateView.register(env, exports); - try state_transition.register(env, exports); -} - comptime { js.exportModule(@This(), .{ .init = init, .cleanup = cleanup, - .register = register, }); } diff --git a/bindings/napi/state_transition.zig b/bindings/napi/state_transition.zig index 8e234b287..a90c5761d 100644 --- a/bindings/napi/state_transition.zig +++ b/bindings/napi/state_transition.zig @@ -1,9 +1,11 @@ const std = @import("std"); -const napi = @import("zapi").napi; +const zapi = @import("zapi"); +const js = zapi.js; +const napi = zapi.napi; const builtin = @import("builtin"); const fork_types = @import("fork_types"); -const state_transition = @import("state_transition"); -const CachedBeaconState = state_transition.CachedBeaconState; +const st = @import("state_transition"); +const CachedBeaconState = st.CachedBeaconState; const AnySignedBeaconBlock = fork_types.AnySignedBeaconBlock; var gpa: std.heap.DebugAllocator(.{}) = .init; @@ -12,86 +14,64 @@ const allocator = if (builtin.mode == .Debug) else std.heap.c_allocator; -/// Perform a state transition given a signed beacon block. -/// -/// Arguments: -/// - arg 0: BeaconStateView instance (the pre-state) -/// - arg 1: signed block bytes (Uint8Array) -/// - arg 2: options object (optional) with: -/// - verifyStateRoot: bool (default true) -/// - verifyProposer: bool (default true) -/// - verifySignatures: bool (default false) -/// - transferCache: bool (default true) -/// -/// Returns: BeaconStateView (the post-state) -pub fn stateTransition( - env: napi.Env, - cb: napi.CallbackInfo(3), -) !napi.Value { - const pre_state_value = cb.arg(0); - const cached_state = try env.unwrap(CachedBeaconState, pre_state_value); +fn parseOptions(options: ?js.Value) !st.TransitionOpt { + var opts: st.TransitionOpt = .{}; + if (options) |value| { + const raw = value.toValue(); + if (try raw.typeof() == .object) { + if (try raw.hasNamedProperty("verifyStateRoot")) { + opts.verify_state_root = try (try raw.getNamedProperty("verifyStateRoot")).getValueBool(); + } + if (try raw.hasNamedProperty("verifyProposer")) { + opts.verify_proposer = try (try raw.getNamedProperty("verifyProposer")).getValueBool(); + } + if (try raw.hasNamedProperty("verifySignatures")) { + opts.verify_signatures = try (try raw.getNamedProperty("verifySignatures")).getValueBool(); + } + if (try raw.hasNamedProperty("transferCache")) { + opts.transfer_cache = try (try raw.getNamedProperty("transferCache")).getValueBool(); + } + } + } + return opts; +} - const bytes_info = try cb.arg(1).getTypedarrayInfo(); +pub fn stateTransition( + pre_state_value: js.Value, + signed_block_bytes: js.Uint8Array, + options: ?js.Value, +) !js.Value { + const env = js.env(); + const pre_state = pre_state_value.toValue(); + const cached_state = try env.unwrap(CachedBeaconState, pre_state); + const bytes = try signed_block_bytes.toSlice(); - if (bytes_info.array_type != .uint8) { - return error.InvalidSignedBlockBytesType; - } - const current_epoch = state_transition.computeEpochAtSlot(try cached_state.state.slot()); + const current_epoch = st.computeEpochAtSlot(try cached_state.state.slot()); const fork = cached_state.config.forkSeqAtEpoch(current_epoch); const signed_block = try AnySignedBeaconBlock.deserialize( allocator, .full, fork, - bytes_info.data, + bytes, ); defer signed_block.deinit(allocator); - var opts: state_transition.TransitionOpt = .{}; - if (cb.getArg(2)) |options_arg| { - if (try options_arg.typeof() == .object) { - if (try options_arg.hasNamedProperty("verifyStateRoot")) { - opts.verify_state_root = try (try options_arg.getNamedProperty("verifyStateRoot")).getValueBool(); - } - if (try options_arg.hasNamedProperty("verifyProposer")) { - opts.verify_proposer = try (try options_arg.getNamedProperty("verifyProposer")).getValueBool(); - } - if (try options_arg.hasNamedProperty("verifySignatures")) { - opts.verify_signatures = try (try options_arg.getNamedProperty("verifySignatures")).getValueBool(); - } - if (try options_arg.hasNamedProperty("transferCache")) { - opts.transfer_cache = try (try options_arg.getNamedProperty("transferCache")).getValueBool(); - } - } - } - - const post_state = try state_transition.stateTransition( + const post_state = try st.stateTransition( allocator, cached_state, signed_block, - opts, + try parseOptions(options), ); errdefer { post_state.deinit(); allocator.destroy(post_state); } - const ctor = try pre_state_value.getNamedProperty("constructor"); + const ctor = try pre_state.getNamedProperty("constructor"); const new_state_value = try env.newInstance(ctor, .{}); const dummy_state = try env.unwrap(CachedBeaconState, new_state_value); - dummy_state.* = post_state.*; allocator.destroy(post_state); - return new_state_value; -} - -pub fn register(env: napi.Env, exports: napi.Value) !void { - const state_transition_obj = try env.createObject(); - try state_transition_obj.setNamedProperty("stateTransition", try env.createFunction( - "stateTransition", - 3, - stateTransition, - null, - )); - try exports.setNamedProperty("stateTransition", state_transition_obj); + return .{ .val = new_state_value }; } diff --git a/build.zig.zon b/build.zig.zon index b5abaf9c7..05f09d8af 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -23,8 +23,8 @@ .hash = "zig_yaml-0.1.0-C1161m2NAgDhth5OvjG1o1UNKcdo-XNfO82m10J1g4Cl", }, .zapi = .{ - .url = "git+https://github.com/chainsafe/zapi#e40f8d1e022435f68bc76159f15db2c1c6cb6856", - .hash = "zapi-0.1.0-rIqzUbuhAwDHKpdfh4b3zS27O26yg4FtKtIbWz3ueRw9", + .url = "git+https://github.com/chainsafe/zapi#390d398955fd6c7568fc2edeaedd15e1a98e0638", + .hash = "zapi-0.1.0-rIqzURG_AwB3XhSpuXJjRYSg_nD4uBFKTG8zIUN2YwZL", }, .zbench = .{ .url = "git+https://github.com/hendriknielaender/zBench#d8c7dd485306b88b757d52005614ebcb0a336942", diff --git a/zbuild.zon b/zbuild.zon index 74eee5354..8ffaf2252 100644 --- a/zbuild.zon +++ b/zbuild.zon @@ -22,7 +22,7 @@ .url = "git+https://github.com/chainsafe/zig-yaml#e0e5962579e990a66c21424416c7ac092b20b772", }, .zapi = .{ - .url = "git+https://github.com/chainsafe/zapi#e40f8d1e022435f68bc76159f15db2c1c6cb6856", + .url = "git+https://github.com/chainsafe/zapi#390d398955fd6c7568fc2edeaedd15e1a98e0638", }, .zbench = .{ .url = "git+https://github.com/hendriknielaender/zBench#d8c7dd485306b88b757d52005614ebcb0a336942", From 273fcf1ed80a99658675fb0520685079265b00d6 Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Tue, 21 Apr 2026 17:52:54 +0200 Subject: [PATCH 8/9] chore: upgrade zapi version --- build.zig.zon | 4 ++-- package.json | 2 +- pnpm-lock.yaml | 10 +++++----- zbuild.zon | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 05f09d8af..984e5bd71 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -23,8 +23,8 @@ .hash = "zig_yaml-0.1.0-C1161m2NAgDhth5OvjG1o1UNKcdo-XNfO82m10J1g4Cl", }, .zapi = .{ - .url = "git+https://github.com/chainsafe/zapi#390d398955fd6c7568fc2edeaedd15e1a98e0638", - .hash = "zapi-0.1.0-rIqzURG_AwB3XhSpuXJjRYSg_nD4uBFKTG8zIUN2YwZL", + .url = "git+https://github.com/chainsafe/zapi?ref=zapi-v1.0.1#8dc18e68842968673bb3f2d7dbd21f505f1703e1", + .hash = "zapi-1.0.1-rIqzUTNDBAAbLOERQ59FuZuVUfTZpX_OxJyvlc0kNZrB", }, .zbench = .{ .url = "git+https://github.com/hendriknielaender/zBench#d8c7dd485306b88b757d52005614ebcb0a336942", diff --git a/package.json b/package.json index c40ad0766..78fe4304d 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ ] }, "dependencies": { - "@chainsafe/zapi": "0.1.1" + "@chainsafe/zapi": "1.0.1" }, "devDependencies": { "@biomejs/biome": "^2.3.11", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b6450458b..dc1420a64 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: '@chainsafe/zapi': - specifier: 0.1.1 - version: 0.1.1 + specifier: 1.0.1 + version: 1.0.1 devDependencies: '@biomejs/biome': specifier: ^2.3.11 @@ -308,8 +308,8 @@ packages: resolution: {integrity: sha512-H8YdEoXXv2Hw17gDWGOJEya4LHlBbpChJP3jDQRfIk9hhwr0c/zbBemBRmjADowZhArL+ymkO+j5hGaYySjdpw==} engines: {node: '>= 18'} - '@chainsafe/zapi@0.1.1': - resolution: {integrity: sha512-HpjNyRaXODjXMqVTpq75jaY6TfeBk3icPWYUAbkqrQFpFfYCbPUdWPJ4FCYrejxYGUbaJGFKp8IHHEmMYp2MBw==} + '@chainsafe/zapi@1.0.1': + resolution: {integrity: sha512-AskwfpFdNfFIxXfHNesH2zfBkCHYGMNfFm27o+JJosm3gwv90EjE5KpX7m03PQB2lBjxbmTs9HCgPMFWEVVBnA==} hasBin: true '@emnapi/core@1.8.1': @@ -1547,7 +1547,7 @@ snapshots: '@chainsafe/swap-or-not-shuffle-win32-arm64-msvc': 1.2.1 '@chainsafe/swap-or-not-shuffle-win32-x64-msvc': 1.2.1 - '@chainsafe/zapi@0.1.1': {} + '@chainsafe/zapi@1.0.1': {} '@emnapi/core@1.8.1': dependencies: diff --git a/zbuild.zon b/zbuild.zon index 8ffaf2252..30765f648 100644 --- a/zbuild.zon +++ b/zbuild.zon @@ -22,7 +22,7 @@ .url = "git+https://github.com/chainsafe/zig-yaml#e0e5962579e990a66c21424416c7ac092b20b772", }, .zapi = .{ - .url = "git+https://github.com/chainsafe/zapi#390d398955fd6c7568fc2edeaedd15e1a98e0638", + .url = "git+https://github.com/chainsafe/zapi#zapi-v1.0.1", }, .zbench = .{ .url = "git+https://github.com/hendriknielaender/zBench#d8c7dd485306b88b757d52005614ebcb0a336942", From c8962d8ccae73e0de5780b555038ab2b3a6c6dca Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Tue, 21 Apr 2026 17:53:23 +0200 Subject: [PATCH 9/9] chore: revert changes to bls,state-view,pubkeys --- bindings/napi/BeaconStateView.zig | 1664 ++++++++++++++++------------- bindings/napi/blst.zig | 1239 ++++++++++++++------- bindings/napi/pubkeys.zig | 127 ++- bindings/napi/root.zig | 20 +- bindings/test/pubkeys.test.ts | 4 +- bindings/test/shuffle.test.ts | 2 +- 6 files changed, 1865 insertions(+), 1191 deletions(-) diff --git a/bindings/napi/BeaconStateView.zig b/bindings/napi/BeaconStateView.zig index 4f2f8d112..53d061dd6 100644 --- a/bindings/napi/BeaconStateView.zig +++ b/bindings/napi/BeaconStateView.zig @@ -1,7 +1,5 @@ const std = @import("std"); -const zapi = @import("zapi"); -const js = zapi.js; -const napi = zapi.napi; +const napi = @import("zapi").napi; const c = @import("config"); const fork_types = @import("fork_types"); const st = @import("state_transition"); @@ -12,866 +10,1088 @@ const AnySignedBeaconBlock = fork_types.AnySignedBeaconBlock; const preset = @import("preset").preset; const ct = @import("consensus_types"); const pool = @import("./pool.zig"); -const config_bindings = @import("./config.zig"); +const config = @import("./config.zig"); const pubkey = @import("./pubkeys.zig"); const sszValueToNapiValue = @import("./to_napi_value.zig").sszValueToNapiValue; const numberSliceToNapiValue = @import("./to_napi_value.zig").numberSliceToNapiValue; +const getter = @import("napi_property_descriptor.zig").getter; +const method = @import("napi_property_descriptor.zig").method; + +/// Allocator used for all BeaconStateView instances. var gpa: std.heap.DebugAllocator(.{}) = .init; const allocator = gpa.allocator(); -fn asJsValue(value: napi.Value) js.Value { - return .{ .val = value }; -} - -fn jsNull() !js.Value { - return asJsValue(try js.env().getNull()); +pub fn BeaconStateView_finalize(_: napi.Env, cached_state: *CachedBeaconState, _: ?*anyopaque) void { + cached_state.deinit(); + allocator.destroy(cached_state); } -fn throwNull(code: [:0]const u8, message: [:0]const u8) !js.Value { - const env = js.env(); - try env.throwError(code, message); - return jsNull(); -} +pub fn BeaconStateView_ctor(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { + const cached_state = try allocator.create(CachedBeaconState); + errdefer allocator.destroy(cached_state); -fn optionalBool(options: ?js.Value, name: [:0]const u8, default_value: bool) !bool { - if (options) |value| { - const raw = value.toValue(); - if (try raw.typeof() == .object and try raw.hasNamedProperty(name)) { - return try (try raw.getNamedProperty(name)).getValueBool(); - } - } - return default_value; -} - -pub const BeaconStateView = struct { - pub const js_meta = js.class(.{ - .properties = .{ - .slot = true, - .fork = true, - .epoch = true, - .genesisTime = true, - .genesisValidatorsRoot = true, - .eth1Data = true, - .latestBlockHeader = true, - .previousJustifiedCheckpoint = true, - .currentJustifiedCheckpoint = true, - .finalizedCheckpoint = true, - .previousEpochParticipation = true, - .currentEpochParticipation = true, - .latestExecutionPayloadHeader = true, - .historicalSummaries = true, - .pendingDeposits = true, - .pendingDepositsCount = true, - .pendingPartialWithdrawals = true, - .pendingPartialWithdrawalsCount = true, - .pendingConsolidations = true, - .pendingConsolidationsCount = true, - .proposerLookahead = true, - .previousDecisionRoot = true, - .currentDecisionRoot = true, - .nextDecisionRoot = true, - .currentProposers = true, - .nextProposers = true, - .previousProposers = true, - .currentSyncCommittee = true, - .nextSyncCommittee = true, - .currentSyncCommitteeIndexed = true, - .syncProposerReward = true, - .effectiveBalanceIncrements = true, - .validatorCount = true, - .activeValidatorCount = true, - .isMergeTransitionComplete = true, - .isExecutionStateType = true, - .proposerRewards = true, - .clonedCount = true, - .clonedCountWithTransferCache = true, - .createdWithTransferCache = true, - }, - }); + _ = try env.wrap( + cb.this(), + CachedBeaconState, + cached_state, + BeaconStateView_finalize, + null, + null, + ); - cached_state: ?*CachedBeaconState = null, + return cb.this(); +} - const Self = @This(); +pub fn BeaconStateView_createFromBytes(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { + const ctor = cb.this(); - pub fn init() Self { - return .{}; - } + const bytes_info = try cb.arg(0).getTypedarrayInfo(); + const state = try allocator.create(AnyBeaconState); + errdefer allocator.destroy(state); - pub fn deinit(self: *Self) void { - if (self.cached_state) |cached_state| { - cached_state.deinit(); - allocator.destroy(cached_state); - self.cached_state = null; - } - } + const slot = fork_types.readSlotFromAnyBeaconStateBytes(bytes_info.data); + const fork = config.state.config.forkSeq(slot); + state.* = try AnyBeaconState.deserialize(allocator, &pool.state.pool, fork, bytes_info.data); + errdefer state.deinit(); - fn requireState(self: *const Self) !*CachedBeaconState { - return self.cached_state orelse error.InvalidState; - } + const cached_state_value = try env.newInstance(ctor, .{}); - pub fn createFromBytes(bytes: js.Uint8Array) !Self { - const state = try allocator.create(AnyBeaconState); - errdefer allocator.destroy(state); - - const byte_slice = try bytes.toSlice(); - const slot_value = fork_types.readSlotFromAnyBeaconStateBytes(byte_slice); - const fork_seq = config_bindings.state.config.forkSeq(slot_value); - state.* = try AnyBeaconState.deserialize(allocator, &pool.state.pool, fork_seq, byte_slice); - errdefer state.deinit(); - - const cached_state = try allocator.create(CachedBeaconState); - errdefer allocator.destroy(cached_state); - - try cached_state.init( - allocator, - state, - .{ - .config = &config_bindings.state.config, - .index_to_pubkey = &pubkey.state.index2pubkey, - .pubkey_to_index = &pubkey.state.pubkey2index, - }, - null, - ); + const cached_state = try env.unwrap(CachedBeaconState, cached_state_value); - return .{ .cached_state = cached_state }; - } + try cached_state.init( + allocator, + state, + .{ + .config = &config.state.config, + .index_to_pubkey = &pubkey.state.index2pubkey, + .pubkey_to_index = &pubkey.state.pubkey2index, + }, + null, + ); - pub fn slot(self: *const Self) !js.Number { - const slot_value = try (try self.requireState()).state.slot(); - return js.Number.from(slot_value); - } + return cached_state_value; +} - pub fn fork(self: *const Self) !js.Value { - const env = js.env(); - var fork_view = try (try self.requireState()).state.fork(); - var fork_value: ct.phase0.Fork.Type = undefined; - try fork_view.toValue(allocator, &fork_value); - return asJsValue(try sszValueToNapiValue(env, ct.phase0.Fork, &fork_value)); - } +pub fn BeaconStateView_slot(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + const slot = try cached_state.state.slot(); + return try env.createInt64(@intCast(slot)); +} - pub fn epoch(self: *const Self) !js.Number { - const slot_value = try (try self.requireState()).state.slot(); - return js.Number.from(slot_value / preset.SLOTS_PER_EPOCH); - } +pub fn BeaconStateView_fork(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + var fork_view = try cached_state.state.fork(); + var fork: ct.phase0.Fork.Type = undefined; + try fork_view.toValue(allocator, &fork); + return try sszValueToNapiValue(env, ct.phase0.Fork, &fork); +} - pub fn genesisTime(self: *const Self) !js.Number { - return js.Number.from(try (try self.requireState()).state.genesisTime()); - } +pub fn BeaconStateView_epoch(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + const slot = try cached_state.state.slot(); + return try env.createInt64(@intCast(slot / preset.SLOTS_PER_EPOCH)); +} - pub fn genesisValidatorsRoot(self: *const Self) !js.Value { - const env = js.env(); - return asJsValue(try sszValueToNapiValue(env, ct.primitive.Root, try (try self.requireState()).state.genesisValidatorsRoot())); - } +pub fn BeaconStateView_genesisTime(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + return try env.createInt64(@intCast(try cached_state.state.genesisTime())); +} - pub fn eth1Data(self: *const Self) !js.Value { - const env = js.env(); - var eth1_data_view = try (try self.requireState()).state.eth1Data(); - var eth1_data: ct.phase0.Eth1Data.Type = undefined; - try eth1_data_view.toValue(allocator, ð1_data); - return asJsValue(try sszValueToNapiValue(env, ct.phase0.Eth1Data, ð1_data)); - } +pub fn BeaconStateView_genesisValidatorsRoot(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + return sszValueToNapiValue(env, ct.primitive.Root, try cached_state.state.genesisValidatorsRoot()); +} - pub fn latestBlockHeader(self: *const Self) !js.Value { - const env = js.env(); - var header_view = try (try self.requireState()).state.latestBlockHeader(); - var header: ct.phase0.BeaconBlockHeader.Type = undefined; - try header_view.toValue(allocator, &header); - return asJsValue(try sszValueToNapiValue(env, ct.phase0.BeaconBlockHeader, &header)); - } +pub fn BeaconStateView_eth1Data(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + var eth1_data_view = try cached_state.state.eth1Data(); + var eth1_data: ct.phase0.Eth1Data.Type = undefined; + try eth1_data_view.toValue(allocator, ð1_data); + return try sszValueToNapiValue(env, ct.phase0.Eth1Data, ð1_data); +} - pub fn previousJustifiedCheckpoint(self: *const Self) !js.Value { - const env = js.env(); - var cp: ct.phase0.Checkpoint.Type = undefined; - try (try self.requireState()).state.previousJustifiedCheckpoint(&cp); - return asJsValue(try sszValueToNapiValue(env, ct.phase0.Checkpoint, &cp)); - } +pub fn BeaconStateView_latestBlockHeader(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + var header_view = try cached_state.state.latestBlockHeader(); + var header: ct.phase0.BeaconBlockHeader.Type = undefined; + try header_view.toValue(allocator, &header); + return try sszValueToNapiValue(env, ct.phase0.BeaconBlockHeader, &header); +} - pub fn currentJustifiedCheckpoint(self: *const Self) !js.Value { - const env = js.env(); - var cp: ct.phase0.Checkpoint.Type = undefined; - try (try self.requireState()).state.currentJustifiedCheckpoint(&cp); - return asJsValue(try sszValueToNapiValue(env, ct.phase0.Checkpoint, &cp)); - } +pub fn BeaconStateView_previousJustifiedCheckpoint(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + var cp: ct.phase0.Checkpoint.Type = undefined; + try cached_state.state.previousJustifiedCheckpoint(&cp); + return try sszValueToNapiValue(env, ct.phase0.Checkpoint, &cp); +} - pub fn finalizedCheckpoint(self: *const Self) !js.Value { - const env = js.env(); - var cp: ct.phase0.Checkpoint.Type = undefined; - try (try self.requireState()).state.finalizedCheckpoint(&cp); - return asJsValue(try sszValueToNapiValue(env, ct.phase0.Checkpoint, &cp)); - } +pub fn BeaconStateView_currentJustifiedCheckpoint(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + var cp: ct.phase0.Checkpoint.Type = undefined; + try cached_state.state.currentJustifiedCheckpoint(&cp); + return try sszValueToNapiValue(env, ct.phase0.Checkpoint, &cp); +} - pub fn getBlockRoot(self: *const Self, slot_arg: js.Number) !js.Value { - const env = js.env(); - const cached_state = try self.requireState(); - const slot_value: u64 = @intCast(try slot_arg.toI64()); +pub fn BeaconStateView_finalizedCheckpoint(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + var cp: ct.phase0.Checkpoint.Type = undefined; + try cached_state.state.finalizedCheckpoint(&cp); + return try sszValueToNapiValue(env, ct.phase0.Checkpoint, &cp); +} - const result = switch (cached_state.state.forkSeq()) { - inline else => |f| st.getBlockRootAtSlot(f, cached_state.state.castToFork(f), slot_value), - }; - const root = result catch |err| { - const msg = switch (err) { - error.SlotTooBig => "Can only get block root in the past", - error.SlotTooSmall => "Cannot get block root more than SLOTS_PER_HISTORICAL_ROOT in the past", - else => "Failed to get block root", - }; - return throwNull("INVALID_SLOT", msg); +/// Get the block root at a given slot. +/// Arguments: +/// - arg 0: slot (number) +/// Returns: Root (Uint8Array) +pub fn BeaconStateView_getBlockRoot(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + const slot: u64 = @intCast(try cb.arg(0).getValueInt64()); + + const result = switch (cached_state.state.forkSeq()) { + inline else => |f| st.getBlockRootAtSlot(f, cached_state.state.castToFork(f), slot), + }; + const root = result catch |err| { + const msg = switch (err) { + error.SlotTooBig => "Can only get block root in the past", + error.SlotTooSmall => "Cannot get block root more than SLOTS_PER_HISTORICAL_ROOT in the past", + else => "Failed to get block root", }; + try env.throwError("INVALID_SLOT", msg); + return env.getNull(); + }; - return asJsValue(try sszValueToNapiValue(env, ct.primitive.Root, root)); - } + return sszValueToNapiValue(env, ct.primitive.Root, root); +} - pub fn getRandaoMix(self: *const Self, epoch_arg: js.Number) !js.Value { - const env = js.env(); - const cached_state = try self.requireState(); - const epoch_value: u64 = @intCast(try epoch_arg.toI64()); +/// Get the randao mix at a given epoch. +/// Arguments: +/// - arg 0: epoch (number) +/// Returns: Bytes32 (Uint8Array) +pub fn BeaconStateView_getRandaoMix(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + const epoch: u64 = @intCast(try cb.arg(0).getValueInt64()); + + const result = switch (cached_state.state.forkSeq()) { + inline else => |f| st.getRandaoMix(f, cached_state.state.castToFork(f), epoch), + }; + const mix = result catch { + try env.throwError("INVALID_EPOCH", "Failed to get randao mix for epoch"); + return env.getNull(); + }; + + return sszValueToNapiValue(env, ct.primitive.Bytes32, mix); +} - const result = switch (cached_state.state.forkSeq()) { - inline else => |f| st.getRandaoMix(f, cached_state.state.castToFork(f), epoch_value), - }; - const mix = result catch { - return throwNull("INVALID_EPOCH", "Failed to get randao mix for epoch"); - }; +pub fn BeaconStateView_previousEpochParticipation(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + var view = try cached_state.state.previousEpochParticipation(); - return asJsValue(try sszValueToNapiValue(env, ct.primitive.Bytes32, mix)); - } + const size = try view.serializedSize(); + var bytes: [*]u8 = undefined; + const buf = try env.createArrayBuffer(size, &bytes); + _ = try view.serializeIntoBytes(bytes[0..size]); - pub fn previousEpochParticipation(self: *const Self) !js.Uint8Array { - const env = js.env(); - var view = try (try self.requireState()).state.previousEpochParticipation(); + return try env.createTypedarray(.uint8, size, buf, 0); +} - const size = try view.serializedSize(); - var bytes: [*]u8 = undefined; - const buf = try env.createArrayBuffer(size, &bytes); - _ = try view.serializeIntoBytes(bytes[0..size]); +pub fn BeaconStateView_currentEpochParticipation(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + var view = try cached_state.state.currentEpochParticipation(); - return .{ .val = try env.createTypedarray(.uint8, size, buf, 0) }; - } + const size = try view.serializedSize(); + var bytes: [*]u8 = undefined; + const buf = try env.createArrayBuffer(size, &bytes); + _ = try view.serializeIntoBytes(bytes[0..size]); - pub fn currentEpochParticipation(self: *const Self) !js.Uint8Array { - const env = js.env(); - var view = try (try self.requireState()).state.currentEpochParticipation(); + return try env.createTypedarray(.uint8, size, buf, 0); +} - const size = try view.serializedSize(); - var bytes: [*]u8 = undefined; - const buf = try env.createArrayBuffer(size, &bytes); - _ = try view.serializeIntoBytes(bytes[0..size]); +pub fn BeaconStateView_latestExecutionPayloadHeader(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + var header: AnyExecutionPayloadHeader = undefined; + try cached_state.state.latestExecutionPayloadHeader(allocator, &header); + defer header.deinit(allocator); + + return switch (header) { + .bellatrix => |*h| try sszValueToNapiValue(env, ct.bellatrix.ExecutionPayloadHeader, h), + .capella => |*h| try sszValueToNapiValue(env, ct.capella.ExecutionPayloadHeader, h), + .deneb => |*h| try sszValueToNapiValue(env, ct.deneb.ExecutionPayloadHeader, h), + }; +} - return .{ .val = try env.createTypedarray(.uint8, size, buf, 0) }; - } +/// Get the historical summaries from the state (Capella+). +/// Returns: array of {blockSummaryRoot: Uint8Array, stateSummaryRoot: Uint8Array} +pub fn BeaconStateView_historicalSummaries(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - pub fn latestExecutionPayloadHeader(self: *const Self) !js.Value { - const env = js.env(); - var header: AnyExecutionPayloadHeader = undefined; - try (try self.requireState()).state.latestExecutionPayloadHeader(allocator, &header); - defer header.deinit(allocator); + var historical_summaries_view = try cached_state.state.historicalSummaries(); + var historical_summaries = ct.capella.HistoricalSummaries.default_value; + try historical_summaries_view.toValue(allocator, &historical_summaries); + defer historical_summaries.deinit(allocator); - const value = switch (header) { - .bellatrix => |*h| try sszValueToNapiValue(env, ct.bellatrix.ExecutionPayloadHeader, h), - .capella => |*h| try sszValueToNapiValue(env, ct.capella.ExecutionPayloadHeader, h), - .deneb => |*h| try sszValueToNapiValue(env, ct.deneb.ExecutionPayloadHeader, h), - }; - return asJsValue(value); - } + return try sszValueToNapiValue(env, ct.capella.HistoricalSummaries, &historical_summaries); +} - pub fn historicalSummaries(self: *const Self) !js.Value { - const env = js.env(); - var historical_summaries_view = try (try self.requireState()).state.historicalSummaries(); - var historical_summaries = ct.capella.HistoricalSummaries.default_value; - try historical_summaries_view.toValue(allocator, &historical_summaries); - defer historical_summaries.deinit(allocator); - return asJsValue(try sszValueToNapiValue(env, ct.capella.HistoricalSummaries, &historical_summaries)); - } +/// Get the pending deposits from the state (Electra+). +/// Returns: Uint8Array of SSZ serialized PendingDeposits list +pub fn BeaconStateView_pendingDeposits(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + + var pending_deposits = cached_state.state.pendingDeposits() catch { + try env.throwError("STATE_ERROR", "Failed to get pendingDeposits"); + return env.getNull(); + }; + + const size = pending_deposits.serializedSize() catch { + try env.throwError("STATE_ERROR", "Failed to get pendingDeposits size"); + return env.getNull(); + }; + + var bytes: [*]u8 = undefined; + const buf = try env.createArrayBuffer(size, &bytes); + _ = pending_deposits.serializeIntoBytes(bytes[0..size]) catch { + try env.throwError("STATE_ERROR", "Failed to serialize pendingDeposits"); + return env.getNull(); + }; + + return try env.createTypedarray(.uint8, size, buf, 0); +} - pub fn pendingDeposits(self: *const Self) !js.Value { - const env = js.env(); - const cached_state = try self.requireState(); +pub fn BeaconStateView_pendingDepositsCount(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + var pending_deposits = try cached_state.state.pendingDeposits(); + return try env.createInt64(@intCast(try pending_deposits.length())); +} - var pending_deposits = cached_state.state.pendingDeposits() catch { - return throwNull("STATE_ERROR", "Failed to get pendingDeposits"); - }; +/// Get the pending partial withdrawals from the state (Electra+). +/// Returns: Uint8Array of SSZ serialized PendingPartialWithdrawals list +pub fn BeaconStateView_pendingPartialWithdrawals(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + + var pending_partial_withdrawals = cached_state.state.pendingPartialWithdrawals() catch { + try env.throwError("STATE_ERROR", "Failed to get pendingPartialWithdrawals"); + return env.getNull(); + }; + + const size = pending_partial_withdrawals.serializedSize() catch { + try env.throwError("STATE_ERROR", "Failed to get pendingPartialWithdrawals size"); + return env.getNull(); + }; + + var bytes: [*]u8 = undefined; + const buf = try env.createArrayBuffer(size, &bytes); + _ = pending_partial_withdrawals.serializeIntoBytes(bytes[0..size]) catch { + try env.throwError("STATE_ERROR", "Failed to serialize pendingPartialWithdrawals"); + return env.getNull(); + }; + + return try env.createTypedarray(.uint8, size, buf, 0); +} - const size = pending_deposits.serializedSize() catch { - return throwNull("STATE_ERROR", "Failed to get pendingDeposits size"); - }; +pub fn BeaconStateView_pendingPartialWithdrawalsCount(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + var pending_partial_withdrawals = try cached_state.state.pendingPartialWithdrawals(); + return try env.createInt64(@intCast(try pending_partial_withdrawals.length())); +} - var bytes: [*]u8 = undefined; - const buf = try env.createArrayBuffer(size, &bytes); - _ = pending_deposits.serializeIntoBytes(bytes[0..size]) catch { - return throwNull("STATE_ERROR", "Failed to serialize pendingDeposits"); - }; +/// Get the pending consolidations from the state +pub fn BeaconStateView_pendingConsolidations(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - return asJsValue(try env.createTypedarray(.uint8, size, buf, 0)); - } + var pending_consolidations = cached_state.state.pendingConsolidations() catch { + try env.throwError("STATE_ERROR", "Failed to get pendingConsolidations"); + return env.getNull(); + }; - pub fn pendingDepositsCount(self: *const Self) !js.Number { - var pending_deposits = try (try self.requireState()).state.pendingDeposits(); - return js.Number.from(try pending_deposits.length()); - } + const size = pending_consolidations.serializedSize() catch { + try env.throwError("STATE_ERROR", "Failed to get pendingConsolidations size"); + return env.getNull(); + }; - pub fn pendingPartialWithdrawals(self: *const Self) !js.Value { - const env = js.env(); - const cached_state = try self.requireState(); + var bytes: [*]u8 = undefined; + const buf = try env.createArrayBuffer(size, &bytes); + _ = pending_consolidations.serializeIntoBytes(bytes[0..size]) catch { + try env.throwError("STATE_ERROR", "Failed to serialize pendingConsolidations"); + return env.getNull(); + }; - var pending_partial_withdrawals = cached_state.state.pendingPartialWithdrawals() catch { - return throwNull("STATE_ERROR", "Failed to get pendingPartialWithdrawals"); - }; - const size = pending_partial_withdrawals.serializedSize() catch { - return throwNull("STATE_ERROR", "Failed to get pendingPartialWithdrawals size"); - }; + return try env.createTypedarray(.uint8, size, buf, 0); +} - var bytes: [*]u8 = undefined; - const buf = try env.createArrayBuffer(size, &bytes); - _ = pending_partial_withdrawals.serializeIntoBytes(bytes[0..size]) catch { - return throwNull("STATE_ERROR", "Failed to serialize pendingPartialWithdrawals"); - }; - return asJsValue(try env.createTypedarray(.uint8, size, buf, 0)); - } +pub fn BeaconStateView_pendingConsolidationsCount(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + var pending_consolidations = try cached_state.state.pendingConsolidations(); + return try env.createInt64(@intCast(try pending_consolidations.length())); +} - pub fn pendingPartialWithdrawalsCount(self: *const Self) !js.Number { - var pending_partial_withdrawals = try (try self.requireState()).state.pendingPartialWithdrawals(); - return js.Number.from(try pending_partial_withdrawals.length()); - } +/// Get the proposer lookahead from the state (Fulu+). +pub fn BeaconStateView_proposerLookahead(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - pub fn pendingConsolidations(self: *const Self) !js.Value { - const env = js.env(); - const cached_state = try self.requireState(); + var proposer_lookahead = cached_state.state.proposerLookahead() catch { + try env.throwError("STATE_ERROR", "Failed to get proposerLookahead"); + return env.getNull(); + }; - var pending_consolidations = cached_state.state.pendingConsolidations() catch { - return throwNull("STATE_ERROR", "Failed to get pendingConsolidations"); - }; - const size = pending_consolidations.serializedSize() catch { - return throwNull("STATE_ERROR", "Failed to get pendingConsolidations size"); - }; + const lookahead = proposer_lookahead.getAll(allocator) catch { + try env.throwError("STATE_ERROR", "Failed to get proposerLookahead values"); + return env.getNull(); + }; + defer allocator.free(lookahead); - var bytes: [*]u8 = undefined; - const buf = try env.createArrayBuffer(size, &bytes); - _ = pending_consolidations.serializeIntoBytes(bytes[0..size]) catch { - return throwNull("STATE_ERROR", "Failed to serialize pendingConsolidations"); - }; + return try numberSliceToNapiValue(env, u64, lookahead, .{ .typed_array = .uint32 }); +} - return asJsValue(try env.createTypedarray(.uint8, size, buf, 0)); - } +// pub fn BeaconStateView_executionPayloadAvailability - pub fn pendingConsolidationsCount(self: *const Self) !js.Number { - var pending_consolidations = try (try self.requireState()).state.pendingConsolidations(); - return js.Number.from(try pending_consolidations.length()); - } +// pub fn BeaconStateView_getShufflingAtEpoch - pub fn proposerLookahead(self: *const Self) !js.Value { - const env = js.env(); - const cached_state = try self.requireState(); +/// Get the previous decision root for the state. +pub fn BeaconStateView_previousDecisionRoot(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + const root = cached_state.previousDecisionRoot(); + return sszValueToNapiValue(env, ct.primitive.Root, &root); +} - var proposer_lookahead = cached_state.state.proposerLookahead() catch { - return throwNull("STATE_ERROR", "Failed to get proposerLookahead"); - }; +/// Get the current decision root for the state. +pub fn BeaconStateView_currentDecisionRoot(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + const root = cached_state.currentDecisionRoot(); + return sszValueToNapiValue(env, ct.primitive.Root, &root); +} - const lookahead = proposer_lookahead.getAll(allocator) catch { - return throwNull("STATE_ERROR", "Failed to get proposerLookahead values"); - }; - defer allocator.free(lookahead); +/// Get the next decision root for the state. +pub fn BeaconStateView_nextDecisionRoot(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + const root = cached_state.nextDecisionRoot(); + return sszValueToNapiValue(env, ct.primitive.Root, &root); +} - return asJsValue(try numberSliceToNapiValue(env, u64, lookahead, .{ .typed_array = .uint32 })); - } +/// Get the shuffling decision root for a given epoch. +pub fn BeaconStateView_getShufflingDecisionRoot(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + const epoch: u64 = @intCast(try cb.arg(0).getValueInt64()); + const root = st.calculateShufflingDecisionRoot(cached_state.state, epoch) catch { + try env.throwError("STATE_ERROR", "Failed to calculate shuffling decision root"); + return env.getNull(); + }; + return sszValueToNapiValue(env, ct.primitive.Root, &root); +} - pub fn previousDecisionRoot(self: *const Self) !js.Value { - const env = js.env(); - const root = (try self.requireState()).previousDecisionRoot(); - return asJsValue(try sszValueToNapiValue(env, ct.primitive.Root, &root)); +pub fn BeaconStateView_previousProposers(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + if (cached_state.epoch_cache.proposers_prev_epoch) |*proposers| { + return numberSliceToNapiValue( + env, + u64, + proposers, + .{}, + ); } + return env.getNull(); +} +pub fn BeaconStateView_currentProposers(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + return numberSliceToNapiValue( + env, + u64, + &cached_state.epoch_cache.proposers, + .{}, + ); +} - pub fn currentDecisionRoot(self: *const Self) !js.Value { - const env = js.env(); - const root = (try self.requireState()).currentDecisionRoot(); - return asJsValue(try sszValueToNapiValue(env, ct.primitive.Root, &root)); +pub fn BeaconStateView_nextProposers(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + if (cached_state.epoch_cache.proposers_next_epoch) |*proposers| { + return numberSliceToNapiValue( + env, + u64, + proposers, + .{}, + ); } + return env.getNull(); +} - pub fn nextDecisionRoot(self: *const Self) !js.Value { - const env = js.env(); - const root = (try self.requireState()).nextDecisionRoot(); - return asJsValue(try sszValueToNapiValue(env, ct.primitive.Root, &root)); - } +/// Get the beacon proposer for a given slot. +/// Arguments: +/// - arg 0: slot (number) +/// Returns: validator index of the proposer +pub fn BeaconStateView_getBeaconProposer(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + const slot: u64 = @intCast(try cb.arg(0).getValueInt64()); + const proposer = try cached_state.epoch_cache.getBeaconProposer(slot); + return try env.createInt64(@intCast(proposer)); +} - pub fn getShufflingDecisionRoot(self: *const Self, epoch_arg: js.Number) !js.Value { - const env = js.env(); - const epoch_value: u64 = @intCast(try epoch_arg.toI64()); - const root = st.calculateShufflingDecisionRoot((try self.requireState()).state, epoch_value) catch { - return throwNull("STATE_ERROR", "Failed to calculate shuffling decision root"); - }; - return asJsValue(try sszValueToNapiValue(env, ct.primitive.Root, &root)); - } +pub fn BeaconStateView_currentSyncCommittee(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + var current_sync_committee = try cached_state.state.currentSyncCommittee(); + var result: ct.altair.SyncCommittee.Type = undefined; + try current_sync_committee.toValue(allocator, &result); + return try sszValueToNapiValue(env, ct.altair.SyncCommittee, &result); +} - pub fn previousProposers(self: *const Self) !?js.Array { - const env = js.env(); - const cached_state = try self.requireState(); - if (cached_state.epoch_cache.proposers_prev_epoch) |*proposers| { - return .{ .val = try numberSliceToNapiValue(env, u64, proposers, .{}) }; - } - return null; - } +pub fn BeaconStateView_nextSyncCommittee(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + var next_sync_committee = try cached_state.state.nextSyncCommittee(); + var result: ct.altair.SyncCommittee.Type = undefined; + try next_sync_committee.toValue(allocator, &result); + return try sszValueToNapiValue(env, ct.altair.SyncCommittee, &result); +} - pub fn currentProposers(self: *const Self) !js.Array { - const env = js.env(); - const cached_state = try self.requireState(); - return .{ .val = try numberSliceToNapiValue(env, u64, &cached_state.epoch_cache.proposers, .{}) }; - } +pub fn BeaconStateView_currentSyncCommitteeIndexed(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + const sync_committee_cache = cached_state.epoch_cache.current_sync_committee_indexed.get(); + const validator_indices = sync_committee_cache.getValidatorIndices(); + const validator_index_map = sync_committee_cache.getValidatorIndexMap(); + const obj = try env.createObject(); + try obj.setNamedProperty( + "validatorIndices", + try numberSliceToNapiValue( + env, + u32, + @as([]const u32, @ptrCast(validator_indices)), + .{ .typed_array = null }, + ), + ); - pub fn nextProposers(self: *const Self) !?js.Array { - const env = js.env(); - const cached_state = try self.requireState(); - if (cached_state.epoch_cache.proposers_next_epoch) |*proposers| { - return .{ .val = try numberSliceToNapiValue(env, u64, proposers, .{}) }; - } - return null; - } + const global = try env.getGlobal(); + const map_ctor = try global.getNamedProperty("Map"); + const map = try env.newInstance(map_ctor, .{}); + const set_fn = try map.getNamedProperty("set"); - pub fn getBeaconProposer(self: *const Self, slot_arg: js.Number) !js.Number { - const slot_value: u64 = @intCast(try slot_arg.toI64()); - const proposer = try (try self.requireState()).epoch_cache.getBeaconProposer(slot_value); - return js.Number.from(proposer); - } + // TODO: might need to check the perf here; another way is to send a array instead and convert them in js side. + var iterator = validator_index_map.iterator(); + while (iterator.next()) |entry| { + const idx = entry.key_ptr.*; + const positions = entry.value_ptr; - pub fn currentSyncCommittee(self: *const Self) !js.Value { - const env = js.env(); - var current_sync_committee = try (try self.requireState()).state.currentSyncCommittee(); - var result: ct.altair.SyncCommittee.Type = undefined; - try current_sync_committee.toValue(allocator, &result); - return asJsValue(try sszValueToNapiValue(env, ct.altair.SyncCommittee, &result)); - } + const key_value_napi = try env.createInt64(@intCast(idx)); + const positions_napi = try numberSliceToNapiValue(env, u32, @as([]const u32, @ptrCast(positions.items)), .{ .typed_array = .uint32 }); - pub fn nextSyncCommittee(self: *const Self) !js.Value { - const env = js.env(); - var next_sync_committee = try (try self.requireState()).state.nextSyncCommittee(); - var result: ct.altair.SyncCommittee.Type = undefined; - try next_sync_committee.toValue(allocator, &result); - return asJsValue(try sszValueToNapiValue(env, ct.altair.SyncCommittee, &result)); + _ = try env.callFunction(set_fn, map, .{ key_value_napi, positions_napi }); } - pub fn currentSyncCommitteeIndexed(self: *const Self) !js.Value { - const env = js.env(); - const cached_state = try self.requireState(); - const sync_committee_cache = cached_state.epoch_cache.current_sync_committee_indexed.get(); - const validator_indices = sync_committee_cache.getValidatorIndices(); - const validator_index_map = sync_committee_cache.getValidatorIndexMap(); - - const obj = try env.createObject(); - try obj.setNamedProperty( - "validatorIndices", - try numberSliceToNapiValue( - env, - u32, - @as([]const u32, @ptrCast(validator_indices)), - .{ .typed_array = null }, - ), - ); - - const global = try env.getGlobal(); - const map_ctor = try global.getNamedProperty("Map"); - const map = try env.newInstance(map_ctor, .{}); - const set_fn = try map.getNamedProperty("set"); - - var iterator = validator_index_map.iterator(); - while (iterator.next()) |entry| { - const idx = entry.key_ptr.*; - const positions = entry.value_ptr; - - const key_value_napi = try env.createInt64(@intCast(idx)); - const positions_napi = try numberSliceToNapiValue( - env, - u32, - @as([]const u32, @ptrCast(positions.items)), - .{ .typed_array = .uint32 }, - ); - - _ = try env.callFunction(set_fn, map, .{ key_value_napi, positions_napi }); - } + try obj.setNamedProperty("validatorIndexMap", map); - try obj.setNamedProperty("validatorIndexMap", map); - return asJsValue(obj); - } + return obj; +} - pub fn syncProposerReward(self: *const Self) !js.Number { - const sync_proposer_reward = (try self.requireState()).epoch_cache.sync_proposer_reward; - return js.Number.from(sync_proposer_reward); - } +pub fn BeaconStateView_syncProposerReward(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + const sync_proposer_reward = cached_state.epoch_cache.sync_proposer_reward; + return try env.createInt64(@intCast(sync_proposer_reward)); +} - pub fn getIndexedSyncCommitteeAtEpoch(self: *const Self, epoch_arg: js.Number) !js.Value { - const env = js.env(); - const cached_state = try self.requireState(); - const epoch_value: u64 = @intCast(try epoch_arg.toI64()); +/// Get the indexed sync committee at a given epoch. +/// Arguments: +/// - arg 0: epoch (number) +/// Returns: object with validatorIndices (number[]) +pub fn BeaconStateView_getIndexedSyncCommitteeAtEpoch(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + const epoch: u64 = @intCast(try cb.arg(0).getValueInt64()); + + const sync_committee = cached_state.epoch_cache.getIndexedSyncCommitteeAtEpoch(epoch) catch { + try env.throwError("NO_SYNC_COMMITTEE", "Sync committee not available for requested epoch"); + return env.getNull(); + }; + + const obj = try env.createObject(); + try obj.setNamedProperty( + "validatorIndices", + try numberSliceToNapiValue(env, u64, sync_committee.getValidatorIndices(), .{}), + ); + return obj; +} - const sync_committee = cached_state.epoch_cache.getIndexedSyncCommitteeAtEpoch(epoch_value) catch { - return throwNull("NO_SYNC_COMMITTEE", "Sync committee not available for requested epoch"); - }; +pub fn BeaconStateView_effectiveBalanceIncrements(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + const increments = cached_state.epoch_cache.getEffectiveBalanceIncrements(); + return try numberSliceToNapiValue(env, u16, increments.items, .{ .typed_array = .uint16 }); +} - const obj = try env.createObject(); - try obj.setNamedProperty( - "validatorIndices", - try numberSliceToNapiValue(env, u64, sync_committee.getValidatorIndices(), .{}), - ); - return asJsValue(obj); - } +pub fn BeaconStateView_getEffectiveBalanceIncrementsZeroInactive(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - pub fn effectiveBalanceIncrements(self: *const Self) !js.Uint16Array { - const env = js.env(); - const increments = (try self.requireState()).epoch_cache.getEffectiveBalanceIncrements(); - return .{ .val = try numberSliceToNapiValue(env, u16, increments.items, .{ .typed_array = .uint16 }) }; - } + var result = try st.getEffectiveBalanceIncrementsZeroInactive(allocator, cached_state); + defer result.deinit(); - pub fn getEffectiveBalanceIncrementsZeroInactive(self: *const Self) !js.Uint16Array { - const env = js.env(); - var result = try st.getEffectiveBalanceIncrementsZeroInactive(allocator, try self.requireState()); - defer result.deinit(); - return .{ .val = try numberSliceToNapiValue(env, u16, result.items, .{ .typed_array = .uint16 }) }; - } + return try numberSliceToNapiValue(env, u16, result.items, .{ .typed_array = .uint16 }); +} - pub fn getBalance(self: *const Self, index_arg: js.Number) !js.BigInt { - const index_value: u64 = @intCast(try index_arg.toI64()); - var balances = try (try self.requireState()).state.balances(); - const balance = try balances.get(index_value); - return js.BigInt.from(balance); - } +pub fn BeaconStateView_getBalance(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + const index: u64 = @intCast(try cb.arg(0).getValueInt64()); + var balances = try cached_state.state.balances(); + const balance = try balances.get(index); + return try env.createBigintUint64(balance); +} - pub fn getValidator(self: *const Self, index_arg: js.Number) !js.Value { - const env = js.env(); - const index_value: u64 = @intCast(try index_arg.toI64()); +/// Get a validator by index. +pub fn BeaconStateView_getValidator(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + const index: u64 = @intCast(try cb.arg(0).getValueInt64()); - var validators = try (try self.requireState()).state.validators(); - var validator_view = try validators.get(index_value); - var validator: ct.phase0.Validator.Type = undefined; - try validator_view.toValue(allocator, &validator); + var validators = try cached_state.state.validators(); + var validator_view = try validators.get(index); + var validator: ct.phase0.Validator.Type = undefined; + try validator_view.toValue(allocator, &validator); - return asJsValue(try sszValueToNapiValue(env, ct.phase0.Validator, &validator)); - } + return try sszValueToNapiValue(env, ct.phase0.Validator, &validator); +} - pub fn getValidatorStatus(self: *const Self, index_arg: js.Number) !js.String { - const cached_state = try self.requireState(); - const index_value: u64 = @intCast(try index_arg.toI64()); - const current_epoch = cached_state.epoch_cache.epoch; +/// Get the status of a validator by index. +/// Arguments: +/// - arg 0: validator index (number) +/// Returns: status string +pub fn BeaconStateView_getValidatorStatus(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + const index: u64 = @intCast(try cb.arg(0).getValueInt64()); + const current_epoch = cached_state.epoch_cache.epoch; + + var validators = try cached_state.state.validators(); + var validator_view = try validators.get(index); + var validator: ct.phase0.Validator.Type = undefined; + try validator_view.toValue(allocator, &validator); + + const status = st.getValidatorStatus(&validator, current_epoch); + return try env.createStringUtf8(status.toString()); +} - var validators = try cached_state.state.validators(); - var validator_view = try validators.get(index_value); - var validator: ct.phase0.Validator.Type = undefined; - try validator_view.toValue(allocator, &validator); +/// Get the total number of validators in the registry. +pub fn BeaconStateView_validatorCount(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + const count = try cached_state.state.validatorsCount(); + return try env.createInt64(@intCast(count)); +} - const status = st.getValidatorStatus(&validator, current_epoch); - return js.String.from(status.toString()); - } +/// Get the number of active validators at the current epoch. +pub fn BeaconStateView_activeValidatorCount(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + const epoch_cache = cached_state.epoch_cache; + const count = epoch_cache.current_shuffling.get().active_indices.len; + return try env.createInt64(@intCast(count)); +} - pub fn validatorCount(self: *const Self) !js.Number { - const count = try (try self.requireState()).state.validatorsCount(); - return js.Number.from(count); - } +pub fn BeaconStateView_isExecutionStateType(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + const fork_seq = cached_state.state.forkSeq(); + return try env.getBoolean(fork_seq.gte(.bellatrix)); +} - pub fn activeValidatorCount(self: *const Self) !js.Number { - const count = (try self.requireState()).epoch_cache.current_shuffling.get().active_indices.len; - return js.Number.from(count); - } +pub fn BeaconStateView_isExecutionEnabled(env: napi.Env, cb: napi.CallbackInfo(2)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + + // arg 0: fork name + var fork_name_buf: [16]u8 = undefined; + const fork_name = try cb.arg(0).getValueStringUtf8(&fork_name_buf); + const fork = c.ForkSeq.fromName(fork_name); + + // arg 1: signed block bytes + const bytes_info = try cb.arg(1).getTypedarrayInfo(); + + // Deserialize the signed block + const signed_block = try AnySignedBeaconBlock.deserialize( + allocator, + .full, + fork, + bytes_info.data, + ); + defer signed_block.deinit(allocator); + + if (signed_block.forkSeq() != cached_state.state.forkSeq()) { + try env.throwError("FORK_MISMATCH", "Fork of signed block does not match state fork"); + return env.getNull(); + } + const result = switch (cached_state.state.forkSeq()) { + inline else => |f| switch (signed_block.blockType()) { + inline else => |bt| if (comptime bt == .blinded and f.lt(.bellatrix)) { + return error.InvalidBlockTypeForFork; + } else st.isExecutionEnabled( + f, + cached_state.state.castToFork(f), + bt, + signed_block.beaconBlock().castToFork(bt, f), + ), + }, + }; + return try env.getBoolean(result); +} - pub fn isExecutionStateType(self: *const Self) !js.Boolean { - const fork_seq = (try self.requireState()).state.forkSeq(); - return js.Boolean.from(fork_seq.gte(.bellatrix)); - } +/// Check if the merge transition is complete. +/// Returns: boolean +pub fn BeaconStateView_isMergeTransitionComplete(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + const result = switch (cached_state.state.forkSeq()) { + inline else => |f| st.isMergeTransitionComplete(f, cached_state.state.castToFork(f)), + }; + return env.getBoolean(result); +} - pub fn isExecutionEnabled(self: *const Self, fork_name_value: js.String, signed_block_bytes: js.Uint8Array) !js.Value { - const env = js.env(); - const cached_state = try self.requireState(); +// pub fn BeaconStateView_getExpectedWithdrawals + +/// Get the proposer rewards for the state. +pub fn BeaconStateView_proposerRewards(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + const rewards = cached_state.getProposerRewards(); + + const obj = try env.createObject(); + try obj.setNamedProperty( + "attestations", + try env.createBigintUint64(rewards.attestations), + ); + try obj.setNamedProperty( + "syncAggregate", + try env.createBigintUint64(rewards.sync_aggregate), + ); + try obj.setNamedProperty( + "slashing", + try env.createBigintUint64(rewards.slashing), + ); + return obj; +} - var fork_name_buf: [16]u8 = undefined; - const fork_name = try fork_name_value.toSlice(&fork_name_buf); - const fork_seq = c.ForkSeq.fromName(fork_name); +// pub fn BeaconStateView_computeBlockRewards - const bytes = try signed_block_bytes.toSlice(); - const signed_block = try AnySignedBeaconBlock.deserialize( - allocator, - .full, - fork_seq, - bytes, - ); - defer signed_block.deinit(allocator); +// pub fn BeaconStateView_computeAttestationRewards - if (signed_block.forkSeq() != cached_state.state.forkSeq()) { - return throwNull("FORK_MISMATCH", "Fork of signed block does not match state fork"); - } +// pub fn BeaconStateView_computeSyncCommitteeRewards - const result = switch (cached_state.state.forkSeq()) { - inline else => |f| switch (signed_block.blockType()) { - inline else => |bt| if (comptime bt == .blinded and f.lt(.bellatrix)) { - return error.InvalidBlockTypeForFork; - } else st.isExecutionEnabled( - f, - cached_state.state.castToFork(f), - bt, - signed_block.beaconBlock().castToFork(bt, f), - ), - }, - }; - return asJsValue(try env.getBoolean(result)); - } +// pub fn BeaconStateView_getLatestWeakSubjectivityCheckpointEpoch - pub fn isMergeTransitionComplete(self: *const Self) !js.Boolean { - const cached_state = try self.requireState(); - const result = switch (cached_state.state.forkSeq()) { - inline else => |f| st.isMergeTransitionComplete(f, cached_state.state.castToFork(f)), - }; - return js.Boolean.from(result); - } +/// Get the validity status of a signed voluntary exit. +pub fn BeaconStateView_getVoluntaryExitValidity(env: napi.Env, cb: napi.CallbackInfo(2)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + const verify_signature = try cb.arg(1).getValueBool(); - pub fn proposerRewards(self: *const Self) !js.Value { - const env = js.env(); - const rewards = (try self.requireState()).getProposerRewards(); + const bytes_info = try cb.arg(0).getTypedarrayInfo(); - const obj = try env.createObject(); - try obj.setNamedProperty("attestations", try env.createBigintUint64(rewards.attestations)); - try obj.setNamedProperty("syncAggregate", try env.createBigintUint64(rewards.sync_aggregate)); - try obj.setNamedProperty("slashing", try env.createBigintUint64(rewards.slashing)); - return asJsValue(obj); - } + var signed_voluntary_exit: ct.phase0.SignedVoluntaryExit.Type = ct.phase0.SignedVoluntaryExit.default_value; + ct.phase0.SignedVoluntaryExit.deserializeFromBytes(bytes_info.data, &signed_voluntary_exit) catch { + try env.throwError("DESERIALIZE_ERROR", "Failed to deserialize SignedVoluntaryExit"); + return env.getNull(); + }; - pub fn getVoluntaryExitValidity(self: *const Self, signed_exit_bytes: js.Uint8Array, verify_signature_value: js.Boolean) !js.Value { - const env = js.env(); - const cached_state = try self.requireState(); - const verify_signature = verify_signature_value.assertBool(); - const bytes = try signed_exit_bytes.toSlice(); + const result = switch (cached_state.state.forkSeq()) { + inline else => |f| st.getVoluntaryExitValidity( + f, + cached_state.config, + cached_state.epoch_cache, + cached_state.state.castToFork(f), + &signed_voluntary_exit, + verify_signature, + ), + }; + const validity = result catch { + try env.throwError("VALIDATION_ERROR", "Failed to get voluntary exit validity"); + return env.getNull(); + }; - var signed_voluntary_exit: ct.phase0.SignedVoluntaryExit.Type = ct.phase0.SignedVoluntaryExit.default_value; - ct.phase0.SignedVoluntaryExit.deserializeFromBytes(bytes, &signed_voluntary_exit) catch { - return throwNull("DESERIALIZE_ERROR", "Failed to deserialize SignedVoluntaryExit"); - }; + return env.createStringUtf8(@tagName(validity)); +} - const result = switch (cached_state.state.forkSeq()) { - inline else => |f| st.getVoluntaryExitValidity( - f, - cached_state.config, - cached_state.epoch_cache, - cached_state.state.castToFork(f), - &signed_voluntary_exit, - verify_signature, - ), - }; - const validity = result catch { - return throwNull("VALIDATION_ERROR", "Failed to get voluntary exit validity"); - }; +/// Check if a signed voluntary exit is valid. +pub fn BeaconStateView_isValidVoluntaryExit(env: napi.Env, cb: napi.CallbackInfo(2)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + const verify_signature = try cb.arg(1).getValueBool(); + + const bytes_info = try cb.arg(0).getTypedarrayInfo(); + + var signed_voluntary_exit: ct.phase0.SignedVoluntaryExit.Type = ct.phase0.SignedVoluntaryExit.default_value; + ct.phase0.SignedVoluntaryExit.deserializeFromBytes(bytes_info.data, &signed_voluntary_exit) catch { + try env.throwError("DESERIALIZE_ERROR", "Failed to deserialize SignedVoluntaryExit"); + return env.getNull(); + }; + + const result = switch (cached_state.state.forkSeq()) { + inline else => |f| st.isValidVoluntaryExit( + f, + cached_state.config, + cached_state.epoch_cache, + cached_state.state.castToFork(f), + &signed_voluntary_exit, + verify_signature, + ), + }; + const is_valid = result catch { + try env.throwError("VALIDATION_ERROR", "Failed to validate voluntary exit"); + return env.getNull(); + }; + + return env.getBoolean(is_valid); +} - return asJsValue(try env.createStringUtf8(@tagName(validity))); - } +pub fn BeaconStateView_getFinalizedRootProof(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + var proof = try cached_state.state.getFinalizedRootProof(allocator); + defer proof.deinit(allocator); + + const witnesses = std.ArrayListUnmanaged([32]u8).fromOwnedSlice(proof.witnesses); + return try sszValueToNapiValue( + env, + // a compatible type for "a list of roots" + ct.phase0.HistoricalRoots, + &witnesses, + ); +} - pub fn isValidVoluntaryExit(self: *const Self, signed_exit_bytes: js.Uint8Array, verify_signature_value: js.Boolean) !js.Value { - const env = js.env(); - const cached_state = try self.requireState(); - const verify_signature = verify_signature_value.assertBool(); - const bytes = try signed_exit_bytes.toSlice(); +// pub fn BeaconStateView_getSyncCommitteesWitness - var signed_voluntary_exit: ct.phase0.SignedVoluntaryExit.Type = ct.phase0.SignedVoluntaryExit.default_value; - ct.phase0.SignedVoluntaryExit.deserializeFromBytes(bytes, &signed_voluntary_exit) catch { - return throwNull("DESERIALIZE_ERROR", "Failed to deserialize SignedVoluntaryExit"); - }; +/// Get a single Merkle proof for a node at the given generalized index. +pub fn BeaconStateView_getSingleProof(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + const gindex: u64 = @intCast(try cb.arg(0).getValueInt64()); - const result = switch (cached_state.state.forkSeq()) { - inline else => |f| st.isValidVoluntaryExit( - f, - cached_state.config, - cached_state.epoch_cache, - cached_state.state.castToFork(f), - &signed_voluntary_exit, - verify_signature, - ), - }; - const is_valid = result catch { - return throwNull("VALIDATION_ERROR", "Failed to validate voluntary exit"); - }; + var proof = cached_state.state.getSingleProof(allocator, gindex) catch { + try env.throwError("STATE_ERROR", "Failed to get single proof"); + return env.getNull(); + }; + defer proof.deinit(allocator); - return asJsValue(try env.getBoolean(is_valid)); + const result = try env.createArray(); + for (proof.witnesses, 0..) |witness, i| { + var witness_bytes: [*]u8 = undefined; + const witness_buf = try env.createArrayBuffer(32, &witness_bytes); + @memcpy(witness_bytes[0..32], &witness); + try result.setElement(@intCast(i), try env.createTypedarray(.uint8, 32, witness_buf, 0)); } - pub fn getFinalizedRootProof(self: *const Self) !js.Value { - const env = js.env(); - var proof = try (try self.requireState()).state.getFinalizedRootProof(allocator); - defer proof.deinit(allocator); + return result; +} - const witnesses = std.ArrayListUnmanaged([32]u8).fromOwnedSlice(proof.witnesses); - return asJsValue(try sszValueToNapiValue( - env, - ct.phase0.HistoricalRoots, - &witnesses, - )); +/// Create a compact multi-proof from a descriptor. +/// Arguments: +/// - arg 0: descriptor (Uint8Array) +/// Returns: {type: string, leaves: Uint8Array[], descriptor: Uint8Array} +pub fn BeaconStateView_createMultiProof(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { + const persistent_merkle_tree = @import("persistent_merkle_tree"); + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + + const descriptor_info = try cb.arg(0).getTypedarrayInfo(); + if (descriptor_info.array_type != .uint8) { + try env.throwTypeError("STATE_ERROR", "Expected descriptor to be a Uint8Array"); + return env.getNull(); + } + const descriptor = descriptor_info.data; + + // Get the root node from the state + try cached_state.state.commit(); + const root_node = switch (cached_state.state.*) { + inline else => |state| state.root, + }; + + // Create proof input for compact multi-proof + const proof_input = persistent_merkle_tree.proof.ProofInput{ + .compactMulti = .{ .descriptor = descriptor }, + }; + + var proof = persistent_merkle_tree.proof.createProof( + allocator, + &pool.state.pool, + root_node, + proof_input, + ) catch { + try env.throwError("STATE_ERROR", "Failed to create proof"); + return env.getNull(); + }; + defer proof.deinit(allocator); + + // Create result object matching Proof interface + const result = try env.createObject(); + + // Add type field + const proof_type_str = switch (proof) { + inline else => |_, tag| @tagName(tag), + }; + try result.setNamedProperty("type", try env.createStringUtf8(proof_type_str)); + + // Extract data based on proof type + switch (proof) { + .compactMulti => |compact| { + // Create leaves array + const leaves_array = try env.createArray(); + for (compact.leaves, 0..) |leaf, i| { + var leaf_bytes: [*]u8 = undefined; + const leaf_buf = try env.createArrayBuffer(32, &leaf_bytes); + @memcpy(leaf_bytes[0..32], &leaf); + try leaves_array.setElement(@intCast(i), try env.createTypedarray(.uint8, 32, leaf_buf, 0)); + } + try result.setNamedProperty("leaves", leaves_array); + + // Create descriptor Uint8Array + var descriptor_bytes: [*]u8 = undefined; + const descriptor_buf = try env.createArrayBuffer(compact.descriptor.len, &descriptor_bytes); + @memcpy(descriptor_bytes[0..compact.descriptor.len], compact.descriptor); + try result.setNamedProperty("descriptor", try env.createTypedarray(.uint8, compact.descriptor.len, descriptor_buf, 0)); + }, + else => { + try env.throwError("STATE_ERROR", "Unexpected proof type"); + return env.getNull(); + }, } - pub fn getSingleProof(self: *const Self, gindex_arg: js.Number) !js.Value { - const env = js.env(); - const gindex: u64 = @intCast(try gindex_arg.toI64()); - - var proof = (try self.requireState()).state.getSingleProof(allocator, gindex) catch { - return throwNull("STATE_ERROR", "Failed to get single proof"); - }; - defer proof.deinit(allocator); - - const result = try env.createArray(); - for (proof.witnesses, 0..) |witness, i| { - var witness_bytes: [*]u8 = undefined; - const witness_buf = try env.createArrayBuffer(32, &witness_bytes); - @memcpy(witness_bytes[0..32], &witness); - try result.setElement(@intCast(i), try env.createTypedarray(.uint8, 32, witness_buf, 0)); - } + return result; +} - return asJsValue(result); - } +pub fn BeaconStateView_computeUnrealizedCheckpoints(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + const result = try st.computeUnrealizedCheckpoints(cached_state, allocator); + + const obj = try env.createObject(); + try obj.setNamedProperty( + "justifiedCheckpoint", + try sszValueToNapiValue(env, ct.phase0.Checkpoint, &result.justified_checkpoint), + ); + try obj.setNamedProperty( + "finalizedCheckpoint", + try sszValueToNapiValue(env, ct.phase0.Checkpoint, &result.finalized_checkpoint), + ); + return obj; +} - pub fn createMultiProof(self: *const Self, descriptor: js.Uint8Array) !js.Value { - const persistent_merkle_tree = @import("persistent_merkle_tree"); - const env = js.env(); - const cached_state = try self.requireState(); - const descriptor_bytes = try descriptor.toSlice(); +pub fn BeaconStateView_clonedCount(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + return try env.createInt64(@intCast(cached_state.cloned_count)); +} - try cached_state.state.commit(); - const root_node = switch (cached_state.state.*) { - inline else => |state| state.root, - }; +pub fn BeaconStateView_clonedCountWithTransferCache(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + return try env.createInt64(@intCast(cached_state.cloned_count_with_transfer_cache)); +} - const proof_input = persistent_merkle_tree.proof.ProofInput{ - .compactMulti = .{ .descriptor = descriptor_bytes }, - }; +pub fn BeaconStateView_createdWithTransferCache(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + return try env.getBoolean(cached_state.created_with_transfer_cache); +} - var proof = persistent_merkle_tree.proof.createProof( - allocator, - &pool.state.pool, - root_node, - proof_input, - ) catch { - return throwNull("STATE_ERROR", "Failed to create proof"); - }; - defer proof.deinit(allocator); +// pub fn BeaconStateView_isStateValidatorsNodesPopulated - const result = try env.createObject(); - const proof_type_str = switch (proof) { - inline else => |_, tag| @tagName(tag), - }; - try result.setNamedProperty("type", try env.createStringUtf8(proof_type_str)); - - switch (proof) { - .compactMulti => |compact| { - const leaves_array = try env.createArray(); - for (compact.leaves, 0..) |leaf, i| { - var leaf_bytes: [*]u8 = undefined; - const leaf_buf = try env.createArrayBuffer(32, &leaf_bytes); - @memcpy(leaf_bytes[0..32], &leaf); - try leaves_array.setElement(@intCast(i), try env.createTypedarray(.uint8, 32, leaf_buf, 0)); - } - try result.setNamedProperty("leaves", leaves_array); - - var descriptor_buf_bytes: [*]u8 = undefined; - const descriptor_buf = try env.createArrayBuffer(compact.descriptor.len, &descriptor_buf_bytes); - @memcpy(descriptor_buf_bytes[0..compact.descriptor.len], compact.descriptor); - try result.setNamedProperty( - "descriptor", - try env.createTypedarray(.uint8, compact.descriptor.len, descriptor_buf, 0), - ); - }, - else => return throwNull("STATE_ERROR", "Unexpected proof type"), - } +// pub fn BeaconStateView_loadOtherState - return asJsValue(result); - } +pub fn BeaconStateView_serialize(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + const result = try cached_state.state.serialize(allocator); + defer allocator.free(result); - pub fn computeUnrealizedCheckpoints(self: *const Self) !js.Value { - const env = js.env(); - const result = try st.computeUnrealizedCheckpoints(try self.requireState(), allocator); + return try numberSliceToNapiValue(env, u8, result, .{ .typed_array = .uint8 }); +} - const obj = try env.createObject(); - try obj.setNamedProperty( - "justifiedCheckpoint", - try sszValueToNapiValue(env, ct.phase0.Checkpoint, &result.justified_checkpoint), - ); - try obj.setNamedProperty( - "finalizedCheckpoint", - try sszValueToNapiValue(env, ct.phase0.Checkpoint, &result.finalized_checkpoint), - ); - return asJsValue(obj); - } +pub fn BeaconStateView_serializedSize(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + const size = switch (cached_state.state.*) { + inline else => |state| try state.serializedSize(), + }; + return try env.createInt64(@intCast(size)); +} - pub fn clonedCount(self: *const Self) !js.Number { - return js.Number.from((try self.requireState()).cloned_count); - } +/// arg 0: output: preallocated Uint8Array buffer +/// arg 1: offset: offset of buffer where serialization should start +pub fn BeaconStateView_serializeToBytes(env: napi.Env, cb: napi.CallbackInfo(2)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - pub fn clonedCountWithTransferCache(self: *const Self) !js.Number { - return js.Number.from((try self.requireState()).cloned_count_with_transfer_cache); + const output_info = try cb.arg(0).getTypedarrayInfo(); + if (output_info.array_type != .uint8) { + return error.InvalidOutputBufferType; } - pub fn createdWithTransferCache(self: *const Self) !js.Boolean { - return js.Boolean.from((try self.requireState()).created_with_transfer_cache); + const offset = try cb.arg(1).getValueUint32(); + if (offset > output_info.length) { + return error.InvalidOffset; } - pub fn serialize(self: *const Self) !js.Uint8Array { - const env = js.env(); - const result = try (try self.requireState()).state.serialize(allocator); - defer allocator.free(result); - return .{ .val = try numberSliceToNapiValue(env, u8, result, .{ .typed_array = .uint8 }) }; - } + const output_slice = output_info.data[offset..]; + const bytes_written = switch (cached_state.state.*) { + inline else => |state| try state.serializeIntoBytes(output_slice), + }; - pub fn serializedSize(self: *const Self) !js.Number { - const cached_state = try self.requireState(); - const size = switch (cached_state.state.*) { - inline else => |state| try state.serializedSize(), - }; - return js.Number.from(size); - } + return try env.createInt64(@intCast(bytes_written)); +} - pub fn serializeToBytes(self: *const Self, output: js.Uint8Array, offset: js.Number) !js.Number { - const output_slice = try output.toSlice(); - const off = try offset.toU32(); - if (off > output_slice.len) return error.InvalidOffset; +pub fn BeaconStateView_serializeValidators(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + var validators_view = try cached_state.state.validators(); - const cached_state = try self.requireState(); - const bytes_written = switch (cached_state.state.*) { - inline else => |state| try state.serializeIntoBytes(output_slice[off..]), - }; + const size = try validators_view.serializedSize(); + var arraybuffer_bytes: [*]u8 = undefined; + const arraybuffer = try env.createArrayBuffer(size, &arraybuffer_bytes); + _ = try validators_view.serializeIntoBytes(arraybuffer_bytes[0..size]); + return try env.createTypedarray(.uint8, size, arraybuffer, 0); +} - return js.Number.from(bytes_written); - } +pub fn BeaconStateView_serializedValidatorsSize(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + var validators_view = try cached_state.state.validators(); + const size = try validators_view.serializedSize(); + return try env.createInt64(@intCast(size)); +} - pub fn serializeValidators(self: *const Self) !js.Uint8Array { - const env = js.env(); - var validators_view = try (try self.requireState()).state.validators(); +pub fn BeaconStateView_serializeValidatorsToBytes(env: napi.Env, cb: napi.CallbackInfo(2)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); - const size = try validators_view.serializedSize(); - var arraybuffer_bytes: [*]u8 = undefined; - const arraybuffer = try env.createArrayBuffer(size, &arraybuffer_bytes); - _ = try validators_view.serializeIntoBytes(arraybuffer_bytes[0..size]); - return .{ .val = try env.createTypedarray(.uint8, size, arraybuffer, 0) }; + // arg 0: output + const output_info = try cb.arg(0).getTypedarrayInfo(); + if (output_info.array_type != .uint8) { + return error.InvalidOutputBufferType; } - pub fn serializedValidatorsSize(self: *const Self) !js.Number { - var validators_view = try (try self.requireState()).state.validators(); - const size = try validators_view.serializedSize(); - return js.Number.from(size); + // arg 1: offset + const offset = try cb.arg(1).getValueUint32(); + if (offset > output_info.length) { + return error.InvalidOffset; } - pub fn serializeValidatorsToBytes(self: *const Self, output: js.Uint8Array, offset: js.Number) !js.Number { - const output_slice = try output.toSlice(); - const off = try offset.toU32(); - if (off > output_slice.len) return error.InvalidOffset; + var validators_view = try cached_state.state.validators(); + const output_slice = output_info.data[offset..]; + const bytes_written = try validators_view.serializeIntoBytes(output_slice); + return try env.createInt64(@intCast(bytes_written)); +} - var validators_view = try (try self.requireState()).state.validators(); - const bytes_written = try validators_view.serializeIntoBytes(output_slice[off..]); - return js.Number.from(bytes_written); - } +pub fn BeaconStateView_hashTreeRoot(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + const root = try cached_state.state.hashTreeRoot(); + return try numberSliceToNapiValue(env, u8, root, .{ .typed_array = .uint8 }); +} - pub fn hashTreeRoot(self: *const Self) !js.Uint8Array { - const env = js.env(); - const root = try (try self.requireState()).state.hashTreeRoot(); - return .{ .val = try numberSliceToNapiValue(env, u8, root, .{ .typed_array = .uint8 }) }; +// pub fn BeaconStateView_stateTransition + +/// Process slots from current state slot to target slot, returning a new BeaconStateView. +/// +/// Arguments: +/// - arg 0: target slot (number) +/// - arg 1: options object (optional) with `transferCache` boolean +pub fn BeaconStateView_processSlots(env: napi.Env, cb: napi.CallbackInfo(2)) !napi.Value { + const cached_state = try env.unwrap(CachedBeaconState, cb.this()); + const slot: u64 = @intCast(try cb.arg(0).getValueInt64()); + + var transfer_cache = false; + if (cb.getArg(1)) |options_arg| { + if (try options_arg.typeof() == .object) { + if (try options_arg.hasNamedProperty("transferCache")) { + transfer_cache = try (try options_arg.getNamedProperty("transferCache")).getValueBool(); + std.debug.print("has transferCache: {}\n", .{transfer_cache}); + } + } + } + const post_state = try cached_state.clone(allocator, .{ .transfer_cache = transfer_cache }); + errdefer { + post_state.deinit(); + allocator.destroy(post_state); } - pub fn processSlots(self: *const Self, slot_arg: js.Number, options: ?js.Value) !Self { - const slot_value: u64 = @intCast(try slot_arg.toI64()); - const transfer_cache = try optionalBool(options, "transferCache", false); - const post_state = try (try self.requireState()).clone(allocator, .{ .transfer_cache = transfer_cache }); - errdefer { - post_state.deinit(); - allocator.destroy(post_state); - } + try st.processSlots(allocator, post_state, slot, .{}); - try st.processSlots(allocator, post_state, slot_value, .{}); - return .{ .cached_state = post_state }; - } -}; + const ctor = try cb.this().getNamedProperty("constructor"); + const new_state_value = try env.newInstance(ctor, .{}); + const dummy_state = try env.unwrap(CachedBeaconState, new_state_value); + dummy_state.* = post_state.*; + allocator.destroy(post_state); + + return new_state_value; +} + +pub fn register(env: napi.Env, exports: napi.Value) !void { + const beacon_state_view_ctor = try env.defineClass( + "BeaconStateView", + 0, + BeaconStateView_ctor, + null, + &[_]napi.c.napi_property_descriptor{ + getter(BeaconStateView_slot), + getter(BeaconStateView_fork), + getter(BeaconStateView_epoch), + getter(BeaconStateView_genesisTime), + getter(BeaconStateView_genesisValidatorsRoot), + getter(BeaconStateView_eth1Data), + getter(BeaconStateView_latestBlockHeader), + getter(BeaconStateView_previousJustifiedCheckpoint), + getter(BeaconStateView_currentJustifiedCheckpoint), + getter(BeaconStateView_finalizedCheckpoint), + method(1, BeaconStateView_getBlockRoot), + method(1, BeaconStateView_getRandaoMix), + getter(BeaconStateView_previousEpochParticipation), + getter(BeaconStateView_currentEpochParticipation), + getter(BeaconStateView_latestExecutionPayloadHeader), + getter(BeaconStateView_historicalSummaries), + getter(BeaconStateView_pendingDeposits), + getter(BeaconStateView_pendingDepositsCount), + getter(BeaconStateView_pendingPartialWithdrawals), + getter(BeaconStateView_pendingPartialWithdrawalsCount), + getter(BeaconStateView_pendingConsolidations), + getter(BeaconStateView_pendingConsolidationsCount), + getter(BeaconStateView_proposerLookahead), + // getter(BeaconStateView_executionPayloadAvailability), + + // method(1, BeaconStateView_getShufflingAtEpoch), + getter(BeaconStateView_previousDecisionRoot), + getter(BeaconStateView_currentDecisionRoot), + getter(BeaconStateView_nextDecisionRoot), + method(1, BeaconStateView_getShufflingDecisionRoot), + getter(BeaconStateView_currentProposers), + getter(BeaconStateView_nextProposers), + getter(BeaconStateView_previousProposers), + method(1, BeaconStateView_getBeaconProposer), + getter(BeaconStateView_currentSyncCommittee), + getter(BeaconStateView_nextSyncCommittee), + getter(BeaconStateView_currentSyncCommitteeIndexed), + getter(BeaconStateView_syncProposerReward), + method(1, BeaconStateView_getIndexedSyncCommitteeAtEpoch), + + getter(BeaconStateView_effectiveBalanceIncrements), + method(0, BeaconStateView_getEffectiveBalanceIncrementsZeroInactive), + method(1, BeaconStateView_getBalance), + method(1, BeaconStateView_getValidator), + method(1, BeaconStateView_getValidatorStatus), + getter(BeaconStateView_validatorCount), + getter(BeaconStateView_activeValidatorCount), + + getter(BeaconStateView_isMergeTransitionComplete), + getter(BeaconStateView_isExecutionStateType), + method(2, BeaconStateView_isExecutionEnabled), + + // method(0, BeaconStateView_getExpectedWithdrawals), + + getter(BeaconStateView_proposerRewards), + // method(2, BeaconStateView_computeBlockRewards), + // method(2, BeaconStateView_computeAttestationRewards), + // method(2, BeaconStateView_computeSyncCommitteeRewards), + // method(0, BeaconStateView_getLatestWeakSubjectivityCheckpointEpoch), + + method(2, BeaconStateView_getVoluntaryExitValidity), + method(2, BeaconStateView_isValidVoluntaryExit), + + method(0, BeaconStateView_getFinalizedRootProof), + // method(1, BeaconStateView_getSyncCommitteesWitness), + method(1, BeaconStateView_getSingleProof), + method(1, BeaconStateView_createMultiProof), + + method(0, BeaconStateView_computeUnrealizedCheckpoints), + + getter(BeaconStateView_clonedCount), + getter(BeaconStateView_clonedCountWithTransferCache), + getter(BeaconStateView_createdWithTransferCache), + // getter(BeaconStateView_isStateValidatorsNodesPopulated), + + // method(2, BeaconStateView_loadOtherState), + method(0, BeaconStateView_serialize), + method(0, BeaconStateView_serializedSize), + method(2, BeaconStateView_serializeToBytes), + method(0, BeaconStateView_serializeValidators), + method(0, BeaconStateView_serializedValidatorsSize), + method(2, BeaconStateView_serializeValidatorsToBytes), + + method(0, BeaconStateView_hashTreeRoot), + + // method(2, BeaconStateView_stateTransition), + method(2, BeaconStateView_processSlots), + }, + ); + // Static method on constructor + try beacon_state_view_ctor.defineProperties(&[_]napi.c.napi_property_descriptor{.{ + .utf8name = "createFromBytes", + .method = napi.wrapCallback(1, BeaconStateView_createFromBytes), + }}); + + try exports.setNamedProperty("BeaconStateView", beacon_state_view_ctor); +} diff --git a/bindings/napi/blst.zig b/bindings/napi/blst.zig index d8acc3251..f2e3faa84 100644 --- a/bindings/napi/blst.zig +++ b/bindings/napi/blst.zig @@ -1,29 +1,53 @@ -//! Contains the necessary bindings for blst operations in lodestar-ts. +//! NAPI bindings for BLS (blst) cryptographic operations used by lodestar. +//! +//! This module uses a **Zig ThreadPool** (`thread_pool`) — a fixed-size pool of OS threads +//! initialized once via `initThreadPool`. Used by synchronous NAPI functions (`aggregateVerify`, +//! `fastAggregateVerify`, `verifyMultipleAggregateSignatures`) to fan out pairing checks +//! across worker threads. The call still blocks the JS thread while it waits for the pool +//! to finish, but the crypto work itself is parallelized. +//! +//! `aggregateWithRandomness` runs synchronously on the calling thread and does not +//! rely on the native `thread_pool`. In lodestar, this is called from a Node.js +//! worker thread (BLS thread pool), not the main thread. const std = @import("std"); -const zapi = @import("zapi"); -const js = zapi.js; -const napi = zapi.napi; +const napi = @import("zapi").napi; const bls = @import("bls"); const builtin = @import("builtin"); +const getter = @import("napi_property_descriptor.zig").getter; +const method = @import("napi_property_descriptor.zig").method; -const NativePublicKey = bls.PublicKey; -const NativeSignature = bls.Signature; -const NativeSecretKey = bls.SecretKey; +const PublicKey = bls.PublicKey; +const Signature = bls.Signature; +const SecretKey = bls.SecretKey; const Pairing = bls.Pairing; const AggregatePublicKey = bls.AggregatePublicKey; const AggregateSignature = bls.AggregateSignature; +const ThreadPool = bls.ThreadPool; const DST = bls.DST; -const VerifySet = struct { - msg: js.Value, - pk: js.Value, - sig: js.Value, -}; +/// Cached thread pool reference for parallel verification. +/// Initialized lazily on first use, torn down via `deinitThreadPool`. +var thread_pool: ?*ThreadPool = null; -const AsyncAggregateSet = struct { - pk: js.Value, - sig: js.Value, -}; +pub fn initThreadPool(n_workers: u16) !void { + if (thread_pool != null) return error.PoolExists; + thread_pool = try ThreadPool.init(std.heap.page_allocator, .{ .n_workers = n_workers }); +} + +/// Closes the `ThreadPool` used for blst operations. +/// +/// Note: this can invalidate any inflight verification requests. Consumer is responsible +/// for the lifecycle of their program and should only call this when all work is done. +/// +/// This note is however application dependent. For the use case of lodestar, +/// it's likely that this would not be called at all. +/// Same goes for any other long-lived processes. +pub fn deinitThreadPool() void { + if (thread_pool) |p| { + p.deinit(); + thread_pool = null; + } +} var gpa: std.heap.DebugAllocator(.{}) = .init; const allocator = if (builtin.mode == .Debug) @@ -31,327 +55,637 @@ const allocator = if (builtin.mode == .Debug) else std.heap.c_allocator; -fn boolOrDefault(value: ?js.Boolean, default: bool) bool { - return if (value) |v| v.assertBool() else default; +/// Per-context (per-thread) instance data for constructor references +const InstanceData = struct { + public_key_ctor_ref: ?napi.c.napi_ref = null, + signature_ctor_ref: ?napi.c.napi_ref = null, + + fn init(env: napi.Env) !*InstanceData { + const self = try allocator.create(InstanceData); + errdefer allocator.destroy(self); + + self.* = .{}; + try napi.status.check(napi.c.napi_set_instance_data( + env.env, + @ptrCast(self), + InstanceData.finalize, + null, + )); + return self; + } + + fn finalize(env: napi.c.napi_env, data: ?*anyopaque, _: ?*anyopaque) callconv(.C) void { + const self: *InstanceData = @ptrCast(@alignCast(data orelse return)); + self.clearRefs(env); + allocator.destroy(self); + } + + fn get(env: napi.Env) !*InstanceData { + var raw: ?*anyopaque = null; + try napi.status.check(napi.c.napi_get_instance_data(env.env, &raw)); + return @ptrCast(@alignCast(raw orelse return error.InstanceDataNotInitialized)); + } + + fn clearRefs(self: *InstanceData, env: napi.c.napi_env) void { + if (self.public_key_ctor_ref) |ref| { + napi.status.check(napi.c.napi_delete_reference(env, ref)) catch {}; + self.public_key_ctor_ref = null; + } + if (self.signature_ctor_ref) |ref| { + napi.status.check(napi.c.napi_delete_reference(env, ref)) catch {}; + self.signature_ctor_ref = null; + } + } +}; + +fn setRef(env: napi.Env, ctor: napi.Value, slot: *?napi.c.napi_ref) !void { + if (slot.*) |ref| { + try napi.status.check(napi.c.napi_delete_reference(env.env, ref)); + } + + var ref: napi.c.napi_ref = undefined; + try napi.status.check(napi.c.napi_create_reference(env.env, ctor.value, 1, &ref)); + slot.* = ref; } -fn hexFromString(hex_string: js.String, buf: []u8) ![]const u8 { - const full = try hex_string.toSlice(buf); - return if (full.len >= 2 and full[0] == '0' and full[1] == 'x') full[2..] else full; +fn getFromRef(env: napi.Env, slot: ?napi.c.napi_ref) !napi.Value { + const ref_ = slot orelse return error.RefNotInitialized; + + var value: napi.c.napi_value = undefined; + try napi.status.check(napi.c.napi_get_reference_value(env.env, ref_, &value)); + return .{ + .env = env.env, + .value = value, + }; } -fn formatHex(bytes: []const u8, buf: []u8) !js.String { - const written = try std.fmt.bufPrint(buf, "0x{x}", .{std.fmt.fmtSliceHexLower(bytes)}); - return js.String.from(written); +pub fn newPublicKeyInstance(env: napi.Env) !napi.Value { + const state = try InstanceData.get(env); + const ctor = try getFromRef(env, state.public_key_ctor_ref); + return try env.newInstance(ctor, .{}); } -fn unwrapClass(comptime T: type, value: js.Value) !*T { - return js.env().unwrap(T, value.toValue()); +pub fn newSignatureInstance(env: napi.Env) !napi.Value { + const state = try InstanceData.get(env); + const ctor = try getFromRef(env, state.signature_ctor_ref); + return try env.newInstance(ctor, .{}); } -pub const PublicKey = struct { - pub const js_meta = js.class(.{}); +fn coerceToBool(boolish: napi.Value) napi.status.NapiError!bool { + const b = try boolish.coerceToBool(); + return b.getValueBool(); +} - raw: NativePublicKey = .{}, +pub fn PublicKey_finalize(_: napi.Env, pk: *PublicKey, _: ?*anyopaque) void { + allocator.destroy(pk); +} - pub fn init() PublicKey { - return .{}; - } +pub fn PublicKey_ctor(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { + const pk = try allocator.create(PublicKey); + errdefer allocator.destroy(pk); + + _ = try env.wrap( + cb.this(), + PublicKey, + pk, + PublicKey_finalize, + null, // finalize hint + null, // ref + ); + return cb.this(); +} - pub fn fromBytes(bytes: js.Uint8Array, pk_validate: ?js.Boolean) !PublicKey { - const slice = try bytes.toSlice(); - var pk = try NativePublicKey.deserialize(slice); - if (boolOrDefault(pk_validate, false)) { - try pk.validate(); - } - return .{ .raw = pk }; - } +/// Converts given array of bytes to a `PublicKey`. +/// 1) bytes: Uint8Array +/// 2) pk_validate: ?bool +pub fn PublicKey_fromBytes(env: napi.Env, cb: napi.CallbackInfo(2)) !napi.Value { + const ctor = cb.this(); + const bytes_info = try cb.arg(0).getTypedarrayInfo(); + const pk_validate: bool = if (cb.getArg(1)) |sgc| + try coerceToBool(sgc) + else + false; - pub fn fromHex(hex_string: js.String, pk_validate: ?js.Boolean) !PublicKey { - var hex_buf: [NativePublicKey.SERIALIZE_SIZE * 2 + 2]u8 = undefined; - const hex = try hexFromString(hex_string, &hex_buf); + const pk_value = try env.newInstance(ctor, .{}); + const pk = try env.unwrap(PublicKey, pk_value); - var bytes_buf: [NativePublicKey.SERIALIZE_SIZE]u8 = undefined; - const bytes = try std.fmt.hexToBytes(&bytes_buf, hex); + pk.* = try PublicKey.deserialize(bytes_info.data[0..]); - var pk = try NativePublicKey.deserialize(bytes); - if (boolOrDefault(pk_validate, false)) { - try pk.validate(); - } - return .{ .raw = pk }; + if (pk_validate) { + try pk.validate(); } - pub fn validate(self: *const PublicKey) !void { - try self.raw.validate(); - } + return pk_value; +} - pub fn toBytes(self: *const PublicKey, compress: ?js.Boolean) js.Uint8Array { - if (boolOrDefault(compress, true)) { - const bytes = self.raw.compress(); - return js.Uint8Array.from(bytes[0..]); - } +/// Converts given hex string to a `PublicKey`. +/// +/// 1) bytes: Uint8Array +/// 2) pk_validate: ?bool +pub fn PublicKey_fromHex(env: napi.Env, cb: napi.CallbackInfo(2)) !napi.Value { + const ctor = cb.this(); + var hex_buf: [PublicKey.SERIALIZE_SIZE * 2 + 2]u8 = undefined; + const hex = try hexFromValue(cb.arg(0), &hex_buf); + const pk_validate: bool = if (cb.getArg(1)) |sgc| + try coerceToBool(sgc) + else + false; + + const pk_value = try env.newInstance(ctor, .{}); + const pk = try env.unwrap(PublicKey, pk_value); + + var buf: [PublicKey.SERIALIZE_SIZE]u8 = undefined; + const bytes = try std.fmt.hexToBytes(&buf, hex); + pk.* = try PublicKey.deserialize(bytes); + + if (pk_validate) try pk.validate(); + + return pk_value; +} - const bytes = self.raw.serialize(); - return js.Uint8Array.from(bytes[0..]); - } +/// Converts given array of bytes to a `PublicKey`. +pub fn PublicKey_validate(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { + const pk = try env.unwrap(PublicKey, cb.this()); + try pk.validate(); - pub fn toHex(self: *const PublicKey, compress: ?js.Boolean) !js.String { - var hex_buf: [NativePublicKey.SERIALIZE_SIZE * 2 + 2]u8 = undefined; - if (boolOrDefault(compress, true)) { - const bytes = self.raw.compress(); - return formatHex(bytes[0..], &hex_buf); - } + return try env.getUndefined(); +} - const bytes = self.raw.serialize(); - return formatHex(bytes[0..], &hex_buf); - } -}; +/// Serializes this public key to bytes. +pub fn PublicKey_toBytes(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { + const pk = try env.unwrap(PublicKey, cb.this()); + const compress = try if (cb.getArg(0)) |c| coerceToBool(c) else true; -pub const Signature = struct { - pub const js_meta = js.class(.{}); + if (compress) { + const bytes = pk.compress(); - raw: NativeSignature = .{}, + var arraybuffer_bytes: [*]u8 = undefined; + const arraybuffer = try env.createArrayBuffer(PublicKey.COMPRESS_SIZE, &arraybuffer_bytes); + @memcpy(arraybuffer_bytes[0..PublicKey.COMPRESS_SIZE], &bytes); + return try env.createTypedarray(.uint8, PublicKey.COMPRESS_SIZE, arraybuffer, 0); + } else { + const bytes = pk.serialize(); - pub fn init() Signature { - return .{}; + var arraybuffer_bytes: [*]u8 = undefined; + const arraybuffer = try env.createArrayBuffer(PublicKey.SERIALIZE_SIZE, &arraybuffer_bytes); + @memcpy(arraybuffer_bytes[0..PublicKey.SERIALIZE_SIZE], &bytes); + return try env.createTypedarray(.uint8, PublicKey.SERIALIZE_SIZE, arraybuffer, 0); } +} - pub fn fromBytes(bytes: js.Uint8Array, sig_validate: ?js.Boolean, sig_infcheck: ?js.Boolean) !Signature { - const slice = try bytes.toSlice(); - var sig = NativeSignature.deserialize(slice) catch return error.DeserializationFailed; +pub fn PublicKey_toHex(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { + const pk = try env.unwrap(PublicKey, cb.this()); + const compress = try if (cb.getArg(0)) |c| coerceToBool(c) else true; - if (boolOrDefault(sig_validate, false)) { - try sig.validate(boolOrDefault(sig_infcheck, false)); - } + if (compress) { + const bytes = pk.compress(); - return .{ .raw = sig }; + const hex = try std.fmt.allocPrint(allocator, "0x{x}", .{std.fmt.fmtSliceHexLower(&bytes)}); + defer allocator.free(hex); + + return try env.createStringUtf8(hex); + } else { + const bytes = pk.serialize(); + + const hex = try std.fmt.allocPrint(allocator, "0x{x}", .{std.fmt.fmtSliceHexLower(&bytes)}); + defer allocator.free(hex); + + return try env.createStringUtf8(hex); } +} + +pub fn Signature_finalize(_: napi.Env, sig: *Signature, _: ?*anyopaque) void { + allocator.destroy(sig); +} + +pub fn Signature_ctor(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { + const sig = try allocator.create(Signature); + errdefer allocator.destroy(sig); + + _ = try env.wrap( + cb.this(), + Signature, + sig, + Signature_finalize, + null, // finalize hint + null, // ref + ); + return cb.this(); +} - pub fn fromHex(hex_string: js.String, sig_validate: ?js.Boolean, sig_infcheck: ?js.Boolean) !Signature { - var hex_buf: [NativeSignature.SERIALIZE_SIZE * 2 + 2]u8 = undefined; - const hex = try hexFromString(hex_string, &hex_buf); +/// Converts given array of bytes to a `Signature`. +pub fn Signature_fromBytes(env: napi.Env, cb: napi.CallbackInfo(3)) !napi.Value { + const ctor = cb.this(); + const bytes_info = try cb.arg(0).getTypedarrayInfo(); + const sig_validate: bool = if (cb.getArg(1)) |sgc| + try coerceToBool(sgc) + else + false; + const sig_infcheck: bool = if (cb.getArg(2)) |v| + try coerceToBool(v) + else + false; - var bytes_buf: [NativeSignature.SERIALIZE_SIZE]u8 = undefined; - const bytes = try std.fmt.hexToBytes(&bytes_buf, hex); + const sig_value = try env.newInstance(ctor, .{}); + const sig = try env.unwrap(Signature, sig_value); - var sig = NativeSignature.deserialize(bytes) catch return error.DeserializationFailed; - if (boolOrDefault(sig_validate, false)) { - try sig.validate(boolOrDefault(sig_infcheck, false)); - } + sig.* = Signature.deserialize(bytes_info.data[0..]) catch return error.DeserializationFailed; - return .{ .raw = sig }; + if (sig_validate) { + try sig.validate(sig_infcheck); } - pub fn aggregate(signatures: js.Array, sigs_groupcheck: ?js.Boolean) !Signature { - const signatures_len = try signatures.length(); - if (signatures_len == 0) return error.EmptySignatureArray; + return sig_value; +} - const sigs = try allocator.alloc(NativeSignature, signatures_len); - defer allocator.free(sigs); +/// Converts given hex string to a `Signature`. +/// +/// If `sig_validate` is `true`, the public key will be infinity and group checked. +/// If `sig_infcheck` is `false`, the infinity check will be skipped. +pub fn Signature_fromHex(env: napi.Env, cb: napi.CallbackInfo(3)) !napi.Value { + const ctor = cb.this(); + + var hex_buf: [Signature.SERIALIZE_SIZE * 2 + 2]u8 = undefined; + const hex = try hexFromValue(cb.arg(0), &hex_buf); + const sig_validate: bool = if (cb.getArg(1)) |sgc| + try coerceToBool(sgc) + else + false; + const sig_infcheck: bool = if (cb.getArg(2)) |v| + try coerceToBool(v) + else + false; + + const sig_value = try env.newInstance(ctor, .{}); + const sig = try env.unwrap(Signature, sig_value); + + var buf: [Signature.SERIALIZE_SIZE]u8 = undefined; + const bytes = try std.fmt.hexToBytes(&buf, hex); + sig.* = Signature.deserialize(bytes) catch return error.DeserializationFailed; + + if (sig_validate) try sig.validate(sig_infcheck); + + return sig_value; +} - for (0..signatures_len) |i| { - const wrapped = try unwrapClass(Signature, try signatures.get(@intCast(i))); - sigs[i] = wrapped.raw; - } +/// Serializes this signature to bytes. +pub fn Signature_toBytes(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { + const sig = try env.unwrap(Signature, cb.this()); + const compress = try if (cb.getArg(0)) |c| coerceToBool(c) else true; - const agg_sig = AggregateSignature.aggregate(sigs, boolOrDefault(sigs_groupcheck, false)) catch { - return error.AggregationFailed; - }; - return .{ .raw = agg_sig.toSignature() }; - } + if (compress) { + const bytes = sig.compress(); - pub fn toBytes(self: *const Signature, compress: ?js.Boolean) js.Uint8Array { - if (boolOrDefault(compress, true)) { - const bytes = self.raw.compress(); - return js.Uint8Array.from(bytes[0..]); - } + var arraybuffer_bytes: [*]u8 = undefined; + const arraybuffer = try env.createArrayBuffer(Signature.COMPRESS_SIZE, &arraybuffer_bytes); + @memcpy(arraybuffer_bytes[0..Signature.COMPRESS_SIZE], &bytes); + return try env.createTypedarray(.uint8, Signature.COMPRESS_SIZE, arraybuffer, 0); + } else { + const bytes = sig.serialize(); - const bytes = self.raw.serialize(); - return js.Uint8Array.from(bytes[0..]); + var arraybuffer_bytes: [*]u8 = undefined; + const arraybuffer = try env.createArrayBuffer(Signature.SERIALIZE_SIZE, &arraybuffer_bytes); + @memcpy(arraybuffer_bytes[0..Signature.SERIALIZE_SIZE], &bytes); + return try env.createTypedarray(.uint8, Signature.SERIALIZE_SIZE, arraybuffer, 0); } +} - pub fn toHex(self: *const Signature, compress: ?js.Boolean) !js.String { - var hex_buf: [NativeSignature.SERIALIZE_SIZE * 2 + 2]u8 = undefined; - if (boolOrDefault(compress, true)) { - const bytes = self.raw.compress(); - return formatHex(bytes[0..], &hex_buf); - } +pub fn Signature_toHex(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { + const sig = try env.unwrap(Signature, cb.this()); + const compress = try if (cb.getArg(0)) |c| coerceToBool(c) else true; - const bytes = self.raw.serialize(); - return formatHex(bytes[0..], &hex_buf); - } + if (compress) { + const bytes = sig.compress(); - pub fn validate(self: *const Signature, sig_infcheck: js.Boolean) !void { - self.raw.validate(sig_infcheck.assertBool()) catch return error.InvalidSignature; - } -}; + const hex = try std.fmt.allocPrint(allocator, "0x{x}", .{std.fmt.fmtSliceHexLower(&bytes)}); + defer allocator.free(hex); -pub const SecretKey = struct { - pub const js_meta = js.class(.{}); + return try env.createStringUtf8(hex); + } else { + const bytes = sig.serialize(); - raw: NativeSecretKey = .{}, + const hex = try std.fmt.allocPrint(allocator, "0x{x}", .{std.fmt.fmtSliceHexLower(&bytes)}); + defer allocator.free(hex); - pub fn init() SecretKey { - return .{}; + return try env.createStringUtf8(hex); } +} - pub fn fromBytes(bytes: js.Uint8Array) !SecretKey { - const slice = try bytes.toSlice(); - if (slice.len != NativeSecretKey.serialize_size) { - return error.InvalidSecretKeyLength; - } +pub fn SecretKey_finalize(_: napi.Env, sk: *SecretKey, _: ?*anyopaque) void { + allocator.destroy(sk); +} - const sk = NativeSecretKey.deserialize(slice[0..NativeSecretKey.serialize_size]) catch { - return error.DeserializationFailed; - }; - return .{ .raw = sk }; - } +pub fn SecretKey_ctor(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { + const sk = try allocator.create(SecretKey); + errdefer allocator.destroy(sk); + + _ = try env.wrap( + cb.this(), + SecretKey, + sk, + SecretKey_finalize, + null, // finalize hint + null, // ref + ); + return cb.this(); +} - pub fn fromHex(hex_string: js.String) !SecretKey { - var hex_buf: [NativeSecretKey.serialize_size * 2 + 3]u8 = undefined; - const hex = try hexFromString(hex_string, &hex_buf); +/// Creates a `SecretKey` from raw bytes. +pub fn SecretKey_fromBytes(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { + const ctor = cb.this(); + const bytes_info = try cb.arg(0).getTypedarrayInfo(); - var bytes_buf: [NativeSecretKey.serialize_size]u8 = undefined; - const bytes = try std.fmt.hexToBytes(&bytes_buf, hex); - const sk = NativeSecretKey.deserialize(bytes[0..NativeSecretKey.serialize_size]) catch { - return error.DeserializationFailed; - }; - return .{ .raw = sk }; + if (bytes_info.data.len != SecretKey.serialize_size) { + return error.InvalidSecretKeyLength; } - pub fn fromKeygen(seed: js.Uint8Array, key_info: ?js.Value) !SecretKey { - const seed_slice = try seed.toSlice(); - if (seed_slice.len < 32) return error.InvalidSeedLength; + const sk_value = try env.newInstance(ctor, .{}); + const sk = try env.unwrap(SecretKey, sk_value); + sk.* = SecretKey.deserialize(bytes_info.data[0..SecretKey.serialize_size]) catch return error.DeserializationFailed; - const key_info_slice: ?[]const u8 = if (key_info) |value| blk: { - if (value.isUndefined() or value.isNull()) break :blk null; - const typed = try value.asUint8Array(); - break :blk try typed.toSlice(); - } else null; + return sk_value; +} - const sk = NativeSecretKey.keyGen(seed_slice, key_info_slice) catch return error.KeyGenFailed; - return .{ .raw = sk }; - } +/// Creates a `SecretKey` from a hex string. +pub fn SecretKey_fromHex(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { + const ctor = cb.this(); - pub fn sign(self: *const SecretKey, msg: js.Uint8Array) !Signature { - const slice = try msg.toSlice(); - return .{ .raw = self.raw.sign(slice, DST, null) }; - } + var hex_buf: [SecretKey.serialize_size * 2 + 3]u8 = undefined; + const hex = try hexFromValue(cb.arg(0), &hex_buf); + const sk_value = try env.newInstance(ctor, .{}); + const sk = try env.unwrap(SecretKey, sk_value); - pub fn toPublicKey(self: *const SecretKey) !PublicKey { - return .{ .raw = self.raw.toPublicKey() }; - } + var buf: [SecretKey.serialize_size]u8 = undefined; + const bytes = try std.fmt.hexToBytes(&buf, hex); + sk.* = SecretKey.deserialize(bytes[0..SecretKey.serialize_size]) catch return error.DeserializationFailed; - pub fn toBytes(self: *const SecretKey) js.Uint8Array { - const bytes = self.raw.serialize(); - return js.Uint8Array.from(bytes[0..]); - } + return sk_value; +} + +pub fn SecretKey_toHex(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { + const sk = try env.unwrap(SecretKey, cb.this()); + const bytes = sk.serialize(); + + const hex = try std.fmt.allocPrint(allocator, "0x{x}", .{std.fmt.fmtSliceHexLower(&bytes)}); + defer allocator.free(hex); + + return try env.createStringUtf8(hex); +} + +/// Generates a `SecretKey` from a seed (IKM) using key derivation. +/// +/// Seed must be at least 32 bytes. +pub fn SecretKey_fromKeygen(env: napi.Env, cb: napi.CallbackInfo(2)) !napi.Value { + const ctor = cb.this(); + const bytes_info = try cb.arg(0).getTypedarrayInfo(); + + const key_info_data: ?[]const u8 = if (cb.getArg(1)) |ki| blk: { + const typeof = try ki.typeof(); + if (typeof == .undefined or typeof == .null) break :blk null; + const info = try ki.getTypedarrayInfo(); + if (info.array_type != .uint8) return error.InvalidArgument; + break :blk info.data; + } else null; + + if (bytes_info.data.len < 32) return error.InvalidSeedLength; + + const sk_value = try env.newInstance(ctor, .{}); + const sk = try env.unwrap(SecretKey, sk_value); + sk.* = SecretKey.keyGen(bytes_info.data, key_info_data) catch return error.KeyGenFailed; + + return sk_value; +} + +/// Signs a message with this `SecretKey`, returns a `Signature`. +pub fn SecretKey_sign(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { + const sk = try env.unwrap(SecretKey, cb.this()); + const msg = try cb.arg(0).getTypedarrayInfo(); + + const sig_value = try newSignatureInstance(env); + const sig = try env.unwrap(Signature, sig_value); + sig.* = sk.sign(msg.data, DST, null); + + return sig_value; +} + +/// Derives the PublicKey from this SecretKey. +pub fn SecretKey_toPublicKey(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { + const sk = try env.unwrap(SecretKey, cb.this()); + + const pk_value = try newPublicKeyInstance(env); + const pk = try env.unwrap(PublicKey, pk_value); + pk.* = sk.toPublicKey(); + + return pk_value; +} - pub fn toHex(self: *const SecretKey) !js.String { - const bytes = self.raw.serialize(); - var hex_buf: [NativeSecretKey.serialize_size * 2 + 2]u8 = undefined; - return formatHex(bytes[0..], &hex_buf); +/// Serializes the SecretKey to bytes (32 bytes). +pub fn SecretKey_toBytes(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { + const sk = try env.unwrap(SecretKey, cb.this()); + const bytes = sk.serialize(); + + var arraybuffer_bytes: [*]u8 = undefined; + const arraybuffer = try env.createArrayBuffer(SecretKey.serialize_size, &arraybuffer_bytes); + @memcpy(arraybuffer_bytes[0..SecretKey.serialize_size], &bytes); + return try env.createTypedarray(.uint8, SecretKey.serialize_size, arraybuffer, 0); +} + +/// Aggregates multiple Signature objects into one. +/// +/// 1) sigs_array: []Signature +/// 2) sigs_groupcheck: bool +pub fn Signature_aggregate(env: napi.Env, cb: napi.CallbackInfo(2)) !napi.Value { + const ctor = cb.this(); + const sigs_array = cb.arg(0); + const sigs_groupcheck = try coerceToBool(cb.arg(1)); + + const sigs_len = try sigs_array.getArrayLength(); + if (sigs_len == 0) return error.EmptySignatureArray; + + const sigs = try allocator.alloc(Signature, sigs_len); + defer allocator.free(sigs); + + for (0..sigs_len) |i| { + const sig_value = try sigs_array.getElement(@intCast(i)); + const sig = try env.unwrap(Signature, sig_value); + sigs[i] = sig.*; } -}; -/// Verifies a given `msg` against a `Signature` and a `PublicKey`. -pub fn verify(msg: js.Uint8Array, pk: PublicKey, sig: Signature, pk_validate: ?js.Boolean, sig_groupcheck: ?js.Boolean) !js.Boolean { - const msg_slice = try msg.toSlice(); + const agg_sig = AggregateSignature.aggregate(sigs, sigs_groupcheck) catch return error.AggregationFailed; - sig.raw.verify( - boolOrDefault(sig_groupcheck, false), - msg_slice, - DST, - null, - &pk.raw, - boolOrDefault(pk_validate, false), - ) catch return js.Boolean.from(false); + const sig_value = try env.newInstance(ctor, .{}); + const sig = try env.unwrap(Signature, sig_value); + sig.* = agg_sig.toSignature(); + + return sig_value; +} + +/// Validates the signature. +/// Throws an error if the signature is invalid. +pub fn Signature_validate(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { + const sig = try env.unwrap(Signature, cb.this()); + const sig_infcheck = try coerceToBool(cb.arg(0)); + + sig.validate(sig_infcheck) catch return error.InvalidSignature; + + return try env.getUndefined(); +} + +/// Verifies a given `msg` against a `Signature` and a `PublicKey`. +/// +/// Returns `true` if signature is valid, `false` otherwise. +/// +/// Arguments: +/// 1) msg: Uint8Array +/// 2) pk: PublicKey +/// 3) sig: Signature +/// 4) pk_validate: ?bool +/// 5) sig_groupcheck: ?bool +pub fn blst_verify(env: napi.Env, cb: napi.CallbackInfo(5)) !napi.Value { + const msg_info = try cb.arg(0).getTypedarrayInfo(); + const pk = try env.unwrap(PublicKey, cb.arg(1)); + const sig = try env.unwrap(Signature, cb.arg(2)); + const pk_validate: bool = if (cb.getArg(3)) |sgc| + try coerceToBool(sgc) + else + false; + const sig_groupcheck: bool = if (cb.getArg(4)) |v| + try coerceToBool(v) + else + false; + + sig.verify(sig_groupcheck, msg_info.data, DST, null, pk, pk_validate) catch { + return try env.getBoolean(false); + }; - return js.Boolean.from(true); + return try env.getBoolean(true); } -/// Verifies an aggregated signature against multiple messages and public keys. -pub fn aggregateVerify(msgs: js.Array, pks: js.Array, sig: Signature, pks_validate: ?js.Boolean, sig_groupcheck: ?js.Boolean) !js.Boolean { - const msgs_len = try msgs.length(); - const pks_len = try pks.length(); +/// Verify an aggregated signature against multiple messages and multiple public keys. +/// 1) msgs: Uint8Array[] +/// 2) pks: PublicKey[] +/// 3) sig: Signature +/// 4) pks_validate: ?bool +/// 5) sig_groupcheck: ?bool +pub fn blst_aggregateVerify( + env: napi.Env, + cb: napi.CallbackInfo(5), +) !napi.Value { + const msgs_array = cb.arg(0); + const pks_array = cb.arg(1); + const sig = try env.unwrap(Signature, cb.arg(2)); + const pks_validate: bool = if (cb.getArg(3)) |sgc| + try coerceToBool(sgc) + else + false; + const sig_groupcheck: bool = if (cb.getArg(4)) |v| + try coerceToBool(v) + else + false; + + const msgs_len = try msgs_array.getArrayLength(); + const pks_len = try pks_array.getArrayLength(); if (msgs_len == 0 or pks_len == 0 or msgs_len != pks_len) { return error.InvalidAggregateVerifyInput; } - const msg_bufs = try allocator.alloc([32]u8, msgs_len); - defer allocator.free(msg_bufs); - - const native_pks = try allocator.alloc(NativePublicKey, pks_len); - defer allocator.free(native_pks); + const msgs = try allocator.alloc([32]u8, msgs_len); + defer allocator.free(msgs); + const pk_ptrs = try allocator.alloc(*PublicKey, pks_len); + defer allocator.free(pk_ptrs); for (0..msgs_len) |i| { - const msg_value = try msgs.get(@intCast(i)); - const msg_bytes = try (try msg_value.asUint8Array()).toSlice(); - if (msg_bytes.len != 32) return error.InvalidMessageLength; - @memcpy(&msg_bufs[i], msg_bytes[0..32]); + const msg_value = try msgs_array.getElement(@intCast(i)); + const msg_info = try msg_value.getTypedarrayInfo(); + if (msg_info.data.len != 32) return error.InvalidMessageLength; + @memcpy(&msgs[i], msg_info.data[0..32]); - const wrapped_pk = try unwrapClass(PublicKey, try pks.get(@intCast(i))); - native_pks[i] = wrapped_pk.raw; + const pk_value = try pks_array.getElement(@intCast(i)); + pk_ptrs[i] = try env.unwrap(PublicKey, pk_value); } - var pairing_buf: [Pairing.sizeOf()]u8 align(Pairing.buf_align) = undefined; - const result = sig.raw.aggregateVerify( - boolOrDefault(sig_groupcheck, false), - &pairing_buf, - msg_bufs, - DST, - native_pks, - boolOrDefault(pks_validate, false), - ) catch return js.Boolean.from(false); + const pool = thread_pool orelse @panic("ThreadPool not initialized; call initThreadPool first"); + const result = pool.aggregateVerify(sig, sig_groupcheck, msgs, DST, pk_ptrs, pks_validate) catch { + return try env.getBoolean(false); + }; - return js.Boolean.from(result); + return try env.getBoolean(result); } -/// Aggregates and verifies multiple public keys against one 32-byte message. -pub fn fastAggregateVerify(msg: js.Uint8Array, pks: js.Array, sig: Signature, sigs_groupcheck: ?js.Boolean) !js.Boolean { - const msg_slice = try msg.toSlice(); - if (msg_slice.len != 32) return error.InvalidMessageLength; - - const pks_len = try pks.length(); +/// Aggregate and verify an array of `PublicKey`s. Returns `false` if pks array is empty or if signature is invalid. +/// +/// `msg` (signing root) must be exactly 32 bytes. +/// +/// Arguments: +/// 1) msg: Uint8Array +/// 2) pks: PublicKey[] +/// 3) sig: Signature +/// 4) sigs_groupcheck: ?bool +pub fn blst_fastAggregateVerify(env: napi.Env, cb: napi.CallbackInfo(4)) !napi.Value { + const msg_info = try cb.arg(0).getTypedarrayInfo(); + if (msg_info.data.len != 32) return error.InvalidMessageLength; + + const pks_array = cb.arg(1); + const sig = try env.unwrap(Signature, cb.arg(2)); + const sigs_groupcheck = if (cb.getArg(3)) |sgc| + try coerceToBool(sgc) + else + false; + + const pks_len = try pks_array.getArrayLength(); if (pks_len == 0) { - return js.Boolean.from(false); + return try env.getBoolean(false); } - const native_pks = try allocator.alloc(NativePublicKey, pks_len); - defer allocator.free(native_pks); + const pks = try allocator.alloc(PublicKey, pks_len); + defer allocator.free(pks); for (0..pks_len) |i| { - const wrapped_pk = try unwrapClass(PublicKey, try pks.get(@intCast(i))); - native_pks[i] = wrapped_pk.raw; + const pk_value = try pks_array.getElement(@intCast(i)); + const pk = try env.unwrap(PublicKey, pk_value); + pks[i] = pk.*; } var pairing_buf: [Pairing.sizeOf()]u8 align(Pairing.buf_align) = undefined; - const result = sig.raw.fastAggregateVerify( - boolOrDefault(sigs_groupcheck, false), - &pairing_buf, - msg_slice[0..32], - DST, - native_pks, - false, - ) catch return js.Boolean.from(false); + // `pks_validate` is always false here since we assume proof of possession for public keys. + const result = sig.fastAggregateVerify(sigs_groupcheck, &pairing_buf, msg_info.data[0..32], DST, pks, false) catch { + return try env.getBoolean(false); + }; - return js.Boolean.from(result); + return try env.getBoolean(result); } -/// Batch verify multiple aggregate signature sets. -pub fn verifyMultipleAggregateSignatures(sets: js.Array, pks_validate: ?js.Boolean, sigs_groupcheck: ?js.Boolean) !js.Boolean { - const n_elems = try sets.length(); +/// Batch verify multiple signature sets. +/// Returns `false` if verification fails. +/// +/// Arguments: +/// 1) sets: Array of { msg: Uint8Array, pk: PublicKey, sig: Signature } +/// 2) pks_validate: ?bool +/// 3) sigs_groupcheck: ?bool +pub fn blst_verifyMultipleAggregateSignatures(env: napi.Env, cb: napi.CallbackInfo(3)) !napi.Value { + const sets = cb.arg(0); + const n_elems = try sets.getArrayLength(); + + const pks_validate: bool = if (cb.getArg(1)) |v| + try coerceToBool(v) + else + false; + const sigs_groupcheck: bool = if (cb.getArg(2)) |sgc| + try coerceToBool(sgc) + else + false; + if (n_elems == 0) { - return js.Boolean.from(false); + return try env.getBoolean(false); } const msgs = try allocator.alloc([32]u8, n_elems); defer allocator.free(msgs); - const pks = try allocator.alloc(*NativePublicKey, n_elems); + const pks = try allocator.alloc(*PublicKey, n_elems); defer allocator.free(pks); - const sigs = try allocator.alloc(*NativeSignature, n_elems); + const sigs = try allocator.alloc(*Signature, n_elems); defer allocator.free(sigs); const rands = try allocator.alloc([32]u8, n_elems); @@ -361,228 +695,327 @@ pub fn verifyMultipleAggregateSignatures(sets: js.Array, pks_validate: ?js.Boole const rand = prng.random(); for (0..n_elems) |i| { - const set_obj = try (try sets.get(@intCast(i))).asObject(VerifySet); - const set = try set_obj.get(); + const set_value = try sets.getElement(@intCast(i)); - const msg_bytes = try (try set.msg.asUint8Array()).toSlice(); - if (msg_bytes.len != 32) return error.InvalidMessageLength; - @memcpy(&msgs[i], msg_bytes[0..32]); + const msg_value = try set_value.getNamedProperty("msg"); + const msg = try msg_value.getTypedarrayInfo(); + if (msg.data.len != 32) return error.InvalidMessageLength; + @memcpy(&msgs[i], msg.data[0..32]); - const wrapped_pk = try unwrapClass(PublicKey, set.pk); - pks[i] = &wrapped_pk.raw; + // Use unwrapped pointers directly - no copy needed + const pk_value = try set_value.getNamedProperty("pk"); + pks[i] = try env.unwrap(PublicKey, pk_value); - const wrapped_sig = try unwrapClass(Signature, set.sig); - sigs[i] = &wrapped_sig.raw; + const sig_value = try set_value.getNamedProperty("sig"); + sigs[i] = try env.unwrap(Signature, sig_value); rand.bytes(&rands[i]); + // Ensure first 8 bytes (RAND_BITS=64) are non-zero + while (std.mem.allEqual(u8, rands[i][0..8], 0)) { + rand.bytes(rands[i][0..8]); + } } - var pairing_buf: [Pairing.sizeOf()]u8 align(Pairing.buf_align) = undefined; - const result = bls.verifyMultipleAggregateSignatures( - &pairing_buf, + const pool = thread_pool orelse @panic("ThreadPool not initialized; call initThreadPool first"); + const result = pool.verifyMultipleAggregateSignatures( n_elems, msgs, DST, pks, - boolOrDefault(pks_validate, false), + pks_validate, sigs, - boolOrDefault(sigs_groupcheck, false), + sigs_groupcheck, rands, - ) catch return js.Boolean.from(false); + ) catch { + return try env.getBoolean(false); + }; - return js.Boolean.from(result); + return try env.getBoolean(result); } -/// Aggregates multiple `Signature` objects into one. -pub fn aggregateSignatures(signatures: js.Array, sigs_groupcheck: ?js.Boolean) !Signature { - const signatures_len = try signatures.length(); - if (signatures_len == 0) return error.EmptySignatureArray; +/// Aggregate multiple Signature objects into one. +/// Validates each signature if `sigs_groupcheck` is true. +/// +/// Arguments: +/// 1) signatures: Signature[] +/// 2) sigs_groupcheck: ?bool +pub fn blst_aggregateSignatures(env: napi.Env, cb: napi.CallbackInfo(2)) !napi.Value { + const sigs_array = cb.arg(0); + + const sigs_groupcheck: bool = if (cb.getArg(1)) |sgc| + try coerceToBool(sgc) + else + false; + + const sigs_len = try sigs_array.getArrayLength(); - const sigs = try allocator.alloc(NativeSignature, signatures_len); + if (sigs_len == 0) return error.EmptySignatureArray; + + const sigs = try allocator.alloc(Signature, sigs_len); defer allocator.free(sigs); - for (0..signatures_len) |i| { - const wrapped = try unwrapClass(Signature, try signatures.get(@intCast(i))); - sigs[i] = wrapped.raw; + for (0..sigs_len) |i| { + const sig_value = try sigs_array.getElement(@intCast(i)); + const sig = try env.unwrap(Signature, sig_value); + sigs[i] = sig.*; } - const agg_sig = AggregateSignature.aggregate(sigs, boolOrDefault(sigs_groupcheck, false)) catch { - return error.AggregationFailed; - }; + const agg_sig = AggregateSignature.aggregate(sigs, sigs_groupcheck) catch return error.AggregationFailed; + const result_sig = agg_sig.toSignature(); + + const sig_value = try newSignatureInstance(env); + const sig = try env.unwrap(Signature, sig_value); + sig.* = result_sig; - return .{ .raw = agg_sig.toSignature() }; + return sig_value; } -/// Aggregates multiple `PublicKey` objects into one. -pub fn aggregatePublicKeys(pks: js.Array, pks_validate: ?js.Boolean) !PublicKey { - const pks_len = try pks.length(); - if (pks_len == 0) return error.EmptyPublicKeyArray; +/// Aggregate multiple `PublicKey` objects into one. +/// +/// Arguments: +/// 1) pks: PublicKey[] +/// 2) pks_validate: ?bool +pub fn blst_aggregatePublicKeys(env: napi.Env, cb: napi.CallbackInfo(2)) !napi.Value { + const pks_array = cb.arg(0); + const pks_len = try pks_array.getArrayLength(); - const native_pks = try allocator.alloc(NativePublicKey, pks_len); - defer allocator.free(native_pks); + const pks_validate: bool = if (cb.getArg(1)) |v| + try coerceToBool(v) + else + false; - for (0..pks_len) |i| { - const wrapped = try unwrapClass(PublicKey, try pks.get(@intCast(i))); - native_pks[i] = wrapped.raw; + if (pks_len == 0) { + return error.EmptyPublicKeyArray; } - const agg_pk = AggregatePublicKey.aggregate(native_pks, boolOrDefault(pks_validate, false)) catch { - return error.AggregationFailed; - }; - - return .{ .raw = agg_pk.toPublicKey() }; -} - -/// Aggregates public keys from serialized bytes. -pub fn aggregateSerializedPublicKeys(serialized_public_keys: js.Array, pks_validate: ?js.Boolean) !PublicKey { - const pks_len = try serialized_public_keys.length(); - if (pks_len == 0) return error.EmptyPublicKeyArray; - - const native_pks = try allocator.alloc(NativePublicKey, pks_len); - defer allocator.free(native_pks); + const pks = try allocator.alloc(PublicKey, pks_len); + defer allocator.free(pks); for (0..pks_len) |i| { - const bytes = try (try (try serialized_public_keys.get(@intCast(i))).asUint8Array()).toSlice(); - native_pks[i] = NativePublicKey.deserialize(bytes) catch return error.DeserializationFailed; + const pk_value = try pks_array.getElement(@intCast(i)); + const pk = try env.unwrap(PublicKey, pk_value); + pks[i] = pk.*; } - const agg_pk = AggregatePublicKey.aggregate(native_pks, boolOrDefault(pks_validate, false)) catch { - return error.AggregationFailed; - }; + const agg_pk = AggregatePublicKey.aggregate(pks, pks_validate) catch return error.AggregationFailed; + const result_pk = agg_pk.toPublicKey(); - return .{ .raw = agg_pk.toPublicKey() }; -} + const pk_value = try newPublicKeyInstance(env); + const pk = try env.unwrap(PublicKey, pk_value); + pk.* = result_pk; -/// Asynchronously aggregates public keys and signatures with randomness. -pub fn asyncAggregateWithRandomness(sets: js.Array) !js.Promise(js.Value) { - const n = try sets.length(); - if (n == 0) return error.EmptyArray; - if (n > MAX_AGGREGATE_PER_JOB) return error.TooManySets; + return pk_value; +} - const pks = try allocator.alloc(NativePublicKey, n); - errdefer allocator.free(pks); +/// Aggregate public keys from serialized bytes. +/// +/// Arguments: +/// 1) serializedPublicKeys: Uint8Array[] - array of serialized (96-bytes each) `PublicKey`s. +pub fn blst_aggregateSerializedPublicKeys(env: napi.Env, cb: napi.CallbackInfo(2)) !napi.Value { + const pks_array = cb.arg(0); + const pks_len = try pks_array.getArrayLength(); + const pks_validate: bool = if (cb.getArg(1)) |v| + try coerceToBool(v) + else + false; - const sigs = try allocator.alloc(NativeSignature, n); - errdefer allocator.free(sigs); + if (pks_len == 0) return error.EmptyPublicKeyArray; - for (0..n) |i| { - const set_obj = try (try sets.get(@intCast(i))).asObject(AsyncAggregateSet); - const set = try set_obj.get(); + const pks = try allocator.alloc(PublicKey, pks_len); + defer allocator.free(pks); - const wrapped_pk = try unwrapClass(PublicKey, set.pk); - pks[i] = wrapped_pk.raw; + for (0..pks_len) |i| { + const pk_bytes_value = try pks_array.getElement(@intCast(i)); + const bytes_info = try pk_bytes_value.getTypedarrayInfo(); - const sig_bytes = try (try set.sig.asUint8Array()).toSlice(); - sigs[i] = NativeSignature.deserialize(sig_bytes[0..]) catch return error.DeserializationFailed; - sigs[i].validate(true) catch return error.InvalidSignature; + pks[i] = PublicKey.deserialize(bytes_info.data) catch + return error.DeserializationFailed; } - const promise = try js.createPromise(js.Value); - const data = try allocator.create(AsyncAggregateData); - errdefer allocator.destroy(data); + const agg_pk = AggregatePublicKey.aggregate(pks, pks_validate) catch return error.AggregationFailed; + const result_pk = agg_pk.toPublicKey(); - data.* = .{ - .pks = pks, - .sigs = sigs, - .n = n, - .deferred = promise.deferred, - }; + const pk_value = try newPublicKeyInstance(env); + const pk = try env.unwrap(PublicKey, pk_value); + pk.* = result_pk; - const resource_name = try js.env().createStringUtf8("asyncAggregateWithRandomness"); - data.work = try napi.AsyncWork(AsyncAggregateData).create( - js.env(), - null, - resource_name, - asyncAggregateExecute, - asyncAggregateComplete, - data, - ); - try data.work.queue(); + return pk_value; +} - return promise; +/// Unpacks a hex string from a `napi.Value`. Returns the slice representing the hex string. +fn hexFromValue(value: napi.Value, buf: []u8) ![]const u8 { + const hex_str = try value.getValueStringUtf8(buf); + const hex = if (hex_str.len >= 2 and hex_str[0] == '0' and hex_str[1] == 'x') hex_str[2..] else hex_str; + return hex; } const MAX_AGGREGATE_PER_JOB = bls.MAX_AGGREGATE_PER_JOB; -const AsyncAggregateData = struct { - pks: []NativePublicKey, - sigs: []NativeSignature, - n: usize, +/// Synchronously aggregates public keys and signatures with randomness using +/// Pippenger multi-scalar multiplication. Runs on the worker thread. +/// +/// Arguments: +/// 1) sets: Array of {pk: PublicKey, sig: Uint8Array} +/// +/// Returns: {pk: PublicKey, sig: Signature} +pub fn blst_aggregateWithRandomness(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { + const sets = cb.arg(0); + const n = try sets.getArrayLength(); - result_pk: NativePublicKey = .{}, - result_sig: NativeSignature = .{}, - err: bool = false, + if (n == 0) return error.EmptyArray; + if (n > MAX_AGGREGATE_PER_JOB) return error.TooManySets; - deferred: napi.Deferred, - work: napi.AsyncWork(AsyncAggregateData) = undefined, -}; + const nbits: usize = 64; + const nbytes: usize = 8; -fn asyncAggregateExecute(_: napi.Env, data: *AsyncAggregateData) void { - const n = data.n; + var pk_ptrs: [MAX_AGGREGATE_PER_JOB]*const PublicKey = undefined; + var sigs: [MAX_AGGREGATE_PER_JOB]Signature = undefined; + var sig_ptrs: [MAX_AGGREGATE_PER_JOB]*const Signature = undefined; - var rands: [32 * MAX_AGGREGATE_PER_JOB]u8 = undefined; - std.crypto.random.bytes(rands[0 .. n * 32]); + // Generate 8-byte scalars (64 bits each) using a fast PRNG seeded from OS entropy + var prng = std.Random.DefaultPrng.init(std.crypto.random.int(u64)); + const rand = prng.random(); + var scalars: [8 * MAX_AGGREGATE_PER_JOB]u8 = undefined; + var sca_ptrs: [MAX_AGGREGATE_PER_JOB]*const u8 = undefined; + rand.bytes(scalars[0 .. n * nbytes]); - var pk_refs: [MAX_AGGREGATE_PER_JOB]*const NativePublicKey = undefined; - var sig_refs: [MAX_AGGREGATE_PER_JOB]*const NativeSignature = undefined; for (0..n) |i| { - pk_refs[i] = &data.pks[i]; - sig_refs[i] = &data.sigs[i]; - } + const set_value = try sets.getElement(@intCast(i)); - const p1_scratch_size = bls.c.blst_p1s_mult_pippenger_scratch_sizeof(n); - const p2_scratch_size = bls.c.blst_p2s_mult_pippenger_scratch_sizeof(n); - const scratch_size = @max(p1_scratch_size, p2_scratch_size); - const scratch = allocator.alloc(u64, scratch_size) catch { - data.err = true; - return; - }; - defer allocator.free(scratch); - - const agg_pk = AggregatePublicKey.aggregateWithRandomness( - pk_refs[0..n], - rands[0 .. n * 32], - false, - scratch, - ) catch { - data.err = true; - return; - }; - - const agg_sig = AggregateSignature.aggregateWithRandomness( - sig_refs[0..n], - rands[0 .. n * 32], - false, - scratch, - ) catch { - data.err = true; - return; - }; + const pk_value = try set_value.getNamedProperty("pk"); + const unwrapped_pk = try env.unwrap(PublicKey, pk_value); + pk_ptrs[i] = unwrapped_pk; - data.result_pk = agg_pk.toPublicKey(); - data.result_sig = agg_sig.toSignature(); -} + const sig_value = try set_value.getNamedProperty("sig"); + const sig_bytes = try sig_value.getTypedarrayInfo(); + sigs[i] = Signature.deserialize(sig_bytes.data[0..]) catch return error.DeserializationFailed; + sigs[i].validate(true) catch return error.InvalidSignature; + sig_ptrs[i] = &sigs[i]; -fn asyncAggregateComplete(env: napi.Env, _: napi.status.Status, data: *AsyncAggregateData) void { - defer { - data.work.delete() catch {}; - allocator.free(data.pks); - allocator.free(data.sigs); - allocator.destroy(data); + while (std.mem.allEqual(u8, scalars[i * nbytes ..][0..nbytes], 0)) { + rand.bytes(scalars[i * nbytes ..][0..nbytes]); + } + sca_ptrs[i] = &scalars[i * nbytes]; } - if (data.err) { - const msg = env.createStringUtf8("BLST_ERROR: Aggregation failed") catch return; - data.deferred.reject(msg) catch return; - return; - } + const scratch_size = @max( + bls.c.blst_p1s_mult_pippenger_scratch_sizeof(n), + bls.c.blst_p2s_mult_pippenger_scratch_sizeof(n), + ); + const scratch = try allocator.alloc(u64, scratch_size); + defer allocator.free(scratch); - const pk_value = napi.Value{ .env = env.env, .value = js.convertReturn(PublicKey, .{ .raw = data.result_pk }, env.env) }; - const sig_value = napi.Value{ .env = env.env, .value = js.convertReturn(Signature, .{ .raw = data.result_sig }, env.env) }; + // Pippenger multi-scalar multiplication on G1 (pubkeys) + var p1_ret: bls.c.blst_p1 = std.mem.zeroes(bls.c.blst_p1); + bls.c.blst_p1s_mult_pippenger( + &p1_ret, + @ptrCast(&pk_ptrs), + n, + @ptrCast(&sca_ptrs), + nbits, + scratch.ptr, + ); + var result_pk: PublicKey = .{}; + bls.c.blst_p1_to_affine(&result_pk.point, &p1_ret); + + // Pippenger multi-scalar multiplication on G2 (signatures) + var p2_ret: bls.c.blst_p2 = std.mem.zeroes(bls.c.blst_p2); + bls.c.blst_p2s_mult_pippenger( + &p2_ret, + @ptrCast(&sig_ptrs), + n, + @ptrCast(&sca_ptrs), + nbits, + scratch.ptr, + ); + var result_sig: Signature = .{}; + bls.c.blst_p2_to_affine(&result_sig.point, &p2_ret); + + // Wrap results as NAPI PublicKey/Signature instances + const pk_result = try newPublicKeyInstance(env); + const pk_out = try env.unwrap(PublicKey, pk_result); + pk_out.* = result_pk; + + const sig_result = try newSignatureInstance(env); + const sig_out = try env.unwrap(Signature, sig_result); + sig_out.* = result_sig; + + const result = try env.createObject(); + try result.setNamedProperty("pk", pk_result); + try result.setNamedProperty("sig", sig_result); + return result; +} - const result = env.createObject() catch return; - result.setNamedProperty("pk", pk_value) catch return; - result.setNamedProperty("sig", sig_value) catch return; +pub fn register(env: napi.Env, exports: napi.Value) !void { + const blst_obj = try env.createObject(); - data.deferred.resolve(result) catch return; + const sk_ctor = try env.defineClass( + "SecretKey", + 0, + SecretKey_ctor, + null, + &[_]napi.c.napi_property_descriptor{ + method(1, SecretKey_sign), + method(0, SecretKey_toPublicKey), + method(0, SecretKey_toBytes), + method(0, SecretKey_toHex), + }, + ); + try sk_ctor.defineProperties(&[_]napi.c.napi_property_descriptor{ + method(1, SecretKey_fromBytes), + method(1, SecretKey_fromHex), + method(2, SecretKey_fromKeygen), + }); + + const pk_ctor = try env.defineClass( + "PublicKey", + 0, + PublicKey_ctor, + null, + &[_]napi.c.napi_property_descriptor{ + method(1, PublicKey_toBytes), + method(1, PublicKey_toHex), + method(0, PublicKey_validate), + }, + ); + try pk_ctor.defineProperties(&[_]napi.c.napi_property_descriptor{ + method(2, PublicKey_fromBytes), + method(2, PublicKey_fromHex), + }); + + const sig_ctor = try env.defineClass( + "Signature", + 0, + Signature_ctor, + null, + &[_]napi.c.napi_property_descriptor{ + method(1, Signature_toBytes), + method(1, Signature_toHex), + method(1, Signature_validate), + }, + ); + try sig_ctor.defineProperties(&[_]napi.c.napi_property_descriptor{ + method(3, Signature_fromBytes), + method(3, Signature_fromHex), + method(2, Signature_aggregate), + }); + + const state = try InstanceData.init(env); + try setRef(env, pk_ctor, &state.public_key_ctor_ref); + try setRef(env, sig_ctor, &state.signature_ctor_ref); + + try blst_obj.setNamedProperty("SecretKey", sk_ctor); + try blst_obj.setNamedProperty("PublicKey", pk_ctor); + try blst_obj.setNamedProperty("Signature", sig_ctor); + + try blst_obj.setNamedProperty("verify", try env.createFunction("verify", 5, blst_verify, null)); + try blst_obj.setNamedProperty("aggregateVerify", try env.createFunction("aggregateVerify", 5, blst_aggregateVerify, null)); + try blst_obj.setNamedProperty("fastAggregateVerify", try env.createFunction("fastAggregateVerify", 4, blst_fastAggregateVerify, null)); + try blst_obj.setNamedProperty("verifyMultipleAggregateSignatures", try env.createFunction("verifyMultipleAggregateSignatures", 3, blst_verifyMultipleAggregateSignatures, null)); + try blst_obj.setNamedProperty("aggregateSignatures", try env.createFunction("aggregateSignatures", 2, blst_aggregateSignatures, null)); + try blst_obj.setNamedProperty("aggregatePublicKeys", try env.createFunction("aggregatePublicKeys", 2, blst_aggregatePublicKeys, null)); + try blst_obj.setNamedProperty("aggregateSerializedPublicKeys", try env.createFunction("aggregateSerializedPublicKeys", 2, blst_aggregateSerializedPublicKeys, null)); + try blst_obj.setNamedProperty("aggregateWithRandomness", try env.createFunction("aggregateWithRandomness", 1, blst_aggregateWithRandomness, null)); + + try exports.setNamedProperty("blst", blst_obj); } diff --git a/bindings/napi/pubkeys.zig b/bindings/napi/pubkeys.zig index 06246d540..b09dcddd1 100644 --- a/bindings/napi/pubkeys.zig +++ b/bindings/napi/pubkeys.zig @@ -1,10 +1,11 @@ const std = @import("std"); -const js = @import("zapi").js; const napi = @import("zapi").napi; const bls = @import("bls"); const blst_bindings = @import("./blst.zig"); const PubkeyIndexMap = @import("state_transition").PubkeyIndexMap; const Index2PubkeyCache = @import("state_transition").Index2PubkeyCache; +const getter = @import("napi_property_descriptor.zig").getter; +const method = @import("napi_property_descriptor.zig").method; /// Uses page allocator for internal allocations. /// It's recommended to never reallocate the pubkey2index after initialization. @@ -69,14 +70,15 @@ fn pubkey2indexWrittenSize() usize { return total_size - @sizeOf(Header); } -/// JS: pubkeys.save(filePath) -pub fn save(file_path: js.String) !void { +pub fn pubkeys_save(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { var file_path_buf: [1024]u8 = undefined; - const path = try file_path.toSlice(&file_path_buf); - var file = try std.fs.cwd().createFile(path, .{ .truncate = true }); + const file_path = try cb.arg(0).getValueStringUtf8(&file_path_buf); + var file = try std.fs.cwd().createFile(file_path, .{ .truncate = true }); defer file.close(); - // Write header: Magic "PKIX" + len + capacity + // Write header + // Magic "PKIX" + len + capacity + var header: [12]u8 = [_]u8{ 'P', 'K', 'I', 'X', 0, 0, 0, 0, 0, 0, 0, 0 }; std.mem.writeInt(u32, header[4..8], @intCast(state.index2pubkey.items.len), .little); std.mem.writeInt(u32, header[8..12], @intCast(state.index2pubkey.capacity), .little); @@ -85,17 +87,19 @@ pub fn save(file_path: js.String) !void { // Write pubkey2index entries const p2i_size = pubkey2indexWrittenSize(); const ptr: [*]u8 = @ptrCast(state.pubkey2index.unmanaged.metadata.?); - try file.writeAll(ptr[0..p2i_size]); + const slice = ptr[0..p2i_size]; + try file.writeAll(slice); // Write index2pubkey entries try file.writeAll(std.mem.sliceAsBytes(state.index2pubkey.items)); + + return env.getUndefined(); } -/// JS: pubkeys.load(filePath) -pub fn load(file_path: js.String) !void { +pub fn pubkeys_load(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { var file_path_buf: [1024]u8 = undefined; - const path = try file_path.toSlice(&file_path_buf); - var file = try std.fs.cwd().openFile(path, .{}); + const file_path = try cb.arg(0).getValueStringUtf8(&file_path_buf); + var file = try std.fs.cwd().openFile(file_path, .{}); defer file.close(); if (state.initialized) { @@ -133,108 +137,119 @@ pub fn load(file_path: js.String) !void { // Read pubkey2index entries const ptr: [*]u8 = @ptrCast(state.pubkey2index.unmanaged.metadata.?); - _ = try file.readAll(ptr[0..p2i_size]); + const slice = ptr[0..p2i_size]; + _ = try file.readAll(slice); state.pubkey2index.unmanaged.size = len; state.pubkey2index.unmanaged.available = capacity - len; // Read index2pubkey entries - _ = try file.readAll(std.mem.sliceAsBytes(state.index2pubkey.items)); + const index2pubkey_bytes = std.mem.sliceAsBytes(state.index2pubkey.items); + _ = try file.readAll(index2pubkey_bytes); state.initialized = true; + return env.getUndefined(); } -/// JS: pubkeys.getIndex(pubkeyBytes) → number | null -/// Returns the index for a 48-byte pubkey, or undefined if not found. -pub fn getIndex(pubkey: js.Uint8Array) !js.Value { +pub fn pubkeys_getIndex(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { if (!state.initialized) { return error.PubkeyIndexNotInitialized; } - const pubkey_slice = try pubkey.toSlice(); - if (pubkey_slice.len != 48) { + const pubkey_info = try cb.arg(0).getTypedarrayInfo(); + if (pubkey_info.data.len != 48) { return error.InvalidPubkeyLength; } - const e = js.env(); - if (state.pubkey2index.get(pubkey_slice[0..48].*)) |index| { - const val = try e.createUint32(@intCast(index)); - return .{ .val = val }; - } else { - const val = try e.getNull(); - return .{ .val = val }; - } + const index = state.pubkey2index.get(pubkey_info.data[0..48].*) orelse return env.getNull(); + return try env.createUint32(@intCast(index)); } -/// JS: pubkeys.get(index) → PublicKey | undefined -/// Returns the PublicKey at the given index, or undefined if out of bounds. -pub fn get(index: js.Number) !?blst_bindings.PublicKey { +pub fn pubkeys_get(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { if (!state.initialized) { return error.PubkeyIndexNotInitialized; } - const idx = index.assertU32(); - if (idx >= state.index2pubkey.items.len) { - return null; + const index = try cb.arg(0).getValueUint32(); + if (index >= state.index2pubkey.items.len) { + return env.getUndefined(); } - return .{ .raw = state.index2pubkey.items[@intCast(idx)] }; + const out = try blst_bindings.newPublicKeyInstance(env); + const out_pubkey = try env.unwrap(bls.PublicKey, out); + out_pubkey.* = state.index2pubkey.items[@intCast(index)]; + return out; } -/// JS: pubkeys.set(index, pubkeyBytes) -pub fn set(index: js.Number, pubkey: js.Uint8Array) !void { +pub fn pubkeys_set(env: napi.Env, cb: napi.CallbackInfo(2)) !napi.Value { if (!state.initialized) { return error.PubkeyIndexNotInitialized; } - const idx = index.assertU32(); - const pubkey_slice = try pubkey.toSlice(); - if (pubkey_slice.len != 48) { + const index = try cb.arg(0).getValueUint32(); + const pubkey_info = try cb.arg(1).getTypedarrayInfo(); + if (pubkey_info.data.len != 48) { return error.InvalidPubkeyLength; } - const pubkey_bytes = pubkey_slice[0..48]; + const pubkey_bytes = pubkey_info.data[0..48]; // Ensure capacity if needed - if (idx >= state.index2pubkey.capacity) { - const new_cap: u32 = @intCast(@max(idx + 1, state.index2pubkey.capacity * 2)); + if (index >= state.index2pubkey.capacity) { + const new_cap: u32 = @intCast(@max(index + 1, state.index2pubkey.capacity * 2)); try state.pubkey2index.ensureTotalCapacity(new_cap); try state.index2pubkey.ensureTotalCapacity(new_cap); } // Extend length if needed - if (idx >= state.index2pubkey.items.len) { - state.index2pubkey.items.len = idx + 1; + if (index >= state.index2pubkey.items.len) { + state.index2pubkey.items.len = index + 1; } // Set pubkey2index - state.pubkey2index.put(pubkey_bytes.*, @intCast(idx)) catch return error.PubkeyIndexInsertFailed; + state.pubkey2index.put(pubkey_bytes.*, @intCast(index)) catch return error.PubkeyIndexInsertFailed; // Deserialize and set index2pubkey - state.index2pubkey.items[@intCast(idx)] = try bls.PublicKey.uncompress(pubkey_bytes); + state.index2pubkey.items[@intCast(index)] = try bls.PublicKey.uncompress(pubkey_bytes); + + return env.getUndefined(); } -/// JS: pubkeys.size → number (getter) -/// Note: DSL doesn't support getters yet, so this is a function. -pub fn size() !js.Number { +pub fn pubkeys_size(env: napi.Env, _: napi.CallbackInfo(0)) !napi.Value { if (!state.initialized) { return error.PubkeyIndexNotInitialized; } - return js.Number.from(@as(u32, @intCast(state.index2pubkey.items.len))); + return try env.createUint32(@intCast(state.index2pubkey.items.len)); } -/// JS: pubkeys.ensureCapacity(newSize) -pub fn ensureCapacity(new_size: js.Number) !void { +pub fn pubkeys_ensureCapacity(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { if (!state.initialized) { return error.PubkeyIndexNotInitialized; } - const requested = new_size.assertU32(); const old_size = state.index2pubkey.capacity; - if (requested <= old_size) { - return; + const new_size = try cb.arg(0).getValueUint32(); + if (new_size <= old_size) { + return env.getUndefined(); } - try state.pubkey2index.ensureTotalCapacity(requested); - try state.index2pubkey.ensureTotalCapacity(requested); + try state.pubkey2index.ensureTotalCapacity(new_size); + try state.index2pubkey.ensureTotalCapacity(new_size); + return env.getUndefined(); +} + +pub fn register(env: napi.Env, exports: napi.Value) !void { + const pubkeys_obj = try env.createObject(); + + try pubkeys_obj.defineProperties(&[_]napi.c.napi_property_descriptor{ + method(1, pubkeys_load), + method(1, pubkeys_save), + method(1, pubkeys_ensureCapacity), + method(1, pubkeys_get), + method(1, pubkeys_getIndex), + method(2, pubkeys_set), + getter(pubkeys_size), + }); + + try exports.setNamedProperty("pubkeys", pubkeys_obj); } diff --git a/bindings/napi/root.zig b/bindings/napi/root.zig index 53e64ec80..1d1a31692 100644 --- a/bindings/napi/root.zig +++ b/bindings/napi/root.zig @@ -1,15 +1,18 @@ const std = @import("std"); const zapi = @import("zapi"); const js = zapi.js; +const napi = zapi.napi; pub const pool = @import("./pool.zig"); pub const shuffle = @import("./shuffle.zig"); pub const config = @import("./config.zig"); -pub const pubkeys = @import("./pubkeys.zig"); + pub const metrics = @import("./metrics.zig"); -pub const blst = @import("./blst.zig"); -pub const BeaconStateView = @import("./BeaconStateView.zig").BeaconStateView; pub const stateTransition = @import("./state_transition.zig"); +const pubkeys = @import("./pubkeys.zig"); +const blst = @import("./blst.zig"); +const BeaconStateView = @import("./BeaconStateView.zig"); + fn init(old_ref_count: u32) !void { if (old_ref_count == 0) { // First environment — initialize shared state. @@ -29,9 +32,12 @@ fn cleanup(new_ref_count: u32) void { } } +fn setup(env: napi.Env, exports: napi.Value) !void { + try blst.register(env, exports); + try BeaconStateView.register(env, exports); + try pubkeys.register(env, exports); +} + comptime { - js.exportModule(@This(), .{ - .init = init, - .cleanup = cleanup, - }); + js.exportModule(@This(), .{ .init = init, .cleanup = cleanup, .register = setup }); } diff --git a/bindings/test/pubkeys.test.ts b/bindings/test/pubkeys.test.ts index 8e32c0260..c285921b9 100644 --- a/bindings/test/pubkeys.test.ts +++ b/bindings/test/pubkeys.test.ts @@ -29,7 +29,7 @@ describe("pubkeys", () => { for (const {index, pubkeyBytes} of keypairs) { pubkeyCache.set(index, pubkeyBytes); } - expect(pubkeyCache.size()).toBe(keypairs.length); + expect(pubkeyCache.size).toBe(keypairs.length); for (const {index, pubkeyBytes} of keypairs) { expect(pubkeyCache.get(index)).toBeDefined(); @@ -66,7 +66,7 @@ describe("pubkeys", () => { pubkeyCache.save(tempPkixPath); pubkeyCache.load(tempPkixPath); - expect(pubkeyCache.size()).toBe(keypairs.length); + expect(pubkeyCache.size).toBe(keypairs.length); for (const {index, pubkeyBytes} of keypairs) { expect(pubkeyCache.getIndex(pubkeyBytes)).toBe(index); expect(pubkeyCache.get(index)).toBeDefined(); diff --git a/bindings/test/shuffle.test.ts b/bindings/test/shuffle.test.ts index cfbe0745a..ca55079de 100644 --- a/bindings/test/shuffle.test.ts +++ b/bindings/test/shuffle.test.ts @@ -43,7 +43,7 @@ describe("innerShuffleList", () => { const forwards = false; expect(() => { innerShuffleList(invalidInput as any, seed, rounds, forwards); - }).toThrow("InvalidArg"); + }).toThrow("Argument 1 must be a Uint32Array"); }); it("should fail with invalid rounds", async () => {