]> git.codecow.com Git - libnemo.git/commitdiff
Start refactoring vault to derive accounts in batches which is way more performant.
authorChris Duncan <chris@codecow.com>
Fri, 7 Aug 2026 23:19:50 +0000 (16:19 -0700)
committerChris Duncan <chris@codecow.com>
Fri, 7 Aug 2026 23:19:50 +0000 (16:19 -0700)
src/lib/vault/index.ts
src/lib/vault/parsers.ts
src/lib/vault/vault-worker.ts
src/lib/wallet/accounts.ts
src/lib/wallet/index.ts
test/test.derive-accounts.mjs

index ca5a9d5c990f6c0e97962fe2e52ae4c7146689dc..a021204270f129c54874216cb3215c9c6be208fb 100644 (file)
@@ -42,12 +42,15 @@ export class Vault {
                const listener = (message: MessageEvent<any>) => {
                        this.#report(message)
                }
+               const terminator = () => {
+                       this.terminate()
+               }
                BROWSER: {
                        this.#url = URL.createObjectURL(new Blob([vaultWorker], { type: 'text/javascript' }))
                        this.#worker = new Worker(this.#url, { type: 'module' })
                        this.#worker.addEventListener('message', listener)
-                       this.#worker.addEventListener('error', this.terminate)
-                       this.#worker.addEventListener('messageerror', this.terminate)
+                       this.#worker.addEventListener('error', terminator)
+                       this.#worker.addEventListener('messageerror', terminator)
                }
                NODE: {
                        this.#worker = new NodeWorker(vaultWorker, {
@@ -57,8 +60,8 @@ export class Vault {
                        })
                        this.#url = dec.toString(this.#worker.threadId)
                        this.#worker.on('message', listener)
-                       this.#worker.on('error', this.terminate)
-                       this.#worker.on('messageerror', this.terminate)
+                       this.#worker.on('error', terminator)
+                       this.#worker.on('messageerror', terminator)
                }
        }
 
index db696f203d71b8b53e4e6b3410d352a1a8e69435..f910e5bebd95ecb41601e55ba248facd793e2896 100644 (file)
@@ -89,6 +89,14 @@ export function parseData (action: string, data: Record<string, unknown>) {
                        ? data.index
                        : undefined
 
+               // Number of public keys to derive
+               if (action === 'derive' && (typeof data.count !== 'number' || data.count < 1)) {
+                       throw new TypeError('Count is required to derive a batch of keys')
+               }
+               const count = typeof data.count === 'number'
+                       ? data.count
+                       : undefined
+
                // Data to sign
                if ('message' in data) {
                        if (action === 'sign') {
@@ -114,7 +122,7 @@ export function parseData (action: string, data: Record<string, unknown>) {
                        ? data.timeout
                        : undefined
 
-               return { seed, mnemonicPhrase, mnemonicSalt, encrypted, index, message, timeout }
+               return { seed, mnemonicPhrase, mnemonicSalt, encrypted, index, count, message, timeout }
        } catch (err) {
                new Uint8Array(seed ?? []).fill(0)
                new Uint8Array(encrypted ?? []).fill(0)
index c976ea54f742dac2e992715d520ad081f7a59341..6363247be4d7162aa47de983de0523f9c04e9074 100644 (file)
@@ -40,7 +40,7 @@ const listener = (event: MessageEvent<any>): void => {
        const id = parseId(action, data)
        const iv = parseIv(action, data)
        const parsed = parseData(action, data)
-       const { seed, mnemonicPhrase, mnemonicSalt, index, encrypted, message, timeout } = parsed
+       const { seed, mnemonicPhrase, mnemonicSalt, index, count, encrypted, message, timeout } = parsed
        passkey(action, keySalt, data)
                .then((key: CryptoKey | undefined): Promise<Record<string, boolean | number | ArrayBuffer> | void> => {
                        switch (action) {
@@ -56,7 +56,7 @@ const listener = (event: MessageEvent<any>): void => {
                                        return create(type, id, key, keySalt, mnemonicSalt)
                                }
                                case 'derive': {
-                                       return derive(index)
+                                       return derive(index, count)
                                }
                                case 'load': {
                                        return load(type, id, key, keySalt, mnemonicPhrase ?? seed, mnemonicSalt)
@@ -170,7 +170,7 @@ function create (type?: WalletType, id?: UUID, key?: CryptoKey, keySalt?: ArrayB
  * wallet seed at a specified index and then returns the public key. The wallet
  * must be unlocked prior to derivation.
  */
-function derive (index?: number): Promise<Record<string, number | ArrayBuffer>> {
+function derive (index?: number, count?: number): Promise<Record<string, number | ArrayBuffer>> {
        try {
                _timer.pause()
                if (_locked) {
@@ -182,16 +182,39 @@ function derive (index?: number): Promise<Record<string, number | ArrayBuffer>>
                if (_type !== 'BIP-44' && _type !== 'BLAKE2b' && _type !== 'Exodus') {
                        throw new Error('Invalid wallet type')
                }
-               if (typeof index !== 'number') {
+               if (typeof index !== 'number' || index < 0) {
                        throw new Error('Invalid wallet account index')
                }
-               return _ckd(index).then(result => {
-                       const prv = new Uint8Array(result)
-                       const pub = nano25519_derive(prv)
-                       prv.fill(0)
-                       _timer = new VaultTimer(_autolock, _timeout)
-                       return { index, publicKey: pub.buffer }
-               })
+               if (typeof count !== 'number' || count < 1) {
+                       throw new Error('Invalid wallet account batch size')
+               }
+               const max = index + count
+               if (max > (_type === 'BLAKE2b' ? 0xffffffff : 0x7fffffff)) {
+                       throw new Error('Wallet account range exceeded ')
+               }
+               const promises = []
+               for (let i = index; i < max; i++) {
+                       promises.push(_ckd(index).then(result => {
+                               const prv = new Uint8Array(result)
+                               const pub = nano25519_derive(prv)
+                               prv.fill(0)
+                               return { index, publicKey: pub.buffer }
+                       }))
+               }
+               return Promise.all(promises)
+                       .then(results => {
+                               const data: Record<string, ArrayBuffer> = {}
+                               for (const result of results) {
+                                       data[result.index] = result.publicKey
+                               }
+                               _timer = new VaultTimer(_autolock, _timeout)
+                               return data
+                       })
+                       .catch(err => {
+                               console.error(err)
+                               _timer.resume()
+                               throw new Error('Failed to derive account', { cause: err })
+                       })
        } catch (err) {
                console.error(err)
                _timer.resume()
index 65f79e2c0eac8b66b9c38b0b7b1a2185bb76446b..1a74fd64640c2eab3fa60cca2f383acb929f8092 100644 (file)
@@ -22,7 +22,7 @@ export async function _accounts (type: WalletType, accounts: Map<number, Account
                throw new TypeError('Invalid account range', { cause: `${from}-${to}` })
        }
        if (to - from > 100) {
-               console.warn(`libnemo performance may degrade when deriving many accounts at once`)
+               console.warn('libnemo performance may degrade when deriving many accounts at once')
        }
        const output = new Map<number, Account>()
        const indexes: number[] = []
@@ -46,19 +46,27 @@ export async function _accounts (type: WalletType, accounts: Map<number, Account
                                accounts.set(index, account)
                        }
                } else {
-                       const promises = []
-                       for (const index of indexes) {
-                               promises.push(vault.request<number | ArrayBuffer>({
-                                       action: 'derive',
-                                       index
-                               }))
-                       }
-                       const publicKeys = await Promise.all(promises)
-                       for (const { index, publicKey } of publicKeys) {
-                               if (typeof index === 'number' && publicKey instanceof ArrayBuffer) {
+                       // const promises = []
+                       // for (const index of indexes) {
+                       //      promises.push(vault.request<number | ArrayBuffer>({
+                       //              action: 'derive',
+                       //              index
+                       //      }))
+                       // }
+                       // const publicKeys = await Promise.all(promises)
+                       const min = Math.min(...indexes)
+                       const count = Math.max(...indexes) - min + 1
+                       const publicKeys = await vault.request<ArrayBuffer>({
+                               action: 'derive',
+                               index: min,
+                               count
+                       })
+                       console.log(publicKeys)
+                       for (const [index, publicKey] of Object.entries(publicKeys)) {
+                               if (typeof Number(index) === 'number' && publicKey instanceof ArrayBuffer) {
                                        const account = new Account(publicKey)
-                                       output.set(index, account)
-                                       accounts.set(index, account)
+                                       output.set(Number(index), account)
+                                       accounts.set(Number(index), account)
                                }
                        }
                }
index 6db838ec288071641b0ef7e3310597d11f23eed4..a866e7307ec5c150cccf849de60077c5fd16d7c7 100644 (file)
@@ -268,6 +268,11 @@ export class Wallet {
        * Retrieves accounts from a wallet using its child key derivation function.\r
        * Defaults to the first account at index 0.\r
        *\r
+       * Derives a maximum of 1000 accounts per call and will throw an error if\r
+       * exceeded. This is not a hard limit since the method can be called multiple\r
+       * times, but it is a safety measure to let developers know when they may be\r
+       * creating a poor user experience.\r
+       *\r
        * The returned object will have keys corresponding with the requested range\r
        * of account indexes. The value of each key will be the Account derived for\r
        * that index in the wallet.\r
index de7a935a74613064258ce31ce1947d0799ebb501..d3e7cd6570db3f0efc07977c9572002053721265 100644 (file)
@@ -10,6 +10,19 @@ import { CUSTOM_TEST_VECTORS, NANO_TEST_VECTORS, TEST_PASSWORD } from './VECTORS
 await Promise.all([\r
        suite('Derive accounts from BIP-44 wallet', async () => {\r
 \r
+               await test('derive the first 10000 accounts from the given BIP-44 seed', async () => {\r
+                       const wallet = await Wallet.load('BIP-44', TEST_PASSWORD, NANO_TEST_VECTORS.BIP39_SEED)\r
+                       await wallet.unlock(TEST_PASSWORD)\r
+\r
+                       let start, end\r
+                       start = performance.now()\r
+                       await wallet.accounts(0, 10000)\r
+                       end = performance.now()\r
+                       console.log('duration', end - start)\r
+\r
+                       await assert.resolves(wallet.destroy())\r
+               })\r
+\r
                await test('derive the first account from the given BIP-44 seed', async () => {\r
                        const wallet = await Wallet.load('BIP-44', TEST_PASSWORD, NANO_TEST_VECTORS.BIP39_SEED)\r
                        await wallet.unlock(TEST_PASSWORD)\r