feat(bip85): add gen-child command (v1.0.6)
Implements BIP85 child mnemonic derivation with full interoperability.
Features:
- Derives child BIP39 mnemonics (12/15/18/21/24 words) from master mnemonic
- BIP85 path: m/83696968'/39'/0'/{words}'/{index}'
- Supports optional master BIP39 passphrase
- Reuses existing input modes (--interactive, --mnemonic-stdin)
- Follows existing UX patterns (--off-screen, --file, PGP encryption)
- Offline-first with NetworkGuard protection
Testing:
- Adds deterministic regression tests for BIP85 spec compliance
- Verified against official BIP85 test vectors
- CLI smoke tests for end-to-end validation
Interoperability:
- Produces mnemonics compatible with Coldcard, Ian Coleman tool, etc.
- Test vector verified: 'girl mad pet galaxy egg matter matrix prison refuse sense ordinary nose'
Version bumped to v1.0.6
This commit is contained in:
@@ -89,6 +89,91 @@ def test_address_derivation_integrity(vectors):
|
||||
)
|
||||
assert result["addresses"] == expected_addresses, f"Mismatch for profile {profile}"
|
||||
|
||||
def test_bip85_derivation(vectors):
|
||||
"""
|
||||
Verifies BIP85 child mnemonic derivation matches expected test vectors.
|
||||
|
||||
Tests:
|
||||
- Path construction
|
||||
- Entropy64 derivation (HMAC-SHA512)
|
||||
- Entropy truncation
|
||||
- Child mnemonic generation
|
||||
"""
|
||||
from bip_utils import Bip39SeedGenerator
|
||||
|
||||
for case in vectors["bip85"]:
|
||||
master_mnemonic = case["master_mnemonic"]
|
||||
master_passphrase = case["master_passphrase"]
|
||||
child_words = case["child_words"]
|
||||
index = case["index"]
|
||||
|
||||
# Generate master seed
|
||||
master_seed = Bip39SeedGenerator(master_mnemonic).Generate(master_passphrase)
|
||||
|
||||
# Derive child using BIP85
|
||||
path, child_mnemonic, entropy64, truncated_entropy = pyhdwallet.bip85_derive_child_mnemonic(
|
||||
master_seed,
|
||||
child_words,
|
||||
index
|
||||
)
|
||||
|
||||
# Assert path
|
||||
assert path == case["bip85_path"], f"Path mismatch: {path} != {case['bip85_path']}"
|
||||
|
||||
# Assert entropy64 (full 64-byte HMAC output)
|
||||
assert entropy64.hex() == case["expected_entropy64_hex"], \
|
||||
f"Entropy64 mismatch for {case['description']}"
|
||||
|
||||
# Assert truncated entropy
|
||||
assert truncated_entropy.hex() == case["expected_entropy_truncated_hex"], \
|
||||
f"Truncated entropy mismatch for {case['description']}"
|
||||
|
||||
# Assert child mnemonic
|
||||
assert child_mnemonic == case["expected_child_mnemonic"], \
|
||||
f"Child mnemonic mismatch for {case['description']}"
|
||||
|
||||
|
||||
def test_cli_gen_child_smoke(tmp_path, vectors):
|
||||
"""
|
||||
CLI smoke test for gen-child command.
|
||||
|
||||
Verifies:
|
||||
- Command runs without error
|
||||
- ZIP file is created
|
||||
- Off-screen mode works
|
||||
- File output works
|
||||
"""
|
||||
vector = vectors["bip85"][0] # Use first BIP85 test case
|
||||
master_mnemonic = vector["master_mnemonic"]
|
||||
|
||||
cmd = [
|
||||
sys.executable, "src/pyhdwallet.py", "gen-child",
|
||||
"--mnemonic-stdin",
|
||||
"--index", "0",
|
||||
"--words", "12",
|
||||
"--off-screen",
|
||||
"--file",
|
||||
"--zip-password-mode", "auto",
|
||||
"--wallet-location", str(tmp_path)
|
||||
]
|
||||
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
input=master_mnemonic.encode("utf-8"),
|
||||
capture_output=True
|
||||
)
|
||||
|
||||
assert result.returncode == 0, f"CLI gen-child failed: {result.stderr.decode()}"
|
||||
|
||||
# Check ZIP file created
|
||||
zips = list(tmp_path.glob("*.zip"))
|
||||
assert len(zips) == 1, f"Expected exactly one zip file, found {len(zips)}"
|
||||
|
||||
# Check filename pattern
|
||||
zip_name = zips[0].name
|
||||
assert zip_name.startswith("bip85_child_"), f"Unexpected zip name: {zip_name}"
|
||||
|
||||
|
||||
def test_cli_recover_smoke(tmp_path, vectors):
|
||||
"""
|
||||
Runs the CLI in a subprocess to verify end-to-end wiring
|
||||
|
||||
Reference in New Issue
Block a user