diff --git a/src/Bls.ts b/src/Bls.ts new file mode 100644 index 00000000..f6dac563 --- /dev/null +++ b/src/Bls.ts @@ -0,0 +1,396 @@ +import type { ProjPointType } from '@noble/curves/abstract/weierstrass' +import { bls12_381 as bls } from '@noble/curves/bls12-381' + +import type * as BlsPoint from './BlsPoint.js' +import * as Bytes from './Bytes.js' +import type * as Errors from './Errors.js' +import * as Hex from './Hex.js' +import type { OneOf } from './internal/types.js' + +export type Size = 'short-key:long-sig' | 'long-key:short-sig' + +/** Re-export of noble/curves BLS12-381 utilities. */ +export const noble = bls + +/** + * Aggregates a set of BLS points that are either on the G1 or G2 curves (ie. public keys or signatures). + * + * @example + * ### Aggregating Signatures + * + * ```ts twoslash + * import { Bls } from 'ox' + * + * const signatures = [ + * Bls.sign({ payload: '0x...', privateKey: '0x...' }), + * Bls.sign({ payload: '0x...', privateKey: '0x...' }), + * ] + * const signature = Bls.aggregate(signatures) + * ``` + * + * @example + * ### Aggregating Public Keys + * + * ```ts twoslash + * import { Bls } from 'ox' + * + * const publicKeys = [ + * Bls.getPublicKey({ privateKey: '0x...' }), + * Bls.getPublicKey({ privateKey: '0x...' }), + * ] + * const publicKey = Bls.aggregate(publicKeys) + * ``` + * + * @param points - The points to aggregate. + * @returns The aggregated point. + */ +export function aggregate( + points: points, +): points extends readonly BlsPoint.G1[] ? BlsPoint.G1 : BlsPoint.G2 +// +export function aggregate( + points: readonly BlsPoint.BlsPoint[], +): BlsPoint.BlsPoint { + const group = typeof points[0]?.x === 'bigint' ? bls.G1 : bls.G2 + const point = points.reduce( + (acc, point) => + acc.add(new (group as any).ProjectivePoint(point.x, point.y, point.z)), + group.ProjectivePoint.ZERO, + ) + return { + x: point.px, + y: point.py, + z: point.pz, + } +} + +export declare namespace aggregate { + type ErrorType = Errors.GlobalErrorType +} + +aggregate.parseError = (error: unknown) => + /* v8 ignore next */ + error as aggregate.ErrorType + +/** + * Computes the BLS12-381 public key from a provided private key. + * + * Public Keys can be derived as a point on one of the BLS12-381 groups: + * + * - G1 Point (Default): + * - short (48 bytes) + * - computes longer G2 Signatures (96 bytes) + * - G2 Point: + * - long (96 bytes) + * - computes short G1 Signatures (48 bytes) + * + * @example + * ### Short G1 Public Keys (Default) + * + * ```ts twoslash + * import { Bls } from 'ox' + * + * const publicKey = Bls.getPublicKey({ privateKey: '0x...' }) + * // ^? + * + * + * + * + * ``` + * + * @example + * ### Long G2 Public Keys + * + * A G2 Public Key can be derived as a G2 point (96 bytes) using `size: 'long'`. + * + * This will allow you to compute G1 Signatures (48 bytes) with {@link ox#Bls.(sign:function)}. + * + * ```ts twoslash + * import { Bls } from 'ox' + * + * const publicKey = Bls.getPublicKey({ + * privateKey: '0x...', + * size: 'long-key:short-sig', + * }) + * + * publicKey + * // ^? + * + * + * + * + * + * ``` + * + * @param options - The options to compute the public key. + * @returns The computed public key. + */ +export function getPublicKey( + options: getPublicKey.Options, +): size extends 'short-key:long-sig' ? BlsPoint.G1 : BlsPoint.G2 +// eslint-disable-next-line jsdoc/require-jsdoc +export function getPublicKey(options: getPublicKey.Options): BlsPoint.BlsPoint { + const { privateKey, size = 'short-key:long-sig' } = options + const group = size === 'short-key:long-sig' ? bls.G1 : bls.G2 + const { px, py, pz } = group.ProjectivePoint.fromPrivateKey( + Hex.from(privateKey).slice(2), + ) + return { x: px, y: py, z: pz } +} + +export declare namespace getPublicKey { + type Options = { + /** + * Private key to compute the public key from. + */ + privateKey: Hex.Hex | Bytes.Bytes + /** + * Size of the public key to compute. + * + * - `'short-key:long-sig'`: 48 bytes; computes long signatures (96 bytes) + * - `'long-key:short-sig'`: 96 bytes; computes short signatures (48 bytes) + * + * @default 'short-key:long-sig' + */ + size?: size | Size | undefined + } + + type ErrorType = Hex.from.ErrorType | Errors.GlobalErrorType +} + +/** + * Generates a random BLS12-381 private key. + * + * @example + * ```ts twoslash + * import { Bls } from 'ox' + * + * const privateKey = Bls.randomPrivateKey() + * ``` + * + * @param options - The options to generate the private key. + * @returns The generated private key. + */ +export function randomPrivateKey( + options: randomPrivateKey.Options = {}, +): randomPrivateKey.ReturnType { + const { as = 'Hex' } = options + const bytes = bls.utils.randomPrivateKey() + if (as === 'Hex') return Hex.fromBytes(bytes) as never + return bytes as never +} + +export declare namespace randomPrivateKey { + type Options = { + /** + * Format of the returned private key. + * @default 'Hex' + */ + as?: as | 'Hex' | 'Bytes' | undefined + } + + type ReturnType = + | (as extends 'Bytes' ? Bytes.Bytes : never) + | (as extends 'Hex' ? Hex.Hex : never) + + type ErrorType = Hex.fromBytes.ErrorType | Errors.GlobalErrorType +} + +/** + * Signs the payload with the provided private key. + * + * @example + * ```ts twoslash + * import { Bls } from 'ox' + * + * const signature = Bls.sign({ // [!code focus] + * payload: '0xdeadbeef', // [!code focus] + * privateKey: '0x...' // [!code focus] + * }) // [!code focus] + * ``` + * + * @param options - The signing options. + * @returns BLS Point. + */ +export function sign( + options: sign.Options, +): size extends 'short-key:long-sig' ? BlsPoint.G2 : BlsPoint.G1 +export function sign(options: sign.Options): BlsPoint.BlsPoint { + const { payload, privateKey, suite, size = 'short-key:long-sig' } = options + + const payloadGroup = size === 'short-key:long-sig' ? bls.G2 : bls.G1 + const payloadPoint = payloadGroup.hashToCurve( + Bytes.from(payload), + suite ? { DST: Bytes.fromString(suite) } : undefined, + ) + + const privateKeyGroup = size === 'short-key:long-sig' ? bls.G1 : bls.G2 + const signature = payloadPoint.multiply( + privateKeyGroup.normPrivateKeyToScalar(privateKey.slice(2)), + ) as ProjPointType + + return { + x: signature.px, + y: signature.py, + z: signature.pz, + } +} + +export declare namespace sign { + type Options = { + /** + * Payload to sign. + */ + payload: Hex.Hex | Bytes.Bytes + /** + * BLS private key. + */ + privateKey: Hex.Hex | Bytes.Bytes + /** + * Ciphersuite to use for signing. Defaults to "Basic". + * + * @see https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-bls-signature-05#section-4 + * @default 'BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_' + */ + suite?: string | undefined + /** + * Size of the signature to compute. + * + * - `'long-key:short-sig'`: 48 bytes + * - `'short-key:long-sig'`: 96 bytes + * + * @default 'short-key:long-sig' + */ + size?: size | Size | undefined + } + + type ErrorType = Bytes.from.ErrorType | Errors.GlobalErrorType +} + +sign.parseError = (error: unknown) => + /* v8 ignore next */ + error as sign.ErrorType + +/** + * Verifies a payload was signed by the provided public key(s). + * + * @example + * + * ```ts twoslash + * import { Bls } from 'ox' + * + * const privateKey = Bls.randomPrivateKey() + * const publicKey = Bls.getPublicKey({ privateKey }) + * const signature = Bls.sign({ payload: '0xdeadbeef', privateKey }) + * + * const verified = Bls.verify({ // [!code focus] + * payload: '0xdeadbeef', // [!code focus] + * publicKey, // [!code focus] + * signature, // [!code focus] + * }) // [!code focus] + * ``` + * + * @example + * ### Verify Aggregated Signatures + * + * We can also pass a public key and signature that was aggregated with {@link ox#Bls.(aggregate:function)} to `Bls.verify`. + * + * ```ts twoslash + * import { Bls, Hex } from 'ox' + * + * const payload = Hex.random(32) + * const privateKeys = Array.from({ length: 100 }, () => Bls.randomPrivateKey()) + * + * const publicKeys = privateKeys.map((privateKey) => + * Bls.getPublicKey({ privateKey }), + * ) + * const signatures = privateKeys.map((privateKey) => + * Bls.sign({ payload, privateKey }), + * ) + * + * const publicKey = Bls.aggregate(publicKeys) // [!code focus] + * const signature = Bls.aggregate(signatures) // [!code focus] + * + * const valid = Bls.verify({ payload, publicKey, signature }) // [!code focus] + * ``` + * + * @param options - Verification options. + * @returns Whether the payload was signed by the provided public key. + */ +export function verify(options: verify.Options): boolean { + const { payload, suite } = options + + const publicKey = options.publicKey as unknown as BlsPoint.BlsPoint + const signature = options.signature as unknown as BlsPoint.BlsPoint + + const isShortSig = typeof signature.x === 'bigint' + + const group = isShortSig ? bls.G1 : bls.G2 + const payloadPoint = group.hashToCurve( + Bytes.from(payload), + suite ? { DST: Bytes.fromString(suite) } : undefined, + ) as ProjPointType + + const shortSigPairing = () => + bls.pairingBatch([ + { + g1: payloadPoint, + g2: new bls.G2.ProjectivePoint(publicKey.x, publicKey.y, publicKey.z), + }, + { + g1: new bls.G1.ProjectivePoint(signature.x, signature.y, signature.z), + g2: bls.G2.ProjectivePoint.BASE.negate(), + }, + ]) + + const longSigPairing = () => + bls.pairingBatch([ + { + g1: new bls.G1.ProjectivePoint( + publicKey.x, + publicKey.y, + publicKey.z, + ).negate(), + g2: payloadPoint, + }, + { + g1: bls.G1.ProjectivePoint.BASE, + g2: new bls.G2.ProjectivePoint(signature.x, signature.y, signature.z), + }, + ]) + + return bls.fields.Fp12.eql( + isShortSig ? shortSigPairing() : longSigPairing(), + bls.fields.Fp12.ONE, + ) +} + +export declare namespace verify { + type Options = { + /** + * Payload that was signed. + */ + payload: Hex.Hex | Bytes.Bytes + /** + * Ciphersuite to use for verification. Defaults to "Basic". + * + * @see https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-bls-signature-05#section-4 + * @default 'BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_' + */ + suite?: string | undefined + } & OneOf< + | { + publicKey: BlsPoint.G1 + signature: BlsPoint.G2 + } + | { + publicKey: BlsPoint.G2 + signature: BlsPoint.G1 + } + > + + type ErrorType = Errors.GlobalErrorType +} + +/* v8 ignore next */ +verify.parseError = (error: unknown) => error as verify.ErrorType diff --git a/src/BlsPoint.ts b/src/BlsPoint.ts new file mode 100644 index 00000000..2207684b --- /dev/null +++ b/src/BlsPoint.ts @@ -0,0 +1,229 @@ +import { bls12_381 as bls } from '@noble/curves/bls12-381' + +import type * as Bytes from './Bytes.js' +import type * as Errors from './Errors.js' +import * as Hex from './Hex.js' +import type { Branded } from './internal/types.js' + +/** Type for a field element in the base field of the BLS12-381 curve. */ +export type Fp = bigint +/** Type for a field element in the extension field of the BLS12-381 curve. */ +export type Fp2 = { c0: Fp; c1: Fp } + +/** Root type for a BLS point on the G1 or G2 curve. */ +export type BlsPoint = { + x: type + y: type + z: type +} + +/** Type for a BLS point on the G1 curve. */ +export type G1 = BlsPoint +/** Branded type for a bytes representation of a G1 point. */ +export type G1Bytes = Branded +/** Branded type for a hex representation of a G1 point. */ +export type G1Hex = Branded + +/** Type for a BLS point on the G2 curve. */ +export type G2 = BlsPoint +/** Branded type for a bytes representation of a G2 point. */ +export type G2Bytes = Branded +/** Branded type for a hex representation of a G2 point. */ +export type G2Hex = Branded + +/** + * Converts a BLS point to {@link ox#Bytes.Bytes}. + * + * @example + * ### Public Key to Bytes + * ```ts twoslash + * import { Bls, BlsPoint } from 'ox' + * + * const publicKey = Bls.getPublicKey({ privateKey: '0x...' }) + * const publicKeyBytes = BlsPoint.toBytes(publicKey) + * // @log: Uint8Array [172, 175, 255, ...] + * ``` + * + * @example + * ### Signature to Bytes + * ```ts twoslash + * import { Bls, BlsPoint } from 'ox' + * + * const signature = Bls.sign({ payload: '0x...', privateKey: '0x...' }) + * const signatureBytes = BlsPoint.toBytes(signature) + * // @log: Uint8Array [172, 175, 255, ...] + * ``` + * + * @param point - The BLS point to convert. + * @returns The bytes representation of the BLS point. + */ +export function toBytes( + point: point, +): point extends G1 ? G1Bytes : G2Bytes { + const group = typeof point.z === 'bigint' ? bls.G1 : bls.G2 + return new (group as any).ProjectivePoint( + point.x, + point.y, + point.z, + ).toRawBytes() +} + +export declare namespace toBytes { + type ErrorType = Errors.GlobalErrorType +} + +/* v8 ignore next */ +toBytes.parseError = (error: unknown) => error as toBytes.ErrorType + +/** + * Converts a BLS point to {@link ox#Hex.Hex}. + * + * @example + * ### Public Key to Hex + * + * ```ts twoslash + * import { Bls, BlsPoint } from 'ox' + * + * const publicKey = Bls.getPublicKey({ privateKey: '0x...' }) + * const publicKeyHex = BlsPoint.toHex(publicKey) + * // @log: '0xacafff52270773ad1728df2807c0f1b0b271fa6b37dfb8b2f75448573c76c81bcd6790328a60e40ef5a13343b32d9e66' + * ``` + * + * @example + * ### Signature to Hex + * + * ```ts twoslash + * import { Bls, BlsPoint } from 'ox' + * + * const signature = Bls.sign({ payload: '0x...', privateKey: '0x...' }) + * const signatureHex = BlsPoint.toHex(signature) + * // @log: '0xb4698f7611999fba87033b9cf72312c76c683bbc48175e2d4cb275907d6a267ab9840a66e3051e5ed36fd13aa712f9a9024f9fa9b67f716dfb74ae4efb7d9f1b7b43b4679abed6644cf476c12e79f309351ea8452487cd93f66e29e04ebe427c' + * ``` + * + * @param point - The BLS point to convert. + * @returns The hex representation of the BLS point. + */ +export function toHex( + point: point, +): point extends G1 ? G1Hex : G2Hex +export function toHex(point: G1 | G2): Hex.Hex { + return Hex.fromBytes(toBytes(point)) +} + +export declare namespace toHex { + type ErrorType = Errors.GlobalErrorType +} + +/* v8 ignore next */ +toHex.parseError = (error: unknown) => error as toHex.ErrorType + +/** + * Converts {@link ox#Bytes.Bytes} to a BLS point. + * + * @example + * ### Bytes to Public Key + * + * ```ts twoslash + * // @noErrors + * import { BlsPoint } from 'ox' + * + * const publicKey = BlsPoint.fromBytes(Bytes.from([172, 175, 255, ...]), 'G1') + * // @log: { + * // @log: x: 172...n, + * // @log: y: 175...n, + * // @log: z: 1n, + * // @log: } + * ``` + * + * @example + * ### Bytes to Signature + * + * ```ts twoslash + * // @noErrors + * import { BlsPoint } from 'ox' + * + * const signature = BlsPoint.fromBytes(Bytes.from([172, 175, 255, ...]), 'G2') + * // @log: { + * // @log: x: 511...n, + * // @log: y: 234...n, + * // @log: z: 1n, + * // @log: } + * ``` + * + * @param bytes - The bytes to convert. + * @returns The BLS point. + */ +export function fromBytes( + bytes: Bytes.Bytes, + group: group, +): group extends 'G1' ? G1 : G2 +export function fromBytes(bytes: Bytes.Bytes): BlsPoint { + const group = bytes.length === 48 ? bls.G1 : bls.G2 + const point = group.ProjectivePoint.fromHex(bytes) + return { + x: point.px, + y: point.py, + z: point.pz, + } +} + +export declare namespace fromBytes { + type ErrorType = Errors.GlobalErrorType +} + +/* v8 ignore next */ +fromBytes.parseError = (error: unknown) => error as fromBytes.ErrorType + +/** + * Converts {@link ox#Hex.Hex} to a BLS point. + * + * @example + * ### Hex to Public Key + * + * ```ts twoslash + * // @noErrors + * import { BlsPoint } from 'ox' + * + * const publicKey = BlsPoint.fromHex('0xacafff52270773ad1728df2807c0f1b0b271fa6b37dfb8b2f75448573c76c81bcd6790328a60e40ef5a13343b32d9e66', 'G1') + * // @log: { + * // @log: x: 172...n, + * // @log: y: 175...n, + * // @log: z: 1n, + * // @log: } + * ``` + * + * @example + * ### Hex to Signature + * + * ```ts twoslash + * // @noErrors + * import { BlsPoint } from 'ox' + * + * const signature = BlsPoint.fromHex( + * '0xb4698f7611999fba87033b9cf72312c76c683bbc48175e2d4cb275907d6a267ab9840a66e3051e5ed36fd13aa712f9a9024f9fa9b67f716dfb74ae4efb7d9f1b7b43b4679abed6644cf476c12e79f309351ea8452487cd93f66e29e04ebe427c', + * 'G2', + * ) + * // @log: { + * // @log: x: 511...n, + * // @log: y: 234...n, + * // @log: z: 1n, + * // @log: } + * ``` + * + * @param bytes - The bytes to convert. + * @returns The BLS point. + */ +export function fromHex( + hex: Hex.Hex, + group: group, +): group extends 'G1' ? G1 : G2 +export function fromHex(hex: Hex.Hex, group: 'G1' | 'G2'): BlsPoint { + return fromBytes(Hex.toBytes(hex), group) +} + +export declare namespace fromHex { + type ErrorType = Errors.GlobalErrorType +} + +/* v8 ignore next */ +fromHex.parseError = (error: unknown) => error as fromHex.ErrorType diff --git a/src/P256.ts b/src/P256.ts index 09782f6f..b3481d07 100644 --- a/src/P256.ts +++ b/src/P256.ts @@ -5,6 +5,9 @@ import * as Hex from './Hex.js' import * as PublicKey from './PublicKey.js' import type * as Signature from './Signature.js' +/** Re-export of noble/curves P256 utilities. */ +export const noble = secp256r1 + /** * Computes the P256 ECDSA public key from a provided private key. * diff --git a/src/PublicKey.ts b/src/PublicKey.ts index 93f76014..056030ac 100644 --- a/src/PublicKey.ts +++ b/src/PublicKey.ts @@ -4,7 +4,7 @@ import * as Hex from './Hex.js' import * as Json from './Json.js' import type { Compute, ExactPartial } from './internal/types.js' -/** Root type for a Public Key. */ +/** Root type for an ECDSA Public Key. */ export type PublicKey< compressed extends boolean = false, bigintType = bigint, diff --git a/src/Secp256k1.ts b/src/Secp256k1.ts index 4c13813b..c7a81a45 100644 --- a/src/Secp256k1.ts +++ b/src/Secp256k1.ts @@ -7,6 +7,9 @@ import * as PublicKey from './PublicKey.js' import type * as Signature from './Signature.js' import type { OneOf } from './internal/types.js' +/** Re-export of noble/curves secp256k1 utilities. */ +export const noble = secp256k1 + /** * Computes the secp256k1 ECDSA public key from a provided private key. * diff --git a/src/_test/Bls.test.ts b/src/_test/Bls.test.ts new file mode 100644 index 00000000..b8d0b3d1 --- /dev/null +++ b/src/_test/Bls.test.ts @@ -0,0 +1,171 @@ +import { Bls, Hex } from 'ox' +import { describe, expect, it, test } from 'vitest' + +const privateKey = + '0x527f85c60ed7402247da21f1835cea651d0954fc15b7288f096d3608400cb6ac' + +describe('aggregate', () => { + test('default', () => { + const payload = Hex.random(32) + const privateKeys = Array.from({ length: 100 }, () => + Bls.randomPrivateKey(), + ) + + const signatures = privateKeys.map((privateKey) => + Bls.sign({ payload, privateKey }), + ) + const signature = Bls.aggregate(signatures) + + const publicKeys = privateKeys.map((privateKey) => + Bls.getPublicKey({ privateKey }), + ) + const publicKey = Bls.aggregate(publicKeys) + + const valid = Bls.verify({ + payload, + publicKey, + signature, + }) + expect(valid).toBe(true) + }) + + test('size: "long-key:short-sig"', () => { + const payload = Hex.random(32) + const privateKeys = Array.from({ length: 100 }, () => + Bls.randomPrivateKey(), + ) + + const signatures = privateKeys.map((privateKey) => + Bls.sign({ payload, privateKey, size: 'long-key:short-sig' }), + ) + const signature = Bls.aggregate(signatures) + + const publicKeys = privateKeys.map((privateKey) => + Bls.getPublicKey({ privateKey, size: 'long-key:short-sig' }), + ) + const publicKey = Bls.aggregate(publicKeys) + + const valid = Bls.verify({ + payload, + publicKey, + signature, + }) + expect(valid).toBe(true) + }) +}) + +describe('getPublicKey', () => { + it('default', () => { + const publicKey = Bls.getPublicKey({ privateKey }) + expect(publicKey).toMatchInlineSnapshot(` + { + "x": 1952783380189056174522580903352347766701573809635723835268303492562286913265402164399416172997666069723894699105894n, + "y": 3394089175947417419526317884165437122243448720528225119792553064107324599006426161993648623279289675312316462718429n, + "z": 1n, + } + `) + }) + + it('size: "long-key:short-sig"', () => { + const publicKey = Bls.getPublicKey({ + privateKey, + size: 'long-key:short-sig', + }) + expect(publicKey).toMatchInlineSnapshot(` + { + "x": { + "c0": 355700073819052008684778820175963255205495140126954969787089018774753222070622698188835174184164179369078130885244n, + "c1": 3141747483469678201696152507180044107401052508149828985947848597238369234167677882679751100991611433759974704216489n, + }, + "y": { + "c0": 2066498625632373741693121319338450383866133445054897600869581552265032944423583015562782022208222353927807243866749n, + "c1": 2094957017561088565638483770249057751981351081887405244515911122357724535677090041039038848651564427361620547334206n, + }, + "z": { + "c0": 1n, + "c1": 0n, + }, + } + `) + }) +}) + +describe('randomPrivateKey', () => { + it('default', () => { + const privateKey = Bls.randomPrivateKey() + expect(privateKey).toBeDefined() + expect(privateKey.length).toBe(66) + }) + + it('as: bytes', () => { + const privateKey = Bls.randomPrivateKey({ as: 'Bytes' }) + expect(privateKey).toBeDefined() + expect(privateKey.length).toBe(32) + }) +}) + +describe('sign', () => { + test('default', () => { + const payload = Hex.fromString('hello world') + const signature = Bls.sign({ payload, privateKey }) + + expect(signature).toMatchInlineSnapshot(` + { + "x": { + "c0": 3746086905447253682610543432049782958935576956103835696177151771320417199129097598091428899350812694802984340320507n, + "c1": 3283146048622226517954881634398987000360291525283271875090618615961785504260074427269084149051224291832699744536875n, + }, + "y": { + "c0": 2635882389000876446384650976799081995605300411488416553944693011070686484045367229225761824211357395609735141830301n, + "c1": 3150725065472868899001777665935568609665827996367588938443411055163385844006645735808933132167606601018627944394270n, + }, + "z": { + "c0": 1n, + "c1": 0n, + }, + } + `) + }) + + test('size: "long-key:short-sig"', () => { + const payload = Hex.fromString('hello world') + const signature = Bls.sign({ + payload, + privateKey, + size: 'long-key:short-sig', + }) + + expect(signature).toMatchInlineSnapshot(` + { + "x": 3755087731136217504597239089482980992941034609034858157163586991013626465307525157337003693748653301444139684957942n, + "y": 1939521305169884934876256226456095365402898060507274143668904400539743214063601510507290152344771164790023535191463n, + "z": 1n, + } + `) + }) +}) + +describe('verify', () => { + test('default', () => { + const payload = Hex.fromString('hello world') + const signature = Bls.sign({ payload, privateKey }) + const publicKey = Bls.getPublicKey({ privateKey }) + const verified = Bls.verify({ payload, publicKey, signature }) + expect(verified).toBe(true) + }) + + test('size: "long-key:short-sig"', () => { + const payload = Hex.fromString('hello world') + const signature = Bls.sign({ + payload, + privateKey, + size: 'long-key:short-sig', + }) + const publicKey = Bls.getPublicKey({ + privateKey, + size: 'long-key:short-sig', + }) + const verified = Bls.verify({ payload, publicKey, signature }) + expect(verified).toBe(true) + }) +}) diff --git a/src/_test/BlsPoint.test.ts b/src/_test/BlsPoint.test.ts new file mode 100644 index 00000000..01490146 --- /dev/null +++ b/src/_test/BlsPoint.test.ts @@ -0,0 +1,231 @@ +import { Bls, BlsPoint } from 'ox' +import { describe, expect, test } from 'vitest' + +const privateKey = + '0x527f85c60ed7402247da21f1835cea651d0954fc15b7288f096d3608400cb6ac' + +describe('fromBytes', () => { + test('G1', () => { + const publicKey = Bls.getPublicKey({ privateKey }) + const publicKeyHex = BlsPoint.toBytes(publicKey) + expect(BlsPoint.fromBytes(publicKeyHex, 'G1')).toEqual(publicKey) + }) + + test('G2', () => { + const publicKey = Bls.getPublicKey({ + privateKey, + size: 'long-key:short-sig', + }) + const publicKeyHex = BlsPoint.toBytes(publicKey) + expect(BlsPoint.fromBytes(publicKeyHex, 'G2')).toEqual(publicKey) + }) +}) + +describe('fromHex', () => { + test('G1', () => { + const publicKey = Bls.getPublicKey({ privateKey }) + const publicKeyHex = BlsPoint.toHex(publicKey) + expect(BlsPoint.fromHex(publicKeyHex, 'G1')).toEqual(publicKey) + }) + + test('G2', () => { + const publicKey = Bls.getPublicKey({ + privateKey, + size: 'long-key:short-sig', + }) + const publicKeyHex = BlsPoint.toHex(publicKey) + expect(BlsPoint.fromHex(publicKeyHex, 'G2')).toEqual(publicKey) + }) +}) + +describe('toHex', () => { + test('G1', () => { + const publicKey = Bls.getPublicKey({ privateKey }) + const publicKeyHex = BlsPoint.toHex(publicKey) + expect(publicKeyHex.toString()).toMatchInlineSnapshot( + `"0xacafff52270773ad1728df2807c0f1b0b271fa6b37dfb8b2f75448573c76c81bcd6790328a60e40ef5a13343b32d9e66"`, + ) + }) + + test('G2', () => { + const publicKey = Bls.getPublicKey({ + privateKey, + size: 'long-key:short-sig', + }) + const publicKeyHex = BlsPoint.toHex(publicKey) + expect(publicKeyHex.toString()).toMatchInlineSnapshot( + `"0xb4698f7611999fba87033b9cf72312c76c683bbc48175e2d4cb275907d6a267ab9840a66e3051e5ed36fd13aa712f9a9024f9fa9b67f716dfb74ae4efb7d9f1b7b43b4679abed6644cf476c12e79f309351ea8452487cd93f66e29e04ebe427c"`, + ) + }) +}) + +describe('toBytes', () => { + test('G1', () => { + const publicKey = Bls.getPublicKey({ privateKey }) + const publicKeyHex = BlsPoint.toBytes(publicKey) + expect(publicKeyHex).toMatchInlineSnapshot( + ` + Uint8Array [ + 172, + 175, + 255, + 82, + 39, + 7, + 115, + 173, + 23, + 40, + 223, + 40, + 7, + 192, + 241, + 176, + 178, + 113, + 250, + 107, + 55, + 223, + 184, + 178, + 247, + 84, + 72, + 87, + 60, + 118, + 200, + 27, + 205, + 103, + 144, + 50, + 138, + 96, + 228, + 14, + 245, + 161, + 51, + 67, + 179, + 45, + 158, + 102, + ] + `, + ) + }) + + test('G2', () => { + const publicKey = Bls.getPublicKey({ + privateKey, + size: 'long-key:short-sig', + }) + const publicKeyHex = BlsPoint.toBytes(publicKey) + expect(publicKeyHex).toMatchInlineSnapshot( + ` + Uint8Array [ + 180, + 105, + 143, + 118, + 17, + 153, + 159, + 186, + 135, + 3, + 59, + 156, + 247, + 35, + 18, + 199, + 108, + 104, + 59, + 188, + 72, + 23, + 94, + 45, + 76, + 178, + 117, + 144, + 125, + 106, + 38, + 122, + 185, + 132, + 10, + 102, + 227, + 5, + 30, + 94, + 211, + 111, + 209, + 58, + 167, + 18, + 249, + 169, + 2, + 79, + 159, + 169, + 182, + 127, + 113, + 109, + 251, + 116, + 174, + 78, + 251, + 125, + 159, + 27, + 123, + 67, + 180, + 103, + 154, + 190, + 214, + 100, + 76, + 244, + 118, + 193, + 46, + 121, + 243, + 9, + 53, + 30, + 168, + 69, + 36, + 135, + 205, + 147, + 246, + 110, + 41, + 224, + 78, + 190, + 66, + 124, + ] + `, + ) + }) +}) diff --git a/src/_test/P256.test.ts b/src/_test/P256.test.ts index 05229ca5..3cc49276 100644 --- a/src/_test/P256.test.ts +++ b/src/_test/P256.test.ts @@ -208,6 +208,7 @@ describe('verify', () => { test('exports', () => { expect(Object.keys(P256)).toMatchInlineSnapshot(` [ + "noble", "getPublicKey", "randomPrivateKey", "recoverPublicKey", diff --git a/src/_test/Secp256k1.test.ts b/src/_test/Secp256k1.test.ts index b1901a71..407869fd 100644 --- a/src/_test/Secp256k1.test.ts +++ b/src/_test/Secp256k1.test.ts @@ -218,6 +218,7 @@ describe('verify', () => { test('exports', () => { expect(Object.keys(Secp256k1)).toMatchInlineSnapshot(` [ + "noble", "getPublicKey", "randomPrivateKey", "recoverAddress", diff --git a/src/_test/index.test.ts b/src/_test/index.test.ts index 82c35f4e..77167ef7 100644 --- a/src/_test/index.test.ts +++ b/src/_test/index.test.ts @@ -21,6 +21,8 @@ test('exports', () => { "Blobs", "Block", "Bloom", + "Bls", + "BlsPoint", "Bytes", "Caches", "ContractAddress", diff --git a/src/index.ts b/src/index.ts index 6433625b..8fe5bf0f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -989,6 +989,167 @@ export * as Block from './Block.js' */ export * as Bloom from './Bloom.js' +/** + * Utility functions for [BLS12-381](https://hackmd.io/@benjaminion/bls12-381) cryptography. + * + * :::info + * + * The `Bls` module is a friendly wrapper over [`@noble/curves/bls12-381`](https://github.com/paulmillr/noble-curves), an **audited** implementation of BLS12-381. + * + * ::: + * + * @example + * ### Computing a Random Private Key + * + * A random private key can be computed using {@link ox#Bls.(randomPrivateKey:function)}: + * + * ```ts twoslash + * import { Bls } from 'ox' + * + * const privateKey = Bls.randomPrivateKey() + * // @log: '0x...' + * ``` + * + * @example + * ### Getting a Public Key + * + * A public key can be derived from a private key using {@link ox#Bls.(getPublicKey:function)}: + * + * ```ts twoslash + * import { Bls } from 'ox' + * + * const privateKey = Bls.randomPrivateKey() + * const publicKey = Bls.getPublicKey({ privateKey }) + * // @log: { x: 3251...5152n, y: 1251...5152n, z: 1n } + * ``` + * + * @example + * ### Signing a Payload + * + * A payload can be signed using {@link ox#Bls.(sign:function)}: + * + * ```ts twoslash + * import { Bls } from 'ox' + * + * const privateKey = Bls.randomPrivateKey() + * const signature = Bls.sign({ payload: '0xdeadbeef', privateKey }) + * // @log: { x: 1251...5152n, y: 1251...5152n, z: 1n } + * ``` + * + * @example + * ### Verifying a Signature + * + * A signature can be verified using {@link ox#Secp256k1.(verify:function)}: + * + * ```ts twoslash + * import { Bls } from 'ox' + * + * const privateKey = Bls.randomPrivateKey() + * const publicKey = Bls.getPublicKey({ privateKey }) + * const signature = Bls.sign({ payload: '0xdeadbeef', privateKey }) + * + * const isValid = Bls.verify({ // [!code focus] + * payload: '0xdeadbeef', // [!code focus] + * publicKey, // [!code focus] + * signature, // [!code focus] + * }) // [!code focus] + * // @log: true + * ``` + * + * @example + * ### Aggregating Public Keys & Signatures + * + * Public keys and signatures can be aggregated using {@link ox#Bls.(aggregate:function)}: + * + * ```ts twoslash + * import { Bls } from 'ox' + * + * const publicKeys = [ + * Bls.getPublicKey({ privateKey: '0x...' }), + * Bls.getPublicKey({ privateKey: '0x...' }), + * ] + * const publicKey = Bls.aggregate(publicKeys) + * + * const signatures = [ + * Bls.sign({ payload: '0x...', privateKey: '0x...' }), + * Bls.sign({ payload: '0x...', privateKey: '0x...' }), + * ] + * const signature = Bls.aggregate(signatures) + * ``` + * + * @example + * ### Verify Aggregated Signatures + * + * We can also pass a public key and signature that was aggregated with {@link ox#Bls.(aggregate:function)} to `Bls.verify`. + * + * ```ts twoslash + * import { Bls, Hex } from 'ox' + * + * const payload = Hex.random(32) + * const privateKeys = Array.from({ length: 100 }, () => Bls.randomPrivateKey()) + * + * const publicKeys = privateKeys.map((privateKey) => + * Bls.getPublicKey({ privateKey }), + * ) + * const signatures = privateKeys.map((privateKey) => + * Bls.sign({ payload, privateKey }), + * ) + * + * const publicKey = Bls.aggregate(publicKeys) // [!code focus] + * const signature = Bls.aggregate(signatures) // [!code focus] + * + * const valid = Bls.verify({ payload, publicKey, signature }) // [!code focus] + * ``` + * + * @category Crypto + */ +export * as Bls from './Bls.js' + +/** + * Utility functions for working with BLS12-381 points. + * + * :::info + * + * The `BlsPoint` module is a friendly wrapper over [`@noble/curves/bls12-381`](https://github.com/paulmillr/noble-curves), an **audited** implementation of BLS12-381. + * + * ::: + * + * @example + * ### Public Keys or Signatures to Hex + * + * BLS points can be converted to hex using {@link ox#BlsPoint.(toHex:function)}: + * + * ```ts twoslash + * import { Bls, BlsPoint } from 'ox' + * + * const publicKey = Bls.getPublicKey({ privateKey: '0x...' }) + * const publicKeyHex = BlsPoint.toHex(publicKey) + * // @log: '0xacafff52270773ad1728df2807c0f1b0b271fa6b37dfb8b2f75448573c76c81bcd6790328a60e40ef5a13343b32d9e66' + * + * const signature = Bls.sign({ payload: '0xdeadbeef', privateKey: '0x...' }) + * const signatureHex = BlsPoint.toHex(signature) + * // @log: '0xb4698f7611999fba87033b9cf72312c76c683bbc48175e2d4cb275907d6a267ab9840a66e3051e5ed36fd13aa712f9a9024f9fa9b67f716dfb74ae4efb7d9f1b7b43b4679abed6644cf476c12e79f309351ea8452487cd93f66e29e04ebe427c' + * ``` + * + * @example + * ### Hex to Public Keys or Signatures + * + * BLS points can be converted from hex using {@link ox#BlsPoint.(fromHex:function)}: + * + * ```ts twoslash + * import { Bls, BlsPoint } from 'ox' + * + * const publicKey = BlsPoint.fromHex('0xacafff52270773ad1728df2807c0f1b0b271fa6b37dfb8b2f75448573c76c81bcd6790328a60e40ef5a13343b32d9e66', 'G1') + * // @log: { x: 172...514n, y: 175...235n, z: 1n } + * + * const signature = BlsPoint.fromHex('0xb4698f7611999fba87033b9cf72312c76c683bbc48175e2d4cb275907d6a267ab9840a66e3051e5ed36fd13aa712f9a9024f9fa9b67f716dfb74ae4efb7d9f1b7b43b4679abed6644cf476c12e79f309351ea8452487cd93f66e29e04ebe427c', 'G2') + * // @log: { x: 1251...5152n, y: 1251...5152n, z: 1n } + * ``` + * + * @category Crypto + */ +export * as BlsPoint from './BlsPoint.js' + /** * A set of Ethereum-related utility functions for working with [`Uint8Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array) instances. * @@ -1802,6 +1963,13 @@ export * as RpcResponse from './RpcResponse.js' /** * Utility functions for working with JSON-RPC Transports. * + * :::note + * This is a convenience module distributed for experimenting with network connectivity on Ox. + * + * Consider using networking functionality from a higher-level library such as [Viem's Transports](https://viem.sh/docs/clients/transports/http) + * if you need more features such as: retry logic, WebSockets/IPC, middleware, batch JSON-RPC, etc. + * ::: + * * @example * ### HTTP Instantiation *