diff --git a/bindings/napi/BeaconStateView.zig b/bindings/napi/BeaconStateView.zig index 63d6a3f94..53d061dd6 100644 --- a/bindings/napi/BeaconStateView.zig +++ b/bindings/napi/BeaconStateView.zig @@ -1,5 +1,5 @@ const std = @import("std"); -const napi = @import("zapi:zapi"); +const napi = @import("zapi").napi; const c = @import("config"); const fork_types = @import("fork_types"); const st = @import("state_transition"); diff --git a/bindings/napi/blst.zig b/bindings/napi/blst.zig index 3f3664304..f2e3faa84 100644 --- a/bindings/napi/blst.zig +++ b/bindings/napi/blst.zig @@ -1,6 +1,16 @@ -//! 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 napi = @import("zapi:zapi"); +const napi = @import("zapi").napi; const bls = @import("bls"); const builtin = @import("builtin"); const getter = @import("napi_property_descriptor.zig").getter; @@ -12,8 +22,33 @@ const SecretKey = bls.SecretKey; const Pairing = bls.Pairing; const AggregatePublicKey = bls.AggregatePublicKey; const AggregateSignature = bls.AggregateSignature; +const ThreadPool = bls.ThreadPool; const DST = bls.DST; +/// Cached thread pool reference for parallel verification. +/// Initialized lazily on first use, torn down via `deinitThreadPool`. +var thread_pool: ?*ThreadPool = null; + +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) gpa.allocator() @@ -556,8 +591,8 @@ pub fn blst_aggregateVerify( 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 pk_ptrs = try allocator.alloc(*PublicKey, pks_len); + defer allocator.free(pk_ptrs); for (0..msgs_len) |i| { const msg_value = try msgs_array.getElement(@intCast(i)); @@ -566,20 +601,11 @@ pub fn blst_aggregateVerify( @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.*; + pk_ptrs[i] = try env.unwrap(PublicKey, pk_value); } - var pairing_buf: [Pairing.sizeOf()]u8 align(Pairing.buf_align) = undefined; - - const result = sig.aggregateVerify( - sig_groupcheck, - &pairing_buf, - msgs, - DST, - pks, - pks_validate, - ) catch { + 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); }; @@ -684,11 +710,14 @@ pub fn blst_verifyMultipleAggregateSignatures(env: napi.Env, cb: napi.CallbackIn 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, @@ -822,163 +851,99 @@ fn hexFromValue(value: napi.Value, buf: []u8) ![]const u8 { 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, - n: usize, - - // Outputs (set in execute) - result_pk: PublicKey = .{}, - result_sig: Signature = .{}, - err: bool = false, - - // NAPI handles - deferred: napi.Deferred, - work: napi.AsyncWork(AsyncAggregateData) = undefined, -}; - -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; - 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); - const scratch = allocator.alloc(u64, scratch_size) catch { - data.err = true; - return; - }; - 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 - 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 - scratch, - ) catch { - data.err = true; - return; - }; - - data.result_pk = agg_pk.toPublicKey(); - data.result_sig = agg_sig.toSignature(); -} - -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); - } - - if (data.err) { - const msg = env.createStringUtf8("BLST_ERROR: Aggregation failed") catch return; - data.deferred.reject(msg) catch return; - 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; - - // 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. +/// 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: Promise<{pk: PublicKey, sig: Signature}> -pub fn blst_asyncAggregateWithRandomness(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { +/// 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(); 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 nbits: usize = 64; + const nbytes: usize = 8; + + 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; - const sigs = try allocator.alloc(Signature, n); - errdefer allocator.free(sigs); + // 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]); 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.*; + pk_ptrs[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; + sig_ptrs[i] = &sigs[i]; + + while (std.mem.allEqual(u8, scalars[i * nbytes ..][0..nbytes], 0)) { + rand.bytes(scalars[i * nbytes ..][0..nbytes]); + } + sca_ptrs[i] = &scalars[i * nbytes]; } - const data = try allocator.create(AsyncAggregateData); - errdefer allocator.destroy(data); + 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); - data.* = .{ - .pks = pks, - .sigs = sigs, - .n = n, - .deferred = try napi.Deferred.create(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); - const resource_name = try env.createStringUtf8("asyncAggregateWithRandomness"); - data.work = try napi.AsyncWork(AsyncAggregateData).create( - env, - null, - resource_name, - asyncAggregateExecute, - asyncAggregateComplete, - data, + // 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, ); - try data.work.queue(); + 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; - return data.deferred.getPromise(); + const result = try env.createObject(); + try result.setNamedProperty("pk", pk_result); + try result.setNamedProperty("sig", sig_result); + return result; } pub fn register(env: napi.Env, exports: napi.Value) !void { @@ -1050,7 +1015,7 @@ pub fn register(env: napi.Env, exports: napi.Value) !void { 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 blst_obj.setNamedProperty("aggregateWithRandomness", try env.createFunction("aggregateWithRandomness", 1, blst_aggregateWithRandomness, null)); try exports.setNamedProperty("blst", blst_obj); } diff --git a/bindings/napi/config.zig b/bindings/napi/config.zig index 475f49ec2..c0ee6fdf8 100644 --- a/bindings/napi/config.zig +++ b/bindings/napi/config.zig @@ -1,5 +1,6 @@ const std = @import("std"); -const napi = @import("zapi:zapi"); +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,29 @@ 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 chain_config = try chainConfigFromObject(js.env(), try obj.toValue().coerceToObject()); + + 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 +144,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 feb356e11..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:zapi"); +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 4d2bff4fc..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:zapi"); +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 ad9ef15b5..8e5b644e9 100644 --- a/bindings/napi/pool.zig +++ b/bindings/napi/pool.zig @@ -1,5 +1,5 @@ const std = @import("std"); -const napi = @import("zapi:zapi"); +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 241a26fad..b09dcddd1 100644 --- a/bindings/napi/pubkeys.zig +++ b/bindings/napi/pubkeys.zig @@ -1,5 +1,5 @@ const std = @import("std"); -const napi = @import("zapi:zapi"); +const napi = @import("zapi").napi; const bls = @import("bls"); const blst_bindings = @import("./blst.zig"); const PubkeyIndexMap = @import("state_transition").PubkeyIndexMap; diff --git a/bindings/napi/root.zig b/bindings/napi/root.zig index 2b38817dc..1d1a31692 100644 --- a/bindings/napi/root.zig +++ b/bindings/napi/root.zig @@ -1,53 +1,43 @@ const std = @import("std"); -const napi = @import("zapi:zapi"); -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 BeaconStateView = @import("./BeaconStateView.zig"); -const blst = @import("./blst.zig"); -const state_transition = @import("./state_transition.zig"); - -comptime { - napi.module.register(register); -} +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"); -/// 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); +pub const metrics = @import("./metrics.zig"); +pub const stateTransition = @import("./state_transition.zig"); -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 = .{}; +const pubkeys = @import("./pubkeys.zig"); +const blst = @import("./blst.zig"); +const BeaconStateView = @import("./BeaconStateView.zig"); -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); +fn setup(env: napi.Env, exports: napi.Value) !void { try blst.register(env, exports); - try state_transition.register(env, exports); - try metrics.register(env, exports); + try BeaconStateView.register(env, exports); + try pubkeys.register(env, exports); +} + +comptime { + js.exportModule(@This(), .{ .init = init, .cleanup = cleanup, .register = setup }); } diff --git a/bindings/napi/shuffle.zig b/bindings/napi/shuffle.zig index 565cfdf67..f8378634a 100644 --- a/bindings/napi/shuffle.zig +++ b/bindings/napi/shuffle.zig @@ -1,35 +1,16 @@ -const std = @import("std"); -const napi = @import("zapi:zapi"); -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(rounds_i32); + const is_forwards = 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, is_forwards); } diff --git a/bindings/napi/state_transition.zig b/bindings/napi/state_transition.zig index d33387650..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:zapi"); +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/bindings/napi/to_napi_value.zig b/bindings/napi/to_napi_value.zig index c7356727b..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:zapi"); +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/shuffle.test.ts b/bindings/test/shuffle.test.ts index 6357276aa..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("Native callback failed"); + }).toThrow("Argument 1 must be a Uint32Array"); }); it("should fail with invalid rounds", async () => { diff --git a/build.zig b/build.zig index e60abeee8..b48ee98a0 100644 --- a/build.zig +++ b/build.zig @@ -1161,7 +1161,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:zapi", dep_zapi.module("zapi")); + 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); diff --git a/build.zig.zon b/build.zig.zon index 8c3c7f95e..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#5a51cf21121372451bd56af97db896f474bce384", - .hash = "zapi-0.1.0-rIqzUTM-AgDqeGAe3zmoh8_y6CtuBra5RE_BJVmUFMfX", + .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 d1171defc..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#5a51cf21121372451bd56af97db896f474bce384", + .url = "git+https://github.com/chainsafe/zapi#zapi-v1.0.1", }, .zbench = .{ .url = "git+https://github.com/hendriknielaender/zBench#d8c7dd485306b88b757d52005614ebcb0a336942", @@ -253,7 +253,7 @@ .config, .fork_types, .state_transition, - "zapi:zapi", + .zapi, }, }, .linkage = .dynamic,