From 32c522c039b4ce49c954fe10c6c22f4120c497ad Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Sat, 15 Jan 2022 14:00:12 +0000 Subject: [PATCH] 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 --- mmgen/addr.py | 279 ++++------------------------ mmgen/addrfile.py | 4 +- mmgen/addrgen.py | 131 +++++++++++++ mmgen/addrlist.py | 14 +- mmgen/data/version | 2 +- mmgen/exception.py | 1 + mmgen/globalvars.py | 5 +- mmgen/help.py | 11 ++ mmgen/key.py | 33 ++-- mmgen/keygen.py | 239 ++++++++++++++++++++++++ mmgen/main_addrgen.py | 17 +- mmgen/main_tool.py | 8 +- mmgen/main_txbump.py | 9 +- mmgen/main_txdo.py | 9 +- mmgen/main_txsign.py | 10 +- mmgen/opts.py | 4 - mmgen/passwdlist.py | 17 +- mmgen/protocol.py | 59 +++--- mmgen/tool.py | 44 ++--- mmgen/tx.py | 8 +- test/gentest.py | 46 +++-- test/misc/opts.py | 4 +- test/objtest_py_d/ot_btc_mainnet.py | 17 +- test/objtest_py_d/ot_btc_testnet.py | 8 +- test/objtest_py_d/ot_ltc_mainnet.py | 8 +- test/objtest_py_d/ot_ltc_testnet.py | 8 +- test/test-release.sh | 77 ++++---- test/test_py_d/ts_main.py | 4 +- test/test_py_d/ts_ref_altcoin.py | 8 +- test/test_py_d/ts_regtest.py | 4 +- test/unit_tests_d/ut_gen.py | 99 ++++++++++ 31 files changed, 728 insertions(+), 459 deletions(-) create mode 100755 mmgen/addrgen.py create mode 100755 mmgen/keygen.py create mode 100755 test/unit_tests_d/ut_gen.py diff --git a/mmgen/addr.py b/mmgen/addr.py index 961c34ef..00d90528 100755 --- a/mmgen/addr.py +++ b/mmgen/addr.py @@ -17,7 +17,7 @@ # along with this program. If not, see . """ -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) diff --git a/mmgen/addrfile.py b/mmgen/addrfile.py index 9515e8b3..e530fa44 100755 --- a/mmgen/addrfile.py +++ b/mmgen/addrfile.py @@ -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') diff --git a/mmgen/addrgen.py b/mmgen/addrgen.py new file mode 100755 index 00000000..f8406c39 --- /dev/null +++ b/mmgen/addrgen.py @@ -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 +# +# 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 . + +""" +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 ) diff --git a/mmgen/addrlist.py b/mmgen/addrlist.py index dc2c0267..cac3d04a 100755 --- a/mmgen/addrlist.py +++ b/mmgen/addrlist.py @@ -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'} diff --git a/mmgen/data/version b/mmgen/data/version index 0bafadb7..9c6d5cd4 100644 --- a/mmgen/data/version +++ b/mmgen/data/version @@ -1 +1 @@ -13.1.dev002 +13.1.dev003 diff --git a/mmgen/exception.py b/mmgen/exception.py index a526022a..e2b162f5 100755 --- a/mmgen/exception.py +++ b/mmgen/exception.py @@ -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 diff --git a/mmgen/globalvars.py b/mmgen/globalvars.py index 81800e0d..59ad9ee6 100755 --- a/mmgen/globalvars.py +++ b/mmgen/globalvars.py @@ -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 diff --git a/mmgen/help.py b/mmgen/help.py index 77a30fa3..352fe257 100755 --- a/mmgen/help.py +++ b/mmgen/help.py @@ -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() diff --git a/mmgen/key.py b/mmgen/key.py index 0577eb6b..72cb1a95 100755 --- a/mmgen/key.py +++ b/mmgen/key.py @@ -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: diff --git a/mmgen/keygen.py b/mmgen/keygen.py new file mode 100755 index 00000000..19b560c9 --- /dev/null +++ b/mmgen/keygen.py @@ -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 +# +# 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 . + +""" +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' ) diff --git a/mmgen/main_addrgen.py b/mmgen/main_addrgen.py index adc6d325..6049c7a9 100755 --- a/mmgen/main_addrgen.py +++ b/mmgen/main_addrgen.py @@ -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()) diff --git a/mmgen/main_tool.py b/mmgen/main_tool.py index d802ef1e..12774732 100755 --- a/mmgen/main_tool.py +++ b/mmgen/main_tool.py @@ -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 ' 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) diff --git a/mmgen/main_txbump.py b/mmgen/main_txbump.py index b6e0d2e0..5f042b55 100755 --- a/mmgen/main_txbump.py +++ b/mmgen/main_txbump.py @@ -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'), diff --git a/mmgen/main_txdo.py b/mmgen/main_txdo.py index 8a3d8b0a..e88b6c83 100755 --- a/mmgen/main_txdo.py +++ b/mmgen/main_txdo.py @@ -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'), diff --git a/mmgen/main_txsign.py b/mmgen/main_txsign.py index 0501e239..ba3e4844 100755 --- a/mmgen/main_txsign.py +++ b/mmgen/main_txsign.py @@ -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), diff --git a/mmgen/opts.py b/mmgen/opts.py index be77b983..7c68458c 100755 --- a/mmgen/opts.py +++ b/mmgen/opts.py @@ -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') diff --git a/mmgen/passwdlist.py b/mmgen/passwdlist.py index 565587b3..5f525e67 100755 --- a/mmgen/passwdlist.py +++ b/mmgen/passwdlist.py @@ -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:] diff --git a/mmgen/protocol.py b/mmgen/protocol.py index d7ec4f5d..b694870e 100755 --- a/mmgen/protocol.py +++ b/mmgen/protocol.py @@ -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 diff --git a/mmgen/tool.py b/mmgen/tool.py index c23ea05a..7702f386 100755 --- a/mmgen/tool.py +++ b/mmgen/tool.py @@ -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" diff --git a/mmgen/tx.py b/mmgen/tx.py index 9ef4d4eb..c96ed921 100755 --- a/mmgen/tx.py +++ b/mmgen/tx.py @@ -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}...') diff --git a/test/gentest.py b/test/gentest.py index 3be7e15a..fffc7811 100755 --- a/test/gentest.py +++ b/test/gentest.py @@ -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']) diff --git a/test/misc/opts.py b/test/misc/opts.py index 16c5ea65..ca5927a9 100755 --- a/test/misc/opts.py +++ b/test/misc/opts.py @@ -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'), diff --git a/test/objtest_py_d/ot_btc_mainnet.py b/test/objtest_py_d/ot_btc_mainnet.py index f6fd0f3d..4e8c9dc1 100755 --- a/test/objtest_py_d/ot_btc_mainnet.py +++ b/test/objtest_py_d/ot_btc_mainnet.py @@ -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 diff --git a/test/objtest_py_d/ot_btc_testnet.py b/test/objtest_py_d/ot_btc_testnet.py index 704e0366..40d2fe87 100755 --- a/test/objtest_py_d/ot_btc_testnet.py +++ b/test/objtest_py_d/ot_btc_testnet.py @@ -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} ), }, } diff --git a/test/objtest_py_d/ot_ltc_mainnet.py b/test/objtest_py_d/ot_ltc_mainnet.py index 518ebf4b..22e34750 100755 --- a/test/objtest_py_d/ot_ltc_mainnet.py +++ b/test/objtest_py_d/ot_ltc_mainnet.py @@ -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} ) }, } diff --git a/test/objtest_py_d/ot_ltc_testnet.py b/test/objtest_py_d/ot_ltc_testnet.py index 0037c7ce..d815553d 100755 --- a/test/objtest_py_d/ot_ltc_testnet.py +++ b/test/objtest_py_d/ot_ltc_testnet.py @@ -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} ) }, } diff --git a/test/test-release.sh b/test/test-release.sh index cab453ad..846ee530 100755 --- a/test/test-release.sh +++ b/test/test-release.sh @@ -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' diff --git a/test/test_py_d/ts_main.py b/test/test_py_d/ts_main.py index d39d2bee..67cf0bff 100755 --- a/test/test_py_d/ts_main.py +++ b/test/test_py_d/ts_main.py @@ -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) diff --git a/test/test_py_d/ts_ref_altcoin.py b/test/test_py_d/ts_ref_altcoin.py index 561f30d3..8adb86fc 100755 --- a/test/test_py_d/ts_ref_altcoin.py +++ b/test/test_py_d/ts_ref_altcoin.py @@ -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') diff --git a/test/test_py_d/ts_regtest.py b/test/test_py_d/ts_regtest.py index 0d241c40..4674af0e 100755 --- a/test/test_py_d/ts_regtest.py +++ b/test/test_py_d/ts_regtest.py @@ -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): diff --git a/test/unit_tests_d/ut_gen.py b/test/unit_tests_d/ut_gen.py new file mode 100755 index 00000000..e76a2067 --- /dev/null +++ b/test/unit_tests_d/ut_gen.py @@ -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')