Browse Source

Subwallets, Part 3: transaction signing using the parent wallet

See commit 7538a94 for Part 1
See commit d1b8aef for Part 2

NOTE: to guard against Seed ID collisions, only the default or first wallet
specified in `mmgen-txsign` will be used for signing subwallet transactions.

Live example using MMGen regtest (Bob and Alice) mode:

    $ mmgen-regtest setup

    # Create a bogus wallet for Bob in mnemonic format:
    $ echo $(yes bee | head -n24) > bogus.mmwords

    # Convert the wallet and make it Bob's default:
    $ mkdir -p $HOME/.mmgen/regtest/btc/bob
    $ mmgen-walletconv -d $HOME/.mmgen/regtest/btc/bob bogus.mmwords
    ...
    MMGen wallet written to file /home/user/.mmgen/regtest/btc/bob/DF449DA4-*.mmdat

    # Choose two subwallets, 1S and 2L, and get their Seed IDs:
    $ mmgen-tool --bob get_subseed 1S # ==> 930E1AD5
    $ mmgen-tool --bob get_subseed 2L # ==> 62B02F54

    # Generate five bech32 addresses each from default wallet and subwallets:
    $ mmgen-addrgen --bob --type=bech32 1-5
    $ mmgen-addrgen --bob --type=bech32 --subwallet=1S 1-5
    $ mmgen-addrgen --bob --type=bech32 --subwallet=2L 1-5

    # Import the addresses into Bob's tracking wallet:
    $ mmgen-regtest bob
    $ mmgen-addrimport --bob DF449DA4*addrs
    $ mmgen-addrimport --bob 930E1AD5*addrs
    $ mmgen-addrimport --bob 62B02F54*addrs

    # Fund addresses from each of the wallets:
    $ mmgen-regtest send bcrt1q0v8eczv37ynl9zn8w3rh53xrkyuddrunuz74rd 10 # DF449DA4:B:1
    $ mmgen-regtest send bcrt1qtzxlnng6jd7yakzp7r69y6nmh5wp0wx7xg6e9w 10 # 930E1AD5:B:1
    $ mmgen-regtest send bcrt1qxnj0wgusq357qj62hq88thrw9cwanxc7926vrz 10 # 62B02F54:B:1

    # Create a transaction spending to and from each of the wallets:
    $ mmgen-txcreate --bob --tx-fee=3s DF449DA4:B:2,1.11 930E1AD5:B:2,1.23 62B02F54:B:2
    ...
    (choose inputs 1-3)
    ...
    Transaction written to file '<MMGen txid>[2.34].testnet.rawtx'

    # Sign the transaction:
    $ mmgen-txsign --bob *\[2.34\].testnet.rawtx
    ...
    Found subseed 930E1AD5 (DF449DA4:1S)
    ...
    Found subseed 62B02F54 (DF449DA4:2L)
    ...
    Signed transaction written to file '<MMGen txid>[2.34].testnet.sigtx'

    # Send the transaction:
    $ mmgen-txsend -q --bob *\[2.34\].testnet.sigtx
    Transaction sent: <BTC txid>

    # Mine a block and view the result:
    $ mmgen-regtest generate
    $ mmgen-tool --bob twview
MMGen 5 years ago
parent
commit
82086c9936
4 changed files with 115 additions and 8 deletions
  1. 5 0
      mmgen/main_txdo.py
  2. 5 0
      mmgen/main_txsign.py
  3. 13 4
      mmgen/txsign.py
  4. 92 4
      test/test_py_d/ts_regtest.py

+ 5 - 0
mmgen/main_txdo.py

@@ -21,6 +21,7 @@ mmgen-txdo: Create, sign and broadcast an online MMGen transaction
 """
 
 from mmgen.common import *
+from mmgen.obj import SubSeedIdxRange
 
 opts_data = {
 	'sets': [('yes', True, 'quiet', True)],
@@ -71,6 +72,9 @@ opts_data = {
 -P, --passwd-file=   f Get {pnm} wallet passphrase from file 'f'
 -r, --rbf              Make transaction BIP 125 (replace-by-fee) replaceable
 -q, --quiet            Suppress warnings; overwrite files without prompting
+-u, --subseeds=      n The number of subseed pairs to scan for (default: {ss},
+                       maximum: {ss_max}). Only the default or first supplied
+                       wallet is scanned for subseeds.
 -v, --verbose          Produce more verbose output
 -V, --vsize-adj=     f Adjust transaction's estimated vsize by factor 'f'
 -y, --yes              Answer 'yes' to prompts, suppress non-essential output
@@ -84,6 +88,7 @@ opts_data = {
 			kgs=' '.join(['{}:{}'.format(n,k) for n,k in enumerate(g.key_generators,1)]),
 			fu=help_notes('rel_fee_desc'),
 			fl=help_notes('fee_spec_letters'),
+			ss=g.subseeds,ss_max=SubSeedIdxRange.max_idx,
 			kg=g.key_generator,
 			cu=g.coin),
 		'notes': lambda s: s.format(

+ 5 - 0
mmgen/main_txsign.py

@@ -21,6 +21,7 @@ mmgen-txsign: Sign a transaction generated by 'mmgen-txcreate'
 """
 
 from mmgen.common import *
+from mmgen.obj import SubSeedIdxRange
 from mmgen.seed import SeedSource
 
 # -w, --use-wallet-dat (keys from running coin daemon) removed: use walletdump rpc instead
@@ -60,6 +61,9 @@ opts_data = {
 -q, --quiet           Suppress warnings; overwrite files without prompting
 -I, --info            Display information about the transaction and exit
 -t, --terse-info      Like '--info', but produce more concise output
+-u, --subseeds=     n The number of subseed pairs to scan for (default: {ss},
+                      maximum: {ss_max}). Only the default or first supplied
+                      wallet is scanned for subseeds.
 -v, --verbose         Produce more verbose output
 -V, --vsize-adj=   f  Adjust transaction's estimated vsize by factor 'f'
 -y, --yes             Answer 'yes' to prompts, suppress non-essential output
@@ -77,6 +81,7 @@ column below:
 			g=g,pnm=g.proj_name,pnl=g.proj_name.lower(),dn=g.proto.daemon_name,
 			kgs=' '.join(['{}:{}'.format(n,k) for n,k in enumerate(g.key_generators,1)]),
 			kg=g.key_generator,
+			ss=g.subseeds,ss_max=SubSeedIdxRange.max_idx,
 			cu=g.coin),
 		'notes': lambda s: s.format(
 			help_notes('txsign'),

+ 13 - 4
mmgen/txsign.py

@@ -38,26 +38,35 @@ ERROR: a key file must be supplied for the following non-{pnm} address{{}}:\n
 """.format(pnm=pnm).strip()
 }
 
-saved_seeds = {}
+from collections import OrderedDict
+saved_seeds = OrderedDict()
 
 def get_seed_for_seed_id(sid,infiles,saved_seeds):
 
 	if sid in saved_seeds:
 		return saved_seeds[sid]
 
+	subseeds_checked = False
 	while True:
 		if infiles:
 			seed = SeedSource(infiles.pop(0),ignore_in_fmt=True).seed
+		elif subseeds_checked == False:
+			seed = saved_seeds[list(saved_seeds)[0]].subseed_by_seed_id(sid,print_msg=True)
+			subseeds_checked = True
+			if not seed: continue
 		elif opt.in_fmt:
 			qmsg('Need seed data for Seed ID {}'.format(sid))
 			seed = SeedSource().seed
 			msg('User input produced Seed ID {}'.format(seed.sid))
+			if not seed.sid == sid: # TODO: add test
+				seed = seed.subseed_by_seed_id(sid,print_msg=True)
+
+		if seed:
+			saved_seeds[seed.sid] = seed
+			if seed.sid == sid: return seed
 		else:
 			die(2,'ERROR: No seed source found for Seed ID: {}'.format(sid))
 
-		saved_seeds[seed.sid] = seed
-		if seed.sid == sid: return seed
-
 def generate_kals_for_mmgen_addrs(need_keys,infiles,saved_seeds):
 	mmids = [e.mmid for e in need_keys]
 	sids = {i.sid for i in mmids}

+ 92 - 4
test/test_py_d/ts_regtest.py

@@ -41,13 +41,13 @@ rt_data = {
 	},
 	'rtBals': {
 		'btc': ('499.9999488','399.9998282','399.9998147','399.9996877',
-				'52.99990000','946.99933647','999.99923647','52.9999',
+				'52.99980410','946.99933647','999.99914057','52.9999',
 				'946.99933647'),
 		'bch': ('499.9999484','399.9999194','399.9998972','399.9997692',
-				'46.78900000','953.20966920','999.99866920','46.789',
+				'46.78890380','953.20966920','999.99857300','46.789',
 				'953.2096692'),
 		'ltc': ('5499.99744','5399.994425','5399.993885','5399.987535',
-				'52.99000000','10946.93753500','10999.92753500','52.99',
+				'52.98520500','10946.93753500','10999.92274000','52.99',
 				'10946.937535'),
 	},
 	'rtBals_gb': {
@@ -114,6 +114,21 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared):
 		('alice_send_estimatefee',   'tx creation with no fee on command line'),
 		('generate',                 'mining a block'),
 		('bob_bal6',                 "Bob's balance"),
+
+		('bob_subwallet_addrgen1',     "generating Bob's addrs from subwallet 29L"),
+		('bob_subwallet_addrgen2',     "generating Bob's addrs from subwallet 127S"),
+		('bob_subwallet_addrimport1',  "importing Bob's addrs from subwallet 29L"),
+		('bob_subwallet_addrimport2',  "importing Bob's addrs from subwallet 127S"),
+		('bob_subwallet_fund',         "funding Bob's subwallet addrs"),
+		('generate',                   'mining a block'),
+		('bob_twview2',                "viewing Bob's tracking wallet"),
+		('bob_twview3',                "viewing Bob's tracking wallet"),
+		('bob_subwallet_txcreate',     'creating a transaction with subwallet inputs'),
+		('bob_subwallet_txsign',       'signing a transaction with subwallet inputs'),
+		('bob_subwallet_txdo',         "sending from Bob's subwallet addrs"),
+		('generate',                   'mining a block'),
+		('bob_twview4',                "viewing Bob's tracking wallet"),
+
 		('bob_alice_bal',            "Bob and Alice's balances"),
 		('alice_bal2',               "Alice's balance"),
 
@@ -134,6 +149,7 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared):
 
 		('stop',                     'stopping regtest daemon'),
 	)
+	usr_subsids = { 'bob': {}, 'alice': {} }
 
 	def __init__(self,trunner,cfgs,spawn):
 		coin = g.coin.lower()
@@ -184,12 +200,25 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared):
 	def _user_sid(self,user):
 		return os.path.basename(get_file_with_ext(self._user_dir(user),'mmdat'))[:8]
 
-	def addrgen(self,user,wf=None,addr_range='1-5',mmtypes=[]):
+	def _get_user_subsid(self,user,subseed_idx):
+
+		if subseed_idx in self.usr_subsids[user]:
+			return self.usr_subsids[user][subseed_idx]
+
+		fn = get_file_with_ext(self._user_dir(user),'mmdat')
+		t = self.spawn('mmgen-tool',['get_subseed',subseed_idx,'wallet='+fn],no_msg=True)
+		t.passphrase('MMGen wallet',rt_pw)
+		sid = t.read().strip()[:8]
+		self.usr_subsids[user][subseed_idx] = sid
+		return sid
+
+	def addrgen(self,user,wf=None,addr_range='1-5',subseed_idx=None,mmtypes=[]):
 		from mmgen.addr import MMGenAddrType
 		for mmtype in mmtypes or g.proto.mmtypes:
 			t = self.spawn('mmgen-addrgen',
 				['--quiet','--'+user,'--type='+mmtype,'--outdir={}'.format(self._user_dir(user))] +
 				([wf] if wf else []) +
+				(['--subwallet='+subseed_idx] if subseed_idx else []) +
 				[addr_range],
 				extra_desc='({})'.format(MMGenAddrType.mmtypes[mmtype]['name']))
 			t.passphrase('MMGen wallet',rt_pw)
@@ -302,6 +331,65 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared):
 	def bob_bal6(self):
 		return self.user_bal('bob',rtBals[7])
 
+	def bob_subwallet_addrgen1(self):
+		return self.addrgen('bob',subseed_idx='29L',mmtypes=['C'])  # 29L: 2FA7BBA8
+
+	def bob_subwallet_addrgen2(self):
+		return self.addrgen('bob',subseed_idx='127S',mmtypes=['C']) # 127S: '09E8E286'
+
+	def subwallet_addrimport(self,user,subseed_idx):
+		sid = self._get_user_subsid(user,subseed_idx)
+		return self.addrimport(user,sid=sid,mmtypes=['C'])
+
+	def bob_subwallet_addrimport1(self): return self.subwallet_addrimport('bob','29L')
+	def bob_subwallet_addrimport2(self): return self.subwallet_addrimport('bob','127S')
+
+	def bob_subwallet_fund(self):
+		sid1 = self._get_user_subsid('bob','29L')
+		sid2 = self._get_user_subsid('bob','127S')
+		chg_addr = self._user_sid('bob') + (':B:1',':L:1')[g.coin=='BCH']
+		outputs_cl = [sid1+':C:2,0.29',sid2+':C:3,0.127',chg_addr]
+		inputs = ('3','1')[g.coin=='BCH']
+		return self.user_txdo('bob',rtFee[1],outputs_cl,inputs,extra_args=['--subseeds=127'])
+
+	def bob_twview2(self):
+		sid1 = self._get_user_subsid('bob','29L')
+		return self.user_twview('bob',chk=r'\b{}:C:2\b\s+{}'.format(sid1,'0.29'),sort='twmmid')
+
+	def bob_twview3(self):
+		sid2 = self._get_user_subsid('bob','127S')
+		return self.user_twview('bob',chk=r'\b{}:C:3\b\s+{}'.format(sid2,'0.127'),sort='amt')
+
+	def bob_subwallet_txcreate(self):
+		sid1 = self._get_user_subsid('bob','29L')
+		sid2 = self._get_user_subsid('bob','127S')
+		outputs_cl = [sid1+':C:5,0.0159',sid2+':C:5']
+		t = self.spawn('mmgen-txcreate',['-d',self.tmpdir,'-B','--bob'] + outputs_cl)
+		return self.txcreate_ui_common(t,
+								menu            = ['a'],
+								inputs          = ('1,2','2,3')[g.coin=='BCH'],
+								interactive_fee = '0.00001')
+
+	def bob_subwallet_txsign(self):
+		fn = get_file_with_ext(self.tmpdir,'rawtx')
+		t = self.spawn('mmgen-txsign',['-d',self.tmpdir,'--bob','--subseeds=127',fn])
+		t.view_tx('t')
+		t.passphrase('MMGen wallet',rt_pw)
+		t.do_comment(None)
+		t.expect('(Y/n): ','y')
+		t.written_to_file('Signed transaction')
+		return t
+
+	def bob_subwallet_txdo(self):
+		outputs_cl = [self._user_sid('bob')+':L:5']
+		inputs = ('1,2','2,3')[g.coin=='BCH']
+		return self.user_txdo('bob',rtFee[5],outputs_cl,inputs,menu=['a'],extra_args=['--subseeds=127']) # sort: amt
+
+	def bob_twview4(self):
+		sid = self._user_sid('bob')
+		amt = ('0.4169328','0.41364')[g.coin=='LTC']
+		return self.user_twview('bob',chk=r'\b{}:L:5\b\s+.*\s+\b{}\b'.format(sid,amt),sort='twmmid')
+
 	def bob_bal5_getbalance(self):
 		t_ext,t_mmgen = rtBals_gb[0],rtBals_gb[1]
 		assert Decimal(t_ext) + Decimal(t_mmgen) == Decimal(rtBals[3])