bip_hd: a minimal, easy-to-use BIP-32/BIP-44 implementation

- this is a work in progress, only a few coins are currently supported

Testing:

    $ test/unit_tests.py -v bip_hd

Examples:

    $ PYTHONPATH=. examples/bip_hd.py
This commit is contained in:
The MMGen Project 2024-01-26 10:54:03 +00:00
commit ea1e8d1228
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
9 changed files with 2363 additions and 2 deletions

88
examples/bip_hd.py Executable file
View file

@ -0,0 +1,88 @@
#!/usr/bin/env python3
#
# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
# Copyright (C)2013-2024 The MMGen Project <mmgen@tuta.io>
# Licensed under the GNU General Public License, Version 3:
# https://www.gnu.org/licenses
# Public project repositories:
# https://github.com/mmgen/mmgen-wallet
# https://gitlab.com/mmgen/mmgen-wallet
"""
examples/bip_hd.py: Usage examples for the MMGen BIP-32/-44 hierarchical/deterministic library
"""
from mmgen.cfg import Config
from mmgen.util import fmt
from mmgen.bip39 import bip39
from mmgen.bip_hd import MasterNode,BipHDNode
cfg = Config()
bip39_mnemonic = 'cat swing flag economy stadium alone churn speed unique patch report train'
seed = bip39().generate_seed(bip39_mnemonic.split())
m = MasterNode(cfg, seed)
# Derive sample path:
# to_chain() derives default chain for coin/addr_type pair:
dfl_pub_chain = m.to_chain(idx=0, coin='ltc', addr_type='bech32')
dfl_chg_chain = m.to_chain(idx=1, coin='ltc', addr_type='bech32')
print(f'Default path (LTC, bech32):\n')
print(f' public chain xpub:\n {dfl_pub_chain.xpub}\n')
print(f' internal chain xpub:\n {dfl_chg_chain.xpub}\n')
print(f' public chain addr 0:\n {dfl_pub_chain.derive_public(0).address}\n')
print(f' public chain addr 1:\n {dfl_pub_chain.derive_public(1).address}\n')
# Derive sample path using path string:
dfl_pub_chain_from_path = BipHDNode.from_path(
base_cfg = cfg,
seed = seed,
# purpose=84 (bech32 [BIP-84]), coin_type=2 (LTC mainnet [SLIP-44]), account=0, chain=0 (public)
# as per BIP-44, ‘purpose’, ‘coin_type’ and ‘account’ are hardened, while ‘chain’ is not
path_str = "m/84'/2'/0'/0",
coin = 'ltc',
addr_type = 'bech32')
assert dfl_pub_chain_from_path.xpub == dfl_pub_chain.xpub
# Derive sample path step-by-step:
# Configure master node with coin/addr_type pair:
master = m.init_cfg(coin='ltc', addr_type='bech32')
# ‘idx’ and ‘hardened’ args may be omitted at depths where defaults exist:
purpose = master.derive_private() # ‘idx’ is auto-computed from addr_type (BIP-44/49/84)
coin_type = purpose.derive_private() # ‘idx’ is auto-computed from coin/network (SLIP-44)
account = coin_type.derive_private(idx=0)
pub_chain = account.derive_public(idx=0)
assert pub_chain.xpub == dfl_pub_chain.xpub
# Initialize node from xpub:
pub_chain_from_xpub = BipHDNode.from_extended_key(cfg, 'ltc', pub_chain.xpub)
assert pub_chain_from_xpub.xpub == pub_chain.xpub
# To derive arbitrary BIP-32 paths, ignoring BIP-44, specify ‘no_path_checks’
nonstd_path = BipHDNode.from_path(
base_cfg = cfg,
seed = seed,
path_str = "m/111'/222/333/444",
coin = 'eth',
addr_type = 'E',
no_path_checks = True)
print(f'Non-standard path (ETH):\n')
print(f' xpub:\n {nonstd_path.xpub}\n')
print(f' WIF key:\n {nonstd_path.privkey.wif}\n')
print(f' address:\n {nonstd_path.address}\n')
# Display parsed xpub:
parsed_xpub = nonstd_path.key_extended(public=True)
print('Default path parsed xpub:\n')
print(fmt(str(parsed_xpub), indent=' '))

504
mmgen/bip_hd/__init__.py Normal file
View file

@ -0,0 +1,504 @@
#!/usr/bin/env python3
#
# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
# Copyright (C)2013-2024 The MMGen Project <mmgen@tuta.io>
# Licensed under the GNU General Public License, Version 3:
# https://www.gnu.org/licenses
# Public project repositories:
# https://github.com/mmgen/mmgen-wallet
# https://gitlab.com/mmgen/mmgen-wallet
"""
bip_hd: BIP-44/49/84, SLIP-44 hierarchical-deterministic key derivation library
"""
# One motivation for this implementation:
# https://blog.unit410.com/bitcoin/bip32/bip39/kdf/2021/05/17/inconsistent-bip32-derivations.html
import hmac
from collections import namedtuple
from ..cfg import Config
from ..util import is_int, fmt
from ..base_obj import Lockable
from ..keygen import KeyGenerator, keygen_public_data
from ..addrgen import AddrGenerator
from ..addr import MMGenAddrType
from ..key import PrivKey
from ..protocol import CoinProtocol
from ..proto.btc.common import hash160, b58chk_encode, b58chk_decode
from ..proto.secp256k1.secp256k1 import pubkey_tweak_add, pubkey_check
from . import chainparams
chainparams_data = chainparams.parse_data()
secp256k1_order = CoinProtocol.Secp256k1.secp256k1_group_order
hardened_idx0 = 0x80000000
def get_chain_params(bipnum, chain):
return chainparams_data[f'bip-{bipnum}'][chain.upper()]
def get_version_bytes(bip_proto, coin, public):
return getattr(
chainparams_data[f'bip-{bip_proto}'][coin],
'vb_pub' if public else 'vb_prv')
def parse_version_bytes(vb_hex):
e = chainparams_data['defaults']
if vb_hex in (e.vb_pub, e.vb_prv):
return (None, e)
for bipnum in (49, 84, 86, 44): # search bip-44 last, since it has the most entries
for e in chainparams_data[f'bip-{bipnum}'].values():
if vb_hex in (e.vb_pub, e.vb_prv):
return (bipnum, e)
else:
raise ValueError(f'0x{vb_hex}: unrecognized extended key version bytes')
def compress_pubkey(pubkey_bytes):
# see: proto.secp256k1.keygen.pubkey_format()
return (b'\x02',b'\x03')[pubkey_bytes[-1] & 1] + pubkey_bytes[1:33]
def decompress_pubkey(pubkey_bytes):
import ecdsa
return b'\x04' + ecdsa.VerifyingKey.from_string(pubkey_bytes, curve=ecdsa.curves.SECP256k1).to_string()
class Bip32ExtendedKey(Lockable):
def __init__(self, key_b58):
try:
key = b58chk_decode(key_b58)
except Exception as e:
raise type(e)(f'invalid extended key: {e}')
assert len(key) == 78, f'len(key) == {len(key)} (not 78)'
# Serialization:
# ver_bytes | depth | par_print | idx | chaincode | serialized_key
# 0:4 (4) | 4 (1) | 5:9 (4) | 9:13 (4) | 13:45 (32) | 45(46): 33(32)
ver_hex = key[:4].hex()
bipnum, cp_entry = parse_version_bytes(ver_hex)
public = ver_hex == cp_entry.vb_pub
idx_raw = int.from_bytes(key[9:13])
self.base58 = key_b58
self.ver_bytes = key[:4]
self.depth = key[4]
self.par_print = key[5:9]
self.idx = idx_raw if idx_raw < hardened_idx0 else idx_raw - hardened_idx0
self.chaincode = key[13:45]
self.key = key[45 if public else 46:]
self.hardened = idx_raw >= hardened_idx0 or self.depth == 0
self.bip_proto = bipnum or 44
self.network = cp_entry.network if bipnum else 'mainnet'
self.public = public
self.coin = cp_entry.chain if bipnum and cp_entry.chain != 'BTC' else '-'
if self.public:
if not key[45] in (2, 3):
raise ValueError(f'0x{key[45]:02x}: invalid first byte for public key data (not 2 or 3)')
elif key[45]:
raise ValueError(f'0x{key[45]:02x}: invalid first byte for private key data (not zero)')
if self.depth == 0:
if self.par_print != bytes(4):
raise ValueError(f'{self.par_print.hex()}: non-zero parent fingerprint at depth 0')
if idx_raw:
raise ValueError(f'{idx_raw}: non-zero index at depth 0')
def __str__(self):
return fmt(f"""
base58: {self.base58}
ver_bytes: {self.ver_bytes.hex()}
depth: {self.depth} [{bip_hd_nodes[self.depth].desc}]
par_print: {self.par_print.hex()}
idx: {self.idx}
chaincode: {self.chaincode.hex()}
key: {self.key.hex()}
hardened: {self.hardened}
bip_proto: {self.bip_proto}
network: {self.network}
public: {self.public}
coin: {self.coin}
""")
def get_bip_by_addr_type(addr_type):
return (
84 if addr_type.name == 'bech32' else
49 if addr_type.name == 'segwit' else
44)
def check_privkey(key_int):
if key_int == 0:
raise ValueError('private key is zero!')
elif key_int >= secp256k1_order:
raise ValueError(f'{key_int:x}: private key >= group order!')
class BipHDConfig(Lockable):
supported_coins = ('btc', 'eth', 'doge', 'ltc')
def __init__(self, base_cfg, coin, network, addr_type, from_path, no_path_checks):
if not coin.lower() in self.supported_coins:
raise ValueError(f'bip_hd: coin {coin.upper()} not supported')
base_cfg = Config({
'_clone': base_cfg,
'coin': coin,
'network': network,
'type': addr_type or None,
'quiet': True
})
dfl_type = base_cfg._proto.dfl_mmtype
addr_type = MMGenAddrType(
proto = base_cfg._proto,
id_str = base_cfg.type or ('C' if dfl_type == 'L' else dfl_type))
self.base_cfg = base_cfg
self.addr_type = addr_type
self.kg = KeyGenerator(base_cfg, base_cfg._proto, addr_type.pubkey_type)
self.ag = AddrGenerator(base_cfg, base_cfg._proto, addr_type)
self.bip_proto = get_bip_by_addr_type(addr_type)
self.from_path = from_path
self.no_path_checks = no_path_checks
class MasterNode(Lockable):
desc = 'Unconfigured Bip32 Master Node'
_use_class_attr = True
def __init__(self, base_cfg, bytes_data):
H = hmac.digest(b'Bitcoin seed', bytes_data, 'sha512')
self.par_print = bytes(4)
self.depth = 0
self.key = H[:32]
self.chaincode = H[32:]
self.idx = 0
self.hardened = True
self.public = False
self.base_cfg = base_cfg
check_privkey(int.from_bytes(self.key))
def init_cfg(self, coin=None, network=None, addr_type=None, from_path=False, no_path_checks=False):
new = BipHDNodeMaster()
new.cfg = BipHDConfig(self.base_cfg, coin, network, addr_type, from_path, no_path_checks)
new.par_print = self.par_print
new.depth = self.depth
new.key = self.key
new.chaincode = self.chaincode
new.idx = self.idx
new.hardened = self.hardened
new.public = self.public
new._lock()
return new
def to_coin_type(self, coin=None, network=None, addr_type=None):
return self.init_cfg(coin, network, addr_type).to_coin_type()
def to_chain(self, idx, coin=None, network=None, addr_type=None, hardened=False, public=False):
return self.init_cfg(coin, network, addr_type).to_chain(
idx = idx,
hardened = hardened,
public = public)
class BipHDNode(Lockable):
_autolock = False
_generated_pubkey = None
_set_ok = ('_generated_pubkey',)
def check_param(self, name, val):
cls = type(self)
if val is None:
if not hasattr(cls, name):
raise ValueError(f'{name}’ at depth {self.depth} ({self.desc!r}) must be set')
elif hasattr(cls, name) and val != getattr(cls, name):
raise ValueError(
'{}: invalid value for ‘{}’ at depth {} ({!r}) (must be {})'.format(
val, name, self.depth, self.desc,
'None' if getattr(cls, name) is None else f'None or {getattr(cls, name)}')
)
def set_params(self, cfg, idx, hardened):
self.check_param('idx', idx)
self.check_param('hardened', hardened)
return (
type(self).idx if idx is None else idx,
type(self).hardened if hardened is None else hardened)
@property
def privkey(self):
assert not self.public
return PrivKey(
self.cfg.base_cfg._proto,
self.key,
compressed = self.cfg.addr_type.compressed,
pubkey_type = self.cfg.addr_type.pubkey_type)
@property
def pubkey_bytes(self):
if self.public:
return self.key
elif self.cfg.addr_type.compressed:
return self.priv2pub().pubkey
else:
return compress_pubkey(self.priv2pub().pubkey)
def priv2pub(self):
if not self._generated_pubkey:
self._generated_pubkey = self.cfg.kg.gen_data(self.privkey)
return self._generated_pubkey
@property
def address(self):
return self.cfg.ag.to_addr(
keygen_public_data(
pubkey = self.key if self.cfg.addr_type.compressed else decompress_pubkey(self.key),
viewkey_bytes = None,
pubkey_type = self.cfg.addr_type.pubkey_type,
compressed = self.cfg.addr_type.compressed)
if self.public else
self.priv2pub()
)
# Extended keys can be identified by the Hash160 (RIPEMD160 after SHA256) of the serialized ECDSA
# public key K, ignoring the chain code. This corresponds exactly to the data used in traditional
# Bitcoin addresses. It is not advised to represent this data in base58 format though, as it may be
# interpreted as an address that way (and wallet software is not required to accept payment to the
# chain key itself).
@property
def id(self):
return hash160(self.pubkey_bytes)
# The first 32 bits of the identifier are called the key fingerprint.
@property
def fingerprint(self):
return self.id[:4]
@property
def xpub(self):
return self.key_extended(public=True, as_str=True)
@property
def xprv(self):
return self.key_extended(public=False, as_str=True)
def key_extended(self, public, as_str=False):
if self.public and not public:
raise ValueError('cannot create extended private key for public node!')
ret = b58chk_encode(
bytes.fromhex(get_version_bytes(self.cfg.bip_proto, self.cfg.base_cfg.coin, public))
+ int.to_bytes(self.depth, length=1)
+ self.par_print
+ int.to_bytes(self.idx + (hardened_idx0 if self.hardened and self.depth else 0), length=4)
+ self.chaincode
+ (self.pubkey_bytes if public else b'\x00' + self.key)
)
return ret if as_str else Bip32ExtendedKey(ret)
def derive_public(self, idx=None):
return self.derive(idx=idx, hardened=False, public=True)
def derive_private(self, idx=None, hardened=None):
return self.derive(idx=idx, hardened=hardened, public=False)
def derive(self, idx, hardened, public):
if self.public and not public:
raise ValueError('cannot derive private node from public node!')
new = bip_hd_nodes[self.depth + 1]()
new.depth = self.depth + 1
new.cfg = self.cfg
new.par_print = self.fingerprint
new.public = public
if new.cfg.no_path_checks:
new.idx, new.hardened = (idx, hardened)
else:
if new.public and type(new).hardened:
raise ValueError(
f'‘public’ requested, but node of depth {new.depth} ({new.desc}) must be hardened!')
new.idx, new.hardened = new.set_params(new.cfg, idx, hardened)
key_in = b'\x00' + self.key if new.hardened else self.pubkey_bytes
I = hmac.digest(
self.chaincode,
key_in + ((hardened_idx0 if new.hardened else 0) + new.idx).to_bytes(length=4),
'sha512')
pk_addend_bytes = I[:32]
new.chaincode = I[32:]
if new.public:
new.key = pubkey_tweak_add(key_in, pk_addend_bytes) # checks range of pk_addend
else:
pk_addend = int.from_bytes(pk_addend_bytes)
check_privkey(pk_addend)
key_int = (int.from_bytes(self.key) + pk_addend) % secp256k1_order
check_privkey(key_int)
new.key = int.to_bytes(key_int, length=32)
new._lock()
return new
@staticmethod
def from_path(
base_cfg,
seed,
path_str,
coin = None,
addr_type = None,
no_path_checks = False):
path = path_str.lower().split('/')
if path.pop(0) != 'm':
raise ValueError(f'{path_str}: invalid path string (first component is not "m")')
res = MasterNode(base_cfg, seed).init_cfg(
coin = coin or 'btc',
addr_type = addr_type or 'compressed',
no_path_checks = no_path_checks,
from_path = True)
for s in path:
for suf in ("'", 'h'):
if s.endswith(suf):
idx = s.removesuffix(suf)
hardened = True
break
else:
idx = s
hardened = False
if not is_int(idx):
raise ValueError(f'invalid path component {s!r}')
res = res.derive(int(idx), hardened, public=False)
return res
@staticmethod
# ‘addr_type’ is required for broken coins with duplicate version bytes across BIP protocols
# (i.e. Dogecoin)
def from_extended_key(base_cfg, coin, xkey_b58, addr_type=None):
xk = Bip32ExtendedKey(xkey_b58)
if xk.public:
pubkey_check(xk.key)
else:
check_privkey(int.from_bytes(xk.key))
addr_types = {
84: 'bech32',
49: 'segwit',
44: None
}
new = bip_hd_nodes[xk.depth]()
new.cfg = BipHDConfig(
base_cfg,
coin,
xk.network,
addr_type or addr_types[xk.bip_proto],
False,
False)
new.par_print = xk.par_print
new.depth = xk.depth
new.key = xk.key
new.chaincode = xk.chaincode
new.idx = xk.idx
new.hardened = xk.hardened
new.public = xk.public
new._lock()
return new
class BipHDNodeMaster(BipHDNode):
desc = 'Bip32 Master Node'
hardened = True
idx = None
def to_coin_type(self):
# purpose coin_type
return self.derive_private().derive_private()
def to_chain(self, idx, hardened=False, public=False):
# purpose coin_type account #0 chain
return self.derive_private().derive_private().derive_private(idx=0).derive(
idx = idx,
hardened = False if public else hardened,
public = public)
class BipHDNodePurpose(BipHDNode):
desc = 'Purpose'
hardened = True
def set_params(self, cfg, idx, hardened):
self.check_param('hardened', hardened)
if idx not in (None, cfg.bip_proto):
raise ValueError(
f'index for path component {self.desc!r} with address type {cfg.addr_type!r} '
f'must be {cfg.bip_proto}, not {idx}')
return (cfg.bip_proto, type(self).hardened)
class BipHDNodeCoinType(BipHDNode):
desc = 'Coin Type'
hardened = True
def set_params(self, cfg, idx, hardened):
self.check_param('hardened', hardened)
chain_idx = get_chain_params(
bipnum = get_bip_by_addr_type(cfg.addr_type),
chain = cfg.base_cfg.coin).idx
if idx not in (None, chain_idx):
raise ValueError(
f'index {idx} at depth {self.depth} ({self.desc}) does not match '
f'chain index {chain_idx} for coin {cfg.base_cfg.coin!r}')
return (chain_idx, type(self).hardened)
def to_chain(self, idx, hardened=False, public=False):
# account #0 chain
return self.derive_private(idx=0).derive(
idx = idx,
hardened = False if public else hardened,
public = public)
class BipHDNodeAccount(BipHDNode):
desc = 'Account'
hardened = True
class BipHDNodeChain(BipHDNode):
desc = 'Chain'
hardened = False
def set_params(self, cfg, idx, hardened):
self.check_param('hardened', hardened)
if idx not in (0, 1):
raise ValueError(
f'at depth {self.depth} ({self.desc}), ‘idx’ must be either 0 (external) or 1 (internal)')
return (idx, type(self).hardened)
class BipHDNodeAddrIdx(BipHDNode):
desc = 'Address Index'
hardened = False
bip_hd_nodes = {
0: BipHDNodeMaster,
1: BipHDNodePurpose,
2: BipHDNodeCoinType,
3: BipHDNodeAccount,
4: BipHDNodeChain,
5: BipHDNodeAddrIdx
}

1219
mmgen/bip_hd/chainparams.py Normal file

File diff suppressed because it is too large Load diff

View file

@ -1 +1 @@
14.1.dev3
14.1.dev4

View file

@ -0,0 +1,115 @@
#!/usr/bin/env python3
import json
from bip_utils.slip.slip44 import Slip44
from bip_utils.bip.conf.bip44.bip44_conf import Bip44Conf
from bip_utils.bip.conf.bip49.bip49_conf import Bip49Conf
from bip_utils.bip.conf.bip84.bip84_conf import Bip84Conf
from bip_utils.bip.conf.bip86.bip86_conf import Bip86Conf
import script_init
from mmgen.cfg import Config
from mmgen.main import launch
opts_data = {
'text': {
'desc': 'Aggregate bip_lib and SLIP-44 data into a bip_hd chainparams table',
'usage':'[opts] infile',
'options': """
-h, --help Print this help message.
""",
'notes': """
source: https://github.com/MetaMask/slip44/blob/main/slip44.json
"""
}
}
cfg = Config(opts_data=opts_data)
def curve_clsname_abbr(s):
return {
'Bip32Slip10Secp256k1': 'x',
'Bip32Slip10Ed25519': 'edw',
'Bip32Slip10Ed25519Blake2b': 'blk',
'Bip32Slip10Nist256p1': 'nist',
'Bip32KholawEd25519': 'khol',
}.get(s,s)
fs2 = '{:5} {:2} {:16} {:8} {:8} {:6} {:8} {:8}'
hdr2 = fs2.format('CURVE','NW','ADDR_CLS','VB_PRV','VB_PUB','VB_WIF','VB_ADDR','DFL_PATH')
dfl_vb_prv = '0488ade4'
dfl_vb_pub = '0488b21e'
dfl_curve = 'secp'
dfl_dfl_path = "0'/0/0"
def get_bip_utils_data(bipnum,n):
name,v = bip_utils_data[bipnum][n]
#pexit(v.__dict__)
vb_prv = v.m_key_net_ver.m_priv_net_ver.hex()
vb_pub = v.m_key_net_ver.m_pub_net_ver.hex()
ap = v.m_addr_params
return fs2.format(
curve_clsname_abbr(v.m_bip32_cls.__name__),
'T' if v.m_is_testnet else 'm',
v.m_addr_cls.__name__.removesuffix('AddrEncoder'),
'x' if vb_prv == dfl_vb_prv else vb_prv,
'x' if vb_pub == dfl_vb_pub else vb_pub,
v.m_wif_net_ver.hex() if isinstance(v.m_wif_net_ver,bytes) else '-',
ap['net_ver'].hex() if 'net_ver' in ap else 'h:'+ap['hrp'] if 'hrp' in ap else 'spec' if ap else '-',
'x' if v.m_def_path == dfl_dfl_path else v.m_def_path,
)
def gen():
def format_data(bipnum,n,sym,name):
return fs.format(
n,
sym if sym else '---',
get_bip_utils_data(bipnum,n) if bipnum else '-',
name if name else '---')
fs = '{:<6} {:6} {:1} {}'
yield f'[defaults]'
yield fs.format('IDX','CHAIN',hdr2,'NAME')
yield fs.format('0', '-', fs2.format(dfl_curve, '-', '-', dfl_vb_prv, dfl_vb_pub, '-', '-', dfl_dfl_path), '-')
yield f'\n[bip-44]'
yield fs.format('IDX','CHAIN',hdr2,'NAME')
for k,v in slip44_data.items():
if int(k) in bip_utils_data[44]:
yield format_data(44,int(k),v['symbol'],v['name'])
for bipnum in (49, 84, 86):
yield f'\n[bip-{bipnum}]'
yield fs.format('IDX','CHAIN',hdr2,'NAME')
for n,v in sorted(bip_utils_data[bipnum].items()):
nd = v[1].m_coin_names
yield format_data(bipnum,n,nd.m_abbr,nd.m_name)
yield f'\n[bip-44-unsupported]'
yield fs.format('IDX','CHAIN','','NAME')
for k,v in slip44_data.items():
if not int(k) in bip_utils_data[44]:
yield format_data(None,int(k),v['symbol'],v['name'])
def main():
global slip44_data, bip_utils_data
if len(cfg._args) != 1:
cfg._opts.usage()
with open(cfg._args[0]) as fh:
slip44_data = json.loads(fh.read())
bip_utils_data = {
n:{v.m_coin_idx:(k,v) for k,v in globals()[f'Bip{n}Conf'].__dict__.items() if not k.startswith('_')}
for n in (44, 49, 84, 86)
}
print('\n'.join(gen()))
launch(func=main)

View file

@ -35,7 +35,7 @@ classifiers =
Development Status :: 5 - Production/Stable
[options]
python_requires = >=3.8
python_requires = >=3.9
include_package_data = True
install_requires =
@ -53,6 +53,7 @@ install_requires =
packages =
mmgen
mmgen.altcoin
mmgen.bip_hd
mmgen.contrib
mmgen.data
mmgen.help

View file

@ -34,6 +34,7 @@ class CmdTestDev(CmdTestBase):
networks = ('btc',)
cmd_group = (
('compute_file_chksum', 'scripts/compute-file-chksum.py'),
('create_bip_hd_chain_params', 'scripts/create-bip-hd-chain-params.py'),
)
tmpdir_nums = [99]
color = True
@ -46,6 +47,15 @@ class CmdTestDev(CmdTestBase):
t.expect('3df942')
return t
def create_bip_hd_chain_params(self):
t = self._spawn('scripts/create-bip-hd-chain-params.py', ['test/ref/altcoin/slip44-mini.json'])
t.expect('[defaults]')
t.expect(r"secp.*0488ade4.*0488b21e.*0'\/0\/0",regex=True)
t.expect('[bip-44]')
t.expect('[bip-49]')
t.match_expect_list(['0','BTC','x','m','P2SH','049d7878','049d7cb2','80','05','x','Bitcoin','1'])
return t
class CmdTestMisc(CmdTestBase):
'miscellaneous tests (RPC backends, xmrwallet_txview, term)'
networks = ('btc',)
@ -56,6 +66,7 @@ class CmdTestMisc(CmdTestBase):
('xmrwallet_txview', "'mmgen-xmrwallet' txview"),
('xmrwallet_txlist', "'mmgen-xmrwallet' txlist"),
('coin_daemon_info', "'examples/coin-daemon-info.py'"),
('examples_bip_hd', "'examples/bip_hd.py'"),
('term_echo', "term.set('echo')"),
('term_cleanup', 'term.register_cleanup()'),
)
@ -87,6 +98,9 @@ class CmdTestMisc(CmdTestBase):
def xmrwallet_txlist(self):
return self.xmrwallet_txview(op='txlist')
def examples_bip_hd(self):
return self.spawn('examples/bip_hd.py',cmd_dir='.')
def coin_daemon_info(self):
if cfg.no_altcoin:
coins = ['btc']

View file

@ -0,0 +1 @@
{"0":{"index":"0","hex":"0x80000000","symbol":"BTC","name":"Bitcoin"},"1":{"index":"1","hex":"0x80000001","symbol":"","name":"Testnet (all coins)"},"2":{"index":"2","hex":"0x80000002","symbol":"LTC","name":"Litecoin"},"3":{"index":"3","hex":"0x80000003","symbol":"DOGE","name":"Dogecoin"},"5":{"index":"5","hex":"0x80000005","symbol":"DASH","name":"Dash"},"7":{"index":"7","hex":"0x80000007","symbol":"NMC","name":"Namecoin"},"99999999":{"index":"99999999","hex":"0x85f5e0ff","symbol":"QKC","name":"QuarkChain"},"608589380":{"index":"608589380","hex":"0xa4465644","symbol":"FVDC","name":"ForumCoin"},"1179993420":{"index":"1179993420","hex":"0xc655454c","symbol":"","name":"Fuel"}}

419
test/unit_tests_d/ut_bip_hd.py Executable file
View file

@ -0,0 +1,419 @@
#!/usr/bin/env python3
#
# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
# Copyright (C)2013-2024 The MMGen Project <mmgen@tuta.io>
# Licensed under the GNU General Public License, Version 3:
# https://www.gnu.org/licenses
# Public project repositories:
# https://github.com/mmgen/mmgen-wallet
# https://gitlab.com/mmgen/mmgen-wallet
"""
test.unit_tests_d.ut_bip_hd: bip_hd unit test for the MMGen suite
"""
from mmgen.color import gray,pink,blue
from mmgen.util import fmt
from mmgen.bip_hd import Bip32ExtendedKey,BipHDConfig,BipHDNode,MasterNode,get_chain_params
from ..include.common import cfg,vmsg
# Source: BIP-32
vectors_bip32 = [
{
'seed': '000102030405060708090a0b0c0d0e0f',
"m": {
'xpub': 'xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8',
'xprv': 'xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi',
},
"m/0'": {
'xpub': 'xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw',
'xprv': 'xprv9uHRZZhk6KAJC1avXpDAp4MDc3sQKNxDiPvvkX8Br5ngLNv1TxvUxt4cV1rGL5hj6KCesnDYUhd7oWgT11eZG7XnxHrnYeSvkzY7d2bhkJ7',
},
"m/0'/1": {
'xpub': 'xpub6ASuArnXKPbfEwhqN6e3mwBcDTgzisQN1wXN9BJcM47sSikHjJf3UFHKkNAWbWMiGj7Wf5uMash7SyYq527Hqck2AxYysAA7xmALppuCkwQ',
'xprv': 'xprv9wTYmMFdV23N2TdNG573QoEsfRrWKQgWeibmLntzniatZvR9BmLnvSxqu53Kw1UmYPxLgboyZQaXwTCg8MSY3H2EU4pWcQDnRnrVA1xe8fs',
},
"m/0'/1/2'": {
'xpub': 'xpub6D4BDPcP2GT577Vvch3R8wDkScZWzQzMMUm3PWbmWvVJrZwQY4VUNgqFJPMM3No2dFDFGTsxxpG5uJh7n7epu4trkrX7x7DogT5Uv6fcLW5',
'xprv': 'xprv9z4pot5VBttmtdRTWfWQmoH1taj2axGVzFqSb8C9xaxKymcFzXBDptWmT7FwuEzG3ryjH4ktypQSAewRiNMjANTtpgP4mLTj34bhnZX7UiM',
},
"m/0'/1/2'/2": {
'xpub': 'xpub6FHa3pjLCk84BayeJxFW2SP4XRrFd1JYnxeLeU8EqN3vDfZmbqBqaGJAyiLjTAwm6ZLRQUMv1ZACTj37sR62cfN7fe5JnJ7dh8zL4fiyLHV',
'xprv': 'xprvA2JDeKCSNNZky6uBCviVfJSKyQ1mDYahRjijr5idH2WwLsEd4Hsb2Tyh8RfQMuPh7f7RtyzTtdrbdqqsunu5Mm3wDvUAKRHSC34sJ7in334',
},
"m/0'/1/2'/2/1000000000": {
'xpub': 'xpub6H1LXWLaKsWFhvm6RVpEL9P4KfRZSW7abD2ttkWP3SSQvnyA8FSVqNTEcYFgJS2UaFcxupHiYkro49S8yGasTvXEYBVPamhGW6cFJodrTHy',
'xprv': 'xprvA41z7zogVVwxVSgdKUHDy1SKmdb533PjDz7J6N6mV6uS3ze1ai8FHa8kmHScGpWmj4WggLyQjgPie1rFSruoUihUZREPSL39UNdE3BBDu76',
},
},{
'seed': 'fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542',
'm': {
'xpub': 'xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB',
'xprv': 'xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U',
},
"m/0": {
'xpub': 'xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH',
'xprv': 'xprv9vHkqa6EV4sPZHYqZznhT2NPtPCjKuDKGY38FBWLvgaDx45zo9WQRUT3dKYnjwih2yJD9mkrocEZXo1ex8G81dwSM1fwqWpWkeS3v86pgKt',
},
"m/0/2147483647'": {
'xpub': 'xpub6ASAVgeehLbnwdqV6UKMHVzgqAG8Gr6riv3Fxxpj8ksbH9ebxaEyBLZ85ySDhKiLDBrQSARLq1uNRts8RuJiHjaDMBU4Zn9h8LZNnBC5y4a',
'xprv': 'xprv9wSp6B7kry3Vj9m1zSnLvN3xH8RdsPP1Mh7fAaR7aRLcQMKTR2vidYEeEg2mUCTAwCd6vnxVrcjfy2kRgVsFawNzmjuHc2YmYRmagcEPdU9',
},
"m/0/2147483647'/1": {
'xpub': 'xpub6DF8uhdarytz3FWdA8TvFSvvAh8dP3283MY7p2V4SeE2wyWmG5mg5EwVvmdMVCQcoNJxGoWaU9DCWh89LojfZ537wTfunKau47EL2dhHKon',
'xprv': 'xprv9zFnWC6h2cLgpmSA46vutJzBcfJ8yaJGg8cX1e5StJh45BBciYTRXSd25UEPVuesF9yog62tGAQtHjXajPPdbRCHuWS6T8XA2ECKADdw4Ef',
},
"m/0/2147483647'/1/2147483646'": {
'xpub': 'xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL',
'xprv': 'xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc',
},
"m/0/2147483647'/1/2147483646'/2": {
'xpub': 'xpub6FnCn6nSzZAw5Tw7cgR9bi15UV96gLZhjDstkXXxvCLsUXBGXPdSnLFbdpq8p9HmGsApME5hQTZ3emM2rnY5agb9rXpVGyy3bdW6EEgAtqt',
'xprv': 'xprvA2nrNbFZABcdryreWet9Ea4LvTJcGsqrMzxHx98MMrotbir7yrKCEXw7nadnHM8Dq38EGfSh6dqA9QWTyefMLEcBYJUuekgW4BYPJcr9E7j',
},
},{
'comment': 'These vectors test for the retention of leading zeros. See bitpay/bitcore-lib#47 and iancoleman/bip39#58 for more information.',
'seed': '4b381541583be4423346c643850da4b320e46a87ae3d2a4e6da11eba819cd4acba45d239319ac14f863b8d5ab5a0d0c64d2e8a1e7d1457df2e5a3c51c73235be',
'm': {
'xpub': 'xpub661MyMwAqRbcEZVB4dScxMAdx6d4nFc9nvyvH3v4gJL378CSRZiYmhRoP7mBy6gSPSCYk6SzXPTf3ND1cZAceL7SfJ1Z3GC8vBgp2epUt13',
'xprv': 'xprv9s21ZrQH143K25QhxbucbDDuQ4naNntJRi4KUfWT7xo4EKsHt2QJDu7KXp1A3u7Bi1j8ph3EGsZ9Xvz9dGuVrtHHs7pXeTzjuxBrCmmhgC6',
},
"m/0'": {
'xpub': 'xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y',
'xprv': 'xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L',
},
},{
'comment': 'These vectors test for the retention of leading zeros. See btcsuite/btcutil#172 for more information.',
'seed': '3ddd5602285899a946114506157c7997e5444528f3003f6134712147db19b678',
"m": {
'xpub': 'xpub661MyMwAqRbcGczjuMoRm6dXaLDEhW1u34gKenbeYqAix21mdUKJyuyu5F1rzYGVxyL6tmgBUAEPrEz92mBXjByMRiJdba9wpnN37RLLAXa',
'xprv': 'xprv9s21ZrQH143K48vGoLGRPxgo2JNkJ3J3fqkirQC2zVdk5Dgd5w14S7fRDyHH4dWNHUgkvsvNDCkvAwcSHNAQwhwgNMgZhLtQC63zxwhQmRv',
},
"m/0'": {
'xpub': 'xpub69AUMk3qDBi3uW1sXgjCmVjJ2G6WQoYSnNHyzkmdCHEhSZ4tBok37xfFEqHd2AddP56Tqp4o56AePAgCjYdvpW2PU2jbUPFKsav5ut6Ch1m',
'xprv': 'xprv9vB7xEWwNp9kh1wQRfCCQMnZUEG21LpbR9NPCNN1dwhiZkjjeGRnaALmPXCX7SgjFTiCTT6bXes17boXtjq3xLpcDjzEuGLQBM5ohqkao9G',
},
"m/0'/1'": {
'xpub': 'xpub6BJA1jSqiukeaesWfxe6sNK9CCGaujFFSJLomWHprUL9DePQ4JDkM5d88n49sMGJxrhpjazuXYWdMf17C9T5XnxkopaeS7jGk1GyyVziaMt',
'xprv': 'xprv9xJocDuwtYCMNAo3Zw76WENQeAS6WGXQ55RCy7tDJ8oALr4FWkuVoHJeHVAcAqiZLE7Je3vZJHxspZdFHfnBEjHqU5hG1Jaj32dVoS6XLT1',
},
}]
# Source: BIP-32
# These vectors test that invalid extended keys are recognized as invalid.
vectors_bip32_invalid = [
('xpub661MyMwAqRbcEYS8w7XLSVeEsBXy79zSzH1J8vCdxAZningWLdN3zgtU6LBpB85b3D2yc8sfvZU521AAwdZafEz7mnzBBsz4wKY5fTtTQBm', 'pubkey version / prvkey mismatch'),
('xprv9s21ZrQH143K24Mfq5zL5MhWK9hUhhGbd45hLXo2Pq2oqzMMo63oStZzFGTQQD3dC4H2D5GBj7vWvSQaaBv5cxi9gafk7NF3pnBju6dwKvH', 'prvkey version / pubkey mismatch'),
('xpub661MyMwAqRbcEYS8w7XLSVeEsBXy79zSzH1J8vCdxAZningWLdN3zgtU6Txnt3siSujt9RCVYsx4qHZGc62TG4McvMGcAUjeuwZdduYEvFn', 'invalid pubkey prefix 04'),
('xprv9s21ZrQH143K24Mfq5zL5MhWK9hUhhGbd45hLXo2Pq2oqzMMo63oStZzFGpWnsj83BHtEy5Zt8CcDr1UiRXuWCmTQLxEK9vbz5gPstX92JQ', 'invalid prvkey prefix 04'),
('xpub661MyMwAqRbcEYS8w7XLSVeEsBXy79zSzH1J8vCdxAZningWLdN3zgtU6N8ZMMXctdiCjxTNq964yKkwrkBJJwpzZS4HS2fxvyYUA4q2Xe4', 'invalid pubkey prefix 01'),
('xprv9s21ZrQH143K24Mfq5zL5MhWK9hUhhGbd45hLXo2Pq2oqzMMo63oStZzFAzHGBP2UuGCqWLTAPLcMtD9y5gkZ6Eq3Rjuahrv17fEQ3Qen6J', 'invalid prvkey prefix 01'),
('xprv9s2SPatNQ9Vc6GTbVMFPFo7jsaZySyzk7L8n2uqKXJen3KUmvQNTuLh3fhZMBoG3G4ZW1N2kZuHEPY53qmbZzCHshoQnNf4GvELZfqTUrcv', 'zero depth with non-zero parent fingerprint'),
('xpub661no6RGEX3uJkY4bNnPcw4URcQTrSibUZ4NqJEw5eBkv7ovTwgiT91XX27VbEXGENhYRCf7hyEbWrR3FewATdCEebj6znwMfQkhRYHRLpJ', 'zero depth with non-zero parent fingerprint'),
('xprv9s21ZrQH4r4TsiLvyLXqM9P7k1K3EYhA1kkD6xuquB5i39AU8KF42acDyL3qsDbU9NmZn6MsGSUYZEsuoePmjzsB3eFKSUEh3Gu1N3cqVUN', 'zero depth with non-zero index'),
('xpub661MyMwAuDcm6CRQ5N4qiHKrJ39Xe1R1NyfouMKTTWcguwVcfrZJaNvhpebzGerh7gucBvzEQWRugZDuDXjNDRmXzSZe4c7mnTK97pTvGS8', 'zero depth with non-zero index'),
('DMwo58pR1QLEFihHiXPVykYB6fJmsTeHvyTp7hRThAtCX8CvYzgPcn8XnmdfHGMQzT7ayAmfo4z3gY5KfbrZWZ6St24UVf2Qgo6oujFktLHdHY4', 'unknown extended key version'),
('DMwo58pR1QLEFihHiXPVykYB6fJmsTeHvyTp7hRThAtCX8CvYzgPcn8XnmdfHPmHJiEDXkTiJTVV9rHEBUem2mwVbbNfvT2MTcAqj3nesx8uBf9', 'unknown extended key version'),
('xprv9s21ZrQH143K24Mfq5zL5MhWK9hUhhGbd45hLXo2Pq2oqzMMo63oStZzF93Y5wvzdUayhgkkFoicQZcP3y52uPPxFnfoLZB21Teqt1VvEHx', 'private key 0 not in 1..n-1'),
('xprv9s21ZrQH143K24Mfq5zL5MhWK9hUhhGbd45hLXo2Pq2oqzMMo63oStZzFAzHGBP2UuGCqWLTAPLcMtD5SDKr24z3aiUvKr9bJpdrcLg1y3G', 'private key n not in 1..n-1'),
('xpub661MyMwAqRbcEYS8w7XLSVeEsBXy79zSzH1J8vCdxAZningWLdN3zgtU6Q5JXayek4PRsn35jii4veMimro1xefsM58PgBMrvdYre8QyULY', 'invalid pubkey 02000000...07'),
('xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHL', 'invalid checksum'),
]
# Source: bip_utils
vectors_derive = {
'bech32': {
0: 'bc1qwg77fxw0tkmc3h58tcnnpegxk7mp3h6ly44d3n',
1: 'bc1q6g79y6kwpkufevv2njacvnqnsdxmen68jyvjde',
2: 'bc1qknujpwlxc9e9e6avz50q5k90p552xy8g3qjd8u',
}
}
# Source: bip_utils
vectors_addrfmt = {
'pub': {
'compressed': 'xpub6GJknXsmpFcEubJsddGacncHhyY5Bk9zMQKC8pC97vBVAphrchYxuoJsAqfZW2uEMfPr6umSPRrhuaA7zeuExkwuAWiUcKcXjSf437VMLwR',
'segwit': 'ypub6aNved9dRKbMRjLfMbGPoXKgNN1tr86qi8WoBxSVr4mHX9fdzUxJFtEH63QZZxArk4f2fwFZQUQ7FRkNiBarTLu2Y69SRxzn68WysngXPrp',
'bech32': 'zpub6urFg31yVogtpf3Y7aV3CLuxQUsEpZ2asqBx3fzYMuFRTFrMNKxn5B2QXAGMSuwVfEA9KSJr2CUs8vqbmhUCkzVvxysB4p6vybLS2CgQnze',
},
'prv': {
'compressed': 'xprvA3KQP2Lsyt3wh7EQXbjaFefZ9whanHS8zBPbLRnXZaeWJ2Ni5AEiMzzPKbKHsG7Dn6hkhSkG8V4H4XUsjszxU4nd2sMnc5ag9sHLLYBqrr4',
'segwit': 'yprvAMPaF7cjax34DFGCFZjPSPNwpLBQSfNzLubCPa2tHjEJeMLVSwe3i5uoEnUg4etxP3XEqr5ZJinjGkUJrte3xFNZ1jVKbjVaFJVHi4Msekw',
'bech32': 'zprvAgruGXV5fS8bcAy51Yx2qCyDrT2kR6JjWcGMFHavoZiSaTXCpneXXNhvfsMLeyBmjzACqkpxB2KGCMAe85wUrW1dnenu6kVHCk4kXh7XFE6',
}
}
# Source: Asgardex Wallet
vectors_multicoin = {
'btc_bech32': 'bc1qwg77fxw0tkmc3h58tcnnpegxk7mp3h6ly44d3n',
'eth': '373731f4d885Fc7Da05498F9f0804a87A14F891b',
'doge': 'DFX88RXpi4S4W24YVvuMgbdUcCAYNeEYGd',
'avax-c': '0x373731f4d885Fc7Da05498F9f0804a87A14F891b',
'ltc_bech32': 'ltc1q3uh5ga5cp9kkdfx6a52uymxj9keq4tpzep7er0',
'bch_cashaddr': 'bitcoincash:qpqpcllprftg4s0chdgkpxhxv23wfymq3gj7n0a9vw',
'bsc_smart': '0x373731f4d885Fc7Da05498F9f0804a87A14F891b',
'bnb_beacon': 'bnb179c3ymltqm4utlp089zxqeta5dvn48a305rhe5',
}
def wif2addr(cfg,wif):
from mmgen.tool.coin import tool_cmd
return tool_cmd(
cfg = cfg.base_cfg,
cmdname = 'wif2addr',
proto = cfg.base_cfg._proto,
mmtype = cfg.addr_type).wif2addr(wif)
class unit_tests:
altcoin_deps = ('multicoin',)
@property
def _seed(self):
if not hasattr(self,'__seed'):
with open('test/ref/98831F3A.bip39') as fh:
mnemonic = fh.read().strip()
from mmgen.bip39 import bip39
self.__seed = bip39().generate_seed(mnemonic.split())
return self.__seed
def chainparams(self,name,ut):
for bipnum,idx,chain,addr_cls in (
(44, 0, 'btc', 'P2PKH'),
(49, 0, 'btc', 'P2SH'),
(84, 0, 'btc', 'P2WPKH'),
(44, 60, 'eth', 'Eth'),
(44, 61, 'etc', 'Eth'),
(44, 2, 'ltc', 'P2PKH'),
(44, 3, 'doge', 'P2PKH'),
):
res = get_chain_params(bipnum,chain)
assert res.idx == idx, res.idx
assert res.chain == chain.upper()
assert res.addr_cls == addr_cls
vmsg(f' {res}')
vmsg('')
return True
def derive(self,name,ut):
vmsg('seed: 98831F3A (default derivation)')
m = MasterNode(cfg,self._seed)
purpose = m.init_cfg(coin='btc',addr_type='bech32').derive_private()
vmsg(f' {purpose.address=}')
coin_type1 = purpose.derive_private()
coin_type2 = m.to_coin_type('btc',addr_type='bech32')
assert coin_type1.address == coin_type2.address
vmsg(f' {coin_type1.address=}')
acct = coin_type2.derive_private(idx=0)
chain1 = acct.derive_private(idx=0,hardened=False)
chain2 = m.to_chain(idx=0,coin='btc',addr_type='bech32',public=False)
assert chain2.address == chain1.address
chain3 = m.to_coin_type(coin='btc',addr_type='bech32').to_chain(0,public=True)
assert chain3.address == chain1.address
vmsg(f' {chain1.address=}')
a = BipHDNode.from_extended_key(cfg,'btc',chain2.xpub)
b = BipHDNode.from_extended_key(cfg,'btc',chain2.xprv)
vmsg(
'\n xpub:\n' +
fmt(str(Bip32ExtendedKey(b.xpub)),indent=' ')
)
assert a.xpub == b.xpub
vmsg(' Addresses:')
for i in range(3):
res = chain1.derive_public(i)
vmsg(f' {i} {res.address}')
assert res.address == vectors_derive['bech32'][i]
res = chain1.derive_private(i)
assert res.address == vectors_derive['bech32'][i]
vmsg('')
return True
def derive_addrfmt(self,name,ut):
vmsg('seed: 98831F3A (default derivation)')
m = MasterNode(cfg,self._seed)
for addr_type in ('compressed','segwit','bech32'):
chk_xpub = vectors_addrfmt['pub'][addr_type]
chk_xprv = vectors_addrfmt['prv'][addr_type]
res1 = m.to_chain(idx=0,coin='btc',addr_type=addr_type).derive_public(0)
vmsg(f' {addr_type}: {res1.xpub}')
assert res1.xpub == chk_xpub
res2 = m.to_chain(idx=0,coin='btc',addr_type=addr_type).derive_private(0,False)
vmsg(f' {addr_type}: {res2.xprv}')
assert res2.xprv == chk_xprv
assert res2.xpub == chk_xpub
assert res2.address == wif2addr(res2.cfg, res2.privkey.wif)
vmsg('')
return True
def path(self,name,ut):
for vec in vectors_bip32:
seed = bytes.fromhex(vec['seed'])
vmsg(f'Seed: {vec["seed"]}')
for n,path_str in enumerate(vec):
if path_str in ('seed','comment'):
continue
path_arg = path_str.replace("'",'H') if n % 2 else path_str
node = BipHDNode.from_path(cfg,seed,path_arg,no_path_checks=True)
vmsg(' Path {} {}'.format(pink(path_str),blue('('+node.desc+')')))
for xkey_type in ('xpub','xprv'):
vmsg(f' {getattr(node,xkey_type)}')
assert getattr(node,xkey_type) == vec[path_str][xkey_type]
vmsg('')
return True
def parse_extended(self,name,ut):
vmsg('Parsing and validating extended keys:\n')
for vec in vectors_bip32:
seed = bytes.fromhex(vec['seed'])
vmsg(f' Seed: {vec["seed"]}')
for n,path_str in enumerate(vec):
if path_str in ('seed','comment'):
continue
vmsg(' Path {}'.format(pink(path_str)))
for xkey_type in ('xpub','xprv'):
xkey = vec[path_str][xkey_type]
vmsg(f' {xkey}')
node = BipHDNode.from_extended_key(cfg,'btc',xkey)
assert getattr(node,xkey_type) == xkey
vmsg('')
return True
def multicoin(self,name,ut):
m = MasterNode(cfg,self._seed)
fs = ' {:6} {:10} {}'
vmsg(fs.format('COIN','ADDR_TYPE','ADDR'))
for id_str,addr_chk in vectors_multicoin.items():
ss = id_str.split('_')
coin = ss[0]
addr_type = ss[1] if len(ss) == 2 else None
if coin not in BipHDConfig.supported_coins:
vmsg(gray(fs.format(coin.upper(), (addr_type or ''), '[not supported yet]')))
continue
node = m.to_chain(idx=0,coin=coin,addr_type=addr_type).derive_private(0)
xpub_parsed = node.key_extended(public=True)
xprv_parsed = node.key_extended(public=False)
addr = node.address
at_arg = 'compressed' if coin == 'doge' else None
from_xpub = BipHDNode.from_extended_key(node.cfg.base_cfg, coin, xpub_parsed.base58, addr_type=at_arg)
from_xprv = BipHDNode.from_extended_key(node.cfg.base_cfg, coin, xprv_parsed.base58, addr_type=at_arg)
assert from_xpub.xpub == node.xpub, f'{from_xpub.xpub=} != {node.xpub}'
assert from_xprv.xpub == node.xpub, f'{from_xprv.xpub=} != {node.xpub}'
assert from_xpub.address == addr, f'{from_xpub.address} != {addr}'
assert from_xprv.address == addr, f'{from_xprv.address} != {addr}'
addr_from_wif = wif2addr(node.cfg, node.privkey.wif)
proto = node.cfg.base_cfg._proto
if proto.base_proto == 'Ethereum':
addr = proto.checksummed_addr(node.address)
addr_from_wif = proto.checksummed_addr(addr_from_wif)
vmsg(fs.format(coin.upper(), (addr_type or 'auto'), addr))
assert addr == addr_chk, f'{addr} != {addr_chk}'
assert addr == addr_from_wif, f'{addr} != {addr_from_wif}'
vmsg('')
return True
def errors(self,name,ut):
vmsg('Checking error handling:')
m = MasterNode(cfg,self._seed)
m_btc = m.init_cfg(coin='btc',addr_type='bech32')
purpose = m_btc.derive_private()
coin_type = purpose.derive_private()
acct = coin_type.derive_private(idx=0)
chain = acct.derive_private(idx=0,hardened=False)
def bad01():
m.to_chain(idx=0,coin='erq',addr_type='C')
def bad02():
m_btc.derive_private(idx=0)
def bad03():
m_btc.derive_private(hardened=False)
def bad04():
purpose.derive_private(idx=8)
def bad05():
purpose.derive_private(hardened=False)
def bad06():
coin_type.derive_private() # no acct idx
def bad08():
m_btc.derive_public() # must be private
def bad09():
coin_type.derive_private(idx=8,hardened=False)
def bad10():
acct.derive_private()
def bad11():
chain.derive_private()
def bad12():
chain.derive_private(hardened=True,idx=3)
bad_data = (
('unsupported coin', 'ValueError', 'not supported', bad01),
('depth 1 (purpose): idx not None', 'ValueError', 'index for path comp', bad02),
('depth 1 (purpose): hardened False', 'ValueError', 'value for ‘hardened’', bad03),
('depth 2 (coin type): idx mismatch', 'ValueError', 'index 8 at depth', bad04),
('depth 2 (coin type): hardened False', 'ValueError', 'value for ‘hardened’', bad05),
('depth 3 (account): idx not set', 'ValueError', 'must be set', bad06),
('depth 1 (purpose): node not hardened', 'ValueError', 'must be hardened', bad08),
('depth 3 (account): node not hardened', 'ValueError', 'value for ‘hardened’', bad09),
('depth 4 (chain): idx not set', 'ValueError', 'must be either 0', bad10),
('depth 5 (leaf node): idx not set', 'ValueError', 'must be set', bad11),
('depth 5 (leaf node): hardened True', 'ValueError', 'must be None', bad12),
)
ut.process_bad_data(bad_data,pfx='')
vmsg('')
return True
def parse_extended_errors(self,name,ut):
vmsg('Parsing and validating invalid extended keys:')
vec = vectors_bip32_invalid
func = [lambda m=n: BipHDNode.from_extended_key(cfg,'btc',vec[m][0]) for n in range(len(vec))]
exc = (
'first byte for public',
'first byte for private',
'first byte for public',
'first byte for private',
'first byte for public',
'first byte for private',
'non-zero parent fingerprint',
'non-zero parent fingerprint',
'non-zero index',
'non-zero index',
'unrecognized extended key v',
'unrecognized extended key v',
'private key is zero!',
'private key >= group order!',
'Public key could not be parsed', # extmod
'incorrect checksum',
)
ut.process_bad_data([(vec[n][1], 'ValueError', exc[n], func[n]) for n in range(len(vec))],pfx='')
vmsg('')
return True