Files
easy-wallet/tests/test_hd_wallet.py
Davide Grilli c98d4db711 test: expand test suite to 194 tests across all modules
Add tests/test_crypto.py (22 tests) and tests/test_single_wallet.py
(46 tests) for previously uncovered modules; extend existing test files
with WIF format checks, error/validation cases, BIP39 passphrase,
account index, words_num, and redeem script structure tests
2026-03-10 09:40:01 +01:00

413 lines
15 KiB
Python

import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from src.hd_wallet import generate_hd_wallet, encrypt_wallet, decrypt_wallet
from src.crypto import pw_decode
SECRETSCAN_URL = "https://secretscan.org/Bitcoin?address={}"
TEST_MNEMONIC = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
BIP_TYPES = ['bip44', 'bip49', 'bip84', 'bip86']
# --- Electrum top-level structure ---
def test_hd_top_level_fields():
result = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC)
assert set(result.keys()) == {'keystore', 'wallet_type', 'use_encryption', 'seed_type', 'seed_version', 'addresses'}
def test_hd_electrum_metadata():
result = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC)
assert result['wallet_type'] == 'standard'
assert result['use_encryption'] is False
assert result['seed_type'] == 'bip39'
assert result['seed_version'] == 17
# --- Keystore structure ---
def test_hd_keystore_fields():
result = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC)
ks = result['keystore']
assert set(ks.keys()) == {'type', 'xpub', 'xprv', 'seed', 'passphrase', 'derivation', 'root_fingerprint'}
def test_hd_keystore_type():
result = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC)
assert result['keystore']['type'] == 'bip32'
def test_hd_keystore_seed():
result = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC)
assert result['keystore']['seed'] == TEST_MNEMONIC
def test_hd_keystore_root_fingerprint_length():
result = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC)
assert len(result['keystore']['root_fingerprint']) == 8 # 4 bytes hex
def test_hd_keystore_same_root_fingerprint_across_bip_types():
# Root fingerprint derives from master key, same for all BIP types with same mnemonic
fingerprints = {
bip: generate_hd_wallet('mainnet', bip, TEST_MNEMONIC)['keystore']['root_fingerprint']
for bip in BIP_TYPES
}
assert len(set(fingerprints.values())) == 1
# --- xpub/xprv prefixes per BIP type ---
def test_hd_bip44_xpub_prefix():
ks = generate_hd_wallet('mainnet', 'bip44', TEST_MNEMONIC)['keystore']
assert ks['xpub'].startswith('xpub')
assert ks['xprv'].startswith('xprv')
def test_hd_bip49_xpub_prefix():
ks = generate_hd_wallet('mainnet', 'bip49', TEST_MNEMONIC)['keystore']
assert ks['xpub'].startswith('ypub')
assert ks['xprv'].startswith('yprv')
def test_hd_bip84_xpub_prefix():
ks = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC)['keystore']
assert ks['xpub'].startswith('zpub')
assert ks['xprv'].startswith('zprv')
def test_hd_bip86_xpub_prefix():
ks = generate_hd_wallet('mainnet', 'bip86', TEST_MNEMONIC)['keystore']
assert ks['xpub'].startswith('xpub')
assert ks['xprv'].startswith('xprv')
# --- Derivation paths ---
def test_hd_derivation_bip44_mainnet():
ks = generate_hd_wallet('mainnet', 'bip44', TEST_MNEMONIC)['keystore']
assert ks['derivation'] == "m/44'/0'/0'"
def test_hd_derivation_bip49_mainnet():
ks = generate_hd_wallet('mainnet', 'bip49', TEST_MNEMONIC)['keystore']
assert ks['derivation'] == "m/49'/0'/0'"
def test_hd_derivation_bip84_mainnet():
ks = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC)['keystore']
assert ks['derivation'] == "m/84'/0'/0'"
def test_hd_derivation_bip86_mainnet():
ks = generate_hd_wallet('mainnet', 'bip86', TEST_MNEMONIC)['keystore']
assert ks['derivation'] == "m/86'/0'/0'"
def test_hd_derivation_testnet_coin_type():
ks = generate_hd_wallet('testnet', 'bip84', TEST_MNEMONIC)['keystore']
assert ks['derivation'] == "m/84'/1'/0'"
# --- Addresses structure ---
def test_hd_addresses_keys():
result = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC)
assert set(result['addresses'].keys()) == {'receiving', 'change'}
assert result['addresses']['change'] == []
def test_hd_address_entry_fields():
result = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC, num_addresses=1)
entry = result['addresses']['receiving'][0]
assert set(entry.keys()) == {'index', 'path', 'address', 'public_key', 'private_key_wif', 'private_key_hex'}
def test_hd_address_path_format():
result = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC, num_addresses=3)
for i, entry in enumerate(result['addresses']['receiving']):
assert entry['path'] == f"m/84'/0'/0'/0/{i}"
assert entry['index'] == i
# --- Address prefixes mainnet ---
def test_hd_bip44_address_mainnet():
result = generate_hd_wallet('mainnet', 'bip44', TEST_MNEMONIC, num_addresses=1)
assert result['addresses']['receiving'][0]['address'].startswith('1')
def test_hd_bip49_address_mainnet():
result = generate_hd_wallet('mainnet', 'bip49', TEST_MNEMONIC, num_addresses=1)
assert result['addresses']['receiving'][0]['address'].startswith('3')
def test_hd_bip84_address_mainnet():
result = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC, num_addresses=1)
assert result['addresses']['receiving'][0]['address'].startswith('bc1q')
def test_hd_bip86_address_mainnet():
result = generate_hd_wallet('mainnet', 'bip86', TEST_MNEMONIC, num_addresses=1)
assert result['addresses']['receiving'][0]['address'].startswith('bc1p')
# --- Address prefixes testnet ---
def test_hd_bip44_address_testnet():
result = generate_hd_wallet('testnet', 'bip44', TEST_MNEMONIC, num_addresses=1)
assert result['addresses']['receiving'][0]['address'][0] in ('m', 'n')
def test_hd_bip84_address_testnet():
result = generate_hd_wallet('testnet', 'bip84', TEST_MNEMONIC, num_addresses=1)
assert result['addresses']['receiving'][0]['address'].startswith('tb1q')
def test_hd_bip86_address_testnet():
result = generate_hd_wallet('testnet', 'bip86', TEST_MNEMONIC, num_addresses=1)
assert result['addresses']['receiving'][0]['address'].startswith('tb1p')
# --- Determinism ---
def test_hd_same_mnemonic_same_addresses():
r1 = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC, num_addresses=3)
r2 = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC, num_addresses=3)
addrs1 = [a['address'] for a in r1['addresses']['receiving']]
addrs2 = [a['address'] for a in r2['addresses']['receiving']]
assert addrs1 == addrs2
def test_hd_same_mnemonic_same_xpub():
r1 = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC)
r2 = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC)
assert r1['keystore']['xpub'] == r2['keystore']['xpub']
def test_hd_different_mnemonic_different_xpub():
r1 = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC)
r2 = generate_hd_wallet('mainnet', 'bip84') # random mnemonic
assert r1['keystore']['xpub'] != r2['keystore']['xpub']
# --- num_addresses ---
def test_hd_num_addresses():
for n in [1, 3, 10]:
result = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC, num_addresses=n)
assert len(result['addresses']['receiving']) == n
# --- Key format ---
def test_hd_private_key_length():
result = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC, num_addresses=1)
assert len(result['addresses']['receiving'][0]['private_key_hex']) == 64
def test_hd_public_key_compressed():
result = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC, num_addresses=1)
pub = result['addresses']['receiving'][0]['public_key']
assert len(pub) == 66
assert pub[:2] in ('02', '03')
# --- Mnemonic generation ---
def test_hd_generates_mnemonic_when_none():
result = generate_hd_wallet('mainnet', 'bip84')
assert len(result['keystore']['seed'].split()) == 12
# --- Encryption ---
TEST_PASSWORD = "correct_horse_battery_staple"
def test_hd_no_encryption_by_default():
result = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC, num_addresses=1)
assert result['use_encryption'] is False
def test_hd_encryption_sets_flag():
result = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC, num_addresses=1)
encrypted = encrypt_wallet(result, TEST_PASSWORD)
assert encrypted['use_encryption'] is True
def test_hd_encryption_encrypts_seed():
result = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC, num_addresses=1)
encrypted = encrypt_wallet(result, TEST_PASSWORD)
assert encrypted['keystore']['seed'] != TEST_MNEMONIC
def test_hd_encryption_encrypts_xprv():
result = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC, num_addresses=1)
original_xprv = result['keystore']['xprv']
encrypted = encrypt_wallet(result, TEST_PASSWORD)
assert encrypted['keystore']['xprv'] != original_xprv
def test_hd_encryption_leaves_xpub_plaintext():
result = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC, num_addresses=1)
encrypted = encrypt_wallet(result, TEST_PASSWORD)
assert encrypted['keystore']['xpub'] == result['keystore']['xpub']
def test_hd_encryption_leaves_addresses_plaintext():
result = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC, num_addresses=1)
encrypted = encrypt_wallet(result, TEST_PASSWORD)
assert encrypted['addresses']['receiving'][0]['address'] == result['addresses']['receiving'][0]['address']
assert encrypted['addresses']['receiving'][0]['public_key'] == result['addresses']['receiving'][0]['public_key']
def test_hd_encryption_encrypts_private_keys():
result = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC, num_addresses=2)
encrypted = encrypt_wallet(result, TEST_PASSWORD)
for i in range(2):
orig = result['addresses']['receiving'][i]
enc = encrypted['addresses']['receiving'][i]
assert enc['private_key_hex'] != orig['private_key_hex']
assert enc['private_key_wif'] != orig['private_key_wif']
def test_hd_encryption_round_trip_seed():
result = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC, num_addresses=1)
encrypted = encrypt_wallet(result, TEST_PASSWORD)
decrypted = decrypt_wallet(encrypted, TEST_PASSWORD)
assert decrypted['keystore']['seed'] == TEST_MNEMONIC
def test_hd_encryption_round_trip_xprv():
result = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC, num_addresses=1)
encrypted = encrypt_wallet(result, TEST_PASSWORD)
decrypted = decrypt_wallet(encrypted, TEST_PASSWORD)
assert decrypted['keystore']['xprv'] == result['keystore']['xprv']
def test_hd_encryption_round_trip_private_keys():
result = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC, num_addresses=3)
encrypted = encrypt_wallet(result, TEST_PASSWORD)
decrypted = decrypt_wallet(encrypted, TEST_PASSWORD)
for i in range(3):
assert decrypted['addresses']['receiving'][i]['private_key_hex'] == result['addresses']['receiving'][i]['private_key_hex']
assert decrypted['addresses']['receiving'][i]['private_key_wif'] == result['addresses']['receiving'][i]['private_key_wif']
def test_hd_decrypt_wrong_password():
result = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC, num_addresses=1)
encrypted = encrypt_wallet(result, TEST_PASSWORD)
try:
decrypt_wallet(encrypted, "wrong_password")
assert False, "Should have raised ValueError"
except ValueError:
pass
def test_hd_encrypt_does_not_mutate_original():
result = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC, num_addresses=1)
original_seed = result['keystore']['seed']
encrypt_wallet(result, TEST_PASSWORD)
assert result['keystore']['seed'] == original_seed
def test_hd_decrypt_unencrypted_is_noop():
result = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC, num_addresses=1)
decrypted = decrypt_wallet(result, TEST_PASSWORD)
assert decrypted['keystore']['seed'] == result['keystore']['seed']
# --- Error cases ---
def test_hd_invalid_network_raises():
import pytest
with pytest.raises(ValueError):
generate_hd_wallet('invalid_net', 'bip84', TEST_MNEMONIC)
def test_hd_invalid_bip_type_raises():
import pytest
with pytest.raises(ValueError):
generate_hd_wallet('mainnet', 'bip99', TEST_MNEMONIC)
# --- BIP39 passphrase changes derived addresses ---
def test_hd_passphrase_changes_addresses():
r_no_pass = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC, passphrase='', num_addresses=1)
r_with_pass = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC, passphrase='secret', num_addresses=1)
assert r_no_pass['addresses']['receiving'][0]['address'] != r_with_pass['addresses']['receiving'][0]['address']
def test_hd_passphrase_changes_xpub():
r1 = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC, passphrase='')
r2 = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC, passphrase='passphrase')
assert r1['keystore']['xpub'] != r2['keystore']['xpub']
def test_hd_same_passphrase_same_addresses():
r1 = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC, passphrase='TREZOR', num_addresses=2)
r2 = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC, passphrase='TREZOR', num_addresses=2)
assert r1['addresses']['receiving'] == r2['addresses']['receiving']
# --- Account index changes derived addresses ---
def test_hd_different_account_different_addresses():
r0 = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC, account=0, num_addresses=1)
r1 = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC, account=1, num_addresses=1)
assert r0['addresses']['receiving'][0]['address'] != r1['addresses']['receiving'][0]['address']
def test_hd_different_account_different_xpub():
r0 = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC, account=0)
r1 = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC, account=1)
assert r0['keystore']['xpub'] != r1['keystore']['xpub']
def test_hd_account_reflected_in_derivation_path():
r = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC, account=2)
assert r['keystore']['derivation'] == "m/84'/0'/2'"
def test_hd_address_path_uses_correct_account():
r = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC, account=1, num_addresses=1)
assert r['addresses']['receiving'][0]['path'].startswith("m/84'/0'/1'/")
# --- words_num generates correct mnemonic length ---
def test_hd_words_num_15():
r = generate_hd_wallet('mainnet', 'bip84', words_num=15)
assert len(r['keystore']['seed'].split()) == 15
def test_hd_words_num_18():
r = generate_hd_wallet('mainnet', 'bip84', words_num=18)
assert len(r['keystore']['seed'].split()) == 18
def test_hd_words_num_21():
r = generate_hd_wallet('mainnet', 'bip84', words_num=21)
assert len(r['keystore']['seed'].split()) == 21
def test_hd_words_num_24():
r = generate_hd_wallet('mainnet', 'bip84', words_num=24)
assert len(r['keystore']['seed'].split()) == 24
# --- SecretScan ---
def test_hd_print_secretscan(capsys):
result = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC, num_addresses=3)
for a in result['addresses']['receiving']:
url = SECRETSCAN_URL.format(a['address'])
print(f"\n[HD BIP84] [{a['index']}] {a['address']}")
print(f"[HD BIP84] Verify on SecretScan: {url}")
captured = capsys.readouterr()
assert captured.out.count('secretscan.org') == 3