__init__.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519
  1. #!/usr/bin/env python3
  2. #
  3. # MMGen Wallet, a terminal-based cryptocurrency wallet
  4. # Copyright (C)2013-2025 The MMGen Project <mmgen@tuta.io>
  5. # Licensed under the GNU General Public License, Version 3:
  6. # https://www.gnu.org/licenses
  7. # Public project repositories:
  8. # https://github.com/mmgen/mmgen-wallet
  9. # https://gitlab.com/mmgen/mmgen-wallet
  10. """
  11. bip_hd: BIP-44/49/84, SLIP-44 hierarchical-deterministic key derivation library
  12. """
  13. # One motivation for this implementation:
  14. # https://blog.unit410.com/bitcoin/bip32/bip39/kdf/2021/05/17/inconsistent-bip32-derivations.html
  15. import hmac
  16. from ..cfg import Config
  17. from ..util import is_int, fmt
  18. from ..base_obj import Lockable
  19. from ..keygen import KeyGenerator, keygen_public_data
  20. from ..addrgen import AddrGenerator
  21. from ..addr import MMGenAddrType
  22. from ..key import PrivKey
  23. from ..protocol import CoinProtocol
  24. from ..proto.btc.common import hash160, b58chk_encode, b58chk_decode
  25. from ..proto.secp256k1.secp256k1 import pubkey_tweak_add, pubkey_check
  26. from . import chainparams
  27. chainparams_data = chainparams.parse_data()
  28. secp256k1_order = CoinProtocol.Secp256k1.secp256k1_group_order
  29. hardened_idx0 = 0x80000000
  30. def get_chain_params(bipnum, chain):
  31. return chainparams_data[f'bip-{bipnum}'][chain.upper()]
  32. def get_version_bytes(bip_proto, coin, public):
  33. return getattr(
  34. chainparams_data[f'bip-{bip_proto}'][coin],
  35. 'vb_pub' if public else 'vb_prv')
  36. def parse_version_bytes(vb_hex):
  37. e = chainparams_data['defaults']
  38. if vb_hex in (e.vb_pub, e.vb_prv):
  39. return (None, e)
  40. for bipnum in (49, 84, 86, 44): # search bip-44 last, since it has the most entries
  41. for e in chainparams_data[f'bip-{bipnum}'].values():
  42. if vb_hex in (e.vb_pub, e.vb_prv):
  43. return (bipnum, e)
  44. raise ValueError(f'0x{vb_hex}: unrecognized extended key version bytes')
  45. def compress_pubkey(pubkey_bytes):
  46. # see: proto.secp256k1.keygen.pubkey_format()
  47. return (b'\x02', b'\x03')[pubkey_bytes[-1] & 1] + pubkey_bytes[1:33]
  48. def decompress_pubkey(pubkey_bytes):
  49. import ecdsa
  50. return b'\x04' + ecdsa.VerifyingKey.from_string(pubkey_bytes, curve=ecdsa.curves.SECP256k1).to_string()
  51. class Bip32ExtendedKey(Lockable):
  52. def __init__(self, key_b58):
  53. try:
  54. key = b58chk_decode(key_b58)
  55. except Exception as e:
  56. raise type(e)(f'invalid extended key: {e}')
  57. assert len(key) == 78, f'len(key) == {len(key)} (not 78)'
  58. # Serialization:
  59. # ver_bytes | depth | par_print | idx | chaincode | serialized_key
  60. # 0:4 (4) | 4 (1) | 5:9 (4) | 9:13 (4) | 13:45 (32) | 45(46): 33(32)
  61. ver_hex = key[:4].hex()
  62. bipnum, cp_entry = parse_version_bytes(ver_hex)
  63. public = ver_hex == cp_entry.vb_pub
  64. idx_raw = int.from_bytes(key[9:13], byteorder='big')
  65. self.base58 = key_b58
  66. self.ver_bytes = key[:4]
  67. self.depth = key[4]
  68. self.par_print = key[5:9]
  69. self.idx = idx_raw if idx_raw < hardened_idx0 else idx_raw - hardened_idx0
  70. self.chaincode = key[13:45]
  71. self.key = key[45 if public else 46:]
  72. self.hardened = idx_raw >= hardened_idx0 or self.depth == 0
  73. self.bip_proto = bipnum or 44
  74. self.network = cp_entry.network if bipnum else 'mainnet'
  75. self.public = public
  76. self.coin = cp_entry.chain if bipnum and cp_entry.chain != 'BTC' else '-'
  77. if self.public:
  78. if not key[45] in (2, 3):
  79. raise ValueError(f'0x{key[45]:02x}: invalid first byte for public key data (not 2 or 3)')
  80. elif key[45]:
  81. raise ValueError(f'0x{key[45]:02x}: invalid first byte for private key data (not zero)')
  82. if self.depth == 0:
  83. if self.par_print != bytes(4):
  84. raise ValueError(f'{self.par_print.hex()}: non-zero parent fingerprint at depth 0')
  85. if idx_raw:
  86. raise ValueError(f'{idx_raw}: non-zero index at depth 0')
  87. def __str__(self):
  88. return fmt(f"""
  89. base58: {self.base58}
  90. ver_bytes: {self.ver_bytes.hex()}
  91. depth: {self.depth} [{bip_hd_nodes[self.depth].desc}]
  92. par_print: {self.par_print.hex()}
  93. idx: {self.idx}
  94. chaincode: {self.chaincode.hex()}
  95. key: {self.key.hex()}
  96. hardened: {self.hardened}
  97. bip_proto: {self.bip_proto}
  98. network: {self.network}
  99. public: {self.public}
  100. coin: {self.coin}
  101. """)
  102. def get_bip_by_addr_type(addr_type):
  103. return (
  104. 84 if addr_type.name == 'bech32' else
  105. 49 if addr_type.name == 'segwit' else
  106. 44)
  107. def check_privkey(key_int):
  108. if key_int == 0:
  109. raise ValueError('private key is zero!')
  110. elif key_int >= secp256k1_order:
  111. raise ValueError(f'{key_int:x}: private key >= group order!')
  112. class BipHDConfig(Lockable):
  113. supported_coins = ('btc', 'eth', 'doge', 'ltc', 'bch')
  114. def __init__(self, base_cfg, coin, network, addr_type, from_path, no_path_checks):
  115. if not coin.lower() in self.supported_coins:
  116. raise ValueError(f'bip_hd: coin {coin.upper()} not supported')
  117. base_cfg = Config({
  118. '_clone': base_cfg,
  119. 'coin': coin,
  120. 'network': network,
  121. 'type': addr_type or None,
  122. 'quiet': True
  123. })
  124. dfl_type = base_cfg._proto.dfl_mmtype
  125. addr_type = MMGenAddrType(
  126. proto = base_cfg._proto,
  127. id_str = base_cfg.type or ('C' if dfl_type == 'L' else dfl_type))
  128. self.base_cfg = base_cfg
  129. self.addr_type = addr_type
  130. self.kg = KeyGenerator(base_cfg, base_cfg._proto, addr_type.pubkey_type)
  131. self.ag = AddrGenerator(base_cfg, base_cfg._proto, addr_type)
  132. self.bip_proto = get_bip_by_addr_type(addr_type)
  133. self.from_path = from_path
  134. self.no_path_checks = no_path_checks
  135. class MasterNode(Lockable):
  136. desc = 'Unconfigured Bip32 Master Node'
  137. _use_class_attr = True
  138. def __init__(self, base_cfg, bytes_data):
  139. H = hmac.digest(b'Bitcoin seed', bytes_data, 'sha512')
  140. self.par_print = bytes(4)
  141. self.depth = 0
  142. self.key = H[:32]
  143. self.chaincode = H[32:]
  144. self.idx = 0
  145. self.hardened = True
  146. self.public = False
  147. self.base_cfg = base_cfg
  148. check_privkey(int.from_bytes(self.key, byteorder='big'))
  149. def init_cfg(
  150. self,
  151. coin = None,
  152. *,
  153. network = None,
  154. addr_type = None,
  155. from_path = False,
  156. no_path_checks = False):
  157. new = BipHDNodeMaster()
  158. new.cfg = BipHDConfig(
  159. self.base_cfg,
  160. coin,
  161. network,
  162. addr_type,
  163. from_path,
  164. no_path_checks)
  165. new.par_print = self.par_print
  166. new.depth = self.depth
  167. new.key = self.key
  168. new.chaincode = self.chaincode
  169. new.idx = self.idx
  170. new.hardened = self.hardened
  171. new.public = self.public
  172. new._lock()
  173. return new
  174. def to_coin_type(self, *, coin=None, network=None, addr_type=None):
  175. return self.init_cfg(coin, network=network, addr_type=addr_type).to_coin_type()
  176. def to_chain(self, idx, *, coin=None, network=None, addr_type=None, hardened=False, public=False):
  177. return self.init_cfg(coin, network=network, addr_type=addr_type).to_chain(
  178. idx = idx,
  179. hardened = hardened,
  180. public = public)
  181. class BipHDNode(Lockable):
  182. _autolock = False
  183. _generated_pubkey = None
  184. _set_ok = ('_generated_pubkey',)
  185. def check_param(self, name, val):
  186. cls = type(self)
  187. if val is None:
  188. if not hasattr(cls, name):
  189. raise ValueError(f'‘{name}’ at depth {self.depth} ({self.desc!r}) must be set')
  190. elif hasattr(cls, name) and val != getattr(cls, name):
  191. raise ValueError(
  192. '{}: invalid value for ‘{}’ at depth {} ({!r}) (must be {})'.format(
  193. val, name, self.depth, self.desc,
  194. 'None' if getattr(cls, name) is None else f'None or {getattr(cls, name)}')
  195. )
  196. def set_params(self, cfg, idx, hardened):
  197. self.check_param('idx', idx)
  198. self.check_param('hardened', hardened)
  199. return (
  200. type(self).idx if idx is None else idx,
  201. type(self).hardened if hardened is None else hardened)
  202. @property
  203. def privkey(self):
  204. assert not self.public
  205. return PrivKey(
  206. self.cfg.base_cfg._proto,
  207. self.key,
  208. compressed = self.cfg.addr_type.compressed,
  209. pubkey_type = self.cfg.addr_type.pubkey_type)
  210. @property
  211. def pubkey_bytes(self):
  212. if self.public:
  213. return self.key
  214. elif self.cfg.addr_type.compressed:
  215. return self.priv2pub().pubkey
  216. else:
  217. return compress_pubkey(self.priv2pub().pubkey)
  218. def priv2pub(self):
  219. if not self._generated_pubkey:
  220. self._generated_pubkey = self.cfg.kg.gen_data(self.privkey)
  221. return self._generated_pubkey
  222. @property
  223. def address(self):
  224. return self.cfg.ag.to_addr(
  225. keygen_public_data(
  226. pubkey = self.key if self.cfg.addr_type.compressed else decompress_pubkey(self.key),
  227. viewkey_bytes = None,
  228. pubkey_type = self.cfg.addr_type.pubkey_type,
  229. compressed = self.cfg.addr_type.compressed)
  230. if self.public else
  231. self.priv2pub()
  232. )
  233. # Extended keys can be identified by the Hash160 (RIPEMD160 after SHA256) of the serialized ECDSA
  234. # public key K, ignoring the chain code. This corresponds exactly to the data used in traditional
  235. # Bitcoin addresses. It is not advised to represent this data in base58 format though, as it may be
  236. # interpreted as an address that way (and wallet software is not required to accept payment to the
  237. # chain key itself).
  238. @property
  239. def id(self):
  240. return hash160(self.pubkey_bytes)
  241. # The first 32 bits of the identifier are called the key fingerprint.
  242. @property
  243. def fingerprint(self):
  244. return self.id[:4]
  245. @property
  246. def xpub(self):
  247. return self.key_extended(public=True, as_str=True)
  248. @property
  249. def xprv(self):
  250. return self.key_extended(public=False, as_str=True)
  251. def key_extended(self, public, *, as_str=False):
  252. if self.public and not public:
  253. raise ValueError('cannot create extended private key for public node!')
  254. ret = b58chk_encode(
  255. bytes.fromhex(get_version_bytes(self.cfg.bip_proto, self.cfg.base_cfg.coin, public))
  256. + int.to_bytes(self.depth, length=1, byteorder='big')
  257. + self.par_print
  258. + int.to_bytes(
  259. self.idx + (hardened_idx0 if self.hardened and self.depth else 0),
  260. length = 4,
  261. byteorder = 'big')
  262. + self.chaincode
  263. + (self.pubkey_bytes if public else b'\x00' + self.key)
  264. )
  265. return ret if as_str else Bip32ExtendedKey(ret)
  266. def derive_public(self, idx=None):
  267. return self.derive(idx=idx, hardened=False, public=True)
  268. def derive_private(self, idx=None, hardened=None):
  269. return self.derive(idx=idx, hardened=hardened, public=False)
  270. def derive(self, idx, hardened, public):
  271. if self.public and not public:
  272. raise ValueError('cannot derive private node from public node!')
  273. new = bip_hd_nodes[self.depth + 1]()
  274. new.depth = self.depth + 1
  275. new.cfg = self.cfg
  276. new.par_print = self.fingerprint
  277. new.public = public
  278. if new.cfg.no_path_checks:
  279. new.idx, new.hardened = (idx, hardened)
  280. else:
  281. if new.public and type(new).hardened:
  282. raise ValueError(
  283. f'‘public’ requested, but node of depth {new.depth} ({new.desc}) must be hardened!')
  284. new.idx, new.hardened = new.set_params(new.cfg, idx, hardened)
  285. key_in = b'\x00' + self.key if new.hardened else self.pubkey_bytes
  286. I = hmac.digest(
  287. self.chaincode,
  288. key_in + ((hardened_idx0 if new.hardened else 0) + new.idx).to_bytes(length=4, byteorder='big'),
  289. 'sha512')
  290. pk_addend_bytes = I[:32]
  291. new.chaincode = I[32:]
  292. if new.public:
  293. new.key = pubkey_tweak_add(key_in, pk_addend_bytes) # checks range of pk_addend
  294. else:
  295. pk_addend = int.from_bytes(pk_addend_bytes, byteorder='big')
  296. check_privkey(pk_addend)
  297. key_int = (int.from_bytes(self.key, byteorder='big') + pk_addend) % secp256k1_order
  298. check_privkey(key_int)
  299. new.key = int.to_bytes(key_int, length=32, byteorder='big')
  300. new._lock()
  301. return new
  302. @staticmethod
  303. def from_path(
  304. base_cfg,
  305. seed,
  306. path_str,
  307. *,
  308. coin = None,
  309. addr_type = None,
  310. no_path_checks = False):
  311. path = path_str.lower().split('/')
  312. if path.pop(0) != 'm':
  313. raise ValueError(f'{path_str}: invalid path string (first component is not "m")')
  314. res = MasterNode(base_cfg, seed).init_cfg(
  315. coin = coin or 'btc',
  316. addr_type = addr_type or 'compressed',
  317. no_path_checks = no_path_checks,
  318. from_path = True)
  319. for s in path:
  320. for suf in ("'", 'h'):
  321. if s.endswith(suf):
  322. idx = s.removesuffix(suf)
  323. hardened = True
  324. break
  325. else:
  326. idx = s
  327. hardened = False
  328. if not is_int(idx):
  329. raise ValueError(f'invalid path component {s!r}')
  330. res = res.derive(int(idx), hardened, public=False)
  331. return res
  332. @staticmethod
  333. # ‘addr_type’ is required for broken coins with duplicate version bytes across BIP protocols
  334. # (i.e. Dogecoin)
  335. def from_extended_key(base_cfg, coin, xkey_b58, addr_type=None):
  336. xk = Bip32ExtendedKey(xkey_b58)
  337. if xk.public:
  338. pubkey_check(xk.key)
  339. else:
  340. check_privkey(int.from_bytes(xk.key, byteorder='big'))
  341. addr_types = {
  342. 84: 'bech32',
  343. 49: 'segwit',
  344. 44: None
  345. }
  346. new = bip_hd_nodes[xk.depth]()
  347. new.cfg = BipHDConfig(
  348. base_cfg,
  349. coin,
  350. xk.network,
  351. addr_type or addr_types[xk.bip_proto],
  352. False,
  353. False)
  354. new.par_print = xk.par_print
  355. new.depth = xk.depth
  356. new.key = xk.key
  357. new.chaincode = xk.chaincode
  358. new.idx = xk.idx
  359. new.hardened = xk.hardened
  360. new.public = xk.public
  361. new._lock()
  362. return new
  363. class BipHDNodeMaster(BipHDNode):
  364. desc = 'Bip32 Master Node'
  365. hardened = True
  366. idx = None
  367. def to_coin_type(self):
  368. # purpose coin_type
  369. return self.derive_private().derive_private()
  370. def to_chain(self, idx, *, hardened=False, public=False):
  371. # purpose coin_type account #0 chain
  372. return self.derive_private().derive_private().derive_private(idx=0).derive(
  373. idx = idx,
  374. hardened = False if public else hardened,
  375. public = public)
  376. class BipHDNodePurpose(BipHDNode):
  377. desc = 'Purpose'
  378. hardened = True
  379. def set_params(self, cfg, idx, hardened):
  380. self.check_param('hardened', hardened)
  381. if idx not in (None, cfg.bip_proto):
  382. raise ValueError(
  383. f'index for path component {self.desc!r} with address type {cfg.addr_type!r} '
  384. f'must be {cfg.bip_proto}, not {idx}')
  385. return (cfg.bip_proto, type(self).hardened)
  386. class BipHDNodeCoinType(BipHDNode):
  387. desc = 'Coin Type'
  388. hardened = True
  389. def set_params(self, cfg, idx, hardened):
  390. self.check_param('hardened', hardened)
  391. chain_idx = get_chain_params(
  392. bipnum = get_bip_by_addr_type(cfg.addr_type),
  393. chain = cfg.base_cfg.coin).idx
  394. if idx not in (None, chain_idx):
  395. raise ValueError(
  396. f'index {idx} at depth {self.depth} ({self.desc}) does not match '
  397. f'chain index {chain_idx} for coin {cfg.base_cfg.coin!r}')
  398. return (chain_idx, type(self).hardened)
  399. def to_chain(self, idx, *, hardened=False, public=False):
  400. # account #0 chain
  401. return self.derive_private(idx=0).derive(
  402. idx = idx,
  403. hardened = False if public else hardened,
  404. public = public)
  405. class BipHDNodeAccount(BipHDNode):
  406. desc = 'Account'
  407. hardened = True
  408. class BipHDNodeChain(BipHDNode):
  409. desc = 'Chain'
  410. hardened = False
  411. def set_params(self, cfg, idx, hardened):
  412. self.check_param('hardened', hardened)
  413. if idx not in (0, 1):
  414. raise ValueError(
  415. f'at depth {self.depth} ({self.desc}), ‘idx’ must be either 0 (external) or 1 (internal)')
  416. return (idx, type(self).hardened)
  417. class BipHDNodeAddrIdx(BipHDNode):
  418. desc = 'Address Index'
  419. hardened = False
  420. bip_hd_nodes = {
  421. 0: BipHDNodeMaster,
  422. 1: BipHDNodePurpose,
  423. 2: BipHDNodeCoinType,
  424. 3: BipHDNodeAccount,
  425. 4: BipHDNodeChain,
  426. 5: BipHDNodeAddrIdx
  427. }