From f6259ae528624eafbbcb7065cec70205382c29c3 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Thu, 29 Jan 2026 14:27:35 +0100 Subject: [PATCH 1/2] feat(wasm-utxo): add fromSeedSha256 to BIP32 for deterministic test keys Implements a new method to create BIP32 keys from string seeds by hashing them with SHA256. This simplifies deterministic test key generation and eliminates the need for the crypto module in testutils. Issue: BTC-2980 Co-authored-by: llm-git --- packages/wasm-utxo/js/bip32.ts | 12 +++++++ packages/wasm-utxo/js/testutils/keys.ts | 3 +- packages/wasm-utxo/src/wasm/bip32.rs | 12 +++++++ packages/wasm-utxo/test/bip32.ts | 42 +++++++++++++++++++++++++ 4 files changed, 67 insertions(+), 2 deletions(-) diff --git a/packages/wasm-utxo/js/bip32.ts b/packages/wasm-utxo/js/bip32.ts index 42a3e6822ed..dd5329ef23d 100644 --- a/packages/wasm-utxo/js/bip32.ts +++ b/packages/wasm-utxo/js/bip32.ts @@ -97,6 +97,18 @@ export class BIP32 implements BIP32Interface { return new BIP32(wasm); } + /** + * Create a BIP32 master key from a string by hashing it with SHA256. + * Useful for deterministic test key generation. + * @param seedString - The seed string to hash + * @param network - Optional network string + * @returns A BIP32 instance + */ + static fromSeedSha256(seedString: string, network?: string | null): BIP32 { + const wasm = WasmBIP32.from_seed_sha256(seedString, network); + return new BIP32(wasm); + } + /** * Get the chain code as a Uint8Array */ diff --git a/packages/wasm-utxo/js/testutils/keys.ts b/packages/wasm-utxo/js/testutils/keys.ts index cb1881a1e1f..c71edcae179 100644 --- a/packages/wasm-utxo/js/testutils/keys.ts +++ b/packages/wasm-utxo/js/testutils/keys.ts @@ -1,4 +1,3 @@ -import * as crypto from "crypto"; import { BIP32 } from "../bip32.js"; import { RootWalletKeys } from "../fixedScriptWallet/RootWalletKeys.js"; import type { Triple } from "../triple.js"; @@ -17,7 +16,7 @@ import type { Triple } from "../triple.js"; * ``` */ export function getKey(seed: string): BIP32 { - return BIP32.fromSeed(crypto.createHash("sha256").update(seed).digest()); + return BIP32.fromSeedSha256(seed); } /** diff --git a/packages/wasm-utxo/src/wasm/bip32.rs b/packages/wasm-utxo/src/wasm/bip32.rs index c03934b82ff..316e20623a7 100644 --- a/packages/wasm-utxo/src/wasm/bip32.rs +++ b/packages/wasm-utxo/src/wasm/bip32.rs @@ -210,6 +210,18 @@ impl WasmBIP32 { Ok(WasmBIP32(BIP32Key::Private(xpriv))) } + /// Create a BIP32 master key from a string by hashing it with SHA256. + /// This is useful for deterministic test key generation. + #[wasm_bindgen] + pub fn from_seed_sha256( + seed_string: &str, + network: Option, + ) -> Result { + use crate::bitcoin::hashes::{sha256, Hash}; + let hash = sha256::Hash::hash(seed_string.as_bytes()); + Self::from_seed(&hash[..], network) + } + /// Get the chain code as a Uint8Array #[wasm_bindgen(getter)] pub fn chain_code(&self) -> js_sys::Uint8Array { diff --git a/packages/wasm-utxo/test/bip32.ts b/packages/wasm-utxo/test/bip32.ts index 4ddd8f762d2..d65e9d493b6 100644 --- a/packages/wasm-utxo/test/bip32.ts +++ b/packages/wasm-utxo/test/bip32.ts @@ -1,4 +1,5 @@ import * as assert from "assert"; +import * as crypto from "crypto"; import { bip32 as utxolibBip32 } from "@bitgo/utxo-lib"; import { BIP32 } from "../js/bip32.js"; @@ -138,6 +139,25 @@ describe("WasmBIP32", () => { assert.strictEqual(key.isNeutered(), false); assert.ok(key.toBase58().startsWith("tprv")); }); + + it("should create from seed string using SHA256", () => { + const seedString = "test"; + const key = bip32.BIP32.fromSeedSha256(seedString); + assert.strictEqual(key.depth, 0); + assert.strictEqual(key.isNeutered(), false); + assert.ok(key.privateKey instanceof Uint8Array); + // Should be deterministic + const key2 = bip32.BIP32.fromSeedSha256(seedString); + assert.strictEqual(key.toBase58(), key2.toBase58()); + }); + + it("should create from seed string with network", () => { + const seedString = "test"; + const key = bip32.BIP32.fromSeedSha256(seedString, "BitcoinTestnet3"); + assert.strictEqual(key.depth, 0); + assert.strictEqual(key.isNeutered(), false); + assert.ok(key.toBase58().startsWith("tprv")); + }); }); describe("WasmBIP32 parity with utxolib", () => { @@ -336,4 +356,26 @@ describe("WasmBIP32 parity with utxolib", () => { const wasmParentFp = new DataView(wasmKey.fingerprint.buffer).getUint32(0, false); assert.strictEqual(wasmChild.parentFingerprint, wasmParentFp); }); + + it("should match utxolib when using fromSeedSha256", () => { + // Test various seed strings to ensure parity with manual SHA256 + fromSeed + const seedStrings = ["test", "user", "backup", "bitgo", "default.0", "default.1", "default.2"]; + + for (const seedString of seedStrings) { + // Manual approach: hash with SHA256, then create from seed + const hash = crypto.createHash("sha256").update(seedString).digest(); + const utxolibKey = utxolibBip32.fromSeed(hash); + + // WASM approach: fromSeedSha256 does hashing internally + const wasmKey = bip32.BIP32.fromSeedSha256(seedString); + + assert.strictEqual( + wasmKey.toBase58(), + utxolibKey.toBase58(), + `Failed for seed string: ${seedString}`, + ); + assert.ok(bufferEqual(wasmKey.publicKey, utxolibKey.publicKey)); + assert.ok(bufferEqual(wasmKey.chainCode, utxolibKey.chainCode)); + } + }); }); From fec213d925f2c50e07b13d1d216a0912115a267e Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Thu, 29 Jan 2026 14:05:39 +0100 Subject: [PATCH 2/2] feat(wasm-utxo): export testutils module Add testutils namespace export to the main index.ts file to make test utilities available to consumers of the library. Issue: BTC-2980 Co-authored-by: llm-git --- packages/wasm-utxo/js/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/wasm-utxo/js/index.ts b/packages/wasm-utxo/js/index.ts index c318c31a28a..c238f86d1a0 100644 --- a/packages/wasm-utxo/js/index.ts +++ b/packages/wasm-utxo/js/index.ts @@ -14,6 +14,7 @@ export * as utxolibCompat from "./utxolibCompat.js"; export * as fixedScriptWallet from "./fixedScriptWallet/index.js"; export * as bip32 from "./bip32.js"; export * as ecpair from "./ecpair.js"; +export * as testutils from "./testutils/index.js"; // Only the most commonly used classes and types are exported at the top level for convenience export { ECPair } from "./ecpair.js";