Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
266 changes: 0 additions & 266 deletions bindings/napi/blst.zig
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,6 @@
//! `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 `state.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 builtin = @import("builtin");
const zapi = @import("zapi:zapi");
Expand Down Expand Up @@ -566,185 +562,6 @@ pub fn aggregateSerializedPublicKeys(serialized_public_keys: js.Array, pks_valid
return .{ .raw = agg_pk.toPublicKey() };
}

/// Synchronously aggregates public keys and signatures with randomness using
/// Pippenger multi-scalar multiplication. Runs on the calling thread.
///
/// Arguments:
/// 1) sets: Array of {pk: PublicKey, sig: Uint8Array}
///
/// Returns: {pk: PublicKey, sig: Signature}
///
/// 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 NativePublicKey = undefined;
var sigs: [MAX_AGGREGATE_PER_JOB]NativeSignature = undefined;
var sig_ptrs: [MAX_AGGREGATE_PER_JOB]*const NativeSignature = undefined;

const io = js.io();
var scalars: [8 * MAX_AGGREGATE_PER_JOB]u8 = undefined;
var sca_ptrs: [MAX_AGGREGATE_PER_JOB]*const u8 = undefined;
io.random(scalars[0 .. n * nbytes]);

const env = js.env();
for (0..n) |i| {
const set_value = try sets.get(@intCast(i));
const set = try (try set_value.asObject(RandomizedAggregationInput)).get();
const public_key = try unwrapClass(PublicKey, set.pk);
pk_ptrs[i] = &public_key.raw;

const signature_bytes = try set.sig.toSlice();
sigs[i] = NativeSignature.deserialize(signature_bytes[0..]) catch return error.DeserializationFailed;
sigs[i].validate(true) catch return error.InvalidSignature;
sig_ptrs[i] = &sigs[i];

const scalar = scalars[i * nbytes ..][0..nbytes];
try bls_verifier.ensureNonzeroRandomScalar(io, scalar);
sca_ptrs[i] = &scalars[i * nbytes];
}

const scratch_size_bytes = @max(
bls.c.blst_p1s_mult_pippenger_scratch_sizeof(n),
bls.c.blst_p2s_mult_pippenger_scratch_sizeof(n),
);
const scratch_len = @divExact(scratch_size_bytes, @sizeOf(u64));

const scratch = try allocator.alloc(u64, scratch_len);
defer allocator.free(scratch);

// 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: NativePublicKey = .{};
bls.c.blst_p1_to_affine(&result_pk.point, &p1_ret);

// Pippenger multi-scalar multiplication on G2 (signatures)
var p2_ret: bls.c.blst_p2 = std.mem.zeroes(bls.c.blst_p2);
bls.c.blst_p2s_mult_pippenger(
&p2_ret,
@ptrCast(&sig_ptrs),
n,
@ptrCast(&sca_ptrs),
nbits,
scratch.ptr,
);
var result_sig: NativeSignature = .{};
bls.c.blst_p2_to_affine(&result_sig.point, &p2_ret);

const pk_value = napi.Value{ .env = env.env, .value = js.convertReturn(PublicKey, AddonIdentity, .{ .raw = result_pk }, env.env) };
const sig_value = napi.Value{ .env = env.env, .value = js.convertReturn(Signature, AddonIdentity, .{ .raw = result_sig }, env.env) };

const result = try env.createObject();
try result.setNamedProperty("pk", pk_value);
try result.setNamedProperty("sig", sig_value);
return .{ .val = result };
}

/// Heap-allocated context shared between the JS thread (which kicks off the work),
/// the libuv worker thread (which calls `ThreadPool.aggregateWithRandomness`), and
/// the JS thread again (which resolves/rejects the Promise).
///
/// All input data should be copied into this struct so the worker thread doesn't depend on
/// any JS-managed memory staying alive.
const AsyncAggRandData = struct {
n: usize,
pks: [MAX_AGGREGATE_PER_JOB]NativePublicKey,
sigs: [MAX_AGGREGATE_PER_JOB]NativeSignature,
pk_ptrs: [MAX_AGGREGATE_PER_JOB]*const NativePublicKey,
sig_ptrs: [MAX_AGGREGATE_PER_JOB]*const NativeSignature,
randomness: [MAX_AGGREGATE_PER_JOB * 32]u8,
pk_out: NativePublicKey,
sig_out: NativeSignature,
err: ?anyerror,
deferred: napi.Deferred,
work: napi.c.napi_async_work,

fn destroy(self: *AsyncAggRandData) void {
allocator.destroy(self);
}
};

/// Execute `aggregateWithRandomness` on a libuv worker thread.
///
/// Assumes that:
/// 1) pubkeys are already validated,
/// 2) signatures are not group-checked on JS thread
///
/// Note: MUST NOT call any napi APIs.
fn asyncAggRand_execute(_: napi.Env, data: *AsyncAggRandData) void {
const pool = state.thread_pool orelse {
data.err = error.PoolNotInitialized;
return;
};
pool.aggregateWithRandomness(
js.io(),
data.pk_ptrs[0..data.n],
data.sig_ptrs[0..data.n],
data.randomness[0 .. data.n * 32],
false, // pks already validated implicitly by being deserialized PublicKey instances
true, // sigs were deserialized but not group-checked on the JS thread
&data.pk_out,
&data.sig_out,
) catch |err| {
data.err = err;
};
}

/// Ran on the JS thread once the worker has finished. Always settles the
/// promise — `settle` does the resolve/reject; if it errors we fall back to a
/// bare reject so callers never see a dangling Promise.
fn asyncAggRand_complete(env: napi.Env, status: napi.status.Status, data: *AsyncAggRandData) void {
defer {
napi.status.check(napi.c.napi_delete_async_work(env.env, data.work)) catch {};
data.destroy();
}

settle(env, status, data) catch {
// Catch all rejection: if we reach this point we might want
// better errors upstream
rejectWithError(env, data.deferred, "asyncAggregateWithRandomness", "InternalError") catch {};
};
}

fn settle(env: napi.Env, status: napi.status.Status, data: *AsyncAggRandData) !void {
if (status != .ok) {
// libuv's async work itself failed (e.g. cancelled), not crypto.
return rejectWithError(env, data.deferred, "asyncAggregateWithRandomness/asyncWork", @tagName(status));
}
if (data.err) |err| {
// Worker captured a Zig error — surface its name as the JS Error.code
// (e.g. "PointNotInGroup", "PoolNotInitialized", "OutOfMemory") so JS
// callers can branch on it.
return rejectWithError(env, data.deferred, "asyncAggregateWithRandomness", @errorName(err));
}

const pk_value = napi.Value{ .env = env.env, .value = js.convertReturn(PublicKey, AddonIdentity, .{ .raw = data.pk_out }, env.env) };
const sig_value = napi.Value{ .env = env.env, .value = js.convertReturn(Signature, AddonIdentity, .{ .raw = data.sig_out }, env.env) };

const result = try env.createObject();
try result.setNamedProperty("pk", pk_value);
try result.setNamedProperty("sig", sig_value);

try data.deferred.resolve(result);
}

/// Build a JS `Error` with `.code = code` and `.message = "<where>: <code>"`
/// and reject `deferred` with it. JS callers see a real `Error` instance, not
/// a bare string, so they can branch on `err.code` cleanly.
Expand All @@ -757,86 +574,3 @@ fn rejectWithError(env: napi.Env, deferred: napi.Deferred, where: []const u8, co
const err_val = try env.createError(code_val, msg_val);
try deferred.reject(err_val);
}

/// Asynchronously aggregates public keys and signatures with randomness using
/// Pippenger multi-scalar multiplication. The PK and Sig multi-scalar mults
/// run in parallel on the bls `ThreadPool`.
///
/// This call is non-blocking.
///
/// This is modeled after blst's rust pippenger implementation.
///
/// See: https://github.com/supranational/blst/blob/dece82ea537b422890888bacde4034ca5b5a44d8/bindings/rust/src/pippenger.rs
///
/// Arguments:
/// 1) sets: Array of {pk: PublicKey, sig: Uint8Array}
///
/// Returns: Promise<{pk: PublicKey, sig: Signature}>
pub fn asyncAggregateWithRandomness(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;
if (state.thread_pool == null) return error.PoolNotInitialized;

const env = js.env();

const data = try allocator.create(AsyncAggRandData);
errdefer allocator.destroy(data);

data.n = n;
data.pk_out = .{};
data.sig_out = .{};
data.err = null;
data.deferred = undefined;
data.work = undefined;

const io = js.io();
io.random(data.randomness[0 .. n * 32]);
for (0..n) |i| {
const scalar = data.randomness[i * 32 ..][0..8];
try bls_verifier.ensureNonzeroRandomScalar(io, scalar);
}

for (0..n) |i| {
const set_value = try sets.get(@intCast(i));
const set = try (try set_value.asObject(RandomizedAggregationInput)).get();
const public_key = try unwrapClass(PublicKey, set.pk);
data.pks[i] = public_key.raw;
data.pk_ptrs[i] = &data.pks[i];

const signature_bytes = try set.sig.toSlice();
data.sigs[i] = NativeSignature.deserialize(signature_bytes[0..]) catch return error.DeserializationFailed;
data.sig_ptrs[i] = &data.sigs[i];
}

const deferred_cleanup_value = try env.getUndefined();
const resource_name = try env.createStringUtf8("asyncAggregateWithRandomness");

// Until queue succeeds, this function owns the unqueued work handle. Deletion should
// not fail after successful creation. If that invariant breaks, later error cleanup may
// free `data` while the work handle still points to it.
const work = try env.createAsyncWork(
AsyncAggRandData,
null,
resource_name,
asyncAggRand_execute,
asyncAggRand_complete,
data,
);
errdefer work.delete() catch |err| {
std.log.err("failed to delete unqueued async BLS work: {s}", .{@errorName(err)});
};

data.work = work.work;

// Settle the unreturned Promise so Node can release its deferred handle.
data.deferred = try env.createPromise();
errdefer data.deferred.resolve(deferred_cleanup_value) catch |err| {
std.log.err("failed to settle unreturned async BLS promise: {s}", .{@errorName(err)});
};

try work.queue();

return .{ .val = data.deferred.getPromise() };
}
56 changes: 0 additions & 56 deletions bindings/perf/blst.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ import {
aggregatePublicKeys as aggregatePublicKeysTS,
aggregateSignatures as aggregateSignaturesTS,
aggregateVerify as aggregateVerifyTS,
aggregateWithRandomness as aggregateWithRandomnessTS,
asyncAggregateWithRandomness as asyncAggregateWithRandomnessTS,
verifyMultipleAggregateSignatures as verifyTS,
} from "@chainsafe/blst";
import {
Expand All @@ -18,8 +16,6 @@ import {
aggregatePublicKeys as aggregatePublicKeysZig,
aggregateSignatures as aggregateSignaturesZig,
aggregateVerify as aggregateVerifyZig,
aggregateWithRandomness as aggregateWithRandomnessZig,
asyncAggregateWithRandomness as asyncAggregateWithRandomnessZig,
verifyMultipleAggregateSignatures as verifyZig,
} from "../src/blst.js";

Expand Down Expand Up @@ -152,55 +148,3 @@ describe("verifyMultipleAggregateSignatures", () => {
});
}
});

describe("aggregateWithRandomness", () => {
for (const count of [1, 8, 32, 64, 128]) {
bench({
beforeEach: () => {
const sets = generateZigSets(count);
return sets.map((s) => ({pk: s.pk, sig: s.sig.toBytes()}));
},
fn: (sets) => {
aggregateWithRandomnessZig(sets);
},
id: `aggregateWithRandomness lodestar-z (sync) ${count} sets`,
});

bench({
beforeEach: () => {
const sets = generateTSSets(count);
return sets.map((s) => ({pk: s.pk, sig: s.sig.toBytes()}));
},
fn: (sets) => {
aggregateWithRandomnessTS(sets);
},
id: `aggregateWithRandomness @chainsafe/blst ${count} sets`,
});
}
});

describe("asyncAggregateWithRandomness", () => {
for (const count of [1, 8, 32, 64, 128]) {
bench({
beforeEach: () => {
const sets = generateZigSets(count);
return sets.map((s) => ({pk: s.pk, sig: s.sig.toBytes()}));
},
fn: async (sets) => {
await asyncAggregateWithRandomnessZig(sets);
},
id: `asyncAggregateWithRandomness lodestar-z ${count} sets`,
});

bench({
beforeEach: () => {
const sets = generateTSSets(count);
return sets.map((s) => ({pk: s.pk, sig: s.sig.toBytes()}));
},
fn: async (sets) => {
await asyncAggregateWithRandomnessTS(sets);
},
id: `asyncAggregateWithRandomness @chainsafe/blst ${count} sets`,
});
}
});
13 changes: 0 additions & 13 deletions bindings/src/blst.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,19 +161,6 @@ export function verifyMultipleAggregateSignatures(
sigsGroupcheck?: boolean
): boolean;

/**
* Aggregate multiple public keys and multiple serialized signatures into a single blinded public key and blinded signature.
*
* Signatures are deserialized and validated with infinity and group checks before aggregation.
*/
export declare function aggregateWithRandomness(sets: PkAndSerializedSig[]): PkAndSig;

/**
* Same as `aggregateWithRandomness`, but the multi-scalar multiplications run on the
* native thread pool and the call returns a Promise instead of blocking the JS thread.
*/
export declare function asyncAggregateWithRandomness(sets: PkAndSerializedSig[]): Promise<PkAndSig>;

/**
* Aggregate multiple signatures into a single signature.
*
Expand Down
2 changes: 0 additions & 2 deletions bindings/src/blst.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,3 @@ export const verifyMultipleAggregateSignatures = blst.verifyMultipleAggregateSig
export const aggregateSignatures = blst.aggregateSignatures;
export const aggregatePublicKeys = blst.aggregatePublicKeys;
export const aggregateSerializedPublicKeys = blst.aggregateSerializedPublicKeys;
export const aggregateWithRandomness = blst.aggregateWithRandomness;
export const asyncAggregateWithRandomness = blst.asyncAggregateWithRandomness;
Loading
Loading