From 82086c9936843dc43c1892b672cdf1680763ee84 Mon Sep 17 00:00:00 2001 From: MMGen Date: Thu, 16 May 2019 09:48:09 +0000 Subject: [PATCH] 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 '[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 '[2.34].testnet.sigtx' # Send the transaction: $ mmgen-txsend -q --bob *\[2.34\].testnet.sigtx Transaction sent: # Mine a block and view the result: $ mmgen-regtest generate $ mmgen-tool --bob twview --- mmgen/main_txdo.py | 5 ++ mmgen/main_txsign.py | 5 ++ mmgen/txsign.py | 17 +++++-- test/test_py_d/ts_regtest.py | 96 ++++++++++++++++++++++++++++++++++-- 4 files changed, 115 insertions(+), 8 deletions(-) diff --git a/mmgen/main_txdo.py b/mmgen/main_txdo.py index c28459ad..fbee840d 100755 --- a/mmgen/main_txdo.py +++ b/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( diff --git a/mmgen/main_txsign.py b/mmgen/main_txsign.py index aa3bab80..510a25d3 100755 --- a/mmgen/main_txsign.py +++ b/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'), diff --git a/mmgen/txsign.py b/mmgen/txsign.py index 2f9a9525..03f1dbd3 100755 --- a/mmgen/txsign.py +++ b/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} diff --git a/test/test_py_d/ts_regtest.py b/test/test_py_d/ts_regtest.py index 7ffdb5a4..35e676b2 100755 --- a/test/test_py_d/ts_regtest.py +++ b/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])