From: Chris Duncan Date: Tue, 10 Dec 2024 22:08:35 +0000 (-0800) Subject: Rewrite workers to be consolidated into one stringified file to ease Pool management... X-Git-Url: https://zoso.dev/?a=commitdiff_plain;h=fcbaac6c03067fd4cc4ec2954bf6501dee9aba9a;p=libnemo.git Rewrite workers to be consolidated into one stringified file to ease Pool management. Pow confirmed working, bip44-ckd and nano-nacl need to be tested, blake might be worth revisiting. --- diff --git a/src/lib/block.ts b/src/lib/block.ts index 024877b..5f919c1 100644 --- a/src/lib/block.ts +++ b/src/lib/block.ts @@ -8,7 +8,6 @@ import { dec, hex } from './convert.js' import { NanoNaCl } from './workers/nano-nacl.js' import { Pool } from './pool.js' import { Rpc } from './rpc.js' -import { Pow } from './workers.js' /** * Represents a block as defined by the Nano cryptocurrency protocol. The Block @@ -82,16 +81,15 @@ abstract class Block { * A successful response sets the `work` property. */ async pow (): Promise { - const pool = new Pool(Pow) const data = { "hash": this.previous, - "threshold": (this instanceof SendBlock || this instanceof ChangeBlock) - ? THRESHOLD_SEND - : THRESHOLD_RECEIVE + "threshold": '0xf' + // (this instanceof SendBlock || this instanceof ChangeBlock) + // ? THRESHOLD_SEND + // : THRESHOLD_RECEIVE } - const [{ work }] = await pool.work('converge', [data]) + const [{ work }] = await Pool.work('converge', 'pow', [data]) this.work = work - pool.dismiss() } /** diff --git a/src/lib/pool.ts b/src/lib/pool.ts index 7c249b9..16c8707 100644 --- a/src/lib/pool.ts +++ b/src/lib/pool.ts @@ -1,42 +1,40 @@ // SPDX-FileCopyrightText: 2024 Chris Duncan // SPDX-License-Identifier: GPL-3.0-or-later +import { Workers } from './workers.js' + +type Job = { + id: number + name: string + reject: (value: any) => void + resolve: (value: any) => void + data: any[] + results: any[] +} + type Thread = { - isBusy: boolean worker: Worker + job: Job | null } /** * Processes an array of tasks using Web Workers. */ export class Pool { - #approach: 'converge' | 'divide' = 'divide' - #cores: number = Math.max(1, navigator.hardwareConcurrency - 1) - #queue: object[] = [] - #resolve: Function = (value: unknown): void => { } - #results: object[] = [] - #threads: Thread[] = [] - #url: string - - get isDone (): boolean { - for (const thread of this.#threads) { - if (thread.isBusy) { - return false - } - } - return true - } + static #threads: Thread[] = [] + static #cores: number = Math.max(1, navigator.hardwareConcurrency - 1) + static #queue: Job[] = [] - constructor (fn: string) { - this.#url = URL.createObjectURL(new Blob([fn], { type: 'text/javascript' })) + static { + const url = URL.createObjectURL(new Blob([Workers], { type: 'text/javascript' })) for (let i = this.#cores; i > 0; i--) { const thread = { - isBusy: false, - worker: new Worker(this.#url, { type: 'module' }) + worker: new Worker(url, { type: 'module' }), + job: null } thread.worker.addEventListener('message', message => { - const data = new TextDecoder().decode(message.data ?? message) - let result = JSON.parse(data || "[]") + const data: string = new TextDecoder().decode(message.data) + let result: any = JSON.parse(data || "[]") if (!Array.isArray(result)) result = [result] this.#report(thread, result) }) @@ -44,57 +42,61 @@ export class Pool { } } - #assign (thread: Thread, next: any[]): void { + static #assign (thread: Thread, job: Job): void { + const chunk: number = 1 + (job.data.length / this.#cores) + const next = job.data.slice(0, chunk) + job.data = job.data.slice(chunk) + if (job.data.length === 0) this.#queue.shift() if (next?.length > 0) { - thread.isBusy = true - const buf = new TextEncoder().encode(JSON.stringify(next)).buffer - thread.worker.postMessage(buf, [buf]) + const buffer = new TextEncoder().encode(JSON.stringify(next)).buffer + thread.job = job + thread.worker.postMessage({ name: job.name, buffer }, [buffer]) } } - #report (thread: Thread, result: any[]): void { - thread.isBusy = false - if (result?.length > 0) { - this.#results.push(...result) - } - if (this.#approach === 'converge') { - this.#stop() - } - if (this.isDone) { - this.#resolve(this.#results) + static #isJobDone (jobId: number): boolean { + for (const thread of this.#threads) { + if (thread.job?.id === jobId) return false } + return true } - #stop (): void { - for (const thread of this.#threads) { - const msg = ['stop'] - const buf = new TextEncoder().encode(JSON.stringify(msg)).buffer - thread.worker.postMessage(buf, [buf]) + static #report (thread: Thread, results: any[]): void { + if (thread.job == null) { + throw new Error('Thread returned results but had nowhere to report it.') + } + const job = thread.job + if (this.#queue.length > 0) { + this.#assign(thread, this.#queue[0]) + } else { + thread.job = null + } + if (results.length > 0) { + job.results.push(...results) + } + if (this.#isJobDone(job.id)) { + job.resolve(job.results) } } - async work (approach: 'converge' | 'divide', data: object[],): Promise { - if (approach !== 'converge' && approach !== 'divide') + static async work (approach: 'converge' | 'distribute', name: string, data: object[]): Promise { + if (approach !== 'converge' && approach !== 'distribute') throw new TypeError('Invalid work approach') if (!Array.isArray(data)) data = [data] - return new Promise(resolve => { - this.#approach = approach - this.#queue = data - this.#resolve = resolve - const chunk = 1 + (this.#queue.length / this.#threads.length) - for (const thread of this.#threads) { - if (approach === 'converge') { - this.#assign(thread, this.#queue) - } else { - const next = this.#queue.slice(0, chunk) - this.#queue = this.#queue.slice(chunk) - this.#assign(thread, next) - } + return new Promise((resolve, reject) => { + const job: Job = { + id: performance.now(), + results: [], + data, + name, + resolve, + reject + } + if (Pool.#queue.length > 0) { + Pool.#queue.push(job) + } else { + for (const thread of Pool.#threads) Pool.#assign(thread, job) } }) } - - dismiss (): void { - URL.revokeObjectURL(this.#url) - } } diff --git a/src/lib/wallet.ts b/src/lib/wallet.ts index 447c205..40104f9 100644 --- a/src/lib/wallet.ts +++ b/src/lib/wallet.ts @@ -9,7 +9,6 @@ import { Entropy } from './entropy.js' import { Pool } from './pool.js' import { Rpc } from './rpc.js' import { Safe } from './safe.js' -import { Bip44Ckd, NanoNaCl } from './workers.js' import type { Ledger } from './ledger.js' type KeyPair = { @@ -93,9 +92,7 @@ abstract class Wallet { let results = await this.ckd(indexes) const data: any = [] results.forEach(r => data.push({ privateKey: r.privateKey, index: r.index })) - const pool = new Pool(NanoNaCl) - const keypairs: KeyPair[] = await pool.work('divide', data) - pool.dismiss() + const keypairs: KeyPair[] = await Pool.work('distribute', 'nano-nacl', data) for (const keypair of keypairs) { if (keypair.privateKey == null) throw new RangeError('Account private key missing') if (keypair.publicKey == null) throw new RangeError('Account public key missing') @@ -422,11 +419,9 @@ export class Bip44Wallet extends Wallet { * @returns {Promise} */ async ckd (indexes: number[]): Promise { - const pool = new Pool(Bip44Ckd) const data: any = [] indexes.forEach(i => data.push({ seed: this.seed, index: i })) - const privateKeys: KeyPair[] = await pool.work('divide', data) - pool.dismiss() + const privateKeys: KeyPair[] = await Pool.work('distribute', 'bip44-cdk', data) return privateKeys } } diff --git a/src/lib/workers.ts b/src/lib/workers.ts index 631a4f7..d6d03a7 100644 --- a/src/lib/workers.ts +++ b/src/lib/workers.ts @@ -1,6 +1,90 @@ -import { worker as Bip44Ckd } from './workers/bip44-ckd.js' -import { worker as NanoNaCl } from './workers/nano-nacl.js' +import { Bip44Ckd, worker as Bip44CkdWorker } from './workers/bip44-ckd.js' +import { NanoNaCl, worker as NanoNaClWorker } from './workers/nano-nacl.js' // import './workers/passkey.js' -import { worker as Pow } from './workers/pow.js' +import { Pow } from './workers/pow.js' -export { Bip44Ckd, NanoNaCl, Pow } + +const w = () => { + /** + * Listens for messages from a calling function. + */ + addEventListener('message', (message: any): void => { + const { name, buffer } = message.data + if (name === 'STOP') { + close() + const buffer = new ArrayBuffer(0) + //@ts-expect-error + postMessage(buffer, [buffer]) + } else { + const data = JSON.parse(new TextDecoder().decode(buffer)) + switch (name) { + case 'bip44-ckd': { + getPrivateKey(data).then(report) + break + } + case 'nano-nacl': { + getPublicKey(data).then(report) + break + } + case 'pow': { + getPow(data).then(report) + break + } + } + } + }) + + function report (results: any) { + const buffer = new TextEncoder().encode(JSON.stringify(results)).buffer + //@ts-expect-error + postMessage(buffer, [buffer]) + } + + + //BIP-44 + async function getPrivateKey (data: any): Promise { + const BIP44_PURPOSE = 44 + return new Promise(async (resolve) => { + for (const d of data) { + if (d.coin != null && d.coin !== BIP44_PURPOSE) { + d.privateKey = await Bip44Ckd.ckd(d.seed, d.coin, d.index) + } else { + d.privateKey = await Bip44Ckd.nanoCKD(d.seed, d.index) + } + } + resolve(data) + }) + } + + + + //NACL + async function getPublicKey (data: any): Promise { + return new Promise(async (resolve) => { + for (const d of data) { + d.publicKey = await NanoNaCl.convert(d.privateKey) + } + resolve(data) + }) + } + + + + // POW + async function getPow (data: any) { + return new Promise(async (resolve) => { + for (const d of data) { + d.work = await Pow.find(d.hash, d.threshold) + } + resolve(data) + }) + } +} + +const bip44ckd = `const Bip44Ckd = () => {\n${Bip44CkdWorker}\n}\n` +const nanonacl = `const NanoNaCl = () => {\n${NanoNaClWorker}\n}\n` +const pow = `const Pow = ${Pow}` +const start = w.toString().indexOf('{') + 1 +const end = w.toString().lastIndexOf('}') +const body = w.toString().substring(start, end) +export const Workers = `${bip44ckd}${nanonacl}${pow}${body}` diff --git a/src/lib/workers/bip44-ckd.ts b/src/lib/workers/bip44-ckd.ts index e430752..d51864a 100644 --- a/src/lib/workers/bip44-ckd.ts +++ b/src/lib/workers/bip44-ckd.ts @@ -26,16 +26,14 @@ const b = () => { }) async function calculate (data: any[]): Promise { - return new Promise(async (resolve) => { - for (const d of data) { - if (d.coin != null && d.coin !== BIP44_PURPOSE) { - d.privateKey = await ckd(d.seed, d.coin, d.index) - } else { - d.privateKey = await nanoCKD(d.seed, d.index) - } + for (const d of data) { + if (d.coin != null && d.coin !== BIP44_PURPOSE) { + d.privateKey = await ckd(d.seed, d.coin, d.index) + } else { + d.privateKey = await nanoCKD(d.seed, d.index) } - resolve(data) - }) + } + return data } /** @@ -133,7 +131,7 @@ const b = () => { return new Uint8Array(signature) } - return { nanoCKD } + return { ckd, nanoCKD } } export const Bip44Ckd = b() diff --git a/src/lib/workers/pow.ts b/src/lib/workers/pow.ts index 1bfe34b..32eb139 100644 --- a/src/lib/workers/pow.ts +++ b/src/lib/workers/pow.ts @@ -1,33 +1,12 @@ // SPDX-FileCopyrightText: 2024 Chris Duncan // SPDX-License-Identifier: GPL-3.0-or-later -const p = async () => { - const SEND_THRESHOLD = '0xfffffff8' - /** - * Listens for messages from a calling function. - */ - if (typeof addEventListener === 'function') { - addEventListener('message', (message: any): void => { - const data = JSON.parse(new TextDecoder().decode(message.data ?? message)) - for (const d of data) { - if (d === 'stop') { - close() - postMessage(new ArrayBuffer(0)) - } else { - find(d.hash, d.threshold ?? SEND_THRESHOLD).then(nonce => { - d.work = nonce - const buf = new TextEncoder().encode(JSON.stringify(data)).buffer - //@ts-expect-error - postMessage(buf, [buf]) - }) - } - } - }) - } +export class Pow { + static SEND_THRESHOLD = '0xfffffff8' - async function find (hash: string, threshold: string = SEND_THRESHOLD): Promise { + static async find (hash: string, threshold: string = this.SEND_THRESHOLD): Promise { return new Promise(resolve => { - calculate(hash, resolve, undefined, threshold) + this.calculate(hash, resolve, undefined, threshold) }) } @@ -50,10 +29,10 @@ const p = async () => { // Both width and height must be multiple of 256, (one byte) // but do not need to be the same, // matching GPU capabilities is the aim - const webglWidth = 256 * 4 - const webglHeight = 256 * 4 + static webglWidth = 256 * 4 + static webglHeight = 256 * 4 - function hexify (arr: number[] | Uint8Array): string { + static hexify (arr: number[] | Uint8Array): string { let out = '' for (let i = arr.length - 1; i >= 0; i--) { out += arr[i].toString(16).padStart(2, '0') @@ -61,7 +40,7 @@ const p = async () => { return out } - function hex_reverse (hex: string): string { + static hex_reverse (hex: string): string { let out = '' for (let i = hex.length; i > 0; i -= 2) { out += hex.slice(i - 2, i) @@ -69,10 +48,10 @@ const p = async () => { return out } - function calculate (hashHex: string, callback: (nonce: string | PromiseLike) => any, progressCallback?: (frames: number) => any, threshold: number | string = '0xFFFFFFF8'): void { + static calculate (hashHex: string, callback: (nonce: string | PromiseLike) => any, progressCallback?: (frames: number) => any, threshold: number | string = '0xFFFFFFF8'): void { if (typeof threshold === 'number') threshold = '0x' + threshold.toString(16) - const canvas = new OffscreenCanvas(webglWidth, webglHeight) + const canvas = new OffscreenCanvas(this.webglWidth, this.webglHeight) const gl = canvas.getContext('webgl2') if (!gl) @@ -83,7 +62,7 @@ const p = async () => { gl.clearColor(0, 0, 0, 1) - const reverseHex = hex_reverse(hashHex) + const reverseHex = this.hex_reverse(hashHex) // Vertext Shader const vsSource = `#version 300 es @@ -329,8 +308,8 @@ const p = async () => { crypto.getRandomValues(work0) crypto.getRandomValues(work1) - gl.uniform4uiv(work0Location, Array.from(work0)) - gl.uniform4uiv(work1Location, Array.from(work1)) + gl.uniform4uiv(work0Location, work0) + gl.uniform4uiv(work1Location, work1) // Check with progressCallback every 100 frames if (n % 100 === 0 && typeof progressCallback === 'function' && progressCallback(n)) @@ -339,14 +318,18 @@ const p = async () => { gl.clear(gl.COLOR_BUFFER_BIT) gl.drawArrays(gl.TRIANGLES, 0, 6) const pixels = new Uint8Array(gl.drawingBufferWidth * gl.drawingBufferHeight * 4) + performance.mark('readPixels start') gl.readPixels(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight, gl.RGBA, gl.UNSIGNED_BYTE, pixels) + performance.mark('readPixels end') + for (const e of performance.getEntries()) console.dir(e.toJSON()) + performance.clearMarks() // Check the pixels for any success for (let i = 0; i < pixels.length; i += 4) { if (pixels[i] !== 0) { // Return the work value with the custom bits typeof callback === 'function' && - callback(hexify(work1) + hexify([ + callback(this.hexify(work1) + this.hexify([ pixels[i + 2], pixels[i + 3], work0[2] ^ (pixels[i] - 1), @@ -362,12 +345,6 @@ const p = async () => { // Begin generation self.requestAnimationFrame(draw) } - - return { find } } -export const Pow = await p() - -const start = p.toString().indexOf('{') + 1 -const end = p.toString().lastIndexOf('return') -export const worker = p.toString().substring(start, end) +export default Pow.toString()