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
* A successful response sets the `work` property.
*/
async pow (): Promise<void> {
- 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()
}
/**
// SPDX-FileCopyrightText: 2024 Chris Duncan <chris@zoso.dev>
// 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)
})
}
}
- #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<any> {
- if (approach !== 'converge' && approach !== 'divide')
+ static async work (approach: 'converge' | 'distribute', name: string, data: object[]): Promise<any> {
+ 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)
- }
}
import { Pool } from './pool.js'\r
import { Rpc } from './rpc.js'\r
import { Safe } from './safe.js'\r
-import { Bip44Ckd, NanoNaCl } from './workers.js'\r
import type { Ledger } from './ledger.js'\r
\r
type KeyPair = {\r
let results = await this.ckd(indexes)\r
const data: any = []\r
results.forEach(r => data.push({ privateKey: r.privateKey, index: r.index }))\r
- const pool = new Pool(NanoNaCl)\r
- const keypairs: KeyPair[] = await pool.work('divide', data)\r
- pool.dismiss()\r
+ const keypairs: KeyPair[] = await Pool.work('distribute', 'nano-nacl', data)\r
for (const keypair of keypairs) {\r
if (keypair.privateKey == null) throw new RangeError('Account private key missing')\r
if (keypair.publicKey == null) throw new RangeError('Account public key missing')\r
* @returns {Promise<Account>}\r
*/\r
async ckd (indexes: number[]): Promise<KeyPair[]> {\r
- const pool = new Pool(Bip44Ckd)\r
const data: any = []\r
indexes.forEach(i => data.push({ seed: this.seed, index: i }))\r
- const privateKeys: KeyPair[] = await pool.work('divide', data)\r
- pool.dismiss()\r
+ const privateKeys: KeyPair[] = await Pool.work('distribute', 'bip44-cdk', data)\r
return privateKeys\r
}\r
}\r
-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<any[]> {
+ 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<any[]> {
+ 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}`
})
async function calculate (data: any[]): Promise<any[]> {
- 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
}
/**
return new Uint8Array(signature)
}
- return { nanoCKD }
+ return { ckd, nanoCKD }
}
export const Bip44Ckd = b()
// SPDX-FileCopyrightText: 2024 Chris Duncan <chris@zoso.dev>
// 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<string> {
+ static async find (hash: string, threshold: string = this.SEND_THRESHOLD): Promise<string> {
return new Promise<string>(resolve => {
- calculate(hash, resolve, undefined, threshold)
+ this.calculate(hash, resolve, undefined, threshold)
})
}
// 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')
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)
return out
}
- function calculate (hashHex: string, callback: (nonce: string | PromiseLike<string>) => any, progressCallback?: (frames: number) => any, threshold: number | string = '0xFFFFFFF8'): void {
+ static calculate (hashHex: string, callback: (nonce: string | PromiseLike<string>) => 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)
gl.clearColor(0, 0, 0, 1)
- const reverseHex = hex_reverse(hashHex)
+ const reverseHex = this.hex_reverse(hashHex)
// Vertext Shader
const vsSource = `#version 300 es
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))
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),
// 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()