Skip to content

Commit

Permalink
[sha256] refactor to class with faster execution
Browse files Browse the repository at this point in the history
  • Loading branch information
dmonad committed Jul 12, 2023
1 parent d08f772 commit 5cdd1dc
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 52 deletions.
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -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
Expand Down
114 changes: 65 additions & 49 deletions hash/sha256.fallback.js
Expand Up @@ -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]
}
Expand All @@ -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
Expand All @@ -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)
}
9 changes: 8 additions & 1 deletion hash/sha256.test.js
Expand Up @@ -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')
}

/**
Expand Down
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -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",
Expand Down

0 comments on commit 5cdd1dc

Please sign in to comment.