hash-to-curve: speed-up os2ip, change code a bit

This commit is contained in:
Paul Miller 2023-02-26 17:55:30 +00:00
parent 0163b63532
commit 6bc4b35cf4
No known key found for this signature in database
GPG Key ID: 697079DA6878B89B
4 changed files with 45 additions and 32 deletions

@ -692,6 +692,16 @@ aggregatePublicKeys/128 x 7 ops/sec @ 125ms/op
aggregateSignatures/8 x 45 ops/sec @ 22ms/op aggregateSignatures/8 x 45 ops/sec @ 22ms/op
aggregateSignatures/32 x 11 ops/sec @ 84ms/op aggregateSignatures/32 x 11 ops/sec @ 84ms/op
aggregateSignatures/128 x 3 ops/sec @ 332ms/opp aggregateSignatures/128 x 3 ops/sec @ 332ms/opp
hash-to-curve
hash_to_field x 850,340 ops/sec @ 1μs/op
hashToCurve
├─secp256k1 x 1,850 ops/sec @ 540μs/op
├─P256 x 3,352 ops/sec @ 298μs/op
├─P384 x 1,367 ops/sec @ 731μs/op
├─P521 x 691 ops/sec @ 1ms/op
├─ed25519 x 2,492 ops/sec @ 401μs/op
└─ed448 x 1,045 ops/sec @ 956μs/op
``` ```
## Resources ## Resources

@ -11,6 +11,7 @@ import { hashToCurve as P521 } from '../p521.js';
import { hashToCurve as ed25519 } from '../ed25519.js'; import { hashToCurve as ed25519 } from '../ed25519.js';
import { hashToCurve as ed448 } from '../ed448.js'; import { hashToCurve as ed448 } from '../ed448.js';
import { utf8ToBytes } from '../abstract/utils.js'; import { utf8ToBytes } from '../abstract/utils.js';
const N = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n; const N = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n;
run(async () => { run(async () => {
const rand = randomBytes(40); const rand = randomBytes(40);

@ -1,11 +1,10 @@
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */ /*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
import type { Group, GroupConstructor, AffinePoint } from './curve.js'; import type { Group, GroupConstructor, AffinePoint } from './curve.js';
import { mod, Field } from './modular.js'; import { mod, Field } from './modular.js';
import { CHash, concatBytes, utf8ToBytes, validateObject } from './utils.js'; import { bytesToNumberBE, CHash, concatBytes, utf8ToBytes, validateObject } from './utils.js';
export type Opts = { export type Opts = {
DST: string; // DST: a domain separation tag, defined in section 2.2.5 DST: string | Uint8Array; // DST: a domain separation tag, defined in section 2.2.5
encodeDST: string;
p: bigint; // characteristic of F, where F is a finite field of characteristic p and order q = p^m p: bigint; // characteristic of F, where F is a finite field of characteristic p and order q = p^m
m: number; // extension degree of F, m >= 1 m: number; // extension degree of F, m >= 1
k: number; // k: the target security level for the suite in bits, defined in section 5.1 k: number; // k: the target security level for the suite in bits, defined in section 5.1
@ -13,21 +12,20 @@ export type Opts = {
// Hash functions for: expand_message_xmd is appropriate for use with a // Hash functions for: expand_message_xmd is appropriate for use with a
// wide range of hash functions, including SHA-2, SHA-3, BLAKE2, and others. // wide range of hash functions, including SHA-2, SHA-3, BLAKE2, and others.
// BBS+ uses blake2: https://github.com/hyperledger/aries-framework-go/issues/2247 // BBS+ uses blake2: https://github.com/hyperledger/aries-framework-go/issues/2247
// TODO: verify that hash is shake if expand==='xof' via types // TODO: verify that hash is shake if expand === 'xof' via types
hash: CHash; hash: CHash;
}; };
// Octet Stream to Integer (bytesToNumberBE) function validateDST(dst: string | Uint8Array): Uint8Array {
function os2ip(bytes: Uint8Array): bigint { if (dst instanceof Uint8Array) return dst;
let result = 0n; if (typeof dst === 'string') return utf8ToBytes(dst);
for (let i = 0; i < bytes.length; i++) { throw new Error('DST must be Uint8Array or string');
result <<= 8n;
result += BigInt(bytes[i]);
}
return result;
} }
// Integer to Octet Stream // Octet Stream to Integer. "spec" implementation of os2ip is 2.5x slower vs bytesToNumberBE.
const os2ip = bytesToNumberBE;
// Integer to Octet Stream (numberToBytesBE)
function i2osp(value: number, length: number): Uint8Array { function i2osp(value: number, length: number): Uint8Array {
if (value < 0 || value >= 1 << (8 * length)) { if (value < 0 || value >= 1 << (8 * length)) {
throw new Error(`bad I2OSP call: value=${value} length=${length}`); throw new Error(`bad I2OSP call: value=${value} length=${length}`);
@ -68,13 +66,12 @@ export function expand_message_xmd(
isNum(lenInBytes); isNum(lenInBytes);
// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-16#section-5.3.3 // https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-16#section-5.3.3
if (DST.length > 255) DST = H(concatBytes(utf8ToBytes('H2C-OVERSIZE-DST-'), DST)); if (DST.length > 255) DST = H(concatBytes(utf8ToBytes('H2C-OVERSIZE-DST-'), DST));
const b_in_bytes = H.outputLen; const { outputLen: b_in_bytes, blockLen: r_in_bytes } = H;
const r_in_bytes = H.blockLen;
const ell = Math.ceil(lenInBytes / b_in_bytes); const ell = Math.ceil(lenInBytes / b_in_bytes);
if (ell > 255) throw new Error('Invalid xmd length'); if (ell > 255) throw new Error('Invalid xmd length');
const DST_prime = concatBytes(DST, i2osp(DST.length, 1)); const DST_prime = concatBytes(DST, i2osp(DST.length, 1));
const Z_pad = i2osp(0, r_in_bytes); const Z_pad = i2osp(0, r_in_bytes);
const l_i_b_str = i2osp(lenInBytes, 2); const l_i_b_str = i2osp(lenInBytes, 2); // len_in_bytes_str
const b = new Array<Uint8Array>(ell); const b = new Array<Uint8Array>(ell);
const b_0 = H(concatBytes(Z_pad, msg, l_i_b_str, i2osp(0, 1), DST_prime)); const b_0 = H(concatBytes(Z_pad, msg, l_i_b_str, i2osp(0, 1), DST_prime));
b[0] = H(concatBytes(b_0, i2osp(1, 1), DST_prime)); b[0] = H(concatBytes(b_0, i2osp(1, 1), DST_prime));
@ -118,32 +115,40 @@ export function expand_message_xof(
/** /**
* Hashes arbitrary-length byte strings to a list of one or more elements of a finite field F * Hashes arbitrary-length byte strings to a list of one or more elements of a finite field F
* https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#section-5.3 * https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#section-5.3
* As for options:
* * `p` is field prime, m=field extension (1 for prime fields)
* * `k` is security target in bits (e.g. 128).
* * `expand` should be `xmd` for SHA2, SHA3, BLAKE; `xof` for SHAKE, BLAKE-XOF
* * `hash` conforming to `utils.CHash` interface, with `outputLen` / `blockLen` props
* @param msg a byte string containing the message to hash * @param msg a byte string containing the message to hash
* @param count the number of elements of F to output * @param count the number of elements of F to output
* @param options `{DST: string, p: bigint, m: number, k: number, expand: 'xmd' | 'xof', hash: H}` * @param options `{DST: string, p: bigint, m: number, k: number, expand: 'xmd' | 'xof', hash: H}`, see above
* @returns [u_0, ..., u_(count - 1)], a list of field elements. * @returns [u_0, ..., u_(count - 1)], a list of field elements.
*/ */
export function hash_to_field(msg: Uint8Array, count: number, options: Opts): bigint[][] { export function hash_to_field(msg: Uint8Array, count: number, options: Opts): bigint[][] {
const { p, k, m, hash, expand, DST: _DST } = options; const { p, k, m, hash, expand, DST: _DST } = options;
isBytes(msg); isBytes(msg);
isNum(count); isNum(count);
if (typeof _DST !== 'string') throw new Error('DST must be valid'); const DST = validateDST(_DST);
const log2p = p.toString(2).length; const log2p = p.toString(2).length;
const L = Math.ceil((log2p + k) / 8); // section 5.1 of ietf draft link above const L = Math.ceil((log2p + k) / 8); // section 5.1 of ietf draft link above
const len_in_bytes = count * m * L; const len_in_bytes = count * m * L;
const DST = utf8ToBytes(_DST); let prb; // pseudo_random_bytes
const pseudo_random_bytes = if (expand === 'xmd') {
expand === 'xmd' prb = expand_message_xmd(msg, DST, len_in_bytes, hash);
? expand_message_xmd(msg, DST, len_in_bytes, hash) } else if (expand === 'xof') {
: expand === 'xof' prb = expand_message_xof(msg, DST, len_in_bytes, k, hash);
? expand_message_xof(msg, DST, len_in_bytes, k, hash) } else if (expand === undefined) {
: msg; prb = msg;
} else {
throw new Error('expand must be "xmd", "xof" or undefined');
}
const u = new Array(count); const u = new Array(count);
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
const e = new Array(m); const e = new Array(m);
for (let j = 0; j < m; j++) { for (let j = 0; j < m; j++) {
const elm_offset = L * (j + i * m); const elm_offset = L * (j + i * m);
const tv = pseudo_random_bytes.subarray(elm_offset, elm_offset + L); const tv = prb.subarray(elm_offset, elm_offset + L);
e[j] = mod(os2ip(tv), p); e[j] = mod(os2ip(tv), p);
} }
u[i] = e; u[i] = e;
@ -184,7 +189,7 @@ export type htfBasicOpts = { DST: string };
export function createHasher<T>( export function createHasher<T>(
Point: H2CPointConstructor<T>, Point: H2CPointConstructor<T>,
mapToCurve: MapToCurve<T>, mapToCurve: MapToCurve<T>,
def: Opts def: Opts & { encodeDST?: string }
) { ) {
validateObject(def, { validateObject(def, {
DST: 'string', DST: 'string',
@ -193,10 +198,7 @@ export function createHasher<T>(
k: 'isSafeInteger', k: 'isSafeInteger',
hash: 'hash', hash: 'hash',
}); });
if (def.expand !== 'xmd' && def.expand !== 'xof' && def.expand !== undefined) if (typeof mapToCurve !== 'function') throw new Error('mapToCurve() must be defined');
throw new Error('Invalid htf/expand');
if (typeof mapToCurve !== 'function')
throw new Error('hashToCurve: mapToCurve() has not been defined');
return { return {
// Encodes byte string to elliptic curve // Encodes byte string to elliptic curve
// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#section-3 // https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#section-3

@ -857,7 +857,7 @@ describe('bls12-381/basic', () => {
const options = { const options = {
p: bls.CURVE.r, p: bls.CURVE.r,
m: 1, m: 1,
expand: false, expand: undefined,
}; };
for (let vector of SCALAR_VECTORS) { for (let vector of SCALAR_VECTORS) {
const [okmAscii, expectedHex] = vector; const [okmAscii, expectedHex] = vector;