diff --git a/mmgen/addrgen.py b/mmgen/addrgen.py index 0cf2be94..550276c4 100755 --- a/mmgen/addrgen.py +++ b/mmgen/addrgen.py @@ -20,7 +20,7 @@ addrgen.py: Address and view key generation classes for the MMGen suite """ -from .protocol import hash160,_b58chk_encode +from .proto.btc import hash160,_b58chk_encode from .addr import CoinAddr,MMGenAddrType,MoneroViewKey,ZcashViewKey # decorator for to_addr() and to_viewkey() diff --git a/mmgen/addrlist.py b/mmgen/addrlist.py index ea64c880..3819eb7c 100755 --- a/mmgen/addrlist.py +++ b/mmgen/addrlist.py @@ -157,9 +157,9 @@ class AddrList(MMGenObject): # Address info for a single seed ID mmtype = mmtype or proto.dfl_mmtype assert mmtype in MMGenAddrType.mmtypes, f'{mmtype}: mmtype not in {MMGenAddrType.mmtypes!r}' - from .protocol import CoinProtocol + from .proto.btc import mainnet self.bitcoin_addrtypes = tuple( - MMGenAddrType(CoinProtocol.Bitcoin,key).name for key in CoinProtocol.Bitcoin.mmtypes) + MMGenAddrType(mainnet,key).name for key in mainnet.mmtypes ) if seed and addr_idxs and mmtype: # data from seed + idxs self.al_id,src = AddrListID(seed.sid,mmtype),'gen' @@ -281,7 +281,7 @@ class AddrList(MMGenObject): # Address info for a single seed ID return out def gen_wallet_passwd(self,privbytes): - from .protocol import hash256 + from .proto.btc import hash256 return WalletPassword( hash256(privbytes)[:16].hex() ) def check_format(self,addr): diff --git a/mmgen/altcoin.py b/mmgen/altcoin.py index 984b5a19..4ca26f58 100755 --- a/mmgen/altcoin.py +++ b/mmgen/altcoin.py @@ -556,7 +556,7 @@ class CoinInfo(object): return '1' def phash2addr(ver_num,pk_hash): - from .protocol import _b58chk_encode + from .proto.btc import _b58chk_encode bl = ver_num.bit_length() ver_bytes = int.to_bytes(ver_num,bl//8 + bool(bl%8),'big') return _b58chk_encode(ver_bytes + pk_hash) @@ -736,6 +736,7 @@ def init_genonly_altcoins(usr_coin=None,testnet=False): def create_altcoin_protos(data): from .protocol import CoinProtocol + from .proto.btc import mainnet def make_proto(e,testnet=False): @@ -751,8 +752,8 @@ def create_altcoin_protos(data): CoinProtocol, proto, type( - 'CoinProtocol.' + proto, - (CoinProtocol.Bitcoin,), + proto, + (mainnet,), { 'base_coin': e.symbol, 'addr_ver_bytes': dict( diff --git a/mmgen/data/version b/mmgen/data/version index 26db9932..4423f435 100644 --- a/mmgen/data/version +++ b/mmgen/data/version @@ -1 +1 @@ -13.1.dev011 +13.1.dev012 diff --git a/mmgen/help.py b/mmgen/help.py index 8d5a9b67..dd45a262 100755 --- a/mmgen/help.py +++ b/mmgen/help.py @@ -158,7 +158,7 @@ one address with no amount on the command line. """ def txsign(): - from .protocol import CoinProtocol + from .proto.btc import mainnet return """ Transactions may contain both {pnm} or non-{pnm} input addresses. @@ -180,7 +180,7 @@ source. Therefore, seed files or a key-address file for all {pnm} outputs must also be supplied on the command line if the data can't be found in the default wallet. """.format( - wd = (f'{coind_exec()} wallet dump or ' if isinstance(proto,CoinProtocol.Bitcoin) else ''), + wd = (f'{coind_exec()} wallet dump or ' if isinstance(proto,mainnet) else ''), pnm = g.proj_name, pnu = proto.name, pnl = g.proj_name.lower() ) diff --git a/mmgen/keygen.py b/mmgen/keygen.py index 2250dfab..f272995c 100755 --- a/mmgen/keygen.py +++ b/mmgen/keygen.py @@ -106,8 +106,8 @@ class keygen_backend: def __init__(self): - from .protocol import CoinProtocol - self.proto_cls = CoinProtocol.Monero + from .proto.xmr import mainnet + self.proto_cls = mainnet from .util import get_keccak self.keccak_256 = get_keccak() diff --git a/mmgen/proto/__init__.py b/mmgen/proto/__init__.py new file mode 100755 index 00000000..e69de29b diff --git a/mmgen/proto/bch.py b/mmgen/proto/bch.py new file mode 100755 index 00000000..f2509296 --- /dev/null +++ b/mmgen/proto/bch.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +# +# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet +# Copyright (C)2013-2022 The MMGen Project +# Licensed under the GNU General Public License, Version 3: +# https://www.gnu.org/licenses +# Public project repositories: +# https://github.com/mmgen/mmgen +# https://gitlab.com/mmgen/mmgen + +""" +Bitcoin Cash protocol +""" + +from .btc import mainnet,_finfo + +class mainnet(mainnet): + is_fork_of = 'Bitcoin' + mmtypes = ('L','C') + sighash_type = 'ALL|FORKID' + forks = [ + _finfo(478559,'000000000000000000651ef99cb9fcbe0dadde1d424bd9f15ff20136191a5eec','BTC',False) + ] + caps = () + coin_amt = 'BCHAmt' + max_tx_fee = '0.1' + ignore_daemon_version = False + + def pubkey2redeem_script(self,pubkey): raise NotImplementedError + def pubkey2segwitaddr(self,pubkey): raise NotImplementedError + +class testnet(mainnet): + addr_ver_bytes = { '6f': 'p2pkh', 'c4': 'p2sh' } + wif_ver_num = { 'std': 'ef' } + +class regtest(testnet): + halving_interval = 150 diff --git a/mmgen/proto/btc.py b/mmgen/proto/btc.py new file mode 100755 index 00000000..ed1f7246 --- /dev/null +++ b/mmgen/proto/btc.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +# +# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet +# Copyright (C)2013-2022 The MMGen Project +# Licensed under the GNU General Public License, Version 3: +# https://www.gnu.org/licenses +# Public project repositories: +# https://github.com/mmgen/mmgen +# https://gitlab.com/mmgen/mmgen + +""" +Bitcoin protocol +""" + +from ..protocol import CoinProtocol,parsed_wif,parsed_addr,_finfo,_b58a,_nw +import hashlib + +def hash160(in_bytes): # OP_HASH160 + return hashlib.new('ripemd160',hashlib.sha256(in_bytes).digest()).digest() + +def hash256(in_bytes): # OP_HASH256 + return hashlib.sha256(hashlib.sha256(in_bytes).digest()).digest() + +# From en.bitcoin.it: +# The Base58 encoding used is home made, and has some differences. +# Especially, leading zeroes are kept as single zeroes when conversion happens. +# Test: 5JbQQTs3cnoYN9vDYaGY6nhQ1DggVsY4FJNBUfEfpSQqrEp3srk +# The 'zero address': +# 1111111111111111111114oLvT2 (pubkeyhash = '\0'*20) + +def _b58chk_encode(in_bytes): + lzeroes = len(in_bytes) - len(in_bytes.lstrip(b'\x00')) + def do_enc(n): + while n: + yield _b58a[n % 58] + n //= 58 + return ('1' * lzeroes) + ''.join(do_enc(int.from_bytes(in_bytes+hash256(in_bytes)[:4],'big')))[::-1] + +def _b58chk_decode(s): + lzeroes = len(s) - len(s.lstrip('1')) + res = sum(_b58a.index(ch) * 58**n for n,ch in enumerate(s[::-1])) + bl = res.bit_length() + out = b'\x00' * lzeroes + res.to_bytes(bl//8 + bool(bl%8),'big') + if out[-4:] != hash256(out[:-4])[:4]: + raise ValueError('_b58chk_decode(): incorrect checksum') + return out[:-4] + +class mainnet(CoinProtocol.Secp256k1): # chainparams.cpp + """ + All Bitcoin code and chain forks inherit from this class + """ + mod_clsname = 'Bitcoin' + network_names = _nw('mainnet','testnet','regtest') + addr_ver_bytes = { '00': 'p2pkh', '05': 'p2sh' } + addr_len = 20 + wif_ver_num = { 'std': '80' } + mmtypes = ('L','C','S','B') + dfl_mmtype = 'L' + coin_amt = 'BTCAmt' + max_tx_fee = '0.003' + sighash_type = 'ALL' + block0 = '000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f' + forks = [ + _finfo(478559,'00000000000000000019f112ec0a9982926f1258cdcc558dd7c3b7e5dc7fa148','BCH',False), + ] + caps = ('rbf','segwit') + mmcaps = ('key','addr','rpc','tx') + base_coin = 'BTC' + base_proto = 'Bitcoin' + # From BIP173: witness version 'n' is stored as 'OP_n'. OP_0 is encoded as 0x00, + # but OP_1 through OP_16 are encoded as 0x51 though 0x60 (81 to 96 in decimal). + witness_vernum_hex = '00' + witness_vernum = int(witness_vernum_hex,16) + bech32_hrp = 'bc' + sign_mode = 'daemon' + avg_bdi = int(9.7 * 60) # average block discovery interval (historical) + halving_interval = 210000 + max_halvings = 64 + start_subsidy = 50 + ignore_daemon_version = False + max_int = 0xffffffff + + def bytes2wif(self,privbytes,pubkey_type,compressed): # input is preprocessed hex + assert len(privbytes) == self.privkey_len, f'{len(privbytes)} bytes: incorrect private key length!' + assert pubkey_type in self.wif_ver_num, f'{pubkey_type!r}: invalid pubkey_type' + return _b58chk_encode( + bytes.fromhex(self.wif_ver_num[pubkey_type]) + + privbytes + + (b'',b'\x01')[bool(compressed)]) + + def parse_wif(self,wif): + key = _b58chk_decode(wif) + + for k,v in self.wif_ver_num.items(): + v = bytes.fromhex(v) + if key[:len(v)] == v: + pubkey_type = k + key = key[len(v):] + break + else: + raise ValueError('Invalid WIF version number') + + if len(key) == self.privkey_len + 1: + assert key[-1] == 0x01, f'{key[-1]!r}: invalid compressed key suffix byte' + compressed = True + elif len(key) == self.privkey_len: + compressed = False + else: + raise ValueError(f'{len(key)}: invalid key length') + + return parsed_wif( + sec = key[:self.privkey_len], + pubkey_type = pubkey_type, + compressed = compressed ) + + def parse_addr(self,addr): + + if 'B' in self.mmtypes and addr[:len(self.bech32_hrp)] == self.bech32_hrp: + import mmgen.bech32 as bech32 + ret = bech32.decode(self.bech32_hrp,addr) + + if ret[0] != self.witness_vernum: + from ..util import msg + msg(f'{ret[0]}: Invalid witness version number') + return False + + return parsed_addr( bytes(ret[1]), 'bech32' ) if ret[1] else False + + return self.parse_addr_bytes(_b58chk_decode(addr)) + + def pubhash2addr(self,pubkey_hash,p2sh): + assert len(pubkey_hash) == 20, f'{len(pubkey_hash)}: invalid length for pubkey hash' + return _b58chk_encode( + self.addr_fmt_to_ver_bytes(('p2pkh','p2sh')[p2sh],return_hex=False) + pubkey_hash + ) + + # Segwit: + def pubkey2redeem_script(self,pubkey): + # https://bitcoincore.org/en/segwit_wallet_dev/ + # The P2SH redeemScript is always 22 bytes. It starts with a OP_0, followed + # by a canonical push of the keyhash (i.e. 0x0014{20-byte keyhash}) + return bytes.fromhex(self.witness_vernum_hex + '14') + hash160(pubkey) + + def pubkey2segwitaddr(self,pubkey): + return self.pubhash2addr( + hash160( self.pubkey2redeem_script(pubkey)), p2sh=True ) + + def pubhash2bech32addr(self,pubhash): + d = list(pubhash) + import mmgen.bech32 as bech32 + return bech32.bech32_encode(self.bech32_hrp,[self.witness_vernum]+bech32.convertbits(d,8,5)) + +class testnet(mainnet): + addr_ver_bytes = { '6f': 'p2pkh', 'c4': 'p2sh' } + wif_ver_num = { 'std': 'ef' } + bech32_hrp = 'tb' + +class regtest(testnet): + bech32_hrp = 'bcrt' + halving_interval = 150 diff --git a/mmgen/proto/etc.py b/mmgen/proto/etc.py new file mode 100755 index 00000000..e7892232 --- /dev/null +++ b/mmgen/proto/etc.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +# +# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet +# Copyright (C)2013-2022 The MMGen Project +# Licensed under the GNU General Public License, Version 3: +# https://www.gnu.org/licenses +# Public project repositories: +# https://github.com/mmgen/mmgen +# https://gitlab.com/mmgen/mmgen + +""" +Ethereum Classic protocol +""" + +from .eth import mainnet + +class mainnet(mainnet): + chain_names = ['classic','ethereum_classic'] + max_tx_fee = '0.005' + ignore_daemon_version = False + +class testnet(mainnet): + chain_names = ['morden','morden_testnet','classic-testnet'] + +class regtest(testnet): + chain_names = ['developmentchain'] diff --git a/mmgen/proto/eth.py b/mmgen/proto/eth.py new file mode 100755 index 00000000..1964cdc4 --- /dev/null +++ b/mmgen/proto/eth.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +# +# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet +# Copyright (C)2013-2022 The MMGen Project +# Licensed under the GNU General Public License, Version 3: +# https://www.gnu.org/licenses +# Public project repositories: +# https://github.com/mmgen/mmgen +# https://gitlab.com/mmgen/mmgen + +""" +Ethereum protocol +""" + +from ..protocol import CoinProtocol,_nw,parsed_addr +from ..util import is_hex_str_lc,Msg + +class mainnet(CoinProtocol.DummyWIF,CoinProtocol.Secp256k1): + + network_names = _nw('mainnet','testnet','devnet') + addr_len = 20 + mmtypes = ('E',) + dfl_mmtype = 'E' + mod_clsname = 'Ethereum' + base_coin = 'ETH' + pubkey_type = 'std' # required by DummyWIF + + coin_amt = 'ETHAmt' + max_tx_fee = '0.005' + chain_names = ['ethereum','foundation'] + sign_mode = 'standalone' + caps = ('token',) + mmcaps = ('key','addr','rpc','tx') + base_proto = 'Ethereum' + avg_bdi = 15 + ignore_daemon_version = False + + chain_ids = { + 1: 'ethereum', # ethereum mainnet + 2: 'morden', # morden testnet (deprecated) + 3: 'ropsten', # ropsten testnet + 4: 'rinkeby', # rinkeby testnet + 5: 'goerli', # goerli testnet + 42: 'kovan', # kovan testnet + 61: 'classic', # ethereum classic mainnet + 62: 'morden', # ethereum classic testnet + 17: 'developmentchain', # parity dev chain + 1337: 'developmentchain', # geth dev chain + } + + @property + def dcoin(self): + return self.tokensym or self.coin + + def parse_addr(self,addr): + if is_hex_str_lc(addr) and len(addr) == self.addr_len * 2: + return parsed_addr( bytes.fromhex(addr), 'ethereum' ) + if g.debug: + Msg(f'Invalid address: {addr}') + return False + + @classmethod + def checksummed_addr(cls,addr): + h = self.keccak_256(addr.encode()).digest().hex() + return ''.join(addr[i].upper() if int(h[i],16) > 7 else addr[i] for i in range(len(addr))) + + def pubhash2addr(self,pubkey_hash,p2sh): + assert len(pubkey_hash) == 20, f'{len(pubkey_hash)}: invalid length for {self.name} pubkey hash' + assert not p2sh, f'{self.name} protocol has no P2SH address format' + return pubkey_hash.hex() + +class testnet(mainnet): + chain_names = ['kovan','goerli','rinkeby'] + +class regtest(testnet): + chain_names = ['developmentchain'] diff --git a/mmgen/proto/ltc.py b/mmgen/proto/ltc.py new file mode 100755 index 00000000..cb19e4eb --- /dev/null +++ b/mmgen/proto/ltc.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +# +# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet +# Copyright (C)2013-2022 The MMGen Project +# Licensed under the GNU General Public License, Version 3: +# https://www.gnu.org/licenses +# Public project repositories: +# https://github.com/mmgen/mmgen +# https://gitlab.com/mmgen/mmgen + +""" +Litecoin protocol +""" + +from .btc import mainnet + +class mainnet(mainnet): + block0 = '12a765e31ffd4059bada1e25190f6e98c99d9714d334efa41a195a7e7e04bfe2' + addr_ver_bytes = { '30': 'p2pkh', '32': 'p2sh', '05': 'p2sh' } # new p2sh ver 0x32 must come first + wif_ver_num = { 'std': 'b0' } + mmtypes = ('L','C','S','B') + coin_amt = 'LTCAmt' + max_tx_fee = '0.3' + base_coin = 'LTC' + forks = [] + bech32_hrp = 'ltc' + avg_bdi = 150 + halving_interval = 840000 + ignore_daemon_version = False + +class testnet(mainnet): + # addr ver nums same as Bitcoin testnet, except for 'p2sh' + addr_ver_bytes = { '6f':'p2pkh', '3a':'p2sh', 'c4':'p2sh' } + wif_ver_num = { 'std': 'ef' } # same as Bitcoin testnet + bech32_hrp = 'tltc' + +class regtest(testnet): + bech32_hrp = 'rltc' + halving_interval = 150 diff --git a/mmgen/proto/xmr.py b/mmgen/proto/xmr.py new file mode 100755 index 00000000..d5dd30ee --- /dev/null +++ b/mmgen/proto/xmr.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +# +# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet +# Copyright (C)2013-2022 The MMGen Project +# Licensed under the GNU General Public License, Version 3: +# https://www.gnu.org/licenses +# Public project repositories: +# https://github.com/mmgen/mmgen +# https://gitlab.com/mmgen/mmgen + +""" +Monero protocol +""" + +from ..protocol import CoinProtocol,_nw + +# https://github.com/monero-project/monero/blob/master/src/cryptonote_config.h +class mainnet(CoinProtocol.DummyWIF,CoinProtocol.Base): + + network_names = _nw('mainnet','stagenet',None) + base_coin = 'XMR' + addr_ver_bytes = { '12': 'monero', '2a': 'monero_sub' } + addr_len = 68 + wif_ver_num = {} + pubkey_types = ('monero',) + mmtypes = ('M',) + dfl_mmtype = 'M' + pubkey_type = 'monero' # required by DummyWIF + avg_bdi = 120 + privkey_len = 32 + mmcaps = ('key','addr') + ignore_daemon_version = False + coin_amt = 'XMRAmt' + + def preprocess_key(self,sec,pubkey_type): # reduce key + from ..ed25519 import l + return int.to_bytes( + int.from_bytes( sec[::-1], 'big' ) % l, + self.privkey_len, + 'big' )[::-1] + + def parse_addr(self,addr): + + from ..baseconv import baseconv + + def b58dec(addr_str): + bc = baseconv('b58') + l = len(addr_str) + a = b''.join([bc.tobytes( addr_str[i*11:i*11+11], pad=8 ) for i in range(l//11)]) + b = bc.tobytes( addr_str[-(l%11):], pad=5 ) + return a + b + + ret = b58dec(addr) + + chk = self.keccak_256(ret[:-4]).digest()[:4] + + assert ret[-4:] == chk, f'{ret[-4:].hex()}: incorrect checksum. Correct value: {chk.hex()}' + + return self.parse_addr_bytes(ret) + +class testnet(mainnet): # use stagenet for testnet + addr_ver_bytes = { '18': 'monero', '24': 'monero_sub' } # testnet is ('35','3f') diff --git a/mmgen/proto/zec.py b/mmgen/proto/zec.py new file mode 100755 index 00000000..628de4b7 --- /dev/null +++ b/mmgen/proto/zec.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +# +# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet +# Copyright (C)2013-2022 The MMGen Project +# Licensed under the GNU General Public License, Version 3: +# https://www.gnu.org/licenses +# Public project repositories: +# https://github.com/mmgen/mmgen +# https://gitlab.com/mmgen/mmgen + +""" +Zcash protocol +""" + +from .btc import mainnet + +class mainnet(mainnet): + base_coin = 'ZEC' + addr_ver_bytes = { '1cb8': 'p2pkh', '1cbd': 'p2sh', '169a': 'zcash_z', 'a8abd3': 'viewkey' } + wif_ver_num = { 'std': '80', 'zcash_z': 'ab36' } + pubkey_types = ('std','zcash_z') + mmtypes = ('L','C','Z') + mmcaps = ('key','addr') + dfl_mmtype = 'L' + avg_bdi = 75 + + def __init__(self,*args,**kwargs): + super().__init__(*args,**kwargs) + from ..opts import opt + self.coin_id = 'ZEC-Z' if opt.type in ('zcash_z','Z') else 'ZEC-T' + + def get_addr_len(self,addr_fmt): + return (20,64)[addr_fmt in ('zcash_z','viewkey')] + + def preprocess_key(self,sec,pubkey_type): + if pubkey_type == 'zcash_z': # zero the first four bits + return bytes([sec[0] & 0x0f]) + sec[1:] + else: + return super().preprocess_key(sec,pubkey_type) + + def pubhash2addr(self,pubkey_hash,p2sh): + hash_len = len(pubkey_hash) + if hash_len == 20: + return super().pubhash2addr(pubkey_hash,p2sh) + elif hash_len == 64: + raise NotImplementedError('Zcash z-addresses do not support pubhash2addr()') + else: + raise ValueError(f'{hash_len}: incorrect pubkey_hash length') + +class testnet(mainnet): + wif_ver_num = { 'std': 'ef', 'zcash_z': 'ac08' } + addr_ver_bytes = { '1d25': 'p2pkh', '1cba': 'p2sh', '16b6': 'zcash_z', 'a8ac0c': 'viewkey' } diff --git a/mmgen/protocol.py b/mmgen/protocol.py index 6677b21d..b49aabe0 100755 --- a/mmgen/protocol.py +++ b/mmgen/protocol.py @@ -17,10 +17,9 @@ # along with this program. If not, see . """ -protocol.py: Coin protocol functions, classes and methods +protocol.py: Coin protocol base classes and initializer """ -import sys,os,hashlib from collections import namedtuple from .devtools import * @@ -29,41 +28,11 @@ from .globalvars import g parsed_wif = namedtuple('parsed_wif',['sec','pubkey_type','compressed']) parsed_addr = namedtuple('parsed_addr',['bytes','fmt']) -def hash160(in_bytes): # OP_HASH160 - return hashlib.new('ripemd160',hashlib.sha256(in_bytes).digest()).digest() - -def hash256(in_bytes): # OP_HASH256 - return hashlib.sha256(hashlib.sha256(in_bytes).digest()).digest() - -_b58a='123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' - -# From en.bitcoin.it: -# The Base58 encoding used is home made, and has some differences. -# Especially, leading zeroes are kept as single zeroes when conversion happens. -# Test: 5JbQQTs3cnoYN9vDYaGY6nhQ1DggVsY4FJNBUfEfpSQqrEp3srk -# The 'zero address': -# 1111111111111111111114oLvT2 (pubkeyhash = '\0'*20) - -def _b58chk_encode(in_bytes): - lzeroes = len(in_bytes) - len(in_bytes.lstrip(b'\x00')) - def do_enc(n): - while n: - yield _b58a[n % 58] - n //= 58 - return ('1' * lzeroes) + ''.join(do_enc(int.from_bytes(in_bytes+hash256(in_bytes)[:4],'big')))[::-1] - -def _b58chk_decode(s): - lzeroes = len(s) - len(s.lstrip('1')) - res = sum(_b58a.index(ch) * 58**n for n,ch in enumerate(s[::-1])) - bl = res.bit_length() - out = b'\x00' * lzeroes + res.to_bytes(bl//8 + bool(bl%8),'big') - if out[-4:] != hash256(out[:-4])[:4]: - raise ValueError('_b58chk_decode(): incorrect checksum') - return out[:-4] - _finfo = namedtuple('fork_info',['height','hash','name','replayable']) _nw = namedtuple('coin_networks',['mainnet','testnet','regtest']) +_b58a='123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' # shared by Bitcoin and Monero + class CoinProtocol(MMGenObject): proto_info = namedtuple('proto_info',['name','trust_level']) # trust levels: see altcoin.py @@ -107,7 +76,7 @@ class CoinProtocol(MMGenObject): self.chain_names = [self.network] if self.tokensym: - assert isinstance(self,CoinProtocol.Ethereum), 'CoinProtocol.Base_chk1' + assert self.name.startswith('Ethereum'), 'CoinProtocol.Base_chk1' if self.base_coin in ('ETH','XMR'): from .util import get_keccak @@ -212,168 +181,10 @@ class CoinProtocol(MMGenObject): ymsg(f'Warning: private key is greater than secp256k1 group order!:\n {hexpriv}') return (pk % self.secp256k1_ge).to_bytes(self.privkey_len,'big') - class Bitcoin(Secp256k1): # chainparams.cpp - """ - All Bitcoin code and chain forks inherit from this class - """ - mod_clsname = 'Bitcoin' - network_names = _nw('mainnet','testnet','regtest') - addr_ver_bytes = { '00': 'p2pkh', '05': 'p2sh' } - addr_len = 20 - wif_ver_num = { 'std': '80' } - mmtypes = ('L','C','S','B') - dfl_mmtype = 'L' - coin_amt = 'BTCAmt' - max_tx_fee = '0.003' - sighash_type = 'ALL' - block0 = '000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f' - forks = [ - _finfo(478559,'00000000000000000019f112ec0a9982926f1258cdcc558dd7c3b7e5dc7fa148','BCH',False), - ] - caps = ('rbf','segwit') - mmcaps = ('key','addr','rpc','tx') - base_coin = 'BTC' - base_proto = 'Bitcoin' - # From BIP173: witness version 'n' is stored as 'OP_n'. OP_0 is encoded as 0x00, - # but OP_1 through OP_16 are encoded as 0x51 though 0x60 (81 to 96 in decimal). - witness_vernum_hex = '00' - witness_vernum = int(witness_vernum_hex,16) - bech32_hrp = 'bc' - sign_mode = 'daemon' - avg_bdi = int(9.7 * 60) # average block discovery interval (historical) - halving_interval = 210000 - max_halvings = 64 - start_subsidy = 50 - ignore_daemon_version = False - max_int = 0xffffffff - - def bytes2wif(self,privbytes,pubkey_type,compressed): # input is preprocessed hex - assert len(privbytes) == self.privkey_len, f'{len(privbytes)} bytes: incorrect private key length!' - assert pubkey_type in self.wif_ver_num, f'{pubkey_type!r}: invalid pubkey_type' - return _b58chk_encode( - bytes.fromhex(self.wif_ver_num[pubkey_type]) - + privbytes - + (b'',b'\x01')[bool(compressed)]) - - def parse_wif(self,wif): - key = _b58chk_decode(wif) - - for k,v in self.wif_ver_num.items(): - v = bytes.fromhex(v) - if key[:len(v)] == v: - pubkey_type = k - key = key[len(v):] - break - else: - raise ValueError('Invalid WIF version number') - - if len(key) == self.privkey_len + 1: - assert key[-1] == 0x01, f'{key[-1]!r}: invalid compressed key suffix byte' - compressed = True - elif len(key) == self.privkey_len: - compressed = False - else: - raise ValueError(f'{len(key)}: invalid key length') - - return parsed_wif( - sec = key[:self.privkey_len], - pubkey_type = pubkey_type, - compressed = compressed ) - - def parse_addr(self,addr): - - if 'B' in self.mmtypes and addr[:len(self.bech32_hrp)] == self.bech32_hrp: - import mmgen.bech32 as bech32 - ret = bech32.decode(self.bech32_hrp,addr) - - if ret[0] != self.witness_vernum: - from .util import msg - msg(f'{ret[0]}: Invalid witness version number') - return False - - return parsed_addr( bytes(ret[1]), 'bech32' ) if ret[1] else False - - return self.parse_addr_bytes(_b58chk_decode(addr)) - - def pubhash2addr(self,pubkey_hash,p2sh): - assert len(pubkey_hash) == 20, f'{len(pubkey_hash)}: invalid length for pubkey hash' - return _b58chk_encode( - self.addr_fmt_to_ver_bytes(('p2pkh','p2sh')[p2sh],return_hex=False) + pubkey_hash - ) - - # Segwit: - def pubkey2redeem_script(self,pubkey): - # https://bitcoincore.org/en/segwit_wallet_dev/ - # The P2SH redeemScript is always 22 bytes. It starts with a OP_0, followed - # by a canonical push of the keyhash (i.e. 0x0014{20-byte keyhash}) - return bytes.fromhex(self.witness_vernum_hex + '14') + hash160(pubkey) - - def pubkey2segwitaddr(self,pubkey): - return self.pubhash2addr( - hash160( self.pubkey2redeem_script(pubkey)), p2sh=True ) - - def pubhash2bech32addr(self,pubhash): - d = list(pubhash) - import mmgen.bech32 as bech32 - return bech32.bech32_encode(self.bech32_hrp,[self.witness_vernum]+bech32.convertbits(d,8,5)) - - class BitcoinTestnet(Bitcoin): - addr_ver_bytes = { '6f': 'p2pkh', 'c4': 'p2sh' } - wif_ver_num = { 'std': 'ef' } - bech32_hrp = 'tb' - - class BitcoinRegtest(BitcoinTestnet): - bech32_hrp = 'bcrt' - halving_interval = 150 - - class BitcoinCash(Bitcoin): - is_fork_of = 'Bitcoin' - mmtypes = ('L','C') - sighash_type = 'ALL|FORKID' - forks = [ - _finfo(478559,'000000000000000000651ef99cb9fcbe0dadde1d424bd9f15ff20136191a5eec','BTC',False) - ] - caps = () - coin_amt = 'BCHAmt' - max_tx_fee = '0.1' - ignore_daemon_version = False - - def pubkey2redeem_script(self,pubkey): raise NotImplementedError - def pubkey2segwitaddr(self,pubkey): raise NotImplementedError - - class BitcoinCashTestnet(BitcoinCash): - addr_ver_bytes = { '6f': 'p2pkh', 'c4': 'p2sh' } - wif_ver_num = { 'std': 'ef' } - - class BitcoinCashRegtest(BitcoinCashTestnet): - halving_interval = 150 - - class Litecoin(Bitcoin): - block0 = '12a765e31ffd4059bada1e25190f6e98c99d9714d334efa41a195a7e7e04bfe2' - addr_ver_bytes = { '30': 'p2pkh', '32': 'p2sh', '05': 'p2sh' } # new p2sh ver 0x32 must come first - wif_ver_num = { 'std': 'b0' } - mmtypes = ('L','C','S','B') - coin_amt = 'LTCAmt' - max_tx_fee = '0.3' - base_coin = 'LTC' - forks = [] - bech32_hrp = 'ltc' - avg_bdi = 150 - halving_interval = 840000 - ignore_daemon_version = False - - class LitecoinTestnet(Litecoin): - # addr ver nums same as Bitcoin testnet, except for 'p2sh' - addr_ver_bytes = { '6f':'p2pkh', '3a':'p2sh', 'c4':'p2sh' } - wif_ver_num = { 'std': 'ef' } # same as Bitcoin testnet - bech32_hrp = 'tltc' - - class LitecoinRegtest(LitecoinTestnet): - bech32_hrp = 'rltc' - halving_interval = 150 - class DummyWIF: - + """ + Ethereum and Monero protocols inherit from this class + """ def bytes2wif(self,privbytes,pubkey_type,compressed): assert pubkey_type == self.pubkey_type, f'{pubkey_type}: invalid pubkey_type for {self.name} protocol!' assert compressed == False, f'{self.name} protocol does not support compressed pubkeys!' @@ -385,163 +196,6 @@ class CoinProtocol(MMGenObject): pubkey_type = self.pubkey_type, compressed = False ) - class Ethereum(DummyWIF,Secp256k1): - - network_names = _nw('mainnet','testnet','devnet') - addr_len = 20 - mmtypes = ('E',) - dfl_mmtype = 'E' - mod_clsname = 'Ethereum' - base_coin = 'ETH' - pubkey_type = 'std' # required by DummyWIF - - coin_amt = 'ETHAmt' - max_tx_fee = '0.005' - chain_names = ['ethereum','foundation'] - sign_mode = 'standalone' - caps = ('token',) - mmcaps = ('key','addr','rpc','tx') - base_proto = 'Ethereum' - avg_bdi = 15 - ignore_daemon_version = False - - chain_ids = { - 1: 'ethereum', # ethereum mainnet - 2: 'morden', # morden testnet (deprecated) - 3: 'ropsten', # ropsten testnet - 4: 'rinkeby', # rinkeby testnet - 5: 'goerli', # goerli testnet - 42: 'kovan', # kovan testnet - 61: 'classic', # ethereum classic mainnet - 62: 'morden', # ethereum classic testnet - 17: 'developmentchain', # parity dev chain - 1337: 'developmentchain', # geth dev chain - } - - @property - def dcoin(self): - return self.tokensym or self.coin - - def parse_addr(self,addr): - from .util import is_hex_str_lc - if is_hex_str_lc(addr) and len(addr) == self.addr_len * 2: - return parsed_addr( bytes.fromhex(addr), 'ethereum' ) - if g.debug: - from .util import Msg - Msg(f'Invalid address: {addr}') - return False - - @classmethod - def checksummed_addr(cls,addr): - h = self.keccak_256(addr.encode()).digest().hex() - return ''.join(addr[i].upper() if int(h[i],16) > 7 else addr[i] for i in range(len(addr))) - - def pubhash2addr(self,pubkey_hash,p2sh): - assert len(pubkey_hash) == 20, f'{len(pubkey_hash)}: invalid length for {self.name} pubkey hash' - assert not p2sh, f'{self.name} protocol has no P2SH address format' - return pubkey_hash.hex() - - class EthereumTestnet(Ethereum): - chain_names = ['kovan','goerli','rinkeby'] - - class EthereumRegtest(EthereumTestnet): - chain_names = ['developmentchain'] - - class EthereumClassic(Ethereum): - chain_names = ['classic','ethereum_classic'] - max_tx_fee = '0.005' - ignore_daemon_version = False - - class EthereumClassicTestnet(EthereumClassic): - chain_names = ['morden','morden_testnet','classic-testnet'] - - class EthereumClassicRegtest(EthereumClassicTestnet): - chain_names = ['developmentchain'] - - class Zcash(Bitcoin): - base_coin = 'ZEC' - addr_ver_bytes = { '1cb8': 'p2pkh', '1cbd': 'p2sh', '169a': 'zcash_z', 'a8abd3': 'viewkey' } - wif_ver_num = { 'std': '80', 'zcash_z': 'ab36' } - pubkey_types = ('std','zcash_z') - mmtypes = ('L','C','Z') - mmcaps = ('key','addr') - dfl_mmtype = 'L' - avg_bdi = 75 - - def __init__(self,*args,**kwargs): - super().__init__(*args,**kwargs) - from .opts import opt - self.coin_id = 'ZEC-Z' if opt.type in ('zcash_z','Z') else 'ZEC-T' - - def get_addr_len(self,addr_fmt): - return (20,64)[addr_fmt in ('zcash_z','viewkey')] - - def preprocess_key(self,sec,pubkey_type): - if pubkey_type == 'zcash_z': # zero the first four bits - return bytes([sec[0] & 0x0f]) + sec[1:] - else: - return super().preprocess_key(sec,pubkey_type) - - def pubhash2addr(self,pubkey_hash,p2sh): - hash_len = len(pubkey_hash) - if hash_len == 20: - return super().pubhash2addr(pubkey_hash,p2sh) - elif hash_len == 64: - raise NotImplementedError('Zcash z-addresses do not support pubhash2addr()') - else: - raise ValueError(f'{hash_len}: incorrect pubkey_hash length') - - class ZcashTestnet(Zcash): - wif_ver_num = { 'std': 'ef', 'zcash_z': 'ac08' } - addr_ver_bytes = { '1d25': 'p2pkh', '1cba': 'p2sh', '16b6': 'zcash_z', 'a8ac0c': 'viewkey' } - - # https://github.com/monero-project/monero/blob/master/src/cryptonote_config.h - class Monero(DummyWIF,Base): - - network_names = _nw('mainnet','stagenet',None) - base_coin = 'XMR' - addr_ver_bytes = { '12': 'monero', '2a': 'monero_sub' } - addr_len = 68 - wif_ver_num = {} - pubkey_types = ('monero',) - mmtypes = ('M',) - dfl_mmtype = 'M' - pubkey_type = 'monero' # required by DummyWIF - avg_bdi = 120 - privkey_len = 32 - mmcaps = ('key','addr') - ignore_daemon_version = False - coin_amt = 'XMRAmt' - - def preprocess_key(self,sec,pubkey_type): # reduce key - from .ed25519 import l - return int.to_bytes( - int.from_bytes( sec[::-1], 'big' ) % l, - self.privkey_len, - 'big' )[::-1] - - def parse_addr(self,addr): - - from .baseconv import baseconv - - def b58dec(addr_str): - bc = baseconv('b58') - l = len(addr_str) - a = b''.join([bc.tobytes( addr_str[i*11:i*11+11], pad=8 ) for i in range(l//11)]) - b = bc.tobytes( addr_str[-(l%11):], pad=5 ) - return a + b - - ret = b58dec(addr) - - chk = self.keccak_256(ret[:-4]).digest()[:4] - - assert ret[-4:] == chk, f'{ret[-4:].hex()}: incorrect checksum. Correct value: {chk.hex()}' - - return self.parse_addr_bytes(ret) - - class MoneroTestnet(Monero): # use stagenet for testnet - addr_ver_bytes = { '18': 'monero', '24': 'monero_sub' } # testnet is ('35','3f') - def init_proto(coin=None,testnet=False,regtest=False,network=None,network_id=None,tokensym=None,need_amt=False): assert type(testnet) == bool, 'init_proto_chk1' @@ -567,6 +221,14 @@ def init_proto(coin=None,testnet=False,regtest=False,network=None,network_id=Non name = CoinProtocol.coins[coin].name proto_name = name + ('' if network == 'mainnet' else network.capitalize()) + if not hasattr(CoinProtocol,proto_name): + import importlib + setattr( + CoinProtocol, + proto_name, + getattr(importlib.import_module(f'mmgen.proto.{coin}'),network) + ) + return getattr(CoinProtocol,proto_name)( coin = coin, name = name, @@ -622,4 +284,5 @@ def warn_trustlevel(coinsym): return if not keypress_confirm(warning,default_yes=True): + import sys sys.exit(0) diff --git a/mmgen/tool/coin.py b/mmgen/tool/coin.py index 1a3c4e79..d970ef4c 100755 --- a/mmgen/tool/coin.py +++ b/mmgen/tool/coin.py @@ -133,7 +133,7 @@ class tool_cmd(tool_cmd_base): if self.mmtype.name == 'segwit': return self.proto.pubkey2segwitaddr( pubkey ) else: - from ..protocol import hash160 + from ..proto.btc import hash160 return self.pubhash2addr( hash160(pubkey).hex() ) def pubhex2redeem_script(self,pubkeyhex:'sstr'): # new @@ -146,7 +146,7 @@ class tool_cmd(tool_cmd_base): assert self.mmtype.name == 'segwit', 'This command is meaningful only for --type=segwit' assert redeem_scripthex[:4] == '0014', f'{redeem_scripthex!r}: invalid redeem script' assert len(redeem_scripthex) == 44, f'{len(redeem_scripthex)//2} bytes: invalid redeem script length' - from ..protocol import hash160 + from ..proto.btc import hash160 return self.pubhash2addr( hash160(bytes.fromhex(redeem_scripthex)).hex() ) def pubhash2addr(self,pubhashhex:'sstr'): diff --git a/mmgen/tool/util.py b/mmgen/tool/util.py index 6b79ac70..e05ee4a2 100755 --- a/mmgen/tool/util.py +++ b/mmgen/tool/util.py @@ -78,7 +78,7 @@ class tool_cmd(tool_cmd_base): def hash160(self,hexstr:'sstr'): "compute ripemd160(sha256(data)) (convert hex pubkey to hex addr)" - from ..protocol import hash160 + from ..proto.btc import hash160 return hash160( bytes.fromhex(hexstr) ).hex() def hash256(self,string_or_bytes:str,file_input=False,hex_input=False): # TODO: handle stdin @@ -143,12 +143,12 @@ class tool_cmd(tool_cmd_base): def hextob58chk(self,hexstr:'sstr'): "convert a hexadecimal number to base58-check encoding" - from ..protocol import _b58chk_encode + from ..proto.btc import _b58chk_encode return _b58chk_encode( bytes.fromhex(hexstr) ) def b58chktohex(self,b58chk_num:'sstr'): "convert a base58-check encoded number to hexadecimal" - from ..protocol import _b58chk_decode + from ..proto.btc import _b58chk_decode return _b58chk_decode(b58chk_num).hex() def hextob32(self,hexstr:'sstr',pad=0): diff --git a/mmgen/util.py b/mmgen/util.py index bb5deffe..e259962b 100755 --- a/mmgen/util.py +++ b/mmgen/util.py @@ -674,8 +674,8 @@ def altcoin_subclass(cls,proto,mod_dir): """ magic module loading and class retrieval """ - from .protocol import CoinProtocol - if isinstance(proto,CoinProtocol.Bitcoin): + + if proto.base_coin != 'ETH': return cls modname = f'mmgen.altcoins.{proto.base_coin.lower()}.{mod_dir}' diff --git a/mmgen/xmrwallet.py b/mmgen/xmrwallet.py index d8b67903..e140cb94 100755 --- a/mmgen/xmrwallet.py +++ b/mmgen/xmrwallet.py @@ -26,7 +26,7 @@ from .common import * from .objmethods import Hilite,InitErrors from .obj import CoinTxID from .seed import SeedID -from .protocol import _b58a,init_proto +from .protocol import init_proto,_b58a from .addr import CoinAddr,AddrIdx from .addrlist import KeyAddrList,AddrIdxList from .rpc import MoneroRPCClientRaw,MoneroWalletRPCClient,json_encoder diff --git a/setup.cfg b/setup.cfg index 88369786..6aecfb8e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -42,6 +42,7 @@ install_requires = packages = mmgen mmgen.share + mmgen.proto mmgen.tool mmgen.altcoins mmgen.altcoins.eth diff --git a/test/misc/tool_api_test.py b/test/misc/tool_api_test.py index 68b28957..53911eed 100755 --- a/test/misc/tool_api_test.py +++ b/test/misc/tool_api_test.py @@ -73,7 +73,7 @@ def run_test(): tool.usr_randchars = 0 # setter check_equal(tool.usr_randchars,0) - check_equal(f'{tool.coin} {tool.proto.cls_name} {tool.addrtype}', 'BTC Bitcoin L' ) + check_equal(f'{tool.coin} {tool.proto.cls_name} {tool.addrtype}', 'BTC mainnet L' ) # test vectors from tooltest2.py: diff --git a/test/overlay/__init__.py b/test/overlay/__init__.py index 9f9aa601..3333824b 100644 --- a/test/overlay/__init__.py +++ b/test/overlay/__init__.py @@ -37,6 +37,7 @@ def overlay_setup(repo_root): 'mmgen.data', 'mmgen.share', 'mmgen.tool', + 'mmgen.proto', 'mmgen.altcoins', 'mmgen.altcoins.eth', 'mmgen.altcoins.eth.pyethereum',