overhaul public key and address generation code

- pubkey generation code has been rewritten and moved from addr.py to
  keygen.py
- address generation code has been rewritten and moved from addr.py to
  addrgen.py
- keygen/addrgen classes now present a consistent API across all pubkey and
  address types
- key/address operations and related data objects now use bytes internally
  instead of hex strings
- pubkey generator backends are now selected using the `--keygen-backend`
  option
- for Monero pubkeys, the new `nacl` backend has replaced `ed25519ll_djbec`
  as the default
- a minimal unit test has been added

Examples:

    # Generate a random Monero keypair using the unoptimized 'ed25519' backend:
    $ mmgen-tool --coin=xmr --keygen-backend=3 randpair

    # Generate an LTC Bech32 address list from the default wallet using the
    # 'python-ecdsa' backend:
    $ mmgen-addrgen --coin=ltc --type=bech32 --keygen-backend=2 1-10

Testing:

    # Run the minimal unit test:
    $ test/unit_tests_py gen

    # Compare BTC Segwit addresses from default 'libsecp256k1' backend to
    # 'pycoin' library, with edge cases and 10,000 random rounds:
    $ test/gentest.py --type=segwit 1:pycoin 10000

    # Test all configured Monero backends against 'moneropy', with edge cases
    # and 10 random rounds:
    $ test/gentest.py --coin=xmr all:moneropy 10

    # Test the 'nacl' and 'ed25519ll_djbec' backends against each other, with
    # edge cases and 1000 random rounds:
    $ test/gentest.py --coin=xmr 1:2 1000

    # Test the speed of the Monero 'nacl' backend using 10,000 rounds:
    $ test/gentest.py --coin=xmr 1 10000

    # Same for Zcash:
    $ test/gentest.py --coin=zec --type=zcash_z 1 10000
This commit is contained in:
The MMGen Project 2022-01-15 14:00:12 +00:00
commit 32c522c039
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
31 changed files with 735 additions and 466 deletions

View file

@ -17,7 +17,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
addr.py: Address generation/display routines for the MMGen suite
addr.py: MMGen address-related types
"""
from string import ascii_letters,digits
@ -179,260 +179,43 @@ class MoneroViewKey(HexStr):
class ZcashViewKey(CoinAddr):
hex_width = 128
from .opts import opt
from .util import qmsg
from .protocol import hash160
from .key import PrivKey,PubKey
from .baseconv import baseconv
def KeyGenerator(proto,pubkey_type,backend=None,silent=False):
"""
factory function returning a key generator backend for the specified pubkey type
"""
assert pubkey_type in proto.pubkey_types, f'{pubkey_type!r}: invalid pubkey type for coin {proto.coin}'
class AddrGenerator(MMGenObject):
def __new__(cls,proto,addr_type):
from .keygen import keygen_backend,_check_backend
if type(addr_type) == str:
addr_type = MMGenAddrType(proto=proto,id_str=addr_type)
elif type(addr_type) == MMGenAddrType:
assert addr_type in proto.mmtypes, f'{addr_type}: invalid address type for coin {proto.coin}'
else:
raise TypeError(f'{type(addr_type)}: incorrect argument type for {cls.__name__}()')
pubkey_type_cls = getattr(keygen_backend,pubkey_type)
addr_generators = {
'p2pkh': AddrGeneratorP2PKH,
'segwit': AddrGeneratorSegwit,
'bech32': AddrGeneratorBech32,
'ethereum': AddrGeneratorEthereum,
'zcash_z': AddrGeneratorZcashZ,
'monero': AddrGeneratorMonero,
}
me = super(cls,cls).__new__(addr_generators[addr_type.gen_method])
me.desc = type(me).__name__
me.proto = proto
me.addr_type = addr_type
me.pubkey_type = addr_type.pubkey_type
return me
from .opts import opt
backend = backend or getattr(opt,'keygen_backend',None)
class AddrGeneratorP2PKH(AddrGenerator):
def to_addr(self,pubhex):
assert pubhex.privkey.pubkey_type == self.pubkey_type
return CoinAddr(self.proto,self.proto.pubhash2addr(hash160(pubhex),p2sh=False))
if backend:
_check_backend(backend,pubkey_type)
def to_segwit_redeem_script(self,pubhex):
raise NotImplementedError('Segwit redeem script not supported by this address type')
backend_id = pubkey_type_cls.backends[int(backend) - 1 if backend else 0]
class AddrGeneratorSegwit(AddrGenerator):
def to_addr(self,pubhex):
assert pubhex.privkey.pubkey_type == self.pubkey_type
assert pubhex.compressed,'Uncompressed public keys incompatible with Segwit'
return CoinAddr(self.proto,self.proto.pubhex2segwitaddr(pubhex))
if backend_id == 'libsecp256k1':
if not pubkey_type_cls.libsecp256k1.test_avail(silent=silent):
backend_id = 'python-ecdsa'
if not backend:
qmsg('Using (slow) native Python ECDSA library for public key generation')
def to_segwit_redeem_script(self,pubhex):
assert pubhex.compressed,'Uncompressed public keys incompatible with Segwit'
return HexStr(self.proto.pubhex2redeem_script(pubhex))
return getattr(pubkey_type_cls,backend_id.replace('-','_'))()
class AddrGeneratorBech32(AddrGenerator):
def to_addr(self,pubhex):
assert pubhex.privkey.pubkey_type == self.pubkey_type
assert pubhex.compressed,'Uncompressed public keys incompatible with Segwit'
return CoinAddr(self.proto,self.proto.pubhash2bech32addr(hash160(pubhex)))
def AddrGenerator(proto,addr_type):
"""
factory function returning an address generator for the specified address type
"""
if type(addr_type) == str:
addr_type = MMGenAddrType(proto=proto,id_str=addr_type)
elif type(addr_type) == MMGenAddrType:
assert addr_type in proto.mmtypes, f'{addr_type}: invalid address type for coin {proto.coin}'
else:
raise TypeError(f'{type(addr_type)}: incorrect argument type for {cls.__name__}()')
def to_segwit_redeem_script(self,pubhex):
raise NotImplementedError('Segwit redeem script not supported by this address type')
from .addrgen import addr_generator
class AddrGeneratorEthereum(AddrGenerator):
def __init__(self,proto,addr_type):
from .util import get_keccak
self.keccak_256 = get_keccak()
from .protocol import hash256
self.hash256 = hash256
def to_addr(self,pubhex):
assert pubhex.privkey.pubkey_type == self.pubkey_type
return CoinAddr(self.proto,self.keccak_256(bytes.fromhex(pubhex[2:])).hexdigest()[24:])
def to_wallet_passwd(self,sk_hex):
return WalletPassword(self.hash256(sk_hex)[:32])
def to_segwit_redeem_script(self,pubhex):
raise NotImplementedError('Segwit redeem script not supported by this address type')
# github.com/FiloSottile/zcash-mini/zcash/address.go
class AddrGeneratorZcashZ(AddrGenerator):
def zhash256(self,s,t):
s = bytearray(s + bytes(32))
s[0] |= 0xc0
s[32] = t
from .sha2 import Sha256
return Sha256(s,preprocess=False).digest()
def to_addr(self,pubhex): # pubhex is really privhex
assert pubhex.privkey.pubkey_type == self.pubkey_type
key = bytes.fromhex(pubhex)
assert len(key) == 32, f'{len(key)}: incorrect privkey length'
from nacl.bindings import crypto_scalarmult_base
p2 = crypto_scalarmult_base(self.zhash256(key,1))
from .protocol import _b58chk_encode
ver_bytes = self.proto.addr_fmt_to_ver_bytes('zcash_z')
ret = _b58chk_encode(ver_bytes + self.zhash256(key,0) + p2)
return CoinAddr(self.proto,ret)
def to_viewkey(self,pubhex): # pubhex is really privhex
key = bytes.fromhex(pubhex)
assert len(key) == 32, f'{len(key)}: incorrect privkey length'
vk = bytearray(self.zhash256(key,0)+self.zhash256(key,1))
vk[32] &= 0xf8
vk[63] &= 0x7f
vk[63] |= 0x40
from .protocol import _b58chk_encode
ver_bytes = self.proto.addr_fmt_to_ver_bytes('viewkey')
ret = _b58chk_encode(ver_bytes + vk)
return ZcashViewKey(self.proto,ret)
def to_segwit_redeem_script(self,pubhex):
raise NotImplementedError('Zcash z-addresses incompatible with Segwit')
class AddrGeneratorMonero(AddrGenerator):
def __init__(self,proto,addr_type):
from .util import get_keccak
self.keccak_256 = get_keccak()
from .protocol import hash256
self.hash256 = hash256
if getattr(opt,'use_old_ed25519',False):
from .ed25519 import edwards,encodepoint,B,scalarmult
else:
from .ed25519ll_djbec import scalarmult
from .ed25519 import edwards,encodepoint,B
self.edwards = edwards
self.encodepoint = encodepoint
self.scalarmult = scalarmult
self.B = B
def b58enc(self,addr_bytes):
enc = baseconv.frombytes
l = len(addr_bytes)
a = ''.join([enc(addr_bytes[i*8:i*8+8],'b58',pad=11,tostr=True) for i in range(l//8)])
b = enc(addr_bytes[l-l%8:],'b58',pad=7,tostr=True)
return a + b
def to_addr(self,sk_hex): # sk_hex instead of pubhex
assert sk_hex.privkey.pubkey_type == self.pubkey_type
# Source and license for scalarmultbase function:
# https://github.com/bigreddmachine/MoneroPy/blob/master/moneropy/crypto/ed25519.py
# Copyright (c) 2014-2016, The Monero Project
# All rights reserved.
def scalarmultbase(e):
if e == 0: return [0, 1]
Q = self.scalarmult(self.B, e//2)
Q = self.edwards(Q, Q)
if e & 1: Q = self.edwards(Q, self.B)
return Q
def hex2int_le(hexstr):
return int((bytes.fromhex(hexstr)[::-1]).hex(),16)
vk_hex = self.to_viewkey(sk_hex)
pk_str = self.encodepoint(scalarmultbase(hex2int_le(sk_hex)))
pvk_str = self.encodepoint(scalarmultbase(hex2int_le(vk_hex)))
addr_p1 = self.proto.addr_fmt_to_ver_bytes('monero') + pk_str + pvk_str
return CoinAddr(
proto = self.proto,
addr = self.b58enc(addr_p1 + self.keccak_256(addr_p1).digest()[:4]) )
def to_wallet_passwd(self,sk_hex):
return WalletPassword(self.hash256(sk_hex)[:32])
def to_viewkey(self,sk_hex):
assert len(sk_hex) == 64, f'{len(sk_hex)}: incorrect privkey length'
return MoneroViewKey(
self.proto.preprocess_key(self.keccak_256(bytes.fromhex(sk_hex)).digest(),None).hex() )
def to_segwit_redeem_script(self,sk_hex):
raise NotImplementedError('Monero addresses incompatible with Segwit')
class KeyGenerator(MMGenObject):
def __new__(cls,proto,addr_type,generator=None,silent=False):
if type(addr_type) == str: # allow override w/o check
pubkey_type = addr_type
elif type(addr_type) == MMGenAddrType:
assert addr_type in proto.mmtypes, f'{address}: invalid address type for coin {proto.coin}'
pubkey_type = addr_type.pubkey_type
else:
raise TypeError(f'{type(addr_type)}: incorrect argument type for {cls.__name__}()')
if pubkey_type == 'std':
if cls.test_for_secp256k1(silent=silent) and generator != 1:
if not opt.key_generator or opt.key_generator == 2 or generator == 2:
me = super(cls,cls).__new__(KeyGeneratorSecp256k1)
else:
qmsg('Using (slow) native Python ECDSA library for address generation')
me = super(cls,cls).__new__(KeyGeneratorPython)
elif pubkey_type in ('zcash_z','monero'):
me = super(cls,cls).__new__(KeyGeneratorDummy)
me.desc = 'mmgen-'+pubkey_type
else:
raise ValueError(f'{pubkey_type}: invalid pubkey_type argument')
me.proto = proto
return me
@classmethod
def test_for_secp256k1(self,silent=False):
try:
from .secp256k1 import priv2pub
m = 'Unable to execute priv2pub() from secp256k1 extension module'
assert priv2pub(bytes.fromhex('deadbeef'*8),1),m
return True
except Exception as e:
if not silent:
ymsg(str(e))
return False
class KeyGeneratorPython(KeyGenerator):
desc = 'mmgen-python-ecdsa'
# devdoc/guide_wallets.md:
# Uncompressed public keys start with 0x04; compressed public keys begin with 0x03 or
# 0x02 depending on whether they're greater or less than the midpoint of the curve.
def privnum2pubhex(self,numpriv,compressed=False):
import ecdsa
pko = ecdsa.SigningKey.from_secret_exponent(numpriv,curve=ecdsa.SECP256k1)
# pubkey = x (32 bytes) + y (32 bytes) (unsigned big-endian)
pubkey = pko.get_verifying_key().to_string().hex()
if compressed: # discard Y coord, replace with appropriate version byte
# even y: <0, odd y: >0 -- https://bitcointalk.org/index.php?topic=129652.0
return ('03','02')[pubkey[-1] in '02468ace'] + pubkey[:64]
else:
return '04' + pubkey
def to_pubhex(self,privhex):
assert type(privhex) == PrivKey
return PubKey(
s = self.privnum2pubhex(int(privhex,16),compressed=privhex.compressed),
privkey = privhex )
class KeyGeneratorSecp256k1(KeyGenerator):
desc = 'mmgen-secp256k1'
def to_pubhex(self,privhex):
assert type(privhex) == PrivKey
from .secp256k1 import priv2pub
return PubKey(
s = priv2pub(bytes.fromhex(privhex),int(privhex.compressed)).hex(),
privkey = privhex )
class KeyGeneratorDummy(KeyGenerator):
desc = 'mmgen-dummy'
def to_pubhex(self,privhex):
assert type(privhex) == PrivKey
return PubKey(
s = privhex,
privkey = privhex )
return getattr(addr_generator,addr_type.name)(proto,addr_type)

View file

@ -119,7 +119,7 @@ class AddrFile(MMGenObject):
if p.has_keys:
from .opts import opt
if opt.b16:
out.append(fs.format( '', f'orig_hex: {e.sec.orig_hex()}', c ))
out.append(fs.format( '', f'orig_hex: {e.sec.orig_bytes.hex()}', c ))
out.append(fs.format( '', f'{p.al_id.mmtype.wif_label}: {e.sec.wif}', c ))
for k in ('viewkey','wallet_passwd'):
v = getattr(e,k)
@ -174,7 +174,7 @@ class AddrFile(MMGenObject):
llen = len(ret)
for n,e in enumerate(ret):
qmsg_r(f'\rVerifying keys {n+1}/{llen}')
assert e.addr == ag.to_addr(kg.to_pubhex(e.sec)),(
assert e.addr == ag.to_addr(kg.gen_data(e.sec)),(
f'Key doesn’t match address!\n {e.sec.wif}\n {e.addr}')
qmsg(' - done')

131
mmgen/addrgen.py Executable file
View file

@ -0,0 +1,131 @@
#!/usr/bin/env python3
#
# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
addrgen.py: Address and view key generation classes for the MMGen suite
"""
from .protocol import hash160,_b58chk_encode
from .addr import CoinAddr,MMGenAddrType,MoneroViewKey,ZcashViewKey
# decorator for to_addr() and to_viewkey()
def check_data(orig_func):
def f(self,data):
assert data.pubkey_type == self.pubkey_type, 'addrgen.py:check_data() pubkey_type mismatch'
assert data.compressed == self.compressed,(
f'addrgen.py:check_data() expected compressed={self.compressed} but got compressed={data.compressed}'
)
return orig_func(self,data)
return f
class addr_generator:
"""
provide a generator for each supported address format
"""
class base:
def __init__(self,proto,addr_type):
self.proto = proto
self.pubkey_type = addr_type.pubkey_type
self.compressed = addr_type.compressed
desc = f'AddrGenerator {type(self).__name__!r}'
def to_segwit_redeem_script(self,data):
raise NotImplementedError('Segwit redeem script not supported by this address type')
class p2pkh(base):
@check_data
def to_addr(self,data):
return CoinAddr(
self.proto,
self.proto.pubhash2addr( hash160(data.pubkey), p2sh=False ))
class legacy(p2pkh): pass
class compressed(p2pkh): pass
class segwit(base):
@check_data
def to_addr(self,data):
return CoinAddr(
self.proto,
self.proto.pubkey2segwitaddr(data.pubkey) )
def to_segwit_redeem_script(self,data): # NB: returns hex
return self.proto.pubkey2redeem_script(data.pubkey).hex()
class bech32(base):
@check_data
def to_addr(self,data):
return CoinAddr(
self.proto,
self.proto.pubhash2bech32addr( hash160(data.pubkey)) )
class keccak(base):
def __init__(self,proto,addr_type):
super().__init__(proto,addr_type)
from .util import get_keccak
self.keccak_256 = get_keccak()
class ethereum(keccak):
@check_data
def to_addr(self,data):
return CoinAddr(
self.proto,
self.keccak_256(data.pubkey[1:]).hexdigest()[24:] )
class monero(keccak):
def b58enc(self,addr_bytes):
from .baseconv import baseconv
enc = baseconv.frombytes
l = len(addr_bytes)
a = ''.join([enc( addr_bytes[i*8:i*8+8], 'b58', pad=11, tostr=True ) for i in range(l//8)])
b = enc( addr_bytes[l-l%8:], 'b58', pad=7, tostr=True )
return a + b
@check_data
def to_addr(self,data):
step1 = self.proto.addr_fmt_to_ver_bytes('monero') + data.pubkey
return CoinAddr(
proto = self.proto,
addr = self.b58enc( step1 + self.keccak_256(step1).digest()[:4]) )
@check_data
def to_viewkey(self,data):
return MoneroViewKey( data.viewkey_bytes.hex() )
class zcash_z(base):
@check_data
def to_addr(self,data):
ret = _b58chk_encode(
self.proto.addr_fmt_to_ver_bytes('zcash_z')
+ data.pubkey )
return CoinAddr( self.proto, ret )
@check_data
def to_viewkey(self,data):
ret = _b58chk_encode(
self.proto.addr_fmt_to_ver_bytes('viewkey')
+ data.viewkey_bytes )
return ZcashViewKey( self.proto, ret )

View file

@ -227,7 +227,7 @@ class AddrList(MMGenObject): # Address info for a single seed ID
if self.gen_addrs:
from .addr import KeyGenerator,AddrGenerator
kg = KeyGenerator( self.proto, mmtype )
kg = KeyGenerator( self.proto, mmtype.pubkey_type )
ag = AddrGenerator( self.proto, mmtype )
t_addrs,out = ( len(addr_idxs), AddrListData() )
@ -258,12 +258,12 @@ class AddrList(MMGenObject): # Address info for a single seed ID
pubkey_type = mmtype.pubkey_type )
if self.gen_addrs:
pubhex = kg.to_pubhex(e.sec)
e.addr = ag.to_addr(pubhex)
data = kg.gen_data(e.sec)
e.addr = ag.to_addr(data)
if gen_viewkey:
e.viewkey = ag.to_viewkey(pubhex)
e.viewkey = ag.to_viewkey(data)
if gen_wallet_passwd:
e.wallet_passwd = ag.to_wallet_passwd(e.sec)
e.wallet_passwd = self.gen_wallet_passwd(e.sec)
elif self.gen_passwds:
e.passwd = self.gen_passwd(e.sec) # TODO - own type
@ -356,9 +356,9 @@ class AddrList(MMGenObject): # Address info for a single seed ID
def gen_addr(pk,t):
at = self.proto.addr_type(t)
from .addr import KeyGenerator,AddrGenerator
kg = KeyGenerator(self.proto,at)
kg = KeyGenerator(self.proto,at.pubkey_type)
ag = AddrGenerator(self.proto,at)
return ag.to_addr(kg.to_pubhex(pk))
return ag.to_addr(kg.gen_data(pk))
compressed_types = set(self.proto.mmtypes) - {'L','E'}
uncompressed_types = set(self.proto.mmtypes) & {'L','E'}

View file

@ -1 +1 @@
13.1.dev002
13.1.dev003

View file

@ -47,6 +47,7 @@ class BaseConversionPadError(Exception): mmcode = 2
class TransactionChainMismatch(Exception):mmcode = 2
class ObjectInitError(Exception): mmcode = 2
class ClassFlagsError(Exception): mmcode = 2
class ExtensionModuleError(Exception): mmcode = 2
# 3: yellow hl, 'MMGen Error' + exception + message
class RPCFailure(Exception): mmcode = 3

View file

@ -173,7 +173,7 @@ class GlobalContext(Lockable):
required_opts = (
'quiet','verbose','debug','outdir','echo_passphrase','passwd_file','stdout',
'show_hash_presets','label','keep_passphrase','keep_hash_preset','yes',
'brain_params','b16','usr_randchars','coin','bob','alice','key_generator',
'brain_params','b16','usr_randchars','coin','bob','alice',
'hidden_incog_input_params','in_fmt','hash_preset','seed_len',
)
incompatible_opts = (
@ -271,9 +271,6 @@ class GlobalContext(Lockable):
aesctr_dfl_iv = int.to_bytes(1,aesctr_iv_len,'big')
hincog_chk_len = 8
key_generators = ('python-ecdsa','libsecp256k1') # '1','2'
key_generator = 2 # libsecp256k1 is default
force_standalone_scrypt_module = False
# Scrypt params: 'id_num': [N, r, p] (N is an exponent of two)
# NB: hashlib.scrypt in Python (>=v3.6) supports max N value of 14. This means that

View file

@ -39,6 +39,17 @@ def help_notes_func(proto,po,k):
class help_notes:
def coin_id():
return proto.coin_id
def keygen_backends():
from .keygen import get_backends
from .addr import MMGenAddrType
backends = get_backends(
MMGenAddrType(proto,po.user_opts.get('type') or proto.dfl_mmtype).pubkey_type
)
return ' '.join( f'{n}:{k}{" [default]" if n==1 else ""}' for n,k in enumerate(backends,1) )
def coind_exec():
return coind_exec()

View file

@ -22,7 +22,7 @@ key.py: MMGen public and private key objects
from string import ascii_letters,digits
from .objmethods import Hilite,InitErrors,MMGenObject
from .obj import ImmutableAttr,get_obj,HexStr
from .obj import ImmutableAttr,get_obj
class WifKey(str,Hilite,InitErrors):
"""
@ -44,26 +44,26 @@ class WifKey(str,Hilite,InitErrors):
def is_wif(proto,s):
return get_obj( WifKey, proto=proto, wif=s, silent=True, return_bool=True )
class PubKey(HexStr,MMGenObject): # TODO: add some real checks
class PubKey(bytes,InitErrors,MMGenObject): # TODO: add some real checks
def __new__(cls,s,privkey):
def __new__(cls,s,compressed):
try:
me = HexStr.__new__(cls,s,case='lower')
me.privkey = privkey
me.compressed = privkey.compressed
assert isinstance(s,bytes)
me = bytes.__new__(cls,s)
me.compressed = compressed
return me
except Exception as e:
return cls.init_fail(e,s)
class PrivKey(str,Hilite,InitErrors,MMGenObject):
class PrivKey(bytes,Hilite,InitErrors,MMGenObject):
"""
Input: a) raw, non-preprocessed bytes; or b) WIF key.
Output: preprocessed hexadecimal key, plus WIF key in 'wif' attribute
Output: preprocessed key bytes, plus WIF key in 'wif' attribute
For coins without a WIF format, 'wif' contains the preprocessed hex.
The numeric validity of the resulting key is always checked.
"""
color = 'red'
width = 64
width = 32
trunc_ok = False
compressed = ImmutableAttr(bool,typeconv=False)
@ -78,11 +78,11 @@ class PrivKey(str,Hilite,InitErrors,MMGenObject):
assert s == None,"'wif' and key hex args are mutually exclusive"
assert set(wif) <= set(ascii_letters+digits),'not an ascii alphanumeric string'
k = proto.parse_wif(wif) # raises exception on error
me = str.__new__(cls,k.sec.hex())
me = bytes.__new__(cls,k.sec)
me.compressed = k.compressed
me.pubkey_type = k.pubkey_type
me.wif = str.__new__(WifKey,wif) # check has been done
me.orig_hex = None
me.orig_bytes = None
if k.sec != proto.preprocess_key(k.sec,k.pubkey_type):
from .exception import PrivateKeyError
raise PrivateKeyError(
@ -94,19 +94,20 @@ class PrivKey(str,Hilite,InitErrors,MMGenObject):
else:
try:
assert s,'private key bytes data missing'
assert isinstance(s,bytes),'input is not bytes'
assert pubkey_type is not None,"'pubkey_type' arg missing"
assert len(s) == cls.width // 2, f'key length must be {cls.width // 2} bytes'
assert len(s) == cls.width, f'key length must be {cls.width} bytes'
if pubkey_type == 'password': # skip WIF creation and pre-processing for passwds
me = str.__new__(cls,s.hex())
me = bytes.__new__(cls,s)
else:
assert compressed is not None, "'compressed' arg missing"
assert type(compressed) == bool,(
f"'compressed' must be of type bool, not {type(compressed).__name__}" )
me = str.__new__(cls,proto.preprocess_key(s,pubkey_type).hex())
me.wif = WifKey(proto,proto.hex2wif(me,pubkey_type,compressed))
me = bytes.__new__( cls, proto.preprocess_key(s,pubkey_type) )
me.wif = WifKey( proto, proto.bytes2wif(me,pubkey_type,compressed) )
me.compressed = compressed
me.pubkey_type = pubkey_type
me.orig_hex = s.hex() # save the non-preprocessed key
me.orig_bytes = s # save the non-preprocessed key
me.proto = proto
return me
except Exception as e:

239
mmgen/keygen.py Executable file
View file

@ -0,0 +1,239 @@
#!/usr/bin/env python3
#
# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
keygen.py: Public key generation classes for the MMGen suite
"""
from collections import namedtuple
from .key import PubKey,PrivKey
keygen_public_data = namedtuple(
'keygen_public_data', [
'pubkey',
'viewkey_bytes',
'pubkey_type',
'compressed' ])
class keygen_base:
def gen_data(self,privkey):
assert isinstance(privkey,PrivKey)
return keygen_public_data(
self.to_pubkey(privkey),
self.to_viewkey(privkey),
privkey.pubkey_type,
privkey.compressed )
def to_viewkey(self,privkey):
return None
class keygen_backend:
class std:
backends = ('libsecp256k1','python-ecdsa')
class libsecp256k1(keygen_base):
def __init__(self):
from .secp256k1 import priv2pub
self.priv2pub = priv2pub
def to_pubkey(self,privkey):
return PubKey(
s = self.priv2pub( privkey, int(privkey.compressed) ),
compressed = privkey.compressed )
@classmethod
def test_avail(cls,silent=False):
try:
from .secp256k1 import priv2pub
if not priv2pub(bytes.fromhex('deadbeef'*8),1):
from .exception import ExtensionModuleError
raise ExtensionModuleError('Unable to execute priv2pub() from secp256k1 extension module')
return True
except Exception as e:
if not silent:
from .util import ymsg
ymsg(str(e))
return False
class python_ecdsa(keygen_base):
def __init__(self):
import ecdsa
self.ecdsa = ecdsa
def to_pubkey(self,privkey):
"""
devdoc/guide_wallets.md:
Uncompressed public keys start with 0x04; compressed public keys begin with 0x03 or
0x02 depending on whether they're greater or less than the midpoint of the curve.
"""
def privnum2pubkey(numpriv,compressed=False):
pko = self.ecdsa.SigningKey.from_secret_exponent(numpriv,curve=self.ecdsa.SECP256k1)
# pubkey = x (32 bytes) + y (32 bytes) (unsigned big-endian)
pubkey = pko.get_verifying_key().to_string()
if compressed: # discard Y coord, replace with appropriate version byte
# even y: <0, odd y: >0 -- https://bitcointalk.org/index.php?topic=129652.0
return (b'\x02',b'\x03')[pubkey[-1] & 1] + pubkey[:32]
else:
return b'\x04' + pubkey
return PubKey(
s = privnum2pubkey( int.from_bytes(privkey,'big'), compressed=privkey.compressed ),
compressed = privkey.compressed )
class monero:
backends = ('nacl','ed25519ll_djbec','ed25519')
class base(keygen_base):
def __init__(self):
from .protocol import CoinProtocol
self.proto_cls = CoinProtocol.Monero
from .util import get_keccak
self.keccak_256 = get_keccak()
def to_viewkey(self,privkey):
return self.proto_cls.preprocess_key(
self.proto_cls,
self.keccak_256(privkey).digest(),
None )
class nacl(base):
def __init__(self):
super().__init__()
from nacl.bindings import crypto_scalarmult_ed25519_base_noclamp
self.scalarmultbase = crypto_scalarmult_ed25519_base_noclamp
def to_pubkey(self,privkey):
return PubKey(
self.scalarmultbase( privkey ) +
self.scalarmultbase( self.to_viewkey(privkey) ),
compressed = privkey.compressed
)
class ed25519(base):
def __init__(self):
super().__init__()
from .ed25519 import edwards,encodepoint,B,scalarmult
self.edwards = edwards
self.encodepoint = encodepoint
self.B = B
self.scalarmult = scalarmult
def scalarmultbase(self,privnum):
"""
Source and license for scalarmultbase function:
https://github.com/bigreddmachine/MoneroPy/blob/master/moneropy/crypto/ed25519.py
Copyright (c) 2014-2016, The Monero Project
All rights reserved.
"""
if privnum == 0:
return [0, 1]
Q = self.scalarmult(self.B, privnum//2)
Q = self.edwards(Q, Q)
if privnum & 1:
Q = self.edwards(Q, self.B)
return Q
@staticmethod
def rev_bytes2int(in_bytes):
return int.from_bytes( in_bytes[::-1], 'big' )
def to_pubkey(self,privkey):
return PubKey(
self.encodepoint( self.scalarmultbase( self.rev_bytes2int(privkey) )) +
self.encodepoint( self.scalarmultbase( self.rev_bytes2int(self.to_viewkey(privkey)) )),
compressed = privkey.compressed
)
class ed25519ll_djbec(ed25519):
def __init__(self):
super().__init__()
from .ed25519ll_djbec import scalarmult
self.scalarmult = scalarmult
class zcash_z:
backends = ('nacl',)
class nacl(keygen_base):
def __init__(self):
from nacl.bindings import crypto_scalarmult_base
self.crypto_scalarmult_base = crypto_scalarmult_base
from .sha2 import Sha256
self.Sha256 = Sha256
def zhash256(self,s,t):
s = bytearray(s + bytes(32))
s[0] |= 0xc0
s[32] = t
return self.Sha256(s,preprocess=False).digest()
def to_pubkey(self,privkey):
return PubKey(
self.zhash256(privkey,0)
+ self.crypto_scalarmult_base(self.zhash256(privkey,1)),
compressed = privkey.compressed
)
def to_viewkey(self,privkey):
vk = bytearray( self.zhash256(privkey,0) + self.zhash256(privkey,1) )
vk[32] &= 0xf8
vk[63] &= 0x7f
vk[63] |= 0x40
return vk
def get_backends(pubkey_type):
return getattr(keygen_backend,pubkey_type).backends
def _check_backend(backend,pubkey_type,desc='keygen backend'):
from .util import is_int,qmsg,die
assert is_int(backend), f'illegal value for {desc} (must be an integer)'
backends = get_backends(pubkey_type)
if not (1 <= int(backend) <= len(backends)):
die(1,
f'{backend}: {desc} out of range\n' +
f'Configured backends: ' +
' '.join( f'{n}:{k}' for n,k in enumerate(backends,1) )
)
qmsg(f'Using backend {backends[int(backend)-1]!r} for public key generation')
return True
def check_backend(proto,backend,addr_type):
from .addr import MMGenAddrType
pubkey_type = MMGenAddrType(proto,addr_type or proto.dfl_mmtype).pubkey_type
return _check_backend(
backend,
pubkey_type,
desc = '--keygen-backend parameter' )

View file

@ -56,15 +56,13 @@ opts_data = {
-c, --print-checksum Print address list checksum and exit
-d, --outdir= d Output files to directory 'd' instead of working dir
-e, --echo-passphrase Echo passphrase or mnemonic to screen upon entry
-E, --use-old-ed25519 Use original (and slow) ed25519 module for Monero
address generation instead of ed25519ll_djbec
-i, --in-fmt= f Input is from wallet format 'f' (see FMT CODES below)
-H, --hidden-incog-input-params=f,o Read hidden incognito data from file
'f' at offset 'o' (comma-separated)
-O, --old-incog-fmt Specify old-format incognito input
-k, --use-internal-keccak-module Force use of the internal keccak module
-K, --key-generator=m Use method 'm' for public key generation
Options: {kgs} (default: {kg})
-K, --keygen-backend=n Use backend 'n' for public key generation. Options
for {coin_id}: {kgs}
-l, --seed-len= l Specify wallet seed length of 'l' bits. This option
is required only for brainwallet and incognito inputs
with non-standard (< {g.dfl_seed_len}-bit) seed lengths
@ -106,11 +104,11 @@ FMT CODES:
"""
},
'code': {
'options': lambda proto,s: s.format(
'options': lambda proto,help_notes,s: s.format(
seed_lens=', '.join(map(str,g.seed_lens)),
dmat="'{}' or '{}'".format(proto.dfl_mmtype,MMGenAddrType.mmtypes[proto.dfl_mmtype].name),
kgs=' '.join(['{}:{}'.format(n,k) for n,k in enumerate(g.key_generators,1)]),
kg=g.key_generator,
kgs=help_notes('keygen_backends'),
coin_id=help_notes('coin_id'),
pnm=g.proj_name,
what=gen_what,
g=g,
@ -142,8 +140,9 @@ addr_type = MMGenAddrType(
if len(cmd_args) < 1:
opts.usage()
if getattr(opt,'use_old_ed25519',False):
msg('Using old (slow) ed25519 module by user request')
if opt.keygen_backend:
from .keygen import check_backend
check_backend( proto, opt.keygen_backend, opt.type )
idxs = AddrIdxList(fmt_str=cmd_args.pop())

View file

@ -63,6 +63,8 @@ opts_data = {
-h, --help Print this help message
--, --longhelp Print help message for long options (common options)
-k, --use-internal-keccak-module Force use of the internal keccak module
-K, --keygen-backend=n Use backend 'n' for public key generation. Options
for {coin_id}: {kgs}
-p, --hash-preset= p Use the scrypt hash parameters defined by preset 'p'
for password hashing (default: '{g.dfl_hash_preset}')
-P, --passwd-file= f Get passphrase from file 'f'.
@ -84,7 +86,11 @@ Type '{pn} help <command>' for help on a particular command
"""
},
'code': {
'options': lambda s: s.format(g=g),
'options': lambda s, help_notes: s.format(
kgs=help_notes('keygen_backends'),
coin_id=help_notes('coin_id'),
g=g,
),
'notes': lambda s: s.format(
ch=make_cmd_help(),
pn=g.prog_name)

View file

@ -51,9 +51,8 @@ opts_data = {
is required only for brainwallet and incognito inputs
with non-standard (< {g.dfl_seed_len}-bit) seed lengths.
-k, --keys-from-file=f Provide additional keys for non-{pnm} addresses
-K, --key-generator= m Use method 'm' for public key generation
Options: {kgs}
(default: {kg})
-K, --keygen-backend=n Use backend 'n' for public key generation. Options
for {coin_id}: {kgs}
-M, --mmgen-keys-from-file=f Provide keys for {pnm} addresses in a key-
address file (output of '{pnl}-keygen'). Permits
online signing without an {pnm} seed source. The
@ -87,8 +86,8 @@ column below:
pnl=g.proj_name.lower(),
fu=help_notes('rel_fee_desc'),
fl=help_notes('fee_spec_letters'),
kgs=' '.join([f'{n}:{k}' for n,k in enumerate(g.key_generators,1)]),
kg=g.key_generator,
kgs=help_notes('keygen_backends'),
coin_id=help_notes('coin_id'),
cu=proto.coin),
'notes': lambda help_notes,s: s.format(
help_notes('fee'),

View file

@ -58,9 +58,8 @@ opts_data = {
is required only for brainwallet and incognito inputs
with non-standard (< {g.dfl_seed_len}-bit) seed lengths.
-k, --keys-from-file=f Provide additional keys for non-{pnm} addresses
-K, --key-generator= m Use method 'm' for public key generation
Options: {kgs}
(default: {kg})
-K, --keygen-backend=n Use backend 'n' for public key generation. Options
for {coin_id}: {kgs}
-L, --locktime= t Lock time (block height or unix seconds) (default: 0)
-m, --minconf=n Minimum number of confirmations required to spend
outputs (default: 1)
@ -95,14 +94,14 @@ column below:
'code': {
'options': lambda proto,help_notes,s: s.format(
g=g,pnm=g.proj_name,pnl=g.proj_name.lower(),
kgs=' '.join([f'{n}:{k}' for n,k in enumerate(g.key_generators,1)]),
kgs=help_notes('keygen_backends'),
coin_id=help_notes('coin_id'),
fu=help_notes('rel_fee_desc'),
fl=help_notes('fee_spec_letters'),
ss=g.subseeds,
ss_max=SubSeedIdxRange.max_idx,
fe_all=fmt_list(g.autoset_opts['fee_estimate_mode'].choices,fmt='no_spc'),
fe_dfl=g.autoset_opts['fee_estimate_mode'].choices[0],
kg=g.key_generator,
cu=proto.coin),
'notes': lambda help_notes,s: s.format(
help_notes('txcreate'),

View file

@ -50,8 +50,8 @@ opts_data = {
for password hashing (default: '{g.dfl_hash_preset}')
-z, --show-hash-presets Show information on available hash presets
-k, --keys-from-file=f Provide additional keys for non-{pnm} addresses
-K, --key-generator=m Use method 'm' for public key generation
Options: {kgs} (default: {kg})
-K, --keygen-backend=n Use backend 'n' for public key generation. Options
for {coin_id}: {kgs}
-M, --mmgen-keys-from-file=f Provide keys for {pnm} addresses in a key-
address file (output of '{pnl}-keygen'). Permits
online signing without an {pnm} seed source. The
@ -77,12 +77,12 @@ column below:
"""
},
'code': {
'options': lambda proto,s: s.format(
'options': lambda proto,help_notes,s: s.format(
g=g,
pnm=g.proj_name,
pnl=g.proj_name.lower(),
kgs=' '.join([f'{n}:{k}' for n,k in enumerate(g.key_generators,1)]),
kg=g.key_generator,
kgs=help_notes('keygen_backends'),
coin_id=help_notes('coin_id'),
ss=g.subseeds,
ss_max=SubSeedIdxRange.max_idx,
cu=proto.coin),

View file

@ -544,10 +544,6 @@ def check_usr_opts(usr_opts): # Raises an exception if any check fails
opt_is_float(val,desc)
ymsg(f'Adjusting transaction vsize by a factor of {float(val):1.2f}')
def chk_key_generator(key,val,desc):
opt_compares(val,'<=',len(g.key_generators),desc)
opt_compares(val,'>',0,desc)
def chk_coin(key,val,desc):
from .protocol import CoinProtocol
opt_is_in_list(val.lower(),CoinProtocol.coins,'coin')

View file

@ -185,28 +185,27 @@ class PasswordList(AddrList):
default_yes = True ):
die(1,'Exiting at user request')
def gen_passwd(self,hex_sec):
def gen_passwd(self,secbytes):
assert self.pw_fmt in self.pw_info
if self.pw_fmt == 'hex':
# take most significant part
return hex_sec[:self.pw_len]
return secbytes.hex()[:self.pw_len]
elif self.pw_fmt == 'bip39':
from .bip39 import bip39
pw_len_hex = bip39.nwords2seedlen(self.pw_len,in_hex=True)
pw_len_bytes = bip39.nwords2seedlen( self.pw_len, in_bytes=True )
# take most significant part
return ' '.join(bip39.fromhex(hex_sec[:pw_len_hex],wl_id='bip39'))
return ' '.join( bip39.fromhex( secbytes[:pw_len_bytes].hex(), wl_id='bip39' ) )
elif self.pw_fmt == 'xmrseed':
pw_len_hex = baseconv.seedlen_map_rev['xmrseed'][self.pw_len] * 2
# take most significant part
pw_len_bytes = baseconv.seedlen_map_rev['xmrseed'][self.pw_len]
from .protocol import init_proto
bytes_preproc = init_proto('xmr').preprocess_key(
bytes.fromhex(hex_sec[:pw_len_hex]),
secbytes[:pw_len_bytes], # take most significant part
None )
return ' '.join( baseconv.frombytes( bytes_preproc, wl_id='xmrseed' ) )
else:
# take least significant part
return baseconv.fromhex(
hex_sec,
return baseconv.frombytes(
secbytes,
self.pw_fmt,
pad = self.pw_len,
tostr = True )[-self.pw_len:]

View file

@ -33,14 +33,11 @@ import mmgen.bech32 as bech32
parsed_wif = namedtuple('parsed_wif',['sec','pubkey_type','compressed'])
parsed_addr = namedtuple('parsed_addr',['bytes','fmt'])
def hash160(hexnum): # take hex, return hex - OP_HASH160
return hashlib.new('ripemd160',hashlib.sha256(bytes.fromhex(hexnum)).digest()).hexdigest()
def hash160(in_bytes): # OP_HASH160
return hashlib.new('ripemd160',hashlib.sha256(in_bytes).digest()).digest()
def hash256(hexnum): # take hex, return hex - OP_HASH256
return hashlib.sha256(hashlib.sha256(bytes.fromhex(hexnum)).digest()).hexdigest()
def hash256bytes(bstr): # bytes in, bytes out - OP_HASH256
return hashlib.sha256(hashlib.sha256(bstr).digest()).digest()
def hash256(in_bytes): # OP_HASH256
return hashlib.sha256(hashlib.sha256(in_bytes).digest()).digest()
_b58a='123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
@ -57,14 +54,14 @@ def _b58chk_encode(in_bytes):
while n:
yield _b58a[n % 58]
n //= 58
return ('1' * lzeroes) + ''.join(do_enc(int.from_bytes(in_bytes+hash256bytes(in_bytes)[:4],'big')))[::-1]
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:] != hash256bytes(out[:-4])[:4]:
if out[-4:] != hash256(out[:-4])[:4]:
raise ValueError('_b58chk_decode(): incorrect checksum')
return out[:-4]
@ -190,6 +187,7 @@ class CoinProtocol(MMGenObject):
"""
secp256k1_ge = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141
privkey_len = 32
pubkey_types = ('std',)
def preprocess_key(self,sec,pubkey_type):
# Key must be non-zero and less than group order of secp256k1 curve
@ -240,13 +238,12 @@ class CoinProtocol(MMGenObject):
start_subsidy = 50
ignore_daemon_version = False
def hex2wif(self,hexpriv,pubkey_type,compressed): # input is preprocessed hex
sec = bytes.fromhex(hexpriv)
assert len(sec) == self.privkey_len, f'{len(sec)} bytes: incorrect private key length!'
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])
+ sec
+ privbytes
+ (b'',b'\x01')[bool(compressed)])
def parse_wif(self,wif):
@ -288,24 +285,24 @@ class CoinProtocol(MMGenObject):
return self.parse_addr_bytes(_b58chk_decode(addr))
def pubhash2addr(self,pubkey_hash,p2sh):
assert len(pubkey_hash) == 40, f'{len(pubkey_hash)}: invalid length for pubkey hash'
return _b58chk_encode(bytes.fromhex(
self.addr_fmt_to_ver_bytes(('p2pkh','p2sh')[p2sh],return_hex=True) + pubkey_hash
))
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 pubhex2redeem_script(self,pubhex):
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 self.witness_vernum_hex + '14' + hash160(pubhex)
return bytes.fromhex(self.witness_vernum_hex + '14') + hash160(pubkey)
def pubhex2segwitaddr(self,pubhex):
def pubkey2segwitaddr(self,pubkey):
return self.pubhash2addr(
hash160( self.pubhex2redeem_script(pubhex)), p2sh=True )
hash160( self.pubkey2redeem_script(pubkey)), p2sh=True )
def pubhash2bech32addr(self,pubhash):
d = list(bytes.fromhex(pubhash))
d = list(pubhash)
return bech32.bech32_encode(self.bech32_hrp,[self.witness_vernum]+bech32.convertbits(d,8,5))
class BitcoinTestnet(Bitcoin):
@ -329,8 +326,8 @@ class CoinProtocol(MMGenObject):
max_tx_fee = BCHAmt('0.1')
ignore_daemon_version = False
def pubhex2redeem_script(self,pubhex): raise NotImplementedError
def pubhex2segwitaddr(self,pubhex): raise NotImplementedError
def pubkey2redeem_script(self,pubkey): raise NotImplementedError
def pubkey2segwitaddr(self,pubkey): raise NotImplementedError
class BitcoinCashTestnet(BitcoinCash):
addr_ver_bytes = { '6f': 'p2pkh', 'c4': 'p2sh' }
@ -365,10 +362,10 @@ class CoinProtocol(MMGenObject):
class DummyWIF:
def hex2wif(self,hexpriv,pubkey_type,compressed):
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!'
return hexpriv
return privbytes.hex()
def parse_wif(self,wif):
return parsed_wif(
@ -427,9 +424,9 @@ class CoinProtocol(MMGenObject):
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) == 40, f'{len(pubkey_hash)}: invalid length for {self.name} pubkey hash'
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
return pubkey_hash.hex()
class EthereumTestnet(Ethereum):
chain_names = ['kovan','goerli','rinkeby']
@ -452,6 +449,7 @@ class CoinProtocol(MMGenObject):
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'
@ -473,9 +471,9 @@ class CoinProtocol(MMGenObject):
def pubhash2addr(self,pubkey_hash,p2sh):
hash_len = len(pubkey_hash)
if hash_len == 40:
if hash_len == 20:
return super().pubhash2addr(pubkey_hash,p2sh)
elif hash_len == 128:
elif hash_len == 64:
raise NotImplementedError('Zcash z-addresses do not support pubhash2addr()')
else:
raise ValueError(f'{hash_len}: incorrect pubkey_hash length')
@ -492,6 +490,7 @@ class CoinProtocol(MMGenObject):
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

View file

@ -303,7 +303,7 @@ class MMGenToolCmds(metaclass=MMGenToolCmdMeta):
else:
return gd(
at,
KeyGenerator(self.proto,at),
KeyGenerator(self.proto,at.pubkey_type),
AddrGenerator(self.proto,at),
)
@ -364,7 +364,7 @@ class MMGenToolCmdUtil(MMGenToolCmds):
def hash160(self,hexstr:'sstr'):
"compute ripemd160(sha256(data)) (convert hex pubkey to hex addr)"
return hash160(hexstr)
return hash160(bytes.fromhex(hexstr)).hex()
def hash256(self,string_or_bytes:str,file_input=False,hex_input=False): # TODO: handle stdin
"compute sha256(sha256(data)) (double sha256)"
@ -458,19 +458,19 @@ class MMGenToolCmdCoin(MMGenToolCmds):
def randpair(self):
"generate a random private key/address pair"
gd = self.init_generators()
privhex = PrivKey(
privkey = PrivKey(
self.proto,
get_random(32),
pubkey_type = gd.at.pubkey_type,
compressed = gd.at.compressed )
addr = gd.ag.to_addr(gd.kg.to_pubhex(privhex))
return (privhex.wif,addr)
addr = gd.ag.to_addr(gd.kg.gen_data(privkey))
return ( privkey.wif, addr )
def wif2hex(self,wifkey:'sstr'):
"convert a private key from WIF to hex format"
return PrivKey(
self.proto,
wif = wifkey )
wif = wifkey ).hex()
def hex2wif(self,privhex:'sstr'):
"convert a private key from hex to WIF format"
@ -484,31 +484,31 @@ class MMGenToolCmdCoin(MMGenToolCmds):
def wif2addr(self,wifkey:'sstr'):
"generate a coin address from a key in WIF format"
gd = self.init_generators()
privhex = PrivKey(
privkey = PrivKey(
self.proto,
wif = wifkey )
addr = gd.ag.to_addr(gd.kg.to_pubhex(privhex))
addr = gd.ag.to_addr(gd.kg.gen_data(privkey))
return addr
def wif2redeem_script(self,wifkey:'sstr'): # new
"convert a WIF private key to a Segwit P2SH-P2WPKH redeem script"
assert self.mmtype.name == 'segwit','This command is meaningful only for --type=segwit'
gd = self.init_generators()
privhex = PrivKey(
privkey = PrivKey(
self.proto,
wif = wifkey )
return gd.ag.to_segwit_redeem_script(gd.kg.to_pubhex(privhex))
return gd.ag.to_segwit_redeem_script(gd.kg.gen_data(privkey))
def wif2segwit_pair(self,wifkey:'sstr'):
"generate both a Segwit P2SH-P2WPKH redeem script and address from WIF"
assert self.mmtype.name == 'segwit','This command is meaningful only for --type=segwit'
gd = self.init_generators()
pubhex = gd.kg.to_pubhex(PrivKey(
data = gd.kg.gen_data(PrivKey(
self.proto,
wif = wifkey ))
return (
gd.ag.to_segwit_redeem_script(pubhex),
gd.ag.to_addr(pubhex) )
gd.ag.to_segwit_redeem_script(data),
gd.ag.to_addr(data) )
def privhex2addr(self,privhex:'sstr',output_pubhex=False):
"generate coin address from raw private key data in hexadecimal format"
@ -518,8 +518,8 @@ class MMGenToolCmdCoin(MMGenToolCmds):
bytes.fromhex(privhex),
compressed = gd.at.compressed,
pubkey_type = gd.at.pubkey_type )
ph = gd.kg.to_pubhex(pk)
return ph if output_pubhex else gd.ag.to_addr(ph)
data = gd.kg.gen_data(pk)
return data.pubkey.hex() if output_pubhex else gd.ag.to_addr(data)
def privhex2pubhex(self,privhex:'sstr'): # new
"generate a hex public key from a hex private key"
@ -527,30 +527,32 @@ class MMGenToolCmdCoin(MMGenToolCmds):
def pubhex2addr(self,pubkeyhex:'sstr'):
"convert a hex pubkey to an address"
pubkey = bytes.fromhex(pubkeyhex)
if self.mmtype.name == 'segwit':
return self.proto.pubhex2segwitaddr(pubkeyhex)
return self.proto.pubkey2segwitaddr( pubkey )
else:
return self.pubhash2addr(hash160(pubkeyhex))
return self.pubhash2addr( hash160(pubkey).hex() )
def pubhex2redeem_script(self,pubkeyhex:'sstr'): # new
"convert a hex pubkey to a Segwit P2SH-P2WPKH redeem script"
assert self.mmtype.name == 'segwit','This command is meaningful only for --type=segwit'
return self.proto.pubhex2redeem_script(pubkeyhex)
return self.proto.pubkey2redeem_script( bytes.fromhex(pubkeyhex) ).hex()
def redeem_script2addr(self,redeem_scripthex:'sstr'): # new
"convert a Segwit P2SH-P2WPKH redeem script to an address"
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'
return self.pubhash2addr(hash160(redeem_scripthex))
return self.pubhash2addr( hash160(bytes.fromhex(redeem_scripthex)).hex() )
def pubhash2addr(self,pubhashhex:'sstr'):
"convert public key hash to address"
pubhash = bytes.fromhex(pubhashhex)
if self.mmtype.name == 'bech32':
return self.proto.pubhash2bech32addr(pubhashhex)
return self.proto.pubhash2bech32addr( pubhash )
else:
gd = self.init_generators('addrtype_only')
return self.proto.pubhash2addr(pubhashhex,gd.at.addr_fmt=='p2sh')
return self.proto.pubhash2addr( pubhash, gd.at.addr_fmt=='p2sh' )
def addr2pubhash(self,addr:'sstr'):
"convert coin address to public key hash"

View file

@ -96,11 +96,11 @@ def addr2scriptPubKey(proto,addr):
def scriptPubKey2addr(proto,s):
if len(s) == 50 and s[:6] == '76a914' and s[-4:] == '88ac':
return proto.pubhash2addr(s[6:-4],p2sh=False),'p2pkh'
return proto.pubhash2addr(bytes.fromhex(s[6:-4]),p2sh=False),'p2pkh'
elif len(s) == 46 and s[:4] == 'a914' and s[-2:] == '87':
return proto.pubhash2addr(s[4:-2],p2sh=True),'p2sh'
return proto.pubhash2addr(bytes.fromhex(s[4:-2]),p2sh=True),'p2sh'
elif len(s) == 44 and s[:4] == proto.witness_vernum_hex + '14':
return proto.pubhash2bech32addr(s[4:]),'bech32'
return proto.pubhash2bech32addr(bytes.fromhex(s[4:])),'bech32'
else:
raise NotImplementedError(f'Unknown scriptPubKey ({s})')
@ -1260,7 +1260,7 @@ class MMGenTX:
e['amount'] = e['amt']
del e['amt']
if d.mmtype == 'S':
e['redeemScript'] = ag.to_segwit_redeem_script(kg.to_pubhex(keydict[d.addr]))
e['redeemScript'] = ag.to_segwit_redeem_script(kg.gen_data(keydict[d.addr]))
sig_data.append(e)
msg_r(f'Signing transaction{tx_num_str}...')

View file

@ -155,13 +155,13 @@ class GenTool(object):
def run_tool(self,sec):
vcoin = 'BTC' if self.proto.coin == 'BCH' else self.proto.coin
ret = self.run(sec,vcoin)
self.data[sec] = ret._asdict()
self.data[sec.hex()] = ret._asdict()
return ret
class GenToolEthkey(GenTool):
desc = 'ethkey'
def run(self,sec,vcoin):
o = get_cmd_output(['ethkey','info',sec])
o = get_cmd_output(['ethkey','info',sec.hex()])
return gtr(o[0].split()[1],o[-1].split()[1],None)
class GenToolKeyconv(GenTool):
@ -194,10 +194,10 @@ class GenToolPycoin(GenTool):
vcoin = ci.external_tests['testnet']['pycoin'][vcoin]
network = self.nfnc(vcoin)
key = network.keys.private(
secret_exponent = int(sec,16),
secret_exponent = int(sec.hex(),16),
is_compressed = self.addr_type.name != 'legacy' )
if key is None:
die(1,f'can’t parse {sec}')
die(1,f'can’t parse {sec.hex()}')
if self.addr_type.name in ('segwit','bech32'):
hash160_c = key.hash160(is_compressed=True)
if self.addr_type.name == 'segwit':
@ -221,10 +221,10 @@ class GenToolMoneropy(GenTool):
self.mpa = moneropy.account
def run(self,sec,vcoin):
if sec in self.data:
return gtr(**self.data[sec])
if sec.hex() in self.data:
return gtr(**self.data[sec.hex()])
else:
sk,vk,addr = self.mpa.account_from_spend_key(sec) # VERY slow!
sk,vk,addr = self.mpa.account_from_spend_key(sec.hex()) # VERY slow!
return gtr(sk,addr,vk)
def find_or_check_tool(proto,addr_type,toolname):
@ -274,14 +274,14 @@ def do_ab_test(proto,addr_type,kg_b,rounds,backend_num):
qmsg_r(f'\rRound {i+1}/{trounds} ')
last_t = time.time()
sec = PrivKey(proto,in_bytes,compressed=addr_type.compressed,pubkey_type=addr_type.pubkey_type)
data = kg_a.to_pubhex(sec)
data = kg_a.gen_data(sec)
ag = AddrGenerator(proto,addr_type)
a_addr = ag.to_addr(data)
tinfo = (in_bytes,sec,sec.wif,type(kg_a).__name__,type(kg_b).__name__)
a_vk = None
def do_msg():
vmsg( fs.format( b=in_bytes.hex(), r=sec, k=sec.wif, v=a_vk, a=a_addr ))
vmsg( fs.format( b=in_bytes.hex(), r=sec.hex(), k=sec.wif, v=a_vk, a=a_addr ))
if isinstance(kg_b,GenTool):
def run_tool():
@ -294,12 +294,12 @@ def do_ab_test(proto,addr_type,kg_b,rounds,backend_num):
a_vk = run_tool()
do_msg()
else:
test_equal( 'addresses', a_addr, ag.to_addr(kg_b.to_pubhex(sec)), *tinfo )
test_equal( 'addresses', a_addr, ag.to_addr(kg_b.gen_data(sec)), *tinfo )
do_msg()
qmsg_r(f'\rRound {n+1}/{trounds} ')
kg_a = KeyGenerator(proto,addr_type,backend_num)
kg_a = KeyGenerator(proto,addr_type.pubkey_type,backend_num)
if type(kg_a) == type(kg_b):
rdie(1,'Key generators are the same!')
@ -349,16 +349,13 @@ def do_ab_test(proto,addr_type,kg_b,rounds,backend_num):
def init_tool(proto,addr_type,toolname):
return globals()['GenTool'+capfirst(toolname.replace('-','_'))](proto,addr_type)
def get_backends(proto,foo):
return (1,) if isinstance(proto,CoinProtocol.Zcash) else (1,2)
def ab_test(proto,gen_num,rounds,toolname_or_gen2_num):
addr_type = MMGenAddrType( proto=proto, id_str=opt.type or proto.dfl_mmtype )
if is_int(toolname_or_gen2_num):
assert gen_num != 'all', "'all' must be used only with external tool"
tool = KeyGenerator( proto, addr_type, int(toolname_or_gen2_num) )
tool = KeyGenerator( proto, addr_type.pubkey_type, int(toolname_or_gen2_num) )
else:
toolname = find_or_check_tool( proto, addr_type, toolname_or_gen2_num )
if toolname == None:
@ -367,12 +364,12 @@ def ab_test(proto,gen_num,rounds,toolname_or_gen2_num):
tool = init_tool( proto, addr_type, toolname )
if gen_num == 'all': # check all backends against external tool
for n in range(len(get_backends(proto,addr_type.pubkey_type))):
for n in range(len(get_backends(addr_type.pubkey_type))):
do_ab_test( proto, addr_type, tool, rounds, n+1 )
else: # check specific backend against external tool or another backend
do_ab_test( proto, addr_type, tool, rounds, int(gen_num) )
do_ab_test( proto, addr_type, tool, rounds, gen_num )
def speed_test(proto,addr_type,kg,ag,rounds):
def speed_test(proto,kg,ag,rounds):
qmsg(green('Testing speed of address generator {!r} for coin {}'.format(
type(kg).__name__,
proto.coin )))
@ -387,8 +384,8 @@ def speed_test(proto,addr_type,kg,ag,rounds):
if time.time() - last_t >= 0.1:
qmsg_r(f'\rRound {i+1}/{rounds} ')
last_t = time.time()
sec = PrivKey( proto, seed+pack('I', i), compressed=addr_type.compressed, pubkey_type=addr_type.pubkey_type )
addr = ag.to_addr(kg.to_pubhex(sec))
sec = PrivKey( proto, seed+pack('I', i), compressed=ag.compressed, pubkey_type=ag.pubkey_type )
addr = ag.to_addr(kg.gen_data(sec))
vmsg(f'\nkey: {sec.wif}\naddr: {addr}\n')
qmsg(
f'\rRound {i+1}/{rounds} ' +
@ -415,9 +412,9 @@ def dump_test(proto,kg,ag,filename):
b_sec = PrivKey(proto,wif=b_wif)
except:
die(2,f'\nInvalid {proto.network} WIF address in dump file: {b_wif}')
a_addr = ag.to_addr(kg.to_pubhex(b_sec))
a_addr = ag.to_addr(kg.gen_data(b_sec))
vmsg(f'\nwif: {b_wif}\naddr: {b_addr}\n')
tinfo = (b_sec,b_sec,b_wif,type(kg).__name__,filename)
tinfo = (b_sec,b_sec.hex(),b_wif,type(kg).__name__,filename)
test_equal('addresses',a_addr,b_addr,*tinfo)
qmsg(green(('\n','')[bool(opt.verbose)] + 'OK'))
@ -481,10 +478,10 @@ def main():
for proto in protos:
ab_test( proto, pa.gen_num, pa.rounds, toolname_or_gen2_num=pa.arg )
else:
kg = KeyGenerator( proto, addr_type, pa.gen_num )
kg = KeyGenerator( proto, addr_type.pubkey_type, pa.gen_num )
ag = AddrGenerator( proto, addr_type )
if pa.test == 'speed':
speed_test( proto, addr_type, kg, ag, pa.rounds )
speed_test( proto, kg, ag, pa.rounds )
elif pa.test == 'dump':
dump_test( proto, kg, ag, filename=pa.arg )
@ -499,6 +496,7 @@ from mmgen.protocol import init_proto,init_proto_from_opts,CoinProtocol,init_gen
from mmgen.altcoin import CoinInfo as ci
from mmgen.key import PrivKey
from mmgen.addr import KeyGenerator,AddrGenerator,MMGenAddrType
from mmgen.keygen import get_backends
sys.argv = [sys.argv[0]] + ['--skip-cfg-file'] + sys.argv[1:]
cmd_args = opts.init(opts_data,add_opts=['exact_output'])

View file

@ -34,7 +34,9 @@ opts_data = {
"""
},
'code': {
'options': lambda s: s.format(
'options': lambda help_notes,s: s.format(
kgs=help_notes('keygen_backends'),
coin_id=help_notes('coin_id'),
g=g,
),
'notes': lambda s: s.format(nn='a note'),

View file

@ -227,8 +227,13 @@ tests = {
},
'PubKey': {
'arg1': 's',
'bad': ({'s':1,'privkey':False},{'s':'F00BAA12','privkey':False},),
'good': ({'s':'deadbeef','privkey':privkey},) # TODO: add real pubkeys
'bad': (
{'s':1, 'compressed':True },
{'s':'F00BAA12','compressed':False},
),
'good': ( # TODO: add real pubkeys
{'s':bytes.fromhex('deadbeef'),'compressed':True},
)
},
'PrivKey': {
'arg1': 'proto',
@ -246,11 +251,11 @@ tests = {
),
'good': (
{'proto':proto, 'wif':'5KXEpVzjWreTcQoG5hX357s1969MUKNLuSfcszF6yu84kpsNZKb',
'ret':'e0aef965b905a2fedf907151df8e0a6bac832aa697801c51f58bd2ecb4fd381c'},
'ret':bytes.fromhex('e0aef965b905a2fedf907151df8e0a6bac832aa697801c51f58bd2ecb4fd381c')},
{'proto':proto, 'wif':'KwWr9rDh8KK5TtDa3HLChEvQXNYcUXpwhRFUPc5uSNnMtqNKLFhk',
'ret':'08d0ed83b64b68d56fa064be48e2385060ed205be2b1e63cd56d218038c3a05f'},
{'proto':proto, 's':r32,'compressed':False,'pubkey_type':'std','ret':r32.hex()},
{'proto':proto, 's':r32,'compressed':True,'pubkey_type':'std','ret':r32.hex()}
'ret':bytes.fromhex('08d0ed83b64b68d56fa064be48e2385060ed205be2b1e63cd56d218038c3a05f')},
{'proto':proto, 's':r32,'compressed':False,'pubkey_type':'std','ret':r32},
{'proto':proto, 's':r32,'compressed':True,'pubkey_type':'std','ret':r32}
)
},
'AddrListID': { # a rather pointless test, but do it anyway

View file

@ -59,11 +59,11 @@ tests = {
),
'good': (
{'proto':proto, 'wif':'93HsQEpH75ibaUJYi3QwwiQxnkW4dUuYFPXZxcbcKds7XrqHkY6',
'ret':'e0aef965b905a2fedf907151df8e0a6bac832aa697801c51f58bd2ecb4fd381c'},
'ret':bytes.fromhex('e0aef965b905a2fedf907151df8e0a6bac832aa697801c51f58bd2ecb4fd381c')},
{'proto':proto, 'wif':'cMsqcmDYZP1LdKgqRh9L4ZRU9br28yvdmTPwW2YQwVSN9aQiMAoR',
'ret':'08d0ed83b64b68d56fa064be48e2385060ed205be2b1e63cd56d218038c3a05f'},
{'proto':proto, 's':r32,'compressed':False,'pubkey_type':'std','ret':r32.hex()},
{'proto':proto, 's':r32,'compressed':True,'pubkey_type':'std','ret':r32.hex()}
'ret':bytes.fromhex('08d0ed83b64b68d56fa064be48e2385060ed205be2b1e63cd56d218038c3a05f')},
{'proto':proto, 's':r32,'compressed':False,'pubkey_type':'std','ret':r32},
{'proto':proto, 's':r32,'compressed':True,'pubkey_type':'std','ret':r32}
),
},
}

View file

@ -63,11 +63,11 @@ tests = {
),
'good': (
{'proto':proto, 'wif':'6ufJhtQQiRYA3w2QvDuXNXuLgPFp15i3HR1Wp8An2mx1JnhhJAh',
'ret':'470a974ffca9fca1299b706b09142077bea3acbab6d6480b87dbba79d5fd279b'},
'ret':bytes.fromhex('470a974ffca9fca1299b706b09142077bea3acbab6d6480b87dbba79d5fd279b')},
{'proto':proto, 'wif':'T41Fm7J3mtZLKYPMCLVSFARz4QF8nvSDhLAfW97Ds56Zm9hRJgn8',
'ret':'1c6feab55a4c3b4ad1823d4ecacd1565c64228c01828cf44fb4db1e2d82c3d56'},
{'proto':proto, 's':r32,'compressed':False,'pubkey_type':'std','ret':r32.hex()},
{'proto':proto, 's':r32,'compressed':True,'pubkey_type':'std','ret':r32.hex()}
'ret':bytes.fromhex('1c6feab55a4c3b4ad1823d4ecacd1565c64228c01828cf44fb4db1e2d82c3d56')},
{'proto':proto, 's':r32,'compressed':False,'pubkey_type':'std','ret':r32},
{'proto':proto, 's':r32,'compressed':True,'pubkey_type':'std','ret':r32}
)
},
}

View file

@ -59,11 +59,11 @@ tests = {
),
'good': (
{'proto':proto, 'wif':'92iqzh6NqiKawyB1ronw66YtEHrU4rxRJ5T4aHniZqvuSVZS21f',
'ret':'95b2aa7912550eacdd3844dcc14bee08ce7bc2434ad4858beb136021e945afeb'},
'ret':bytes.fromhex('95b2aa7912550eacdd3844dcc14bee08ce7bc2434ad4858beb136021e945afeb')},
{'proto':proto, 'wif':'cSaJAXBAm9ooHpVJgoxqjDG3AcareFy29Cz8mhnNTRijjv2HLgta',
'ret':'94fa8b90c11fea8fb907c9376b919534b0a75b9a9621edf71a78753544b4101c'},
{'proto':proto, 's':r32,'compressed':False,'pubkey_type':'std','ret':r32.hex()},
{'proto':proto, 's':r32,'compressed':True,'pubkey_type':'std','ret':r32.hex()}
'ret':bytes.fromhex('94fa8b90c11fea8fb907c9376b919534b0a75b9a9621edf71a78753544b4101c')},
{'proto':proto, 's':r32,'compressed':False,'pubkey_type':'std','ret':r32},
{'proto':proto, 's':r32,'compressed':True,'pubkey_type':'std','ret':r32}
)
},
}

View file

@ -303,34 +303,35 @@ i_alts='Gen-only altcoin'
s_alts='The following tests will test generation operations for all supported altcoins'
t_alts="
- # speed tests, no verification:
- $gentest_py --coin=etc 2 $rounds
- $gentest_py --coin=etc --use-internal-keccak-module 2 $rounds_min
- $gentest_py --coin=eth 2 $rounds
- $gentest_py --coin=eth --use-internal-keccak-module 2 $rounds_min
- $gentest_py --coin=xmr 2 $rounds
- $gentest_py --coin=xmr --use-internal-keccak-module 2 $rounds_min
- $gentest_py --coin=zec 2 $rounds
- $gentest_py --coin=zec --type=zcash_z 2 $rounds_mid
- $gentest_py --coin=etc 1 $rounds
- $gentest_py --coin=etc --use-internal-keccak-module 1 $rounds_min
- $gentest_py --coin=eth 1 $rounds
- $gentest_py --coin=eth --use-internal-keccak-module 1 $rounds_min
- $gentest_py --coin=xmr 1 $rounds
- $gentest_py --coin=xmr --use-internal-keccak-module 1 $rounds_min
- $gentest_py --coin=zec 1 $rounds
- $gentest_py --coin=zec --type=zcash_z 1 $rounds_mid
- # verification against external libraries and tools:
- # pycoin
- $gentest_py --all --type=legacy 2:pycoin $rounds
- $gentest_py --all --type=compressed 2:pycoin $rounds
- $gentest_py --all --type=segwit 2:pycoin $rounds
- $gentest_py --all --type=bech32 2:pycoin $rounds
- $gentest_py --all-coins --type=legacy 1:pycoin $rounds
- $gentest_py --all-coins --type=compressed 1:pycoin $rounds
- $gentest_py --all-coins --type=segwit 1:pycoin $rounds
- $gentest_py --all-coins --type=bech32 1:pycoin $rounds
- $gentest_py --all --type=legacy --testnet=1 2:pycoin $rounds
- $gentest_py --all --type=compressed --testnet=1 2:pycoin $rounds
- $gentest_py --all --type=segwit --testnet=1 2:pycoin $rounds
- $gentest_py --all --type=bech32 --testnet=1 2:pycoin $rounds
- $gentest_py --all-coins --type=legacy --testnet=1 1:pycoin $rounds
- $gentest_py --all-coins --type=compressed --testnet=1 1:pycoin $rounds
- $gentest_py --all-coins --type=segwit --testnet=1 1:pycoin $rounds
- $gentest_py --all-coins --type=bech32 --testnet=1 1:pycoin $rounds
- # keyconv
- $gentest_py --all --type=legacy 2:keyconv $rounds
- $gentest_py --all --type=compressed 2:keyconv $rounds
- $gentest_py --all-coins --type=legacy 1:keyconv $rounds
- $gentest_py --all-coins --type=compressed 1:keyconv $rounds
e # ethkey
e $gentest_py --all 2:ethkey $rounds
e $gentest_py --coin=eth 1:ethkey $rounds
e $gentest_py --coin=eth --use-internal-keccak-module 2:ethkey $rounds_mid
m # moneropy
m $gentest_py --all --coin=xmr 2:moneropy $rounds_min # very slow, be patient!
m $gentest_py --coin=xmr all:moneropy $rounds_mid # very slow, please be patient!
z # zcash-mini
z $gentest_py --all --coin=zec --type=zcash_z 1:zcash-mini $rounds_mid
z $gentest_py --coin=zec --type=zcash_z all:zcash-mini $rounds_mid
"
[ "$MSYS2" ] && t_alts_skip='m z' # no moneropy (pysha3), zcash-mini (golang)
@ -501,23 +502,23 @@ i_gen='Gentest'
s_gen="The following tests will run '$gentest_py' for BTC and LTC mainnet and testnet"
t_gen="
- # speed tests, no verification:
- $gentest_py --coin=btc 2 $rounds
- $gentest_py --coin=btc --type=compressed 2 $rounds
- $gentest_py --coin=btc --type=segwit 2 $rounds
- $gentest_py --coin=btc --type=bech32 2 $rounds
- $gentest_py --coin=ltc 2 $rounds
- $gentest_py --coin=ltc --type=compressed 2 $rounds
- $gentest_py --coin=ltc --type=segwit 2 $rounds
- $gentest_py --coin=ltc --type=bech32 2 $rounds
- $gentest_py --coin=btc 1 $rounds
- $gentest_py --coin=btc --type=compressed 1 $rounds
- $gentest_py --coin=btc --type=segwit 1 $rounds
- $gentest_py --coin=btc --type=bech32 1 $rounds
- $gentest_py --coin=ltc 1 $rounds
- $gentest_py --coin=ltc --type=compressed 1 $rounds
- $gentest_py --coin=ltc --type=segwit 1 $rounds
- $gentest_py --coin=ltc --type=bech32 1 $rounds
- # wallet dumps:
- $gentest_py 2 $REFDIR/btcwallet.dump
- $gentest_py --type=segwit 2 $REFDIR/btcwallet-segwit.dump
- $gentest_py --type=bech32 2 $REFDIR/btcwallet-bech32.dump
- $gentest_py --testnet=1 2 $REFDIR/btcwallet-testnet.dump
- $gentest_py --coin=ltc 2 $REFDIR/litecoin/ltcwallet.dump
- $gentest_py --coin=ltc --type=segwit 2 $REFDIR/litecoin/ltcwallet-segwit.dump
- $gentest_py --coin=ltc --type=bech32 2 $REFDIR/litecoin/ltcwallet-bech32.dump
- $gentest_py --coin=ltc --testnet=1 2 $REFDIR/litecoin/ltcwallet-testnet.dump
- $gentest_py --type=compressed 1 $REFDIR/btcwallet.dump
- $gentest_py --type=segwit 1 $REFDIR/btcwallet-segwit.dump
- $gentest_py --type=bech32 1 $REFDIR/btcwallet-bech32.dump
- $gentest_py --type=compressed --testnet=1 1 $REFDIR/btcwallet-testnet.dump
- $gentest_py --coin=ltc --type=compressed 1 $REFDIR/litecoin/ltcwallet.dump
- $gentest_py --coin=ltc --type=segwit 1 $REFDIR/litecoin/ltcwallet-segwit.dump
- $gentest_py --coin=ltc --type=bech32 1 $REFDIR/litecoin/ltcwallet-bech32.dump
- $gentest_py --coin=ltc --type=compressed --testnet=1 1 $REFDIR/litecoin/ltcwallet-testnet.dump
- # libsecp256k1 vs python-ecdsa:
- $gentest_py 1:2 $rounds
- $gentest_py --type=segwit 1:2 $rounds
@ -528,6 +529,8 @@ t_gen="
- $gentest_py --coin=ltc --type=segwit 1:2 $rounds
- $gentest_py --coin=ltc --testnet=1 1:2 $rounds
- $gentest_py --coin=ltc --testnet=1 --type=segwit 1:2 $rounds
- # all backends vs pycoin:
- $gentest_py all:pycoin $rounds
"
f_gen='gentest tests completed'

View file

@ -384,7 +384,7 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared):
rand_coinaddr = AddrGenerator(
self.proto,
('legacy','compressed')[non_mmgen_input_compressed]
).to_addr(KeyGenerator(self.proto,'std').to_pubhex(privkey))
).to_addr(KeyGenerator(self.proto,'std').gen_data(privkey))
of = joinpath(self.cfgs[non_mmgen_input]['tmpdir'],non_mmgen_fn)
write_data_to_file(
outfile = of,
@ -422,7 +422,7 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared):
t = ('compressed','segwit')['S' in self.proto.mmtypes]
from mmgen.addr import KeyGenerator,AddrGenerator
rand_coinaddr = AddrGenerator(self.proto,t).to_addr(
KeyGenerator(self.proto,'std').to_pubhex(privkey)
KeyGenerator(self.proto,'std').gen_data(privkey)
)
# total of two outputs must be < 10 BTC (<1000 LTC)

View file

@ -53,8 +53,8 @@ class TestSuiteRefAltcoin(TestSuiteRef,TestSuiteBase):
('ref_addrfile_gen_zec', 'generate address file (ZEC-T)'),
('ref_addrfile_gen_zec_z','generate address file (ZEC-Z)'),
('ref_addrfile_gen_xmr', 'generate address file (XMR)'),
# we test the old ed25519 library in test-release.sh, so skip this
# ('ref_addrfile_gen_xmr_old','generate address file (XMR - old (slow) ed25519 library)'),
# we test the unoptimized ed25519 mod in unit_tests.py, so skip this
# ('ref_addrfile_gen_xmr_slow','generate address file (XMR - unoptimized ed25519 module)'),
('ref_keyaddrfile_gen_eth', 'generate key-address file (ETH)'),
('ref_keyaddrfile_gen_etc', 'generate key-address file (ETC)'),
@ -153,8 +153,8 @@ class TestSuiteRefAltcoin(TestSuiteRef,TestSuiteBase):
def ref_addrfile_gen_xmr(self):
return self.ref_altcoin_addrgen(coin='XMR',mmtype='monero')
def ref_addrfile_gen_xmr_old(self):
return self.ref_altcoin_addrgen(coin='XMR',mmtype='monero',add_args=['--use-old-ed25519'])
def ref_addrfile_gen_xmr_slow(self):
return self.ref_altcoin_addrgen(coin='XMR',mmtype='monero',add_args=['--keygen-backend=2'])
def ref_keyaddrfile_gen_eth(self):
return self.ref_altcoin_addrgen(coin='ETH',mmtype='ethereum',gen_what='key')

View file

@ -908,7 +908,7 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared):
return self.alice_add_label_badaddr( rt_pw,'Invalid coin address for this chain: ')
def alice_add_label_badaddr2(self):
addr = init_proto(self.proto.coin,network='mainnet').pubhash2addr('00'*20,False) # mainnet zero address
addr = init_proto(self.proto.coin,network='mainnet').pubhash2addr(bytes(20),False) # mainnet zero address
return self.alice_add_label_badaddr( addr, f'Invalid coin address for this chain: {addr}' )
def alice_add_label_badaddr3(self):
@ -916,7 +916,7 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared):
return self.alice_add_label_badaddr( addr, f'MMGen address {addr!r} not found in tracking wallet' )
def alice_add_label_badaddr4(self):
addr = self.proto.pubhash2addr('00'*20,False) # regtest (testnet) zero address
addr = self.proto.pubhash2addr(bytes(20),False) # regtest (testnet) zero address
return self.alice_add_label_badaddr( addr, f'Address {addr!r} not found in tracking wallet' )
def alice_remove_label1(self):

99
test/unit_tests_d/ut_gen.py Executable file
View file

@ -0,0 +1,99 @@
#!/usr/bin/env python3
"""
test.unit_tests_d.ut_gen: key/address generation unit tests for the MMGen suite
"""
from mmgen.common import *
from mmgen.protocol import init_proto
from mmgen.key import PrivKey
from mmgen.addr import MMGenAddrType,KeyGenerator,AddrGenerator
from mmgen.keygen import get_backends
# TODO: add viewkey checks
vectors = { # from tooltest2
'btc': ( (
'5HwzecKMWD82ppJK3qMKpC7ohXXAwcyAN5VgdJ9PLFaAzpBG4sX',
'1C5VPtgq9xQ6AcTgMAR3J6GDrs72HC4pS1',
'legacy'
), (
'KwojSzt1VvW343mQfWQi3J537siAt5ktL2qbuCg1ZyKR8BLQ6UJm',
'1Kz9fVSUMshzPejpzW9D95kScgA3rY6QxF',
'compressed'
), (
'KwojSzt1VvW343mQfWQi3J537siAt5ktL2qbuCg1ZyKR8BLQ6UJm',
'3AhjTiWHhVJAi1s5CfKMcLzYps12x3gZhg',
'segwit'
), (
'KwojSzt1VvW343mQfWQi3J537siAt5ktL2qbuCg1ZyKR8BLQ6UJm',
'bc1q6pqnfwwakuuejpm9w52ds342f9d5u36v0qnz7c',
'bech32' ),
),
'eth': ( (
'0000000000000000000000000000000000000000000000000000000000000001',
'7e5f4552091a69125d5dfcb7b8c2659029395bdf',
'ethereum',
), ),
'xmr': ( (
'0000000000000000000000000000000000000000000000000000000000000001',
'42nsXK8WbVGTNayQ6Kjw5UdgqbQY5KCCufdxdCgF7NgTfjC69Mna7DJSYyie77hZTQ8H92G2HwgFhgEUYnDzrnLnQdF28r3',
'monero',
), ),
'zec': ( (
'SKxny894fJe2rmZjeuoE6GVfNkWoXfPp8337VrLLNWG56FjqVUYR',
'zceQDpyNwek7dKqF5ZuFGj7YrNVxh7X1aPkrVxDLVxWSiZAFDEuy5C7XNV8VhyZ3ghTPQ61xjCGiyLT3wqpiN1Yi6mdmaCq',
'zcash_z',
), ),
}
def do_test(proto,wif,addr_chk,addr_type,internal_keccak):
if internal_keccak:
opt.use_internal_keccak_module = True
add_msg = ' (internal keccak module)'
else:
add_msg = ''
at = MMGenAddrType(proto,addr_type)
privkey = PrivKey(proto,wif=wif)
for n,backend in enumerate(get_backends(at.pubkey_type)):
kg = KeyGenerator(proto,at.pubkey_type,n+1)
qmsg(blue(f' Testing backend {backend!r} for addr type {addr_type!r}{add_msg}'))
data = kg.gen_data(privkey)
for k,v in data._asdict().items():
if v and k in ('pubkey','viewkey_bytes'):
qmsg(f' {k+":":19} {v.hex()}')
ag = AddrGenerator(proto,addr_type)
addr = ag.to_addr(data)
qmsg(f' addr: {addr}\n')
assert addr == addr_chk, f'{addr} != {addr_chk}'
opt.use_internal_keccak_module = False
def do_tests(coin,internal_keccak=False):
proto = init_proto(coin)
for wif,addr,addr_type in vectors[coin]:
do_test(proto,wif,addr,addr_type,internal_keccak)
return True
class unit_tests:
def btc(self,name,ut):
return do_tests('btc')
def eth(self,name,ut):
do_tests('eth')
return do_tests('eth',internal_keccak=True)
def xmr(self,name,ut):
if not opt.fast:
do_tests('xmr')
return do_tests('xmr',internal_keccak=True)
def zec(self,name,ut):
return do_tests('zec')