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:
parent
a8253a692b
commit
ea1e8d1228
9 changed files with 2363 additions and 2 deletions
88
examples/bip_hd.py
Executable file
88
examples/bip_hd.py
Executable 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
504
mmgen/bip_hd/__init__.py
Normal 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
1219
mmgen/bip_hd/chainparams.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1 +1 @@
|
|||
14.1.dev3
|
||||
14.1.dev4
|
||||
|
|
|
|||
115
scripts/create-bip-hd-chain-params.py
Executable file
115
scripts/create-bip-hd-chain-params.py
Executable 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)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
|
|
|
|||
1
test/ref/altcoin/slip44-mini.json
Normal file
1
test/ref/altcoin/slip44-mini.json
Normal 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
419
test/unit_tests_d/ut_bip_hd.py
Executable 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue