From 7161448d5649edd1f2ecb952044ce68d21d73241 Mon Sep 17 00:00:00 2001 From: Chris Duncan Date: Sat, 1 Mar 2025 23:38:59 -0800 Subject: [PATCH] Initial commit of CPU version. --- src/classes/cpu.ts | 220 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 src/classes/cpu.ts diff --git a/src/classes/cpu.ts b/src/classes/cpu.ts new file mode 100644 index 0000000..2b01f8f --- /dev/null +++ b/src/classes/cpu.ts @@ -0,0 +1,220 @@ +// SPDX-FileCopyrightText: 2025 Chris Duncan +// SPDX-License-Identifier: GPL-3.0-or-later +/// + +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 { + 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 { + 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 => { + 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 { + 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 => { + 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) + } +} -- 2.34.1