diff --git a/modules/abstract-utxo/src/deriveKeyWithSeed.ts b/modules/abstract-utxo/src/deriveKeyWithSeed.ts new file mode 100644 index 0000000000..eef0e740e7 --- /dev/null +++ b/modules/abstract-utxo/src/deriveKeyWithSeed.ts @@ -0,0 +1,19 @@ +import { createHash } from 'crypto'; + +import type { BIP32 } from '@bitgo/wasm-utxo'; + +/** + * Derive a child key from `key` using a path determined by `seed`. + * + * Mirrors `BaseCoin.deriveKeyWithSeedBip32` from sdk-core but operates on + * wasm-utxo's BIP32 class (which has the same `derivePath` semantics). + */ +export function deriveKeyWithSeed(key: BIP32, seed: string): { key: BIP32; derivationPath: string } { + const sha = (input: string | Buffer): Buffer => createHash('sha256').update(input).digest(); + const derivationPathInput = sha(sha(seed)).toString('hex'); + const derivationPath = `m/999999/${parseInt(derivationPathInput.slice(0, 7), 16)}/${parseInt( + derivationPathInput.slice(7, 14), + 16 + )}`; + return { key: key.derivePath(derivationPath), derivationPath }; +} diff --git a/modules/abstract-utxo/src/impl/btc/inscriptionBuilder.ts b/modules/abstract-utxo/src/impl/btc/inscriptionBuilder.ts index 4432909a9a..b164d30c6d 100644 --- a/modules/abstract-utxo/src/impl/btc/inscriptionBuilder.ts +++ b/modules/abstract-utxo/src/impl/btc/inscriptionBuilder.ts @@ -1,7 +1,6 @@ import assert from 'assert'; import { - BaseCoin, HalfSignedUtxoTransaction, IInscriptionBuilder, IWallet, @@ -29,8 +28,8 @@ import { import { BIP32, fixedScriptWallet } from '@bitgo/wasm-utxo'; import { AbstractUtxoCoin } from '../../abstractUtxoCoin'; +import { deriveKeyWithSeed } from '../../deriveKeyWithSeed'; import { fetchKeychains } from '../../keychains'; -import { toUtxolibBIP32 } from '../../wasmUtil'; /** Key identifier for signing */ type SignerKey = 'user' | 'backup' | 'bitgo'; @@ -58,8 +57,7 @@ export class InscriptionBuilder implements IInscriptionBuilder { const user = await this.wallet.baseCoin.keychains().get({ id: this.wallet.keyIds()[KeyIndices.USER] }); assert(user.pub); - const userKey = toUtxolibBIP32(BIP32.fromBase58(user.pub)); - const { key: derivedKey } = BaseCoin.deriveKeyWithSeedBip32(userKey, inscriptionData.toString()); + const { key: derivedKey } = deriveKeyWithSeed(BIP32.fromBase58(user.pub), inscriptionData.toString()); const result = inscriptions.createInscriptionRevealData( derivedKey.publicKey, diff --git a/modules/abstract-utxo/src/offlineVault/OfflineVaultHalfSigned.ts b/modules/abstract-utxo/src/offlineVault/OfflineVaultHalfSigned.ts index 83ffccbcef..e1ab318209 100644 --- a/modules/abstract-utxo/src/offlineVault/OfflineVaultHalfSigned.ts +++ b/modules/abstract-utxo/src/offlineVault/OfflineVaultHalfSigned.ts @@ -1,8 +1,7 @@ import { BIP32, bip32, Psbt } from '@bitgo/wasm-utxo'; -import { BaseCoin } from '@bitgo/sdk-core'; +import { deriveKeyWithSeed } from '../deriveKeyWithSeed'; import { UtxoCoinName } from '../names'; -import { toUtxolibBIP32 } from '../wasmUtil'; import { OfflineVaultSignable } from './OfflineVaultSignable'; import { DescriptorTransaction, getHalfSignedPsbt } from './descriptor'; @@ -21,8 +20,8 @@ export function createHalfSigned( derivationId: string, tx: unknown ): OfflineVaultHalfSigned { - const key = typeof prv === 'string' ? BIP32.fromBase58(prv) : prv; - const derivedKey = BaseCoin.deriveKeyWithSeedBip32(toUtxolibBIP32(key), derivationId).key; + const wasmKey = typeof prv === 'string' ? BIP32.fromBase58(prv) : BIP32.fromBase58(prv.toBase58()); + const derivedKey = deriveKeyWithSeed(wasmKey, derivationId).key; if (!OfflineVaultSignable.is(tx)) { throw new Error('unsupported transaction type'); } diff --git a/modules/abstract-utxo/test/unit/deriveKeyWithSeed.ts b/modules/abstract-utxo/test/unit/deriveKeyWithSeed.ts new file mode 100644 index 0000000000..2bcb9c4044 --- /dev/null +++ b/modules/abstract-utxo/test/unit/deriveKeyWithSeed.ts @@ -0,0 +1,31 @@ +import * as assert from 'assert'; + +import * as utxolib from '@bitgo/utxo-lib'; +import { BIP32 } from '@bitgo/wasm-utxo'; +import { BaseCoin } from '@bitgo/sdk-core'; + +import { deriveKeyWithSeed } from '../../src/deriveKeyWithSeed'; + +// Deterministic test xprv — derived from a 32-byte all-ones seed. +const XPRV = + 'xprv9s21ZrQH143K2QPmabzR6Q9tkNRfFxy1jy9p1PRctypZJWAjtjWBJAxxvQ3454vPpnUoLfGH8YP5KcHFX4Z5Jh7bYnFuBhxztHRy72yXmnC'; + +const SEEDS = ['', 'hello', 'some long seed string with spaces', '\u{1F600}']; + +describe('deriveKeyWithSeed', function () { + for (const seed of SEEDS) { + it(`matches BaseCoin.deriveKeyWithSeedBip32 for seed ${JSON.stringify(seed)}`, function () { + const wasmKey = BIP32.fromBase58(XPRV); + const utxolibKey = utxolib.bip32.fromBase58(XPRV); + + const wasmResult = deriveKeyWithSeed(wasmKey, seed); + const sdkCoreResult = BaseCoin.deriveKeyWithSeedBip32(utxolibKey, seed); + + assert.strictEqual(wasmResult.derivationPath, sdkCoreResult.derivationPath); + assert.strictEqual( + Buffer.from(wasmResult.key.publicKey).toString('hex'), + Buffer.from(sdkCoreResult.key.publicKey).toString('hex') + ); + }); + } +});