diff --git a/bindings/napi/blst.zig b/bindings/napi/blst.zig index ce96df8db..6366e3d61 100644 --- a/bindings/napi/blst.zig +++ b/bindings/napi/blst.zig @@ -2,29 +2,30 @@ //! //! 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. +//! `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").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 zapi = @import("zapi:zapi"); +const js = zapi.js; +const napi = zapi.napi; +const bls = @import("bls"); const napi_io = @import("./io.zig"); -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 ThreadPool = bls.ThreadPool; const DST = bls.DST; +const MAX_AGGREGATE_PER_JOB = bls.MAX_AGGREGATE_PER_JOB; /// Cached thread pool reference for parallel verification. /// Initialized lazily on first use, torn down via `deinitThreadPool`. @@ -56,481 +57,254 @@ const allocator = if (builtin.mode == .Debug) 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| try v.toBool() 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, .{}); -} - -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 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); +fn formatHex(bytes: []const u8) !js.String { + const hex = try std.fmt.allocPrint(allocator, "0x{x}", .{bytes}); + defer allocator.free(hex); + return js.String.from(hex); } -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(); +fn unwrapClass(comptime T: type, value: js.Value) !*T { + return js.env().unwrap(T, value.toValue()); } -/// 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); - - pk.* = try PublicKey.deserialize(bytes_info.data[0..]); - - if (pk_validate) { - try pk.validate(); - } - - return pk_value; -} - -/// Converts given hex string to a `PublicKey`. +/// Reads a Uint8Array slice from a generic `js.Value`. /// -/// 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; +/// Workaround: `js.Value.asUint8Array` is currently broken in zapi 2.0.0 +/// (it calls a non-existent `expectType` method). Instead we narrow via +/// the underlying `napi.Value` directly. +fn uint8SliceFromValue(value: js.Value) ![]u8 { + const raw = value.toValue(); + if (!(try raw.isTypedarray())) return error.TypeMismatch; + const info = try raw.getTypedarrayInfo(); + if (info.array_type != .uint8) return error.TypeMismatch; + return info.data; } -/// 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 const PublicKey = struct { + pub const js_meta = js.class(.{}); - return try env.getUndefined(); -} + raw: NativePublicKey = .{}, -/// 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 init() PublicKey { + return .{}; } -} - -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(); - - const hex = try std.fmt.allocPrint(allocator, "0x{x}", .{bytes}); - defer allocator.free(hex); - - return try env.createStringUtf8(hex); - } else { - const bytes = pk.serialize(); - const hex = try std.fmt.allocPrint(allocator, "0x{x}", .{bytes}); - defer allocator.free(hex); - - return try env.createStringUtf8(hex); + /// Converts given array of bytes to a `PublicKey`. + /// 1) bytes: Uint8Array + /// 2) pk_validate: ?bool + pub fn fromBytes(bytes: js.Uint8Array, pk_validate: ?js.Boolean) !PublicKey { + const slice = try bytes.toSlice(); + var pk = try NativePublicKey.deserialize(slice); + if (try boolOrDefault(pk_validate, false)) { + try pk.validate(); + } + return .{ .raw = pk }; } -} -pub fn Signature_finalize(_: napi.Env, sig: *Signature, _: ?*anyopaque) void { - allocator.destroy(sig); -} + /// Converts given hex string to a `PublicKey`. + /// 1) bytes: string + /// 2) pk_validate: ?bool + 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); -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(); -} + var bytes_buf: [NativePublicKey.SERIALIZE_SIZE]u8 = undefined; + const bytes = try std.fmt.hexToBytes(&bytes_buf, hex); -/// 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); + var pk = try NativePublicKey.deserialize(bytes); + if (try boolOrDefault(pk_validate, false)) { + try pk.validate(); + } + return .{ .raw = pk }; } - 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; -} - -/// 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); + /// Validates this public key. + pub fn validate(self: *const PublicKey) !void { + try self.raw.validate(); } -} - -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}", .{bytes}); - defer allocator.free(hex); - - return try env.createStringUtf8(hex); - } else { - const bytes = sig.serialize(); - - const hex = try std.fmt.allocPrint(allocator, "0x{x}", .{bytes}); - defer allocator.free(hex); - - return try env.createStringUtf8(hex); + /// Serializes this public key to bytes. + pub fn toBytes(self: *const PublicKey, compress: ?js.Boolean) !js.Uint8Array { + if (try boolOrDefault(compress, true)) { + const bytes = self.raw.compress(); + return js.Uint8Array.from(bytes[0..]); + } + const bytes = self.raw.serialize(); + return js.Uint8Array.from(bytes[0..]); } -} -pub fn SecretKey_finalize(_: napi.Env, sk: *SecretKey, _: ?*anyopaque) void { - allocator.destroy(sk); -} + pub fn toHex(self: *const PublicKey, compress: ?js.Boolean) !js.String { + if (try boolOrDefault(compress, true)) { + const bytes = self.raw.compress(); + return formatHex(bytes[0..]); + } + const bytes = self.raw.serialize(); + return formatHex(bytes[0..]); + } +}; -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 const Signature = struct { + pub const js_meta = js.class(.{}); -/// 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(); + raw: NativeSignature = .{}, - 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; - - return sk_value; -} + /// Converts given array of bytes to a `Signature`. + 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; + if (try boolOrDefault(sig_validate, false)) { + try sig.validate(try boolOrDefault(sig_infcheck, false)); + } + return .{ .raw = sig }; + } -/// Creates a `SecretKey` from a hex string. -pub fn SecretKey_fromHex(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { - const ctor = cb.this(); + /// Converts given hex string to a `Signature`. + /// + /// If `sig_validate` is `true`, the signature will be infinity and group checked. + /// If `sig_infcheck` is `false`, the infinity check will be skipped. + 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 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); + var bytes_buf: [NativeSignature.SERIALIZE_SIZE]u8 = undefined; + const bytes = try std.fmt.hexToBytes(&bytes_buf, hex); - 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 sig = NativeSignature.deserialize(bytes) catch return error.DeserializationFailed; + if (try boolOrDefault(sig_validate, false)) { + try sig.validate(try boolOrDefault(sig_infcheck, false)); + } + return .{ .raw = sig }; + } - return sk_value; -} + /// Aggregates multiple Signature objects into one. + /// 1) sigs_array: Signature[] + /// 2) sigs_groupcheck: ?bool + pub fn aggregate(signatures: js.Array, sigs_groupcheck: ?js.Boolean) !Signature { + const signatures_len = try signatures.length(); + if (signatures_len == 0) return error.EmptySignatureArray; -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 sigs = try allocator.alloc(NativeSignature, signatures_len); + defer allocator.free(sigs); - const hex = try std.fmt.allocPrint(allocator, "0x{x}", .{bytes}); - defer allocator.free(hex); + for (0..signatures_len) |i| { + const wrapped = try unwrapClass(Signature, try signatures.get(@intCast(i))); + sigs[i] = wrapped.raw; + } - return try env.createStringUtf8(hex); -} + const agg_sig = AggregateSignature.aggregate(sigs, try boolOrDefault(sigs_groupcheck, false)) catch + return error.AggregationFailed; -/// 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; -} + return .{ .raw = agg_sig.toSignature() }; + } -/// 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(); + /// Serializes this signature to bytes. + pub fn toBytes(self: *const Signature, compress: ?js.Boolean) !js.Uint8Array { + if (try boolOrDefault(compress, true)) { + const bytes = self.raw.compress(); + return js.Uint8Array.from(bytes[0..]); + } + const bytes = self.raw.serialize(); + return js.Uint8Array.from(bytes[0..]); + } - const sig_value = try newSignatureInstance(env); - const sig = try env.unwrap(Signature, sig_value); - sig.* = sk.sign(msg.data, DST, null); + pub fn toHex(self: *const Signature, compress: ?js.Boolean) !js.String { + if (try boolOrDefault(compress, true)) { + const bytes = self.raw.compress(); + return formatHex(bytes[0..]); + } + const bytes = self.raw.serialize(); + return formatHex(bytes[0..]); + } - return sig_value; -} + /// Validates the signature. + /// Throws an error if the signature is invalid. + pub fn validate(self: *const Signature, sig_infcheck: js.Boolean) !void { + self.raw.validate(try sig_infcheck.toBool()) catch return error.InvalidSignature; + } +}; -/// 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()); +pub const SecretKey = struct { + pub const js_meta = js.class(.{}); - const pk_value = try newPublicKeyInstance(env); - const pk = try env.unwrap(PublicKey, pk_value); - pk.* = sk.toPublicKey(); + raw: NativeSecretKey = .{}, - return pk_value; -} + pub fn init() SecretKey { + return .{}; + } -/// 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(); + /// Creates a `SecretKey` from raw bytes. + pub fn fromBytes(bytes: js.Uint8Array) !SecretKey { + const slice = try bytes.toSlice(); + if (slice.len != NativeSecretKey.serialize_size) { + return error.InvalidSecretKeyLength; + } + 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); -} + /// Creates a `SecretKey` from a hex string. + 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; + /// Generates a `SecretKey` from a seed (IKM) using key derivation. + /// Seed must be at least 32 bytes. + 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; + break :blk try uint8SliceFromValue(value); + } 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; -} + /// Signs a message with this `SecretKey`, returns a `Signature`. + 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)); + /// Derives the PublicKey from this SecretKey. + pub fn toPublicKey(self: *const SecretKey) !PublicKey { + return .{ .raw = self.raw.toPublicKey() }; + } - sig.validate(sig_infcheck) catch return error.InvalidSignature; + /// Serializes the SecretKey to bytes (32 bytes). + 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(); + return formatHex(bytes[0..]); + } +}; /// Verifies a given `msg` against a `Signature` and a `PublicKey`. /// @@ -542,24 +316,19 @@ pub fn Signature_validate(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { /// 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 try env.getBoolean(true); +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(); + + sig.raw.verify( + try boolOrDefault(sig_groupcheck, false), + msg_slice, + DST, + null, + &pk.raw, + try 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. @@ -568,52 +337,45 @@ pub fn blst_verify(env: napi.Env, cb: napi.CallbackInfo(5)) !napi.Value { /// 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(); +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 pk_ptrs = try allocator.alloc(*PublicKey, pks_len); + const msg_bufs = try allocator.alloc([32]u8, msgs_len); + defer allocator.free(msg_bufs); + + const pk_ptrs = try allocator.alloc(*NativePublicKey, pks_len); defer allocator.free(pk_ptrs); 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 msg_value = try msgs.get(@intCast(i)); + const msg_bytes = try uint8SliceFromValue(msg_value); + if (msg_bytes.len != 32) return error.InvalidMessageLength; + @memcpy(&msg_bufs[i], msg_bytes[0..32]); - const pk_value = try pks_array.getElement(@intCast(i)); - pk_ptrs[i] = try env.unwrap(PublicKey, pk_value); + const wrapped_pk = try unwrapClass(PublicKey, try pks.get(@intCast(i))); + pk_ptrs[i] = &wrapped_pk.raw; } - const pool = thread_pool orelse @panic("ThreadPool not initialized; call initThreadPool first"); - const result = pool.aggregateVerify(napi_io.get(), sig, sig_groupcheck, msgs, DST, pk_ptrs, pks_validate) catch { - return try env.getBoolean(false); - }; + const pool = thread_pool orelse return error.ThreadPoolNotInitialized; + const result = pool.aggregateVerify( + napi_io.get(), + &sig.raw, + try boolOrDefault(sig_groupcheck, false), + msg_bufs, + DST, + pk_ptrs, + try 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. +/// 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. /// @@ -622,38 +384,33 @@ pub fn blst_aggregateVerify( /// 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 try env.getBoolean(false); - } +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 = try allocator.alloc(PublicKey, pks_len); - defer allocator.free(pks); + const pks_len = try pks.length(); + if (pks_len == 0) return js.Boolean.from(false); + + 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( + try 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. @@ -663,55 +420,44 @@ pub fn blst_fastAggregateVerify(env: napi.Env, cb: napi.CallbackInfo(4)) !napi.V /// 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 try env.getBoolean(false); - } +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 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); defer allocator.free(rands); - var seed_bytes2: [8]u8 = undefined; - const io2 = napi_io.get(); - io2.random(&seed_bytes2); - var prng = std.Random.DefaultPrng.init(std.mem.readInt(u64, &seed_bytes2, .little)); + var seed_bytes: [8]u8 = undefined; + const io = napi_io.get(); + io.random(&seed_bytes); + var prng = std.Random.DefaultPrng.init(std.mem.readInt(u64, &seed_bytes, .little)); const rand = prng.random(); + const e = js.env(); for (0..n_elems) |i| { - const set_value = try sets.getElement(@intCast(i)); + const set = (try sets.get(@intCast(i))).toValue(); - 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_napi = try set.getNamedProperty("msg"); + const msg_bytes = try uint8SliceFromValue(.{ .val = msg_napi }); + 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 pk_napi = try set.getNamedProperty("pk"); + const wrapped_pk = try e.unwrap(PublicKey, pk_napi); + pks[i] = &wrapped_pk.raw; - const sig_value = try set_value.getNamedProperty("sig"); - sigs[i] = try env.unwrap(Signature, sig_value); + const sig_napi = try set.getNamedProperty("sig"); + const wrapped_sig = try e.unwrap(Signature, sig_napi); + sigs[i] = &wrapped_sig.raw; rand.bytes(&rands[i]); // Ensure first 8 bytes (RAND_BITS=64) are non-zero @@ -720,22 +466,20 @@ pub fn blst_verifyMultipleAggregateSignatures(env: napi.Env, cb: napi.CallbackIn } } - const pool = thread_pool orelse @panic("ThreadPool not initialized; call initThreadPool first"); + const pool = thread_pool orelse return error.ThreadPoolNotInitialized; const result = pool.verifyMultipleAggregateSignatures( napi_io.get(), n_elems, msgs, DST, pks, - pks_validate, + try boolOrDefault(pks_validate, false), sigs, - sigs_groupcheck, + try 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. @@ -744,35 +488,22 @@ pub fn blst_verifyMultipleAggregateSignatures(env: napi.Env, cb: napi.CallbackIn /// 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(); +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, try boolOrDefault(sigs_groupcheck, false)) catch + return error.AggregationFailed; - return sig_value; + return .{ .raw = agg_sig.toSignature() }; } /// Aggregate multiple `PublicKey` objects into one. @@ -780,104 +511,72 @@ pub fn blst_aggregateSignatures(env: napi.Env, cb: napi.CallbackInfo(2)) !napi.V /// 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 pks_validate: bool = if (cb.getArg(1)) |v| - try coerceToBool(v) - else - false; - - if (pks_len == 0) { - return error.EmptyPublicKeyArray; - } +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 = 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 = try unwrapClass(PublicKey, try pks.get(@intCast(i))); + native_pks[i] = wrapped.raw; } - const agg_pk = AggregatePublicKey.aggregate(pks, pks_validate) catch return error.AggregationFailed; - const result_pk = agg_pk.toPublicKey(); + const agg_pk = AggregatePublicKey.aggregate(native_pks, try boolOrDefault(pks_validate, false)) catch + return error.AggregationFailed; - const pk_value = try newPublicKeyInstance(env); - const pk = try env.unwrap(PublicKey, pk_value); - pk.* = result_pk; - - 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; - +/// 2) pks_validate: ?bool +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 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_bytes_value = try pks_array.getElement(@intCast(i)); - const bytes_info = try pk_bytes_value.getTypedarrayInfo(); - - pks[i] = PublicKey.deserialize(bytes_info.data) catch - return error.DeserializationFailed; + const bytes = try uint8SliceFromValue(try serialized_public_keys.get(@intCast(i))); + 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; - - return pk_value; -} + const agg_pk = AggregatePublicKey.aggregate(native_pks, try boolOrDefault(pks_validate, false)) catch + return error.AggregationFailed; -/// 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 .{ .raw = agg_pk.toPublicKey() }; } -const MAX_AGGREGATE_PER_JOB = bls.MAX_AGGREGATE_PER_JOB; - /// Synchronously aggregates public keys and signatures with randomness using -/// Pippenger multi-scalar multiplication. Runs on the worker thread. +/// Pippenger multi-scalar multiplication. Runs on the calling 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(); - +/// +/// TODO(zapi#23): once the DSL supports returning a struct of class instances, +/// change the return type to `!struct { pk: PublicKey, sig: Signature }` and +/// drop the manual `createObject` + `convertReturn` + `setNamedProperty` plumbing +/// at the bottom of this function. +/// See https://github.com/ChainSafe/zapi/issues/23 +pub fn aggregateWithRandomness(sets: js.Array) !js.Value { + const n = try sets.length(); if (n == 0) return error.EmptyArray; if (n > MAX_AGGREGATE_PER_JOB) return error.TooManySets; 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; + var pk_ptrs: [MAX_AGGREGATE_PER_JOB]*const NativePublicKey = undefined; + var sigs: [MAX_AGGREGATE_PER_JOB]NativeSignature = undefined; + var sig_ptrs: [MAX_AGGREGATE_PER_JOB]*const NativeSignature = undefined; - // Generate 8-byte scalars (64 bits each) using a fast PRNG seeded from OS entropy var seed_bytes: [8]u8 = undefined; const io = napi_io.get(); io.random(&seed_bytes); @@ -887,16 +586,17 @@ pub fn blst_aggregateWithRandomness(env: napi.Env, cb: napi.CallbackInfo(1)) !na var sca_ptrs: [MAX_AGGREGATE_PER_JOB]*const u8 = undefined; rand.bytes(scalars[0 .. n * nbytes]); + const env = js.env(); for (0..n) |i| { - const set_value = try sets.getElement(@intCast(i)); + const set = (try sets.get(@intCast(i))).toValue(); - const pk_value = try set_value.getNamedProperty("pk"); - const unwrapped_pk = try env.unwrap(PublicKey, pk_value); - pk_ptrs[i] = unwrapped_pk; + const pk_napi = try set.getNamedProperty("pk"); + const wrapped_pk = try env.unwrap(PublicKey, pk_napi); + pk_ptrs[i] = &wrapped_pk.raw; - 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; + const sig_napi = try set.getNamedProperty("sig"); + const sig_bytes = try uint8SliceFromValue(.{ .val = sig_napi }); + sigs[i] = NativeSignature.deserialize(sig_bytes[0..]) catch return error.DeserializationFailed; sigs[i].validate(true) catch return error.InvalidSignature; sig_ptrs[i] = &sigs[i]; @@ -923,7 +623,7 @@ pub fn blst_aggregateWithRandomness(env: napi.Env, cb: napi.CallbackInfo(1)) !na nbits, scratch.ptr, ); - var result_pk: PublicKey = .{}; + var result_pk: NativePublicKey = .{}; bls.c.blst_p1_to_affine(&result_pk.point, &p1_ret); // Pippenger multi-scalar multiplication on G2 (signatures) @@ -936,98 +636,14 @@ pub fn blst_aggregateWithRandomness(env: napi.Env, cb: napi.CallbackInfo(1)) !na nbits, scratch.ptr, ); - var result_sig: Signature = .{}; + var result_sig: NativeSignature = .{}; 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 pk_value = napi.Value{ .env = env.env, .value = js.convertReturn(PublicKey, .{ .raw = result_pk }, env.env) }; + const sig_value = napi.Value{ .env = env.env, .value = js.convertReturn(Signature, .{ .raw = result_sig }, env.env) }; 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 { - 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), - }); - try pk_ctor.setNamedProperty("COMPRESS_SIZE", try env.createUint32(@intCast(PublicKey.COMPRESS_SIZE))); - try pk_ctor.setNamedProperty("SERIALIZE_SIZE", try env.createUint32(@intCast(PublicKey.SERIALIZE_SIZE))); - - 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), - }); - try sig_ctor.setNamedProperty("COMPRESS_SIZE", try env.createUint32(@intCast(Signature.COMPRESS_SIZE))); - try sig_ctor.setNamedProperty("SERIALIZE_SIZE", try env.createUint32(@intCast(Signature.SERIALIZE_SIZE))); - - 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); + try result.setNamedProperty("pk", pk_value); + try result.setNamedProperty("sig", sig_value); + return .{ .val = result }; } diff --git a/bindings/napi/pubkeys.zig b/bindings/napi/pubkeys.zig index 3321077b0..c85344947 100644 --- a/bindings/napi/pubkeys.zig +++ b/bindings/napi/pubkeys.zig @@ -1,11 +1,9 @@ const std = @import("std"); -const napi = @import("zapi:zapi").napi; +const js = @import("zapi:zapi").js; 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; const napi_io = @import("./io.zig"); /// Uses page allocator for internal allocations. @@ -71,16 +69,15 @@ 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); + const path = try file_path.toSlice(&file_path_buf); const io = napi_io.get(); - const file = try std.Io.Dir.createFile(.cwd(), io, file_path, .{}); + const file = try std.Io.Dir.createFile(.cwd(), io, path, .{}); defer file.close(io); - // 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); @@ -93,22 +90,20 @@ 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 writer.writeAll(slice); + try writer.writeAll(ptr[0..p2i_size]); // Write index2pubkey entries try writer.writeAll(std.mem.sliceAsBytes(state.index2pubkey.items)); try file_writer.end(); - - 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); + const path = try file_path.toSlice(&file_path_buf); const io = napi_io.get(); - const file = try std.Io.Dir.openFile(.cwd(), io, file_path, .{}); + const file = try std.Io.Dir.openFile(.cwd(), io, path, .{}); defer file.close(io); if (state.initialized) { @@ -145,119 +140,85 @@ 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 p2i_slice = ptr[0..p2i_size]; - try file_reader.interface.readSliceAll(p2i_slice); + try file_reader.interface.readSliceAll(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_reader.interface.readSliceAll(index2pubkey_bytes); + try file_reader.interface.readSliceAll(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 { - if (!state.initialized) { - return error.PubkeyIndexNotInitialized; - } +/// JS: pubkeys.getIndex(pubkeyBytes) → number | null +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) { - return error.InvalidPubkeyLength; - } + 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| { + return .{ .val = try e.createUint32(@intCast(index)) }; + } + return .{ .val = try e.getNull() }; } -pub fn pubkeys_get(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { - if (!state.initialized) { - return error.PubkeyIndexNotInitialized; - } +/// JS: pubkeys.get(index) → PublicKey | undefined +pub fn get(index: js.Number) !?blst_bindings.PublicKey { + if (!state.initialized) return error.PubkeyIndexNotInitialized; - const index = try cb.arg(0).getValueUint32(); - if (index >= state.index2pubkey.items.len) { - return env.getUndefined(); - } + const idx = try index.toU32(); + if (idx >= state.index2pubkey.items.len) return null; - 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; + return .{ .raw = state.index2pubkey.items[@intCast(idx)] }; } -pub fn pubkeys_set(env: napi.Env, cb: napi.CallbackInfo(2)) !napi.Value { - if (!state.initialized) { - return error.PubkeyIndexNotInitialized; - } +/// 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) { - return error.InvalidPubkeyLength; - } + const idx = try index.toU32(); + 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(allocator, new_cap); } // Extend length if needed - if (index >= state.index2pubkey.items.len) { - try state.index2pubkey.resize(allocator, index + 1); + if (idx >= state.index2pubkey.items.len) { + try state.index2pubkey.resize(allocator, 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 { - if (!state.initialized) { - return error.PubkeyIndexNotInitialized; - } - - return try env.createUint32(@intCast(state.index2pubkey.items.len)); +/// JS: pubkeys.size() → number +/// Note: zapi DSL does not yet support namespace-level getters, so this is a function. +pub fn size() !js.Number { + if (!state.initialized) return error.PubkeyIndexNotInitialized; + return js.Number.from(@as(u32, @intCast(state.index2pubkey.items.len))); } -pub fn pubkeys_ensureCapacity(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { - if (!state.initialized) { - return error.PubkeyIndexNotInitialized; - } +/// JS: pubkeys.ensureCapacity(newSize) +pub fn ensureCapacity(new_size: js.Number) !void { + if (!state.initialized) return error.PubkeyIndexNotInitialized; + const requested = try new_size.toU32(); const old_size = state.index2pubkey.capacity; - const new_size = try cb.arg(0).getValueUint32(); - if (new_size <= old_size) { - return env.getUndefined(); - } - try state.pubkey2index.ensureTotalCapacity(new_size); - try state.index2pubkey.ensureTotalCapacity(allocator, 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), - }); + if (requested <= old_size) return; - try exports.setNamedProperty("pubkeys", pubkeys_obj); + try state.pubkey2index.ensureTotalCapacity(requested); + try state.index2pubkey.ensureTotalCapacity(allocator, requested); } diff --git a/bindings/napi/root.zig b/bindings/napi/root.zig index b2f91d240..53bfe33ba 100644 --- a/bindings/napi/root.zig +++ b/bindings/napi/root.zig @@ -1,15 +1,14 @@ const std = @import("std"); const js = @import("zapi:zapi").js; -const napi = @import("zapi:zapi").napi; pub const pool = @import("./pool.zig"); pub const shuffle = @import("./shuffle.zig"); pub const config = @import("./config.zig"); pub const metrics = @import("./metrics.zig"); pub const stateTransition = @import("./stateTransition.zig"); pub const BeaconStateView = @import("./BeaconStateView.zig"); +pub const blst = @import("./blst.zig"); +pub const pubkeys = @import("./pubkeys.zig"); -const pubkeys = @import("./pubkeys.zig"); -const blst = @import("./blst.zig"); const options = @import("bls_options"); const napi_io = @import("./io.zig"); @@ -46,11 +45,6 @@ fn cleanup(new_ref_count: u32) void { } } -fn register(env: napi.Env, exports: napi.Value) !void { - try blst.register(env, exports); - try pubkeys.register(env, exports); -} - comptime { - js.exportModule(@This(), .{ .init = init, .cleanup = cleanup, .register = register }); + js.exportModule(@This(), .{ .init = init, .cleanup = cleanup }); } diff --git a/bindings/src/pubkeys.js b/bindings/src/pubkeys.js index 1a981920f..572ae55e0 100644 --- a/bindings/src/pubkeys.js +++ b/bindings/src/pubkeys.js @@ -36,7 +36,7 @@ export const pubkeyCache = { }, get size() { - return native.size; + return native.size(); }, load(filepath) {