Skip to content

Commit f6259ae

Browse files
OttoAllmendingerllm-git
andcommitted
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 <llm-git@ttll.de>
1 parent fbc7c4a commit f6259ae

4 files changed

Lines changed: 67 additions & 2 deletions

File tree

packages/wasm-utxo/js/bip32.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,18 @@ export class BIP32 implements BIP32Interface {
9797
return new BIP32(wasm);
9898
}
9999

100+
/**
101+
* Create a BIP32 master key from a string by hashing it with SHA256.
102+
* Useful for deterministic test key generation.
103+
* @param seedString - The seed string to hash
104+
* @param network - Optional network string
105+
* @returns A BIP32 instance
106+
*/
107+
static fromSeedSha256(seedString: string, network?: string | null): BIP32 {
108+
const wasm = WasmBIP32.from_seed_sha256(seedString, network);
109+
return new BIP32(wasm);
110+
}
111+
100112
/**
101113
* Get the chain code as a Uint8Array
102114
*/

packages/wasm-utxo/js/testutils/keys.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import * as crypto from "crypto";
21
import { BIP32 } from "../bip32.js";
32
import { RootWalletKeys } from "../fixedScriptWallet/RootWalletKeys.js";
43
import type { Triple } from "../triple.js";
@@ -17,7 +16,7 @@ import type { Triple } from "../triple.js";
1716
* ```
1817
*/
1918
export function getKey(seed: string): BIP32 {
20-
return BIP32.fromSeed(crypto.createHash("sha256").update(seed).digest());
19+
return BIP32.fromSeedSha256(seed);
2120
}
2221

2322
/**

packages/wasm-utxo/src/wasm/bip32.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,18 @@ impl WasmBIP32 {
210210
Ok(WasmBIP32(BIP32Key::Private(xpriv)))
211211
}
212212

213+
/// Create a BIP32 master key from a string by hashing it with SHA256.
214+
/// This is useful for deterministic test key generation.
215+
#[wasm_bindgen]
216+
pub fn from_seed_sha256(
217+
seed_string: &str,
218+
network: Option<String>,
219+
) -> Result<WasmBIP32, WasmUtxoError> {
220+
use crate::bitcoin::hashes::{sha256, Hash};
221+
let hash = sha256::Hash::hash(seed_string.as_bytes());
222+
Self::from_seed(&hash[..], network)
223+
}
224+
213225
/// Get the chain code as a Uint8Array
214226
#[wasm_bindgen(getter)]
215227
pub fn chain_code(&self) -> js_sys::Uint8Array {

packages/wasm-utxo/test/bip32.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as assert from "assert";
2+
import * as crypto from "crypto";
23
import { bip32 as utxolibBip32 } from "@bitgo/utxo-lib";
34
import { BIP32 } from "../js/bip32.js";
45

@@ -138,6 +139,25 @@ describe("WasmBIP32", () => {
138139
assert.strictEqual(key.isNeutered(), false);
139140
assert.ok(key.toBase58().startsWith("tprv"));
140141
});
142+
143+
it("should create from seed string using SHA256", () => {
144+
const seedString = "test";
145+
const key = bip32.BIP32.fromSeedSha256(seedString);
146+
assert.strictEqual(key.depth, 0);
147+
assert.strictEqual(key.isNeutered(), false);
148+
assert.ok(key.privateKey instanceof Uint8Array);
149+
// Should be deterministic
150+
const key2 = bip32.BIP32.fromSeedSha256(seedString);
151+
assert.strictEqual(key.toBase58(), key2.toBase58());
152+
});
153+
154+
it("should create from seed string with network", () => {
155+
const seedString = "test";
156+
const key = bip32.BIP32.fromSeedSha256(seedString, "BitcoinTestnet3");
157+
assert.strictEqual(key.depth, 0);
158+
assert.strictEqual(key.isNeutered(), false);
159+
assert.ok(key.toBase58().startsWith("tprv"));
160+
});
141161
});
142162

143163
describe("WasmBIP32 parity with utxolib", () => {
@@ -336,4 +356,26 @@ describe("WasmBIP32 parity with utxolib", () => {
336356
const wasmParentFp = new DataView(wasmKey.fingerprint.buffer).getUint32(0, false);
337357
assert.strictEqual(wasmChild.parentFingerprint, wasmParentFp);
338358
});
359+
360+
it("should match utxolib when using fromSeedSha256", () => {
361+
// Test various seed strings to ensure parity with manual SHA256 + fromSeed
362+
const seedStrings = ["test", "user", "backup", "bitgo", "default.0", "default.1", "default.2"];
363+
364+
for (const seedString of seedStrings) {
365+
// Manual approach: hash with SHA256, then create from seed
366+
const hash = crypto.createHash("sha256").update(seedString).digest();
367+
const utxolibKey = utxolibBip32.fromSeed(hash);
368+
369+
// WASM approach: fromSeedSha256 does hashing internally
370+
const wasmKey = bip32.BIP32.fromSeedSha256(seedString);
371+
372+
assert.strictEqual(
373+
wasmKey.toBase58(),
374+
utxolibKey.toBase58(),
375+
`Failed for seed string: ${seedString}`,
376+
);
377+
assert.ok(bufferEqual(wasmKey.publicKey, utxolibKey.publicKey));
378+
assert.ok(bufferEqual(wasmKey.chainCode, utxolibKey.chainCode));
379+
}
380+
});
339381
});

0 commit comments

Comments
 (0)