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..0514ca41 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}; @@ -84,6 +84,13 @@ fn from_napi_err(napi_err: Error) -> Error { ) } +fn blst_to_napi_err(blst_error: BLST_ERROR) -> napi::Error { + napi::Error::new( + napi::Status::GenericFailure, + blst_error_to_reason(blst_error), + ) +} + fn invalid_hex_err(e: hex::FromHexError) -> Error { Error::new(ErrorStatus::InvalidHex, format!("Invalid hex: {}", e)) } @@ -393,7 +400,7 @@ pub fn aggregate_with_randomness(env: Env, sets: Vec) -> Res return Err(from_blst_err(BLST_ERROR::BLST_AGGR_TYPE_MISMATCH)); } - let (pks, sigs) = unzip_and_validate_aggregation_sets(&sets)?; + let (pks, sigs) = unzip_aggregation_sets(&sets, true)?; let rands = create_rand_slice(pks.len()); let (pk, sig) = aggregate_with(pks.as_slice(), sigs.as_slice(), rands.as_slice()); @@ -403,6 +410,52 @@ 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(); + for sig in &self.sigs { + if let Err(blst_error) = sig.validate(true) { + return Err(blst_to_napi_err(blst_error)); + } + } + let sig = self.sigs.as_slice().mult(&scalars, 64).to_signature(); + + Ok((pk, sig)) + } + + 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_aggregation_sets(&sets, false)?; + Ok(AsyncTask::new(AsyncAggregateWithRandomness { pks, sigs })) +} + #[napi] /// Verify a signature against a message and public key. /// @@ -525,16 +578,24 @@ fn unzip_signature_sets<'a>( } /// Convert a list of tuples into a tuple of lists (deserializing and validating signatures along the way) -fn unzip_and_validate_aggregation_sets( +fn unzip_aggregation_sets( sets: &[PkAndSerializedSig], + sig_validate: bool, ) -> Result<(Vec, Vec)> { let len = sets.len(); let mut pks = Vec::with_capacity(len); let mut sigs = Vec::with_capacity(len); - for set in sets { - pks.push(set.pk.0); - sigs.push(min_pk::Signature::sig_validate(set.sig.as_ref(), true).map_err(from_blst_err)?); + if sig_validate { + for set in sets { + pks.push(set.pk.0); + sigs.push(min_pk::Signature::sig_validate(set.sig.as_ref(), true).map_err(from_blst_err)?); + } + } else { + for set in sets { + pks.push(set.pk.0); + sigs.push(min_pk::Signature::from_bytes(set.sig.as_ref()).map_err(from_blst_err)?); + } } Ok((pks, sigs)) diff --git a/test/unit/aggregateWithRandomness.test.ts b/test/unit/aggregateWithRandomness.test.ts index 05f78d80..c85c98b5 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, @@ -61,7 +62,8 @@ describe("Aggregate With Randomness", () => { aggregateWithRandomness( sets.concat({ pk: sets[0].pk, - sig: G2_POINT_AT_INFINITY, //TODO: (@matthewkeil) this throws error "Public key is infinity" not signature + //TODO: (@matthewkeil) this throws error "Public key is infinity" not signature because there is only one blst error + sig: G2_POINT_AT_INFINITY, } as any) ) ).to.throw(); @@ -101,4 +103,85 @@ 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", async () => { + try { + await asyncAggregateWithRandomness( + sets.concat({ + pk: sets[0].pk, + //TODO: (@matthewkeil) this throws error "Public key is infinity" not signature because there is only one blst error + sig: G2_POINT_AT_INFINITY, + } as any) + ); + expect.fail("should not get here"); + } catch (err) { + expect((err as Error).message).to.contain("Public key is infinity"); + } + }); + 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",