]> git.codecow.com Git - libnemo.git/commitdiff
Update pools to assign using string metadata as headers and actual data as bytes...
authorChris Duncan <chris@zoso.dev>
Sat, 12 Jul 2025 07:33:09 +0000 (00:33 -0700)
committerChris Duncan <chris@zoso.dev>
Sat, 12 Jul 2025 07:33:09 +0000 (00:33 -0700)
src/lib/account.ts
src/lib/bip39-mnemonic.ts
src/lib/block.ts
src/lib/pool.ts
src/lib/wallets/bip44-wallet.ts
src/lib/wallets/blake2b-wallet.ts
src/lib/wallets/wallet.ts
src/lib/workers/bip44-ckd.ts
src/lib/workers/nano-nacl.ts
src/lib/workers/safe.ts

index 5a641d33cdb36e860c5baa40f08a3d5f1efdecb9..fa6a429176a22398e1e6f01c62d5b50d78bfd35b 100644 (file)
@@ -136,11 +136,8 @@ export class Account {
                this.#validateKey(privateKey)\r
                let publicKey: string\r
                try {\r
-                       const result = (await this.#poolNanoNaCl.assign({\r
-                               method: 'convert',\r
-                               privateKey: bytes.toArray(privateKey)\r
-                       }))[0]\r
-                       publicKey = result.publicKey\r
+                       const result = await this.#poolNanoNaCl.assign({ method: 'convert' }, privateKey)\r
+                       publicKey = result.publicKey[0]\r
                } catch (err) {\r
                        throw new Error(`Failed to derive public key from private key`, { cause: err })\r
                }\r
@@ -161,17 +158,13 @@ export class Account {
                        throw new Error('Failed to lock account')\r
                }\r
                try {\r
-                       const data: { id: string, privateKey: Uint8Array } = {\r
-                               id: this.#pub,\r
-                               privateKey: this.#prv\r
-                       }\r
-                       const response = (await Account.#poolSafe.assign({\r
+                       const headers = {\r
                                method: 'set',\r
                                name: this.#pub,\r
-                               password,\r
-                               data\r
-                       }))[0]\r
-                       const success = response?.result\r
+                               id: this.#pub\r
+                       }\r
+                       const response = await Account.#poolSafe.assign(headers, this.#prv, password)\r
+                       const success = response?.result[0]\r
                        if (!success) {\r
                                throw null\r
                        }\r
@@ -230,12 +223,12 @@ export class Account {
                        throw new Error('Failed to unlock account')\r
                }\r
                try {\r
-                       const response = (await Account.#poolSafe.assign({\r
+                       const headers = {\r
                                method: 'get',\r
-                               name: this.#pub,\r
-                               password\r
-                       }))[0]\r
-                       const { id, privateKey } = response?.result\r
+                               name: this.#pub\r
+                       }\r
+                       const response = await Account.#poolSafe.assign(headers, password)\r
+                       const { id, privateKey } = response?.result[0]\r
                        if (id == null || id !== this.#pub) {\r
                                throw null\r
                        }\r
index 8bef511896849bd9301441612e4894d1f196fd8b..d51c7a8efd1cb419ea3978b8b60076d8954362b2 100644 (file)
@@ -13,8 +13,8 @@ const { subtle } = globalThis.crypto
 */\r
 export class Bip39Mnemonic {\r
        static #isInternal: boolean = false\r
-       #bip44Seed: string = ''\r
-       #blake2bSeed: string = ''\r
+       #bip44Seed: Uint8Array | null = null\r
+       #blake2bSeed: Uint8Array | null = null\r
        #phrase: string = ''\r
        get phrase (): string { return this.#phrase.normalize('NFKD') }\r
 \r
@@ -129,6 +129,7 @@ export class Bip39Mnemonic {
                return true\r
        }\r
 \r
+       async toBip39Seed (passphrase: string): Promise<Uint8Array>\r
        /**\r
        * Converts the mnemonic phrase to a BIP-39 seed.\r
        *\r
@@ -138,8 +139,9 @@ export class Bip39Mnemonic {
        * @param {string} [passphrase=''] - Used as the PBKDF2 salt. Default: ""\r
        * @returns {string} Hexadecimal seed\r
        */\r
-       async toBip39Seed (passphrase: string): Promise<string> {\r
-               if (this.#blake2bSeed === '') {\r
+       async toBip39Seed (passphrase: string, format: 'hex'): Promise<string>\r
+       async toBip39Seed (passphrase: string, format?: 'hex'): Promise<string | Uint8Array> {\r
+               if (this.#bip44Seed == null) {\r
                        if (passphrase == null || typeof passphrase !== 'string') {\r
                                passphrase = ''\r
                        }\r
@@ -160,20 +162,22 @@ export class Bip39Mnemonic {
                        }\r
                        const seedKey = await subtle.deriveKey(algorithm, phraseKey, derivedKeyType, true, ['sign'])\r
                        const seedBuffer = await subtle.exportKey('raw', seedKey)\r
-                       const seedBytes = new Uint8Array(seedBuffer)\r
-                       const seed = bytes.toHex(seedBytes)\r
-                       this.#bip44Seed = seed\r
+                       this.#bip44Seed = new Uint8Array(seedBuffer)\r
                }\r
-               return this.#bip44Seed\r
+               return format === 'hex'\r
+                       ? bytes.toHex(this.#bip44Seed)\r
+                       : this.#bip44Seed\r
        }\r
 \r
+       async toBlake2bSeed (): Promise<Uint8Array>\r
        /**\r
        * Converts the mnemonic phrase to a BLAKE2b seed.\r
        *\r
        * @returns {string} Hexadecimal seed\r
        */\r
-       async toBlake2bSeed (): Promise<string> {\r
-               if (this.#blake2bSeed === '') {\r
+       async toBlake2bSeed (format: 'hex'): Promise<string>\r
+       async toBlake2bSeed (format?: 'hex'): Promise<string | Uint8Array> {\r
+               if (this.#blake2bSeed == null) {\r
                        const wordArray = this.phrase.split(' ')\r
                        const bits = wordArray.map((w: string) => {\r
                                const wordIndex = Bip39Words.indexOf(w)\r
@@ -189,9 +193,11 @@ export class Bip39Mnemonic {
                        if (entropyBytes == null) {\r
                                throw new Error('Invalid mnemonic phrase')\r
                        }\r
-                       this.#blake2bSeed = bytes.toHex(Uint8Array.from(entropyBytes))\r
+                       this.#blake2bSeed = new Uint8Array(entropyBytes)\r
                }\r
-               return this.#blake2bSeed\r
+               return format === 'hex'\r
+                       ? bytes.toHex(this.#blake2bSeed)\r
+                       : this.#blake2bSeed\r
        }\r
 }\r
 \r
index cf99ed37cb9f32affb2340d2228ebb440b561250..7b7e4d32cff9f5fc45484818c701c221f742bb23 100644 (file)
@@ -148,12 +148,12 @@ abstract class Block {
                        }
                        const account = await Account.fromPrivateKey(key)
                        try {
-                               const result = (await Block.#poolNanoNaCl.assign({
+                               const headers = {
                                        method: 'detached',
-                                       msg: hex.toArray(this.hash),
-                                       privateKey: hex.toArray(`${account.privateKey}`)
-                               }))[0]
-                               this.signature = result.signature
+                                       msg: this.hash
+                               }
+                               const result = await Block.#poolNanoNaCl.assign(headers, hex.toBytes(account.privateKey))
+                               this.signature = result.signature[0]
                        } catch (err) {
                                throw new Error(`Failed to sign block`, { cause: err })
                        }
@@ -201,13 +201,14 @@ abstract class Block {
                        throw new Error('Provide a key for block signature verification.')
                }
                try {
-                       const result = (await Block.#poolNanoNaCl.assign({
+                       const headers = {
                                method: 'verify',
-                               msg: hex.toArray(this.hash),
-                               signature: hex.toArray(this.signature ?? ''),
-                               publicKey: hex.toArray(key)
-                       }))[0]
-                       return result.isVerified
+                               msg: this.hash,
+                               signature: this.signature ?? '',
+                               publicKey: key
+                       }
+                       const result = await Block.#poolNanoNaCl.assign(headers)
+                       return result.isVerified[0]
                } catch (err) {
                        throw new Error(`Failed to derive public key from private key`, { cause: err })
                }
index ffe1b82e945c3a1b1fde399d1b003cdf00524eab..d72fb64e7740699fa0519bdee1f0bd49f3170783 100644 (file)
@@ -1,11 +1,16 @@
 // SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
 // SPDX-License-Identifier: GPL-3.0-or-later
 
+export type Header = {
+       [key: string]: number | string
+}
+
 type Job = {
        id: number
        reject: (value: any) => void
        resolve: (value: any) => void
-       data: any
+       headers: Header
+       data: (ArrayBuffer | Uint8Array)[]
        results: any[]
 }
 
@@ -38,12 +43,12 @@ export class Pool {
                return n
        }
 
-       async assign (data: any): Promise<any> {
-               if (!(data instanceof ArrayBuffer || Array.isArray(data))) data = [data]
+       async assign (headers: Header, ...data: (ArrayBuffer | Uint8Array)[]): Promise<any> {
                return new Promise((resolve, reject) => {
                        const job: Job = {
                                id: performance.now(),
                                results: [],
+                               headers,
                                data,
                                resolve,
                                reject
@@ -92,7 +97,7 @@ export class Pool {
        #assign (thread: Thread, job: Job): void {
                if (job.data instanceof ArrayBuffer && job.data.byteLength > 0) {
                        thread.job = job
-                       thread.worker.postMessage({ buffer: job.data }, [job.data])
+                       thread.worker.postMessage({ headers: job.headers, data: job.data }, [job.data])
                } else {
                        const chunk: number = 1 + (job.data.length / this.threadsIdle)
                        const next = job.data.slice(0, chunk)
@@ -101,7 +106,7 @@ export class Pool {
                        if (next?.length > 0) {
                                const buffer = new TextEncoder().encode(JSON.stringify(next)).buffer
                                thread.job = job
-                               thread.worker.postMessage({ buffer }, [buffer])
+                               thread.worker.postMessage({ headers: job.headers, data: buffer }, [buffer])
                        }
                }
        }
@@ -148,7 +153,7 @@ export class Pool {
 * `listen()` function. Finally, they must override the implementation of the
 * `work()` function. See the documentation of those functions for details.
 */
-export class WorkerInterface {
+export abstract class WorkerInterface {
        /**
        * Processes data through a worker.
        *
@@ -156,19 +161,23 @@ export class WorkerInterface {
        * function signature and providing their own processing call in the try-catch
        * block.
        *
-       * @param {any[]} data - Array of data to process
-       * @returns Promise for that data after being processed
+       * @param {Header} headers - Flat object of header data
+       * @param {any[]} data - Transferred buffer of data to process
+       * @returns Promise for processed data
        */
-       static async work (data: any[]): Promise<any[]> {
+       static async work (headers: Header, body: any[]): Promise<any[]> {
                return new Promise(async (resolve, reject): Promise<void> => {
-                       for (let d of data) {
+                       const { example } = headers
+                       const results = []
+                       for (let buf of body) {
                                try {
-                                       d = await d
+                                       const b = JSON.parse(new TextDecoder().decode(buf))
+                                       results.push(await b(example))
                                } catch (err) {
                                        reject(err)
                                }
                        }
-                       resolve(data)
+                       resolve(results)
                })
        }
 
@@ -190,21 +199,20 @@ export class WorkerInterface {
        * Extending classes must call this in a static initialization block:
        * ```
        * static {
-       *       Pow.listen()
+       *       Extension.listen()
        * }
        * ```
        */
        static listen (): void {
                addEventListener('message', (message: any): void => {
-                       const { name, buffer } = message.data
+                       const { name, headers, data } = 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))
-                               this.work(data).then(this.report).catch(this.report)
+                               this.work(headers, data).then(this.report).catch(this.report)
                        }
                })
        }
index 2632535ffe00fc798e3574b14c7d6a730cc9a4fc..ce9b84f00e9619f61a897a8f8d294071782cf87e 100644 (file)
@@ -4,6 +4,7 @@
 import { KeyPair, Wallet } from './wallet'\r
 import { Bip39Mnemonic } from '#src/lib/bip39-mnemonic.js'\r
 import { SEED_LENGTH_BIP44 } from '#src/lib/constants.js'\r
+import { hex } from '#src/lib/convert.js'\r
 import { Entropy } from '#src/lib/entropy.js'\r
 import { Pool } from '#src/lib/pool.js'\r
 import { Bip44CkdWorker } from '#workers'\r
@@ -39,7 +40,7 @@ export class Bip44Wallet extends Wallet {
                        throw new Error(`Bip44Wallet cannot be instantiated directly. Use 'await Bip44Wallet.create()' instead.`)\r
                }\r
                Bip44Wallet.#isInternal = false\r
-               super(id, seed, mnemonic)\r
+               super(id, hex.toBytes(seed), mnemonic)\r
                Bip44Wallet.#poolBip44Ckd ??= new Pool(Bip44CkdWorker)\r
        }\r
 \r
@@ -96,7 +97,7 @@ export class Bip44Wallet extends Wallet {
                        const id = await Entropy.create()\r
                        const e = await Entropy.import(entropy)\r
                        const m = await Bip39Mnemonic.fromEntropy(e.hex)\r
-                       const s = await m.toBip39Seed(salt)\r
+                       const s = await m.toBip39Seed(salt, 'hex')\r
                        Bip44Wallet.#isInternal = true\r
                        wallet = new this(id, s, m)\r
                } catch (err) {\r
@@ -134,7 +135,7 @@ export class Bip44Wallet extends Wallet {
                try {\r
                        const id = await Entropy.create()\r
                        const m = await Bip39Mnemonic.fromPhrase(mnemonic)\r
-                       const s = await m.toBip39Seed(salt)\r
+                       const s = await m.toBip39Seed(salt, 'hex')\r
                        Bip44Wallet.#isInternal = true\r
                        wallet = new this(id, s, m)\r
                } catch (err) {\r
@@ -210,8 +211,8 @@ export class Bip44Wallet extends Wallet {
        */\r
        async ckd (indexes: number[]): Promise<KeyPair[]> {\r
                const data: any = []\r
-               indexes.forEach(i => data.push({ seed: this.seed, index: i }))\r
-               const privateKeys: KeyPair[] = await Bip44Wallet.#poolBip44Ckd.assign(data)\r
+               indexes.forEach(i => data.push({ index: i }))\r
+               const privateKeys: KeyPair[] = await Bip44Wallet.#poolBip44Ckd.assign(data, this.seed)\r
                for (let i = 0; i < privateKeys.length; i++) {\r
                        if (privateKeys[i].privateKey == null) {\r
                                throw new Error('Failed to derive private keys')\r
index c2a405d8b60ea65e01b65890123d0d024ecc2c61..ba5ef98e6976887eb8fa86045244724dd4a711b2 100644 (file)
@@ -5,7 +5,7 @@ import { KeyPair, Wallet } from './wallet'
 import { Bip39Mnemonic } from '#src/lib/bip39-mnemonic.js'\r
 import { Blake2b } from '#src/lib/blake2b.js'\r
 import { SEED_LENGTH_BLAKE2B } from '#src/lib/constants.js'\r
-import { hex } from '#src/lib/convert.js'\r
+import { bytes, hex } from '#src/lib/convert.js'\r
 import { Entropy } from '#src/lib/entropy.js'\r
 \r
 /**\r
@@ -27,7 +27,7 @@ import { Entropy } from '#src/lib/entropy.js'
 export class Blake2bWallet extends Wallet {\r
        static #isInternal: boolean = false\r
 \r
-       constructor (id: Entropy, seed: string, mnemonic?: Bip39Mnemonic) {\r
+       constructor (id: Entropy, seed?: Uint8Array, mnemonic?: Bip39Mnemonic) {\r
                if (!Blake2bWallet.#isInternal) {\r
                        throw new Error(`Blake2bWallet cannot be instantiated directly. Use 'await Blake2bWallet.create()' instead.`)\r
                }\r
@@ -85,8 +85,9 @@ export class Blake2bWallet extends Wallet {
                if (!/^[0-9a-fA-F]+$/i.test(seed)) {\r
                        throw new Error('Seed contains invalid hexadecimal characters.')\r
                }\r
+\r
                const id = await Entropy.create()\r
-               const s = seed\r
+               const s = hex.toBytes(seed)\r
                const m = await Bip39Mnemonic.fromEntropy(seed)\r
                Blake2bWallet.#isInternal = true\r
                const wallet = new this(id, s, m)\r
@@ -120,7 +121,7 @@ export class Blake2bWallet extends Wallet {
                try {\r
                        const id = await Entropy.create()\r
                        const m = await Bip39Mnemonic.fromPhrase(mnemonic)\r
-                       const s = await m.toBlake2bSeed()\r
+                       const s = (await m.toBlake2bSeed())\r
                        Blake2bWallet.#isInternal = true\r
                        wallet = new this(id, s, m)\r
                } catch (err) {\r
@@ -146,7 +147,7 @@ export class Blake2bWallet extends Wallet {
                        throw new TypeError('Wallet ID is required to restore')\r
                }\r
                Blake2bWallet.#isInternal = true\r
-               return new this(await Entropy.import(id), '')\r
+               return new this(await Entropy.import(id))\r
        }\r
 \r
        /**\r
@@ -158,7 +159,7 @@ export class Blake2bWallet extends Wallet {
        async ckd (indexes: number[]): Promise<KeyPair[]> {\r
                const results = indexes.map(index => {\r
                        const indexHex = index.toString(16).padStart(8, '0').toUpperCase()\r
-                       const inputHex = `${this.seed}${indexHex}`.padStart(72, '0')\r
+                       const inputHex = `${bytes.toHex(this.seed)}${indexHex}`.padStart(72, '0')\r
                        const inputBytes = hex.toBytes(inputHex)\r
                        const privateKey: string = new Blake2b(32).update(inputBytes).digest('hex')\r
                        return { privateKey, index }\r
index 29f64b6ce1fbc28e27ccdd5fd80a799e4a80484e..781362722c29238f28032953b942b8597b1b224c 100644 (file)
@@ -4,11 +4,11 @@
 import { Account, AccountList } from '#src/lib/account.js'\r
 import { Bip39Mnemonic } from '#src/lib/bip39-mnemonic.js'\r
 import { ADDRESS_GAP } from '#src/lib/constants.js'\r
+import { bytes, hex, utf8 } from '#src/lib/convert.js'\r
 import { Entropy } from '#src/lib/entropy.js'\r
 import { Pool } from '#src/lib/pool.js'\r
 import { Rpc } from '#src/lib/rpc.js'\r
 import { SafeWorker } from '#workers'\r
-import { utf8 } from '../convert'\r
 \r
 export type KeyPair = {\r
        publicKey?: string,\r
@@ -32,22 +32,22 @@ export abstract class Wallet {
        #id: Entropy\r
        #locked: boolean = true\r
        #m: Bip39Mnemonic | null\r
-       #s: string | null\r
+       #s: Uint8Array\r
 \r
        get id () { return this.#id.hex }\r
        get isLocked () { return this.#locked }\r
        get isUnlocked () { return !this.#locked }\r
        get mnemonic () { return this.#m instanceof Bip39Mnemonic ? this.#m.phrase : '' }\r
-       get seed () { return typeof this.#s === 'string' ? this.#s : '' }\r
+       get seed () { return this.#s }\r
 \r
-       constructor (id: Entropy, seed?: string, mnemonic?: Bip39Mnemonic) {\r
+       constructor (id: Entropy, seed?: Uint8Array, mnemonic?: Bip39Mnemonic) {\r
                if (this.constructor === Wallet) {\r
                        throw new Error('Wallet is an abstract class and cannot be instantiated directly.')\r
                }\r
                this.#accounts = new AccountList()\r
                this.#id = id\r
                this.#m = mnemonic ?? null\r
-               this.#s = seed ?? null\r
+               this.#s = seed ?? new Uint8Array(32)\r
        }\r
 \r
        /**\r
@@ -138,7 +138,7 @@ export abstract class Wallet {
                        i++\r
                }\r
                this.#m = null\r
-               this.#s = null\r
+               this.#s.fill(0)\r
                await Wallet.#poolSafe.assign({\r
                        method: 'destroy',\r
                        name: this.id\r
@@ -160,24 +160,13 @@ export abstract class Wallet {
                        throw new Error('Failed to unlock wallet')\r
                }\r
                try {\r
-                       const data: { id: string, mnemonic: string | null, seed: string | null } = {\r
-                               id: this.id,\r
-                               mnemonic: null,\r
-                               seed: null\r
-                       }\r
-                       if (this.#m instanceof Bip39Mnemonic) {\r
-                               data.mnemonic = this.#m.phrase\r
-                       }\r
-                       if (typeof this.#s === 'string') {\r
-                               data.seed = this.#s\r
-                       }\r
-                       const response = (await Wallet.#poolSafe.assign({\r
+                       const headers = {\r
                                method: 'set',\r
                                name: this.id,\r
-                               password,\r
-                               data\r
-                       }))[0]\r
-                       const success = response?.result\r
+                               id: this.id,\r
+                       }\r
+                       const response = await Wallet.#poolSafe.assign(headers, password, utf8.toBytes(this.#m?.phrase ?? ''), this.#s)\r
+                       const success = response?.result[0]\r
                        if (!success) {\r
                                throw null\r
                        }\r
@@ -192,7 +181,7 @@ export abstract class Wallet {
                        password.fill(0)\r
                }\r
                this.#m = null\r
-               this.#s = null\r
+               this.#s.fill(0)\r
                this.#locked = true\r
                return true\r
        }\r
@@ -238,20 +227,23 @@ export abstract class Wallet {
                        throw new Error('Failed to unlock wallet')\r
                }\r
                try {\r
-                       const response = (await Wallet.#poolSafe.assign({\r
+                       const headers = {\r
                                method: 'get',\r
-                               name: this.id,\r
-                               password\r
-                       }))[0]\r
-                       const { id, mnemonic, seed } = response?.result\r
+                               name: this.id\r
+                       }\r
+                       const response = await Wallet.#poolSafe.assign(headers,\r
+                               password)\r
+                       let { id, mnemonic, seed } = response?.result[0]\r
                        if (id == null || id !== this.id) {\r
                                throw null\r
                        }\r
                        if (mnemonic != null) {\r
                                this.#m = await Bip39Mnemonic.fromPhrase(mnemonic)\r
+                               mnemonic = null\r
                        }\r
                        if (seed != null) {\r
-                               this.#s = seed\r
+                               this.#s.set(hex.toBytes(seed))\r
+                               seed = null\r
                        }\r
                        const promises = []\r
                        for (const account of this.#accounts) {\r
index 1feff73143099cb44658831218a18e074e921f6f..563093c2f6824e625213ee05bfa50c6928360c9f 100644 (file)
@@ -17,7 +17,7 @@ export class Bip44Ckd extends WorkerInterface {
                Bip44Ckd.listen()
        }
 
-       static async work (data: any[]): Promise<any[]> {
+       static async work (headers: { [key: string]: string | number }, data: any[]): Promise<any[]> {
                for (const d of data) {
                        try {
                                if (d.coin != null && d.coin !== this.BIP44_PURPOSE) {
index 0b2dabf5ba5b2dbd8d5651b7053c2f75702c8230..122228f7bbee85a5555ab5f4fea865d3cdc70c1f 100644 (file)
@@ -23,7 +23,7 @@ export class NanoNaCl extends WorkerInterface {
                NanoNaCl.listen()\r
        }\r
 \r
-       static async work (data: any[]): Promise<any[]> {\r
+       static async work (headers: { [key: string]: string | number }, data: any[]): Promise<any[]> {\r
                return new Promise(async (resolve, reject): Promise<void> => {\r
                        for (let d of data) {\r
                                try {\r
index e043cca4702151497f22bac1405bdab1064b83d8..159f4e7d1eca206c28ad34d9c4db87a746d57efd 100644 (file)
@@ -38,11 +38,12 @@ export class Safe extends WorkerInterface {
                Safe.listen()
        }
 
-       static async work (data: any[]): Promise<any[]> {
+       static async work (headers: { [key: string]: string }, data: any[]): Promise<any[]> {
                this.#storage = await this.#open(this.DB_NAME)
+               const { method, name } = headers
                const results: SafeOutput[] = []
                for (const d of data) {
-                       const { name, method, password, data } = d as SafeInput
+                       const { password, data } = d as SafeInput
                        let result
                        try {
                                const passwordBytes = obj.toBytes(password ?? [])