From a5d44762879a5b25cc982b4ef222abeb005d727e Mon Sep 17 00:00:00 2001 From: matthewkeil Date: Fri, 25 Oct 2024 16:34:00 +0700 Subject: [PATCH 1/2] feat: add asyncAggregateWithRandomness --- index.d.ts | 6 ++ index.js | 3 +- src/lib.rs | 48 +++++++++++++- test/unit/aggregateWithRandomness.test.ts | 78 +++++++++++++++++++++++ test/unit/bindings.test.ts | 1 + 5 files changed, 134 insertions(+), 2 deletions(-) diff --git a/index.d.ts b/index.d.ts index bf107f74..baf612f4 100644 --- a/index.d.ts +++ b/index.d.ts @@ -54,6 +54,12 @@ export declare function aggregateSerializedSignatures(sigs: Array, s * Signatures are deserialized and validated with infinity and group checks before aggregation. */ export declare function aggregateWithRandomness(sets: Array): PkAndSig +/** + * 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 asyncAggregateWithRandomness(sets: Array): Promise /** * Verify a signature against a message and public key. * diff --git a/index.js b/index.js index 66656c2c..0ca93728 100644 --- a/index.js +++ b/index.js @@ -310,7 +310,7 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -const { SECRET_KEY_LENGTH, PUBLIC_KEY_LENGTH_COMPRESSED, PUBLIC_KEY_LENGTH_UNCOMPRESSED, SIGNATURE_LENGTH_COMPRESSED, SIGNATURE_LENGTH_UNCOMPRESSED, SecretKey, PublicKey, Signature, aggregatePublicKeys, aggregateSignatures, aggregateSerializedPublicKeys, aggregateSerializedSignatures, aggregateWithRandomness, verify, aggregateVerify, fastAggregateVerify, verifyMultipleAggregateSignatures } = nativeBinding +const { SECRET_KEY_LENGTH, PUBLIC_KEY_LENGTH_COMPRESSED, PUBLIC_KEY_LENGTH_UNCOMPRESSED, SIGNATURE_LENGTH_COMPRESSED, SIGNATURE_LENGTH_UNCOMPRESSED, SecretKey, PublicKey, Signature, aggregatePublicKeys, aggregateSignatures, aggregateSerializedPublicKeys, aggregateSerializedSignatures, aggregateWithRandomness, asyncAggregateWithRandomness, verify, aggregateVerify, fastAggregateVerify, verifyMultipleAggregateSignatures } = nativeBinding module.exports.SECRET_KEY_LENGTH = SECRET_KEY_LENGTH module.exports.PUBLIC_KEY_LENGTH_COMPRESSED = PUBLIC_KEY_LENGTH_COMPRESSED @@ -325,6 +325,7 @@ module.exports.aggregateSignatures = aggregateSignatures module.exports.aggregateSerializedPublicKeys = aggregateSerializedPublicKeys module.exports.aggregateSerializedSignatures = aggregateSerializedSignatures module.exports.aggregateWithRandomness = aggregateWithRandomness +module.exports.asyncAggregateWithRandomness = asyncAggregateWithRandomness module.exports.verify = verify module.exports.aggregateVerify = aggregateVerify module.exports.fastAggregateVerify = fastAggregateVerify diff --git a/src/lib.rs b/src/lib.rs index 6f85e8db..fca99599 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,7 @@ #![deny(clippy::all)] use blst::{blst_scalar, blst_scalar_from_uint64, min_pk, MultiPoint, BLST_ERROR}; -use napi::bindgen_prelude::*; +use napi::{bindgen_prelude::*, Task}; use napi_derive::napi; use rand::{rngs::ThreadRng, Rng}; @@ -29,6 +29,8 @@ pub const SIGNATURE_LENGTH_UNCOMPRESSED: u32 = 192; pub enum ErrorStatus { Blst(BLST_ERROR), InvalidHex, + PublicKeyField, + SignatureField, Other(String), } @@ -37,6 +39,8 @@ impl AsRef for ErrorStatus { match self { ErrorStatus::Blst(err) => blst_error_to_str(*err), ErrorStatus::InvalidHex => "INVALID_HEX", + ErrorStatus::PublicKeyField => "INVALID_PUBLIC_KEY_FIELD", + ErrorStatus::SignatureField => "INVALID_SIGNATURE_FIELD", ErrorStatus::Other(err) => err.as_str(), } } @@ -403,6 +407,48 @@ pub fn aggregate_with_randomness(env: Env, sets: Vec) -> Res }) } +pub struct AsyncAggregateWithRandomness { + pks: Vec, + sigs: Vec, +} + +#[napi] +impl Task for AsyncAggregateWithRandomness { + type Output = (min_pk::PublicKey, min_pk::Signature); + type JsValue = PkAndSig; + + fn compute(&mut self) -> napi::Result { + let scalars = create_rand_slice(self.pks.len()); + let pk = self.pks.as_slice().mult(&scalars, 64).to_public_key(); + let sig = self.sigs.as_slice().mult(&scalars, 64).to_signature(); + + Ok((pk, sig)) + } + + // TODO: (@wemeetagain) How do we return a Result instead of a napi::Result here? + fn resolve(&mut self, env: Env, output: Self::Output) -> napi::Result { + Ok(PkAndSig { + pk: PublicKey::into_reference(PublicKey(output.0), env)?, + sig: Signature::into_reference(Signature(output.1), env)?, + }) + } +} + +#[napi] +/// 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. +pub fn async_aggregate_with_randomness( + sets: Vec, +) -> Result> { + if sets.is_empty() { + return Err(from_blst_err(BLST_ERROR::BLST_AGGR_TYPE_MISMATCH)); + } + + let (pks, sigs) = unzip_and_validate_aggregation_sets(&sets)?; + Ok(AsyncTask::new(AsyncAggregateWithRandomness { pks, sigs })) +} + #[napi] /// Verify a signature against a message and public key. /// diff --git a/test/unit/aggregateWithRandomness.test.ts b/test/unit/aggregateWithRandomness.test.ts index 05f78d80..645d5406 100644 --- a/test/unit/aggregateWithRandomness.test.ts +++ b/test/unit/aggregateWithRandomness.test.ts @@ -3,6 +3,7 @@ import { aggregatePublicKeys, aggregateSerializedSignatures, aggregateWithRandomness, + asyncAggregateWithRandomness, PublicKey, Signature, verify, @@ -101,4 +102,81 @@ describe("Aggregate With Randomness", () => { expect(verify(msg, pk, sig)).to.be.false; }); }); + describe("asyncAggregateWithRandomness()", () => { + it("should not accept an empty array argument", async () => { + try { + await asyncAggregateWithRandomness([]); + expect.fail("asyncAggregateWithRandomness with empty list should throw"); + } catch (e) { + expect((e as any).code).to.equal("BLST_AGGR_TYPE_MISMATCH"); + } + }); + describe("should accept an array of {pk: PublicKey, sig: Uint8Array}", () => { + it("should handle valid case", () => { + expect(() => asyncAggregateWithRandomness([{pk: sets[0].pk, sig: sets[0].sig}])).not.to.throw(); + }); + it("should handle invalid publicKey property name", () => { + expect(() => asyncAggregateWithRandomness([{publicKey: sets[0].pk, sig: sets[0].sig} as any])).to.throw( + "Missing field `pk`" + ); + }); + it("should handle invalid publicKey property value", () => { + expect(() => asyncAggregateWithRandomness([{pk: 1 as any, sig: sets[0].sig}])).to.throw(); + }); + it("should handle invalid signature property name", () => { + expect(() => asyncAggregateWithRandomness([{pk: sets[0].pk, signature: sets[0].sig} as any])).to.throw( + "Missing field `sig`" + ); + }); + it("should handle invalid signature property value", () => { + expect(() => asyncAggregateWithRandomness([{pk: sets[0].pk, sig: "bar" as any}])).to.throw(); + }); + }); + it("should throw for invalid serialized", () => { + expect(() => + asyncAggregateWithRandomness( + sets.concat({ + pk: sets[0].pk, + sig: G2_POINT_AT_INFINITY, //TODO: (@matthewkeil) this throws error "Public key is infinity" not signature + } as any) + ) + ).to.throw(); + }); + it("should return a {pk: PublicKey, sig: Signature} object", async () => { + const aggPromise = asyncAggregateWithRandomness(sets); + expect(aggPromise).to.be.instanceOf(Promise); + const agg = await aggPromise; + expect(agg).to.be.instanceOf(Object); + + expect(agg).to.haveOwnProperty("pk"); + expect(agg.pk).to.be.instanceOf(PublicKey); + expect(() => agg.pk.keyValidate()).not.to.throw(); + + expect(agg).to.haveOwnProperty("sig"); + expect(agg.sig).to.be.instanceOf(Signature); + expect(() => agg.sig.sigValidate()).not.to.throw(); + }); + it("should add randomness to aggregated publicKey", async () => { + const withoutRandomness = aggregatePublicKeys(sets.map(({pk}) => pk)); + const withRandomness = await asyncAggregateWithRandomness(sets); + expectNotEqualHex(withRandomness.pk, withoutRandomness); + }); + it("should add randomness to aggregated signature", async () => { + const withoutRandomness = aggregateSerializedSignatures(sets.map(({sig}) => sig)); + const withRandomness = await asyncAggregateWithRandomness(sets); + expectNotEqualHex(withRandomness.sig, withoutRandomness); + }); + it("should produce verifiable set", async () => { + const {pk, sig} = await asyncAggregateWithRandomness(sets); + expect(verify(msg, pk, sig)); + }); + it("should not validate for different message", async () => { + const {pk, sig} = await asyncAggregateWithRandomness(sets); + expect(verify(randomSet.msg, pk, sig)).to.be.false; + }); + it("should not validate included key/sig for different message", async () => { + const {pk, sig} = await asyncAggregateWithRandomness([...sets, {pk: randomSet.pk, sig: randomSet.sig.toBytes()}]); + expect(verify(msg, pk, sig)).to.be.false; + }); + }); }); diff --git a/test/unit/bindings.test.ts b/test/unit/bindings.test.ts index 7b649020..d5dc42f0 100644 --- a/test/unit/bindings.test.ts +++ b/test/unit/bindings.test.ts @@ -13,6 +13,7 @@ describe("bindings", () => { "aggregateSerializedPublicKeys", "aggregateSerializedSignatures", "aggregateWithRandomness", + "asyncAggregateWithRandomness", "verify", "aggregateVerify", "fastAggregateVerify", From 6c8f945c3c6c3c810dff9d96605e2c52b5cf1ed5 Mon Sep 17 00:00:00 2001 From: matthewkeil Date: Fri, 25 Oct 2024 16:36:55 +0700 Subject: [PATCH 2/2] fix: remove unnecessary errors --- src/lib.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index fca99599..f6d9febf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,8 +29,6 @@ pub const SIGNATURE_LENGTH_UNCOMPRESSED: u32 = 192; pub enum ErrorStatus { Blst(BLST_ERROR), InvalidHex, - PublicKeyField, - SignatureField, Other(String), } @@ -39,8 +37,6 @@ impl AsRef for ErrorStatus { match self { ErrorStatus::Blst(err) => blst_error_to_str(*err), ErrorStatus::InvalidHex => "INVALID_HEX", - ErrorStatus::PublicKeyField => "INVALID_PUBLIC_KEY_FIELD", - ErrorStatus::SignatureField => "INVALID_SIGNATURE_FIELD", ErrorStatus::Other(err) => err.as_str(), } }