From eca7d4abf1ad5641256fc3124fce6c22e08025e5 Mon Sep 17 00:00:00 2001 From: bing Date: Fri, 10 Jul 2026 16:31:18 +0200 Subject: [PATCH 1/6] chore(deps): update zapi --- build.zig.zon | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 47e232b8b..de89540c4 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -40,8 +40,8 @@ .hash = "zig_yaml-0.1.0-C1161kFWAwDxjKAFmklKwWVDvz2mmq0Q__bDhGGjeyd3", }, .zapi = .{ - .url = "https://github.com/ChainSafe/zapi/archive/refs/tags/zapi-v2.2.0.tar.gz", - .hash = "zapi-2.2.0-rIqzUXBaBADUsvvvB2N5kOBdLEO0rGPhowTqmIXe3qVh", + .url = "https://github.com/ChainSafe/zapi/archive/refs/tags/zapi-v3.1.0.tar.gz", + .hash = "zapi-3.1.0-rIqzUQJsBADCFUrmxoTZyOhSTkcW6m3t_OgOHc6oxnm9", }, .zbench = .{ .url = "git+https://github.com/hendriknielaender/zBench#b2b89c475e3ef1bb2bd71255c80478a82d3e0ca8", From 12ab7902fc3ed6936f079e9088563f75753022fa Mon Sep 17 00:00:00 2001 From: bing Date: Fri, 10 Jul 2026 16:58:54 +0200 Subject: [PATCH 2/6] fix: zapi usage bls --- bindings/napi/BeaconStateView.zig | 7 +++++- bindings/napi/blst.zig | 37 +++++++--------------------- bindings/napi/blst_pool.zig | 41 +++++++++++++++++++++++++++++++ bindings/napi/config.zig | 7 ++++-- bindings/napi/pool.zig | 14 +++++++++-- bindings/napi/pubkeys.zig | 3 ++- bindings/napi/root.zig | 7 ++++-- 7 files changed, 80 insertions(+), 36 deletions(-) create mode 100644 bindings/napi/blst_pool.zig diff --git a/bindings/napi/BeaconStateView.zig b/bindings/napi/BeaconStateView.zig index b6a758333..cdc327661 100644 --- a/bindings/napi/BeaconStateView.zig +++ b/bindings/napi/BeaconStateView.zig @@ -11,6 +11,11 @@ const AnySignedBeaconBlock = fork_types.AnySignedBeaconBlock; const preset = @import("preset").preset; const ct = @import("consensus_types"); const pool = @import("./pool.zig"); +const Node = @import("persistent_merkle_tree").Node; +// Same type as pool's private `PoolRc` — generic instantiations are memoized, +// so `RefCount(Node.Pool)` resolves to the identical type. Declared locally +// (and privately) so zapi's module exporter doesn't try to export it. +const PoolRc = st.RefCount(Node.Pool); const config = @import("./config.zig"); const pubkey = @import("./pubkeys.zig"); const js_types = @import("./js_types.zig"); @@ -72,7 +77,7 @@ pub const js_meta = js.class(.{ .properties = .{ } }); cached_state: ?*CachedBeaconState = null, -pool_rc: ?*pool.PoolRc = null, +pool_rc: ?*PoolRc = null, const BeaconStateView = @This(); pub fn init() BeaconStateView { diff --git a/bindings/napi/blst.zig b/bindings/napi/blst.zig index c48298e78..4e80b7dbe 100644 --- a/bindings/napi/blst.zig +++ b/bindings/napi/blst.zig @@ -16,6 +16,9 @@ const js = zapi.js; const napi = zapi.napi; const bls = @import("bls"); const napi_io = @import("./io.zig"); +// Thread-pool lifecycle lives in a privately-imported module so zapi's exporter +// (which rejects non-DSL `pub fn`s like `initThreadPool(u16)`) never sees it. +const blst_pool = @import("./blst_pool.zig"); const NativePublicKey = bls.PublicKey; const NativeSignature = bls.Signature; @@ -23,7 +26,6 @@ const NativeSecretKey = bls.SecretKey; const Pairing = bls.Pairing; const AggregatePublicKey = bls.AggregatePublicKey; const AggregateSignature = bls.AggregateSignature; -const ThreadPool = bls.ThreadPool; const DST = bls.DST; const MAX_AGGREGATE_PER_JOB = bls.MAX_AGGREGATE_PER_JOB; @@ -35,29 +37,8 @@ const MAX_AGGREGATE_PER_JOB = bls.MAX_AGGREGATE_PER_JOB; /// See: packages/beacon-node/src/chain/bls/multithread/worker.ts const BATCH_VERIFY_SIZE = 32; -/// 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, napi_io.get(), .{ .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(napi_io.get()); - thread_pool = null; - } -} +// Thread-pool lifecycle (`init`/`deinit`) and the cached pool reference live in +// `blst_pool.zig`. Use `blst_pool.get()` to reach the current pool. var gpa: std.heap.DebugAllocator(.{}) = .init; const allocator = if (builtin.mode == .Debug) @@ -373,7 +354,7 @@ pub fn aggregateVerify(msgs: js.Array, pks: js.Array, sig: Signature, pks_valida pk_ptrs[i] = &wrapped_pk.raw; } - const pool = thread_pool orelse return error.ThreadPoolNotInitialized; + const pool = blst_pool.get() orelse return error.ThreadPoolNotInitialized; const result = pool.aggregateVerify( napi_io.get(), &sig.raw, @@ -501,7 +482,7 @@ pub fn verifyMultipleAggregateSignatures(sets: js.Array, pks_validate: ?js.Boole @memset(rands[i][8..], 0); } - const pool = thread_pool orelse return error.ThreadPoolNotInitialized; + const pool = blst_pool.get() orelse return error.ThreadPoolNotInitialized; const result = pool.verifyMultipleAggregateSignatures( napi_io.get(), n_elems, @@ -715,7 +696,7 @@ const AsyncAggRandData = struct { /// /// Note: MUST NOT call any napi APIs. fn asyncAggRand_execute(_: napi.Env, data: *AsyncAggRandData) void { - const pool = thread_pool orelse { + const pool = blst_pool.get() orelse { data.err = error.PoolNotInitialized; return; }; @@ -803,7 +784,7 @@ pub fn asyncAggregateWithRandomness(sets: js.Array) !js.Value { if (n == 0) return error.EmptyArray; if (n > MAX_AGGREGATE_PER_JOB) return error.TooManySets; - if (thread_pool == null) return error.PoolNotInitialized; + if (blst_pool.get() == null) return error.PoolNotInitialized; const env = js.env(); diff --git a/bindings/napi/blst_pool.zig b/bindings/napi/blst_pool.zig new file mode 100644 index 000000000..f27a944ee --- /dev/null +++ b/bindings/napi/blst_pool.zig @@ -0,0 +1,41 @@ +//! Lifecycle for the blst verification `ThreadPool`. +//! +//! These functions live here — not in `blst.zig` — on purpose. `blst.zig` is +//! exported to JS via zapi's `exportModule`, which turns every `pub fn` of an +//! exported module into a JS callback and rejects any whose parameters aren't +//! zapi DSL types. `initThreadPool(n_workers: u16)` takes a plain `u16`, so it +//! can't live in an exported module. This module is imported privately (as a +//! plain `const`, never `pub`) by both `blst.zig` and `root.zig`, so zapi never +//! sees it while Zig code on both sides can still call it. +const std = @import("std"); +const bls = @import("bls"); +const napi_io = @import("./io.zig"); + +const ThreadPool = bls.ThreadPool; + +/// Cached thread pool reference for parallel verification. +/// Initialized once via `init`, torn down via `deinit`. +var thread_pool: ?*ThreadPool = null; + +pub fn init(n_workers: u16) !void { + if (thread_pool != null) return error.PoolExists; + thread_pool = try ThreadPool.init(std.heap.page_allocator, napi_io.get(), .{ .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. For lodestar's long-lived process this is typically never +/// called. +pub fn deinit() void { + if (thread_pool) |p| { + p.deinit(napi_io.get()); + thread_pool = null; + } +} + +/// Current pool, or null if not yet initialized. +pub fn get() ?*ThreadPool { + return thread_pool; +} diff --git a/bindings/napi/config.zig b/bindings/napi/config.zig index 0132bd02f..013124703 100644 --- a/bindings/napi/config.zig +++ b/bindings/napi/config.zig @@ -9,7 +9,8 @@ const Preset = @import("preset").Preset; const max_blob_schedule_entries = 16; -pub const State = struct { +// Not `pub`: internal helper (see pool.zig). Only the `state` value below is pub. +const State = struct { config: BeaconConfig = undefined, initialized: bool = false, config_name: [64]u8 = undefined, @@ -71,7 +72,9 @@ pub fn set(object: js.Value, genesis_root: js.Uint8Array) !void { state.initialized = true; } -pub fn chainConfigFromObject(env: napi.Env, obj: napi.Value) !ChainConfig { +// Not `pub`: internal helper called only by `set` above. Takes `napi.Env`, +// which is not a zapi DSL type, so it must not be an exported module function. +fn chainConfigFromObject(env: napi.Env, obj: napi.Value) !ChainConfig { var chain_config: ChainConfig = undefined; inline for (std.meta.fields(ChainConfig)) |field| { diff --git a/bindings/napi/pool.zig b/bindings/napi/pool.zig index 2e0ec480b..842e00a13 100644 --- a/bindings/napi/pool.zig +++ b/bindings/napi/pool.zig @@ -9,14 +9,24 @@ const allocator = std.heap.page_allocator; const default_pool_size: u32 = 0; -pub const PoolRc = RefCount(Node.Pool); +// NOTE: not `pub`. zapi's `exportModule` auto-exports every `pub` decl of this +// module and recurses into any `pub` struct type, trying to wrap its methods as +// JS functions. A `pub const PoolRc = RefCount(Node.Pool)` makes zapi descend +// into `RefCount(Node.Pool).init`, whose params aren't zapi DSL types, which is +// a compile error. Keeping it private hides it from the exporter. Consumers in +// other files declare their own `RefCount(Node.Pool)` alias — generic +// instantiations are memoized, so it's the same type. +const PoolRc = RefCount(Node.Pool); /// Pool is wrapped in `RefCount` so binding objects holding pool refs at /// process exit keep the pool alive until their JS finalizer runs. NAPI /// env cleanup hook fires before module-level JS holders are finalized, /// so an unconditional `pool.deinit()` there would free memory that /// `pool.unref()` calls in those finalizers still need. -pub const State = struct { +// Not `pub`: internal helper. zapi's exporter recurses into every `pub` struct +// type and rejects its non-DSL methods (`init(self: *State)` etc.). `state` +// below stays `pub` (a value, which the exporter skips) so other files can use it. +const State = struct { pool_rc: ?*PoolRc = null, pub fn init(self: *State) !void { diff --git a/bindings/napi/pubkeys.zig b/bindings/napi/pubkeys.zig index e65813520..f7c5c7492 100644 --- a/bindings/napi/pubkeys.zig +++ b/bindings/napi/pubkeys.zig @@ -13,7 +13,8 @@ const allocator = std.heap.page_allocator; const default_initial_capacity: u32 = 0; const max_stack_aggregate_pubkeys = 512; -pub const State = struct { +// Not `pub`: internal helper (see pool.zig). Only the `state` value below is pub. +const State = struct { pubkey2index: PubkeyIndexMap = undefined, index2pubkey: Index2PubkeyCache = undefined, initialized: bool = false, diff --git a/bindings/napi/root.zig b/bindings/napi/root.zig index 0e5f14294..71c76eeb4 100644 --- a/bindings/napi/root.zig +++ b/bindings/napi/root.zig @@ -12,6 +12,9 @@ pub const pubkeys = @import("./pubkeys.zig"); const options = @import("bls_options"); const napi_io = @import("./io.zig"); +// Imported privately (not `pub`) so zapi's exporter doesn't try to export its +// non-DSL lifecycle functions. See blst_pool.zig for why they live outside blst.zig. +const blst_pool = @import("./blst_pool.zig"); var gpa: std.heap.DebugAllocator(.{}) = .init; const allocator = if (builtin.mode == .Debug) gpa.allocator() else std.heap.c_allocator; @@ -32,7 +35,7 @@ fn init(old_ref_count: u32) !void { } const n_workers = @min(cpu_count, @import("bls").ThreadPool.MAX_WORKERS); - try blst.initThreadPool(@intCast(n_workers)); + try blst_pool.init(@intCast(n_workers)); try pool.state.init(); try pubkeys.state.init(); config.state.init(); @@ -55,7 +58,7 @@ fn detectCpuCount() !usize { fn cleanup(new_ref_count: u32) void { if (new_ref_count == 0) { // Last environment — tear down shared state. - blst.deinitThreadPool(); + blst_pool.deinit(); config.state.deinit(); pubkeys.state.deinit(); pool.state.deinit(); From 6451a1af3178276a83fdf3af3f65b109311c1890 Mon Sep 17 00:00:00 2001 From: bing Date: Tue, 14 Jul 2026 14:32:02 +0200 Subject: [PATCH 3/6] reduce verbosity of slop comments --- bindings/napi/blst.zig | 5 ----- bindings/napi/blst_pool.zig | 14 ++++---------- bindings/napi/config.zig | 3 --- bindings/napi/pool.zig | 10 ---------- bindings/napi/root.zig | 4 ++-- 5 files changed, 6 insertions(+), 30 deletions(-) diff --git a/bindings/napi/blst.zig b/bindings/napi/blst.zig index 4e80b7dbe..92be25881 100644 --- a/bindings/napi/blst.zig +++ b/bindings/napi/blst.zig @@ -16,8 +16,6 @@ const js = zapi.js; const napi = zapi.napi; const bls = @import("bls"); const napi_io = @import("./io.zig"); -// Thread-pool lifecycle lives in a privately-imported module so zapi's exporter -// (which rejects non-DSL `pub fn`s like `initThreadPool(u16)`) never sees it. const blst_pool = @import("./blst_pool.zig"); const NativePublicKey = bls.PublicKey; @@ -37,9 +35,6 @@ const MAX_AGGREGATE_PER_JOB = bls.MAX_AGGREGATE_PER_JOB; /// See: packages/beacon-node/src/chain/bls/multithread/worker.ts const BATCH_VERIFY_SIZE = 32; -// Thread-pool lifecycle (`init`/`deinit`) and the cached pool reference live in -// `blst_pool.zig`. Use `blst_pool.get()` to reach the current pool. - var gpa: std.heap.DebugAllocator(.{}) = .init; const allocator = if (builtin.mode == .Debug) gpa.allocator() diff --git a/bindings/napi/blst_pool.zig b/bindings/napi/blst_pool.zig index f27a944ee..f52015c1f 100644 --- a/bindings/napi/blst_pool.zig +++ b/bindings/napi/blst_pool.zig @@ -1,19 +1,13 @@ //! Lifecycle for the blst verification `ThreadPool`. -//! -//! These functions live here — not in `blst.zig` — on purpose. `blst.zig` is -//! exported to JS via zapi's `exportModule`, which turns every `pub fn` of an -//! exported module into a JS callback and rejects any whose parameters aren't -//! zapi DSL types. `initThreadPool(n_workers: u16)` takes a plain `u16`, so it -//! can't live in an exported module. This module is imported privately (as a -//! plain `const`, never `pub`) by both `blst.zig` and `root.zig`, so zapi never -//! sees it while Zig code on both sides can still call it. + const std = @import("std"); const bls = @import("bls"); const napi_io = @import("./io.zig"); const ThreadPool = bls.ThreadPool; -/// Cached thread pool reference for parallel verification. +/// Cached thread pool reference. +/// /// Initialized once via `init`, torn down via `deinit`. var thread_pool: ?*ThreadPool = null; @@ -24,7 +18,7 @@ pub fn init(n_workers: u16) !void { /// Closes the `ThreadPool` used for blst operations. /// -/// Note: this can invalidate any inflight verification requests. Consumer is +/// 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. For lodestar's long-lived process this is typically never /// called. diff --git a/bindings/napi/config.zig b/bindings/napi/config.zig index 013124703..a59092fe3 100644 --- a/bindings/napi/config.zig +++ b/bindings/napi/config.zig @@ -9,7 +9,6 @@ const Preset = @import("preset").Preset; const max_blob_schedule_entries = 16; -// Not `pub`: internal helper (see pool.zig). Only the `state` value below is pub. const State = struct { config: BeaconConfig = undefined, initialized: bool = false, @@ -72,8 +71,6 @@ pub fn set(object: js.Value, genesis_root: js.Uint8Array) !void { state.initialized = true; } -// Not `pub`: internal helper called only by `set` above. Takes `napi.Env`, -// which is not a zapi DSL type, so it must not be an exported module function. fn chainConfigFromObject(env: napi.Env, obj: napi.Value) !ChainConfig { var chain_config: ChainConfig = undefined; diff --git a/bindings/napi/pool.zig b/bindings/napi/pool.zig index 842e00a13..c23dcec40 100644 --- a/bindings/napi/pool.zig +++ b/bindings/napi/pool.zig @@ -9,13 +9,6 @@ const allocator = std.heap.page_allocator; const default_pool_size: u32 = 0; -// NOTE: not `pub`. zapi's `exportModule` auto-exports every `pub` decl of this -// module and recurses into any `pub` struct type, trying to wrap its methods as -// JS functions. A `pub const PoolRc = RefCount(Node.Pool)` makes zapi descend -// into `RefCount(Node.Pool).init`, whose params aren't zapi DSL types, which is -// a compile error. Keeping it private hides it from the exporter. Consumers in -// other files declare their own `RefCount(Node.Pool)` alias — generic -// instantiations are memoized, so it's the same type. const PoolRc = RefCount(Node.Pool); /// Pool is wrapped in `RefCount` so binding objects holding pool refs at @@ -23,9 +16,6 @@ const PoolRc = RefCount(Node.Pool); /// env cleanup hook fires before module-level JS holders are finalized, /// so an unconditional `pool.deinit()` there would free memory that /// `pool.unref()` calls in those finalizers still need. -// Not `pub`: internal helper. zapi's exporter recurses into every `pub` struct -// type and rejects its non-DSL methods (`init(self: *State)` etc.). `state` -// below stays `pub` (a value, which the exporter skips) so other files can use it. const State = struct { pool_rc: ?*PoolRc = null, diff --git a/bindings/napi/root.zig b/bindings/napi/root.zig index 2529584d3..679b45f13 100644 --- a/bindings/napi/root.zig +++ b/bindings/napi/root.zig @@ -10,10 +10,10 @@ pub const BeaconStateView = @import("./BeaconStateView.zig"); pub const blst = @import("./blst.zig"); pub const pubkeys = @import("./pubkeys.zig"); +// The below imports are not `pub` so zapi's exporter doesn't try to export its +// non-DSL lifecycle functions. const options = @import("bls_options"); const napi_io = @import("./io.zig"); -// Imported privately (not `pub`) so zapi's exporter doesn't try to export its -// non-DSL lifecycle functions. See blst_pool.zig for why they live outside blst.zig. const blst_pool = @import("./blst_pool.zig"); var gpa: std.heap.DebugAllocator(.{}) = .init; From 88fabfc480c9e8b80822bf4db4460a6aa57ae6f8 Mon Sep 17 00:00:00 2001 From: bing Date: Tue, 14 Jul 2026 14:33:27 +0200 Subject: [PATCH 4/6] more comment --- bindings/napi/BeaconStateView.zig | 3 --- 1 file changed, 3 deletions(-) diff --git a/bindings/napi/BeaconStateView.zig b/bindings/napi/BeaconStateView.zig index cdc327661..2171e5d57 100644 --- a/bindings/napi/BeaconStateView.zig +++ b/bindings/napi/BeaconStateView.zig @@ -12,9 +12,6 @@ const preset = @import("preset").preset; const ct = @import("consensus_types"); const pool = @import("./pool.zig"); const Node = @import("persistent_merkle_tree").Node; -// Same type as pool's private `PoolRc` — generic instantiations are memoized, -// so `RefCount(Node.Pool)` resolves to the identical type. Declared locally -// (and privately) so zapi's module exporter doesn't try to export it. const PoolRc = st.RefCount(Node.Pool); const config = @import("./config.zig"); const pubkey = @import("./pubkeys.zig"); From a846ef25760d1c1f27b00c37cf95b1f6cdc524ee Mon Sep 17 00:00:00 2001 From: bing Date: Tue, 14 Jul 2026 14:38:45 +0200 Subject: [PATCH 5/6] bump package.json zapi --- package.json | 5 +---- pnpm-lock.yaml | 11 +++++------ 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 28e4cd195..b198cba77 100644 --- a/package.json +++ b/package.json @@ -57,11 +57,8 @@ ] }, "dependencies": { - "@chainsafe/zapi": "github:ChainSafe/zapi#6649cef91b19bcd9854aa1ffa4d0c1aed7270f55" + "@chainsafe/zapi": "3.1.0" }, - "bundleDependencies": [ - "@chainsafe/zapi" - ], "devDependencies": { "@biomejs/biome": "^2.3.11", "@chainsafe/benchmark": "^2.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d6611513b..b57408b12 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: '@chainsafe/zapi': - specifier: github:ChainSafe/zapi#6649cef91b19bcd9854aa1ffa4d0c1aed7270f55 - version: https://codeload.github.com/ChainSafe/zapi/tar.gz/6649cef91b19bcd9854aa1ffa4d0c1aed7270f55 + specifier: 3.1.0 + version: 3.1.0 devDependencies: '@biomejs/biome': specifier: ^2.3.11 @@ -401,9 +401,8 @@ packages: resolution: {integrity: sha512-H8YdEoXXv2Hw17gDWGOJEya4LHlBbpChJP3jDQRfIk9hhwr0c/zbBemBRmjADowZhArL+ymkO+j5hGaYySjdpw==} engines: {node: '>= 18'} - '@chainsafe/zapi@https://codeload.github.com/ChainSafe/zapi/tar.gz/6649cef91b19bcd9854aa1ffa4d0c1aed7270f55': - resolution: {tarball: https://codeload.github.com/ChainSafe/zapi/tar.gz/6649cef91b19bcd9854aa1ffa4d0c1aed7270f55} - version: 2.2.0 + '@chainsafe/zapi@3.1.0': + resolution: {integrity: sha512-WRamllUXrrmx4G4qzmg0vRvcWQ+/Lgm7TgOFbqeJFPKlNv8CApTyA3BP6tudBaJyQYJSXlOn2RWrdv5e0EFwSw==} hasBin: true '@colors/colors@1.5.0': @@ -2443,7 +2442,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@https://codeload.github.com/ChainSafe/zapi/tar.gz/6649cef91b19bcd9854aa1ffa4d0c1aed7270f55': {} + '@chainsafe/zapi@3.1.0': {} '@colors/colors@1.5.0': optional: true From f4a06b0c7af7f714659396a5f020534545b21507 Mon Sep 17 00:00:00 2001 From: bing Date: Wed, 15 Jul 2026 11:48:59 +0200 Subject: [PATCH 6/6] use lifecycle pattern extracted from #469 --- bindings/napi/BeaconStateView.zig | 4 +--- bindings/napi/blst.zig | 40 +++++++++++++++++++++++++++---- bindings/napi/blst_pool.zig | 35 --------------------------- bindings/napi/root.zig | 7 ++---- 4 files changed, 38 insertions(+), 48 deletions(-) delete mode 100644 bindings/napi/blst_pool.zig diff --git a/bindings/napi/BeaconStateView.zig b/bindings/napi/BeaconStateView.zig index 2171e5d57..2cdd4748e 100644 --- a/bindings/napi/BeaconStateView.zig +++ b/bindings/napi/BeaconStateView.zig @@ -11,8 +11,6 @@ const AnySignedBeaconBlock = fork_types.AnySignedBeaconBlock; const preset = @import("preset").preset; const ct = @import("consensus_types"); const pool = @import("./pool.zig"); -const Node = @import("persistent_merkle_tree").Node; -const PoolRc = st.RefCount(Node.Pool); const config = @import("./config.zig"); const pubkey = @import("./pubkeys.zig"); const js_types = @import("./js_types.zig"); @@ -74,7 +72,7 @@ pub const js_meta = js.class(.{ .properties = .{ } }); cached_state: ?*CachedBeaconState = null, -pool_rc: ?*PoolRc = null, +pool_rc: @TypeOf(pool.state.pool_rc) = null, const BeaconStateView = @This(); pub fn init() BeaconStateView { diff --git a/bindings/napi/blst.zig b/bindings/napi/blst.zig index 92be25881..780f70d2f 100644 --- a/bindings/napi/blst.zig +++ b/bindings/napi/blst.zig @@ -16,7 +16,6 @@ const js = zapi.js; const napi = zapi.napi; const bls = @import("bls"); const napi_io = @import("./io.zig"); -const blst_pool = @import("./blst_pool.zig"); const NativePublicKey = bls.PublicKey; const NativeSignature = bls.Signature; @@ -24,6 +23,7 @@ const NativeSecretKey = bls.SecretKey; const Pairing = bls.Pairing; const AggregatePublicKey = bls.AggregatePublicKey; const AggregateSignature = bls.AggregateSignature; +const ThreadPool = bls.ThreadPool; const DST = bls.DST; const MAX_AGGREGATE_PER_JOB = bls.MAX_AGGREGATE_PER_JOB; @@ -35,6 +35,36 @@ const MAX_AGGREGATE_PER_JOB = bls.MAX_AGGREGATE_PER_JOB; /// See: packages/beacon-node/src/chain/bls/multithread/worker.ts const BATCH_VERIFY_SIZE = 32; +/// Cached thread pool reference for parallel verification. +/// Initialized lazily on first use, torn down via `deinitThreadPool`. +var thread_pool: ?*ThreadPool = null; + +/// Native-only thread pool lifecycle, reached from `root.zig` through the +/// pub `lifecycle` var so it is not part of the JS module surface. +const Lifecycle = struct { + pub fn initThreadPool(_: *Lifecycle, n_workers: u16) !void { + if (thread_pool != null) return error.PoolExists; + thread_pool = try ThreadPool.init(std.heap.page_allocator, napi_io.get(), .{ .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(_: *Lifecycle) void { + if (thread_pool) |p| { + p.deinit(napi_io.get()); + thread_pool = null; + } + } +}; + +pub var lifecycle: Lifecycle = .{}; + var gpa: std.heap.DebugAllocator(.{}) = .init; const allocator = if (builtin.mode == .Debug) gpa.allocator() @@ -349,7 +379,7 @@ pub fn aggregateVerify(msgs: js.Array, pks: js.Array, sig: Signature, pks_valida pk_ptrs[i] = &wrapped_pk.raw; } - const pool = blst_pool.get() orelse return error.ThreadPoolNotInitialized; + const pool = thread_pool orelse return error.ThreadPoolNotInitialized; const result = pool.aggregateVerify( napi_io.get(), &sig.raw, @@ -477,7 +507,7 @@ pub fn verifyMultipleAggregateSignatures(sets: js.Array, pks_validate: ?js.Boole @memset(rands[i][8..], 0); } - const pool = blst_pool.get() orelse return error.ThreadPoolNotInitialized; + const pool = thread_pool orelse return error.ThreadPoolNotInitialized; const result = pool.verifyMultipleAggregateSignatures( napi_io.get(), n_elems, @@ -691,7 +721,7 @@ const AsyncAggRandData = struct { /// /// Note: MUST NOT call any napi APIs. fn asyncAggRand_execute(_: napi.Env, data: *AsyncAggRandData) void { - const pool = blst_pool.get() orelse { + const pool = thread_pool orelse { data.err = error.PoolNotInitialized; return; }; @@ -779,7 +809,7 @@ pub fn asyncAggregateWithRandomness(sets: js.Array) !js.Value { if (n == 0) return error.EmptyArray; if (n > MAX_AGGREGATE_PER_JOB) return error.TooManySets; - if (blst_pool.get() == null) return error.PoolNotInitialized; + if (thread_pool == null) return error.PoolNotInitialized; const env = js.env(); diff --git a/bindings/napi/blst_pool.zig b/bindings/napi/blst_pool.zig deleted file mode 100644 index f52015c1f..000000000 --- a/bindings/napi/blst_pool.zig +++ /dev/null @@ -1,35 +0,0 @@ -//! Lifecycle for the blst verification `ThreadPool`. - -const std = @import("std"); -const bls = @import("bls"); -const napi_io = @import("./io.zig"); - -const ThreadPool = bls.ThreadPool; - -/// Cached thread pool reference. -/// -/// Initialized once via `init`, torn down via `deinit`. -var thread_pool: ?*ThreadPool = null; - -pub fn init(n_workers: u16) !void { - if (thread_pool != null) return error.PoolExists; - thread_pool = try ThreadPool.init(std.heap.page_allocator, napi_io.get(), .{ .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. For lodestar's long-lived process this is typically never -/// called. -pub fn deinit() void { - if (thread_pool) |p| { - p.deinit(napi_io.get()); - thread_pool = null; - } -} - -/// Current pool, or null if not yet initialized. -pub fn get() ?*ThreadPool { - return thread_pool; -} diff --git a/bindings/napi/root.zig b/bindings/napi/root.zig index 679b45f13..d48f59bc0 100644 --- a/bindings/napi/root.zig +++ b/bindings/napi/root.zig @@ -10,11 +10,8 @@ pub const BeaconStateView = @import("./BeaconStateView.zig"); pub const blst = @import("./blst.zig"); pub const pubkeys = @import("./pubkeys.zig"); -// The below imports are not `pub` so zapi's exporter doesn't try to export its -// non-DSL lifecycle functions. const options = @import("bls_options"); const napi_io = @import("./io.zig"); -const blst_pool = @import("./blst_pool.zig"); var gpa: std.heap.DebugAllocator(.{}) = .init; const allocator = if (builtin.mode == .Debug) gpa.allocator() else std.heap.c_allocator; @@ -35,7 +32,7 @@ fn init(old_ref_count: u32) !void { } const n_workers = @min(cpu_count, @import("bls").ThreadPool.MAX_WORKERS); - try blst_pool.init(@intCast(n_workers)); + try blst.lifecycle.initThreadPool(@intCast(n_workers)); try pool.state.init(); try pubkeys.state.init(); config.state.init(); @@ -58,7 +55,7 @@ fn detectCpuCount() !usize { fn cleanup(new_ref_count: u32) void { if (new_ref_count == 0) { // Last environment — tear down shared state. - blst_pool.deinit(); + blst.lifecycle.deinitThreadPool(); config.state.deinit(); pubkeys.state.deinit(); pool.state.deinit();