Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add asyncAggregateWithRandomness #159

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
6 changes: 6 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ export declare function aggregateSerializedSignatures(sigs: Array<Uint8Array>, s
* Signatures are deserialized and validated with infinity and group checks before aggregation.
*/
export declare function aggregateWithRandomness(sets: Array<PkAndSerializedSig>): 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<PkAndSerializedSig>): Promise<PkAndSig>
/**
* Verify a signature against a message and public key.
*
Expand Down
3 changes: 2 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
44 changes: 43 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
@@ -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};

Expand Down Expand Up @@ -403,6 +403,48 @@ pub fn aggregate_with_randomness(env: Env, sets: Vec<PkAndSerializedSig>) -> Res
})
}

pub struct AsyncAggregateWithRandomness {
pks: Vec<min_pk::PublicKey>,
sigs: Vec<min_pk::Signature>,
}

#[napi]
impl Task for AsyncAggregateWithRandomness {
type Output = (min_pk::PublicKey, min_pk::Signature);
type JsValue = PkAndSig;

fn compute(&mut self) -> napi::Result<Self::Output> {
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<Self::JsValue> {
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<PkAndSerializedSig>,
) -> Result<AsyncTask<AsyncAggregateWithRandomness>> {
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.
///
Expand Down
78 changes: 78 additions & 0 deletions test/unit/aggregateWithRandomness.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
aggregatePublicKeys,
aggregateSerializedSignatures,
aggregateWithRandomness,
asyncAggregateWithRandomness,
PublicKey,
Signature,
verify,
Expand Down Expand Up @@ -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;
});
});
});
1 change: 1 addition & 0 deletions test/unit/bindings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ describe("bindings", () => {
"aggregateSerializedPublicKeys",
"aggregateSerializedSignatures",
"aggregateWithRandomness",
"asyncAggregateWithRandomness",
"verify",
"aggregateVerify",
"fastAggregateVerify",
Expand Down