kenqtade's picture
Use ken-q org namespace for project Spaces
0865492
Raw
History Blame Contribute Delete
43 kB
/**
* BLAKE3 Hash Function - Simple one-shot API
*
* This provides a simple hash() function optimized for different input sizes.
* For small inputs, uses pure JS. For large inputs, uses WASM SIMD.
*/
import { compress } from "./compress.js";
import {
IV,
CHUNK_START,
CHUNK_END,
PARENT,
ROOT,
BLOCK_LEN,
CHUNK_LEN,
OUT_LEN,
} from "./constants.js";
import {
IS_LITTLE_ENDIAN,
readLittleEndianWordsFull,
readLittleEndianWordsPartial,
writeLittleEndianBytesPartial,
} from "./utils.js";
import {
initSimdSync,
getSimdMemory,
getArenaBuffers,
runCompress4x,
runCompressChunks4x,
runCompressParent,
SIMD_MEMORY,
} from "./wasm-simd.js";
// Pre-allocated buffers for reuse (single-threaded optimization)
let blockWords: Uint32Array | null = null;
// ===== Contiguous Hyper CV Stack (Optimization #6) =====
// Maximum tree depth for practical inputs (2^64 chunks = essentially unlimited)
// Fixed allocation at module load - no runtime allocation
const CV_STACK_DEPTH = 64;
const HYPER_CV_STACK = new Uint32Array(CV_STACK_DEPTH * 8); // 64 CVs × 8 words = 512 words
// Pre-computed offsets for the first few stack levels (hot path optimization)
// Note: These can be used for further optimization if needed
// const CV_STACK_OFF_0 = 0;
// const CV_STACK_OFF_1 = 8;
// const CV_STACK_OFF_2 = 16;
// const CV_STACK_OFF_3 = 24;
// ===== Pre-allocated CV Pool with Views (avoids subarray() in hot paths) =====
const CV_POOL_SIZE = 64;
const CV_POOL = new Uint32Array(CV_POOL_SIZE * 8); // 64 CVs × 8 words = 512 words
const CV_VIEWS: Uint32Array[] = [];
for (let i = 0; i < CV_POOL_SIZE; i++) {
CV_VIEWS.push(CV_POOL.subarray(i * 8, i * 8 + 8));
}
// SIMD initialization state
let simdAvailable = false;
// Threshold for switching to SIMD (must be > 1 chunk to benefit from parallelism)
const SIMD_THRESHOLD = 4 * CHUNK_LEN; // 4KB - need at least 4 chunks for SIMD benefit
/**
* Initialize SIMD synchronously (lazy).
*/
function ensureSimdSync(): boolean {
if (simdAvailable) return true;
simdAvailable = initSimdSync();
return simdAvailable;
}
// Reusable buffer for SIMD chunk CVs (4 chunks × 8 words)
const simdChunkCvs = new Uint32Array(32);
// ===== Module-level reusable buffers (single-threaded safe) =====
// These eliminate heap allocations in hot paths
// For hashChunkWithWords() and hashChunkRoot()
const reusableTempCv = new Uint32Array(8);
// For hashPureJS()
const reusableChunkCv = new Uint32Array(8);
const reusablePureParentBlock = new Uint32Array(16);
const reusablePureParentCv = new Uint32Array(8);
// For hashSimd() - use flat array for 4 chunk CVs (access via subarray)
const reusableSimdCvs = new Uint32Array(32); // 4 × 8 words flat
// For hashSimd() parent compression
const reusableSimdParentBlock = new Uint32Array(16);
const reusableSimdParentCv = new Uint32Array(8);
// For hashSimd() parameters - TypedArrays instead of JS arrays
const reusableOffsets = new Uint32Array(4);
const reusableCounters = new Uint32Array(4);
const reusableBlockLens = new Uint32Array(4);
const reusableFlags = new Uint32Array(4);
// Reusable output buffer for common 32-byte hash (eliminates allocations)
const reusableOut8 = new Uint32Array(8); // Standard 32-byte output
// Pre-created view to avoid allocation in hot path (Task 1 optimization)
const reusableOut8View = new Uint8Array(reusableOut8.buffer, 0, 32);
// ===== Unrolled CV Copy Helper (Task 7 optimization) =====
// V8 will inline this - avoids loop overhead in hot paths
function copyCV8(src: Uint32Array, srcOff: number, dst: Uint32Array, dstOff: number): void {
dst[dstOff] = src[srcOff];
dst[dstOff + 1] = src[srcOff + 1];
dst[dstOff + 2] = src[srcOff + 2];
dst[dstOff + 3] = src[srcOff + 3];
dst[dstOff + 4] = src[srcOff + 4];
dst[dstOff + 5] = src[srcOff + 5];
dst[dstOff + 6] = src[srcOff + 6];
dst[dstOff + 7] = src[srcOff + 7];
}
/**
* Transpose 4 blocks (64 bytes each) into SIMD memory layout.
* The SIMD compress4x expects: [m0_0,m0_1,m0_2,m0_3, m1_0,m1_1,m1_2,m1_3, ...]
* where m{i}_{j} is message word i from block j.
*
* OPTIMIZED: Processes all 4 blocks together for each word position,
* writing 4 consecutive u32s at once for better cache locality.
*
* @param inputWords - Pre-created Uint32Array view of input (null if unaligned/non-LE).
* Created once per hash call to avoid allocation in hot loop.
*/
function transposeBlocksToSimd(
input: Uint8Array,
offsets: Uint32Array, // Starting offsets for each of 4 blocks
blockLens: Uint32Array, // Length of each block (0-64 bytes)
mem32: Uint32Array,
blockCount: number, // 1-4 blocks
inputWords: Uint32Array | null, // Pre-created view passed from caller
): void {
// Fast path: all blocks are full 64-byte blocks with aligned LE input
const allFull =
blockCount === 4 &&
blockLens[0] === 64 &&
blockLens[1] === 64 &&
blockLens[2] === 64 &&
blockLens[3] === 64;
if (
allFull &&
inputWords &&
offsets[0] % 4 === 0 &&
offsets[1] % 4 === 0 &&
offsets[2] % 4 === 0 &&
offsets[3] % 4 === 0
) {
// Ultra-fast path: process all 4 blocks together, write 4 consecutive u32s per word
const wordOff0 = offsets[0] >>> 2;
const wordOff1 = offsets[1] >>> 2;
const wordOff2 = offsets[2] >>> 2;
const wordOff3 = offsets[3] >>> 2;
for (let w = 0; w < 16; w++) {
const dstBase = w * 4;
mem32[dstBase] = inputWords[wordOff0 + w];
mem32[dstBase + 1] = inputWords[wordOff1 + w];
mem32[dstBase + 2] = inputWords[wordOff2 + w];
mem32[dstBase + 3] = inputWords[wordOff3 + w];
}
return;
}
// Standard path: process each block independently (handles partial blocks)
for (let b = 0; b < blockCount; b++) {
const len = blockLens[b];
const off = offsets[b];
if (len === 64) {
// Full block
if (inputWords && off % 4 === 0) {
// Direct Uint32Array access for aligned LE blocks
const wordOff = off >>> 2;
for (let w = 0; w < 16; w++) {
mem32[w * 4 + b] = inputWords[wordOff + w];
}
} else {
// Byte-by-byte reconstruction
for (let w = 0; w < 16; w++) {
const srcOff = off + w * 4;
mem32[w * 4 + b] =
input[srcOff] |
(input[srcOff + 1] << 8) |
(input[srcOff + 2] << 16) |
(input[srcOff + 3] << 24);
}
}
} else if (len === 0) {
// Zero block
for (let w = 0; w < 16; w++) {
mem32[w * 4 + b] = 0;
}
} else {
// Partial block - handle word by word
for (let w = 0; w < 16; w++) {
const wordOff = w * 4;
if (wordOff >= len) {
mem32[w * 4 + b] = 0;
} else if (wordOff + 4 <= len) {
const srcOff = off + wordOff;
mem32[w * 4 + b] =
input[srcOff] |
(input[srcOff + 1] << 8) |
(input[srcOff + 2] << 16) |
(input[srcOff + 3] << 24);
} else {
// Partial word at end of block
let word = 0;
for (let i = 0; i < len - wordOff; i++) {
word |= input[off + wordOff + i] << (i * 8);
}
mem32[w * 4 + b] = word;
}
}
}
}
// Zero unused block slots
for (let b = blockCount; b < 4; b++) {
for (let w = 0; w < 16; w++) {
mem32[w * 4 + b] = 0;
}
}
}
/**
* Transpose 4 full chunks (4 × 16 blocks = 64 blocks) into batch SIMD memory.
* This is used for the batched compressChunks4x function that processes
* all 16 blocks in a single WASM call.
*
* Memory layout: BATCH_BLOCK_WORDS has 16 positions, each with 16 v128 values.
* Position p, word w: mem32[(p * 64) + (w * 4) + lane]
*
* OPTIMIZED: Processes all 4 chunks together for each (pos, word) pair,
* writing 4 consecutive u32s at once for better cache locality.
*
* @param input - Input data (must have at least 4 full chunks = 4096 bytes)
* @param chunkOffsets - Starting offsets for each of 4 chunks
* @param mem32 - WASM memory view
* @param inputWords - Pre-created Uint32Array view (null if unaligned)
*/
function transposeBatchToSimd(
input: Uint8Array,
chunkOffsets: Uint32Array,
mem32: Uint32Array,
inputWords: Uint32Array | null,
): void {
const BATCH_BASE = SIMD_MEMORY.BATCH_BLOCK_WORDS / 4;
// Get base word offsets for each chunk (pre-computed for fast path)
const chunk0WordBase = chunkOffsets[0] >>> 2;
const chunk1WordBase = chunkOffsets[1] >>> 2;
const chunk2WordBase = chunkOffsets[2] >>> 2;
const chunk3WordBase = chunkOffsets[3] >>> 2;
// Fast path: all chunks aligned and LE - process 4 consecutive u32s at once
if (inputWords && chunkOffsets[0] % 4 === 0) {
for (let pos = 0; pos < 16; pos++) {
const posBase = BATCH_BASE + pos * 64; // 16 words × 4 lanes = 64
const blockWordOff = pos * 16; // 16 words per block (64 bytes / 4)
// Process all 16 words, writing 4 chunks at a time (cache-friendly: 16 bytes per write group)
for (let w = 0; w < 16; w++) {
const dstBase = posBase + w * 4;
// Read word w from all 4 chunks at positions that become consecutive in output
mem32[dstBase] = inputWords[chunk0WordBase + blockWordOff + w];
mem32[dstBase + 1] = inputWords[chunk1WordBase + blockWordOff + w];
mem32[dstBase + 2] = inputWords[chunk2WordBase + blockWordOff + w];
mem32[dstBase + 3] = inputWords[chunk3WordBase + blockWordOff + w];
}
}
} else {
// Slow path: byte-by-byte reconstruction, still cache-friendly write pattern
for (let pos = 0; pos < 16; pos++) {
const posBase = BATCH_BASE + pos * 64;
const blockByteOff = pos * 64; // 64 bytes per block
for (let w = 0; w < 16; w++) {
const dstBase = posBase + w * 4;
const wordByteOff = w * 4;
// Chunk 0
const off0 = chunkOffsets[0] + blockByteOff + wordByteOff;
mem32[dstBase] =
input[off0] | (input[off0 + 1] << 8) | (input[off0 + 2] << 16) | (input[off0 + 3] << 24);
// Chunk 1
const off1 = chunkOffsets[1] + blockByteOff + wordByteOff;
mem32[dstBase + 1] =
input[off1] | (input[off1 + 1] << 8) | (input[off1 + 2] << 16) | (input[off1 + 3] << 24);
// Chunk 2
const off2 = chunkOffsets[2] + blockByteOff + wordByteOff;
mem32[dstBase + 2] =
input[off2] | (input[off2 + 1] << 8) | (input[off2 + 2] << 16) | (input[off2 + 3] << 24);
// Chunk 3
const off3 = chunkOffsets[3] + blockByteOff + wordByteOff;
mem32[dstBase + 3] =
input[off3] | (input[off3 + 1] << 8) | (input[off3 + 2] << 16) | (input[off3 + 3] << 24);
}
}
}
}
// Pre-computed memory offsets for SIMD operations (single-block mode)
const SIMD_CV_BASE = SIMD_MEMORY.CHAINING_VALUES / 4;
const SIMD_OUT_BASE = SIMD_MEMORY.OUTPUT / 4;
const SIMD_COUNTER_LOW_BASE = SIMD_MEMORY.COUNTER_LOW / 4;
const SIMD_COUNTER_HIGH_BASE = SIMD_MEMORY.COUNTER_HIGH / 4;
const SIMD_BLOCK_LEN_BASE = SIMD_MEMORY.BLOCK_LEN / 4;
// Pre-computed memory offsets for batch SIMD operations (16-block mode)
const BATCH_CV_BASE = SIMD_MEMORY.BATCH_CV / 4;
const BATCH_COUNTER_LOW_BASE = SIMD_MEMORY.BATCH_COUNTER_LOW / 4;
const BATCH_FLAGS_BASE_OFFSET = SIMD_MEMORY.BATCH_FLAGS_BASE / 4;
const BATCH_OUTPUT_BASE = SIMD_MEMORY.BATCH_OUTPUT / 4;
// Reusable arrays for batch processing
const batchChunkOffsets = new Uint32Array(4);
const SIMD_FLAGS_BASE = SIMD_MEMORY.FLAGS / 4;
/**
* Set up chaining values in SIMD memory (transposed layout).
* Optimized: unrolled loops for common case of 4 chunks.
* cvs is flat: [cv0_word0..cv0_word7, cv1_word0..cv1_word7, ...]
*/
function setupSimdCvs(
cvs: Uint32Array, // Flat array: 4 × 8 words
mem32: Uint32Array,
count: number,
): void {
// Unrolled for 4 chunks (common case)
if (count === 4) {
for (let w = 0; w < 8; w++) {
const base = SIMD_CV_BASE + w * 4;
mem32[base] = cvs[w]; // cv0[w]
mem32[base + 1] = cvs[8 + w]; // cv1[w]
mem32[base + 2] = cvs[16 + w]; // cv2[w]
mem32[base + 3] = cvs[24 + w]; // cv3[w]
}
} else {
for (let w = 0; w < 8; w++) {
const base = SIMD_CV_BASE + w * 4;
for (let c = 0; c < count; c++) {
mem32[base + c] = cvs[c * 8 + w];
}
for (let c = count; c < 4; c++) {
mem32[base + c] = 0;
}
}
}
}
/**
* Set up SIMD parameters (counters, flags, block lengths).
*/
function setupSimdParams(
mem32: Uint32Array,
counters: Uint32Array,
blockLens: Uint32Array,
flagsArr: Uint32Array,
count: number,
): void {
// Most chunk counters fit in 32 bits, so counter high is usually 0
for (let i = 0; i < count; i++) {
mem32[SIMD_COUNTER_LOW_BASE + i] = counters[i];
mem32[SIMD_COUNTER_HIGH_BASE + i] = 0; // Assume counters fit in 32 bits
mem32[SIMD_BLOCK_LEN_BASE + i] = blockLens[i];
mem32[SIMD_FLAGS_BASE + i] = flagsArr[i];
}
// Zero unused slots
for (let i = count; i < 4; i++) {
mem32[SIMD_COUNTER_LOW_BASE + i] = 0;
mem32[SIMD_COUNTER_HIGH_BASE + i] = 0;
mem32[SIMD_BLOCK_LEN_BASE + i] = 0;
mem32[SIMD_FLAGS_BASE + i] = 0;
}
}
/**
* Read output CVs from SIMD memory (untranspose).
*/
function readSimdOutputCvs(
mem32: Uint32Array,
outputCvs: Uint32Array, // Flat array: 4 × 8 words
count: number,
): void {
// Unrolled for 4 chunks (common case)
if (count === 4) {
for (let w = 0; w < 8; w++) {
const base = SIMD_OUT_BASE + w * 4;
outputCvs[w] = mem32[base];
outputCvs[8 + w] = mem32[base + 1];
outputCvs[16 + w] = mem32[base + 2];
outputCvs[24 + w] = mem32[base + 3];
}
} else {
for (let w = 0; w < 8; w++) {
const base = SIMD_OUT_BASE + w * 4;
for (let c = 0; c < count; c++) {
outputCvs[c * 8 + w] = mem32[base + c];
}
}
}
}
function getBlockWords(): Uint32Array {
if (!blockWords) {
blockWords = new Uint32Array(16);
}
return blockWords;
}
/**
* Hash a single chunk (up to 1024 bytes) with pre-created inputWords view.
* This is the optimized version that avoids creating Uint32Array views per chunk.
* (Fleek optimization Step 8)
*/
function hashChunkWithWords(
input: Uint8Array,
inputWords: Uint32Array | null, // Pre-created view of entire input
inputOffset: number,
inputLen: number,
chunkCounter: number,
flags: number,
cv: Uint32Array,
cvOffset: number,
): void {
// Use reusable temporary CV for intermediate blocks (single-threaded safe)
reusableTempCv.set(IV);
// Process full blocks
const fullBlocks = inputLen >>> 6; // inputLen / 64
const remainder = inputLen & 63; // inputLen % 64
// Calculate word offset for this chunk within the pre-created view
const chunkWordOffset = inputOffset >>> 2;
// Fast path for full chunks with aligned little-endian input
if (inputWords && remainder === 0 && inputLen === CHUNK_LEN) {
// All 16 blocks are full, use fast path exclusively
let wordOff = chunkWordOffset;
// Block 0 (CHUNK_START)
compress(
reusableTempCv,
0,
inputWords,
wordOff,
reusableTempCv,
0,
false,
chunkCounter,
BLOCK_LEN,
flags | CHUNK_START,
);
wordOff += 16;
// Blocks 1-14 (no special flags)
for (let i = 1; i < 15; i++) {
compress(
reusableTempCv,
0,
inputWords,
wordOff,
reusableTempCv,
0,
false,
chunkCounter,
BLOCK_LEN,
flags,
);
wordOff += 16;
}
// Block 15 (CHUNK_END)
compress(
reusableTempCv,
0,
inputWords,
wordOff,
reusableTempCv,
0,
false,
chunkCounter,
BLOCK_LEN,
flags | CHUNK_END,
);
cv.set(reusableTempCv, cvOffset);
return;
}
// Slower path for partial chunks or non-aligned input
const totalBlocks = fullBlocks + (remainder > 0 ? 1 : 0);
const block = getBlockWords();
for (let blockIdx = 0; blockIdx < totalBlocks; blockIdx++) {
const isFirst = blockIdx === 0;
const isLast = blockIdx === totalBlocks - 1;
const blockStart = blockIdx << 6;
const blockLen = isLast && remainder > 0 ? remainder : BLOCK_LEN;
// Determine flags for this block
let blockFlags = flags;
if (isFirst) blockFlags |= CHUNK_START;
if (isLast) blockFlags |= CHUNK_END;
// Load block words
if (isLast && remainder > 0) {
// Partial final block - need zero padding
readLittleEndianWordsPartial(input, inputOffset + blockStart, blockLen, block);
} else if (inputWords && chunkWordOffset + (blockStart >>> 2) + 16 <= inputWords.length) {
// Fast path: use pre-created view directly
compress(
reusableTempCv,
0,
inputWords,
chunkWordOffset + (blockStart >>> 2),
reusableTempCv,
0,
false,
chunkCounter,
blockLen,
blockFlags,
);
continue;
} else {
readLittleEndianWordsFull(input, inputOffset + blockStart, block);
}
compress(
reusableTempCv,
0,
block,
0,
reusableTempCv,
0,
false,
chunkCounter,
blockLen,
blockFlags,
);
}
// Copy result to output
cv.set(reusableTempCv, cvOffset);
}
/**
* Hash input using pure JavaScript.
* Handles the full Merkle tree construction.
*/
function hashPureJS(input: Uint8Array, outputLen: number): Uint8Array {
const inputLen = input.length;
// Special case: empty input
if (inputLen === 0) {
const block = getBlockWords();
block.fill(0);
// Use reusable output buffer for common 32-byte case
const out = outputLen === 32 ? reusableOut8 : new Uint32Array(outputLen > 32 ? 16 : 8);
compress(IV, 0, block, 0, out, 0, outputLen > 32, 0, 0, CHUNK_START | CHUNK_END | ROOT);
// Return result - use pre-created view for common 32-byte case
if (outputLen === 32 && IS_LITTLE_ENDIAN) {
return reusableOut8View.slice();
}
const result = new Uint8Array(outputLen);
if (IS_LITTLE_ENDIAN) {
result.set(new Uint8Array(out.buffer, 0, outputLen));
} else {
writeLittleEndianBytesPartial(out, 0, result, 0, outputLen);
}
return result;
}
// Calculate number of chunks
const numChunks = Math.ceil(inputLen / CHUNK_LEN);
// Single chunk optimization
if (numChunks === 1) {
// Use reusable output buffer for common 32-byte case
const cv = outputLen === 32 ? reusableOut8 : new Uint32Array(outputLen > 32 ? 16 : 8);
hashChunkRoot(input, 0, inputLen, 0, 0, cv, outputLen > 32);
// Return result - use pre-created view for common 32-byte case
if (outputLen === 32 && IS_LITTLE_ENDIAN) {
return reusableOut8View.slice();
}
const result = new Uint8Array(outputLen);
if (IS_LITTLE_ENDIAN) {
result.set(new Uint8Array(cv.buffer, 0, outputLen));
} else {
writeLittleEndianBytesPartial(cv, 0, result, 0, outputLen);
}
return result;
}
// Multiple chunks - need Merkle tree
// Use the global contiguous CV stack (no allocation)
const stack = HYPER_CV_STACK;
let stackLen = 0;
// Use reusable buffers (single-threaded safe)
const chunkCv = reusableChunkCv;
const parentBlock = reusablePureParentBlock;
const parentCv = reusablePureParentCv;
// Create Uint32Array view ONCE for entire input (Fleek optimization Step 8)
// This avoids creating views inside each chunk/block processing
let inputWords: Uint32Array | null = null;
const canUseFastPath = IS_LITTLE_ENDIAN && input.byteOffset % 4 === 0;
if (canUseFastPath) {
inputWords = new Uint32Array(input.buffer, input.byteOffset, inputLen >>> 2);
}
// Determine how many full chunks we have
const fullChunks = inputLen >>> 10; // inputLen / 1024
const lastChunkLen = inputLen & 1023; // inputLen % 1024
// Process all full chunks with fast path (inlined for performance)
if (canUseFastPath && inputWords) {
for (let chunkIdx = 0; chunkIdx < fullChunks; chunkIdx++) {
// Inline chunk processing for full chunks
chunkCv.set(IV);
let wordOff = chunkIdx << 8; // chunkIdx * 256 (CHUNK_LEN/4)
// Block 0 (CHUNK_START)
compress(
chunkCv,
0,
inputWords,
wordOff,
chunkCv,
0,
false,
chunkIdx,
BLOCK_LEN,
CHUNK_START,
);
wordOff += 16;
// Blocks 1-14 (no special flags)
for (let b = 1; b < 15; b++) {
compress(chunkCv, 0, inputWords, wordOff, chunkCv, 0, false, chunkIdx, BLOCK_LEN, 0);
wordOff += 16;
}
// Block 15 (CHUNK_END)
compress(chunkCv, 0, inputWords, wordOff, chunkCv, 0, false, chunkIdx, BLOCK_LEN, CHUNK_END);
// Merge completed subtrees (avoid subarray by using index math)
let totalChunks = chunkIdx + 1;
let cvSrcOff = 0;
let cvSrc = chunkCv;
// Check if this is the last chunk overall
const isLastChunk = chunkIdx === fullChunks - 1 && lastChunkLen === 0;
while ((totalChunks & 1) === 0 && stackLen > 0) {
// Skip final merge if it would produce the root; let finalization handle it with ROOT flag
if (stackLen === 1 && isLastChunk) {
break;
}
stackLen--;
const stackOff = stackLen * 8;
// Copy left CV from stack to parentBlock[0..7] (unrolled)
copyCV8(stack, stackOff, parentBlock, 0);
// Copy current CV to parentBlock[8..15] (unrolled)
copyCV8(cvSrc, cvSrcOff, parentBlock, 8);
compress(IV, 0, parentBlock, 0, parentCv, 0, false, 0, BLOCK_LEN, PARENT);
cvSrc = parentCv;
cvSrcOff = 0;
totalChunks >>>= 1;
}
// Push CV to stack (unrolled)
const stackOff = stackLen * 8;
copyCV8(cvSrc, cvSrcOff, stack, stackOff);
stackLen++;
}
// Process last partial chunk if any
if (lastChunkLen > 0) {
hashChunkWithWords(
input,
inputWords,
fullChunks * CHUNK_LEN,
lastChunkLen,
fullChunks,
0,
chunkCv,
0,
);
let totalChunks = fullChunks + 1;
let newCv = chunkCv;
let newCvOffset = 0;
while ((totalChunks & 1) === 0 && stackLen > 0) {
// Skip final merge; this IS the last chunk, let finalization handle ROOT flag
if (stackLen === 1) {
break;
}
stackLen--;
const stackOff = stackLen * 8;
// Copy from stack to parentBlock[0..7] (unrolled)
copyCV8(stack, stackOff, parentBlock, 0);
// Copy from newCv to parentBlock[8..15] (unrolled)
copyCV8(newCv, newCvOffset, parentBlock, 8);
compress(IV, 0, parentBlock, 0, parentCv, 0, false, 0, BLOCK_LEN, PARENT);
newCv = parentCv;
newCvOffset = 0;
totalChunks >>>= 1;
}
// Push CV to stack (unrolled)
const pushOff = stackLen * 8;
copyCV8(newCv, newCvOffset, stack, pushOff);
stackLen++;
}
} else {
// Slow path for unaligned or big-endian
for (let chunkIdx = 0; chunkIdx < numChunks; chunkIdx++) {
const chunkStart = chunkIdx * CHUNK_LEN;
const chunkLen = Math.min(CHUNK_LEN, inputLen - chunkStart);
hashChunkWithWords(input, inputWords, chunkStart, chunkLen, chunkIdx, 0, chunkCv, 0);
// Merge completed subtrees
let totalChunks = chunkIdx + 1;
let newCv = chunkCv;
let newCvOffset = 0;
// Check if this is the last chunk
const isLastChunk = chunkIdx === numChunks - 1;
while ((totalChunks & 1) === 0 && stackLen > 0) {
// Skip final merge if it would produce the root; let finalization handle it with ROOT flag
if (stackLen === 1 && isLastChunk) {
break;
}
stackLen--;
const stackOff = stackLen * 8;
// Copy from stack to parentBlock[0..7] (unrolled)
copyCV8(stack, stackOff, parentBlock, 0);
// Copy from newCv to parentBlock[8..15] (unrolled)
copyCV8(newCv, newCvOffset, parentBlock, 8);
compress(IV, 0, parentBlock, 0, parentCv, 0, false, 0, BLOCK_LEN, PARENT);
newCv = parentCv;
newCvOffset = 0;
totalChunks >>>= 1;
}
// Push CV to stack (unrolled)
const pushOff = stackLen * 8;
copyCV8(newCv, newCvOffset, stack, pushOff);
stackLen++;
}
}
// Finalize: merge remaining stack entries
while (stackLen > 1) {
stackLen--;
const rightOff = stackLen * 8;
stackLen--;
const leftOff = stackLen * 8;
// Copy left CV to parentBlock[0..7] and right CV to parentBlock[8..15] (unrolled)
copyCV8(stack, leftOff, parentBlock, 0);
copyCV8(stack, rightOff, parentBlock, 8);
if (stackLen === 0) {
// This is the root - use reusable output buffer for common 32-byte case
const out = outputLen === 32 ? reusableOut8 : new Uint32Array(outputLen > 32 ? 16 : 8);
compress(IV, 0, parentBlock, 0, out, 0, outputLen > 32, 0, BLOCK_LEN, PARENT | ROOT);
// Return result - use pre-created view for common 32-byte case
if (outputLen === 32 && IS_LITTLE_ENDIAN) {
return reusableOut8View.slice();
}
const result = new Uint8Array(outputLen);
if (IS_LITTLE_ENDIAN) {
result.set(new Uint8Array(out.buffer, 0, outputLen));
} else {
writeLittleEndianBytesPartial(out, 0, result, 0, outputLen);
}
return result;
}
compress(IV, 0, parentBlock, 0, parentCv, 0, false, 0, BLOCK_LEN, PARENT);
// Push to stack (unrolled)
copyCV8(parentCv, 0, stack, stackLen * 8);
stackLen++;
}
// Single entry in stack - this is the root
const out = outputLen === 32 ? reusableOut8 : new Uint32Array(outputLen > 32 ? 16 : 8);
const lastBlock = getBlockWords();
lastBlock.fill(0);
// Copy first 8 words from stack (unrolled)
copyCV8(stack, 0, lastBlock, 0);
compress(IV, 0, lastBlock, 0, out, 0, outputLen > 32, 0, BLOCK_LEN, ROOT);
// Return result - use pre-created view for common 32-byte case
if (outputLen === 32 && IS_LITTLE_ENDIAN) {
return reusableOut8View.slice();
}
const result = new Uint8Array(outputLen);
if (IS_LITTLE_ENDIAN) {
result.set(new Uint8Array(out.buffer, 0, outputLen));
} else {
writeLittleEndianBytesPartial(out, 0, result, 0, outputLen);
}
return result;
}
/**
* Hash a single chunk that is also the root (single chunk input).
*/
function hashChunkRoot(
input: Uint8Array,
inputOffset: number,
inputLen: number,
chunkCounter: number,
flags: number,
out: Uint32Array,
fullOutput: boolean,
): void {
// Use reusable tempCv (single-threaded safe)
reusableTempCv.set(IV);
const block = getBlockWords();
// Process full blocks
const fullBlocks = inputLen >>> 6;
const remainder = inputLen & 63;
const totalBlocks = fullBlocks + (remainder > 0 ? 1 : 0) || 1; // At least 1 block
// Create a Uint32Array view if possible
let inputWords: Uint32Array | null = null;
if (IS_LITTLE_ENDIAN && (input.byteOffset + inputOffset) % 4 === 0 && inputLen >= 4) {
inputWords = new Uint32Array(input.buffer, input.byteOffset + inputOffset, inputLen >>> 2);
}
for (let blockIdx = 0; blockIdx < totalBlocks; blockIdx++) {
const isFirst = blockIdx === 0;
const isLast = blockIdx === totalBlocks - 1;
const blockStart = blockIdx << 6;
const blockLen = isLast ? remainder || (inputLen > 0 ? BLOCK_LEN : 0) : BLOCK_LEN;
// Determine flags
let blockFlags = flags;
if (isFirst) blockFlags |= CHUNK_START;
if (isLast) blockFlags |= CHUNK_END | ROOT;
// Load block
if (isLast && remainder > 0) {
readLittleEndianWordsPartial(input, inputOffset + blockStart, blockLen, block);
} else if (inputLen === 0) {
block.fill(0);
} else if (inputWords && (blockStart >>> 2) + 16 <= inputWords.length) {
// Fast path
compress(
reusableTempCv,
0,
inputWords,
blockStart >>> 2,
isLast ? out : reusableTempCv,
0,
isLast && fullOutput,
chunkCounter,
blockLen,
blockFlags,
);
continue;
} else {
readLittleEndianWordsFull(input, inputOffset + blockStart, block);
}
compress(
reusableTempCv,
0,
block,
0,
isLast ? out : reusableTempCv,
0,
isLast && fullOutput,
chunkCounter,
blockLen,
blockFlags,
);
}
}
/**
* Hash using WASM SIMD - processes 4 chunks in parallel.
* Falls back to pure JS if SIMD fails.
*/
function hashSimd(input: Uint8Array, outputLen: number): Uint8Array {
const mem = getSimdMemory();
if (!mem) {
return hashPureJS(input, outputLen);
}
const { view32 } = mem;
const inputLen = input.length;
const numChunks = Math.ceil(inputLen / CHUNK_LEN);
// For small inputs, pure JS is faster (no transpose overhead)
if (numChunks < 4) {
return hashPureJS(input, outputLen);
}
// Try to use WASM arena buffers (zero JS heap allocation)
// Falls back to JS buffers if arena not available
const arena = getArenaBuffers();
const useWasmParent = arena !== null; // Use WASM parent compress when arena available
let stack: Uint32Array;
let tempCvs: Uint32Array;
let parentBlock: Uint32Array;
let parentCv: Uint32Array;
if (arena) {
// Use WASM-backed arena buffers
stack = arena.cvStack;
tempCvs = arena.tempCvs;
parentBlock = arena.parentBlock;
parentCv = arena.chunkCv;
} else {
// Fallback to JS heap buffers - use global contiguous stack (no allocation)
stack = HYPER_CV_STACK;
tempCvs = reusableSimdCvs;
parentBlock = reusableSimdParentBlock;
parentCv = reusableSimdParentCv;
}
let stackLen = 0;
// Use TypedArrays instead of JS arrays for block parameters
const offsets = reusableOffsets;
const counters = reusableCounters;
const blockLens = reusableBlockLens;
const flagsArr = reusableFlags;
// Create Uint32Array view once for entire hash call (optimization: avoid allocation in hot loop)
const inputWords =
IS_LITTLE_ENDIAN && input.byteOffset % 4 === 0
? new Uint32Array(input.buffer, input.byteOffset, input.byteLength >>> 2)
: null;
// Calculate number of full chunks (1024 bytes each)
const numFullChunks = inputLen >>> 10; // inputLen / 1024
// Process chunks in groups of 4
let chunkIdx = 0;
while (chunkIdx < numChunks) {
const groupSize = Math.min(4, numChunks - chunkIdx);
// === BATCH FAST PATH: 4 full chunks ===
// Use compressChunks4x for groups of exactly 4 full chunks
// This reduces 16 WASM calls to 1 per group
const canUseBatchPath = groupSize === 4 && chunkIdx + 4 <= numFullChunks;
if (canUseBatchPath) {
// Set up chunk offsets for batch transpose
batchChunkOffsets[0] = chunkIdx * CHUNK_LEN;
batchChunkOffsets[1] = (chunkIdx + 1) * CHUNK_LEN;
batchChunkOffsets[2] = (chunkIdx + 2) * CHUNK_LEN;
batchChunkOffsets[3] = (chunkIdx + 3) * CHUNK_LEN;
// Transpose all 64 blocks (4 chunks × 16 blocks) at once
transposeBatchToSimd(input, batchChunkOffsets, view32, inputWords);
// Set up initial CVs (IV) in batch memory - transposed layout
for (let w = 0; w < 8; w++) {
const ivWord = IV[w];
const base = BATCH_CV_BASE + w * 4;
view32[base] = ivWord;
view32[base + 1] = ivWord;
view32[base + 2] = ivWord;
view32[base + 3] = ivWord;
}
// Set up counters in batch memory
view32[BATCH_COUNTER_LOW_BASE] = chunkIdx;
view32[BATCH_COUNTER_LOW_BASE + 1] = chunkIdx + 1;
view32[BATCH_COUNTER_LOW_BASE + 2] = chunkIdx + 2;
view32[BATCH_COUNTER_LOW_BASE + 3] = chunkIdx + 3;
// Set up base flags (0 - no keyed hashing)
view32[BATCH_FLAGS_BASE_OFFSET] = 0;
view32[BATCH_FLAGS_BASE_OFFSET + 1] = 0;
view32[BATCH_FLAGS_BASE_OFFSET + 2] = 0;
view32[BATCH_FLAGS_BASE_OFFSET + 3] = 0;
// Run batched compress (16 blocks × 4 chunks in one call!)
runCompressChunks4x();
// Read output CVs from batch output - untranspose to tempCvs
for (let w = 0; w < 8; w++) {
const base = BATCH_OUTPUT_BASE + w * 4;
tempCvs[w] = view32[base]; // chunk 0
tempCvs[8 + w] = view32[base + 1]; // chunk 1
tempCvs[16 + w] = view32[base + 2]; // chunk 2
tempCvs[24 + w] = view32[base + 3]; // chunk 3
}
} else {
// === STANDARD PATH: block-by-block processing ===
// Used for partial chunks or groups < 4
// Initialize CVs for this group to IV (flat array: 4 × 8 words)
for (let g = 0; g < groupSize; g++) {
const base = g * 8;
tempCvs[base] = IV[0];
tempCvs[base + 1] = IV[1];
tempCvs[base + 2] = IV[2];
tempCvs[base + 3] = IV[3];
tempCvs[base + 4] = IV[4];
tempCvs[base + 5] = IV[5];
tempCvs[base + 6] = IV[6];
tempCvs[base + 7] = IV[7];
}
// Process all 16 blocks of each chunk in this group
for (let blockIdx = 0; blockIdx < 16; blockIdx++) {
// Calculate block offsets and parameters (reuse arrays)
for (let g = 0; g < groupSize; g++) {
const thisChunkIdx = chunkIdx + g;
const chunkStart = thisChunkIdx * CHUNK_LEN;
const chunkLen = Math.min(CHUNK_LEN, inputLen - chunkStart);
const thisBlockStart = chunkStart + blockIdx * BLOCK_LEN;
// Determine block length for this specific block
const blockStartInChunk = blockIdx * BLOCK_LEN;
let thisBlockLen = BLOCK_LEN;
if (blockStartInChunk >= chunkLen) {
thisBlockLen = 0;
} else if (blockStartInChunk + BLOCK_LEN > chunkLen) {
thisBlockLen = chunkLen - blockStartInChunk;
}
offsets[g] = thisBlockStart;
counters[g] = thisChunkIdx;
// Determine flags
let flags = 0;
if (blockIdx === 0) flags |= CHUNK_START;
const totalBlocksInChunk = Math.ceil(chunkLen / BLOCK_LEN) || 1;
if (blockIdx === totalBlocksInChunk - 1) flags |= CHUNK_END;
blockLens[g] = thisBlockLen;
flagsArr[g] = flags;
}
// Check if any blocks need processing
if (blockLens[0] === 0 && blockLens[1] === 0 && blockLens[2] === 0 && blockLens[3] === 0)
continue;
// Transpose blocks into SIMD memory (pass pre-created view to avoid allocation)
transposeBlocksToSimd(input, offsets, blockLens, view32, groupSize, inputWords);
// Set up CVs in SIMD memory
setupSimdCvs(tempCvs, view32, groupSize);
// Set up parameters
setupSimdParams(view32, counters, blockLens, flagsArr, groupSize);
// Run SIMD compress
runCompress4x();
// Read output CVs back
readSimdOutputCvs(view32, simdChunkCvs, groupSize);
// Update tempCvs - copy from simdChunkCvs (both are flat 32-word arrays)
// simdChunkCvs layout matches tempCvs: [cv0_w0..cv0_w7, cv1_w0..cv1_w7, ...]
// IMPORTANT: Only update CVs for chunks that had data in this block!
// Skipping this check would corrupt CVs for partial chunks after their final block.
for (let g = 0; g < groupSize; g++) {
if (blockLens[g] === 0) continue; // Don't update CV for chunks with no data in this block
const base = g * 8;
tempCvs[base] = simdChunkCvs[base];
tempCvs[base + 1] = simdChunkCvs[base + 1];
tempCvs[base + 2] = simdChunkCvs[base + 2];
tempCvs[base + 3] = simdChunkCvs[base + 3];
tempCvs[base + 4] = simdChunkCvs[base + 4];
tempCvs[base + 5] = simdChunkCvs[base + 5];
tempCvs[base + 6] = simdChunkCvs[base + 6];
tempCvs[base + 7] = simdChunkCvs[base + 7];
}
}
}
// Merge each chunk's CV into the Merkle tree
for (let g = 0; g < groupSize; g++) {
const thisChunkIdx = chunkIdx + g;
// Merge completed subtrees
let totalChunks = thisChunkIdx + 1;
// Track newCv source - either from tempCvs or parentCv
let newCvBase = g * 8; // Offset into tempCvs
let newCvSrc = tempCvs;
// Check if this is the last chunk
const isLastChunk = thisChunkIdx === numChunks - 1;
while ((totalChunks & 1) === 0 && stackLen > 0) {
// Skip final merge if it would produce the root; let finalization handle it with ROOT flag
if (stackLen === 1 && isLastChunk) {
break;
}
// Pop left child
stackLen--;
const stackOff = stackLen * 8;
// Copy from stack to parentBlock[0..7] (unrolled)
copyCV8(stack, stackOff, parentBlock, 0);
// Copy from newCv source to parentBlock[8..15] (unrolled)
copyCV8(newCvSrc, newCvBase, parentBlock, 8);
if (useWasmParent) {
// WASM parent compress - data already in arena buffers
runCompressParent();
} else {
compress(IV, 0, parentBlock, 0, parentCv, 0, false, 0, BLOCK_LEN, PARENT);
}
newCvSrc = parentCv;
newCvBase = 0;
totalChunks >>>= 1;
}
// Push to stack (unrolled)
const pushOff = stackLen * 8;
copyCV8(newCvSrc, newCvBase, stack, pushOff);
stackLen++;
}
chunkIdx += groupSize;
}
// Finalize: merge remaining stack entries
while (stackLen > 1) {
stackLen--;
const rightOff = stackLen * 8;
stackLen--;
const leftOff = stackLen * 8;
// Copy left CV to parentBlock[0..7] and right CV to parentBlock[8..15] (unrolled)
copyCV8(stack, leftOff, parentBlock, 0);
copyCV8(stack, rightOff, parentBlock, 8);
if (stackLen === 0) {
// This is the root - use reusable output buffer
const out = outputLen === 32 ? reusableOut8 : new Uint32Array(outputLen > 32 ? 16 : 8);
compress(IV, 0, parentBlock, 0, out, 0, outputLen > 32, 0, BLOCK_LEN, PARENT | ROOT);
// Return result - use pre-created view for common 32-byte case
if (outputLen === 32 && IS_LITTLE_ENDIAN) {
return reusableOut8View.slice();
}
const result = new Uint8Array(outputLen);
if (IS_LITTLE_ENDIAN) {
result.set(new Uint8Array(out.buffer, 0, outputLen));
} else {
writeLittleEndianBytesPartial(out, 0, result, 0, outputLen);
}
return result;
}
if (useWasmParent) {
// WASM parent compress - data already in arena buffers
runCompressParent();
} else {
compress(IV, 0, parentBlock, 0, parentCv, 0, false, 0, BLOCK_LEN, PARENT);
}
// Push to stack (unrolled)
copyCV8(parentCv, 0, stack, stackLen * 8);
stackLen++;
}
// Single entry in stack - finalize as root
if (stackLen === 1) {
const block = getBlockWords();
block.fill(0);
// Copy first 8 words from stack (unrolled)
copyCV8(stack, 0, block, 0);
// Use reusable output buffer
const out = outputLen === 32 ? reusableOut8 : new Uint32Array(outputLen > 32 ? 16 : 8);
compress(IV, 0, block, 0, out, 0, outputLen > 32, 0, BLOCK_LEN, ROOT);
// Return result - use pre-created view for common 32-byte case
if (outputLen === 32 && IS_LITTLE_ENDIAN) {
return reusableOut8View.slice();
}
const result = new Uint8Array(outputLen);
if (IS_LITTLE_ENDIAN) {
result.set(new Uint8Array(out.buffer, 0, outputLen));
} else {
writeLittleEndianBytesPartial(out, 0, result, 0, outputLen);
}
return result;
}
// Should not reach here
return hashPureJS(input, outputLen);
}
/**
* Hash input data and return the result.
* Automatically uses WASM SIMD for large inputs when available.
*
* @param input - Data to hash
* @param outputLength - Number of bytes to output (default: 32)
* @returns The hash output
*/
export function hash(input: Uint8Array, outputLength: number = OUT_LEN): Uint8Array {
// For large inputs, use SIMD for ~1.5x performance improvement
if (input.length >= SIMD_THRESHOLD && ensureSimdSync()) {
return hashSimd(input, outputLength);
}
return hashPureJS(input, outputLength);
}
/**
* Pre-warm SIMD initialization (call early to avoid latency later).
*/
export function warmupSimd(): boolean {
return ensureSimdSync();
}
/**
* Hash input data directly into a caller-provided output buffer.
* Zero-allocation for the common 32-byte case - ideal for performance-critical code.
*
* @param input - Data to hash
* @param output - Pre-allocated output buffer (must be at least outputLength bytes)
* @param outputLength - Number of bytes to output (default: 32, max: output.length)
*/
export function hashInto(
input: Uint8Array,
output: Uint8Array,
outputLength: number = OUT_LEN,
): void {
// Validate output buffer
if (output.length < outputLength) {
throw new Error(`Output buffer too small: ${output.length} < ${outputLength}`);
}
// For large inputs, use SIMD for ~1.5x performance improvement
if (input.length >= SIMD_THRESHOLD && ensureSimdSync()) {
hashSimdInto(input, output, outputLength);
return;
}
hashPureJSInto(input, output, outputLength);
}
/**
* Internal: Hash using pure JS, writing directly to output buffer.
*/
function hashPureJSInto(input: Uint8Array, output: Uint8Array, outputLen: number): void {
const inputLen = input.length;
// Special case: empty input
if (inputLen === 0) {
const block = getBlockWords();
block.fill(0);
const out = outputLen <= 32 ? reusableOut8 : new Uint32Array(16);
compress(IV, 0, block, 0, out, 0, outputLen > 32, 0, 0, CHUNK_START | CHUNK_END | ROOT);
// Copy result to output
if (IS_LITTLE_ENDIAN) {
output.set(new Uint8Array(out.buffer, out.byteOffset, outputLen));
} else {
writeLittleEndianBytesPartial(out, 0, output, 0, outputLen);
}
return;
}
// Calculate number of chunks
const numChunks = Math.ceil(inputLen / CHUNK_LEN);
// Single chunk optimization
if (numChunks === 1) {
const cv = outputLen <= 32 ? reusableOut8 : new Uint32Array(16);
hashChunkRoot(input, 0, inputLen, 0, 0, cv, outputLen > 32);
// Copy result to output
if (IS_LITTLE_ENDIAN) {
output.set(new Uint8Array(cv.buffer, cv.byteOffset, outputLen));
} else {
writeLittleEndianBytesPartial(cv, 0, output, 0, outputLen);
}
return;
}
// Multiple chunks - delegate to hashPureJS and copy result
const result = hashPureJS(input, outputLen);
output.set(result);
}
/**
* Internal: Hash using SIMD, writing directly to output buffer.
*/
function hashSimdInto(input: Uint8Array, output: Uint8Array, outputLen: number): void {
// Delegate to hashSimd and copy result (SIMD path already optimized)
const result = hashSimd(input, outputLen);
output.set(result);
}