]> zoso.dev Git - nano-pow.git/commitdiff
Initial commit of CPU version.
authorChris Duncan <chris@zoso.dev>
Sun, 2 Mar 2025 07:38:59 +0000 (23:38 -0800)
committerChris Duncan <chris@zoso.dev>
Sun, 2 Mar 2025 07:38:59 +0000 (23:38 -0800)
src/classes/cpu.ts [new file with mode: 0644]

diff --git a/src/classes/cpu.ts b/src/classes/cpu.ts
new file mode 100644 (file)
index 0000000..2b01f8f
--- /dev/null
@@ -0,0 +1,220 @@
+// 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)
+       }
+}