|
| 1 | +import {byteArrayIntoHashObject} from "@chainsafe/as-sha256"; |
| 2 | +import { |
| 3 | + HashComputationLevel, |
| 4 | + HashObject, |
| 5 | + Hasher, |
| 6 | + Node, |
| 7 | + doDigestNLevel, |
| 8 | + doMerkleizeBlockArray, |
| 9 | + doMerkleizeBlocksBytes, |
| 10 | +} from "@chainsafe/persistent-merkle-tree"; |
| 11 | +import {digest2Bytes32, digest2Bytes32Into, hashInto} from "@lodestar/bun"; |
| 12 | + |
| 13 | +/** |
| 14 | + * Best SIMD implementation is in 512 bits = 64 bytes |
| 15 | + * If not, hashtree will make a loop inside |
| 16 | + * Given sha256 operates on a block of 4 bytes, we can hash 16 inputs at once |
| 17 | + * Each input is 64 bytes |
| 18 | + */ |
| 19 | +const PARALLEL_FACTOR = 16; |
| 20 | +const MAX_INPUT_SIZE = PARALLEL_FACTOR * 64; |
| 21 | +const uint8Input = new Uint8Array(MAX_INPUT_SIZE); |
| 22 | +const uint32Input = new Uint32Array(uint8Input.buffer); |
| 23 | +const uint8Output = new Uint8Array(PARALLEL_FACTOR * 32); |
| 24 | +// having this will cause more memory to extract uint32 |
| 25 | +// const uint32Output = new Uint32Array(uint8Output.buffer); |
| 26 | +// convenient reusable Uint8Array for hash64 |
| 27 | +const hash64Input = uint8Input.subarray(0, 64); |
| 28 | +const hash64Output = uint8Output.subarray(0, 32); |
| 29 | +// size input array to 2 HashObject per computation * 32 bytes per object |
| 30 | +const destNodes: Node[] = new Array<Node>(PARALLEL_FACTOR); |
| 31 | + |
| 32 | +export const hasher: Hasher = { |
| 33 | + name: "hashtree-bun", |
| 34 | + hashInto, |
| 35 | + digest64(obj1: Uint8Array, obj2: Uint8Array): Uint8Array { |
| 36 | + return digest2Bytes32(obj1, obj2); |
| 37 | + }, |
| 38 | + digest64Into: (obj1: Uint8Array, obj2: Uint8Array, output: Uint8Array): void => { |
| 39 | + digest2Bytes32Into(obj1, obj2, output); |
| 40 | + }, |
| 41 | + digest64HashObjects(left: HashObject, right: HashObject, parent: HashObject): void { |
| 42 | + hashObjectsToUint32Array(left, right, uint32Input); |
| 43 | + hashInto(hash64Input, hash64Output); |
| 44 | + byteArrayIntoHashObject(hash64Output, 0, parent); |
| 45 | + }, |
| 46 | + merkleizeBlocksBytes(blocksBytes: Uint8Array, padFor: number, output: Uint8Array, offset: number): void { |
| 47 | + doMerkleizeBlocksBytes(blocksBytes, padFor, output, offset, hashInto); |
| 48 | + }, |
| 49 | + merkleizeBlockArray(blocks, blockLimit, padFor, output, offset) { |
| 50 | + doMerkleizeBlockArray(blocks, blockLimit, padFor, output, offset, hashInto, uint8Input); |
| 51 | + }, |
| 52 | + digestNLevel(data: Uint8Array, nLevel: number): Uint8Array { |
| 53 | + return doDigestNLevel(data, nLevel, hashInto); |
| 54 | + }, |
| 55 | + executeHashComputations(hashComputations: HashComputationLevel[]): void { |
| 56 | + for (let level = hashComputations.length - 1; level >= 0; level--) { |
| 57 | + const hcArr = hashComputations[level]; |
| 58 | + if (!hcArr) { |
| 59 | + // should not happen |
| 60 | + throw Error(`no hash computations for level ${level}`); |
| 61 | + } |
| 62 | + |
| 63 | + if (hcArr.length === 0) { |
| 64 | + // nothing to hash |
| 65 | + continue; |
| 66 | + } |
| 67 | + |
| 68 | + // hash every 16 inputs at once to avoid memory allocation |
| 69 | + let i = 0; |
| 70 | + for (const {src0, src1, dest} of hcArr) { |
| 71 | + if (!src0 || !src1 || !dest) { |
| 72 | + throw new Error(`Invalid HashComputation at index ${i}`); |
| 73 | + } |
| 74 | + const indexInBatch = i % PARALLEL_FACTOR; |
| 75 | + const offset = indexInBatch * 16; |
| 76 | + |
| 77 | + hashObjectToUint32Array(src0, uint32Input, offset); |
| 78 | + hashObjectToUint32Array(src1, uint32Input, offset + 8); |
| 79 | + destNodes[indexInBatch] = dest; |
| 80 | + if (indexInBatch === PARALLEL_FACTOR - 1) { |
| 81 | + hashInto(uint8Input, uint8Output); |
| 82 | + for (const [j, destNode] of destNodes.entries()) { |
| 83 | + byteArrayIntoHashObject(uint8Output, j * 32, destNode); |
| 84 | + } |
| 85 | + } |
| 86 | + i++; |
| 87 | + } |
| 88 | + |
| 89 | + const remaining = hcArr.length % PARALLEL_FACTOR; |
| 90 | + // we prepared data in input, now hash the remaining |
| 91 | + if (remaining > 0) { |
| 92 | + const remainingInput = uint8Input.subarray(0, remaining * 64); |
| 93 | + const remainingOutput = uint8Output.subarray(0, remaining * 32); |
| 94 | + hashInto(remainingInput, remainingOutput); |
| 95 | + // destNodes was prepared above |
| 96 | + for (let j = 0; j < remaining; j++) { |
| 97 | + byteArrayIntoHashObject(remainingOutput, j * 32, destNodes[j]); |
| 98 | + } |
| 99 | + } |
| 100 | + } |
| 101 | + }, |
| 102 | +}; |
| 103 | + |
| 104 | +function hashObjectToUint32Array(obj: HashObject, arr: Uint32Array, offset: number): void { |
| 105 | + arr[offset] = obj.h0; |
| 106 | + arr[offset + 1] = obj.h1; |
| 107 | + arr[offset + 2] = obj.h2; |
| 108 | + arr[offset + 3] = obj.h3; |
| 109 | + arr[offset + 4] = obj.h4; |
| 110 | + arr[offset + 5] = obj.h5; |
| 111 | + arr[offset + 6] = obj.h6; |
| 112 | + arr[offset + 7] = obj.h7; |
| 113 | +} |
| 114 | + |
| 115 | +// note that uint32ArrayToHashObject will cause more memory |
| 116 | +function hashObjectsToUint32Array(obj1: HashObject, obj2: HashObject, arr: Uint32Array): void { |
| 117 | + arr[0] = obj1.h0; |
| 118 | + arr[1] = obj1.h1; |
| 119 | + arr[2] = obj1.h2; |
| 120 | + arr[3] = obj1.h3; |
| 121 | + arr[4] = obj1.h4; |
| 122 | + arr[5] = obj1.h5; |
| 123 | + arr[6] = obj1.h6; |
| 124 | + arr[7] = obj1.h7; |
| 125 | + arr[8] = obj2.h0; |
| 126 | + arr[9] = obj2.h1; |
| 127 | + arr[10] = obj2.h2; |
| 128 | + arr[11] = obj2.h3; |
| 129 | + arr[12] = obj2.h4; |
| 130 | + arr[13] = obj2.h5; |
| 131 | + arr[14] = obj2.h6; |
| 132 | + arr[15] = obj2.h7; |
| 133 | +} |
0 commit comments