Skip to content

Commit

Permalink
Subwallets, Part 3: transaction signing using the parent wallet
Browse files Browse the repository at this point in the history
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
  • Loading branch information
mmgen committed May 16, 2019
1 parent 83e54cc commit 82086c9
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 8 deletions.
5 changes: 5 additions & 0 deletions mmgen/main_txdo.py
Expand Up @@ -21,6 +21,7 @@
"""

from mmgen.common import *
from mmgen.obj import SubSeedIdxRange

opts_data = {
'sets': [('yes', True, 'quiet', True)],
Expand Down Expand Up @@ -71,6 +72,9 @@
-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
Expand All @@ -84,6 +88,7 @@
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(
Expand Down
5 changes: 5 additions & 0 deletions mmgen/main_txsign.py
Expand Up @@ -21,6 +21,7 @@
"""

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
Expand Down Expand Up @@ -60,6 +61,9 @@
-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
Expand All @@ -77,6 +81,7 @@
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'),
Expand Down
17 changes: 13 additions & 4 deletions mmgen/txsign.py
Expand Up @@ -38,26 +38,35 @@
""".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}
Expand Down
96 changes: 92 additions & 4 deletions test/test_py_d/ts_regtest.py
Expand Up @@ -41,13 +41,13 @@
},
'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': {
Expand Down Expand Up @@ -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"),

Expand All @@ -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()
Expand Down Expand Up @@ -184,12 +200,25 @@ def _user_dir(self,user,coin=None):
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)
Expand Down Expand Up @@ -302,6 +331,65 @@ def bob_bal5(self):
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])
Expand Down

0 comments on commit 82086c9

Please sign in to comment.