From 5cdd1dcfaa327082af5cff6c5f316accdd515a59 Mon Sep 17 00:00:00 2001 From: Kevin Jahns Date: Wed, 12 Jul 2023 20:24:09 +0200 Subject: [PATCH] [sha256] refactor to class with faster execution --- README.md | 2 +- hash/sha256.fallback.js | 114 +++++++++++++++++++++++----------------- hash/sha256.test.js | 9 +++- package.json | 2 +- 4 files changed, 75 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index 5f88fca..c6504a2 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ export {f} Each function in this library is tested thoroughly and is not deoptimized by v8 (except some logging and comparison functions that can't be implemented without deoptimizations). This library implements its own test suite that is designed for randomized testing and inspecting performance issues. -* `node --trace-deop` and `node --trace-opt` +* `node --trace-deopt` and `node --trace-opt` * https://youtu.be/IFWulQnM5E0 Good intro video * https://github.com/thlorenz/v8-perf * https://github.com/thlorenz/deoptigate - A great tool for investigating deoptimizations diff --git a/hash/sha256.fallback.js b/hash/sha256.fallback.js index d08c520..67b63c0 100644 --- a/hash/sha256.fallback.js +++ b/hash/sha256.fallback.js @@ -69,15 +69,21 @@ const HINIT = new Uint32Array([ 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19 ]) -/** - * @param {Uint8Array} data - */ -export const hash = data => { - // Init working variables. - const H = new Uint32Array(HINIT) - // "Message schedule" - a working variable - const W = new Uint32Array(64) - const updateHash = () => { +// time to beat: (large value < 4.35s) + +class Hasher { + constructor () { + const buf = new ArrayBuffer(64 + 64 * 4) + // Init working variables using a single arraybuffer + this.H = new Uint32Array(buf, 0, 8) + this.H.set(HINIT) + // "Message schedule" - a working variable + this.W = new Uint32Array(buf, 64, 64) + } + + _updateHash () { + const H = this.H + const W = this.W for (let t = 16; t < 64; t++) { W[t] = sigma1to256(W[t - 2]) + W[t - 7] + sigma0to256(W[t - 15]) + W[t - 16] } @@ -89,7 +95,6 @@ export const hash = data => { let f = H[5] let g = H[6] let h = H[7] - // Step 3 for (let tt = 0, T1, T2; tt < 64; tt++) { T1 = (h + sum1to256(e) + ((e & f) ^ (~e & g)) + K[tt] + W[tt]) >>> 0 T2 = (sum0to256(a) + ((a & b) ^ (a & c) ^ (b & c))) >>> 0 @@ -111,49 +116,60 @@ export const hash = data => { H[6] += g H[7] += h } - let i = 0 - let isPaddedWith1 = false - for (; i + 56 <= data.length;) { - // write data in big endianess - let j = 0 - for (; j < 16 && i + 3 < data.length; j++) { - W[j] = data[i++] << 24 | data[i++] << 16 | data[i++] << 8 | data[i++] + + /** + * @param {Uint8Array} data + */ + hash (data) { + let i = 0 + for (; i + 56 <= data.length;) { + // write data in big endianess + let j = 0 + for (; j < 16 && i + 3 < data.length; j++) { + this.W[j] = data[i++] << 24 | data[i++] << 16 | data[i++] << 8 | data[i++] + } + if (i % 64 !== 0) { // there is still room to write partial content and the ending bit. + this.W.fill(0, j, 16) + while (i < data.length) { + this.W[j] |= data[i] << ((3 - (i % 4)) * 8) + i++ + } + this.W[j] |= binary.BIT8 << ((3 - (i % 4)) * 8) + } + this._updateHash() } - if (i % 64 !== 0) { // there is still room to write partial content and the ending bit. - W.fill(0, j, 16) - isPaddedWith1 = true - while (i < data.length) { - W[j] |= data[i] << ((3 - (i % 4)) * 8) - i++ + // same check as earlier - the ending bit has been written + const isPaddedWith1 = i % 64 !== 0 + this.W.fill(0, 0, 16) + let j = 0 + for (; i < data.length; j++) { + for (let ci = 3; ci >= 0 && i < data.length; ci--) { + this.W[j] |= data[i++] << (ci * 8) } - W[j] |= binary.BIT8 << ((3 - (i % 4)) * 8) } - updateHash() - } - // write rest of the data, including the padding (using msb endiannes) - let j = 0 - W.fill(0, 0, 16) - for (; i < data.length; j++) { - for (let ci = 3; ci >= 0 && i < data.length; ci--) { - W[j] |= data[i++] << (ci * 8) + // Write padding of the message. See 5.1.2. + if (!isPaddedWith1) { + this.W[j - (i % 4 === 0 ? 0 : 1)] |= binary.BIT8 << ((3 - (i % 4)) * 8) } - } - // Write padding of the message. See 5.1.2. - if (!isPaddedWith1) { - W[j - (i % 4 === 0 ? 0 : 1)] |= binary.BIT8 << ((3 - (i % 4)) * 8) - } - // write length of message (size in bits) as 64 bit uint - // @todo test that this works correctly - W[14] = math.round(data.byteLength / binary.BIT30) - W[15] = data.byteLength * 8 - updateHash() - // correct H endianness and return a Uint8Array view - const dv = new Uint8Array(H.buffer) - for (let i = 0; i < H.length; i++) { - const h = H[i] - for (let ci = 0; ci < 4; ci++) { - dv[i * 4 + ci] = h >>> (3 - ci) * 8 + // write length of message (size in bits) as 64 bit uint + // @todo test that this works correctly + this.W[14] = data.byteLength / binary.BIT30 // same as data.byteLength >>> 30 - but works on floats + this.W[15] = data.byteLength * 8 + this._updateHash() + // correct H endianness to use big endiannes and return a Uint8Array + const dv = new Uint8Array(32) + for (let i = 0; i < this.H.length; i++) { + for (let ci = 0; ci < 4; ci++) { + dv[i * 4 + ci] = this.H[i] >>> (3 - ci) * 8 + } } + return dv } - return dv +} + +/** + * @param {Uint8Array} data + */ +export const hash = data => { + return new Hasher().hash(data) } diff --git a/hash/sha256.test.js b/hash/sha256.test.js index 89427e6..3e83bbb 100644 --- a/hash/sha256.test.js +++ b/hash/sha256.test.js @@ -27,10 +27,17 @@ export const testSha256Basics = async _tc => { const resWebcryptoHex = buffer.toHexString(resWebcrypto) t.assert(resWebcryptoHex === result) } - + // const newInput = 'abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopno' + // const xx = new Uint8Array(await webcrypto.subtle.digest('SHA-256', string.encodeUtf8(newInput))) + // console.log(buffer.toHexString(xx), ' dtrndtndtn', newInput.length) await test('', 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855') + await test(string.encodeUtf8('ab'), 'fb8e20fc2e4c3f248c60c39bd652f3c1347298bb977b8b4d5903b85055620603') await test(string.encodeUtf8('abc'), 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad') + await test(string.encodeUtf8('abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopno'), '71806f0af18dbbe905f8fcaa576b8e687859163cf68b38bc26f32e5120522cc1') await test(string.encodeUtf8('abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq'), '248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1') + await test(string.encodeUtf8('abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopqqqq0001'), 'e54ad86cad2d9d7c03506d6a67ed03fab5fbb8d34012143e9c0eb88ace56ca59') + await test(string.encodeUtf8('abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopqqqq00012'), '7a1e062405a534817dcb89fa2416a69e4fbe75fabece33d528b82d1b71d3e418') + await test(string.encodeUtf8('abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopqqqq000123'), '51691fe639226a9df2c0e4637918990291c73cdbb58665a7c729bb4e8d67784c') } /** diff --git a/package.json b/package.json index 609312f..4550b49 100644 --- a/package.json +++ b/package.json @@ -472,7 +472,7 @@ "debug": "npm run gentesthtml && npx 0serve -o test.html", "test": "c8 --check-coverage --lines 100 --branches 100 --functions 100 --statements 100 node --unhandled-rejections=strict ./test.js --repetition-time 50 --production", "test-inspect": "node --inspect-brk --unhandled-rejections=strict ./test.js --repetition-time 50 --production", - "test-extensive": "node test.js && npm test -- --repetition-time 30000 --extensive", + "test-extensive": "c8 --check-coverage --lines 100 --branches 100 --functions 100 --statements 100 node test.js --repetition-time 30000 --extensive --production", "trace-deopt": "clear && rollup -c && node --trace-deopt dist/test.cjs", "trace-opt": "clear && rollup -c && node --trace-opt dist/test.cjs", "lint": "standard && tsc",