Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions packages/wasm-utxo/js/bip32.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
1 change: 1 addition & 0 deletions packages/wasm-utxo/js/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
3 changes: 1 addition & 2 deletions packages/wasm-utxo/js/testutils/keys.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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);
}

/**
Expand Down
12 changes: 12 additions & 0 deletions packages/wasm-utxo/src/wasm/bip32.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
) -> Result<WasmBIP32, WasmUtxoError> {
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 {
Expand Down
42 changes: 42 additions & 0 deletions packages/wasm-utxo/test/bip32.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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));
}
});
});