From: Chris Duncan Date: Sat, 12 Jul 2025 07:33:09 +0000 (-0700) Subject: Update pools to assign using string metadata as headers and actual data as bytes... X-Git-Tag: v0.10.5~57^2~24 X-Git-Url: https://git.codecow.com/?a=commitdiff_plain;h=c28ed4dbba5427e4275d5914204b37bfff74c89e;p=libnemo.git Update pools to assign using string metadata as headers and actual data as bytes so they can be transferred to the worker. --- diff --git a/src/lib/account.ts b/src/lib/account.ts index 5a641d3..fa6a429 100644 --- a/src/lib/account.ts +++ b/src/lib/account.ts @@ -136,11 +136,8 @@ export class Account { this.#validateKey(privateKey) let publicKey: string try { - const result = (await this.#poolNanoNaCl.assign({ - method: 'convert', - privateKey: bytes.toArray(privateKey) - }))[0] - publicKey = result.publicKey + const result = await this.#poolNanoNaCl.assign({ method: 'convert' }, privateKey) + publicKey = result.publicKey[0] } catch (err) { throw new Error(`Failed to derive public key from private key`, { cause: err }) } @@ -161,17 +158,13 @@ export class Account { throw new Error('Failed to lock account') } try { - const data: { id: string, privateKey: Uint8Array } = { - id: this.#pub, - privateKey: this.#prv - } - const response = (await Account.#poolSafe.assign({ + const headers = { method: 'set', name: this.#pub, - password, - data - }))[0] - const success = response?.result + id: this.#pub + } + const response = await Account.#poolSafe.assign(headers, this.#prv, password) + const success = response?.result[0] if (!success) { throw null } @@ -230,12 +223,12 @@ export class Account { throw new Error('Failed to unlock account') } try { - const response = (await Account.#poolSafe.assign({ + const headers = { method: 'get', - name: this.#pub, - password - }))[0] - const { id, privateKey } = response?.result + name: this.#pub + } + const response = await Account.#poolSafe.assign(headers, password) + const { id, privateKey } = response?.result[0] if (id == null || id !== this.#pub) { throw null } diff --git a/src/lib/bip39-mnemonic.ts b/src/lib/bip39-mnemonic.ts index 8bef511..d51c7a8 100644 --- a/src/lib/bip39-mnemonic.ts +++ b/src/lib/bip39-mnemonic.ts @@ -13,8 +13,8 @@ const { subtle } = globalThis.crypto */ export class Bip39Mnemonic { static #isInternal: boolean = false - #bip44Seed: string = '' - #blake2bSeed: string = '' + #bip44Seed: Uint8Array | null = null + #blake2bSeed: Uint8Array | null = null #phrase: string = '' get phrase (): string { return this.#phrase.normalize('NFKD') } @@ -129,6 +129,7 @@ export class Bip39Mnemonic { return true } + async toBip39Seed (passphrase: string): Promise /** * Converts the mnemonic phrase to a BIP-39 seed. * @@ -138,8 +139,9 @@ export class Bip39Mnemonic { * @param {string} [passphrase=''] - Used as the PBKDF2 salt. Default: "" * @returns {string} Hexadecimal seed */ - async toBip39Seed (passphrase: string): Promise { - if (this.#blake2bSeed === '') { + async toBip39Seed (passphrase: string, format: 'hex'): Promise + async toBip39Seed (passphrase: string, format?: 'hex'): Promise { + if (this.#bip44Seed == null) { if (passphrase == null || typeof passphrase !== 'string') { passphrase = '' } @@ -160,20 +162,22 @@ export class Bip39Mnemonic { } const seedKey = await subtle.deriveKey(algorithm, phraseKey, derivedKeyType, true, ['sign']) const seedBuffer = await subtle.exportKey('raw', seedKey) - const seedBytes = new Uint8Array(seedBuffer) - const seed = bytes.toHex(seedBytes) - this.#bip44Seed = seed + this.#bip44Seed = new Uint8Array(seedBuffer) } - return this.#bip44Seed + return format === 'hex' + ? bytes.toHex(this.#bip44Seed) + : this.#bip44Seed } + async toBlake2bSeed (): Promise /** * Converts the mnemonic phrase to a BLAKE2b seed. * * @returns {string} Hexadecimal seed */ - async toBlake2bSeed (): Promise { - if (this.#blake2bSeed === '') { + async toBlake2bSeed (format: 'hex'): Promise + async toBlake2bSeed (format?: 'hex'): Promise { + if (this.#blake2bSeed == null) { const wordArray = this.phrase.split(' ') const bits = wordArray.map((w: string) => { const wordIndex = Bip39Words.indexOf(w) @@ -189,9 +193,11 @@ export class Bip39Mnemonic { if (entropyBytes == null) { throw new Error('Invalid mnemonic phrase') } - this.#blake2bSeed = bytes.toHex(Uint8Array.from(entropyBytes)) + this.#blake2bSeed = new Uint8Array(entropyBytes) } - return this.#blake2bSeed + return format === 'hex' + ? bytes.toHex(this.#blake2bSeed) + : this.#blake2bSeed } } diff --git a/src/lib/block.ts b/src/lib/block.ts index cf99ed3..7b7e4d3 100644 --- a/src/lib/block.ts +++ b/src/lib/block.ts @@ -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 }) } diff --git a/src/lib/pool.ts b/src/lib/pool.ts index ffe1b82..d72fb64 100644 --- a/src/lib/pool.ts +++ b/src/lib/pool.ts @@ -1,11 +1,16 @@ // SPDX-FileCopyrightText: 2025 Chris Duncan // 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 { - if (!(data instanceof ArrayBuffer || Array.isArray(data))) data = [data] + async assign (headers: Header, ...data: (ArrayBuffer | Uint8Array)[]): Promise { 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 { + static async work (headers: Header, body: any[]): Promise { return new Promise(async (resolve, reject): Promise => { - 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) } }) } diff --git a/src/lib/wallets/bip44-wallet.ts b/src/lib/wallets/bip44-wallet.ts index 2632535..ce9b84f 100644 --- a/src/lib/wallets/bip44-wallet.ts +++ b/src/lib/wallets/bip44-wallet.ts @@ -4,6 +4,7 @@ import { KeyPair, Wallet } from './wallet' import { Bip39Mnemonic } from '#src/lib/bip39-mnemonic.js' import { SEED_LENGTH_BIP44 } from '#src/lib/constants.js' +import { hex } from '#src/lib/convert.js' import { Entropy } from '#src/lib/entropy.js' import { Pool } from '#src/lib/pool.js' import { Bip44CkdWorker } from '#workers' @@ -39,7 +40,7 @@ export class Bip44Wallet extends Wallet { throw new Error(`Bip44Wallet cannot be instantiated directly. Use 'await Bip44Wallet.create()' instead.`) } Bip44Wallet.#isInternal = false - super(id, seed, mnemonic) + super(id, hex.toBytes(seed), mnemonic) Bip44Wallet.#poolBip44Ckd ??= new Pool(Bip44CkdWorker) } @@ -96,7 +97,7 @@ export class Bip44Wallet extends Wallet { const id = await Entropy.create() const e = await Entropy.import(entropy) const m = await Bip39Mnemonic.fromEntropy(e.hex) - const s = await m.toBip39Seed(salt) + const s = await m.toBip39Seed(salt, 'hex') Bip44Wallet.#isInternal = true wallet = new this(id, s, m) } catch (err) { @@ -134,7 +135,7 @@ export class Bip44Wallet extends Wallet { try { const id = await Entropy.create() const m = await Bip39Mnemonic.fromPhrase(mnemonic) - const s = await m.toBip39Seed(salt) + const s = await m.toBip39Seed(salt, 'hex') Bip44Wallet.#isInternal = true wallet = new this(id, s, m) } catch (err) { @@ -210,8 +211,8 @@ export class Bip44Wallet extends Wallet { */ async ckd (indexes: number[]): Promise { const data: any = [] - indexes.forEach(i => data.push({ seed: this.seed, index: i })) - const privateKeys: KeyPair[] = await Bip44Wallet.#poolBip44Ckd.assign(data) + indexes.forEach(i => data.push({ index: i })) + const privateKeys: KeyPair[] = await Bip44Wallet.#poolBip44Ckd.assign(data, this.seed) for (let i = 0; i < privateKeys.length; i++) { if (privateKeys[i].privateKey == null) { throw new Error('Failed to derive private keys') diff --git a/src/lib/wallets/blake2b-wallet.ts b/src/lib/wallets/blake2b-wallet.ts index c2a405d..ba5ef98 100644 --- a/src/lib/wallets/blake2b-wallet.ts +++ b/src/lib/wallets/blake2b-wallet.ts @@ -5,7 +5,7 @@ import { KeyPair, Wallet } from './wallet' import { Bip39Mnemonic } from '#src/lib/bip39-mnemonic.js' import { Blake2b } from '#src/lib/blake2b.js' import { SEED_LENGTH_BLAKE2B } from '#src/lib/constants.js' -import { hex } from '#src/lib/convert.js' +import { bytes, hex } from '#src/lib/convert.js' import { Entropy } from '#src/lib/entropy.js' /** @@ -27,7 +27,7 @@ import { Entropy } from '#src/lib/entropy.js' export class Blake2bWallet extends Wallet { static #isInternal: boolean = false - constructor (id: Entropy, seed: string, mnemonic?: Bip39Mnemonic) { + constructor (id: Entropy, seed?: Uint8Array, mnemonic?: Bip39Mnemonic) { if (!Blake2bWallet.#isInternal) { throw new Error(`Blake2bWallet cannot be instantiated directly. Use 'await Blake2bWallet.create()' instead.`) } @@ -85,8 +85,9 @@ export class Blake2bWallet extends Wallet { if (!/^[0-9a-fA-F]+$/i.test(seed)) { throw new Error('Seed contains invalid hexadecimal characters.') } + const id = await Entropy.create() - const s = seed + const s = hex.toBytes(seed) const m = await Bip39Mnemonic.fromEntropy(seed) Blake2bWallet.#isInternal = true const wallet = new this(id, s, m) @@ -120,7 +121,7 @@ export class Blake2bWallet extends Wallet { try { const id = await Entropy.create() const m = await Bip39Mnemonic.fromPhrase(mnemonic) - const s = await m.toBlake2bSeed() + const s = (await m.toBlake2bSeed()) Blake2bWallet.#isInternal = true wallet = new this(id, s, m) } catch (err) { @@ -146,7 +147,7 @@ export class Blake2bWallet extends Wallet { throw new TypeError('Wallet ID is required to restore') } Blake2bWallet.#isInternal = true - return new this(await Entropy.import(id), '') + return new this(await Entropy.import(id)) } /** @@ -158,7 +159,7 @@ export class Blake2bWallet extends Wallet { async ckd (indexes: number[]): Promise { const results = indexes.map(index => { const indexHex = index.toString(16).padStart(8, '0').toUpperCase() - const inputHex = `${this.seed}${indexHex}`.padStart(72, '0') + const inputHex = `${bytes.toHex(this.seed)}${indexHex}`.padStart(72, '0') const inputBytes = hex.toBytes(inputHex) const privateKey: string = new Blake2b(32).update(inputBytes).digest('hex') return { privateKey, index } diff --git a/src/lib/wallets/wallet.ts b/src/lib/wallets/wallet.ts index 29f64b6..7813627 100644 --- a/src/lib/wallets/wallet.ts +++ b/src/lib/wallets/wallet.ts @@ -4,11 +4,11 @@ import { Account, AccountList } from '#src/lib/account.js' import { Bip39Mnemonic } from '#src/lib/bip39-mnemonic.js' import { ADDRESS_GAP } from '#src/lib/constants.js' +import { bytes, hex, utf8 } from '#src/lib/convert.js' import { Entropy } from '#src/lib/entropy.js' import { Pool } from '#src/lib/pool.js' import { Rpc } from '#src/lib/rpc.js' import { SafeWorker } from '#workers' -import { utf8 } from '../convert' export type KeyPair = { publicKey?: string, @@ -32,22 +32,22 @@ export abstract class Wallet { #id: Entropy #locked: boolean = true #m: Bip39Mnemonic | null - #s: string | null + #s: Uint8Array get id () { return this.#id.hex } get isLocked () { return this.#locked } get isUnlocked () { return !this.#locked } get mnemonic () { return this.#m instanceof Bip39Mnemonic ? this.#m.phrase : '' } - get seed () { return typeof this.#s === 'string' ? this.#s : '' } + get seed () { return this.#s } - constructor (id: Entropy, seed?: string, mnemonic?: Bip39Mnemonic) { + constructor (id: Entropy, seed?: Uint8Array, mnemonic?: Bip39Mnemonic) { if (this.constructor === Wallet) { throw new Error('Wallet is an abstract class and cannot be instantiated directly.') } this.#accounts = new AccountList() this.#id = id this.#m = mnemonic ?? null - this.#s = seed ?? null + this.#s = seed ?? new Uint8Array(32) } /** @@ -138,7 +138,7 @@ export abstract class Wallet { i++ } this.#m = null - this.#s = null + this.#s.fill(0) await Wallet.#poolSafe.assign({ method: 'destroy', name: this.id @@ -160,24 +160,13 @@ export abstract class Wallet { throw new Error('Failed to unlock wallet') } try { - const data: { id: string, mnemonic: string | null, seed: string | null } = { - id: this.id, - mnemonic: null, - seed: null - } - if (this.#m instanceof Bip39Mnemonic) { - data.mnemonic = this.#m.phrase - } - if (typeof this.#s === 'string') { - data.seed = this.#s - } - const response = (await Wallet.#poolSafe.assign({ + const headers = { method: 'set', name: this.id, - password, - data - }))[0] - const success = response?.result + id: this.id, + } + const response = await Wallet.#poolSafe.assign(headers, password, utf8.toBytes(this.#m?.phrase ?? ''), this.#s) + const success = response?.result[0] if (!success) { throw null } @@ -192,7 +181,7 @@ export abstract class Wallet { password.fill(0) } this.#m = null - this.#s = null + this.#s.fill(0) this.#locked = true return true } @@ -238,20 +227,23 @@ export abstract class Wallet { throw new Error('Failed to unlock wallet') } try { - const response = (await Wallet.#poolSafe.assign({ + const headers = { method: 'get', - name: this.id, - password - }))[0] - const { id, mnemonic, seed } = response?.result + name: this.id + } + const response = await Wallet.#poolSafe.assign(headers, + password) + let { id, mnemonic, seed } = response?.result[0] if (id == null || id !== this.id) { throw null } if (mnemonic != null) { this.#m = await Bip39Mnemonic.fromPhrase(mnemonic) + mnemonic = null } if (seed != null) { - this.#s = seed + this.#s.set(hex.toBytes(seed)) + seed = null } const promises = [] for (const account of this.#accounts) { diff --git a/src/lib/workers/bip44-ckd.ts b/src/lib/workers/bip44-ckd.ts index 1feff73..563093c 100644 --- a/src/lib/workers/bip44-ckd.ts +++ b/src/lib/workers/bip44-ckd.ts @@ -17,7 +17,7 @@ export class Bip44Ckd extends WorkerInterface { Bip44Ckd.listen() } - static async work (data: any[]): Promise { + static async work (headers: { [key: string]: string | number }, data: any[]): Promise { for (const d of data) { try { if (d.coin != null && d.coin !== this.BIP44_PURPOSE) { diff --git a/src/lib/workers/nano-nacl.ts b/src/lib/workers/nano-nacl.ts index 0b2dabf..122228f 100644 --- a/src/lib/workers/nano-nacl.ts +++ b/src/lib/workers/nano-nacl.ts @@ -23,7 +23,7 @@ export class NanoNaCl extends WorkerInterface { NanoNaCl.listen() } - static async work (data: any[]): Promise { + static async work (headers: { [key: string]: string | number }, data: any[]): Promise { return new Promise(async (resolve, reject): Promise => { for (let d of data) { try { diff --git a/src/lib/workers/safe.ts b/src/lib/workers/safe.ts index e043cca..159f4e7 100644 --- a/src/lib/workers/safe.ts +++ b/src/lib/workers/safe.ts @@ -38,11 +38,12 @@ export class Safe extends WorkerInterface { Safe.listen() } - static async work (data: any[]): Promise { + static async work (headers: { [key: string]: string }, data: any[]): Promise { 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 ?? [])