modularize coin protocols

- protocols are now in individual modules under `proto`
This commit is contained in:
The MMGen Project 2022-01-29 11:25:10 +00:00
commit 7558c539a1
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
22 changed files with 491 additions and 373 deletions

View file

@ -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()

View file

@ -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):

View file

@ -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(

View file

@ -1 +1 @@
13.1.dev011
13.1.dev012

View file

@ -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() )

View file

@ -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()

0
mmgen/proto/__init__.py Executable file
View file

37
mmgen/proto/bch.py Executable file
View file

@ -0,0 +1,37 @@
#!/usr/bin/env python3
#
# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
# Copyright (C)2013-2022 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
# 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

160
mmgen/proto/btc.py Executable file
View file

@ -0,0 +1,160 @@
#!/usr/bin/env python3
#
# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
# Copyright (C)2013-2022 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
# 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

26
mmgen/proto/etc.py Executable file
View file

@ -0,0 +1,26 @@
#!/usr/bin/env python3
#
# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
# Copyright (C)2013-2022 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
# 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']

76
mmgen/proto/eth.py Executable file
View file

@ -0,0 +1,76 @@
#!/usr/bin/env python3
#
# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
# Copyright (C)2013-2022 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
# 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']

39
mmgen/proto/ltc.py Executable file
View file

@ -0,0 +1,39 @@
#!/usr/bin/env python3
#
# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
# Copyright (C)2013-2022 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
# 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

62
mmgen/proto/xmr.py Executable file
View file

@ -0,0 +1,62 @@
#!/usr/bin/env python3
#
# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
# Copyright (C)2013-2022 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
# 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')

52
mmgen/proto/zec.py Executable file
View file

@ -0,0 +1,52 @@
#!/usr/bin/env python3
#
# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
# Copyright (C)2013-2022 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
# 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' }

View file

@ -17,10 +17,9 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
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)

View file

@ -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'):

View file

@ -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):

View file

@ -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}'

View file

@ -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

View file

@ -42,6 +42,7 @@ install_requires =
packages =
mmgen
mmgen.share
mmgen.proto
mmgen.tool
mmgen.altcoins
mmgen.altcoins.eth

View file

@ -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:

View file

@ -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',