txsign.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. #!/usr/bin/env python
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C)2013-2017 Philemon <mmgen-py@yandex.com>
  5. #
  6. # This program is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation, either version 3 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. """
  19. txsign: Sign a transaction generated by 'mmgen-txcreate'
  20. """
  21. from mmgen.common import *
  22. from mmgen.seed import *
  23. from mmgen.tx import *
  24. from mmgen.addr import *
  25. pnm = g.proj_name
  26. txsign_notes = """
  27. Transactions may contain both {pnm} or non-{pnm} input addresses.
  28. To sign non-{pnm} inputs, a bitcoind wallet dump or flat key list is used
  29. as the key source ('--keys-from-file' option).
  30. To sign {pnm} inputs, key data is generated from a seed as with the
  31. {pnl}-addrgen and {pnl}-keygen commands. Alternatively, a key-address file
  32. may be used (--mmgen-keys-from-file option).
  33. Multiple wallets or other seed files can be listed on the command line in
  34. any order. If the seeds required to sign the transaction's inputs are not
  35. found in these files (or in the default wallet), the user will be prompted
  36. for seed data interactively.
  37. To prevent an attacker from crafting transactions with bogus {pnm}-to-Bitcoin
  38. address mappings, all outputs to {pnm} addresses are verified with a seed
  39. source. Therefore, seed files or a key-address file for all {pnm} outputs
  40. must also be supplied on the command line if the data can't be found in the
  41. default wallet.
  42. Seed source files must have the canonical extensions listed in the 'FileExt'
  43. column below:
  44. {f}
  45. """.format(f='\n '.join(SeedSource.format_fmt_codes().splitlines()),
  46. pnm=pnm,pnl=pnm.lower())
  47. wmsg = {
  48. 'mapping_error': """
  49. {pnm} -> {c} address mappings differ!
  50. {{:<23}} {{}} -> {{}}
  51. {{:<23}} {{}} -> {{}}
  52. """.strip().format(pnm=pnm,c=g.coin),
  53. 'missing_keys_error': """
  54. ERROR: a key file must be supplied for the following non-{pnm} address{{}}:\n {{}}
  55. """.format(pnm=pnm).strip()
  56. }
  57. saved_seeds = {}
  58. def get_seed_for_seed_id(sid,infiles,saved_seeds):
  59. if sid in saved_seeds:
  60. return saved_seeds[sid]
  61. while True:
  62. if infiles:
  63. ss = SeedSource(infiles.pop(0),ignore_in_fmt=True)
  64. elif opt.in_fmt:
  65. qmsg('Need seed data for Seed ID %s' % sid)
  66. ss = SeedSource()
  67. msg('User input produced Seed ID %s' % ss.seed.sid)
  68. else:
  69. die(2,'ERROR: No seed source found for Seed ID: %s' % sid)
  70. saved_seeds[ss.seed.sid] = ss.seed
  71. if ss.seed.sid == sid: return ss.seed
  72. def generate_kals_for_mmgen_addrs(need_keys,infiles,saved_seeds):
  73. mmids = [e.mmid for e in need_keys]
  74. sids = set(i.sid for i in mmids)
  75. vmsg('Need seed%s: %s' % (suf(sids,'s'),' '.join(sids)))
  76. d = MMGenList()
  77. from mmgen.addr import KeyAddrList
  78. for sid in sids:
  79. # Returns only if seed is found
  80. seed = get_seed_for_seed_id(sid,infiles,saved_seeds)
  81. for t in MMGenAddrType.mmtypes:
  82. idx_list = [i.idx for i in mmids if i.sid == sid and i.mmtype == t]
  83. if idx_list:
  84. addr_idxs = AddrIdxList(idx_list=idx_list)
  85. d.append(KeyAddrList(seed=seed,addr_idxs=addr_idxs,do_chksum=False,mmtype=MMGenAddrType(t)))
  86. return d
  87. def add_keys(tx,src,infiles=None,saved_seeds=None,keyaddr_list=None):
  88. need_keys = [e for e in getattr(tx,src) if e.mmid and not e.have_wif]
  89. if not need_keys: return []
  90. desc,m1 = ('key-address file','From key-address file:') if keyaddr_list else \
  91. ('seed(s)','Generated from seed:')
  92. qmsg('Checking {} -> {} address mappings for {} (from {})'.format(pnm,g.coin,src,desc))
  93. d = MMGenList([keyaddr_list]) if keyaddr_list else \
  94. generate_kals_for_mmgen_addrs(need_keys,infiles,saved_seeds)
  95. new_keys = []
  96. for e in need_keys:
  97. for kal in d:
  98. for f in kal.data:
  99. mmid = '{}:{}'.format(kal.al_id,f.idx)
  100. if mmid == e.mmid:
  101. if f.addr == e.addr:
  102. e.have_wif = True
  103. if src == 'inputs':
  104. new_keys.append(f)
  105. else:
  106. die(3,wmsg['mapping_error'].format(m1,mmid,f.addr,'tx file:',e.mmid,e.addr))
  107. if new_keys:
  108. vmsg('Added %s wif key%s from %s' % (len(new_keys),suf(new_keys,'s'),desc))
  109. return new_keys
  110. def _pop_and_return(args,cmplist): # strips found args
  111. return list(reversed([args.pop(args.index(a)) for a in reversed(args) if get_extension(a) in cmplist]))
  112. def get_tx_files(opt,args):
  113. ret = _pop_and_return(args,[MMGenTX.raw_ext])
  114. if not ret: die(1,'You must specify a raw transaction file!')
  115. return ret
  116. def get_seed_files(opt,args):
  117. # favor unencrypted seed sources first, as they don't require passwords
  118. u,e = SeedSourceUnenc,SeedSourceEnc
  119. ret = _pop_and_return(args,u.get_extensions())
  120. from mmgen.filename import find_file_in_dir,find_files_in_dir
  121. if g.bob or g.alice:
  122. import regtest as rt
  123. wf = rt.mmwallet(('alice','bob')[g.bob])
  124. else:
  125. wf = find_file_in_dir(Wallet,g.data_dir) # Make this the first encrypted ss in the list
  126. if wf: ret.append(wf)
  127. ret += _pop_and_return(args,e.get_extensions())
  128. if not (ret or opt.mmgen_keys_from_file or opt.keys_from_file): # or opt.use_wallet_dat
  129. die(1,'You must specify a seed or key source!')
  130. return ret
  131. def get_keyaddrlist(opt):
  132. if opt.mmgen_keys_from_file:
  133. return KeyAddrList(opt.mmgen_keys_from_file)
  134. return None
  135. def get_keylist(opt):
  136. if opt.keys_from_file:
  137. l = get_lines_from_file(opt.keys_from_file,'key-address data',trim_comments=True)
  138. kal = KeyAddrList(keylist=[m.split()[0] for m in l]) # accept bitcoind wallet dumps
  139. kal.generate_addrs_from_keys()
  140. return kal
  141. return None
  142. def txsign(opt,c,tx,seed_files,kl,kal,tx_num_str=''):
  143. keys = MMGenList() # list of AddrListEntry objects
  144. non_mm_addrs = tx.get_non_mmaddrs('inputs')
  145. if non_mm_addrs:
  146. tmp = KeyAddrList(addrlist=non_mm_addrs,do_chksum=False)
  147. tmp.add_wifs(kl)
  148. m = tmp.list_missing('sec')
  149. if m: die(2,wmsg['missing_keys_error'].format(suf(m,'es'),'\n '.join(m)))
  150. keys += tmp.data
  151. if opt.mmgen_keys_from_file:
  152. keys += add_keys(tx,'inputs',keyaddr_list=kal)
  153. add_keys(tx,'outputs',keyaddr_list=kal)
  154. keys += add_keys(tx,'inputs',seed_files,saved_seeds)
  155. add_keys(tx,'outputs',seed_files,saved_seeds)
  156. # this attr must not be written to file
  157. tx.delete_attrs('inputs','have_wif')
  158. tx.delete_attrs('outputs','have_wif')
  159. extra_sids = set(saved_seeds) - tx.get_input_sids() - tx.get_output_sids()
  160. if extra_sids:
  161. msg('Unused Seed ID{}: {}'.format(suf(extra_sids,'s'),' '.join(extra_sids)))
  162. if tx.sign(c,tx_num_str,keys):
  163. return tx
  164. else:
  165. die(3,red('Transaction {}could not be signed.'.format(tx_num_str)))