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
413 lines
15 KiB
Python
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
|