From fee4fad3f10dac3cfbf4ebb71e79714a5552501c Mon Sep 17 00:00:00 2001 From: bing Date: Wed, 19 Aug 2026 15:46:27 +0800 Subject: [PATCH 1/2] chore: deprecate unused blst APIs These following calls are no longer used by prod lodestar following ChainSafe/9820 and are redundant exports/impls: - `aggregateWithRandomness` and `asyncAggregateWithRandomness` N-API exports - `AggregatePublicKey.aggregateWithRandomness` - `AggregateSignature.aggregateWithRandomness` ## Rationale I don't think these would ever be called upstream again, any necessary aggregation (w/ randomness) work will be done internally within our ThreadPool impl. In fact the standalone `aggregateWithRandomness` in `AggregateSignature` and `AggregatePublicKey` were no longer called since we are using MSMs now. We remove their impl as well as their exports (if any). The pool version of aggregateWithRandomness is still kept since it is used. --- bindings/napi/blst.zig | 266 ----------------------- bindings/perf/blst.test.ts | 56 ----- bindings/src/blst.d.ts | 13 -- bindings/src/blst.js | 2 - bindings/test/blst.test.ts | 168 -------------- src/bls/AggregatePublicKey.zig | 136 ------------ src/bls/AggregateSignature.zig | 158 -------------- src/bls/ThreadPool.zig | 3 +- test/fuzz/src/fuzz_bls_aggregate_pk.zig | 41 ---- test/fuzz/src/fuzz_bls_aggregate_sig.zig | 41 ---- 10 files changed, 1 insertion(+), 883 deletions(-) diff --git a/bindings/napi/blst.zig b/bindings/napi/blst.zig index 998411b49..fdbc70f7c 100644 --- a/bindings/napi/blst.zig +++ b/bindings/napi/blst.zig @@ -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"); @@ -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 = ": "` /// and reject `deferred` with it. JS callers see a real `Error` instance, not /// a bare string, so they can branch on `err.code` cleanly. @@ -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() }; -} diff --git a/bindings/perf/blst.test.ts b/bindings/perf/blst.test.ts index df2a653b1..99effe7a5 100644 --- a/bindings/perf/blst.test.ts +++ b/bindings/perf/blst.test.ts @@ -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 { @@ -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"; @@ -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`, - }); - } -}); diff --git a/bindings/src/blst.d.ts b/bindings/src/blst.d.ts index da8d0b8f4..68b09806b 100644 --- a/bindings/src/blst.d.ts +++ b/bindings/src/blst.d.ts @@ -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; - /** * Aggregate multiple signatures into a single signature. * diff --git a/bindings/src/blst.js b/bindings/src/blst.js index cc7a4d6ec..84d60728c 100644 --- a/bindings/src/blst.js +++ b/bindings/src/blst.js @@ -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; diff --git a/bindings/test/blst.test.ts b/bindings/test/blst.test.ts index 7d32df6f1..7b695170c 100644 --- a/bindings/test/blst.test.ts +++ b/bindings/test/blst.test.ts @@ -7,8 +7,6 @@ import { aggregatePublicKeys, aggregateSerializedPublicKeys, aggregateVerify, - aggregateWithRandomness, - asyncAggregateWithRandomness, fastAggregateVerify, verify, verifyMultipleAggregateSignatures, @@ -417,144 +415,6 @@ describe("blst", () => { expect(() => aggregateSerializedPublicKeys([new Uint8Array(32)])).toThrow(); }); }); - - describe("aggregateWithRandomness", () => { - it("should return aggregated pk and sig", () => { - const {_, sets} = getTestSetsSameMessage(8); - const input = sets.map((s) => ({pk: s.pk, sig: s.sig.toBytes()})); - const result = aggregateWithRandomness(input); - expect(result).toHaveProperty("pk"); - expect(result).toHaveProperty("sig"); - expect(result.pk).toBeInstanceOf(PublicKey); - }); - - it("should produce a valid aggregated signature", () => { - const {msg, sets} = getTestSetsSameMessage(8); - const input = sets.map((s) => ({pk: s.pk, sig: s.sig.toBytes()})); - const {pk, sig} = aggregateWithRandomness(input); - const isValid = verify(msg, pk, sig, false, false); - expect(isValid).toBe(true); - }); - - it("should work with a single set", () => { - const {msg, sets} = getTestSetsSameMessage(1); - const input = sets.map((s) => ({pk: s.pk, sig: s.sig.toBytes()})); - const {pk, sig} = aggregateWithRandomness(input); - const isValid = verify(msg, pk, sig, false, false); - expect(isValid).toBe(true); - }); - - it("should throw on empty input", () => { - expect(() => aggregateWithRandomness([])).toThrow(); - }); - - it("should reject invalid signature bytes", () => { - const {sets} = getTestSetsSameMessage(4); - const input = sets.map((s) => ({pk: s.pk, sig: s.sig.toBytes()})); - input[2].sig = new Uint8Array(96).fill(0xff); - expect(() => aggregateWithRandomness(input)).toThrow(); - }); - - it("should reject objects of the wrong class", () => { - const {sets} = getTestSetsSameMessage(1); - const input = [{pk: sets[0].sk as unknown as PublicKey, sig: sets[0].sig.toBytes()}]; - expect(() => aggregateWithRandomness(input)).toThrow("TypeMismatch"); - }); - }); - - describe("asyncAggregateWithRandomness", () => { - it("should be exported as a function", () => { - expect(typeof asyncAggregateWithRandomness).toBe("function"); - }); - - it("should return a Promise", () => { - const {sets} = getTestSetsSameMessage(2); - const input = sets.map((s) => ({pk: s.pk, sig: s.sig.toBytes()})); - const result = asyncAggregateWithRandomness(input); - expect(result).toBeInstanceOf(Promise); - return result; - }); - - it("should resolve with aggregated pk and sig instances", async () => { - const {sets} = getTestSetsSameMessage(8); - const input = sets.map((s) => ({pk: s.pk, sig: s.sig.toBytes()})); - const result = await asyncAggregateWithRandomness(input); - expect(result).toHaveProperty("pk"); - expect(result).toHaveProperty("sig"); - expect(result.pk).toBeInstanceOf(PublicKey); - expect(result.sig).toBeInstanceOf(Signature); - }); - - it("should produce a valid aggregated signature - small MSM", async () => { - const {msg, sets} = getTestSetsSameMessage(8); - const input = sets.map((s) => ({pk: s.pk, sig: s.sig.toBytes()})); - const {pk, sig} = await asyncAggregateWithRandomness(input); - expect(verify(msg, pk, sig, false, false)).toBe(true); - }); - - it("should produce a valid aggregated signature - tiled MSM", async () => { - const {msg, sets} = getTestSetsSameMessage(33); - const input = sets.map((s) => ({pk: s.pk, sig: s.sig.toBytes()})); - const {pk, sig} = await asyncAggregateWithRandomness(input); - expect(verify(msg, pk, sig, false, false)).toBe(true); - }); - - it("should work with a single set", async () => { - const {msg, sets} = getTestSetsSameMessage(1); - const input = sets.map((s) => ({pk: s.pk, sig: s.sig.toBytes()})); - const {pk, sig} = await asyncAggregateWithRandomness(input); - expect(verify(msg, pk, sig, false, false)).toBe(true); - }); - - it("should fail verification against a different message", async () => { - const {sets} = getTestSetsSameMessage(4); - const input = sets.map((s) => ({pk: s.pk, sig: s.sig.toBytes()})); - const {pk, sig} = await asyncAggregateWithRandomness(input); - const wrongMessage = new Uint8Array(32).fill(0); - expect(verify(wrongMessage, pk, sig, false, false)).toBe(false); - }); - - it("should match the synchronous aggregateWithRandomness verification result", async () => { - const {msg, sets} = getTestSetsSameMessage(6); - const input = sets.map((s) => ({pk: s.pk, sig: s.sig.toBytes()})); - const syncResult = aggregateWithRandomness(input); - const asyncResult = await asyncAggregateWithRandomness(input); - // Randomness differs between calls so signatures aren't byte-equal, - // but both must verify against the shared message. - expect(verify(msg, syncResult.pk, syncResult.sig, false, false)).toBe(true); - expect(verify(msg, asyncResult.pk, asyncResult.sig, false, false)).toBe(true); - }); - - it("should reject on empty input", async () => { - await expect(Promise.resolve().then(() => asyncAggregateWithRandomness([]))).rejects.toThrow(); - }); - - it("should reject on invalid signature bytes", async () => { - const {sets} = getTestSetsSameMessage(4); - const input = sets.map((s) => ({pk: s.pk, sig: s.sig.toBytes()})); - input[2].sig = new Uint8Array(96).fill(0xff); - await expect(Promise.resolve().then(() => asyncAggregateWithRandomness(input))).rejects.toThrow(); - }); - - it("should reject objects of the wrong class", async () => { - const {sets} = getTestSetsSameMessage(1); - const input = [{pk: sets[0].sk as unknown as PublicKey, sig: sets[0].sig.toBytes()}]; - await expect(Promise.resolve().then(() => asyncAggregateWithRandomness(input))).rejects.toThrow("TypeMismatch"); - }); - - it("should resolve concurrent invocations correctly", async () => { - const {msg, sets} = getTestSetsSameMessage(8); - const input = sets.map((s) => ({pk: s.pk, sig: s.sig.toBytes()})); - const results = await Promise.all([ - asyncAggregateWithRandomness(input), - asyncAggregateWithRandomness(input), - asyncAggregateWithRandomness(input), - ]); - for (const {pk, sig} of results) { - expect(verify(msg, pk, sig, false, false)).toBe(true); - } - }); - }); }); const DEFAULT_TEST_MESSAGE = Uint8Array.from(Buffer.from("lodestarlodestarlodestarlodestar")); @@ -682,31 +542,3 @@ function expectNotEqualHex(value: Uint8Array, expected: Uint8Array): void { expect(Buffer.from(value).toString("hex")).to.not.equal(Buffer.from(expected).toString("hex")); } -const commonMessage = crypto.randomBytes(32); -const commonMessageSignatures = new Map(); - -function getTestSetSameMessage(i: number): TestSet { - const set = getTestSet(i); - let sig = commonMessageSignatures.get(i); - if (!sig) { - sig = set.sk.sign(commonMessage); - commonMessageSignatures.set(i, sig); - } - return { - msg: commonMessage, - pk: set.pk, - sig, - sk: set.sk, - }; -} - -function getTestSetsSameMessage(count: number): { - msg: Uint8Array; - sets: {sk: SecretKey; pk: PublicKey; sig: Signature}[]; -} { - const sets = arrayOfIndexes(0, count - 1).map(getTestSetSameMessage); - return { - msg: sets[0].msg, - sets: sets.map(({sk, pk, sig}) => ({pk, sig, sk})), - }; -} diff --git a/src/bls/AggregatePublicKey.zig b/src/bls/AggregatePublicKey.zig index 9bb6b089a..855509c1d 100644 --- a/src/bls/AggregatePublicKey.zig +++ b/src/bls/AggregatePublicKey.zig @@ -39,142 +39,6 @@ pub fn aggregate(pks: []const PublicKey, pks_validate: bool) BlstError!Self { return agg_pk; } -/// Aggregates multiple public keys using multi-scalar multiplication with randomness. -/// This method is more efficient for large numbers of public keys and provides -/// enhanced security through the use of randomness. -/// -/// Errors if: -/// - `items` is empty or contains more than `MAX_AGGREGATE_PER_JOB` entries, -/// - `scratch` space is insufficient, or -/// - if any public key validation fails. -/// -/// Returns the `AggregatePublicKey` on success. -pub fn aggregateWithRandomness( - items: []const RandomizedPublicKey, - pks_validate: bool, - scratch: []u64, -) BlstError!Self { - if (items.len == 0) return BlstError.EmptyAggregate; - if (items.len > MAX_AGGREGATE_PER_JOB) return BlstError.TooManyItems; - const scratch_len = @divExact( - c.blst_p1s_mult_pippenger_scratch_sizeof(items.len), - @sizeOf(u64), - ); - if (scratch.len < scratch_len) { - return BlstError.InsufficientScratchSpace; - } - if (pks_validate) for (items) |item| try item.public_key.validate(); - - var scalars_refs: [MAX_AGGREGATE_PER_JOB]*const u8 = undefined; - var pks_refs: [MAX_AGGREGATE_PER_JOB]*const c.blst_p1_affine = undefined; - for (items, 0..) |*item, i| { - scalars_refs[i] = &item.randomness[0]; - pks_refs[i] = &item.public_key.point; - } - - var agg_pk = Self{}; - c.blst_p1s_mult_pippenger( - &agg_pk.point, - pks_refs[0..items.len].ptr, - items.len, - scalars_refs[0..items.len].ptr, - 64, - scratch.ptr, - ); - return agg_pk; -} - -test "aggregateWithRandomness rejects empty items" { - const items: []const RandomizedPublicKey = &.{}; - var scratch: [1]u64 = undefined; - - try std.testing.expectError( - BlstError.EmptyAggregate, - aggregateWithRandomness(items, false, &scratch), - ); -} - -test "aggregateWithRandomness rejects too many public key items" { - const items: [MAX_AGGREGATE_PER_JOB + 1]RandomizedPublicKey = undefined; - var scratch: [1]u64 = undefined; - - try std.testing.expectError( - BlstError.TooManyItems, - aggregateWithRandomness(&items, false, &scratch), - ); -} - -test "aggregateWithRandomness rejects insufficient public key scratch space" { - const public_key: PublicKey = undefined; - const items = [_]RandomizedPublicKey{.{ - .public_key = &public_key, - .randomness = [_]u8{1} ** 32, - }}; - - try std.testing.expectError( - BlstError.InsufficientScratchSpace, - aggregateWithRandomness(&items, false, &.{}), - ); -} - -test "aggregateWithRandomness aggregates 128 public keys" { - const ikm: [32]u8 = [_]u8{ - 0x93, 0xad, 0x7e, 0x65, 0xde, 0xad, 0x05, 0x2a, 0x08, 0x3a, - 0x91, 0x0c, 0x8b, 0x72, 0x85, 0x91, 0x46, 0x4c, 0xca, 0x56, - 0x60, 0x5b, 0xb0, 0x56, 0xed, 0xfe, 0x2b, 0x60, 0xa6, 0x3c, - 0x48, 0x99, - }; - - const num_sigs = MAX_AGGREGATE_PER_JOB; - - var msgs: [num_sigs][32]u8 = undefined; - var sks: [num_sigs]SecretKey = undefined; - var pks: [num_sigs]PublicKey = undefined; - var sigs: [num_sigs]Signature = undefined; - - const scratch_len = @divExact( - c.blst_p1s_mult_pippenger_scratch_sizeof(num_sigs), - @sizeOf(u64), - ); - const allocator = std.testing.allocator; - - const scratch = try allocator.alloc(u64, scratch_len); - defer allocator.free(scratch); - - var prng = std.Random.DefaultPrng.init(blk: { - var seed: u64 = undefined; - std.testing.io.random(std.mem.asBytes(&seed)); - break :blk seed; - }); - const rand = prng.random(); - for (0..num_sigs) |i| { - std.Random.bytes(rand, &msgs[i]); - const sk = try SecretKey.keyGen(&ikm, null); - const pk = sk.toPublicKey(); - const sig = sk.sign(&msgs[i], DST, null); - - sks[i] = sk; - pks[i] = pk; - sigs[i] = sig; - } - var rands: [32 * MAX_AGGREGATE_PER_JOB]u8 = [_]u8{0} ** (32 * MAX_AGGREGATE_PER_JOB); - var items: [MAX_AGGREGATE_PER_JOB]RandomizedPublicKey = undefined; - std.Random.bytes(rand, &rands); - - for (0..num_sigs) |i| { - items[i] = .{ - .public_key = &pks[i], - .randomness = rands[i * 32 ..][0..32].*, - }; - } - - _ = try aggregateWithRandomness( - &items, - true, - scratch[0..], - ); -} - test aggregate { const ikm: [32]u8 = [_]u8{ 0x93, 0xad, 0x7e, 0x65, 0xde, 0xad, 0x05, 0x2a, 0x08, 0x3a, diff --git a/src/bls/AggregateSignature.zig b/src/bls/AggregateSignature.zig index 7bae6c547..1f8de3b17 100644 --- a/src/bls/AggregateSignature.zig +++ b/src/bls/AggregateSignature.zig @@ -41,164 +41,6 @@ pub fn aggregate(sigs: []const Signature, sigs_groupcheck: bool) BlstError!Self return agg_sig; } -/// Aggregates multiple signatures using multi-scalar multiplication with randomness. -/// -/// Errors if `items` is empty, contains more than `MAX_AGGREGATE_PER_JOB` -/// entries, scratch space is insufficient, or any signature validation fails. -/// -/// Returns the `AggregateSignature` on success. -pub fn aggregateWithRandomness( - items: []const RandomizedSignature, - sigs_groupcheck: bool, - scratch: []u64, -) BlstError!Self { - if (items.len == 0) return BlstError.EmptyAggregate; - if (items.len > MAX_AGGREGATE_PER_JOB) return BlstError.TooManyItems; - const scratch_len = @divExact( - c.blst_p2s_mult_pippenger_scratch_sizeof(items.len), - @sizeOf(u64), - ); - if (scratch.len < scratch_len) { - return BlstError.InsufficientScratchSpace; - } - if (sigs_groupcheck) for (items) |item| try item.signature.validate(false); - - var scalars_refs: [MAX_AGGREGATE_PER_JOB]*const u8 = undefined; - var sigs_refs: [MAX_AGGREGATE_PER_JOB]*const c.blst_p2_affine = undefined; - for (items, 0..) |*item, i| { - scalars_refs[i] = &item.randomness[0]; - sigs_refs[i] = &item.signature.point; - } - - var agg_sig = Self{}; - - c.blst_p2s_mult_pippenger( - &agg_sig.point, - sigs_refs[0..items.len].ptr, - items.len, - scalars_refs[0..items.len].ptr, - 64, - scratch.ptr, - ); - return agg_sig; -} - -test "aggregateWithRandomness rejects empty signature items" { - const items: []const RandomizedSignature = &.{}; - var scratch: [1]u64 = undefined; - - try std.testing.expectError( - BlstError.EmptyAggregate, - aggregateWithRandomness(items, false, &scratch), - ); -} - -test "aggregateWithRandomness rejects too many signature items" { - const items: [MAX_AGGREGATE_PER_JOB + 1]RandomizedSignature = undefined; - var scratch: [1]u64 = undefined; - - try std.testing.expectError( - BlstError.TooManyItems, - aggregateWithRandomness(&items, false, &scratch), - ); -} - -test "aggregateWithRandomness rejects insufficient signature scratch space" { - const signature: Signature = undefined; - const items = [_]RandomizedSignature{.{ - .signature = &signature, - .randomness = [_]u8{1} ** 32, - }}; - - try std.testing.expectError( - BlstError.InsufficientScratchSpace, - aggregateWithRandomness(&items, false, &.{}), - ); -} - -test "aggregateWithRandomness aggregates 128 signatures" { - const ikm: [32]u8 = [_]u8{ - 0x93, 0xad, 0x7e, 0x65, 0xde, 0xad, 0x05, 0x2a, 0x08, 0x3a, - 0x91, 0x0c, 0x8b, 0x72, 0x85, 0x91, 0x46, 0x4c, 0xca, 0x56, - 0x60, 0x5b, 0xb0, 0x56, 0xed, 0xfe, 0x2b, 0x60, 0xa6, 0x3c, - 0x48, 0x99, - }; - - const dst = DST; - // aug is null - - const num_sigs = MAX_AGGREGATE_PER_JOB; - - var msgs: [num_sigs][32]u8 = undefined; - var sks: [num_sigs]SecretKey = undefined; - var pks: [num_sigs]PublicKey = undefined; - var sigs: [num_sigs]Signature = undefined; - - const pk_scratch_len = @divExact( - c.blst_p1s_mult_pippenger_scratch_sizeof(num_sigs), - @sizeOf(u64), - ); - const sig_scratch_len = @divExact( - c.blst_p2s_mult_pippenger_scratch_sizeof(num_sigs), - @sizeOf(u64), - ); - const allocator = std.testing.allocator; - - const pk_scratch = try allocator.alloc(u64, pk_scratch_len); - defer allocator.free(pk_scratch); - - const sig_scratch = try allocator.alloc(u64, sig_scratch_len); - defer allocator.free(sig_scratch); - - var prng = std.Random.DefaultPrng.init(blk: { - var seed: u64 = undefined; - std.testing.io.random(std.mem.asBytes(&seed)); - break :blk seed; - }); - const rand = prng.random(); - std.Random.bytes(rand, &msgs[0]); - for (0..num_sigs) |i| { - const msg = msgs[0]; - const sk = try SecretKey.keyGen(&ikm, null); - const pk = sk.toPublicKey(); - const sig = sk.sign(&msg, dst, null); - - sks[i] = sk; - pks[i] = pk; - sigs[i] = sig; - msgs[i] = msg; - try sig.verify(true, &msgs[i], dst, null, &pks[i], true); - } - var rands: [32 * MAX_AGGREGATE_PER_JOB]u8 = [_]u8{0} ** (32 * MAX_AGGREGATE_PER_JOB); - var randomized_sigs: [MAX_AGGREGATE_PER_JOB]RandomizedSignature = undefined; - var randomized_pks: [MAX_AGGREGATE_PER_JOB]AggregatePublicKey.RandomizedPublicKey = undefined; - std.Random.bytes(rand, &rands); - - for (0..num_sigs) |i| { - randomized_sigs[i] = .{ - .signature = &sigs[i], - .randomness = rands[i * 32 ..][0..32].*, - }; - randomized_pks[i] = .{ - .public_key = &pks[i], - .randomness = rands[i * 32 ..][0..32].*, - }; - } - - const agg_pk = try AggregatePublicKey.aggregateWithRandomness( - &randomized_pks, - true, - pk_scratch, - ); - const pk = agg_pk.toPublicKey(); - const agg_sig = try aggregateWithRandomness( - &randomized_sigs, - true, - sig_scratch, - ); - const sig = agg_sig.toSignature(); - try sig.verify(true, &msgs[0], dst, null, &pk, true); -} const std = @import("std"); const c = @import("root.zig").c; diff --git a/src/bls/ThreadPool.zig b/src/bls/ThreadPool.zig index d273aa273..440d4aeae 100644 --- a/src/bls/ThreadPool.zig +++ b/src/bls/ThreadPool.zig @@ -454,8 +454,7 @@ fn mergeAndVerify( /// - `pks` and `sigs` are paired by index. /// - `randomness` must contain at least `pks.len * 32` bytes; /// - only the first 8 bytes per 32-byte slot are read by -/// the underlying 64-bit Pippenger, but the 32-byte stride matches the existing -/// `AggregatePublicKey.aggregateWithRandomness` layout. +/// the underlying 64-bit Pippenger. pub fn aggregateWithRandomness( pool: *ThreadPool, io: std.Io, diff --git a/test/fuzz/src/fuzz_bls_aggregate_pk.zig b/test/fuzz/src/fuzz_bls_aggregate_pk.zig index e96261ca6..4c56e2c95 100644 --- a/test/fuzz/src/fuzz_bls_aggregate_pk.zig +++ b/test/fuzz/src/fuzz_bls_aggregate_pk.zig @@ -13,7 +13,6 @@ pub export fn zig_fuzz_test( ) callconv(.c) void { const input = buf[0..len]; fuzzAggregate(input); - fuzzAggregateWithRandomness(input); } fn fuzzAggregate(input: []const u8) void { @@ -41,43 +40,3 @@ fn fuzzAggregate(input: []const u8) void { } }; } - -fn fuzzAggregateWithRandomness(input: []const u8) void { - const pk_size = PublicKey.COMPRESS_SIZE; - const rand_size = 32; - const item_size = pk_size + rand_size; - if (input.len < item_size) return; - - const n = @min(input.len / item_size, MAX_AGGREGATE_PER_JOB); - if (n == 0) return; - - var pks: [MAX_AGGREGATE_PER_JOB]PublicKey = undefined; - var items: [MAX_AGGREGATE_PER_JOB]AggregatePublicKey.RandomizedPublicKey = undefined; - var count: usize = 0; - - for (0..n) |i| { - const off = i * item_size; - const pk_chunk = input[off .. off + pk_size]; - const rand_chunk = input[off + pk_size .. off + item_size]; - - const pk = PublicKey.deserialize(pk_chunk) catch continue; - pks[count] = pk; - items[count] = .{ - .public_key = &pks[count], - .randomness = rand_chunk[0..rand_size].*, - }; - count += 1; - } - - if (count == 0) return; - - var scratch: [1 << 14]u64 = undefined; - - _ = AggregatePublicKey.aggregateWithRandomness( - items[0..count], - false, - &scratch, - ) catch { - @panic("unexpected aggregateWithRandomness public key error"); - }; -} diff --git a/test/fuzz/src/fuzz_bls_aggregate_sig.zig b/test/fuzz/src/fuzz_bls_aggregate_sig.zig index d0d82eefa..0a527aab2 100644 --- a/test/fuzz/src/fuzz_bls_aggregate_sig.zig +++ b/test/fuzz/src/fuzz_bls_aggregate_sig.zig @@ -13,7 +13,6 @@ pub export fn zig_fuzz_test( ) callconv(.c) void { const input = buf[0..len]; fuzzAggregate(input); - fuzzAggregateWithRandomness(input); } fn fuzzAggregate(input: []const u8) void { @@ -39,43 +38,3 @@ fn fuzzAggregate(input: []const u8) void { } }; } - -fn fuzzAggregateWithRandomness(input: []const u8) void { - const sig_size = Signature.COMPRESS_SIZE; - const rand_size = 32; - const item_size = sig_size + rand_size; - if (input.len < item_size) return; - - const n = @min(input.len / item_size, MAX_AGGREGATE_PER_JOB); - if (n == 0) return; - - var sigs: [MAX_AGGREGATE_PER_JOB]Signature = undefined; - var items: [MAX_AGGREGATE_PER_JOB]AggregateSignature.RandomizedSignature = undefined; - var count: usize = 0; - - for (0..n) |i| { - const off = i * item_size; - const sig_chunk = input[off .. off + sig_size]; - const rand_chunk = input[off + sig_size .. off + item_size]; - - const sig = Signature.deserialize(sig_chunk) catch continue; - sigs[count] = sig; - items[count] = .{ - .signature = &sigs[count], - .randomness = rand_chunk[0..rand_size].*, - }; - count += 1; - } - - if (count == 0) return; - - var scratch: [1 << 16]u64 = undefined; - - _ = AggregateSignature.aggregateWithRandomness( - items[0..count], - false, - &scratch, - ) catch { - @panic("unexpected aggregateWithRandomness signature error"); - }; -} From e1a5b82092ab91f67427e9428e9caecbd86533a4 Mon Sep 17 00:00:00 2001 From: bing Date: Wed, 19 Aug 2026 19:41:35 +0800 Subject: [PATCH 2/2] fmt --- bindings/test/blst.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/bindings/test/blst.test.ts b/bindings/test/blst.test.ts index 7b695170c..89b2766b6 100644 --- a/bindings/test/blst.test.ts +++ b/bindings/test/blst.test.ts @@ -541,4 +541,3 @@ function expectEqualHex(value: Uint8Array, expected: Uint8Array): void { function expectNotEqualHex(value: Uint8Array, expected: Uint8Array): void { expect(Buffer.from(value).toString("hex")).to.not.equal(Buffer.from(expected).toString("hex")); } -