Assorted fixes/improvements:

- Importing addresses with --rescan working again
- Tracking and spending non-MMGen addresses now fully functional
- mmgen-txcreate: improvements in unspent outputs display
- mmgen-txsign: use bitcoind wallet dump as keylist fixed

- Testnet support:
  - Practice sending transactions without risking funds
  	(free testnet coins: https://tpfaucet.appspot.com/)
  - Test suite fully supported
  - To enable, set MMGEN_TESTNET environment variable
This commit is contained in:
philemon 2016-11-11 16:05:27 +03:00
commit 73ca40ea8d
Signed by untrusted user who does not match committer: mmgen
GPG key ID: 62DBE9E5212F05BE
21 changed files with 282 additions and 166 deletions

View file

@ -2,13 +2,13 @@
Install required Debian/Ubuntu packages:
$ sudo apt-get install python-pip python-dev python-pexpect python-ecdsa python-scrypt libssl-dev git
$ sudo apt-get install python-pip python-dev python-pexpect python-ecdsa python-scrypt libssl-dev git autoconf libtool
Install the Python Cryptography Toolkit:
$ sudo pip install pycrypto
Install the secp256k1 library
Install the secp256k1 library:
$ git clone https://github.com/bitcoin-core/secp256k1.git
$ cd secp256k1

View file

@ -58,17 +58,17 @@ internal ECDSA library for address generation.
def _wif2addr_python(wif):
privhex = wif2hex(wif)
if not privhex: return False
return privnum2addr(int(privhex,16),wif[0] != '5')
return privnum2addr(int(privhex,16),wif[0] != ('5','9')[g.testnet])
def _wif2addr_keyconv(wif):
if wif[0] == '5':
if wif[0] == ('5','9')[g.testnet]:
from subprocess import check_output
return check_output(['keyconv', wif]).split()[1]
else:
return _wif2addr_python(wif)
def _wif2addr_secp256k1(wif):
return _privhex2addr_secp256k1(wif2hex(wif),wif[0] != '5')
return _privhex2addr_secp256k1(wif2hex(wif),wif[0] != ('5','9')[g.testnet])
def _privhex2addr_python(privhex,compressed=False):
return privnum2addr(int(privhex,16),compressed)

View file

@ -51,13 +51,15 @@ b58a='123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
# The 'zero address':
# 1111111111111111111114oLvT2 (use step2 = ('0' * 40) to generate)
import mmgen.globalvars as g
def pubhex2hexaddr(pubhex):
step1 = sha256(unhexlify(pubhex)).digest()
return hashlib_new('ripemd160',step1).hexdigest()
def hexaddr2addr(hexaddr, vers_num='00'):
# See above:
hexaddr2 = vers_num + hexaddr
def hexaddr2addr(hexaddr,p2sh=False):
# devdoc/ref_transactions.md:
hexaddr2 = ('00','6f','05','c4')[g.testnet+(2*p2sh)] + hexaddr
step1 = sha256(unhexlify(hexaddr2)).digest()
step2 = sha256(step1).hexdigest()
pubkey = hexaddr2 + step2[:8]
@ -65,9 +67,8 @@ def hexaddr2addr(hexaddr, vers_num='00'):
return ('1' * lzeroes) + _numtob58(int(pubkey,16))
def verify_addr(addr,verbose=False,return_hex=False):
for vers_num,ldigit in ('00','1'),('05','3'):
if addr[0] != ldigit: continue
for vers_num,ldigit in ('00','1'),('05','3'),('6f','mn'),('c4','2'):
if addr[0] not in ldigit: continue
num = _b58tonum(addr)
if num == False: break
addr_hex = '{:050x}'.format(num)
@ -145,7 +146,7 @@ def b58decode_pad(s):
# Compressed address support:
def wif2hex(wif):
compressed = wif[0] != '5'
compressed = wif[0] != ('5','9')[g.testnet]
idx = (66,68)[bool(compressed)]
num = _b58tonum(wif)
if num == False: return False
@ -153,19 +154,25 @@ def wif2hex(wif):
if compressed and key[66:68] != '01': return False
round1 = sha256(unhexlify(key[:idx])).digest()
round2 = sha256(round1).hexdigest()
return key[2:66] if (key[:2] == '80' and key[idx:] == round2[:8]) else False
return key[2:66] if (key[:2] == ('80','ef')[g.testnet] and key[idx:] == round2[:8]) else False
def hex2wif(hexpriv,compressed=False):
step1 = '80' + hexpriv + ('','01')[bool(compressed)]
step1 = ('80','ef')[g.testnet] + hexpriv + ('','01')[bool(compressed)]
step2 = sha256(unhexlify(step1)).digest()
step3 = sha256(step2).hexdigest()
key = step1 + step3[:8]
return _numtob58(int(key,16))
# 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(numpriv,compressed=False):
pko = ecdsa.SigningKey.from_secret_exponent(numpriv,_secp256k1)
# pubkey = 32-byte X coord + 32-byte Y coord (unsigned big-endian)
pubkey = hexlify(pko.get_verifying_key().to_string())
if compressed:
if compressed: # discard Y coord, replace with appropriate version byte
# even Y: <0, odd Y: >0 -- https://bitcointalk.org/index.php?topic=129652.0
p = ('03','02')[pubkey[-1] in '02468ace']
return p+pubkey[:64]
else:

View file

@ -45,6 +45,7 @@ no_license = os.getenv('MMGEN_NOLICENSE')
bogus_wallet_data = os.getenv('MMGEN_BOGUS_WALLET_DATA')
disable_hold_protect = os.getenv('MMGEN_DISABLE_HOLD_PROTECT')
color = (False,True)[sys.stdout.isatty() and not os.getenv('MMGEN_DISABLE_COLOR')]
testnet = (False,True)[bool(os.getenv('MMGEN_TESTNET'))]
proj_name = 'MMGen'
prog_name = os.path.basename(sys.argv[0])
@ -63,12 +64,14 @@ incompatible_opts = (
('label','keep_label'),
('tx_id', 'info'),
('tx_id', 'terse_info'),
('batch', 'rescan'),
)
min_screen_width = 80
minconf = 1
# Global value sets user opt
dfl_vars = 'seed_len','hash_preset','usr_randchars','debug','tx_confs','tx_fee_adj','tx_fee','key_generator'
dfl_vars = 'minconf','seed_len','hash_preset','usr_randchars','debug','tx_confs','tx_fee_adj','tx_fee','key_generator'
keyconv_exec = 'keyconv'

View file

@ -25,14 +25,17 @@ import time
from mmgen.common import *
from mmgen.addr import AddrList,KeyAddrList
# In batch mode, bitcoind just rescans each address separately anyway, so make
# --batch and --rescan incompatible.
opts_data = {
'desc': """Import addresses (both {pnm} and non-{pnm}) into a bitcoind
'desc': """Import addresses (both {pnm} and non-{pnm}) into an {pnm}
tracking wallet""".format(pnm=g.proj_name),
'usage':'[opts] [mmgen address file]',
'options': """
-h, --help Print this help message
-b, --batch Batch mode. Import all addresses in one RPC call
-l, --addrlist Address source is a flat list of addresses
-b, --batch Import all addresses in one RPC call.
-l, --addrlist Address source is a flat list of (non-MMGen) Bitcoin addresses
-k, --keyaddr-file Address source is a key-address file
-q, --quiet Suppress warnings
-r, --rescan Rescan the blockchain. Required if address to import is
@ -42,6 +45,8 @@ opts_data = {
'notes': """\n
This command can also be used to update the comment fields of addresses already
in the tracking wallet.
The --batch option cannot be used with the --rescan option.
"""
}
@ -116,12 +121,15 @@ for n,e in enumerate(ai.data):
if e.idx:
label = '%s:%s' % (ai.seed_id,e.idx)
if e.label: label += ' ' + e.label
else: label = 'non-{pnm}'.format(pnm=g.proj_name)
m = label
else:
label = 'btc:{}'.format(e.addr)
m = 'non-'+g.proj_name
if opt.batch:
arg_list.append((e.addr,label,False))
elif opt.rescan:
t = threading.Thread(target=import_address, args=(e.addr,label,True))
t = threading.Thread(target=import_address,args=[e.addr,label,True])
t.daemon = True
t.start()
@ -131,7 +139,7 @@ for n,e in enumerate(ai.data):
if t.is_alive():
elapsed = int(time.time() - start)
count = '%s/%s:' % (n+1, ai.num_addrs)
msg_r(msg_fmt % (secs_to_hms(elapsed),count,e.addr,'(%s)'%label))
msg_r(msg_fmt % (secs_to_hms(elapsed),count,e.addr,'(%s)' % m))
time.sleep(1)
else:
if err_flag: die(2,'\nImport failed')
@ -140,12 +148,10 @@ for n,e in enumerate(ai.data):
else:
import_address(e.addr,label,False)
count = '%s/%s:' % (n+1, ai.num_addrs)
msg_r(msg_fmt % (count, e.addr, '(%s)'%label))
msg_r(msg_fmt % (count, e.addr, '(%s)' % m))
if err_flag: die(2,'\nImport failed')
msg(' - OK')
if opt.batch:
if opt.rescan:
msg('Warning: this command may take a long time to complete!')
ret = c.importaddress(arg_list,batch=True,timeout=(False,3600)[bool(opt.rescan)])
ret = c.importaddress(arg_list,batch=True)
msg('OK: %s addresses imported' % len(ret))

View file

@ -36,8 +36,9 @@ opts_data = {
-c, --comment-file= f Source the transaction's comment from file 'f'
-C, --tx-confs= c Desired number of confirmations (default: {g.tx_confs})
-d, --outdir= d Specify an alternate directory 'd' for output
-e, --echo-passphrase Print passphrase to screen when typing it
-e, --clear-screen Clear screen before displaying unspent outputs
-f, --tx-fee= f Transaction fee (default: {g.tx_fee} BTC (but see below))
-m, --minconf= n Minimum number of confirmations required to spend outputs (default: 1)
-i, --info Display unspent outputs and exit
-q, --quiet Suppress warnings; overwrite files without prompting
-v, --verbose Produce more verbose output
@ -199,7 +200,7 @@ if not opt.info:
fee_estimate = get_fee_estimate()
tw = MMGenTrackingWallet()
tw = MMGenTrackingWallet(minconf=opt.minconf)
tw.view_and_sort()
tw.display_total()

View file

@ -227,10 +227,9 @@ if opt.mmgen_keys_from_file:
if opt.keys_from_file:
l = get_lines_from_file(opt.keys_from_file,'key-address data',trim_comments=True)
kl = KeyAddrList(keylist=l)
kl = KeyAddrList(keylist=[m.split()[0] for m in l]) # accept bitcoind wallet dumps
if kal: kl.remove_dups(kal,key='wif')
kl.generate_addrs()
# pp_die(kl)
tx_num_str = ''
for tx_num,tx_file in enumerate(tx_files,1):

View file

@ -195,6 +195,7 @@ class AddrIdx(int,InitErrors):
m = "'%s': addr idx cannot be less than one" % num
else:
return me
return cls.init_fail(m,on_fail)
class AddrIdxList(list,InitErrors):
@ -349,7 +350,7 @@ class BTCAddr(str,Hilite,InitErrors):
cls.arg_chk(cls,on_fail)
me = str.__new__(cls,s)
from mmgen.bitcoin import verify_addr
if verify_addr(s):
if type(s) in (str,unicode,BTCAddr) and verify_addr(s):
return me
else:
m = "'%s': value is not a Bitcoin address" % s
@ -375,13 +376,14 @@ class SeedID(str,Hilite,InitErrors):
if seed:
from mmgen.seed import Seed
from mmgen.util import make_chksum_8
assert type(seed) == Seed
return str.__new__(cls,make_chksum_8(seed.get_data()))
if type(seed) == Seed:
return str.__new__(cls,make_chksum_8(seed.get_data()))
elif sid:
from string import hexdigits
assert len(sid) == cls.width and set(sid) <= set(hexdigits.upper())
return str.__new__(cls,sid)
m = "'%s': value cannot be converted to SeedID" % s
if len(sid) == cls.width and set(sid) <= set(hexdigits.upper()):
return str.__new__(cls,sid)
m = "'%s': value cannot be converted to SeedID" % str(seed or sid)
return cls.init_fail(m,on_fail)
class MMGenID(str,Hilite,InitErrors):
@ -395,9 +397,9 @@ class MMGenID(str,Hilite,InitErrors):
s = str(s)
if ':' in s:
a,b = s.split(':',1)
sid = SeedID(sid=a,on_fail='return')
sid = SeedID(sid=a,on_fail='silent')
if sid:
idx = AddrIdx(b,on_fail='return')
idx = AddrIdx(b,on_fail='silent')
if idx:
return str.__new__(cls,'%s:%s' % (sid,idx))

View file

@ -20,9 +20,11 @@
rpc.py: Bitcoin RPC library for the MMGen suite
"""
import httplib,base64,json,decimal
import httplib,base64,json
from mmgen.common import *
from decimal import Decimal
from mmgen.obj import BTCAmt
class BitcoinRPCConnection(object):
@ -30,7 +32,7 @@ class BitcoinRPCConnection(object):
def __init__(
self,
host='localhost',port=8332,
host='localhost',port=(8332,18332)[g.testnet],
user=None,passwd=None,auth_cookie=None,
):
@ -64,7 +66,7 @@ class BitcoinRPCConnection(object):
for k in cf:
if k in kwargs and kwargs[k]: cf[k] = kwargs[k]
c = httplib.HTTPConnection(self.host, self.port, False, cf['timeout'])
hc = httplib.HTTPConnection(self.host, self.port, False, cf['timeout'])
if cf['batch']:
p = [{'method':cmd,'params':r,'id':n} for n,r in enumerate(args[0],1)]
@ -80,7 +82,6 @@ class BitcoinRPCConnection(object):
dmsg('=== rpc.py debug ===')
dmsg(' RPC POST data ==> %s\n' % p)
from mmgen.obj import BTCAmt
caller = self
class MyJSONEncoder(json.JSONEncoder):
def default(self, obj):
@ -94,14 +95,14 @@ class BitcoinRPCConnection(object):
# print(dump)
try:
c.request('POST', '/', json.dumps(p,cls=MyJSONEncoder), {
hc.request('POST', '/', json.dumps(p,cls=MyJSONEncoder), {
'Host': self.host,
'Authorization': 'Basic ' + base64.b64encode(self.auth_str)
})
except Exception as e:
return die_maybe(None,2,'%s\nUnable to connect to bitcoind' % e)
r = c.getresponse() # returns HTTPResponse instance
r = hc.getresponse() # returns HTTPResponse instance
if r.status != 200:
e1 = r.read()
@ -118,7 +119,7 @@ class BitcoinRPCConnection(object):
if not r2:
return die_maybe(r,2,'Error: empty reply')
from decimal import Decimal
# from decimal import Decimal
r3 = json.loads(r2.decode('utf8'), parse_float=Decimal)
ret = []

View file

@ -23,9 +23,6 @@ term.py: Terminal-handling routines for the MMGen suite
import os,struct
from mmgen.common import *
CUR_SHOW = '\033[?25h'
CUR_HIDE = '\033[?25l'
def _kb_hold_protect_unix():
fd = sys.stdin.fileno()

View file

@ -80,6 +80,7 @@ def read_from_tmpfile(cfg,fn,binary=False):
return read_from_file(os.path.join(cfg['tmpdir'],fn),binary=binary)
def ok():
if opt.profile: return
if opt.verbose or opt.exact_output:
sys.stderr.write(green('OK\n'))
else: msg(' OK')

View file

@ -74,7 +74,7 @@ cmd_data = OrderedDict([
('listaddresses',["addrs [str='']",'minconf [int=1]','showempty [bool=False]','pager [bool=False]','showbtcaddrs [bool=False]']),
('getbalance', ['minconf [int=1]']),
('txview', ['<{} TX file> [str]'.format(pnm),'pager [bool=False]','terse [bool=False]']),
('twview', ["sort [str='age']",'reverse [bool=False]','wide [bool=False]','pager [bool=False]']),
('twview', ["sort [str='age']",'reverse [bool=False]','minconf [int=1]','wide [bool=False]','pager [bool=False]']),
('add_label', ['<{} address> [str]'.format(pnm),'<label> [str]']),
('remove_label', ['<{} address> [str]'.format(pnm)]),
@ -373,7 +373,7 @@ def listaddresses(addrs='',minconf=1,showempty=False,pager=False,showbtcaddrs=Fa
for d in c.listunspent(0):
mmaddr,comment = split2(d['account'])
if usr_addr_list and (mmaddr not in usr_addr_list): continue
if is_mmgen_id(mmaddr) and d['confirmations'] >= minconf:
if (mmaddr[:4] == 'btc:' or is_mmgen_id(mmaddr)) and d['confirmations'] >= minconf:
key = mmaddr.replace(':','_')
if key in addrs:
if addrs[key][2] != d['address']:
@ -391,7 +391,7 @@ def listaddresses(addrs='',minconf=1,showempty=False,pager=False,showbtcaddrs=Fa
for acct in accts:
mmaddr,comment = split2(acct)
if usr_addr_list and (mmaddr not in usr_addr_list): continue
if is_mmgen_id(mmaddr):
if mmaddr[:4] == 'btc:' or is_mmgen_id(mmaddr):
key = mmaddr.replace(':','_')
if key not in addrs:
if showbtcaddrs: save_a.append([acct])
@ -407,7 +407,7 @@ def listaddresses(addrs='',minconf=1,showempty=False,pager=False,showbtcaddrs=Fa
die(0,('No addresses with balances!','No tracked addresses!')[showempty])
fs = ('{mid} {lbl} {amt}','{mid} {addr} {lbl} {amt}')[showbtcaddrs]
max_mmid_len = max(len(k) for k in addrs) or 10
max_mmid_len = max([len(k) for k in addrs if k[:4] != 'btc_'] or [10])
max_lbl_len = max(len(addrs[k][1]) for k in addrs) or 7
out = [fs.format(
mid=MMGenID.fmtc('MMGenID',width=max_mmid_len),
@ -419,10 +419,11 @@ def listaddresses(addrs='',minconf=1,showempty=False,pager=False,showbtcaddrs=Fa
old_sid = ''
def s_mmgen(k): return '{:>0{w}}'.format(k,w=AddrIdx.max_digits+9) # TODO
for k in sorted(addrs,key=s_mmgen):
if old_sid and old_sid != k[:8]: out.append('')
old_sid = k[:8]
if old_sid and old_sid != k.split('_')[0]: out.append('')
old_sid = k.split('_')[0]
m = 'non-'+g.proj_name if k[:4] == 'btc_' else k.replace('_',':')
out.append(fs.format(
mid=MMGenID(k.replace('_',':')).fmt(width=max_mmid_len,color=True),
mid = MMGenID.fmtc(m,width=max_mmid_len,color=True),
addr=(addrs[k][2].fmt(color=True) if showbtcaddrs else None),
lbl=addrs[k][1].fmt(width=max_lbl_len,color=True,nullrepl='-'),
amt=addrs[k][0].fmt('3.0',color=True)))
@ -460,9 +461,9 @@ def txview(infile,pager=False,terse=False):
tx = MMGenTX(infile)
tx.view(pager,pause=False,terse=terse)
def twview(pager=False,reverse=False,wide=False,sort='age'):
def twview(pager=False,reverse=False,wide=False,minconf=1,sort='age'):
from mmgen.tw import MMGenTrackingWallet
tw = MMGenTrackingWallet()
tw = MMGenTrackingWallet(minconf=minconf)
tw.do_sort(sort,reverse=reverse)
out = tw.format_for_printing(color=True) if wide else tw.format_for_display()
do_pager(out) if pager else sys.stdout.write(out)

View file

@ -23,16 +23,17 @@ tw: Tracking wallet methods for the MMGen suite
from mmgen.common import *
from mmgen.obj import *
from mmgen.term import get_char
from mmgen.tx import is_mmgen_id
CUR_HOME,ERASE_ALL = '\033[H','\033[0J'
def parse_tw_acct_label(s):
ret = s.split(None,1)
if ret and MMGenID(ret[0],on_fail='silent'):
if len(ret) == 2:
return tuple(ret)
else:
return ret[0],None
else:
return None,None
a1,a2 = None,None
if ret:
a1 = ret[0] if is_mmgen_id(ret[0]) else '' if ret[0][:4] == 'btc:' else None
a2 = ret[1] if len(ret) == 2 else None
return a1,a2
class MMGenTWOutput(MMGenListItem):
attrs_reassign = 'label','skip'
@ -48,7 +49,7 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
}
sort_keys = 'addr','age','amt','txid','mmid'
def __init__(self):
def __init__(self,minconf=1):
self.unspent = []
self.fmt_display = ''
self.fmt_print = ''
@ -57,6 +58,7 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
self.group = False
self.show_days = True
self.show_mmid = True
self.minconf = minconf
self.get_data()
self.sort_key = 'age'
self.do_sort()
@ -69,7 +71,7 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
if g.bogus_wallet_data: # for debugging purposes only
us_rpc = eval(get_data_from_file(g.bogus_wallet_data))
else:
us_rpc = bitcoin_connection().listunspent()
us_rpc = bitcoin_connection().listunspent(self.minconf)
# write_data_to_file('bogus_unspent.json', repr(us), 'bogus unspent data')
# sys.exit()
@ -121,6 +123,7 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
my_raw_input(m1+'\n'+m2.format(g.min_screen_width))
def display(self):
if opt.clear_screen: msg(CUR_HOME+ERASE_ALL)
msg(self.format_for_display())
def format_for_display(self):
@ -158,18 +161,18 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
for n,i in enumerate(unsp):
addr_dots = '|' + '.'*33
mmid_disp = (MMGenID.hlc('.'*mmid_w) \
if i.skip=='addr' else i.mmid.fmt(width=mmid_w,color=True)) \
if i.mmid else ' ' * mmid_w
if self.show_mmid and i.mmid:
mmid_disp = MMGenID.fmtc('.'*mmid_w if i.skip=='addr'
else i.mmid or 'Non-{}'.format(g.proj_name),width=mmid_w,color=True)
if self.show_mmid:
addr_out = '%s %s' % (
type(i.addr).fmtc(addr_dots,width=btaddr_w,color=True) if i.skip == 'addr' \
else i.addr.fmt(width=btaddr_w,color=True),
'{} {}'.format(mmid_disp,i.label.fmt(width=label_w,color=True) if label_w > 0 else '')
'{} {}'.format(mmid_disp,i.label.fmt(width=label_w,color=True) \
if label_w > 0 else '')
)
else:
addr_out = type(i.addr).fmtc(addr_dots,width=addr_w,color=True) if i.skip=='addr' \
else i.addr.fmt(width=addr_w,color=True)
addr_out = type(i.addr).fmtc(addr_dots,width=addr_w,color=True) \
if i.skip=='addr' else i.addr.fmt(width=addr_w,color=True)
tx = ' ' * (tx_w-4) + '|...' if i.skip == 'txid' \
else i.txid[:tx_w-len(txdots)]+txdots
@ -186,13 +189,12 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
out = [fs % ('Num','Tx ID,Vout','Address'.ljust(34),'MMGen ID'.ljust(15),
'Amount(BTC)','Conf.','Age(d)', 'Label')]
max_lbl_len = max(len(i.label) for i in self.unspent if i.label) or 1
max_lbl_len = max([len(i.label) for i in self.unspent if i.label] or [1])
for n,i in enumerate(self.unspent):
addr = '=' if i.skip == 'addr' and self.group else i.addr.fmt(color=color)
tx = ' ' * 63 + '=' if i.skip == 'txid' and self.group else str(i.txid)
s = fs % (str(n+1)+')', tx+','+str(i.vout),addr,
(i.mmid.fmt(width=14,color=color) if i.mmid else
MMGenID.fmtc('',width=14,nullrepl='-',color=color)),
MMGenID.fmtc(i.mmid or 'Non-{}'.format(g.proj_name),width=14,color=color),
i.amt.fmt(color=color),i.confs,i.days,
i.label.hl(color=color) if i.label else
MMGenAddrLabel.fmtc('',color=color,nullrepl='-',width=max_lbl_len))
@ -218,9 +220,9 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
n = AddrIdx(ret,on_fail='silent') # hacky way to test and convert to integer
if not n or n < 1 or n > len(self.unspent):
msg('Choice must be a single number between 1 and %s' % len(self.unspent))
elif not self.unspent[n-1].mmid:
msg('Address #%s is not an %s address. No label can be added to it' %
(n,g.proj_name))
# elif not self.unspent[n-1].mmid:
# msg('Address #%s is not an %s address. No label can be added to it' %
# (n,g.proj_name))
else:
while True:
s = my_raw_input("Enter label text (or 'q' to return to main menu): ")
@ -290,10 +292,19 @@ Display options: show [D]ays, [g]roup, show [m]mgen addr, r[e]draw screen
# returns on failure
@classmethod
def add_label(cls,mmaddr,label='',addr=None):
mmaddr = MMGenID(mmaddr)
def add_label(cls,arg1,label='',addr=None):
from mmgen.tx import is_mmgen_id,is_btc_addr
if is_mmgen_id(arg1):
mmaddr = MMGenID(arg1)
elif is_btc_addr(arg1): # called from 'mmgen-tool add_label'
addr = arg1
mmaddr = 'btc:'+arg1
elif not arg1 and is_btc_addr(addr): # called from view_and_sort(), non-MMGen addr
mmaddr = 'btc:'+addr
else:
die(3,'{}: not a BTC address or {} ID'.format(arg1,g.proj_name))
if addr: # called from view_and_sort()
if addr:
if not BTCAddr(addr,on_fail='return'): return False
else:
from mmgen.addr import AddrData

View file

@ -662,7 +662,8 @@ def get_bitcoind_auth_cookie():
def bitcoin_connection():
host,port,user,passwd = 'localhost',8332,'rpcuser','rpcpassword'
port = (8332,18332)[g.testnet]
host,user,passwd = 'localhost','rpcuser','rpcpassword'
cfg = get_bitcoind_cfg_options((user,passwd))
auth_cookie = get_bitcoind_auth_cookie()

View file

@ -35,73 +35,139 @@ start_mscolor()
rounds = 100
opts_data = {
'desc': "Test address generation using various methods",
'usage':'[options] a:b [rounds]',
'desc': "Test address generation in various ways",
'usage':'[options] [spec] [rounds | dump file]',
'options': """
-h, --help Print this help message
-s, --system Test scripts and modules installed on system rather than
those in the repo root
-v, --verbose Produce more verbose output
-q, --quiet Produce quieter output
""",
'notes': """
{pnm} can generate addresses from secret keys using one of three methods,
as specified by the user:
Tests:
A/B: {prog} a:b [rounds] (compare output of two key generators)
Speed: {prog} a [rounds] (test speed of one key generator)
Compare: {prog} a <dump file> (compare output of a key generator against wallet dump)
where a and b are one of:
'1' - native Python ecdsa library (very slow)
'2' - 'keyconv' utility from the 'vanitygen' package (old default)
'3' - bitcoincore.org's secp256k1 library (default from v0.8.6)
1) with the native Python ecdsa library (very slow)
2) with the 'keyconv' utility from the 'vanitygen' package (old default)
3) using bitcoincore.org's secp256k1 library (default from v0.8.6)
This test suite compares the output of these different methods against each
other over set of randomly generated secret keys ({snum} by default).
EXAMPLE:
gentest.py 2:3 1000
(compare output of 'keyconv' with secp256k1 library, 1000 rounds)
""".format(pnm=g.proj_name,snum=rounds)
EXAMPLES:
{prog} 2:3 1000
(compare output of 'keyconv' with secp256k1 library, 1000 rounds)
{prog} 3 1000
(test speed of secp256k1 library address generation, 1000 rounds)
{prog} 3 my.dump
(compare addrs generated with secp256k1 library to bitcoind wallet dump)
""".format(prog='gentest.py',pnm=g.proj_name,snum=rounds)
}
cmd_args = opts.init(opts_data,add_opts=['exact_output'])
if not 1 <= len(cmd_args) <= 2: opts.usage()
urounds,fh = None,None
dump = []
if len(cmd_args) == 2:
try:
rounds = int(cmd_args[1])
assert rounds > 0
urounds = int(cmd_args[1])
assert urounds > 0
except:
die(1,"'rounds' must be a positive integer")
try:
fh = open(cmd_args[1])
except:
die(1,"Second argument must be filename or positive integer")
else:
for line in fh.readlines():
if 'addr=' in line:
x,addr = line.split('addr=')
dump.append([x.split()[0],addr.split()[0]])
if urounds: rounds = urounds
a,b = None,None
try:
a,b = cmd_args[0].split(':')
a,b = int(a),int(b)
for i in a,b: assert 1 <= i <= len(g.key_generators)
assert a != b
except:
die(1,"%s: incorrect 'a:b' specifier" % cmd_args[0])
try:
a = cmd_args[0]
a = int(a)
assert 1 <= a <= len(g.key_generators)
except:
die(1,"First argument must be one or two generator IDs, colon separated")
else:
try:
a,b = int(a),int(b)
for i in a,b: assert 1 <= i <= len(g.key_generators)
assert a != b
except:
die(1,"%s: invalid generator IDs" % cmd_args[0])
if opt.system: sys.path.pop(0)
def match_error(sec,wif,a_addr,b_addr,a,b):
m = ['','py-ecdsa','keyconv','secp256k1','dump']
msg_r(red('\nERROR: Addresses do not match!'))
die(3,"""
sec key : {}
WIF key : {}
{a:10}: {}
{b:10}: {}
""".format(sec,wif,a_addr,b_addr,pnm=g.proj_name,a=m[a],b=m[b]).rstrip())
m = "Comparing address generators '{}' and '{}'"
msg(green(m.format(g.key_generators[a-1],g.key_generators[b-1])))
from mmgen.addr import get_privhex2addr_f
gen_a = get_privhex2addr_f(selector=a)
gen_b = get_privhex2addr_f(selector=b)
compressed = False
for i in range(1,rounds+1):
msg_r('\rRound %s/%s ' % (i,rounds))
sec = hexlify(os.urandom(32))
wif = hex2wif(sec,compressed=compressed)
a_addr = gen_a(sec,compressed)
b_addr = gen_b(sec,compressed)
vmsg('\nkey: %s\naddr: %s\n' % (wif,a_addr))
if a_addr != b_addr:
msg_r(red('\nERROR: Addresses do not match!'))
die(3,"""
sec key: {}
WIF key: {}
{pnm}: {}
keyconv: {}
""".format(sec,wif,a_addr,b_addr,pnm=g.proj_name).rstrip())
if a != 2 and b != 2:
compressed = not compressed
if a and b:
m = "Comparing address generators '{}' and '{}'"
msg(green(m.format(g.key_generators[a-1],g.key_generators[b-1])))
from mmgen.addr import get_privhex2addr_f
gen_a = get_privhex2addr_f(selector=a)
gen_b = get_privhex2addr_f(selector=b)
compressed = False
for i in range(1,rounds+1):
msg_r('\rRound %s/%s ' % (i,rounds))
sec = hexlify(os.urandom(32))
wif = hex2wif(sec,compressed=compressed)
a_addr = gen_a(sec,compressed)
b_addr = gen_b(sec,compressed)
vmsg('\nkey: %s\naddr: %s\n' % (wif,a_addr))
if a_addr != b_addr:
match_error(sec,wif,a_addr,b_addr,a,b)
if a != 2 and b != 2:
compressed = not compressed
msg(green(('\n','')[bool(opt.verbose)] + 'OK'))
msg(green(('\n','')[bool(opt.verbose)] + 'OK'))
elif a and not fh:
m = "Testing speed of address generator '{}'"
msg(green(m.format(g.key_generators[a-1])))
from mmgen.addr import get_privhex2addr_f
gen_a = get_privhex2addr_f(selector=a)
import time
start = time.time()
from struct import pack,unpack
seed = os.urandom(28)
print 'Incrementing key with each round'
print 'Starting key:', hexlify(seed+pack('I',0))
compressed = False
for i in range(rounds):
if not opt.quiet: msg_r('\rRound %s/%s ' % (i+1,rounds))
sec = hexlify(seed+pack('I',i))
wif = hex2wif(sec,compressed=compressed)
a_addr = gen_a(sec,compressed)
vmsg('\nkey: %s\naddr: %s\n' % (wif,a_addr))
if a != 2:
compressed = not compressed
elapsed = int(time.time() - start)
if not opt.quiet: msg('')
msg('%s addresses generated in %s second%s' % (rounds,elapsed,('s','')[elapsed==1]))
elif a and dump:
m = "Comparing output of address generator '{}' against wallet dump '{}'"
msg(green(m.format(g.key_generators[a-1],cmd_args[1])))
if a == 2:
msg("NOTE: for compressed addresses, 'python-ecdsa' generator will be used")
from mmgen.addr import get_privhex2addr_f
gen_a = get_privhex2addr_f(selector=a)
from mmgen.bitcoin import wif2hex
for n,[wif,a_addr] in enumerate(dump,1):
msg_r('\rKey %s/%s ' % (n,len(dump)))
sec = wif2hex(wif)
compressed = wif[0] != ('5','9')[g.testnet]
b_addr = gen_a(sec,compressed)
if a_addr != b_addr:
match_error(sec,wif,a_addr,b_addr,1 if compressed and a==2 else a,4)
msg(green(('\n','')[bool(opt.verbose)] + 'OK'))

View file

@ -0,0 +1,6 @@
9cc19b
test.py ref. wallet (pw 'abc', seed len 256)
98831f3a e2687906 256 NE 20161110_135346
1: 12 8 1
70413d 74ev zjeq Zw2g DspF RKpE 7H
7c26e6 1otd mVTn 5MCR cDTF sZqY uNKA rsAm mjTw EJmS yzwX ZPJd

View file

@ -0,0 +1,19 @@
# MMGen address file
#
# This file is editable.
# Everything following a hash symbol '#' is a comment and ignored by MMGen.
# A text label of 32 characters or less may be added to the right of each
# address, and it will be appended to the bitcoind wallet label upon import.
# The label may contain any printable ASCII symbol.
# Address data checksum for 98831F3A[1,31-33,500-501,1010-1011]: 3C2C 8558 BB54 079E
# Record this value to a secure location.
98831F3A {
1 n1z4XgmpMzaZJ9Ywjefnv7yrPc2w4h6UxL
31 mfqYRquF5Uw9YCKGoqh7tRqfYQQhdAQi5Q
32 mp31EPM2a8evsuZDYPiPUmA5ChwCLTr9x1
33 mmdqJpHeV1nmVpBmebGyR4Ziu2chE1gc43
500 mu5LCgiNbbzWMod2DAFJpqfwF3vMSGPdnb
501 mtxxVcPsLb237x4tQpiztcE2jHFVWdSs8d
1010 mtVqD1xmEBVMijdDXpfidbF4otQsLzmNxi
1011 msj54iM9CYCtpvcGnb8fz54hG23fQPuueN
}

View file

@ -0,0 +1,6 @@
3c0e60
FFB367 1.234 20150405_102927 350828
01000000013364630b6d290a82c822facc2f7c1db4452cea459b2ce22371135530485a5d010600000000ffffffff0205d7d600010000001976a914bba3993079ccdf40c9bbbe495473f0b3d2dc5eec88ac40ef5a07000000001976a914abe58e1e45f6176910a4c1ac1ee62328d5cc4fd588ac00000000
[{'label': u'Test Wallet', 'mmid': u'98831F3A:500', 'vout': 6, 'txid': u'015d5a483055137123e22c9b45ea2c45b41d7c2fccfa22c8820a296d0b636433', 'amt': BTCAmt('44.32452045'), 'confs': 495L, 'addr': u'mu5LCgiNbbzWMod2DAFJpqfwF3vMSGPdnb', 'scriptPubKey': '76a91494b93bbe8a32f1db80b307482e83c25fa4e99b8c88ac'}]
[{'amt': BTCAmt('43.09047045'), 'mmid': '98831F3A:3', 'addr': u'mxd6dwbbhg4g7tqGxwQ4pueajob7kjWHBG'}, {'amt': BTCAmt('1.23400000'), 'mmid': '98831F3A:2', 'addr': u'mwBrqdQGfj4yH6594qAzZVqmYfLdmB1C7W'}]
TvwWgaAnrkQFpAxxjBa4PHvJ8NsJDsurtiv2HuzdnXWjQmY7LHyt6PZn5J7BNtB5VzHtBG7bUosCAMFon8yxUe2mYTZoH9e6dpoAz9E6JDZtUNYz9YnF1Z3jFND1X89RuKAk6YVBrfWseeyHR8vZDdaFzBPK5SPos

View file

@ -45,6 +45,7 @@ sys.path.__setitem__(0,os.path.abspath(os.curdir))
# Import these _after_ local path's been added to sys.path
from mmgen.common import *
from mmgen.test import *
tn_desc = ('','.testnet')[g.testnet]
start_mscolor()
@ -175,8 +176,8 @@ cfgs = {
'seed_len': 128,
'seed_id': 'FE3C6545',
'ref_bw_seed_id': '33F10310',
'addrfile_chk': 'B230 7526 638F 38CB',
'keyaddrfile_chk': 'CF83 32FB 8A8B 08E2',
'addrfile_chk': ('B230 7526 638F 38CB','B64D 7327 EF2A 60FE')[g.testnet],
'keyaddrfile_chk': ('CF83 32FB 8A8B 08E2','FEBF 7878 97BB CC35')[g.testnet],
'wpasswd': 'reference password',
'ref_wallet': 'FE3C6545-D782B529[128,1].mmdat',
'ic_wallet': 'FE3C6545-E29303EA-5E229E30[128,1].mmincog',
@ -201,8 +202,8 @@ cfgs = {
'seed_len': 192,
'seed_id': '1378FC64',
'ref_bw_seed_id': 'CE918388',
'addrfile_chk': '8C17 A5FA 0470 6E89',
'keyaddrfile_chk': '9648 5132 B98E 3AD9',
'addrfile_chk': ('8C17 A5FA 0470 6E89','0A59 C8CD 9439 8B81')[g.testnet],
'keyaddrfile_chk': ('9648 5132 B98E 3AD9','2F72 C83F 44C5 0FAC')[g.testnet],
'wpasswd': 'reference password',
'ref_wallet': '1378FC64-6F0F9BB4[192,1].mmdat',
'ic_wallet': '1378FC64-2907DE97-F980D21F[192,1].mmincog',
@ -227,17 +228,17 @@ cfgs = {
'seed_len': 256,
'seed_id': '98831F3A',
'ref_bw_seed_id': 'B48CD7FC',
'addrfile_chk': '6FEF 6FB9 7B13 5D91',
'keyaddrfile_chk': '9F2D D781 1812 8BAD',
'addrfile_chk': ('6FEF 6FB9 7B13 5D91','3C2C 8558 BB54 079E')[g.testnet],
'keyaddrfile_chk': ('9F2D D781 1812 8BAD','7410 8F95 4B33 B4B2')[g.testnet],
'wpasswd': 'reference password',
'ref_wallet': '98831F3A-27F2BF93[256,1].mmdat',
'ref_addrfile': '98831F3A[1,31-33,500-501,1010-1011].addrs',
'ref_keyaddrfile': '98831F3A[1,31-33,500-501,1010-1011].akeys.mmenc',
'ref_addrfile_chksum': '6FEF 6FB9 7B13 5D91',
'ref_keyaddrfile_chksum': '9F2D D781 1812 8BAD',
'ref_wallet': '98831F3A-{}[256,1].mmdat'.format(('27F2BF93','E2687906')[g.testnet]),
'ref_addrfile': '98831F3A[1,31-33,500-501,1010-1011]{}.addrs'.format(tn_desc),
'ref_keyaddrfile': '98831F3A[1,31-33,500-501,1010-1011]{}.akeys.mmenc'.format(tn_desc),
'ref_addrfile_chksum': ('6FEF 6FB9 7B13 5D91','3C2C 8558 BB54 079E')[g.testnet],
'ref_keyaddrfile_chksum': ('9F2D D781 1812 8BAD','7410 8F95 4B33 B4B2')[g.testnet],
# 'ref_fake_unspent_data':'98831F3A_unspent.json',
'ref_tx_file': 'FFB367[1.234].rawtx',
'ref_tx_file': 'FFB367[1.234]{}.rawtx'.format(tn_desc),
'ic_wallet': '98831F3A-5482381C-18460FB1[256,1].mmincog',
'ic_wallet_hex': '98831F3A-1630A9F2-870376A9[256,1].mmincox',
@ -483,6 +484,7 @@ opts_data = {
-n, --names Display command names instead of descriptions.
-I, --non-interactive Non-interactive operation (MS Windows mode)
-p, --pause Pause between tests, resuming on keypress.
-P, --profile Record the execution time of each script.
-q, --quiet Produce minimal output. Suppress dependency info.
-r, --resume=c Resume at command 'c' after interrupted run
-s, --system Test scripts and modules installed on system rather
@ -500,6 +502,7 @@ If no command is given, the whole suite of tests is run.
cmd_args = opts.init(opts_data)
if opt.profile: opt.names = True
if opt.resume: opt.skip_deps = True
if opt.log:
log_fd = open(log_file,'a')
@ -1040,7 +1043,10 @@ class MMGenTestSuite(object):
else:
return
if opt.profile: start = time.time()
self.__class__.__dict__[cmd](*([self,cmd] + al))
if opt.profile:
msg('\r\033[50C{:.4f}'.format(time.time() - start))
def generate_file_deps(self,cmd):
return [(str(n),e) for exts,n in cmd_data[cmd][2] for e in exts]
@ -1410,6 +1416,7 @@ class MMGenTestSuite(object):
return
t.expect('Encrypt key list? (y/N): ','y')
t.hash_preset('new key list','1')
# t.passphrase_new('new key list','kafile password')
t.passphrase_new('new key list',cfg['kapasswd'])
t.written_to_file('Secret keys',oo=True)
ok()
@ -1558,27 +1565,6 @@ class MMGenTestSuite(object):
os.unlink(f1)
cmp_or_die(hincog_offset,int(o))
# def pywallet(self,name): # TODO - check output
# pf = get_tmpfile_fn(cfg,pwfile)
# write_data_to_file(pf,cfg['wpasswd']+'\n',silent=True)
# args = ([],['-q','-P',pf])[ni]
# unenc_wf = os.path.join(ref_dir,'wallet-unenc.dat')
# enc_wf = os.path.join(ref_dir,'wallet-enc.dat')
# for wf,enc in (unenc_wf,False),(enc_wf,True):
# for w,o,pk in (
# ('addresses','a',False),
# ('private keys','k',True),
# ('json dump','j',True)
# ):
# ed = '(%sencrypted wallet, %s)' % (('un','')[bool(enc)],w)
# t = MMGenExpect(name,'mmgen-pywallet', args +
# ['-'+o,'-d',cfg['tmpdir']] + [wf], extra_desc=ed)
# if ni: continue
# if pk and enc and not ni:
# t.expect('Enter password: ',cfg['wpasswd']+'\n')
# t.written_to_file(capfirst(w),oo=True)
# if not ni: ok()
# Saved reference file tests
def ref_wallet_conv(self,name):
wf = os.path.join(ref_dir,cfg['ref_wallet'])
@ -1685,6 +1671,7 @@ class MMGenTestSuite(object):
m = grnbg("Answer 'y' at the interactive prompt if Seed ID is")
n = cyan(cfg['seed_id'])
msg('\n%s %s' % (m,n))
if wtype == 'hic_wallet_old' and opt.profile: msg('')
t = MMGenExpect(name,'mmgen-walletchk',
add_args + slarg + hparg + of_arg + ic_arg,
extra_desc=edesc)
@ -1789,6 +1776,7 @@ class MMGenTestSuite(object):
t.close()
ok()
# back check of result
if opt.profile: msg('')
self.walletchk(name,wf,pf=None,
desc='mnemonic data',
sid=cfg['seed_id'],
@ -1844,6 +1832,7 @@ class MMGenTestSuite(object):
if desc == 'hidden incognito data':
add_args += uopts_chk
wf = None
if opt.profile: msg('')
self.walletchk(name,wf,pf=pf,
desc=desc,sid=cfg['seed_id'],pw=pw,
add_args=add_args,

View file

@ -124,7 +124,7 @@ If no command is given, the whole suite of tests is run.
"""
}
cmd_args = opts.init(opts_data,add_opts=['exact_output'])
cmd_args = opts.init(opts_data,add_opts=['exact_output','profile'])
if opt.system: sys.path.pop(0)