--- /dev/null
+// SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
+// SPDX-License-Identifier: GPL-3.0-or-later
+/// <reference types="@webgpu/types" />
+
+import type { NanoPowOptions } from '../../types.d.ts'
+/**
+* Nano proof-of-work using CPU.
+*/
+export class NanoPowCpu {
+ // Initialize constants and buffers
+ static #busy: boolean = false
+ static #debug: boolean = false
+ static #blake2b_IV: BigUint64Array
+ static #blake2b_sigma: number[][]
+ static #v: BigUint64Array
+ static #m: BigUint64Array
+
+ static async init (): Promise<void> {
+ if (this.#busy) return
+ this.#busy = true
+ try {
+ this.#blake2b_IV = new BigUint64Array([
+ 0x6a09e667f3bcc908n,
+ 0xbb67ae8584caa73bn,
+ 0x3c6ef372fe94f82bn,
+ 0xa54ff53a5f1d36f1n,
+ 0x510e527fade682d1n,
+ 0x9b05688c2b3e6c1fn,
+ 0x1f83d9abfb41bd6bn,
+ 0x5be0cd19137e2179n
+ ])
+ this.#blake2b_sigma = [
+ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
+ [14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3],
+ [11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4],
+ [7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8],
+ [9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13],
+ [2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9],
+ [12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11],
+ [13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10],
+ [6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5],
+ [10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0],
+ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
+ [14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3]
+ ]
+ this.#v = new BigUint64Array(16)
+ this.#m = new BigUint64Array(16)
+ console.log(`NanoPow CPU initialized.`)
+ } catch (err) {
+ throw new Error('NanoPow CPU initialization failed.', { cause: err })
+ } finally {
+ this.#busy = false
+ }
+ }
+
+ static reset (): void {
+ console.warn(`NanoPow CPU encountered an error. Reinitializing...`)
+ this.#m?.fill(0n)
+ this.#v?.fill(0n)
+ NanoPowCpu.#busy = false
+ NanoPowCpu.init()
+ }
+
+ static #logAverages (times: number[]): void {
+ let count = times.length, truncatedCount = 0, truncated = 0, sum = 0, reciprocals = 0, logarithms = 0, min = Number.MAX_SAFE_INTEGER, max = 0, median = 0, rate = 0
+ times.sort()
+ for (let i = 0; i < count; i++) {
+ sum += times[i]
+ reciprocals += 1 / times[i]
+ logarithms += Math.log(times[i])
+ min = Math.min(min, times[i])
+ max = Math.max(max, times[i])
+ if (i === Math.ceil(count / 2)) {
+ median = times[i]
+ }
+ if (count < 3 || (i > (0.1 * count) && i < (0.9 * (count - 1)))) {
+ truncated += times[i]
+ truncatedCount++
+ }
+ }
+ const averages = {
+ "Count (dispatches)": count,
+ "Total (ms)": sum,
+ "Rate (d/s)": 1000 * truncatedCount / (truncated || sum),
+ "Minimum (ms)": min,
+ "Maximum (ms)": max,
+ "Median (ms)": median,
+ "Arithmetic Mean (ms)": sum / count,
+ "Truncated Mean (ms)": truncated / truncatedCount,
+ "Harmonic Mean (ms)": count / reciprocals,
+ "Geometric Mean (ms)": Math.exp(logarithms / count)
+ }
+ console.table(averages)
+ }
+
+ static #G (v: BigUint64Array, a: number, b: number, c: number, d: number, m: BigUint64Array, x: number, y: number) {
+ v[a] += v[b]
+ v[a] += m[x]
+ v[d] ^= v[a]
+ v[d] = (v[d] >> 32n) | (v[d] << 32n)
+ v[c] += v[d]
+ v[b] ^= v[c]
+ v[b] = (v[b] >> 24n) | (v[b] << 40n)
+
+ v[a] += v[b]
+ v[a] += m[y]
+ v[d] ^= v[a]
+ v[d] = (v[d] >> 16n) | (v[d] << 48n)
+ v[c] += v[d]
+ v[b] ^= v[c]
+ v[b] = (v[b] >> 63n) | (v[b] << 1n)
+ }
+
+ static #hash (seed: bigint, hash: string): bigint {
+ // Reset buffers before each calculation
+ for (let i = 0; i < 16; i++) {
+ this.#v[i] = this.#blake2b_IV[i]
+ this.#m[i] = 0n
+ }
+
+ // Set up input buffers
+ this.#m[0] = seed
+ if (this.#debug) console.log(`seed: ${this.#m[0]}`)
+ for (let i = 0; i < hash.length; i += 16) {
+ const u64 = hash.slice(i, i + 16)
+ this.#m[i + 1] = BigInt(`0x${u64}`)
+ }
+ if (this.#debug) console.log('m', this.#m)
+
+ for (let r = 0; r < 12; r++) {
+ const s = this.#blake2b_sigma[r]
+ this.#G(this.#v, 0, 4, 8, 12, this.#m, s[0], s[1])
+ this.#G(this.#v, 1, 5, 9, 13, this.#m, s[2], s[3])
+ this.#G(this.#v, 2, 6, 10, 14, this.#m, s[4], s[5])
+ this.#G(this.#v, 3, 7, 11, 15, this.#m, s[6], s[7])
+ this.#G(this.#v, 0, 5, 10, 15, this.#m, s[8], s[9])
+ this.#G(this.#v, 1, 6, 11, 12, this.#m, s[10], s[11])
+ this.#G(this.#v, 2, 7, 8, 13, this.#m, s[12], s[13])
+ this.#G(this.#v, 3, 4, 9, 14, this.#m, s[14], s[15])
+ }
+ return (this.#blake2b_IV[0] ^ this.#v[0] ^ this.#v[8])
+ }
+
+ /**
+ * Finds a nonce that satisfies the Nano proof-of-work requirements.
+ *
+ * @param {string} hash - Hexadecimal hash of previous block, or public key for new accounts
+ * @param {NanoPowOptions} options - Used to configure search execution
+ */
+ static async search (hash: string, options?: NanoPowOptions): Promise<string> {
+ if (!/^[A-Fa-f0-9]{64}$/.test(hash)) throw new TypeError(`Invalid hash ${hash}`)
+ if (this.#busy) {
+ console.log('NanoPowCpu is busy. Retrying search...')
+ return new Promise(resolve => {
+ setTimeout(async (): Promise<void> => {
+ const result = this.search(hash, options)
+ resolve(result)
+ }, 100)
+ })
+ }
+ this.#busy = true
+ const threshold = (typeof options?.threshold !== 'number' || options.threshold < 0x0 || options.threshold > 0xffffffff)
+ ? 0xfffffff800000000n
+ : BigInt(`0x${options.threshold}00000000`)
+ this.#debug = !!(options?.debug)
+
+ let nonce = 0n
+ let seed = 0n
+ return new Promise((resolve, reject) => {
+ const start = performance.now()
+ const check = () => {
+ const random0 = Math.floor(Math.random() * 0xffffffff)
+ const random1 = Math.floor(Math.random() * 0xffffffff)
+ seed = (BigInt(random0) << 32n) | BigInt(random1)
+ nonce = this.#hash(seed, hash)
+ if (nonce >= threshold) {
+ this.#busy = false
+ if (this.#debug) console.log(performance.now() - start)
+ resolve(nonce.toString(16).padStart(16, '0'))
+ } else {
+ requestAnimationFrame(check)
+ }
+ }
+ check()
+ })
+ }
+
+ /**
+ * Validates that a nonce satisfies Nano proof-of-work requirements.
+ *
+ * @param {string} work - Hexadecimal proof-of-work value to validate
+ * @param {string} hash - Hexadecimal hash of previous block, or public key for new accounts
+ * @param {NanoPowOptions} options - Options used to configure search execution
+ */
+ static async validate (work: string, hash: string, options?: NanoPowOptions): Promise<boolean> {
+ if (!/^[A-Fa-f0-9]{16}$/.test(work)) throw new TypeError(`Invalid work ${work}`)
+ if (!/^[A-Fa-f0-9]{64}$/.test(hash)) throw new TypeError(`Invalid hash ${hash}`)
+ if (this.#busy) {
+ console.log('NanoPowCpu is busy. Retrying validate...')
+ return new Promise(resolve => {
+ setTimeout(async (): Promise<void> => {
+ const result = this.validate(work, hash, options)
+ resolve(result)
+ }, 100)
+ })
+ }
+ this.#busy = true
+ this.#debug = !!(options?.debug)
+ const threshold = (typeof options?.threshold !== 'number' || options.threshold < 0x0 || options.threshold > 0xffffffff)
+ ? 0xfffffff800000000n
+ : BigInt(`0x${options.threshold}00000000`)
+
+ const seed = BigInt(`0x${work}`)
+ if (this.#debug) console.log(`work: ${work}`)
+ const nonce = this.#hash(seed, hash)
+ if (this.#debug) console.log(`nonce: ${nonce}`)
+ this.#busy = false
+ return (nonce >= threshold)
+ }
+}