protocol.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. #!/usr/bin/env python3
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
  5. #
  6. # This program is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation, either version 3 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. """
  19. protocol.py: Coin protocol base classes and initializer
  20. """
  21. from collections import namedtuple
  22. from .devtools import *
  23. from .globalvars import g
  24. parsed_wif = namedtuple('parsed_wif',['sec','pubkey_type','compressed'])
  25. parsed_addr = namedtuple('parsed_addr',['bytes','fmt'])
  26. _finfo = namedtuple('fork_info',['height','hash','name','replayable'])
  27. _nw = namedtuple('coin_networks',['mainnet','testnet','regtest'])
  28. class CoinProtocol(MMGenObject):
  29. proto_info = namedtuple('proto_info',['name','trust_level']) # trust levels: see altcoin.py
  30. # keys are mirrored in g.core_coins:
  31. coins = {
  32. 'btc': proto_info('Bitcoin', 5),
  33. 'bch': proto_info('BitcoinCash', 5),
  34. 'ltc': proto_info('Litecoin', 5),
  35. 'eth': proto_info('Ethereum', 4),
  36. 'etc': proto_info('EthereumClassic', 4),
  37. 'zec': proto_info('Zcash', 2),
  38. 'xmr': proto_info('Monero', 4)
  39. }
  40. class Base(MMGenObject):
  41. base_proto = None
  42. is_fork_of = None
  43. networks = ('mainnet','testnet','regtest')
  44. def __init__(self,coin,name,network,tokensym=None,need_amt=False):
  45. self.coin = coin.upper()
  46. self.coin_id = self.coin
  47. self.name = name
  48. self.network = network
  49. self.tokensym = tokensym
  50. self.cls_name = type(self).__name__
  51. self.testnet = network in ('testnet','regtest')
  52. self.regtest = network == 'regtest'
  53. self.networks = tuple(k for k,v in self.network_names._asdict().items() if v)
  54. self.network_id = coin.lower() + {
  55. 'mainnet': '',
  56. 'testnet': '_tn',
  57. 'regtest': '_rt',
  58. }[network]
  59. if 'tx' not in self.mmcaps and g.is_txprog:
  60. from .util import die
  61. die(1,f'Command {g.prog_name!r} not supported for coin {self.coin}')
  62. if hasattr(self,'chain_names'):
  63. self.chain_name = self.chain_names[0] # first chain name is default
  64. else:
  65. self.chain_name = self.network
  66. self.chain_names = [self.network]
  67. if self.tokensym:
  68. assert self.name.startswith('Ethereum'), 'CoinProtocol.Base_chk1'
  69. if self.base_coin in ('ETH','XMR'):
  70. from .util import get_keccak
  71. self.keccak_256 = get_keccak()
  72. if need_amt:
  73. import mmgen.amt
  74. setattr( self, 'coin_amt', getattr(mmgen.amt,self.coin_amt) )
  75. fee = getattr(self,'max_tx_fee',None)
  76. setattr( self, 'max_tx_fee', (self.coin_amt(fee) if fee else None) )
  77. else:
  78. setattr( self, 'coin_amt', None )
  79. setattr( self, 'max_tx_fee', None )
  80. @property
  81. def dcoin(self):
  82. return self.coin
  83. @classmethod
  84. def chain_name_to_network(cls,coin,chain_name):
  85. """
  86. The generic networks 'mainnet', 'testnet' and 'regtest' are required for all coins
  87. that support transaction operations.
  88. For protocols that have specific names for chains corresponding to these networks,
  89. the attribute 'chain_name' is used, while 'network' retains the generic name.
  90. For Bitcoin and Bitcoin forks, 'network' and 'chain_name' are equivalent.
  91. """
  92. for network in ('mainnet','testnet','regtest'):
  93. proto = init_proto(coin,network=network)
  94. for proto_chain_name in proto.chain_names:
  95. if chain_name == proto_chain_name:
  96. return network
  97. raise ValueError(f'{chain_name}: unrecognized chain name for coin {coin}')
  98. @staticmethod
  99. def parse_network_id(network_id):
  100. nid = namedtuple('parsed_network_id',['coin','network'])
  101. if network_id.endswith('_tn'):
  102. return nid(network_id[:-3],'testnet')
  103. elif network_id.endswith('_rt'):
  104. return nid(network_id[:-3],'regtest')
  105. else:
  106. return nid(network_id,'mainnet')
  107. @staticmethod
  108. def create_network_id(coin,network):
  109. return coin.lower() + { 'mainnet':'', 'testnet':'_tn', 'regtest':'_rt' }[network]
  110. def cap(self,s):
  111. return s in self.caps
  112. def addr_fmt_to_ver_bytes(self,req_fmt,return_hex=False):
  113. for ver_hex,fmt in self.addr_ver_bytes.items():
  114. if req_fmt == fmt:
  115. return ver_hex if return_hex else bytes.fromhex(ver_hex)
  116. return False
  117. def get_addr_len(self,addr_fmt):
  118. return self.addr_len
  119. def parse_addr_bytes(self,addr_bytes):
  120. for ver_hex,addr_fmt in self.addr_ver_bytes.items():
  121. ver_bytes = bytes.fromhex(ver_hex)
  122. vlen = len(ver_bytes)
  123. if addr_bytes[:vlen] == ver_bytes:
  124. if len(addr_bytes[vlen:]) == self.get_addr_len(addr_fmt):
  125. return parsed_addr( addr_bytes[vlen:], addr_fmt )
  126. return False
  127. def coin_addr(self,addr):
  128. from .addr import CoinAddr
  129. return CoinAddr( proto=self, addr=addr )
  130. def addr_type(self,id_str):
  131. from .addr import MMGenAddrType
  132. return MMGenAddrType( proto=self, id_str=id_str )
  133. class Secp256k1(Base):
  134. """
  135. Bitcoin and Ethereum protocols inherit from this class
  136. """
  137. secp256k1_ge = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141
  138. privkey_len = 32
  139. pubkey_types = ('std',)
  140. def preprocess_key(self,sec,pubkey_type):
  141. # Key must be non-zero and less than group order of secp256k1 curve
  142. if 0 < int.from_bytes(sec,'big') < self.secp256k1_ge:
  143. return sec
  144. else: # chance of this is less than 1 in 2^127
  145. from .util import ydie
  146. pk = int.from_bytes(sec,'big')
  147. if pk == 0: # chance of this is 1 in 2^256
  148. ydie(3,'Private key is zero!')
  149. elif pk == self.secp256k1_ge: # ditto
  150. ydie(3,'Private key == secp256k1_ge!')
  151. else:
  152. if not g.test_suite:
  153. from .util import ymsg
  154. ymsg(f'Warning: private key is greater than secp256k1 group order!:\n {hexpriv}')
  155. return (pk % self.secp256k1_ge).to_bytes(self.privkey_len,'big')
  156. class DummyWIF:
  157. """
  158. Ethereum and Monero protocols inherit from this class
  159. """
  160. def bytes2wif(self,privbytes,pubkey_type,compressed):
  161. assert pubkey_type == self.pubkey_type, f'{pubkey_type}: invalid pubkey_type for {self.name} protocol!'
  162. assert compressed == False, f'{self.name} protocol does not support compressed pubkeys!'
  163. return privbytes.hex()
  164. def parse_wif(self,wif):
  165. return parsed_wif(
  166. sec = bytes.fromhex(wif),
  167. pubkey_type = self.pubkey_type,
  168. compressed = False )
  169. def init_proto(coin=None,testnet=False,regtest=False,network=None,network_id=None,tokensym=None,need_amt=False):
  170. assert type(testnet) == bool, 'init_proto_chk1'
  171. assert type(regtest) == bool, 'init_proto_chk2'
  172. assert coin or network_id, 'init_proto_chk3'
  173. assert not (coin and network_id), 'init_proto_chk4'
  174. if network_id:
  175. coin,network = CoinProtocol.Base.parse_network_id(network_id)
  176. elif network:
  177. assert network in CoinProtocol.Base.networks, f'init_proto_chk5 - {network!r}: invalid network'
  178. assert testnet == False, 'init_proto_chk6'
  179. assert regtest == False, 'init_proto_chk7'
  180. else:
  181. network = 'regtest' if regtest else 'testnet' if testnet else 'mainnet'
  182. coin = coin.lower()
  183. if coin not in CoinProtocol.coins:
  184. from .altcoin import init_genonly_altcoins
  185. init_genonly_altcoins( coin, testnet=testnet ) # raises exception on failure
  186. name = CoinProtocol.coins[coin].name
  187. proto_name = name + ('' if network == 'mainnet' else network.capitalize())
  188. if not hasattr(CoinProtocol,proto_name):
  189. import importlib
  190. setattr(
  191. CoinProtocol,
  192. proto_name,
  193. getattr(importlib.import_module(f'mmgen.proto.{coin}'),network)
  194. )
  195. return getattr(CoinProtocol,proto_name)(
  196. coin = coin,
  197. name = name,
  198. network = network,
  199. tokensym = tokensym,
  200. need_amt = need_amt )
  201. def init_proto_from_opts(need_amt=False):
  202. return init_proto(
  203. coin = g.coin,
  204. testnet = g.testnet,
  205. regtest = g.regtest,
  206. tokensym = g.token,
  207. need_amt = need_amt )
  208. def warn_trustlevel(coinsym):
  209. if coinsym.lower() in CoinProtocol.coins:
  210. trust_level = CoinProtocol.coins[coinsym.lower()].trust_level
  211. else:
  212. from .altcoin import CoinInfo
  213. e = CoinInfo.get_entry(coinsym,'mainnet')
  214. trust_level = e.trust_level if e else None
  215. if trust_level in (None,-1):
  216. from .util import die
  217. die(1,f'Coin {coinsym} is not supported by {g.proj_name}')
  218. if trust_level > 3:
  219. return
  220. m = """
  221. Support for coin {c!r} is EXPERIMENTAL. The {p} project
  222. assumes no responsibility for any loss of funds you may incur.
  223. This coin’s {p} testing status: {t}
  224. Are you sure you want to continue?
  225. """
  226. from .util import qmsg,fmt,keypress_confirm
  227. from .color import red,yellow,green
  228. warning = fmt(m).strip().format(
  229. c = coinsym.upper(),
  230. t = {
  231. 0: red('COMPLETELY UNTESTED'),
  232. 1: red('LOW'),
  233. 2: yellow('MEDIUM'),
  234. 3: green('OK'),
  235. }[trust_level],
  236. p = g.proj_name )
  237. if g.test_suite:
  238. qmsg(warning)
  239. return
  240. if not keypress_confirm(warning,default_yes=True):
  241. import sys
  242. sys.exit(0)