From: Chris Duncan Date: Sat, 16 Nov 2024 21:12:31 +0000 (-0800) Subject: Scrap worker pool in favor of one worker per wallet. X-Git-Tag: v0.0.20~19 X-Git-Url: https://zoso.dev/?a=commitdiff_plain;h=f386a6c0ab7c5a68382f3fb194cb0f521845a5fa;p=libnemo.git Scrap worker pool in favor of one worker per wallet. --- diff --git a/package.json b/package.json index d46f9d6..fc3ad00 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "url": "git+https://zoso.dev/libnemo.git" }, "scripts": { - "build": "rm -rf dist && tsc && esbuild main.min=dist/main.js global.min=dist/global.js ckd=dist/lib/ckd.js --outdir=dist --target=es2022 --format=esm --platform=browser --bundle --minify --sourcemap", + "build": "rm -rf dist && tsc && esbuild main.min=dist/main.js global.min=dist/global.js ckd=dist/lib/ckd.js --outdir=dist --target=es2022 --format=esm --platform=node --bundle --minify --sourcemap", "test": "npm run build && node --test --env-file .env", "test:coverage": "npm run test -- --experimental-test-coverage", "test:coverage:report": "npm run test:coverage -- --test-reporter=lcov --test-reporter-destination=coverage.info && genhtml coverage.info --output-directory test/coverage && rm coverage.info && xdg-open test/coverage/index.html" diff --git a/src/lib/ckd.ts b/src/lib/ckd.ts index 3338d49..377aacb 100644 --- a/src/lib/ckd.ts +++ b/src/lib/ckd.ts @@ -1,11 +1,17 @@ // SPDX-FileCopyrightText: 2024 Chris Duncan // SPDX-License-Identifier: GPL-3.0-or-later -import { Account } from './account.js' import { nanoCKD } from './bip32-key-derivation.js' import { bytes, dec } from './convert.js' import Tools from './tools.js' -import type { Ledger } from './ledger.js' + +if (globalThis.Worker == null) { + const { isMainThread, parentPort } = await import('node:worker_threads') + if (!isMainThread) { + globalThis.addEventListener ??= Object.getPrototypeOf(parentPort).on.bind(parentPort) + globalThis.postMessage ??= Object.getPrototypeOf(parentPort).postMessage.bind(parentPort) + } +} /** * Derives BIP-44 Nano account private keys. @@ -13,8 +19,8 @@ import type { Ledger } from './ledger.js' * @param {number} index - Index of the account * @returns {Promise} */ -globalThis.onmessage = (event) => { - const { type, seed, index } = event.data +addEventListener('message', (message) => { + const { type, seed, index } = message.data ?? message switch (type) { case 'bip44': { ckdBip44(seed, index).then(postMessage) @@ -25,7 +31,7 @@ globalThis.onmessage = (event) => { break } } -} +}, { once: true }) /** * Derives BIP-44 Nano account private keys. diff --git a/src/lib/pool.ts b/src/lib/pool.ts deleted file mode 100644 index 6d4690f..0000000 --- a/src/lib/pool.ts +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Chris Duncan -// SPDX-License-Identifier: GPL-3.0-or-later -const Worker = globalThis.Worker - -type Task = { - data: object, - resolve: Function -} - -class Thread { - isAvailable: boolean - worker: Worker - - constructor (url: string | URL) { - this.isAvailable = true - this.worker = new Worker(new URL(url, import.meta.url)) - } -} - -/** -* Assigns a Web Worker to process data. Creates a new one if none are available. -* -* @param {object} data - Arbitrary data encapsulated in a JSON object -* @param {string} signature - Hexadcimal-formatted signature -* @param {...string} input - Data to be verified -* @returns {boolean} True if the data was signed by the public key's matching private key -*/ -export class Pool { - #tasks: any[] - #threads: Thread[] - #url - - constructor (url: string | URL) { - this.#tasks = [] - this.#url = new URL(url, import.meta.url) - this.#threads = Array(navigator.hardwareConcurrency) - .fill(undefined) - .map(() => { return new Thread(this.#url) }) - } - - #assign (thread: Thread, task: Task) { - thread.isAvailable = false - thread.worker.onmessage = (event) => { - if (this.#tasks.length > 0) { - const next = this.#tasks.shift() - this.#assign(thread, next) - } else { - thread.isAvailable = true - } - task.resolve(event.data) - } - thread.worker.postMessage(task.data) - } - - async work (data: object): Promise { - return new Promise(resolve => { - const thread = this.#threads.find(t => t.isAvailable) - if (thread) { - this.#assign(thread, { data, resolve }) - } else { - this.#tasks.push({ data, resolve }) - } - }) - } -} diff --git a/src/lib/safe.ts b/src/lib/safe.ts index cebab59..8654756 100644 --- a/src/lib/safe.ts +++ b/src/lib/safe.ts @@ -4,10 +4,10 @@ import { buffer, hex, utf8 } from './convert.js' import { Entropy } from './entropy.js' const { subtle } = globalThis.crypto -const storage = globalThis.sessionStorage const ERR_MSG = 'Failed to store item in Safe' export class Safe { + #storage = globalThis.sessionStorage /** * Encrypts data with a password and stores it in the Safe. @@ -18,7 +18,7 @@ export class Safe { */ async put (name: string, key: CryptoKey, data: any): Promise async put (name: string, passkey: string | CryptoKey, data: any): Promise { - if (storage.getItem(name)) { + if (this.#storage.getItem(name)) { throw new Error(ERR_MSG) } return this.overwrite(name, passkey as string, data) @@ -60,7 +60,7 @@ export class Safe { } await new Promise((resolve, reject) => { try { - storage.setItem(name, JSON.stringify(record)) + this.#storage.setItem(name, JSON.stringify(record)) resolve() } catch (err) { reject(err) @@ -70,7 +70,7 @@ export class Safe { } catch (err) { throw new Error(ERR_MSG) } - return (storage.getItem(name) != null) + return (this.#storage.getItem(name) != null) } /** @@ -87,7 +87,7 @@ export class Safe { } const item = await new Promise(resolve => { - resolve(storage.getItem(name)) + resolve(this.#storage.getItem(name)) }) if (item == null) { return null @@ -110,7 +110,7 @@ export class Safe { const decoded = buffer.toUtf8(decrypted) const data = JSON.parse(decoded) passkey = '' - storage.removeItem(name) + this.#storage.removeItem(name) return data } catch (err) { return null diff --git a/src/lib/wallet.ts b/src/lib/wallet.ts index bebb491..8d8fa80 100644 --- a/src/lib/wallet.ts +++ b/src/lib/wallet.ts @@ -3,18 +3,12 @@ import { Account } from './account.js' import { Bip39Mnemonic } from './bip39-mnemonic.js' -import { nanoCKD } from './bip32-key-derivation.js' import { ADDRESS_GAP, SEED_LENGTH_BIP44, SEED_LENGTH_BLAKE2B } from './constants.js' -import { bytes, dec } from './convert.js' import { Entropy } from './entropy.js' -import { Pool } from './pool.js' import { Rpc } from './rpc.js' import { Safe } from './safe.js' -import Tools from './tools.js' import type { Ledger } from './ledger.js' -const ckdPool = new Pool('./ckd.js') - /** * Represents a wallet containing numerous Nano accounts derived from a single * source, the form of which can vary based on the type of wallet. The Wallet @@ -236,12 +230,14 @@ abstract class Wallet { */ export class Bip44Wallet extends Wallet { static #isInternal: boolean = false + #worker: Worker constructor (seed: string, mnemonic?: Bip39Mnemonic, id?: string) { if (!Bip44Wallet.#isInternal) { throw new Error(`Bip44Wallet cannot be instantiated directly. Use 'await Bip44Wallet.create()' instead.`) } super(seed, mnemonic, id) + this.#worker = new Worker(new URL('./ckd.js', import.meta.url), { type: 'module' }) Bip44Wallet.#isInternal = false } @@ -393,11 +389,16 @@ export class Bip44Wallet extends Wallet { * @returns {Promise} */ async ckd (index: number): Promise { - const key = await ckdPool.work({ type: 'bip44', seed: this.seed, index }) - if (typeof key !== 'string') { - throw new TypeError('BIP-44 child key derivation returned invalid data') - } - return await Account.fromPrivateKey(key, index) + return new Promise(resolve => { + this.#worker.addEventListener('message', (message) => { + const key = message.data ?? message + if (typeof key !== 'string') { + throw new TypeError('BIP-44 child key derivation returned invalid data') + } + Account.fromPrivateKey(key, index).then(resolve) + }, { once: true }) + this.#worker.postMessage({ type: 'bip44', seed: this.seed, index }) + }) } } @@ -419,12 +420,14 @@ export class Bip44Wallet extends Wallet { */ export class Blake2bWallet extends Wallet { static #isInternal: boolean = false + #worker: Worker constructor (seed: string, mnemonic?: Bip39Mnemonic, id?: string) { if (!Blake2bWallet.#isInternal) { throw new Error(`Blake2bWallet cannot be instantiated directly. Use 'await Blake2bWallet.create()' instead.`) } super(seed, mnemonic, id) + this.#worker = new Worker(new URL('./ckd.js', import.meta.url), { type: 'module' }) Blake2bWallet.#isInternal = false } @@ -538,11 +541,16 @@ export class Blake2bWallet extends Wallet { * @returns {Promise} */ async ckd (index: number): Promise { - const key = await ckdPool.work({ type: 'blake2b', seed: this.seed, index }) - if (typeof key !== 'string') { - throw new TypeError('BLAKE2b child key derivation returned invalid data') - } - return await Account.fromPrivateKey(key, index) + return new Promise(resolve => { + this.#worker.addEventListener('message', (message) => { + const key = message.data ?? message + if (typeof key !== 'string') { + throw new TypeError('BLAKE2b child key derivation returned invalid data') + } + Account.fromPrivateKey(key, index).then(resolve) + }, { once: true }) + this.#worker.postMessage({ type: 'blake2b', seed: this.seed, index }) + }) } } diff --git a/test/GLOBALS.js b/test/GLOBALS.mjs similarity index 67% rename from test/GLOBALS.js rename to test/GLOBALS.mjs index fa1af78..0ef5e2f 100644 --- a/test/GLOBALS.js +++ b/test/GLOBALS.mjs @@ -1,11 +1,11 @@ // SPDX-FileCopyrightText: 2024 Chris Duncan // SPDX-License-Identifier: GPL-3.0-or-later -import { Worker } from 'node:worker_threads' - -globalThis.Worker ??= Worker -globalThis.Worker.onmessage ??= (handler) => Worker.prototype.on('message', handler) -globalThis.onmessage ??= (handler) => EventTarget.prototype.addEventListener('message', handler) +if (globalThis.Worker == null) { + const { Worker } = await import('node:worker_threads') + Worker.prototype.addEventListener = Worker.prototype.on + globalThis.Worker = Worker +} if (globalThis.sessionStorage == null) { const _sessionStorage = {} diff --git a/test/create-wallet.test.mjs b/test/create-wallet.test.mjs index b9e576f..26b8e77 100644 --- a/test/create-wallet.test.mjs +++ b/test/create-wallet.test.mjs @@ -3,7 +3,7 @@ 'use strict' -import './GLOBALS.js' +import './GLOBALS.mjs' import { describe, it } from 'node:test' import { strict as assert } from 'assert' import { NANO_TEST_VECTORS, TREZOR_TEST_VECTORS } from './TEST_VECTORS.js' diff --git a/test/derive-accounts.test.mjs b/test/derive-accounts.test.mjs index 076a560..49a4a47 100644 --- a/test/derive-accounts.test.mjs +++ b/test/derive-accounts.test.mjs @@ -3,7 +3,7 @@ 'use strict' -import './GLOBALS.js' +import './GLOBALS.mjs' import { describe, it } from 'node:test' import { strict as assert } from 'assert' import { NANO_TEST_VECTORS } from './TEST_VECTORS.js' @@ -17,7 +17,7 @@ describe('derive child accounts from the same seed', async function () { const wallet = await Bip44Wallet.fromSeed(NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED) await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) - await it('should derive the first account from the given BIP-44 seed', async function () { + it('should derive the first account from the given BIP-44 seed', async function () { const accounts = await wallet.accounts() assert.equal(accounts.length, 1) @@ -26,7 +26,7 @@ describe('derive child accounts from the same seed', async function () { assert.equal(accounts[0].address, NANO_TEST_VECTORS.ADDRESS_0) }) - await it('should derive low indexed accounts from the given BIP-44 seed', async function () { + it('should derive low indexed accounts from the given BIP-44 seed', async function () { const accounts = await wallet.accounts(1, 2) assert.equal(accounts.length, 2) @@ -38,7 +38,7 @@ describe('derive child accounts from the same seed', async function () { assert.equal(accounts[1].address, NANO_TEST_VECTORS.ADDRESS_2) }) - await it('should derive high indexed accounts from the given seed', async function () { + it('should derive high indexed accounts from the given seed', async function () { const accounts = await wallet.accounts(0x70000000, 0x700000ff) assert.equal(accounts.length, 0x100) @@ -51,7 +51,7 @@ describe('derive child accounts from the same seed', async function () { } }) - await it('should derive accounts for a BLAKE2b wallet', async function () { + it('should derive accounts for a BLAKE2b wallet', async function () { const bwallet = await Blake2bWallet.create(NANO_TEST_VECTORS.PASSWORD) await bwallet.unlock(NANO_TEST_VECTORS.PASSWORD) const lowAccounts = await bwallet.accounts(0, 2) @@ -91,7 +91,7 @@ describe('Ledger device accounts', { skip: true }, async () => { }) describe('child key derivation performance', { skip }, async () => { - await it('performance test of BIP-44 ckd', async function () { + it('performance test of BIP-44 ckd', async function () { const wallet = await Bip44Wallet.create(NANO_TEST_VECTORS.PASSWORD) await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) @@ -100,7 +100,7 @@ describe('child key derivation performance', { skip }, async () => { assert.equal(accounts.length, 0x8000) }) - await it('performance test of BLAKE2b ckd', async function () { + it('performance test of BLAKE2b ckd', async function () { const wallet = await Blake2bWallet.create(NANO_TEST_VECTORS.PASSWORD) await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) diff --git a/test/import-wallet.test.mjs b/test/import-wallet.test.mjs index 91f1d07..ba3cbd5 100644 --- a/test/import-wallet.test.mjs +++ b/test/import-wallet.test.mjs @@ -3,7 +3,7 @@ 'use strict' -import './GLOBALS.js' +import './GLOBALS.mjs' import { describe, it } from 'node:test' import { strict as assert } from 'assert' import { BIP32_TEST_VECTORS, CUSTOM_TEST_VECTORS, NANO_TEST_VECTORS, TREZOR_TEST_VECTORS } from './TEST_VECTORS.js' @@ -139,8 +139,8 @@ describe('import wallet with test vectors test', () => { }) it('should successfully import a BLAKE2b wallet with Trezor test vectors', async () => { - const wallet = await Blake2bWallet.fromMnemonic(TREZOR_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.MNEMONIC_1) - await wallet.unlock(TREZOR_TEST_VECTORS.PASSWORD) + const wallet = await Blake2bWallet.fromMnemonic(NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.MNEMONIC_1) + await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) const accounts = await wallet.accounts(0, 1) assert.ok('mnemonic' in wallet) @@ -160,8 +160,8 @@ describe('import wallet with test vectors test', () => { }) it('should get identical BLAKE2b wallets when created with a seed versus with its derived mnemonic', async () => { - const wallet = await Blake2bWallet.fromSeed(TREZOR_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.ENTROPY_2) - await wallet.unlock(TREZOR_TEST_VECTORS.PASSWORD) + const wallet = await Blake2bWallet.fromSeed(NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.ENTROPY_2) + await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) const walletAccounts = await wallet.accounts() const walletAccount = walletAccounts[0] @@ -182,8 +182,8 @@ describe('import wallet with test vectors test', () => { }) it('should get identical BLAKE2b wallets when created with max entropy value', async () => { - const wallet = await Blake2bWallet.fromMnemonic(TREZOR_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.MNEMONIC_3) - await wallet.unlock(TREZOR_TEST_VECTORS.PASSWORD) + const wallet = await Blake2bWallet.fromMnemonic(NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.MNEMONIC_3) + await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) const accounts = await wallet.accounts() assert.ok('mnemonic' in wallet) diff --git a/test/lock-unlock-wallet.mjs b/test/lock-unlock-wallet.mjs index e09f1d9..a67089f 100644 --- a/test/lock-unlock-wallet.mjs +++ b/test/lock-unlock-wallet.mjs @@ -3,7 +3,7 @@ 'use strict' -import './GLOBALS.js' +import './GLOBALS.mjs' import { describe, it } from 'node:test' import { strict as assert } from 'assert' import { NANO_TEST_VECTORS, TREZOR_TEST_VECTORS } from './TEST_VECTORS.js'