mmgen-wallet/mmgen/txsign.py
philemon 3b0257358b
Support for Segwit (P2SH-P2WPKH) addresses:
- Generate Segwit addresses by invoking 'mmgen-addrgen' with the
  '--type segwit' option
- Import Segwit addresses into the tracking wallet as usual
- Segwit and legacy MMGen addresses are distinguished by 'S' and 'L'
  identifiers in the tracking wallet and command line

Transaction example:

  mmgen-txcreate F00BAA12:L:21,1.23 F00BAA12:S:1

(spend 1.23 BTC to legacy address 21 of your default wallet (with Seed ID
F00BAA12) and send the change to Segwit address 1)

Segwit and legacy addresses for a given seed are generated from different
sub-seeds so are cryptographically unrelated to each other.

Since MMGen's legacy P2PKH addresses are uncompressed, use of the new Segwit
addresses significantly reduces transaction size.

Until Segwit activation on mainnet, users can try out the new functionality on
testnet or in regtest mode.
2017-07-27 22:55:52 +03:00

188 lines
6.5 KiB
Python
Executable file

#!/usr/bin/env python
#
# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
# Copyright (C)2013-2017 Philemon <mmgen-py@yandex.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
txsign: Sign a transaction generated by 'mmgen-txcreate'
"""
from mmgen.common import *
from mmgen.seed import *
from mmgen.tx import *
from mmgen.addr import *
pnm = g.proj_name
txsign_notes = """
Transactions may contain both {pnm} or non-{pnm} input addresses.
To sign non-{pnm} inputs, a bitcoind wallet dump or flat key list is used
as the key source ('--keys-from-file' option).
To sign {pnm} inputs, key data is generated from a seed as with the
{pnl}-addrgen and {pnl}-keygen commands. Alternatively, a key-address file
may be used (--mmgen-keys-from-file option).
Multiple wallets or other seed files can be listed on the command line in
any order. If the seeds required to sign the transaction's inputs are not
found in these files (or in the default wallet), the user will be prompted
for seed data interactively.
To prevent an attacker from crafting transactions with bogus {pnm}-to-Bitcoin
address mappings, all outputs to {pnm} addresses are verified with a seed
source. Therefore, seed files or a key-address file for all {pnm} outputs
must also be supplied on the command line if the data can't be found in the
default wallet.
Seed source files must have the canonical extensions listed in the 'FileExt'
column below:
{f}
""".format(f='\n '.join(SeedSource.format_fmt_codes().splitlines()),
pnm=pnm,pnl=pnm.lower())
wmsg = {
'mapping_error': """
{pnm} -> BTC address mappings differ!
%-23s %s -> %s
%-23s %s -> %s
""".strip().format(pnm=pnm),
'missing_keys_error': """
ERROR: a key file must be supplied for the following non-{pnm} address%s:\n %s
""".format(pnm=pnm).strip()
}
saved_seeds = {}
def get_seed_for_seed_id(sid,infiles,saved_seeds):
if sid in saved_seeds:
return saved_seeds[sid]
while True:
if infiles:
ss = SeedSource(infiles.pop(0),ignore_in_fmt=True)
elif opt.in_fmt:
qmsg('Need seed data for Seed ID %s' % sid)
ss = SeedSource()
msg('User input produced Seed ID %s' % ss.seed.sid)
else:
die(2,'ERROR: No seed source found for Seed ID: %s' % sid)
saved_seeds[ss.seed.sid] = ss.seed
if ss.seed.sid == sid: return ss.seed
def generate_keys_for_mmgen_addrs(mmgen_addrs,infiles,saved_seeds):
sids = set(i.sid for i in mmgen_addrs)
vmsg('Need seed%s: %s' % (suf(sids,'s'),' '.join(sids)))
d = AddrListList()
from mmgen.addr import KeyAddrList
for sid in sids:
# Returns only if seed is found
seed = get_seed_for_seed_id(sid,infiles,saved_seeds)
for t in MMGenAddrType.mmtypes:
idx_list = [i.idx for i in mmgen_addrs if i.sid == sid and i.mmtype == t]
if idx_list:
addr_idxs = AddrIdxList(idx_list=idx_list)
d += KeyAddrList(seed=seed,addr_idxs=addr_idxs,do_chksum=False,mmtype=MMGenAddrType(t)).flat_list()
return d
def add_keys(tx,src,infiles=None,saved_seeds=None,keyaddr_list=None):
need_keys = [e for e in getattr(tx,src) if e.mmid and not e.have_wif]
if not need_keys: return []
desc,m1 = ('key-address file','From key-address file:') if keyaddr_list else \
('seed(s)','Generated from seed:')
qmsg('Checking {} -> BTC address mappings for {} (from {})'.format(pnm,src,desc))
d = keyaddr_list.flat_list() if keyaddr_list else \
generate_keys_for_mmgen_addrs([e.mmid for e in need_keys],infiles,saved_seeds)
new_keys = []
for e in need_keys:
for f in d:
if f.mmid == e.mmid:
if f.addr == e.addr:
e.have_wif = True
if src == 'inputs':
new_keys.append((f.addr,f.wif))
else:
die(3,wmsg['mapping_error'] % (m1,f.mmid,f.addr,'tx file:',e.mmid,e.addr))
if new_keys:
vmsg('Added %s wif key%s from %s' % (len(new_keys),suf(new_keys,'s'),desc))
return new_keys
def _pop_and_return(args,cmplist): # strips found args
return list(reversed([args.pop(args.index(a)) for a in reversed(args) if get_extension(a) in cmplist]))
def get_tx_files(opt,args):
ret = _pop_and_return(args,[MMGenTX.raw_ext])
if not ret: die(1,'You must specify a raw transaction file!')
return ret
def get_seed_files(opt,args):
# favor unencrypted seed sources first, as they don't require passwords
u,e = SeedSourceUnenc,SeedSourceEnc
ret = _pop_and_return(args,u.get_extensions())
from mmgen.filename import find_file_in_dir
wf = find_file_in_dir(Wallet,g.data_dir) # Make this the first encrypted ss in the list
if wf: ret.append(wf)
ret += _pop_and_return(args,e.get_extensions())
if not (ret or opt.mmgen_keys_from_file or opt.keys_from_file): # or opt.use_wallet_dat
die(1,'You must specify a seed or key source!')
return ret
def get_keyaddrlist(opt):
if opt.mmgen_keys_from_file:
return KeyAddrList(opt.mmgen_keys_from_file)
return None
def get_keylist(opt):
if opt.keys_from_file:
l = get_lines_from_file(opt.keys_from_file,'key-address data',trim_comments=True)
ret = KeyAddrList(keylist=[m.split()[0] for m in l]) # accept bitcoind wallet dumps
ret.generate_addrs_from_keylist()
return ret
return None
def txsign(opt,c,tx,seed_files,kl,kal,tx_num_str=''):
# Start
keys = []
# tx.pmsg()
non_mm_addrs = tx.get_non_mmaddrs('inputs')
if non_mm_addrs:
tmp = KeyAddrList(addrlist=non_mm_addrs,do_chksum=False)
tmp.add_wifs(kl)
m = tmp.list_missing('wif')
if m: die(2,wmsg['missing_keys_error'] % (suf(m,'es'),'\n '.join(m)))
keys += tmp.get_addr_wif_pairs()
if opt.mmgen_keys_from_file:
keys += add_keys(tx,'inputs',keyaddr_list=kal)
add_keys(tx,'outputs',keyaddr_list=kal)
keys += add_keys(tx,'inputs',seed_files,saved_seeds)
add_keys(tx,'outputs',seed_files,saved_seeds)
tx.delete_attrs('inputs','have_wif')
tx.delete_attrs('outputs','have_wif')
extra_sids = set(saved_seeds) - tx.get_input_sids() - tx.get_output_sids()
if extra_sids:
msg('Unused Seed ID{}: {}'.format(suf(extra_sids,'s'),' '.join(extra_sids)))
if tx.sign(c,tx_num_str,dict(keys)):
return tx
else:
die(3,'failed\nSome keys were missing. Transaction {}could not be signed.'.format(tx_num_str))