]> zoso.dev Git - libnemo.git/commitdiff
Scrap worker pool in favor of one worker per wallet.
authorChris Duncan <chris@zoso.dev>
Sat, 16 Nov 2024 21:12:31 +0000 (13:12 -0800)
committerChris Duncan <chris@zoso.dev>
Sat, 16 Nov 2024 21:12:31 +0000 (13:12 -0800)
package.json
src/lib/ckd.ts
src/lib/pool.ts [deleted file]
src/lib/safe.ts
src/lib/wallet.ts
test/GLOBALS.mjs [moved from test/GLOBALS.js with 67% similarity]
test/create-wallet.test.mjs
test/derive-accounts.test.mjs
test/import-wallet.test.mjs
test/lock-unlock-wallet.mjs

index d46f9d6aeb2ed57c82ded6aac5c33dcf06dfda31..fc3ad005a10edd603d560a7d556d2d9ba172562d 100644 (file)
@@ -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"
index 3338d494051b9aea03b48e0485cd28403fec782e..377aacb032b51a449c09166385208be4c7384960 100644 (file)
@@ -1,11 +1,17 @@
 // SPDX-FileCopyrightText: 2024 Chris Duncan <chris@zoso.dev>
 // 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<Account>}
 */
-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 (file)
index 6d4690f..0000000
+++ /dev/null
@@ -1,65 +0,0 @@
-// SPDX-FileCopyrightText: 2024 Chris Duncan <chris@zoso.dev>
-// 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<any> {
-               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 })
-                       }
-               })
-       }
-}
index cebab59d2e6870d189ba7314e45a133fdbe3417c..8654756877403ba8931cf492a198465dec1d3144 100644 (file)
@@ -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<boolean>
        async put (name: string, passkey: string | CryptoKey, data: any): Promise<boolean> {
-               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<void>((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<string | null>(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
index bebb491c40d8283017f624ca9c0656af57c20ae1..8d8fa80e6785e6aa0034d99987c26a9aacb10b8b 100644 (file)
@@ -3,18 +3,12 @@
 \r
 import { Account } from './account.js'\r
 import { Bip39Mnemonic } from './bip39-mnemonic.js'\r
-import { nanoCKD } from './bip32-key-derivation.js'\r
 import { ADDRESS_GAP, SEED_LENGTH_BIP44, SEED_LENGTH_BLAKE2B } from './constants.js'\r
-import { bytes, dec } from './convert.js'\r
 import { Entropy } from './entropy.js'\r
-import { Pool } from './pool.js'\r
 import { Rpc } from './rpc.js'\r
 import { Safe } from './safe.js'\r
-import Tools from './tools.js'\r
 import type { Ledger } from './ledger.js'\r
 \r
-const ckdPool = new Pool('./ckd.js')\r
-\r
 /**\r
 * Represents a wallet containing numerous Nano accounts derived from a single\r
 * source, the form of which can vary based on the type of wallet. The Wallet\r
@@ -236,12 +230,14 @@ abstract class Wallet {
 */\r
 export class Bip44Wallet extends Wallet {\r
        static #isInternal: boolean = false\r
+       #worker: Worker\r
 \r
        constructor (seed: string, mnemonic?: Bip39Mnemonic, id?: string) {\r
                if (!Bip44Wallet.#isInternal) {\r
                        throw new Error(`Bip44Wallet cannot be instantiated directly. Use 'await Bip44Wallet.create()' instead.`)\r
                }\r
                super(seed, mnemonic, id)\r
+               this.#worker = new Worker(new URL('./ckd.js', import.meta.url), { type: 'module' })\r
                Bip44Wallet.#isInternal = false\r
        }\r
 \r
@@ -393,11 +389,16 @@ export class Bip44Wallet extends Wallet {
        * @returns {Promise<Account>}\r
        */\r
        async ckd (index: number): Promise<Account> {\r
-               const key = await ckdPool.work({ type: 'bip44', seed: this.seed, index })\r
-               if (typeof key !== 'string') {\r
-                       throw new TypeError('BIP-44 child key derivation returned invalid data')\r
-               }\r
-               return await Account.fromPrivateKey(key, index)\r
+               return new Promise(resolve => {\r
+                       this.#worker.addEventListener('message', (message) => {\r
+                               const key = message.data ?? message\r
+                               if (typeof key !== 'string') {\r
+                                       throw new TypeError('BIP-44 child key derivation returned invalid data')\r
+                               }\r
+                               Account.fromPrivateKey(key, index).then(resolve)\r
+                       }, { once: true })\r
+                       this.#worker.postMessage({ type: 'bip44', seed: this.seed, index })\r
+               })\r
        }\r
 }\r
 \r
@@ -419,12 +420,14 @@ export class Bip44Wallet extends Wallet {
 */\r
 export class Blake2bWallet extends Wallet {\r
        static #isInternal: boolean = false\r
+       #worker: Worker\r
 \r
        constructor (seed: string, mnemonic?: Bip39Mnemonic, id?: string) {\r
                if (!Blake2bWallet.#isInternal) {\r
                        throw new Error(`Blake2bWallet cannot be instantiated directly. Use 'await Blake2bWallet.create()' instead.`)\r
                }\r
                super(seed, mnemonic, id)\r
+               this.#worker = new Worker(new URL('./ckd.js', import.meta.url), { type: 'module' })\r
                Blake2bWallet.#isInternal = false\r
        }\r
 \r
@@ -538,11 +541,16 @@ export class Blake2bWallet extends Wallet {
        * @returns {Promise<Account>}\r
        */\r
        async ckd (index: number): Promise<Account> {\r
-               const key = await ckdPool.work({ type: 'blake2b', seed: this.seed, index })\r
-               if (typeof key !== 'string') {\r
-                       throw new TypeError('BLAKE2b child key derivation returned invalid data')\r
-               }\r
-               return await Account.fromPrivateKey(key, index)\r
+               return new Promise(resolve => {\r
+                       this.#worker.addEventListener('message', (message) => {\r
+                               const key = message.data ?? message\r
+                               if (typeof key !== 'string') {\r
+                                       throw new TypeError('BLAKE2b child key derivation returned invalid data')\r
+                               }\r
+                               Account.fromPrivateKey(key, index).then(resolve)\r
+                       }, { once: true })\r
+                       this.#worker.postMessage({ type: 'blake2b', seed: this.seed, index })\r
+               })\r
        }\r
 }\r
 \r
similarity index 67%
rename from test/GLOBALS.js
rename to test/GLOBALS.mjs
index fa1af780f9e16affe00fd4ae2d447ba583583d60..0ef5e2f4a89f15ef13573e6e046dfc6216fa79d8 100644 (file)
@@ -1,11 +1,11 @@
 // SPDX-FileCopyrightText: 2024 Chris Duncan <chris@zoso.dev>
 // 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 = {}
index b9e576f8431c27c87ee68725188b9a9a51650ddf..26b8e7773cf4221219a2c91638d7cac58f2292a1 100644 (file)
@@ -3,7 +3,7 @@
 \r
 'use strict'\r
 \r
-import './GLOBALS.js'\r
+import './GLOBALS.mjs'\r
 import { describe, it } from 'node:test'\r
 import { strict as assert } from 'assert'\r
 import { NANO_TEST_VECTORS, TREZOR_TEST_VECTORS } from './TEST_VECTORS.js'\r
index 076a560827b550d21354fb794185a96dbff273f5..49a4a478f703ece936272d69dcb6aa18720c21d5 100644 (file)
@@ -3,7 +3,7 @@
 \r
 'use strict'\r
 \r
-import './GLOBALS.js'\r
+import './GLOBALS.mjs'\r
 import { describe, it } from 'node:test'\r
 import { strict as assert } from 'assert'\r
 import { NANO_TEST_VECTORS } from './TEST_VECTORS.js'\r
@@ -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)\r
        await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
 \r
-       await it('should derive the first account from the given BIP-44 seed', async function () {\r
+       it('should derive the first account from the given BIP-44 seed', async function () {\r
                const accounts = await wallet.accounts()\r
 \r
                assert.equal(accounts.length, 1)\r
@@ -26,7 +26,7 @@ describe('derive child accounts from the same seed', async function () {
                assert.equal(accounts[0].address, NANO_TEST_VECTORS.ADDRESS_0)\r
        })\r
 \r
-       await it('should derive low indexed accounts from the given BIP-44 seed', async function () {\r
+       it('should derive low indexed accounts from the given BIP-44 seed', async function () {\r
                const accounts = await wallet.accounts(1, 2)\r
 \r
                assert.equal(accounts.length, 2)\r
@@ -38,7 +38,7 @@ describe('derive child accounts from the same seed', async function () {
                assert.equal(accounts[1].address, NANO_TEST_VECTORS.ADDRESS_2)\r
        })\r
 \r
-       await it('should derive high indexed accounts from the given seed', async function () {\r
+       it('should derive high indexed accounts from the given seed', async function () {\r
                const accounts = await wallet.accounts(0x70000000, 0x700000ff)\r
 \r
                assert.equal(accounts.length, 0x100)\r
@@ -51,7 +51,7 @@ describe('derive child accounts from the same seed', async function () {
                }\r
        })\r
 \r
-       await it('should derive accounts for a BLAKE2b wallet', async function () {\r
+       it('should derive accounts for a BLAKE2b wallet', async function () {\r
                const bwallet = await Blake2bWallet.create(NANO_TEST_VECTORS.PASSWORD)\r
                await bwallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
                const lowAccounts = await bwallet.accounts(0, 2)\r
@@ -91,7 +91,7 @@ describe('Ledger device accounts', { skip: true }, async () => {
 })\r
 \r
 describe('child key derivation performance', { skip }, async () => {\r
-       await it('performance test of BIP-44 ckd', async function () {\r
+       it('performance test of BIP-44 ckd', async function () {\r
                const wallet = await Bip44Wallet.create(NANO_TEST_VECTORS.PASSWORD)\r
                await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
 \r
@@ -100,7 +100,7 @@ describe('child key derivation performance', { skip }, async () => {
                assert.equal(accounts.length, 0x8000)\r
        })\r
 \r
-       await it('performance test of BLAKE2b ckd', async function () {\r
+       it('performance test of BLAKE2b ckd', async function () {\r
                const wallet = await Blake2bWallet.create(NANO_TEST_VECTORS.PASSWORD)\r
                await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
 \r
index 91f1d0723b456a752a298745c3bcf2808ebcbdd2..ba3cbd586375784156ebef0be8147cb6b937af21 100644 (file)
@@ -3,7 +3,7 @@
 \r
 'use strict'\r
 \r
-import './GLOBALS.js'\r
+import './GLOBALS.mjs'\r
 import { describe, it } from 'node:test'\r
 import { strict as assert } from 'assert'\r
 import { BIP32_TEST_VECTORS, CUSTOM_TEST_VECTORS, NANO_TEST_VECTORS, TREZOR_TEST_VECTORS } from './TEST_VECTORS.js'\r
@@ -139,8 +139,8 @@ describe('import wallet with test vectors test', () => {
        })\r
 \r
        it('should successfully import a BLAKE2b wallet with Trezor test vectors', async () => {\r
-               const wallet = await Blake2bWallet.fromMnemonic(TREZOR_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.MNEMONIC_1)\r
-               await wallet.unlock(TREZOR_TEST_VECTORS.PASSWORD)\r
+               const wallet = await Blake2bWallet.fromMnemonic(NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.MNEMONIC_1)\r
+               await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
                const accounts = await wallet.accounts(0, 1)\r
 \r
                assert.ok('mnemonic' in wallet)\r
@@ -160,8 +160,8 @@ describe('import wallet with test vectors test', () => {
        })\r
 \r
        it('should get identical BLAKE2b wallets when created with a seed versus with its derived mnemonic', async () => {\r
-               const wallet = await Blake2bWallet.fromSeed(TREZOR_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.ENTROPY_2)\r
-               await wallet.unlock(TREZOR_TEST_VECTORS.PASSWORD)\r
+               const wallet = await Blake2bWallet.fromSeed(NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.ENTROPY_2)\r
+               await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
                const walletAccounts = await wallet.accounts()\r
                const walletAccount = walletAccounts[0]\r
 \r
@@ -182,8 +182,8 @@ describe('import wallet with test vectors test', () => {
        })\r
 \r
        it('should get identical BLAKE2b wallets when created with max entropy value', async () => {\r
-               const wallet = await Blake2bWallet.fromMnemonic(TREZOR_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.MNEMONIC_3)\r
-               await wallet.unlock(TREZOR_TEST_VECTORS.PASSWORD)\r
+               const wallet = await Blake2bWallet.fromMnemonic(NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.MNEMONIC_3)\r
+               await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
                const accounts = await wallet.accounts()\r
 \r
                assert.ok('mnemonic' in wallet)\r
index e09f1d908eec4d2b7ea26fa819ad6cd411b1d84d..a67089fed3b6cbe090e0584e6587bcd84c4f3d48 100644 (file)
@@ -3,7 +3,7 @@
 \r
 'use strict'\r
 \r
-import './GLOBALS.js'\r
+import './GLOBALS.mjs'\r
 import { describe, it } from 'node:test'\r
 import { strict as assert } from 'assert'\r
 import { NANO_TEST_VECTORS, TREZOR_TEST_VECTORS } from './TEST_VECTORS.js'\r