import { writeFile } from 'node:fs/promises'
+/**
+ * Generate the BLAKE2b hashing algorithm for different Web APIs. Specifically,
+ * the output varies by API as follows:
+ *
+ * - WebGPU: outputs `compute.wgsl`, a compute shader
+ * - WebGL: outputs `draw.frag`, a fragment shader
+ * - WASM: outputs `asm/index.ts`, an AssemblyScript file that must be compiled
+ * to a WASM module
+ *
+ * Some whitespace output is removed for brevity in the final files, which
+ * should be checked separately for correctness using language-specific tools
+ * e.g. `wgsl-analyzer` or `asc`
+ *
+ * @param {('wasm'|'webgl'|'webgpu')} api
+ * @returns {string}
+ */
function generate (api) {
if (api !== 'wasm' && api !== 'webgl' && api !== 'webgpu') {
throw new TypeError(`Cannot generate BLAKE2b code for ${api}`)
}
- // Shorthand for adding two operands and storing sum in the first.
+ /**
+ * Shorthand for adding two operands and storing sum in the first.
+ *
+ * @param {string} a - first operand variable name, and target for result
+ * @param {string} b - second operand variable name
+ * @returns {string} API-specific code for addition assignment
+ */
function add (a, b) {
switch (api) {
case 'wasm': {
}
}
- // Shorthand for how values are constructed.
+ /**
+ * Shorthand for how values are constructed. For WASM, the 64-bit uint is
+ * copied into two lanes of a 128-bit vector in order to use them in parallel
+ * with SIMD. For WebGL and WebGPU, which do not have native 64-bit uints, the
+ * 64-bit integer is split into 32-bit values representing its high and low
+ * bits and then assigned to two lanes of a vector; see `add()` for the extra
+ * carry bit step required for this representation.
+ *
+ * @param {string} lo - low 32 bits of a 64-bit integer
+ * @param {string} [hi] - high 32 bits of a 64-bit integer
+ * @returns {string} API-specific code for initializing the full 64-bit value
+ */
function ctr (lo, hi) {
switch (api) {
case 'wasm': {
}
}
- // Shorthand for declaration statement.
+ /**
+ * Shorthand for declaration statement.
+ *
+ * @returns {('let'|'uvec2'|'var')}
+ */
const declare = (() => {
switch (api) {
case 'wasm': return 'let'
}
})()
- // Shorthand for righthand bit rotation of a SIMD i64x2 value.
+ /**
+ * Shorthand for righthand bit rotation of a SIMD vector of 64-bit uints.
+ *
+ * @param {string} v - variable to rotate
+ * @param {number} i - number of bits to rotate
+ * @returns {string} API-specific code for bit-rotating the integer
+ */
function rotr (v, i) {
switch (api) {
case 'wasm': {
}
}
- // Shorthand for XOR'ing two SIMD i64x2 operands and saving result in the first.
+ /**
+ * Shorthand for XOR'ing two SIMD i64x2 operands and assigning to the first.
+ *
+ * @param {string} a - first operand variable name, and target for result
+ * @param {string} b - second operand variable name
+ * @returns {string} API-specific code for xor assignment
+ */
function xor (a, b) {
switch (api) {
case 'wasm': return `${a} = v128.xor(${a}, ${b});`
[14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3]
]
- // Initialization vector defined by BLAKE2.
- // Application of each XOR is defined by BLAKE2 section 2.4 compression
- // function. Each value represents two halves of the original u64 value from the
- // reference implementation. They appear reversed pairwise in order to align
- // with little-endian computation.
+ /**
+ * Initialization vector defined by BLAKE2.
+ *
+ * Application of each XOR is defined by BLAKE2 section 2.4 compression
+ * function. Each value represents two halves of the original u64 value from
+ * the reference implementation. They appear reversed pairwise in order to
+ * align with little-endian computation.
+ */
const blake2b_iv = [
ctr(`0xf3bcc908`, `0x6a09e667`),
ctr(`0x84caa73b`, `0xbb67ae85`),
ctr(`0x137e2179`, `0x5be0cd19`)
]
- // Parameter block as defined in BLAKE2 section 2.8 and configured as follows:
- // maximal depth = 1, fanout = 1, digest byte length = 8
+ /**
+ * Parameter block as defined in BLAKE2 section 2.8 and configured as follows:
+ * maximal depth = 1, fanout = 1, digest byte length = 8
+ */
const blake2b_param = ctr(`0x01010008`, `0x0`)
- // Message input length which is always 40 for Nano.
- // 8 nonce bytes + 32 block hash bytes
+ /**
+ * Message input length which is always 40 for Nano.
+ * 8 nonce bytes + 32 block hash bytes
+ */
const blake2b_inlen = ctr(`0x28`, `0x0`)
- // Finalization flag as defined in BLAKE2 section 2.4 and set to ~0 since this
- // is the final (and only) message block being hashed.
+ /**
+ * Finalization flag as defined in BLAKE2 section 2.4 and set to ~0 since this
+ * is the final (and only) message block being hashed.
+ */
const blake2b_final = ctr(`~0x0`)
+ /**
+ * Mixing function as defined in BLAKE2.
+ *
+ * Specifically skips addition of message input past m[4] since input for
+ * Nano proof-of-work is always precisely 40 bytes and any addition past that
+ * point would just be adding zero.
+ *
+ * @param {number} a - G mixing index
+ * @param {number} b - G mixing index
+ * @param {number} c - G mixing index
+ * @param {number} d - G mixing index
+ * @param {number} x - SIGMA index for first message block
+ * @param {number} y - SIGMA index for second message block
+ * @returns {string} Unrolled code for a pass of columnar and diagonal mixing
+ */
function G (a, b, c, d, x, y) {
return `
${add(`v${a}`, `v${b}`)}
`
}
+ /**
+ * Builds string of code for calling G mixing function as defined by BLAKE2.
+ *
+ * Specifically skips passes 6 and 8 during the final round since they do not
+ * affect the final output when used for Nano proof-of-work.
+ *
+ * @param {number} r
+ * @returns {string} Unrolled code for 8 passes of G
+ */
function ROUND (r) {
let output = `
// ROUND ${r}
return output
}
+ /**
+ * Builds string of code for initializing state vectors. Loops are unrolled.
+ *
+ * @returns {string} API-specific code setting initial state
+ */
function INIT () {
let output = `
// INITIALIZE STATE VECTOR
`
}
+ /**
+ * Builds string of code that first initializes the BLAKE2b state vectors and
+ * then compresses the input message. For Nano proof-of-work, the input is
+ * only 40 bytes, so compression only happens once.
+ *
+ * @returns {string} API-specific code executing 12 rounds of G mixing
+ */
function COMPRESS () {
let output = `
${INIT()}
return output.replace(/\t{2,}/g, '\t')
}
+ /**
+ * Builds string of code that represents the entire BLAKE2b hashing algorithm,
+ * optimized for Nano proof-of-work, based on a particular Web API. This code
+ * in general is not shared between APIs and thus cannot be abstracted into
+ * builder functions like other code.
+ *
+ * @returns {string} API-specific code to search for Nano proof-of-work values
+ */
const main = (() => {
switch (api) {
case 'wasm': {