ox
Version:
Ethereum Standard Library
300 lines • 10.3 kB
JavaScript
import { bls12_381 as bls } from '@noble/curves/bls12-381.js';
import * as BlsPoint from './BlsPoint.js';
import * as Bytes from './Bytes.js';
import * as Errors from './Errors.js';
import * as Hex from './Hex.js';
/**
* Coerces a serialized or structured BLS point into a structured
* {@link ox#BlsPoint.BlsPoint}.
*
* @internal
*/
function normalizeBlsPoint(value, group) {
if (typeof value === 'string')
return BlsPoint.fromHex(value, group);
if (value instanceof Uint8Array)
return BlsPoint.fromBytes(value, group);
return value;
}
/**
* Formats a structured BLS point as the requested representation.
*
* @internal
*/
function formatBlsPoint(point, as) {
if (as === 'Hex')
return BlsPoint.toHex(point);
if (as === 'Bytes')
return BlsPoint.toBytes(point);
return point;
}
/**
* Returns the byte length of a serialized BLS point. Hex strings are measured
* minus the `0x` prefix, divided by two; `Uint8Array`s use `byteLength`.
*
* @internal
*/
function signatureByteLength(value) {
if (typeof value === 'string')
return (value.length - 2) / 2;
return value.byteLength;
}
/** Re-export of noble/curves BLS12-381 utilities. */
export const noble = bls;
// eslint-disable-next-line jsdoc-js/require-jsdoc
export function aggregate(points, options = {}) {
if (points.length === 0)
throw new Errors.BaseError('Bls.aggregate expects a non-empty array of points.');
// Normalize once -- accept structured points, hex strings, or `Uint8Array`s.
const groupHint = options.group;
const normalized = points.map((point) => {
if (typeof point === 'string' || point instanceof Uint8Array) {
if (!groupHint)
throw new Errors.BaseError('Bls.aggregate requires `options.group` (`"G1"` or `"G2"`) when passing serialized points.');
return normalizeBlsPoint(point, groupHint);
}
return point;
});
const first = normalized[0];
// Fast path: a single point aggregates to itself.
if (normalized.length === 1)
return first;
const isG1 = typeof first.x === 'string';
for (let i = 1; i < normalized.length; i++) {
if ((typeof normalized[i].x === 'string') !== isG1)
throw new Errors.BaseError('Bls.aggregate expects all points to be from the same group (G1 or G2).');
}
const groupName = isG1 ? 'G1' : 'G2';
const group = isG1 ? bls.G1 : bls.G2;
let acc = group.Point.ZERO;
for (let i = 0; i < normalized.length; i++) {
const p = normalized[i];
acc = acc.add(BlsPoint.toNoblePoint(p, groupName));
}
return BlsPoint.fromNoblePoint(acc, groupName);
}
/**
* Creates a new BLS12-381 key pair consisting of a private key and its corresponding public key.
*
* - 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.createKeyPair()
* // ^?
* ```
*
* @example
* ### Long G2 Public Keys
*
* A G2 Public Key can be derived as a G2 point (96 bytes) using `size: 'long-key:short-sig'`.
*
* 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.createKeyPair({
* size: 'long-key:short-sig'
* })
*
* publicKey
* // ^?
* ```
*
* ### Serializing
*
* Public Keys can be serialized to hex or bytes using {@link ox#BlsPoint.(toHex:function)} or {@link ox#BlsPoint.(toBytes:function)}:
*
* ```ts twoslash
* import { Bls, BlsPoint } from 'ox'
*
* const { publicKey } = Bls.createKeyPair()
*
* const publicKeyHex = BlsPoint.toHex(publicKey)
* // ^?
*
* const publicKeyBytes = BlsPoint.toBytes(publicKey)
* // ^?
* ```
*
* They can also be deserialized from hex or bytes using {@link ox#BlsPoint.(fromHex:function)} or {@link ox#BlsPoint.(fromBytes:function)}:
*
* ```ts twoslash
* import { Bls, BlsPoint } from 'ox'
*
* const publicKeyHex = '0x...'
*
* const publicKey = BlsPoint.fromHex(publicKeyHex, 'G1')
* // ^?
* ```
*
* @param options - The options to generate the key pair.
* @returns The generated key pair containing both private and public keys.
*/
export function createKeyPair(options = {}) {
const { as = 'Hex', size = 'short-key:long-sig' } = options;
const privateKey = randomPrivateKey({ as });
const publicKey = getPublicKey({ privateKey, size });
return {
privateKey: privateKey,
publicKey: publicKey,
};
}
// eslint-disable-next-line jsdoc-js/require-jsdoc
export function getPublicKey(options) {
const { as = 'Object', privateKey, size = 'short-key:long-sig' } = options;
const groupName = size === 'short-key:long-sig' ? 'G1' : 'G2';
const group = groupName === 'G1' ? bls.G1 : bls.G2;
const point = group.Point.BASE.multiply(group.Point.Fn.fromBytes(Bytes.from(privateKey)));
const publicKey = BlsPoint.fromNoblePoint(point, groupName);
return formatBlsPoint(publicKey, as);
}
/**
* 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 = {}) {
const { as = 'Hex' } = options;
const bytes = bls.utils.randomSecretKey();
if (as === 'Hex')
return Hex.fromBytes(bytes);
return bytes;
}
// eslint-disable-next-line jsdoc-js/require-jsdoc
export function sign(options) {
const { as = 'Object', payload, privateKey, suite, size = 'short-key:long-sig', } = options;
const signatureGroupName = size === 'short-key:long-sig' ? 'G2' : 'G1';
const payloadGroup = signatureGroupName === 'G2' ? 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.Point.Fn.fromBytes(Bytes.from(privateKey)));
const result = BlsPoint.fromNoblePoint(signature, signatureGroupName);
return formatBlsPoint(result, as);
}
/**
* Verifies a payload was signed by the provided public key(s).
*
* @example
*
* ```ts twoslash
* import { Bls, Hex } from 'ox'
*
* const payload = Hex.random(32)
* const privateKey = Bls.randomPrivateKey()
*
* const publicKey = Bls.getPublicKey({ privateKey })
* const signature = Bls.sign({ payload, privateKey })
*
* const verified = Bls.verify({
* // [!code focus]
* payload, // [!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) {
const { payload, suite } = options;
// Accept structured / hex / bytes inputs. Inspect the *signature* group
// first when structured, otherwise fall back to the explicit `group` /
// pair-shape hint to know how to deserialize.
const signatureRaw = options.signature;
const publicKeyRaw = options.publicKey;
// If signature is structured, infer signature group via field shape.
const signatureIsStructured = typeof signatureRaw === 'object' &&
!(signatureRaw instanceof Uint8Array) &&
'z' in signatureRaw;
const publicKeyIsStructured = typeof publicKeyRaw === 'object' &&
!(publicKeyRaw instanceof Uint8Array) &&
'z' in publicKeyRaw;
// Determine signature group: G1 (short sig, x is hex string) or G2 (long
// sig, x is `{ c0, c1 }`). For serialized signatures, infer from the
// byte/hex length.
const signatureGroup = signatureIsStructured
? typeof signatureRaw.x === 'string'
? 'G1'
: 'G2'
: signatureByteLength(signatureRaw) === 48
? 'G1'
: 'G2';
const publicKeyGroup = signatureGroup === 'G1' ? 'G2' : 'G1';
const signature = (signatureIsStructured
? signatureRaw
: normalizeBlsPoint(signatureRaw, signatureGroup));
const publicKey = (publicKeyIsStructured
? publicKeyRaw
: normalizeBlsPoint(publicKeyRaw, publicKeyGroup));
const isShortSig = signatureGroup === 'G1';
const group = isShortSig ? bls.G1 : bls.G2;
const payloadPoint = group.hashToCurve(Bytes.from(payload), suite ? { DST: Bytes.fromString(suite) } : undefined);
const shortSigPairing = () => bls.pairingBatch([
{
g1: payloadPoint,
g2: BlsPoint.toNoblePoint(publicKey, 'G2'),
},
{
g1: BlsPoint.toNoblePoint(signature, 'G1'),
g2: bls.G2.Point.BASE.negate(),
},
]);
const longSigPairing = () => bls.pairingBatch([
{
g1: BlsPoint.toNoblePoint(publicKey, 'G1').negate(),
g2: payloadPoint,
},
{
g1: bls.G1.Point.BASE,
g2: BlsPoint.toNoblePoint(signature, 'G2'),
},
]);
return bls.fields.Fp12.eql(isShortSig ? shortSigPairing() : longSigPairing(), bls.fields.Fp12.ONE);
}
//# sourceMappingURL=Bls.js.map