TX script improvements, added features (tx's with both Satoshi and MMGen inputs)

modified:   mmgen-addrgen
	modified:   mmgen-addrimport
	modified:   mmgen-txcreate
	modified:   mmgen-txsend
	modified:   mmgen-txsign
	modified:   mmgen-walletchk
	modified:   mmgen-walletgen
	modified:   mmgen/addr.py
	modified:   mmgen/config.py
	modified:   mmgen/tx.py
	modified:   mmgen/utils.py
This commit is contained in:
The MMGen Project 2013-12-09 11:20:28 +04:00
commit 7cfca299a3
11 changed files with 257 additions and 97 deletions

View file

@ -72,9 +72,12 @@ Address range may be a single number or a range in the form XXX-YYY{}
If available, external 'keyconv' program will be used for address generation
Data for the --from-<what> options will be taken from <infile> if <infile>
is specified. Otherwise, the user will be prompted to enter the data. Note
that passphrase data in a file may be arranged in free-form fashion, using
any combination of spaces, tabs or newlines to separate words
is specified. Otherwise, the user will be prompted to enter the data.
For passphrases all combinations of whitespace are equal, and leading and
trailing space are ignored. This permits reading passphrase data from a
multi-line file with free spacing and indentation. This is particularly
convenient for long brainwallet passphrases, for example.
BRAINWALLET NOTE:
@ -158,7 +161,7 @@ else:
seed = get_seed(infile,opts)
seed_id = make_chksum_8(seed)
addr_data = generate_addrs(seed, start, end, opts)
addr_data = generate_addrs(seed, range(start, end+1), opts)
addr_data_str = format_addr_data(addr_data, seed_id, opts)
# Output data:

View file

@ -50,7 +50,12 @@ check_infile(cmd_args[0])
seed_id,addr_data = parse_addrs_file(cmd_args[0])
from mmgen.tx import connect_to_bitcoind
from mmgen.tx import check_wallet_addr_label,connect_to_bitcoind
for i in addr_data:
i[2] = " ".join(i[2:]) if len(i) > 2 else ""
check_wallet_addr_label(i[2])
c = connect_to_bitcoind(http_timeout=3600)
message = """
@ -60,8 +65,7 @@ low-powered computer such as a netbook.
confirm_or_exit(message, "continue", expect="YES")
for n,i in enumerate(addr_data):
comment = " " + " ".join(i[2:]) if len(i) > 2 else ""
label = "%s:%s%s" % (seed_id,str(i[0]),comment)
label = "%s:%s %s" % (seed_id,str(i[0]),i[2])
msg("Importing %-6s %-34s (%s)" % (
("%s/%s:" % (n+1,len(addr_data))),
i[1], label)

View file

@ -94,7 +94,30 @@ msg("Total amount to spend: %s BTC" % send_amt)
msg("%s unspent outputs total" % len(unspent))
while True:
sel_unspent = select_outputs(unspent,"Choose the outputs to spend: ")
sel_nums = select_outputs(unspent,"Choose the outputs to spend: ")
sel_unspent = [unspent[i] for i in sel_nums]
mmgen_sel,other_sel = [],[]
for i in sel_nums:
if verify_mmgen_label(unspent[i].account):
mmgen_sel.append(i)
else:
other_sel.append(i)
if mmgen_sel and other_sel:
keygen_args = [unspent[i].account.split()[0][9:] for i in mmgen_sel]
msg("""
NOTE: This transaction uses a mixture of both mmgen and non-mmgen inputs,
which makes the signing process more complicated. When signing the
transaction, keys for the non-mmgen inputs must be supplied in a separate
file using mmgen-txsign's '-k' option. Alternatively, you may import the
mmgen keys into the wallet.dat of your offline bitcoind, first running
mmgen-keygen with address list '%s' to generate the keys. Finally, run
mmgen-txsign with the '-f' option to force the use of wallet.dat as the
key source.
""".strip() % ",".join(sorted(keygen_args)))
if not user_confirm("Accept?"):
continue
total_in = trim_exponent(sum([o.amount for o in sel_unspent]))
change = trim_exponent(total_in - (send_amt + tx_fee))
@ -102,8 +125,9 @@ while True:
prompt = "Transaction produces %s BTC in change. OK?" % change
if user_confirm(prompt,default_yes=True):
break
else:
msg(txmsg['not_enough_btc'] % change)
msg(txmsg['not_enough_btc'] % change)
if change > 0 and not change_addr:
msg(txmsg['throwaway_change'] % (change, total_in-tx_fee))

View file

@ -74,7 +74,7 @@ except:
if not 'quiet' in opts: do_license_msg()
msg("Signed transaction file '%s' appears valid" % infile)
msg("\nSigned transaction file '%s' appears valid" % infile)
warn = "Once this transaction is sent, there's no taking it back!"
what = "broadcast this transaction to the network"

View file

@ -26,47 +26,98 @@ from mmgen.Opts import *
from mmgen.license import *
from mmgen.config import *
from mmgen.tx import *
from mmgen.utils import check_opts, msg, user_confirm, check_infile, get_lines_from_file, my_getpass, my_raw_input
prog_name = sys.argv[0].split("/")[-1]
from mmgen.utils import *
help_data = {
'prog_name': prog_name,
'prog_name': sys.argv[0].split("/")[-1],
'desc': "Sign a Bitcoin transaction generated by mmgen-txcreate",
'usage': "[opts] <transaction file>",
'usage': "[opts] <transaction file> [mmgen wallet/seed/mnemonic file]",
'options': """
-h, --help Print this help message
-d, --outdir d Specify an alternate directory 'd' for output
-e, --echo-passphrase Print passphrase to screen when typing it
-f, --force-wallet-dat Force the use of wallet.dat as a key source
-i, --info Display information about the transaction and exit
-k, --keys-from-file k Provide additional key data from file 'k'
-q, --quiet Suppress warnings; overwrite files without asking
"""
-b, --from-brain l,p Generate keys from a user-created password,
i.e. a "brainwallet", using seed length 'l' and
hash preset 'p' (comma-separated)
-m, --from-mnemonic Generate keys from an electrum-like mnemonic
-s, --from-seed Generate keys from a seed in .{} format
Data for the --from-<what> options will be taken from a file if a second
file is specified on the command line. Otherwise, the user will be
prompted to enter the data.
""".format(seed_ext)
}
short_opts = "hd:eiq"
long_opts = "help","outdir=","echo_passphrase","info","quiet"
short_opts = "hd:efik:qb:ms"
long_opts = "help","outdir=","echo_passphrase","force_wallet_dat","info",\
"keys_from_file=","quiet","from_brain=","from_mnemonic","from_seed"
opts,cmd_args = process_opts(sys.argv,help_data,short_opts,long_opts)
# Exits on invalid input
check_opts(opts, ('outdir',))
check_opts(opts, ('outdir','from_brain'))
if 'keys_from_file' in opts: check_infile(opts['keys_from_file'])
if debug:
print "Processed options: %s" % repr(opts)
print "Cmd args: %s" % repr(cmd_args)
if len(cmd_args) == 1:
infile = cmd_args[0]
check_infile(infile)
if len(cmd_args) in (1,2):
tx_file = cmd_args[0]
check_infile(tx_file)
else: usage(help_data)
# Begin execution
c = connect_to_bitcoind()
tx_data = get_lines_from_file(infile,"transaction data")
tx_data = get_lines_from_file(tx_file,"transaction data")
metadata,tx_hex,sig_data,inputs_data = parse_tx_data(tx_data)
metadata,tx_hex,sig_data,inputs_data = parse_tx_data(tx_data,tx_file)
# Are inputs mmgen addresses?
infile,mmgen_addrs,other_addrs = "",[],[]
# Check that all the seed IDs are the same:
for i in inputs_data:
if verify_mmgen_label(i['account']):
mmgen_addrs.append(i)
else:
other_addrs.append(i)
if mmgen_addrs:
a_ids = list(set([i['account'][:8] for i in mmgen_addrs]))
if len(a_ids) != 1:
msg("Addresses come from different seeds! (%s)" % " ".join(a_ids))
sys.exit(3)
if len(cmd_args) == 2:
infile = cmd_args[1]
else:
if "from_brain" in opts \
or "from_mnemonic" in opts \
or "from_seed" in opts:
infile = ""
else:
msg("Inputs contain mmgen addresses. An MMGen wallet file must be specified on the command line (or use the '-b', '-m' or '-s' options).".strip())
sys.exit(2)
if other_addrs:
if 'keys_from_file' in opts:
add_keys = get_lines_from_file(opts['keys_from_file'])
else:
msg("""
A key file must be supplied (option '-f') for the following non-mmgen
address%s: %s""" % (
"" if len(other_addrs) == 1 else "es",
" ".join([i['address'] for i in other_addrs])
))
sys.exit(2)
if 'info' in opts:
view_tx_data(c,inputs_data,tx_hex,metadata)
@ -74,46 +125,64 @@ if 'info' in opts:
if not 'quiet' in opts and not 'info' in opts: do_license_msg()
msg("Successfully opened transaction file '%s'" % infile)
msg("Successfully opened transaction file '%s'" % tx_file)
if user_confirm("View transaction data? ",default_yes=False):
view_tx_data(c,inputs_data,tx_hex,metadata)
prompt = "Enter passphrase for bitcoind wallet: "
if 'echo_passphrase' in opts:
password = my_raw_input(prompt)
if mmgen_addrs:
seed = get_seed(infile,opts)
seed_id = make_chksum_8(seed)
if seed_id != a_ids[0]:
msg("Seed ID of wallet (%s) doesn't match that of addresses (%s)"
% (seed_id,a_ids[0]))
sys.exit(3)
addr_nums = [int(i['account'].split()[0][9:]) for i in mmgen_addrs]
from mmgen.addr import generate_addrs
o = {'no_addresses': True, 'gen_what': "keys"}
keys = [i['wif'] for i in generate_addrs(seed, addr_nums, o)]
if other_addrs: keys += add_keys
try:
sig_tx = c.signrawtransaction(tx_hex,sig_data,keys)
except:
msg("Failed to sign transaction")
sys.exit(3)
else:
password = my_getpass(prompt)
prompt = "Enter passphrase for bitcoind wallet: "
if 'echo_passphrase' in opts:
password = my_raw_input(prompt)
else:
password = my_getpass(prompt)
wallet_enc = True
from bitcoinrpc import exceptions
wallet_enc = True
from mmgen.rpc import exceptions
try:
c.walletpassphrase(password, 9999)
except exceptions.WalletWrongEncState:
msg("Wallet is unencrypted")
wallet_enc = False
except exceptions.WalletPassphraseIncorrect:
msg("Passphrase incorrect")
sys.exit(3)
except exceptions.WalletAlreadyUnlocked:
msg("WARNING: Wallet already unlocked!")
else:
msg("Passphrase OK")
try:
c.walletpassphrase(password, 9999)
except exceptions.WalletWrongEncState:
msg("Wallet is unencrypted")
wallet_enc = False
except exceptions.WalletPassphraseIncorrect:
msg("Passphrase incorrect")
sys.exit(3)
except exceptions.WalletAlreadyUnlocked:
msg("WARNING: Wallet already unlocked!")
else:
msg("Passphrase OK")
try:
sig_tx = c.signrawtransaction(tx_hex,sig_data)
except:
msg("Failed to sign transaction")
if wallet_enc:
c.walletlock()
msg("Locking wallet")
sys.exit(3)
try:
sig_tx = c.signrawtransaction(tx_hex,sig_data)
# sig_tx = {'hex':"deadbeef0123456789ab",'complete':True}
except:
msg("Failed to sign transaction")
if wallet_enc:
c.walletlock()
msg("Locking wallet")
sys.exit(3)
if wallet_enc:
c.walletlock()
msg("Locking wallet")
if sig_tx['complete']:
msg("Signing completed")

View file

@ -21,11 +21,12 @@ mmgen-walletchk: Check integrity of a mmgen deterministic wallet, display
"""
import sys
import mmgen.Opts as Opts
from mmgen.Opts import *
from mmgen.utils import *
help_data = {
'prog_name': sys.argv[0].split("/")[-1],
'desc': """Check integrity of a %s deterministic wallet, display
its information and export seed and mnemonic data."""\
% proj_name,
@ -45,7 +46,7 @@ short_opts = "hd:emsSv"
long_opts = "help","outdir=","echo_passphrase","export_mnemonic",\
"export_seed","stdout","verbose"
opts,cmd_args = Opts.process_opts(sys.argv,help_data,short_opts,long_opts)
opts,cmd_args = process_opts(sys.argv,help_data,short_opts,long_opts)
check_opts(opts, ('outdir',))

View file

@ -59,8 +59,11 @@ By default (i.e. when invoked without any of the '--from-<what>' options),
Data for the --from-<what> options will be taken from <infile> if <infile>
is specified. Otherwise, the user will be prompted to enter the data.
Note that passphrase data may be input in free-form fashion, using any
combination of spaces or tabs (or newlines, in a file) between words.
For passphrases all combinations of whitespace are equal, and leading and
trailing space are ignored. This permits reading passphrase data from a
multi-line file with free spacing and indentation. This is particularly
convenient for long brainwallet passphrases, for example.
BRAINWALLET NOTE:

View file

@ -24,6 +24,7 @@ from hashlib import sha256, sha512
from binascii import hexlify, unhexlify
from mmgen.bitcoin import numtowif
from mmgen.config import *
def test_for_keyconv():
"""
@ -47,15 +48,15 @@ address generation.
return True
def generate_addrs(seed, start, end, opts):
def generate_addrs(seed, addrnums, opts):
"""
generate_addresses(start, end, seed, opts) => None
Generate a series of Bitcoin addresses from start to end based on a
seed, optionally outputting secret keys
Generate a Bitcoin address or addresses end based on a seed, optionally
outputting secret keys
The 'keyconv' utility will be used for address generation if
installed. Otherwise an internal function is used
The 'keyconv' utility will be used for address generation if installed.
Otherwise an internal function is used
Supported options:
print_secret, no_addresses, no_keyconv, gen_what
@ -73,17 +74,16 @@ def generate_addrs(seed, start, end, opts):
from subprocess import Popen, PIPE
keyconv = "keyconv"
total_addrs = end - start + 1
a,t_addrs,i,out = sorted(addrnums),len(addrnums),0,[]
addrlist = []
while a:
seed = sha512(seed).digest(); i += 1 # round /i/
if i < a[0]: continue
for i in range(1, end+1):
seed = sha512(seed).digest() # round /i/
a.pop(0)
if i < start: continue
sys.stderr.write("\rGenerating %s: %s of %s" %
(opts['gen_what'], (i-start)+1, total_addrs))
sys.stderr.write("\rGenerating %s %s (%s of %s)" %
(opts['gen_what'], i, t_addrs-len(a), t_addrs))
# Secret key is double sha256 of seed hash round /i/
sec = sha256(sha256(seed).digest()).hexdigest()
@ -98,18 +98,18 @@ def generate_addrs(seed, start, end, opts):
if not 'no_addresses' in opts:
if keyconv:
p = Popen([keyconv, wif], stdout=PIPE)
addr = dict([j.split() for j in p.stdout.readlines()])['Address:']
addr = dict([j.split() for j in \
p.stdout.readlines()])['Address:']
else:
addr = privnum2addr(int(sec,16))
el['addr'] = addr
addrlist.append(el)
out.append(el)
sys.stderr.write("\rGenerated %s %s-%s%s\n" %
(opts['gen_what'], start, end, " "*9))
sys.stderr.write("\rGenerated %s %s%s\n"%(t_addrs,opts['gen_what']," "*15))
return addrlist
return out
def format_addr_data(addrlist, seed_chksum, opts):
@ -139,7 +139,16 @@ def format_addr_data(addrlist, seed_chksum, opts):
(5 if 'print_secret' in opts else 1) + len(wif_msg)
)
data = []
header = """
# MMGen address file
#
# This file is editable.
# Everything following a hash symbol '#' is ignored.
# A label may be added to the right of each address, and it will be
# appended to the bitcoind wallet label upon import (max. 24 characters,
# allowed characters: A-Za-z0-9, plus '{}').
""".format("', '".join(wallet_addr_label_symbols)).strip()
data = [header + "\n"]
data.append("%s {" % seed_chksum.upper())
for el in addrlist:

View file

@ -48,3 +48,4 @@ hash_presets = {
'4': [15, 8, 12],
'5': [16, 8, 16],
}
wallet_addr_label_symbols = ".","_",",","-"," "

View file

@ -20,7 +20,7 @@ tx.py: Bitcoin transaction routines
"""
from binascii import unhexlify
from mmgen.utils import msg,msg_r,write_to_file,my_raw_input,get_char,make_chksum_8,make_timestamp
from mmgen.utils import *
import sys, os
from decimal import Decimal
from mmgen.config import *
@ -110,7 +110,7 @@ def get_cfg_options(cfg_keys):
def print_tx_to_file(tx,sel_unspent,send_amt,opts):
sig_data = [{"txid":i.txid,"vout":i.vout,"scriptPubKey":i.scriptPubKey}
for i in sel_unspent]
tx_id = make_chksum_8(unhexlify(tx))
tx_id = make_chksum_6(unhexlify(tx)).upper()
outfile = "%s[%s].tx" % (tx_id,send_amt)
if 'outdir' in opts:
outfile = "%s/%s" % (opts['outdir'], outfile)
@ -124,7 +124,7 @@ def print_tx_to_file(tx,sel_unspent,send_amt,opts):
def print_signed_tx_to_file(tx,sig_tx,metadata,opts):
tx_id = make_chksum_8(unhexlify(tx))
tx_id = make_chksum_6(unhexlify(tx)).upper()
outfile = "{}[{}].txsig".format(*metadata[:2])
if 'outdir' in opts:
outfile = "%s/%s" % (opts['outdir'], outfile)
@ -148,10 +148,11 @@ def sort_and_view(unspent):
return cmp("%s %03s" % (a.txid,a.vout), "%s %03s" % (b.txid,b.vout))
def s_addr(a,b): return cmp(a.address,b.address)
def s_age(a,b): return cmp(b.confirmations,a.confirmations)
def s_mmgen(a,b): return cmp(a.account,b.account)
fs = " %-4s %-11s %-2s %-34s %13s %-s"
fs_hdr = " %-4s %-11s %-4s %-35s %-9s %-s"
sort,group,reverse = "",False,False
sort,group,mmaddr,reverse = "",False,False,False
from copy import deepcopy
msg("")
@ -178,7 +179,16 @@ def sort_and_view(unspent):
for n,i in enumerate(out):
amt = str(trim_exponent(i.amount))
fill = 8 - len(amt.split(".")[-1]) if "." in amt else 9
addr = " |" + "-"*32 if i.skip == "d" else i.address
if i.skip == "d":
addr = " |" + "-"*32
else:
if mmaddr:
if i.account and verify_mmgen_label(i.account):
addr = "%s.. %s" % (i.address[:4],i.account)
else:
addr = i.address
else:
addr = i.address
txid = " |---" if i.skip == "t" else i.txid[:8]+"..."
days = int(i.confirmations * mins_per_block / (60*24))
@ -187,24 +197,44 @@ def sort_and_view(unspent):
while True:
reply = get_char("\n".join(output) +
"""\n
Sort options: [t]xid, [a]mount, a[d]dress, [A]ge, [r]everse, [g]roup
Sort options: [t]xid, [a]mount, a[d]dress, [A]ge, [r]everse, [M]mgen addr
View options: [g]roup, show [m]mgen addr
(Type 'q' to quit sorting): """).strip()
if reply == 'a': unspent.sort(s_amt); sort = "amount"; break
elif reply == 't': unspent.sort(s_txid); sort = "txid"; break
elif reply == 'd': unspent.sort(s_addr); sort = "address"; break
elif reply == 'A': unspent.sort(s_age); sort = "age"; break
elif reply == 'M': unspent.sort(s_mmgen); mmaddr,sort=True,"mmgen"; break
elif reply == 'r':
reverse = False if reverse else True
unspent.reverse()
break
elif reply == 'g': group = False if group else True; break
elif reply == 'm': mmaddr = False if mmaddr else True; break
elif reply == 'q': break
else: msg("Invalid input")
msg("\n")
if reply == 'q': break
return unspent
return tuple(unspent)
def verify_mmgen_label(s,return_str=False):
fail = "" if return_str else False
success = s if return_str else True
if not s: return fail
label = s.split()[0]
if label[8] != ':': return fail
for i in label[:8]:
if not i in "01234567890ABCDEF": return fail
for i in label[9:]:
if not i in "0123456789": return fail
return success
def view_tx_data(c,inputs_data,tx_hex,metadata=[]):
@ -226,10 +256,11 @@ def view_tx_data(c,inputs_data,tx_hex,metadata=[]):
msg(" " + """
%-2s tx,vout: %s,%s
address: %s
label: %s
amount: %s BTC
confirmations: %s (around %s days)
""".strip() %
(n+1,i['txid'],i['vout'],j['address'],
(n+1,i['txid'],i['vout'],j['address'],verify_mmgen_label(j['account'],True),
trim_exponent(j['amount']),j['confirmations'],days)+"\n")
break
@ -251,7 +282,7 @@ def view_tx_data(c,inputs_data,tx_hex,metadata=[]):
msg("TX fee: %s BTC\n" % trim_exponent(total_in-total_out))
def parse_tx_data(tx_data):
def parse_tx_data(tx_data,infile):
if len(tx_data) != 4:
msg("'%s': not a transaction file" % infile)
@ -288,7 +319,7 @@ def select_outputs(unspent,prompt):
while True:
reply = my_raw_input(prompt).strip()
if reply:
selected = ()
selected = []
try:
selected = [int(i) - 1 for i in reply.split()]
except: pass
@ -302,7 +333,7 @@ def select_outputs(unspent,prompt):
msg("'%s': Invalid input" % reply)
return [unspent[i] for i in selected]
return list(set(selected))
def make_tx_out(rcpt_arg):
@ -323,3 +354,18 @@ def make_tx_out(rcpt_arg):
sys.exit(3)
return tx_out
def check_wallet_addr_label(label):
if len(label) > 16:
msg("'%s': illegal label (length must be <= 16 characters)" % label)
sys.exit(3)
from string import ascii_letters, digits
chrs = tuple(ascii_letters + digits) + wallet_addr_label_symbols
for ch in list(label):
if ch not in chrs:
msg("'%s': illegal character in label '%s'" % (ch,label))
msg("Permitted characters: A-Za-z0-9, plus '%s'" %
"', '".join(wallet_addr_label_symbols))
sys.exit(3)

View file

@ -250,7 +250,7 @@ def make_chksum_8(s):
from hashlib import sha256
return sha256(sha256(s).digest()).hexdigest()[:8].upper()
def _make_chksum_6(s):
def make_chksum_6(s):
from hashlib import sha256
return sha256(s).hexdigest()[:6]
@ -438,7 +438,7 @@ def write_seed(seed, opts):
from mmgen.bitcoin import b58encode_pad
data = col4(b58encode_pad(seed))
chk = _make_chksum_6(b58encode_pad(seed))
chk = make_chksum_6(b58encode_pad(seed))
o = "%s %s\n" % (chk,data)
@ -522,11 +522,11 @@ def write_wallet_to_file(seed, passwd, key_id, salt, enc_seed, opts):
label,
"{} {} {} {} {}".format(*metadata),
"{}: {} {} {}".format(hash_preset,*_get_hash_params(hash_preset)),
"{} {}".format(_make_chksum_6(sf), col4(sf)),
"{} {}".format(_make_chksum_6(esf), col4(esf))
"{} {}".format(make_chksum_6(sf), col4(sf)),
"{} {}".format(make_chksum_6(esf), col4(esf))
)
chk = _make_chksum_6(" ".join(lines))
chk = make_chksum_6(" ".join(lines))
confirm = False if 'quiet' in opts else True
write_to_file(outfile, "\n".join((chk,)+lines)+"\n", confirm)
@ -593,7 +593,7 @@ def check_wallet_format(infile, lines, opts):
def _check_chksum_6(chk,val,desc,infile):
comp_chk = _make_chksum_6(val)
comp_chk = make_chksum_6(val)
if chk != comp_chk:
msg("%s checksum incorrect in file '%s'!" % (desc,infile))
msg("Checksum: %s. Computed value: %s" % (chk,comp_chk))
@ -682,7 +682,7 @@ def get_seed_from_seed_data(words):
stored_chk = words[0]
seed_b58 = "".join(words[1:])
chk = _make_chksum_6(seed_b58)
chk = make_chksum_6(seed_b58)
msg_r("Validating %s checksum..." % seed_ext)
if compare_checksums(chk, "from seed", stored_chk, "from input"):