Skip to content

Commit

Permalink
feat: implement verify and aggregate apis
Browse files Browse the repository at this point in the history
  • Loading branch information
twoeths committed Feb 24, 2025
1 parent 4a2ae5a commit a5ddae7
Show file tree
Hide file tree
Showing 14 changed files with 761 additions and 58 deletions.
116 changes: 116 additions & 0 deletions src/aggregate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { binding, writeUint8ArrayArray } from "./binding";
import { MAX_AGGREGATE_PER_JOB } from "./const";
import {PublicKey, writePublicKeysReference} from "./publicKey";
import { Signature, writeSignaturesReference } from "./signature";


// global public keys reference to be reused across multiple calls
// each 2 items are 8 bytes, store the reference of each public key
const public_keys_ref = new Uint32Array(MAX_AGGREGATE_PER_JOB * 2);

const signatures_ref = new Uint32Array(MAX_AGGREGATE_PER_JOB * 2);

/**
* Aggregate multiple public keys into a single public key.
*
* If `pks_validate` is `true`, the public keys will be infinity and group checked.
*/
export function aggregatePublicKeys(pks: Array<PublicKey>, pksValidate?: boolean | undefined | null): PublicKey {
if (pks.length > MAX_AGGREGATE_PER_JOB) {
throw new Error("Too many public keys");
}

const pks_ref = writePublicKeysReference(pks);

const defaultPk = PublicKey.defaultPublicKey();
const res = binding.aggregatePublicKeys(defaultPk.blst_point, pks_ref, pks.length, pksValidate ?? false);
if (res !== 0) {
throw new Error(`Failed to aggregate public keys: ${res}`);
}

return defaultPk;
}

/**
* Aggregate multiple signatures into a single signature.
*
* If `sigs_groupcheck` is `true`, the signatures will be group checked.
*/
export function aggregateSignatures(sigs: Array<Signature>, sigsGroupcheck?: boolean | undefined | null): Signature {
if (sigs.length > MAX_AGGREGATE_PER_JOB) {
throw new Error("Too many signatures");
}

const sigs_ref = writeSignaturesReference(sigs);

const defaultSig = Signature.defaultSignature();
const res = binding.aggregateSignatures(defaultSig.blst_point, sigs_ref, sigs.length, sigsGroupcheck ?? false);
if (res !== 0) {
throw new Error(`Failed to aggregate signatures: ${res}`);
}

return defaultSig;
}

const pks_ref = new Uint32Array(2);

/**
* Aggregate multiple serialized public keys into a single public key.
*
* If `pks_validate` is `true`, the public keys will be infinity and group checked.
*/
export function aggregateSerializedPublicKeys(pks: Array<Uint8Array>, pksValidate?: boolean | undefined | null): PublicKey {
if (pks.length > MAX_AGGREGATE_PER_JOB) {
throw new Error("Too many public keys");
}

if (pks.length < 1) {
throw new Error("At least one public key is required");
}

const pks_ref = writeSerializedPublicKeysReference(pks);

const defaultPk = PublicKey.defaultPublicKey();
const res = binding.aggregateSerializedPublicKeys(defaultPk.blst_point, pks_ref, pks.length, pks[0].length, pksValidate ?? false);
if (res !== 0) {
throw new Error(`Failed to aggregate serialized public keys: ${res}`);
}

return defaultPk;
}

/**
* Aggregate multiple serialized signatures into a single signature.
*
* If `sigs_groupcheck` is `true`, the signatures will be group checked.
*/
export function aggregateSerializedSignatures(sigs: Array<Uint8Array>, sigsGroupcheck?: boolean | undefined | null): Signature {
if (sigs.length > MAX_AGGREGATE_PER_JOB) {
throw new Error("Too many signatures");
}

if (sigs.length < 1) {
throw new Error("At least one signature is required");
}

const sigs_ref = writeSerializedSignaturesReference(sigs);

const defaultSig = Signature.defaultSignature();
const res = binding.aggregateSerializedSignatures(defaultSig.blst_point, sigs_ref, sigs.length, sigs[0].length, sigsGroupcheck ?? false);
if (res !== 0) {
throw new Error(`Failed to aggregate serialized signatures: ${res}`);
}

return defaultSig;
}


function writeSerializedPublicKeysReference(pks: Uint8Array[]): Uint32Array {
writeUint8ArrayArray(pks, MAX_AGGREGATE_PER_JOB, "public key", public_keys_ref);
return public_keys_ref.subarray(0, pks.length * 2);
}

function writeSerializedSignaturesReference(sigs: Uint8Array[]): Uint32Array {
writeUint8ArrayArray(sigs, MAX_AGGREGATE_PER_JOB, "signature", signatures_ref);
return signatures_ref.subarray(0, sigs.length * 2);
}
127 changes: 127 additions & 0 deletions src/aggregateWithRandomness.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import {JSCallback} from "bun:ffi";
import { binding, writeNumber, writeReference } from "./binding";
import { MAX_AGGREGATE_WITH_RANDOMNESS_PER_JOB } from "./const";
import { PublicKey } from "./publicKey";
import { Signature } from "./signature";

export interface PkAndSerializedSig {
pk: PublicKey;
sig: Uint8Array;
};

export interface PkAndSig {
pk: PublicKey;
sig: Signature;
};

// global signature sets reference to be reused across multiple calls
// each 2 tems are 8 bytes, store the reference of each PkAndSerializedSig
const pk_and_serialized_sigs_refs = new Uint32Array(MAX_AGGREGATE_WITH_RANDOMNESS_PER_JOB * 2);

const scratch_pk = new Uint8Array(binding.sizeOfScratchPk(MAX_AGGREGATE_WITH_RANDOMNESS_PER_JOB));
const scratch_sig = new Uint8Array(binding.sizeOfScratchSig(MAX_AGGREGATE_WITH_RANDOMNESS_PER_JOB));

/**
* 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 function aggregateWithRandomness(sets: Array<PkAndSerializedSig>): PkAndSig {
if (sets.length > MAX_AGGREGATE_WITH_RANDOMNESS_PER_JOB) {
throw new Error(`Number of PkAndSerializedSig exceeds the maximum of ${MAX_AGGREGATE_WITH_RANDOMNESS_PER_JOB}`);
}

if (sets.length === 0) {
throw new Error("At least one PkAndSerializedSig is required");
}

const refs = pk_and_serialized_sigs_refs.subarray(0, sets.length * 2);
writePkAndSerializedSigsReference(sets, refs);
const pkOut = PublicKey.defaultPublicKey();
const sigOut = Signature.defaultSignature();

const res = binding.aggregateWithRandomness(refs, sets.length, scratch_pk, scratch_pk.length, scratch_sig, scratch_sig.length, pkOut.blst_point, sigOut.blst_point);

if (res !== 0) {
throw new Error("Failed to aggregate with randomness res = " + res);
}

return { pk: pkOut, sig: sigOut};
}

/**
* 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 function asyncAggregateWithRandomness(sets: Array<PkAndSerializedSig>): Promise<PkAndSig> {
if (sets.length > MAX_AGGREGATE_WITH_RANDOMNESS_PER_JOB) {
throw new Error(`Number of PkAndSerializedSig exceeds the maximum of ${MAX_AGGREGATE_WITH_RANDOMNESS_PER_JOB}`);
}

if (sets.length === 0) {
throw new Error("At least one PkAndSerializedSig is required");
}

return new Promise((resolve, reject) => {
const pkOut = PublicKey.defaultPublicKey();
const sigOut = Signature.defaultSignature();

const jscallback = new JSCallback((res: number): void => {
if (res === 0) {
// setTimeout to unblock zig callback thread, not sure why "res" can only be accessed once
setTimeout(() => {
resolve({ pk: pkOut, sig: sigOut });
}, 0);
} else {
setTimeout(() => {
// setTimeout to unblock zig callback thread, not sure why "res" can only be accessed once
reject(new Error("Failed to aggregate with randomness"));
}, 0);
}
}, {
"args": ["u32"],
"returns": "void",
});

const refs = pk_and_serialized_sigs_refs.subarray(0, sets.length * 2);
writePkAndSerializedSigsReference(sets, refs);

const res = binding.asyncAggregateWithRandomness(pk_and_serialized_sigs_refs, sets.length, scratch_pk, scratch_pk.length, scratch_sig, scratch_sig.length, pkOut.blst_point, sigOut.blst_point, jscallback);

if (res !== 0) {
throw new Error("Failed to aggregate with randomness res = " + res);
}
});
}


// global PkAndSerializedSig data to be reused across multiple calls
// each PkAndSerializedSig are 24 bytes
const sets_data = new Uint32Array(MAX_AGGREGATE_WITH_RANDOMNESS_PER_JOB * 6);
function writePkAndSerializedSigsReference(sets: PkAndSerializedSig[], out: Uint32Array): void {
let offset = 0;
for (const [i, set] of sets.entries()) {
writePkAndSerializedSigReference(set, sets_data, offset + i * 6);
// write pointer, each PkAndSerializedSig takes 8 bytes = 2 * uint32
writeReference(sets_data.subarray(i * 6, i * 6 + 6), out, i * 2);
}
};

// each PkAndSerializedSig needs 16 bytes = 4 * uint32 for references
/**
* Map an instance of PkAndSerializedSig in typescript to this struct in Zig:
* ```zig
* const PkAndSerializedSigC = extern struct {
pk: *pk_aff_type,
sig: [*c]const u8,
sig_len: usize,
};
* ```
*
*/
function writePkAndSerializedSigReference(set: PkAndSerializedSig, out: Uint32Array, offset: number): void {
set.pk.writeReference(out, offset);
writeReference(set.sig, out, offset + 2);
writeNumber(set.sig.length, out, offset + 4);
}
88 changes: 84 additions & 4 deletions src/binding.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import {dlopen, ptr} from "bun:ffi";
import { getBinaryName, getPrebuiltBinaryPath } from "../utils";

const binaryName = getBinaryName();
const binaryPath = getPrebuiltBinaryPath(binaryName);
// const binaryName = getBinaryName();
// const binaryPath = getPrebuiltBinaryPath(binaryName);

const binaryPath = "/Users/tuyennguyen/Projects/workshop/blst-z/zig-out/lib/libblst_min_pk.dylib";

// Load the compiled Zig shared library
const lib = dlopen(binaryPath, {
Expand Down Expand Up @@ -93,6 +95,18 @@ const lib = dlopen(binaryPath, {
args: ["ptr", "bool"],
returns: "u8",
},
verifySignature: {
args: ["ptr", "bool", "ptr", "u32", "ptr", "bool"],
returns: "u8",
},
aggregateVerify: {
args: ["ptr", "bool", "ptr", "u32", "u32", "ptr", "u32", "bool", "ptr", "u32"],
returns: "u8",
},
fastAggregateVerify: {
args: ["ptr", "bool", "ptr", "u32", "ptr", "u32", "ptr", "u32"],
returns: "u8",
},
verifyMultipleAggregateSignatures: {
args: ["ptr", "u32", "u32", "bool", "bool", "ptr", "u32"],
returns: "u32",
Expand All @@ -101,6 +115,39 @@ const lib = dlopen(binaryPath, {
args: [],
returns: "u32",
},
aggregatePublicKeys: {
args: ["ptr", "ptr", "u32", "bool"],
returns: "u32",
},
aggregateSignatures: {
args: ["ptr", "ptr", "u32", "bool"],
returns: "u32",
},
aggregateWithRandomness: {
args: ["ptr", "u32", "ptr", "u32", "ptr", "u32", "ptr", "ptr"],
returns: "u32",
},
asyncAggregateWithRandomness: {
args: ["ptr", "u32", "ptr", "u32", "ptr", "u32", "ptr", "ptr", "callback"],
// TODO: may return void instead
returns: "u32",
},
aggregateSerializedPublicKeys: {
args: ["ptr", "ptr", "u32", "u32", "bool"],
returns: "u32",
},
aggregateSerializedSignatures: {
args: ["ptr", "ptr", "u32", "u32", "bool"],
returns: "u32",
},
sizeOfScratchPk: {
args: ["u32"],
returns: "u32",
},
sizeOfScratchSig: {
args: ["u32"],
returns: "u32",
}
});

export const binding = lib.symbols;
Expand All @@ -121,7 +168,40 @@ export function writeReference(data: Uint8Array | Uint32Array, out: Uint32Array,

const pointer = ptr(data);

writeNumber(pointer, out, offset);
}

/**
* Write a number to "usize" in Zig, which takes 8 bytes
*/
export function writeNumber(data: number, out: Uint32Array, offset: number): void {
if (offset + 2 > out.length) {
throw new Error("Output buffer must be at least 8 bytes long");
}

// TODO: check endianess, this is for little endian
out[offset] = pointer & 0xFFFFFFFF;
out[offset + 1] = Math.floor(pointer / Math.pow(2, 32));
out[offset] = data & 0xFFFFFFFF;
out[offset + 1] = Math.floor(data / Math.pow(2, 32));
}

/**
* Common util to map Uint8Array[] to `[*c][*c]const u8` in Zig
*/
export function writeUint8ArrayArray(data: Uint8Array[], maxItem: number, tag: string, out: Uint32Array): void {
if (data.length > maxItem) {
throw new Error(`Too many ${tag}s, max is ${maxItem}`);
}

if (out.length < data.length * 2) {
throw new Error(`Output buffer must be at least double data size. out: ${out.length}, data: ${data.length}`);
}

const pk_length = data[0].length;

for (let i = 0; i < data.length; i++) {
if (data[i].length !== pk_length) {
throw new Error(`All ${tag}s must be the same length`);
}
writeReference(data[i], out, i * 2);
}
}
3 changes: 2 additions & 1 deletion src/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ export const SIGNATURE_LENGTH_COMPRESSED = 96;
export const SIGNATURE_LENGTH_UNCOMPRESSED = 192;
export const MESSAGE_LENGTH = 32;
export const MAX_SIGNATURE_SETS_PER_JOB = 128;

export const MAX_AGGREGATE_WITH_RANDOMNESS_PER_JOB = 128;
export const MAX_AGGREGATE_PER_JOB = 128;
export const BLST_SUCCESS = 0;
export const BLST_BAD_ENCODING = 1;
export const BLST_POINT_NOT_ON_CURVE = 2;
Expand Down
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
export * from "./publicKey";
export * from "./secretKey";
export * from "./signature";
export * from "./aggregateWithRandomness";
export * from "./verifyMultipleAggregateSignatures";
export * from "./verify";
export * from "./aggregate";
export * from "./const";
Loading

0 comments on commit a5ddae7

Please sign in to comment.