Browse Source

new Lockable class; lock global vars after initialization (amended)

The MMGen Project 4 years ago
parent
commit
4c2410e0

+ 1 - 1
mmgen/altcoins/eth/tw.py

@@ -244,7 +244,7 @@ Actions:         [q]uit view, [p]rint to file, pager [v]iew, [w]ide view,
 		'l':'a_lbl_add','D':'a_addr_delete','R':'a_balance_refresh' }
 
 	async def __ainit__(self,proto,*args,**kwargs):
-		if g.use_cached_balances:
+		if g.cached_balances:
 			self.hdr_fmt += '\n' + yellow('WARNING: Using cached balances. These may be out of date!')
 		await TwUnspentOutputs.__ainit__(self,proto,*args,**kwargs)
 

+ 88 - 0
mmgen/base_obj.py

@@ -0,0 +1,88 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
+# Copyright (C)2013-2020 The MMGen Project <mmgen@tuta.io>
+#
+# 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/>.
+
+"""
+base_obj.py: base objects with no internal imports for the MMGen suite
+"""
+
+class AttrCtrl:
+	"""
+	After instance is locked, forbid setting any attribute if the attribute is not present
+	in either the class or instance dict.
+
+	If _use_class_attr is True, ensure that attribute's type matches that of the class
+	attribute, unless the class attribute is set to None, in which case no type checking
+	is performed.
+	"""
+	_lock = False
+	_use_class_attr = False
+
+	def lock(self):
+		self._lock = True
+
+	def __setattr__(self,name,value):
+		if self._lock:
+			def do_error(name,value,ref_val):
+				raise AttributeError(
+					f'{value!r}: invalid value for attribute {name!r}'
+					+ ' of {} object (must be of type {}, not {})'.format(
+						type(self).__name__,
+						type(ref_val).__name__,
+						type(value).__name__ ) )
+
+			if not hasattr(self,name):
+				raise AttributeError(f'{type(self).__name__} object has no attribute {name!r}')
+
+			ref_val = getattr(type(self),name) if self._use_class_attr else getattr(self,name)
+
+			if (ref_val is not None) and not isinstance(value,type(ref_val)):
+				do_error(name,value,ref_val)
+
+		return object.__setattr__(self,name,value)
+
+	def __delattr__(self,name,value):
+		raise AttributeError('attribute cannot be deleted')
+
+class Lockable(AttrCtrl):
+	"""
+	After instance is locked, its attributes become read-only, with the following exceptions:
+	  - if the attribute's name is in _set_ok, attr can be set once after locking, if unset
+	  - if the attribute's name is in _reset_ok, read-only restrictions are bypassed and only
+	    AttrCtrl checking is performed
+
+	To determine whether an attribute is set, it's matched against either None or the class attribute,
+	if _use_class_attr is True
+	"""
+	_set_ok = ()
+	_reset_ok = ()
+
+	def __setattr__(self,name,value):
+		if self._lock and hasattr(self,name):
+			if name not in (self._set_ok + self._reset_ok):
+				raise AttributeError(f'attribute {name!r} of {type(self).__name__} object is read-only')
+			elif name not in self._reset_ok:
+				#print(self.__dict__)
+				if not (
+					getattr(self,name) is None or
+					( self._use_class_attr and name not in self.__dict__ ) ):
+					raise AttributeError(
+						f'attribute {name!r} of {type(self).__name__} object is already set,'
+						+ ' and resetting is forbidden' )
+			# name is in (_set_ok + _reset_ok) -- allow name to be in both lists
+
+		return AttrCtrl.__setattr__(self,name,value)

+ 34 - 5
mmgen/crypto.py

@@ -216,14 +216,44 @@ def get_hash_preset_from_user(hp=g.dfl_hash_preset,desc='data'):
 		else:
 			return hp
 
-_salt_len,_sha256_len,_nonce_len = 32,32,32
+def get_new_passphrase(desc,passchg=False):
+
+	w = '{}passphrase for {}'.format(('','new ')[bool(passchg)], desc)
+	if opt.passwd_file:
+		pw = ' '.join(get_words_from_file(opt.passwd_file,w))
+	elif opt.echo_passphrase:
+		pw = ' '.join(get_words_from_user(f'Enter {w}: '))
+	else:
+		for i in range(g.passwd_max_tries):
+			pw = ' '.join(get_words_from_user(f'Enter {w}: '))
+			pw_chk = ' '.join(get_words_from_user('Repeat passphrase: '))
+			dmsg(f'Passphrases: [{pw}] [{pw_chk}]')
+			if pw == pw_chk:
+				vmsg('Passphrases match'); break
+			else: msg('Passphrases do not match.  Try again.')
+		else:
+			die(2,f'User failed to duplicate passphrase in {g.passwd_max_tries} attempts')
+
+	if pw == '':
+		qmsg('WARNING: Empty passphrase')
+
+	return pw
+
+def get_passphrase(desc,passchg=False):
+	prompt ='Enter {}passphrase for {}: '.format(('','old ')[bool(passchg)],desc)
+	if opt.passwd_file:
+		pwfile_reuse_warning(opt.passwd_file)
+		return ' '.join(get_words_from_file(opt.passwd_file,'passphrase'))
+	else:
+		return ' '.join(get_words_from_user(prompt))
+
+_salt_len,_sha256_len,_nonce_len = (32,32,32)
 
 def mmgen_encrypt(data,desc='data',hash_preset=''):
 	salt  = get_random(_salt_len)
 	iv    = get_random(g.aesctr_iv_len)
 	nonce = get_random(_nonce_len)
-	hp    = hash_preset or (
-		opt.hash_preset if 'hash_preset' in opt.set_by_user else get_hash_preset_from_user('3',desc))
+	hp    = hash_preset or opt.hash_preset or get_hash_preset_from_user('3',desc)
 	m     = ('user-requested','default')[hp=='3']
 	vmsg(f'Encrypting {desc}')
 	qmsg(f'Using {m} hash preset of {hp!r}')
@@ -238,8 +268,7 @@ def mmgen_decrypt(data,desc='data',hash_preset=''):
 	salt   = data[:_salt_len]
 	iv     = data[_salt_len:dstart]
 	enc_d  = data[dstart:]
-	hp     = hash_preset or (
-		opt.hash_preset if 'hash_preset' in opt.set_by_user else get_hash_preset_from_user('3',desc))
+	hp     = hash_preset or opt.hash_preset or get_hash_preset_from_user('3',desc)
 	m  = ('user-requested','default')[hp=='3']
 	qmsg(f'Using {m} hash preset of {hp!r}')
 	passwd = get_passphrase(desc)

+ 23 - 8
mmgen/globalvars.py

@@ -25,12 +25,14 @@ from decimal import Decimal
 from collections import namedtuple
 from .devtools import *
 
+from .base_obj import Lockable
+
 def die(exit_val,s=''):
 	if s:
 		sys.stderr.write(s+'\n')
 	sys.exit(exit_val)
 
-class GlobalContext:
+class GlobalContext(Lockable):
 	"""
 	Set global vars to default values
 	Globals are overridden in this order:
@@ -38,6 +40,10 @@ class GlobalContext:
 	  2 - environmental vars
 	  3 - command line
 	"""
+	_set_ok = ('user_entropy','session')
+	_reset_ok = ('stdout','stderr','accept_defaults')
+	_use_class_attr = True
+
 	# Constants:
 	version      = '0.12.099'
 	release_date = 'May 2020'
@@ -63,12 +69,12 @@ class GlobalContext:
 	# Variables - these might be altered at runtime:
 
 	user_entropy    = b''
-	hash_preset     = '3'
+	dfl_hash_preset = '3'
+	dfl_seed_len    = 256
 	usr_randchars   = 30
 
 	tx_fee_adj   = Decimal('1.0')
 	tx_confs     = 3
-	seed_len     = 256
 
 	# Constant vars - some of these might be overridden in opts.py, but they don't change thereafter
 
@@ -97,7 +103,8 @@ class GlobalContext:
 	monero_wallet_rpc_password = ''
 	rpc_fail_on_command  = ''
 	aiohttp_rpc_queue_len = 16
-	use_cached_balances  = False
+	session              = None
+	cached_balances      = False
 
 	# regtest:
 	bob                  = False
@@ -141,11 +148,12 @@ class GlobalContext:
 	daemon_data_dir = '' # set by user
 
 	# global var sets user opt:
-	global_sets_opt = ( 'minconf','seed_len','hash_preset','usr_randchars','debug',
-						'quiet','tx_confs','tx_fee_adj','key_generator' )
+	global_sets_opt = (
+		'minconf','usr_randchars','debug', 'quiet','tx_confs','tx_fee_adj','key_generator' )
 
 	# user opt sets global var:
-	opt_sets_global = ( 'use_internal_keccak_module','subseeds' )
+	opt_sets_global = (
+		'use_internal_keccak_module','subseeds','cached_balances' )
 
 	# 'long' opts - opt sets global var
 	common_opts = (
@@ -160,7 +168,7 @@ class GlobalContext:
 		'quiet','verbose','debug','outdir','echo_passphrase','passwd_file','stdout',
 		'show_hash_presets','label','keep_passphrase','keep_hash_preset','yes',
 		'brain_params','b16','usr_randchars','coin','bob','alice','key_generator',
-		'hidden_incog_input_params','in_fmt'
+		'hidden_incog_input_params','in_fmt','hash_preset','seed_len',
 	)
 	incompatible_opts = (
 		('help','longhelp'),
@@ -229,6 +237,10 @@ class GlobalContext:
 	if platform == 'win':
 		autoset_opts['rpc_backend'].choices.remove('aiohttp')
 
+	auto_typeset_opts = {
+		'seed_len': int,
+	}
+
 	min_screen_width = 80
 	minconf = 1
 	max_tx_file_size = 100000
@@ -271,6 +283,9 @@ class GlobalContext:
 		short_disp_timeout = 0.1
 		if os.getenv('MMGEN_TEST_SUITE_POPEN_SPAWN'):
 			stdin_tty = True
+		if prog_name == 'unit_tests.py':
+			_set_ok += ('debug_subseed',)
+			_reset_ok += ('force_standalone_scrypt_module','session')
 
 	if os.getenv('MMGEN_DEBUG_ALL'):
 		for name in env_opts:

+ 2 - 2
mmgen/main_addrgen.py

@@ -65,9 +65,9 @@ opts_data = {
                       Options: {kgs} (default: {kg})
 -l, --seed-len=    l  Specify wallet seed length of 'l' bits.  This option
                       is required only for brainwallet and incognito inputs
-                      with non-standard (< {g.seed_len}-bit) seed lengths
+                      with non-standard (< {g.dfl_seed_len}-bit) seed lengths
 -p, --hash-preset= p  Use the scrypt hash parameters defined by preset 'p'
-                      for password hashing (default: '{g.hash_preset}')
+                      for password hashing (default: '{g.dfl_hash_preset}')
 -z, --show-hash-presets Show information on available hash presets
 -P, --passwd-file= f  Get wallet passphrase from file 'f'
 -q, --quiet           Produce quieter output; suppress some warnings

+ 16 - 17
mmgen/main_autosign.py

@@ -31,6 +31,8 @@ wallet_dir   = '/dev/shm/autosign'
 key_fn       = 'autosign.key'
 
 from .common import *
+opts.UserOpts._set_ok += ('outdir','passwd_file')
+
 prog_name = os.path.basename(sys.argv[0])
 opts_data = {
 	'sets': [('stealth_led', True, 'led', True)],
@@ -107,11 +109,22 @@ This command is currently available only on Linux-based platforms.
 	}
 }
 
-cmd_args = opts.init(opts_data,add_opts=['mmgen_keys_from_file','in_fmt'])
+cmd_args = opts.init(
+	opts_data,
+	add_opts = ['mmgen_keys_from_file','hidden_incog_input_params'],
+	init_opts = {
+		'quiet': True,
+		'in_fmt': 'words',
+		'out_fmt': 'wallet',
+		'usr_randchars': 0,
+		'hash_preset': '1',
+		'label': 'Autosign Wallet',
+	})
 
 exit_if_mswin('autosigning')
 
 import mmgen.tx
+from .wallet import Wallet
 from .txsign import txsign
 from .protocol import init_proto
 from .rpc import rpc_init
@@ -123,6 +136,7 @@ if opt.mountpoint:
 	mountpoint = opt.mountpoint
 
 opt.outdir = tx_dir = os.path.join(mountpoint,'tx')
+opt.passwd_file = os.path.join(tx_dir,key_fn)
 
 async def check_daemons_running():
 	if opt.coin:
@@ -220,15 +234,11 @@ async def sign():
 		return True
 
 def decrypt_wallets():
-	opt.hash_preset = '1'
-	opt.set_by_user = ['hash_preset']
-	opt.passwd_file = os.path.join(tx_dir,key_fn)
-	from .wallet import Wallet
 	msg(f'Unlocking wallet{suf(wfs)} with key from {opt.passwd_file!r}')
 	fails = 0
 	for wf in wfs:
 		try:
-			Wallet(wf)
+			Wallet(wf,ignore_in_fmt=True)
 		except SystemExit as e:
 			if e.code != 0:
 				fails += 1
@@ -335,18 +345,7 @@ def create_wallet_dir():
 def setup():
 	remove_wallet_dir()
 	gen_key(no_unmount=True)
-	from .wallet import Wallet
-	opt.hidden_incog_input_params = None
-	opt.quiet = True
-	opt.in_fmt = 'words'
 	ss_in = Wallet()
-	opt.out_fmt = 'wallet'
-	opt.usr_randchars = 0
-	opt.hash_preset = '1'
-	opt.set_by_user = ['hash_preset']
-	opt.passwd_file = os.path.join(tx_dir,key_fn)
-	from .obj import MMGenWalletLabel
-	opt.label = MMGenWalletLabel('Autosign Wallet')
 	ss_out = Wallet(ss=ss_in)
 	ss_out.write_to_file(desc='autosign wallet',outdir=wallet_dir)
 

+ 2 - 2
mmgen/main_passgen.py

@@ -54,9 +54,9 @@ opts_data = {
                       generate passwords of half the default length.
 -l, --seed-len=    l  Specify wallet seed length of 'l' bits.  This option
                       is required only for brainwallet and incognito inputs
-                      with non-standard (< {g.seed_len}-bit) seed lengths
+                      with non-standard (< {g.dfl_seed_len}-bit) seed lengths
 -p, --hash-preset= p  Use the scrypt hash parameters defined by preset 'p'
-                      for password hashing (default: '{g.hash_preset}')
+                      for password hashing (default: '{g.dfl_hash_preset}')
 -z, --show-hash-presets Show information on available hash presets
 -P, --passwd-file= f  Get wallet passphrase from file 'f'
 -q, --quiet           Produce quieter output; suppress some warnings

+ 1 - 4
mmgen/main_seedjoin.py

@@ -48,7 +48,7 @@ opts_data = {
 -L, --label=       l  Specify a label 'l' for output wallet
 -M, --master-share=i  Use a master share with index 'i' (min:{ms_min}, max:{ms_max})
 -p, --hash-preset= p  Use the scrypt hash parameters defined by preset 'p'
-                      for password hashing (default: '{g.hash_preset}')
+                      for password hashing (default: '{g.dfl_hash_preset}')
 -z, --show-hash-presets Show information on available hash presets
 -P, --passwd-file= f  Get wallet passphrase from file 'f'
 -q, --quiet           Produce quieter output; suppress some warnings
@@ -110,9 +110,6 @@ cmd_args = opts.init(opts_data)
 if len(cmd_args) + bool(opt.hidden_incog_input_params) < 2:
 	opts.usage()
 
-if opt.label:
-	opt.label = MMGenWalletLabel(opt.label,msg="Error in option '--label'")
-
 if opt.master_share:
 	master_idx = MasterShareIdx(opt.master_share)
 	id_str = SeedSplitIDString(opt.id_str or 'default')

+ 1 - 3
mmgen/main_tool.py

@@ -63,7 +63,7 @@ opts_data = {
 --, --longhelp        Print help message for long options (common options)
 -k, --use-internal-keccak-module Force use of the internal keccak module
 -p, --hash-preset= p  Use the scrypt hash parameters defined by preset 'p'
-                      for password hashing (default: '{g.hash_preset}')
+                      for password hashing (default: '{g.dfl_hash_preset}')
 -P, --passwd-file= f  Get passphrase from file 'f'.
 -q, --quiet           Produce quieter output
 -r, --usr-randchars=n Get 'n' characters of additional randomness from
@@ -91,8 +91,6 @@ Type '{pn} help <command>' for help on a particular command
 
 cmd_args = opts.init(opts_data,add_opts=['hidden_incog_input_params','in_fmt','use_old_ed25519'])
 
-g.use_cached_balances = opt.cached_balances
-
 if len(cmd_args) < 1:
 	opts.usage()
 

+ 2 - 2
mmgen/main_txbump.py

@@ -49,7 +49,7 @@ opts_data = {
 -i, --in-fmt=        f Input is from wallet format 'f' (see FMT CODES below)
 -l, --seed-len=      l Specify wallet seed length of 'l' bits. This option
                        is required only for brainwallet and incognito inputs
-                       with non-standard (< {g.seed_len}-bit) seed lengths.
+                       with non-standard (< {g.dfl_seed_len}-bit) seed lengths.
 -k, --keys-from-file=f Provide additional keys for non-{pnm} addresses
 -K, --key-generator= m Use method 'm' for public key generation
                        Options: {kgs}
@@ -63,7 +63,7 @@ opts_data = {
                        for the transaction's change output, if present)
 -O, --old-incog-fmt    Specify old-format incognito input
 -p, --hash-preset=   p Use the scrypt hash parameters defined by preset 'p'
-                       for password hashing (default: '{g.hash_preset}')
+                       for password hashing (default: '{g.dfl_hash_preset}')
 -P, --passwd-file=   f Get {pnm} wallet or {dn} passphrase from file 'f'
 -q, --quiet            Suppress warnings; overwrite files without prompting
 -s, --send             Sign and send the transaction (the default if seed

+ 0 - 2
mmgen/main_txcreate.py

@@ -76,8 +76,6 @@ opts_data = {
 
 cmd_args = opts.init(opts_data)
 
-g.use_cached_balances = opt.cached_balances
-
 async def main():
 
 	from .protocol import init_proto_from_opts

+ 2 - 4
mmgen/main_txdo.py

@@ -56,7 +56,7 @@ opts_data = {
                        outputs associated with each address will be included.
 -l, --seed-len=      l Specify wallet seed length of 'l' bits. This option
                        is required only for brainwallet and incognito inputs
-                       with non-standard (< {g.seed_len}-bit) seed lengths.
+                       with non-standard (< {g.dfl_seed_len}-bit) seed lengths.
 -k, --keys-from-file=f Provide additional keys for non-{pnm} addresses
 -K, --key-generator= m Use method 'm' for public key generation
                        Options: {kgs}
@@ -71,7 +71,7 @@ opts_data = {
                        mappings, so the user should record its checksum.
 -O, --old-incog-fmt    Specify old-format incognito input
 -p, --hash-preset=   p Use the scrypt hash parameters defined by preset 'p'
-                       for password hashing (default: '{g.hash_preset}')
+                       for password hashing (default: '{g.dfl_hash_preset}')
 -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
@@ -114,8 +114,6 @@ column below:
 
 cmd_args = opts.init(opts_data)
 
-g.use_cached_balances = opt.cached_balances
-
 from .tx import *
 from .txsign import *
 

+ 2 - 2
mmgen/main_txsign.py

@@ -45,9 +45,9 @@ opts_data = {
 -O, --old-incog-fmt   Specify old-format incognito input
 -l, --seed-len=    l  Specify wallet seed length of 'l' bits. This option
                       is required only for brainwallet and incognito inputs
-                      with non-standard (< {g.seed_len}-bit) seed lengths.
+                      with non-standard (< {g.dfl_seed_len}-bit) seed lengths.
 -p, --hash-preset=p   Use the scrypt hash parameters defined by preset 'p'
-                      for password hashing (default: '{g.hash_preset}')
+                      for password hashing (default: '{g.dfl_hash_preset}')
 -z, --show-hash-presets Show information on available hash presets
 -k, --keys-from-file=f Provide additional keys for non-{pnm} addresses
 -K, --key-generator=m Use method 'm' for public key generation

+ 2 - 5
mmgen/main_wallet.py

@@ -101,12 +101,12 @@ opts_data = {
 -K, --keep-hash-preset Reuse hash preset of input wallet for output wallet
 -l, --seed-len=    l  Specify wallet seed length of 'l' bits.  This option
                       is required only for brainwallet and incognito inputs
-                      with non-standard (< {g.seed_len}-bit) seed lengths.
+                      with non-standard (< {g.dfl_seed_len}-bit) seed lengths.
 -L, --label=       l  Specify a label 'l' for output wallet
 -m, --keep-label      Reuse label of input wallet for output wallet
 -M, --master-share=i  Use a master share with index 'i' (min:{ms_min}, max:{ms_max})
 -p, --hash-preset= p  Use the scrypt hash parameters defined by preset 'p'
-                      for password hashing (default: '{g.hash_preset}')
+                      for password hashing (default: '{g.dfl_hash_preset}')
 -z, --show-hash-presets Show information on available hash presets
 -P, --passwd-file= f  Get wallet passphrase from file 'f'
 -q, --quiet           Produce quieter output; suppress some warnings
@@ -144,9 +144,6 @@ FMT CODES:
 
 cmd_args = opts.init(opts_data,opt_filter=opt_filter)
 
-if opt.label:
-	opt.label = MMGenWalletLabel(opt.label,msg="Error in option '--label'")
-
 if invoked_as == 'subgen':
 	from .obj import SubSeedIdx
 	ss_idx = SubSeedIdx(cmd_args.pop())

+ 28 - 8
mmgen/opts.py

@@ -21,13 +21,17 @@ opts.py:  MMGen-specific options processing after generic processing by share.Op
 """
 import sys,os,stat
 
-class opt_cls(object):
-	pass
-opt = opt_cls()
-
 from .exception import UserOptError
 from .globalvars import g
+from .base_obj import Lockable
 import mmgen.share.Opts
+
+class UserOpts(Lockable):
+	_set_ok = ('usr_randchars',)
+	_reset_ok = ('quiet','verbose','yes')
+
+opt = UserOpts()
+
 from .util import *
 
 def usage():
@@ -228,7 +232,7 @@ opts_data_dfl = {
 	}
 }
 
-def init(opts_data=None,add_opts=[],opt_filter=None,parse_only=False):
+def init(opts_data=None,add_opts=None,init_opts=None,opt_filter=None,parse_only=False):
 
 	if opts_data is None:
 		opts_data = opts_data_dfl
@@ -238,6 +242,11 @@ def init(opts_data=None,add_opts=[],opt_filter=None,parse_only=False):
 	# po: (user_opts,cmd_args,opts,skipped_opts)
 	po = mmgen.share.Opts.parse_opts(opts_data,opt_filter=opt_filter,parse_only=parse_only)
 
+	if init_opts: # allow programs to preload user opts
+		for uopt,val in init_opts.items():
+			if uopt not in po.user_opts:
+				po.user_opts[uopt] = val
+
 	if parse_only:
 		return po
 
@@ -248,7 +257,8 @@ def init(opts_data=None,add_opts=[],opt_filter=None,parse_only=False):
 	for o in set(
 			po.opts
 			+ po.skipped_opts
-			+ tuple(add_opts)
+			+ tuple(add_opts or [])
+			+ tuple(init_opts or [])
 			+ g.required_opts
 			+ g.common_opts ):
 		setattr(opt,o,po.user_opts[o] if o in po.user_opts else None)
@@ -309,11 +319,9 @@ def init(opts_data=None,add_opts=[],opt_filter=None,parse_only=False):
 	# Set user opts from globals:
 	# - if opt is unset, set it to global value
 	# - if opt is set, convert its type to that of global value
-	opt.set_by_user = []
 	for k in g.global_sets_opt:
 		if hasattr(opt,k) and getattr(opt,k) != None:
 			setattr(opt,k,set_for_type(getattr(opt,k),getattr(g,k),'--'+k))
-			opt.set_by_user.append(k)
 		else:
 			setattr(opt,k,getattr(g,k))
 
@@ -355,6 +363,8 @@ def init(opts_data=None,add_opts=[],opt_filter=None,parse_only=False):
 	# Check all opts against g.autoset_opts, setting if unset
 	check_and_set_autoset_opts()
 
+	set_auto_typeset_opts()
+
 	if opt.verbose:
 		opt.quiet = None
 
@@ -370,6 +380,9 @@ def init(opts_data=None,add_opts=[],opt_filter=None,parse_only=False):
 		if k in opts_data:
 			del opts_data[k]
 
+	g.lock()
+	opt.lock()
+
 	return po.cmd_args
 
 # DISABLED
@@ -575,6 +588,13 @@ def check_usr_opts(usr_opts): # Raises an exception if any check fails
 		elif g.debug:
 			Msg('check_usr_opts(): No test for opt {!r}'.format(key))
 
+def set_auto_typeset_opts():
+	for key,ref_type in g.auto_typeset_opts.items():
+		if hasattr(opt,key):
+			val = getattr(opt,key)
+			if val is not None: # typeset only if opt is set
+				setattr(opt,key,ref_type(val))
+
 def check_and_set_autoset_opts(): # Raises exception if any check fails
 
 	def nocase_str(key,val,asd):

+ 0 - 1
mmgen/protocol.py

@@ -554,7 +554,6 @@ def init_proto(coin=None,testnet=False,regtest=False,network=None,network_id=Non
 		tokensym  = tokensym )
 
 def init_proto_from_opts():
-	from .opts import opt
 	return init_proto(
 		coin      = g.coin,
 		testnet   = g.testnet,

+ 4 - 3
mmgen/seed.py

@@ -32,7 +32,7 @@ class SeedBase(MMGenObject):
 	def __init__(self,seed_bin=None):
 		if not seed_bin:
 			# Truncate random data for smaller seed lengths
-			seed_bin = sha256(get_random(1033)).digest()[:opt.seed_len//8]
+			seed_bin = sha256(get_random(1033)).digest()[:(opt.seed_len or g.dfl_seed_len)//8]
 		elif len(seed_bin)*8 not in g.seed_lens:
 			die(3,'{}: invalid seed length'.format(len(seed_bin)))
 
@@ -58,6 +58,7 @@ class SeedBase(MMGenObject):
 class SubSeedList(MMGenObject):
 	have_short = True
 	nonce_start = 0
+	debug_last_share_sid_len = 3
 
 	def __init__(self,parent_seed):
 		self.member_type = SubSeed
@@ -134,7 +135,7 @@ class SubSeedList(MMGenObject):
 			m2 = 'collision with parent Seed ID {},'.format(sid)
 		else:
 			if debug_last_share:
-				sl = g.debug_last_share_sid_len
+				sl = self.debug_last_share_sid_len
 				colliding_idx = [d[:sl] for d in self.data[slen].keys].index(sid[:sl]) + 1
 				sid = sid[:sl]
 			else:
@@ -285,7 +286,7 @@ class SeedShareList(SubSeedList):
 		def last_share_debug(last_share):
 			if not debug_last_share:
 				return False
-			sid_len = g.debug_last_share_sid_len
+			sid_len = self.debug_last_share_sid_len
 			lsid = last_share.sid[:sid_len]
 			psid = parent_seed.sid[:sid_len]
 			ssids = [d[:sid_len] for d in self.data['long'].keys]

+ 13 - 13
mmgen/tool.py

@@ -269,9 +269,10 @@ class MMGenToolCmdMeta(type):
 
 class MMGenToolCmds(metaclass=MMGenToolCmdMeta):
 
-	def __init__(self,proto=None):
+	def __init__(self,proto=None,mmtype=None):
 		from .protocol import init_proto_from_opts
 		self.proto = proto or init_proto_from_opts()
+		self.mmtype = mmtype or getattr(opt,'type',None) or self.proto.dfl_mmtype
 		if g.token:
 			self.proto.tokensym = g.token.upper()
 
@@ -279,7 +280,7 @@ class MMGenToolCmds(metaclass=MMGenToolCmdMeta):
 		global at,kg,ag
 		at = MMGenAddrType(
 			proto = self.proto,
-			id_str = getattr(opt,'type',None) or self.proto.dfl_mmtype )
+			id_str = self.mmtype )
 		if arg != 'at':
 			kg = KeyGenerator(self.proto,at)
 			ag = AddrGenerator(self.proto,at)
@@ -460,7 +461,7 @@ class MMGenToolCmdCoin(MMGenToolCmds):
 
 	def wif2redeem_script(self,wifkey:'sstr'): # new
 		"convert a WIF private key to a Segwit P2SH-P2WPKH redeem script"
-		assert opt.type == 'segwit','This command is meaningful only for --type=segwit'
+		assert self.mmtype == 'segwit','This command is meaningful only for --type=segwit'
 		self.init_generators()
 		privhex = PrivKey(
 			self.proto,
@@ -469,7 +470,7 @@ class MMGenToolCmdCoin(MMGenToolCmds):
 
 	def wif2segwit_pair(self,wifkey:'sstr'):
 		"generate both a Segwit P2SH-P2WPKH redeem script and address from WIF"
-		assert opt.type == 'segwit','This command is meaningful only for --type=segwit'
+		assert self.mmtype == 'segwit','This command is meaningful only for --type=segwit'
 		self.init_generators()
 		pubhex = kg.to_pubhex(PrivKey(
 			self.proto,
@@ -495,26 +496,26 @@ class MMGenToolCmdCoin(MMGenToolCmds):
 
 	def pubhex2addr(self,pubkeyhex:'sstr'):
 		"convert a hex pubkey to an address"
-		if opt.type == 'segwit':
+		if self.mmtype == 'segwit':
 			return self.proto.pubhex2segwitaddr(pubkeyhex)
 		else:
 			return self.pubhash2addr(hash160(pubkeyhex))
 
 	def pubhex2redeem_script(self,pubkeyhex:'sstr'): # new
 		"convert a hex pubkey to a Segwit P2SH-P2WPKH redeem script"
-		assert opt.type == 'segwit','This command is meaningful only for --type=segwit'
+		assert self.mmtype == 'segwit','This command is meaningful only for --type=segwit'
 		return self.proto.pubhex2redeem_script(pubkeyhex)
 
 	def redeem_script2addr(self,redeem_scripthex:'sstr'): # new
 		"convert a Segwit P2SH-P2WPKH redeem script to an address"
-		assert opt.type == 'segwit','This command is meaningful only for --type=segwit'
+		assert self.mmtype == 'segwit','This command is meaningful only for --type=segwit'
 		assert redeem_scripthex[:4] == '0014','{!r}: invalid redeem script'.format(redeem_scripthex)
 		assert len(redeem_scripthex) == 44,'{} bytes: invalid redeem script length'.format(len(redeem_scripthex)//2)
 		return self.pubhash2addr(hash160(redeem_scripthex))
 
 	def pubhash2addr(self,pubhashhex:'sstr'):
 		"convert public key hash to address"
-		if opt.type == 'bech32':
+		if self.mmtype == 'bech32':
 			return self.proto.pubhash2bech32addr(pubhashhex)
 		else:
 			self.init_generators('at')
@@ -1108,7 +1109,7 @@ class MMGenToolCmdMonero(MMGenToolCmds):
 			return True
 
 		async def process_wallets(op):
-			opt.accept_defaults = opt.accept_defaults or op.accept_defaults
+			g.accept_defaults = g.accept_defaults or op.accept_defaults
 			from .protocol import init_proto
 			proto = init_proto('xmr',network='mainnet')
 			from .addr import AddrList
@@ -1223,8 +1224,7 @@ class tool_api(
 		super().__init__()
 		if not hasattr(opt,'version'):
 			opts.init()
-		opt.use_old_ed25519 = None
-		opt.type = None
+		self.mmtype = self.proto.dfl_mmtype
 
 	def init_coin(self,coinsym,network):
 		"""
@@ -1277,11 +1277,11 @@ class tool_api(
 	@property
 	def addrtype(self):
 		"""The currently configured address type (is assignable)"""
-		return opt.type
+		return self.mmtype
 
 	@addrtype.setter
 	def addrtype(self,val):
-		opt.type = val
+		self.mmtype = val
 
 	@property
 	def usr_randchars(self):

+ 1 - 1
mmgen/tw.py

@@ -801,7 +801,7 @@ class TrackingWallet(MMGenObject,metaclass=aInitMeta):
 	def get_cached_balance(self,addr,session_cache,data_root):
 		if addr in session_cache:
 			return self.proto.coin_amt(session_cache[addr])
-		if not g.use_cached_balances:
+		if not g.cached_balances:
 			return None
 		if addr in data_root and 'balance' in data_root[addr]:
 			return self.proto.coin_amt(data_root[addr]['balance'])

+ 7 - 34
mmgen/util.py

@@ -505,27 +505,6 @@ def get_seed_file(cmd_args,nargs,invoked_as=None):
 
 	return cmd_args[0] if cmd_args else (wf,None)[wd_from_opt]
 
-def get_new_passphrase(desc,passchg=False):
-
-	w = '{}passphrase for {}'.format(('','new ')[bool(passchg)], desc)
-	if opt.passwd_file:
-		pw = ' '.join(get_words_from_file(opt.passwd_file,w))
-	elif opt.echo_passphrase:
-		pw = ' '.join(get_words_from_user('Enter {}: '.format(w)))
-	else:
-		for i in range(g.passwd_max_tries):
-			pw = ' '.join(get_words_from_user('Enter {}: '.format(w)))
-			pw2 = ' '.join(get_words_from_user('Repeat passphrase: '))
-			dmsg('Passphrases: [{}] [{}]'.format(pw,pw2))
-			if pw == pw2:
-				vmsg('Passphrases match'); break
-			else: msg('Passphrases do not match.  Try again.')
-		else:
-			die(2,'User failed to duplicate passphrase in {} attempts'.format(g.passwd_max_tries))
-
-	if pw == '': qmsg('WARNING: Empty passphrase')
-	return pw
-
 def confirm_or_raise(message,q,expect='YES',exit_msg='Exiting at user request'):
 	m = message.strip()
 	if m: msg(m)
@@ -701,21 +680,15 @@ def get_data_from_file(infile,desc='data',dash=False,silent=False,binary=False,q
 
 	return data
 
-def pwfile_reuse_warning():
-	if 'passwd_file_used' in globals():
-		qmsg("Reusing passphrase from file '{}' at user request".format(opt.passwd_file))
+passwd_files_used = {}
+
+def pwfile_reuse_warning(passwd_file):
+	if passwd_file in passwd_files_used:
+		qmsg(f'Reusing passphrase from file {passwd_file!r} at user request')
 		return True
-	globals()['passwd_file_used'] = True
+	passwd_files_used[passwd_file] = True
 	return False
 
-def get_mmgen_passphrase(desc,passchg=False):
-	prompt ='Enter {}passphrase for {}: '.format(('','old ')[bool(passchg)],desc)
-	if opt.passwd_file:
-		pwfile_reuse_warning()
-		return ' '.join(get_words_from_file(opt.passwd_file,'passphrase'))
-	else:
-		return ' '.join(get_words_from_user(prompt))
-
 def my_raw_input(prompt,echo=True,insert_txt='',use_readline=True):
 
 	try: import readline
@@ -760,7 +733,7 @@ def keypress_confirm(prompt,default_yes=False,verbose=False,no_nl=False,complete
 	p = prompt if complete_prompt else '{} {}: '.format(prompt,q)
 	nl = ('\n','\r{}\r'.format(' '*len(p)))[no_nl]
 
-	if opt.accept_defaults:
+	if g.accept_defaults:
 		msg(p)
 		return default_yes
 

+ 12 - 14
mmgen/wallet.py

@@ -29,11 +29,11 @@ from .baseconv import *
 from .seed import Seed
 
 def check_usr_seed_len(seed_len):
-	if opt.seed_len != seed_len and 'seed_len' in opt.set_by_user:
+	if opt.seed_len and opt.seed_len != seed_len:
 		die(1,f"ERROR: requested seed length ({opt.seed_len}) doesn't match seed length of source ({seed_len})")
 
 def _is_mnemonic(s,fmt):
-	oq_save = opt.quiet
+	oq_save = bool(opt.quiet)
 	opt.quiet = True
 	try:
 		Wallet(in_data=s,in_fmt=fmt)
@@ -311,7 +311,7 @@ an empty passphrase, just hit ENTER twice.
 			if opt.keep_hash_preset:
 				qmsg(f'Reusing hash preset {old_hp!r} at user request')
 				self.ssdata.hash_preset = old_hp
-			elif 'hash_preset' in opt.set_by_user:
+			elif opt.hash_preset:
 				hp = self.ssdata.hash_preset = opt.hash_preset
 				qmsg(f'Using hash preset {opt.hash_preset!r} requested on command line')
 			else: # Prompt, using old value as default
@@ -320,11 +320,11 @@ an empty passphrase, just hit ENTER twice.
 			if (not opt.keep_hash_preset) and self.op == 'pwchg_new':
 				m = (f'changed to {hp!r}','unchanged')[hp==old_hp]
 				qmsg(f'Hash preset {m}')
-		elif 'hash_preset' in opt.set_by_user:
+		elif opt.hash_preset:
 			self.ssdata.hash_preset = opt.hash_preset
 			qmsg(f'Using hash preset {opt.hash_preset!r} requested on command line')
 		else:
-			self._get_hash_preset_from_user(opt.hash_preset,desc_suf)
+			self._get_hash_preset_from_user(g.dfl_hash_preset,desc_suf)
 
 	def _get_new_passphrase(self):
 		desc = '{}passphrase for {}{}'.format(
@@ -816,10 +816,8 @@ class MMGenWallet(WalletEnc):
 
 		d.hash_preset = hp = hpdata[0][:-1]  # a string!
 		qmsg("Hash preset of wallet: '{}'".format(hp))
-		if 'hash_preset' in opt.set_by_user:
-			uhp = opt.hash_preset
-			if uhp != hp:
-				qmsg("Warning: ignoring user-requested hash preset '{}'".format(uhp))
+		if opt.hash_preset and opt.hash_preset != hp:
+			qmsg('Warning: ignoring user-requested hash preset {opt.hash_preset}')
 
 		hash_params = list(map(int,hpdata[1:]))
 
@@ -899,11 +897,11 @@ class Brainwallet(WalletEnc):
 			"""
 			bw_seed_len,d.hash_preset = self.get_bw_params()
 		else:
-			if 'seed_len' not in opt.set_by_user:
-				qmsg(f'Using default seed length of {yellow(str(opt.seed_len))} bits\n'
+			if not opt.seed_len:
+				qmsg(f'Using default seed length of {yellow(str(g.dfl_seed_len))} bits\n'
 					+ 'If this is not what you want, use the --seed-len option' )
 			self._get_hash_preset()
-			bw_seed_len = opt.seed_len
+			bw_seed_len = opt.seed_len or g.dfl_seed_len
 		qmsg_r('Hashing brainwallet data.  Please wait...')
 		# Use buflen arg of scrypt.hash() to get seed of desired length
 		seed = scrypt_hash_passphrase(
@@ -954,7 +952,7 @@ to exit and re-run the program with the '--old-incog-fmt' option.
 	def _incog_data_size_chk(self):
 		# valid sizes: 56, 64, 72
 		dlen = len(self.fmt_data)
-		seed_len = opt.seed_len
+		seed_len = opt.seed_len or g.dfl_seed_len
 		valid_dlen = self._get_incog_data_len(seed_len)
 		if dlen == valid_dlen:
 			return True
@@ -1139,7 +1137,7 @@ harder to find, you're advised to choose a much larger file size than this.
 		qmsg("Getting hidden incog data from file '{}'".format(self.infile.name))
 
 		# Already sanity-checked:
-		d.target_data_len = self._get_incog_data_len(opt.seed_len)
+		d.target_data_len = self._get_incog_data_len(opt.seed_len or g.dfl_seed_len)
 		self._check_valid_offset(self.infile,'read')
 
 		flgs = os.O_RDONLY|os.O_BINARY if g.platform == 'win' else os.O_RDONLY

+ 1 - 0
setup.py

@@ -100,6 +100,7 @@ setup(
 			'mmgen.addr',
 			'mmgen.altcoin',
 			'mmgen.baseconv',
+			'mmgen.base_obj',
 			'mmgen.bech32',
 			'mmgen.bip39',
 			'mmgen.cfg',

+ 77 - 74
test/tooltest2.py

@@ -93,11 +93,6 @@ sample_text_hexdump = (
 	'000040: 6261 6e6b 73').format(n=NL)
 
 kafile_opts = ['-p1','-Ptest/ref/keyaddrfile_password']
-kafile_code = (
-	"\nopt.hash_preset = '1'" +
-	"\nopt.set_by_user = ['hash_preset']" +
-	"\nopt.use_old_ed25519 = None" +
-	"\nopt.passwd_file = 'test/ref/keyaddrfile_password'" )
 
 from test.unit_tests_d.ut_bip39 import unit_test as bip39
 tests = {
@@ -418,11 +413,11 @@ tests = {
 		'pubhash2addr': {
 			'btc_mainnet': [
 				( ['118089d66b4a5853765e94923abdd5de4616c6e5'], '12bYUGXS8SRArZneQDN9YEEYAtEa59Rykm',
-					None, 'opt.type="legacy"' ),
+					None, 'legacy' ),
 				( ['8e34586186551f6320fa3eb2d238a9c61ab8264b'], '3Eevao3DRVXnYym3tdrJDqS3Wc39PQzahn',
-					['--type=segwit'], 'opt.type="segwit"' ),
+					['--type=segwit'], 'segwit' ),
 				( ['3057f66ddd26fa6ef826b0d5ca067ec3e8f3c178'], 'bc1qxptlvmwaymaxa7pxkr2u5pn7c0508stcncv7ms',
-					['--type=bech32'], 'opt.type="bech32"' ),
+					['--type=bech32'], 'bech32' ),
 			],
 		},
 		'addr2scriptpubkey': {
@@ -443,32 +438,32 @@ tests = {
 			'btc_mainnet': [
 				( ['118089d66b4a5853765e94923abdd5de4616c6e5118089d66b4a5853765e9492'],
 					'5HwzecKMWD82ppJK3qMKpC7ohXXAwcyAN5VgdJ9PLFaAzpBG4sX',
-					None, 'opt.type="legacy"' ),
+					None, 'legacy' ),
 				( ['118089d66b4a5853765e94923abdd5de4616c6e5118089d66b4a5853765e9492'],
 					'KwojSzt1VvW343mQfWQi3J537siAt5ktL2qbuCg1ZyKR8BLQ6UJm',
-					['--type=compressed'], 'opt.type="compressed"' ),
+					['--type=compressed'], 'compressed' ),
 				( ['118089d66b4a5853765e94923abdd5de4616c6e5118089d66b4a5853765e9492'],
 					'KwojSzt1VvW343mQfWQi3J537siAt5ktL2qbuCg1ZyKR8BLQ6UJm',
-					['--type=segwit'], 'opt.type="segwit"' ),
+					['--type=segwit'], 'segwit' ),
 				( ['118089d66b4a5853765e94923abdd5de4616c6e5118089d66b4a5853765e9492'],
 					'KwojSzt1VvW343mQfWQi3J537siAt5ktL2qbuCg1ZyKR8BLQ6UJm',
-					['--type=bech32'], 'opt.type="bech32"' ),
+					['--type=bech32'], 'bech32' ),
 			],
 		},
 		'privhex2addr': {
 			'btc_mainnet': [
 				( ['118089d66b4a5853765e94923abdd5de4616c6e5118089d66b4a5853765e9492'],
 					'1C5VPtgq9xQ6AcTgMAR3J6GDrs72HC4pS1',
-					None, 'opt.type="legacy"' ),
+					None, 'legacy' ),
 				( ['118089d66b4a5853765e94923abdd5de4616c6e5118089d66b4a5853765e9492'],
 					'1Kz9fVSUMshzPejpzW9D95kScgA3rY6QxF',
-					['--type=compressed'], 'opt.type="compressed"' ),
+					['--type=compressed'], 'compressed' ),
 				( ['118089d66b4a5853765e94923abdd5de4616c6e5118089d66b4a5853765e9492'],
 					'3AhjTiWHhVJAi1s5CfKMcLzYps12x3gZhg',
-					['--type=segwit'], 'opt.type="segwit"' ),
+					['--type=segwit'], 'segwit' ),
 				( ['118089d66b4a5853765e94923abdd5de4616c6e5118089d66b4a5853765e9492'],
 					'bc1q6pqnfwwakuuejpm9w52ds342f9d5u36v0qnz7c',
-					['--type=bech32'], 'opt.type="bech32"' ),
+					['--type=bech32'], 'bech32' ),
 			],
 			'eth_mainnet': [
 				( ['0000000000000000000000000000000000000000000000000000000000000001'],
@@ -501,68 +496,68 @@ tests = {
 			'zec_mainnet': [
 				( ['0000000000000000000000000000000000000000000000000000000000000001'],
 			'zceQDpyNwek7dKqF5ZuFGj7YrNVxh7X1aPkrVxDLVxWSiZAFDEuy5C7XNV8VhyZ3ghTPQ61xjCGiyLT3wqpiN1Yi6mdmaCq',
-					['--type=zcash_z'], 'opt.type="zcash_z"' ),
+					['--type=zcash_z'], 'zcash_z' ),
 				( ['ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'],
 			'zcY1hqJ3P5ifjnWk1BcXpjrLG5XeJZUSPCiiVTF9LXrejxBzAsFWcNyr6PudwQHm8DnQpD8HEaM3dh8sB6cf91ciAa53YQ1',
-					['--type=zcash_z'], 'opt.type="zcash_z"' ),
+					['--type=zcash_z'], 'zcash_z' ),
 				( ['0fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'],
 			'zcY1hqJ3P5ifjnWk1BcXpjrLG5XeJZUSPCiiVTF9LXrejxBzAsFWcNyr6PudwQHm8DnQpD8HEaM3dh8sB6cf91ciAa53YQ1',
-					['--type=zcash_z'], 'opt.type="zcash_z"' ),
+					['--type=zcash_z'], 'zcash_z' ),
 				( ['00000000000000000000000000000000000000000000000000000000000000ff'],
 			'zcck12KgVY34LJwVEDLN8sXhL787zmjKqPsP1uBYRHs75bL9sQu4P7wcc5ZJTjKsL376zaSpsYqGxK94JbiYcNoH8DkeGbN',
-					['--type=zcash_z'], 'opt.type="zcash_z"' ),
+					['--type=zcash_z'], 'zcash_z' ),
 				( ['ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0f'],
 			'zcJ9hEezG1Jeye5dciqiMDh6SXtYbUsircGmpVyhHWyzyxDVRRDs5Q8M7hG3c7nDcvd5Pw4u4wV9RAQmq5RCBZq5wVyMQV8',
-					['--type=zcash_z'], 'opt.type="zcash_z"' ),
+					['--type=zcash_z'], 'zcash_z' ),
 				( ['deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef'],
 			'zchFELwBxqsAubsLQ8yZgPCDDGukjXJssgCbiTPwFNmFwn9haLnDatzfhLdZzJT4PcU4o2yr92B52UFirUzEdF6ZYM2gBkM',
-					['--type=zcash_z'], 'opt.type="zcash_z"' ),
+					['--type=zcash_z'], 'zcash_z' ),
 			],
 		},
 		'privhex2pubhex': {
 			'btc_mainnet': [
 				( ['118089d66b4a5853765e94923abdd5de4616c6e5118089d66b4a5853765e9492'],
 					'044281a85c9ce87279e028410b851410d65136304cfbbbeaaa8e2e3931cf4e972757f3254c322eeaa3cb6bf97cc5ecf8d4387b0df2c0b1e6ee18fe3a6977a7d57a',
-					None, 'opt.type="legacy"' ),
+					None, 'legacy' ),
 				( ['118089d66b4a5853765e94923abdd5de4616c6e5118089d66b4a5853765e9492'],
 					'024281a85c9ce87279e028410b851410d65136304cfbbbeaaa8e2e3931cf4e9727',
-					['--type=compressed'], 'opt.type="compressed"' ),
+					['--type=compressed'], 'compressed' ),
 				( ['118089d66b4a5853765e94923abdd5de4616c6e5118089d66b4a5853765e9492'],
 					'024281a85c9ce87279e028410b851410d65136304cfbbbeaaa8e2e3931cf4e9727',
-					['--type=segwit'], 'opt.type="segwit"' ),
+					['--type=segwit'], 'segwit' ),
 				( ['118089d66b4a5853765e94923abdd5de4616c6e5118089d66b4a5853765e9492'],
 					'024281a85c9ce87279e028410b851410d65136304cfbbbeaaa8e2e3931cf4e9727',
-					['--type=bech32'], 'opt.type="bech32"' ),
+					['--type=bech32'], 'bech32' ),
 			],
 		},
 		'pubhex2addr': {
 			'btc_mainnet': [
 				( ['044281a85c9ce87279e028410b851410d65136304cfbbbeaaa8e2e3931cf4e972757f3254c322eeaa3cb6bf97cc5ecf8d4387b0df2c0b1e6ee18fe3a6977a7d57a'],
 					'1C5VPtgq9xQ6AcTgMAR3J6GDrs72HC4pS1',
-					None, 'opt.type="legacy"' ),
+					None, 'legacy' ),
 				( ['024281a85c9ce87279e028410b851410d65136304cfbbbeaaa8e2e3931cf4e9727'],
 					'1Kz9fVSUMshzPejpzW9D95kScgA3rY6QxF',
-					['--type=compressed'], 'opt.type="compressed"' ),
+					['--type=compressed'], 'compressed' ),
 				( ['024281a85c9ce87279e028410b851410d65136304cfbbbeaaa8e2e3931cf4e9727'],
 					'3AhjTiWHhVJAi1s5CfKMcLzYps12x3gZhg',
-					['--type=segwit'], 'opt.type="segwit"' ),
+					['--type=segwit'], 'segwit' ),
 				( ['024281a85c9ce87279e028410b851410d65136304cfbbbeaaa8e2e3931cf4e9727'],
 					'bc1q6pqnfwwakuuejpm9w52ds342f9d5u36v0qnz7c',
-					['--type=bech32'], 'opt.type="bech32"' ),
+					['--type=bech32'], 'bech32' ),
 			],
 		},
 		'pubhex2redeem_script': {
 			'btc_mainnet': [
 				( ['024281a85c9ce87279e028410b851410d65136304cfbbbeaaa8e2e3931cf4e9727'],
 					'0014d04134b9ddb7399907657514d846aa495b4e474c',
-					['--type=segwit'], 'opt.type="segwit"' ),
+					['--type=segwit'], 'segwit' ),
 			],
 		},
 		'redeem_script2addr': {
 			'btc_mainnet': [
 				( ['0014d04134b9ddb7399907657514d846aa495b4e474c'],
 					'3AhjTiWHhVJAi1s5CfKMcLzYps12x3gZhg',
-					['--type=segwit'], 'opt.type="segwit"' ),
+					['--type=segwit'], 'segwit' ),
 			],
 		},
 		'randpair': {
@@ -576,13 +571,13 @@ tests = {
 		'wif2addr': {
 			'btc_mainnet': [
 				( ['5HwzecKMWD82ppJK3qMKpC7ohXXAwcyAN5VgdJ9PLFaAzpBG4sX'],
-					'1C5VPtgq9xQ6AcTgMAR3J6GDrs72HC4pS1', ['--type=legacy'], 'opt.type="legacy"' ),
+					'1C5VPtgq9xQ6AcTgMAR3J6GDrs72HC4pS1', ['--type=legacy'], 'legacy' ),
 				( ['KwojSzt1VvW343mQfWQi3J537siAt5ktL2qbuCg1ZyKR8BLQ6UJm'],
-					'1Kz9fVSUMshzPejpzW9D95kScgA3rY6QxF', ['--type=compressed'], 'opt.type="compressed"' ),
+					'1Kz9fVSUMshzPejpzW9D95kScgA3rY6QxF', ['--type=compressed'], 'compressed' ),
 				( ['KwojSzt1VvW343mQfWQi3J537siAt5ktL2qbuCg1ZyKR8BLQ6UJm'],
-					'3AhjTiWHhVJAi1s5CfKMcLzYps12x3gZhg', ['--type=segwit'], 'opt.type="segwit"' ),
+					'3AhjTiWHhVJAi1s5CfKMcLzYps12x3gZhg', ['--type=segwit'], 'segwit' ),
 				( ['KwojSzt1VvW343mQfWQi3J537siAt5ktL2qbuCg1ZyKR8BLQ6UJm'],
-					'bc1q6pqnfwwakuuejpm9w52ds342f9d5u36v0qnz7c', ['--type=bech32'], 'opt.type="bech32"' ),
+					'bc1q6pqnfwwakuuejpm9w52ds342f9d5u36v0qnz7c', ['--type=bech32'], 'bech32' ),
 			],
 			'eth_mainnet': [
 				( ['0000000000000000000000000000000000000000000000000000000000000001'],
@@ -615,52 +610,52 @@ tests = {
 			'zec_mainnet': [
 				( ['SKxny894fJe2rmZjeuoE6GVfNkWoXfPp8337VrLLNWG56FjqVUYR'],
 			'zceQDpyNwek7dKqF5ZuFGj7YrNVxh7X1aPkrVxDLVxWSiZAFDEuy5C7XNV8VhyZ3ghTPQ61xjCGiyLT3wqpiN1Yi6mdmaCq',
-					['--type=zcash_z'], 'opt.type="zcash_z"' ),
+					['--type=zcash_z'], 'zcash_z' ),
 				( ['SKxv1peuQvMT4TvqPLqKy1px3oqLm98Evi948VU8N8VKcf7C2umc'],
 			'zcY1hqJ3P5ifjnWk1BcXpjrLG5XeJZUSPCiiVTF9LXrejxBzAsFWcNyr6PudwQHm8DnQpD8HEaM3dh8sB6cf91ciAa53YQ1',
-					['--type=zcash_z'], 'opt.type="zcash_z"' ),
+					['--type=zcash_z'], 'zcash_z' ),
 				( ['SKxv1peuQvMT4TvqPLqKy1px3oqLm98Evi948VU8N8VKcf7C2umc'],
 			'zcY1hqJ3P5ifjnWk1BcXpjrLG5XeJZUSPCiiVTF9LXrejxBzAsFWcNyr6PudwQHm8DnQpD8HEaM3dh8sB6cf91ciAa53YQ1',
-					['--type=zcash_z'], 'opt.type="zcash_z"' ),
+					['--type=zcash_z'], 'zcash_z' ),
 				( ['SKxny894fJe2rmZjeuoE6GVfNkWoXfPp8337VrLLNWG56kQw4qjm'],
 			'zcck12KgVY34LJwVEDLN8sXhL787zmjKqPsP1uBYRHs75bL9sQu4P7wcc5ZJTjKsL376zaSpsYqGxK94JbiYcNoH8DkeGbN',
-					['--type=zcash_z'], 'opt.type="zcash_z"' ),
+					['--type=zcash_z'], 'zcash_z' ),
 				( ['SKxv1peuQvMT4TvqPLqKy1px3oqLm98Evi948VU8N8VKcBwrLwiu'],
 			'zcJ9hEezG1Jeye5dciqiMDh6SXtYbUsircGmpVyhHWyzyxDVRRDs5Q8M7hG3c7nDcvd5Pw4u4wV9RAQmq5RCBZq5wVyMQV8',
-					['--type=zcash_z'], 'opt.type="zcash_z"' ),
+					['--type=zcash_z'], 'zcash_z' ),
 				( ['SKxuS56e99jpCeD9mMQ5o63zoGPakNdM9HCvt4Vt2cypvRjCdvGJ'],
 			'zchFELwBxqsAubsLQ8yZgPCDDGukjXJssgCbiTPwFNmFwn9haLnDatzfhLdZzJT4PcU4o2yr92B52UFirUzEdF6ZYM2gBkM',
-					['--type=zcash_z'], 'opt.type="zcash_z"' ),
+					['--type=zcash_z'], 'zcash_z' ),
 			],
 		},
 		'wif2hex': {
 			'btc_mainnet': [
 				( ['5HwzecKMWD82ppJK3qMKpC7ohXXAwcyAN5VgdJ9PLFaAzpBG4sX'],
 					'118089d66b4a5853765e94923abdd5de4616c6e5118089d66b4a5853765e9492',
-					None, 'opt.type="legacy"' ),
+					None, 'legacy' ),
 				( ['KwojSzt1VvW343mQfWQi3J537siAt5ktL2qbuCg1ZyKR8BLQ6UJm'],
 					'118089d66b4a5853765e94923abdd5de4616c6e5118089d66b4a5853765e9492',
-					['--type=compressed'], 'opt.type="compressed"' ),
+					['--type=compressed'], 'compressed' ),
 				( ['KwojSzt1VvW343mQfWQi3J537siAt5ktL2qbuCg1ZyKR8BLQ6UJm'],
 					'118089d66b4a5853765e94923abdd5de4616c6e5118089d66b4a5853765e9492',
-					['--type=segwit'], 'opt.type="segwit"' ),
+					['--type=segwit'], 'segwit' ),
 				( ['KwojSzt1VvW343mQfWQi3J537siAt5ktL2qbuCg1ZyKR8BLQ6UJm'],
 					'118089d66b4a5853765e94923abdd5de4616c6e5118089d66b4a5853765e9492',
-					['--type=bech32'], 'opt.type="bech32"' ),
+					['--type=bech32'], 'bech32' ),
 			],
 		},
 		'wif2redeem_script': {
 			'btc_mainnet': [
 				( ['KwojSzt1VvW343mQfWQi3J537siAt5ktL2qbuCg1ZyKR8BLQ6UJm'],
 					'0014d04134b9ddb7399907657514d846aa495b4e474c',
-					['--type=segwit'], 'opt.type="segwit"' ),
+					['--type=segwit'], 'segwit' ),
 			],
 		},
 		'wif2segwit_pair': {
 			'btc_mainnet': [
 				( ['KwojSzt1VvW343mQfWQi3J537siAt5ktL2qbuCg1ZyKR8BLQ6UJm'],
 					('0014d04134b9ddb7399907657514d846aa495b4e474c','3AhjTiWHhVJAi1s5CfKMcLzYps12x3gZhg'),
-					['--type=segwit'], 'opt.type="segwit"' ),
+					['--type=segwit'], 'segwit' ),
 			],
 		},
 	},
@@ -720,43 +715,43 @@ tests = {
 		'keyaddrfile_chksum': {
 			'btc_mainnet': [
 				( ['test/ref/98831F3A[1,31-33,500-501,1010-1011].akeys.mmenc'],
-					'9F2D D781 1812 8BAD', kafile_opts, kafile_code ),
+					'9F2D D781 1812 8BAD', kafile_opts ),
 			],
 			'btc_testnet': [
 				( ['test/ref/98831F3A[1,31-33,500-501,1010-1011].testnet.akeys.mmenc'],
-					'88CC 5120 9A91 22C2', kafile_opts, kafile_code ),
+					'88CC 5120 9A91 22C2', kafile_opts ),
 			],
 			'ltc_mainnet': [
 				( ['test/ref/litecoin/98831F3A-LTC[1,31-33,500-501,1010-1011].akeys.mmenc'],
-					'B804 978A 8796 3ED4', kafile_opts, kafile_code ),
+					'B804 978A 8796 3ED4', kafile_opts ),
 			],
 			'ltc_testnet': [
 				( ['test/ref/litecoin/98831F3A-LTC[1,31-33,500-501,1010-1011].testnet.akeys.mmenc'],
-					'98B5 AC35 F334 0398', kafile_opts, kafile_code ),
+					'98B5 AC35 F334 0398', kafile_opts ),
 			],
 			'zec_mainnet': [
 				( ['test/ref/zcash/98831F3A-ZEC-C[1,31-33,500-501,1010-1011].akeys.mmenc'],
-				'F05A 5A5C 0C8E 2617', kafile_opts, kafile_code ),
+				'F05A 5A5C 0C8E 2617', kafile_opts ),
 				( ['test/ref/zcash/98831F3A-ZEC-Z[1,31-33,500-501,1010-1011].akeys.mmenc'], '6B87 9B2D 0D8D 8D1E',
-					kafile_opts + ['--type=zcash_z'], kafile_code + '\nopt.type = "zcash_z"' ),
+					kafile_opts + ['--type=zcash_z'], 'opt.type = "zcash_z"' ),
 			],
 			'xmr_mainnet': [
 				( ['test/ref/monero/98831F3A-XMR-M[1,31-33,500-501,1010-1011].akeys.mmenc'],
-				'E0D7 9612 3D67 404A', kafile_opts, kafile_code ), ],
+				'E0D7 9612 3D67 404A', kafile_opts ), ],
 			'dash_mainnet': [
 				( ['test/ref/dash/98831F3A-DASH-C[1,31-33,500-501,1010-1011].akeys.mmenc'],
-				'E83D 2C63 FEA2 4142', kafile_opts, kafile_code ), ],
+				'E83D 2C63 FEA2 4142', kafile_opts ), ],
 			'eth_mainnet': [
 				( ['test/ref/ethereum/98831F3A-ETH[1,31-33,500-501,1010-1011].akeys.mmenc'],
-				'E400 70D9 0AE3 C7C2', kafile_opts, kafile_code ), ],
+				'E400 70D9 0AE3 C7C2', kafile_opts ), ],
 			'etc_mainnet': [
 				( ['test/ref/ethereum_classic/98831F3A-ETC[1,31-33,500-501,1010-1011].akeys.mmenc'],
-				'EF49 967D BD6C FE45', kafile_opts, kafile_code ), ],
+				'EF49 967D BD6C FE45', kafile_opts ), ],
 		},
 		'passwdfile_chksum': {
 			'btc_mainnet': [
 				( ['test/ref/98831F3A-фубар@crypto.org-b58-20[1,4,1100].pws'],
-					'DDD9 44B0 CA28 183F', kafile_opts, kafile_code ), ],
+					'DDD9 44B0 CA28 183F', kafile_opts ), ],
 		},
 		'txview': {
 			'btc_mainnet': [ ( ['test/ref/0B8D5A[15.31789,14,tl=1320969600].rawtx'], None ), ],
@@ -801,7 +796,7 @@ async def run_test(gid,cmd_name):
 
 	msg_r(green(m)+'\n' if opt.verbose else m)
 
-	def fork_cmd(cmd_name,args,out,opts,exec_code):
+	def fork_cmd(cmd_name,args,out,opts):
 		cmd = list(tool_cmd) + (opts or []) + [cmd_name] + args
 		vmsg('{} {}'.format(green('Executing'),cyan(' '.join(cmd))))
 		cp = run(cmd,input=stdin_input or None,stdout=PIPE,stderr=PIPE)
@@ -819,13 +814,16 @@ async def run_test(gid,cmd_name):
 
 		return cmd_out.strip()
 
-	async def run_func(cmd_name,args,out,opts,exec_code):
+	async def run_func(cmd_name,args,out,opts,mmtype):
 		vmsg('{}: {}{}'.format(purple('Running'),
 				' '.join([cmd_name]+[repr(e) for e in args]),
-				' '+exec_code if exec_code else '' ))
-		if exec_code: exec(exec_code)
+				' '+mmtype if mmtype else '' ))
 		aargs,kwargs = tool._process_args(cmd_name,args)
-		oq_save = opt.quiet
+		tm = tool.MMGenToolCmdMeta
+		cls_name = tm.classname(tm,cmd_name)
+		tobj = getattr(tool,cls_name)(mmtype=mmtype)
+		method = getattr(tobj,cmd_name)
+		oq_save = bool(opt.quiet)
 		if not opt.verbose:
 			opt.quiet = True
 		if stdin_input:
@@ -834,7 +832,7 @@ async def run_test(gid,cmd_name):
 				os.close(fd1)
 				stdin_save = os.dup(0)
 				os.dup2(fd0,0)
-				cmd_out = tc.call(cmd_name,*aargs,**kwargs)
+				cmd_out = method(*aargs,**kwargs)
 				os.dup2(stdin_save,0)
 				os.wait()
 				opt.quiet = oq_save
@@ -845,13 +843,13 @@ async def run_test(gid,cmd_name):
 				vmsg('Input: {!r}'.format(stdin_input))
 				sys.exit(0)
 		else:
-			ret = tc.call(cmd_name,*aargs,**kwargs)
+			ret = method(*aargs,**kwargs)
 			if type(ret).__name__ == 'coroutine':
 				ret = await ret
 			opt.quiet = oq_save
 			return ret
 
-	def tool_api(cmd_name,args,out,opts,exec_code):
+	def tool_api(cmd_name,args,out,opts):
 		from mmgen.tool import tool_api
 		tool = tool_api()
 		if opts:
@@ -868,7 +866,7 @@ async def run_test(gid,cmd_name):
 		return getattr(tool,cmd_name)(*pargs,**kwargs)
 
 	for d in data:
-		args,out,opts,exec_code = d + tuple([None] * (4-len(d)))
+		args,out,opts,mmtype = d + tuple([None] * (4-len(d)))
 		stdin_input = None
 		if args and type(args[0]) == bytes:
 			stdin_input = args[0]
@@ -877,14 +875,14 @@ async def run_test(gid,cmd_name):
 		if opt.tool_api:
 			if args and args[0 ]== '-':
 				continue
-			cmd_out = tool_api(cmd_name,args,out,opts,exec_code)
+			cmd_out = tool_api(cmd_name,args,out,opts)
 		elif opt.fork:
-			cmd_out = fork_cmd(cmd_name,args,out,opts,exec_code)
+			cmd_out = fork_cmd(cmd_name,args,out,opts)
 		else:
 			if stdin_input and g.platform == 'win':
 				msg('Skipping for MSWin - no os.fork()')
 				continue
-			cmd_out = await run_func(cmd_name,args,out,opts,exec_code)
+			cmd_out = await run_func(cmd_name,args,out,opts,mmtype)
 
 		try:    vmsg('Output:\n{}\n'.format(cmd_out))
 		except: vmsg('Output:\n{}\n'.format(repr(cmd_out)))
@@ -967,7 +965,14 @@ def list_tested_cmds():
 
 sys.argv = [sys.argv[0]] + ['--skip-cfg-file'] + sys.argv[1:]
 
-cmd_args = opts.init(opts_data,add_opts=['use_old_ed25519'])
+cmd_args = opts.init(
+	opts_data,
+	add_opts  = ['use_old_ed25519'],
+	init_opts = {
+		'usr_randchars': 0,
+		'hash_preset': '1',
+		'passwd_file': 'test/ref/keyaddrfile_password',
+	})
 
 from mmgen.protocol import init_proto_from_opts
 proto = init_proto_from_opts()
@@ -1014,8 +1019,6 @@ if opt.fork:
 		tool_cmd = ('python3','-m','trace','--count','--coverdir='+d,'--file='+f) + tool_cmd
 	elif g.platform == 'win':
 		tool_cmd = ('python3',) + tool_cmd
-else:
-	opt.usr_randchars = 0
 
 start_time = int(time.time())
 

+ 103 - 0
test/unit_tests_d/ut_lockable.py

@@ -0,0 +1,103 @@
+#!/usr/bin/env python3
+"""
+test/unit_tests_d/ut_lockable.py: unit test for the MMGen suite's Lockable class
+"""
+
+from mmgen.common import *
+from mmgen.exception import *
+
+class unit_test(object):
+
+	def run_test(self,name,ut):
+
+		from mmgen.base_obj import AttrCtrl,Lockable
+
+		qmsg_r('Testing class AttrCtrl...')
+
+		class MyAttrCtrl(AttrCtrl):
+			foo = 'fooval'
+		ac = MyAttrCtrl()
+		ac.lock()
+
+		ac.foo = 'new fooval'
+		ac.foo = 'new fooval2'
+
+		class MyAttrCtrlClsCheck(AttrCtrl):
+			_use_class_attr = True
+			foo = 'fooval'
+			bar = None
+		acc = MyAttrCtrlClsCheck()
+		acc.lock()
+
+		acc.foo = 'new_fooval'
+		acc.foo = 'new_fooval2'
+		acc.bar = 'bar val'
+		acc.bar = 1 # class attribute bar is None, so can be set to any type
+
+		qmsg('OK')
+		qmsg_r('Testing class Lockable...')
+
+		class MyLockable(Lockable): # class has no attrs, like UserOpts
+			_set_ok = ('foo','baz')
+			_reset_ok = ('bar','baz')
+
+		lc = MyLockable()
+		lc.foo = None
+		lc.bar = 'barval'
+		lc.baz = 1
+		lc.qux = 1
+		lc.lock()
+
+		lc.foo = 'fooval2'
+		lc.bar = 'barval2'
+		lc.bar = 'barval3'
+		lc.baz = 2
+		lc.baz = 3
+
+		class MyLockableClsCheck(Lockable): # class has attrs, like GlobalContext
+			_use_class_attr = True
+			_set_ok = ('foo','baz')
+			_reset_ok = ('bar','baz')
+			foo = None
+			bar = 1
+			baz = 3.5
+			qux = 'quxval'
+
+		lcc = MyLockableClsCheck()
+		lcc.lock()
+
+		lcc.foo = 'fooval2' # class attribute foo is None, so can be set to any type
+		lcc.bar = 2
+		lcc.bar = 3   # bar is in reset list
+		lcc.baz = 3.2
+		lcc.baz = 3.1 # baz is in both lists
+		qmsg('OK')
+
+		qmsg('Checking error handling:')
+
+		def bad1(): ac.x = 1
+		def bad2(): acc.foo = 1
+		def bad3(): lc.foo = 'fooval3'
+		def bad4(): lc.baz = 'str'
+		def bad5(): lcc.bar = 'str'
+		def bad6(): lc.qux  = 2
+		def bad7(): lcc.qux = 'quxval2'
+		def bad8(): lcc.foo = 'fooval3'
+		def bad9(): lc.x = 1
+		def bad10(): lcc.x = 1
+
+		ut.process_bad_data((
+			('attr (1)',           'AttributeError', 'has no attr', bad1 ),
+			('attr (2)',           'AttributeError', 'has no attr', bad9 ),
+			('attr (3)',           'AttributeError', 'has no attr', bad10 ),
+			('attr type (1)',      'AttributeError', 'type',        bad2 ),
+			("attr type (2)",      'AttributeError', 'type',        bad4 ),
+			("attr type (3)",      'AttributeError', 'type',        bad5 ),
+			("attr (can't set)",   'AttributeError', 'read-only',   bad6 ),
+			("attr (can't set)",   'AttributeError', 'read-only',   bad7 ),
+			("attr (can't reset)", 'AttributeError', 'reset',       bad3 ),
+			("attr (can't reset)", 'AttributeError', 'reset',       bad8 ),
+		))
+
+		qmsg('OK')
+		return True

+ 0 - 1
test/unit_tests_d/ut_seedsplit.py

@@ -149,7 +149,6 @@ class unit_test(object):
 			seed = Seed(seed_bin)
 			ssm_save = SeedShareIdx.max_val
 			ssm = SeedShareIdx.max_val = 2048
-			g.debug_last_share_sid_len = 3
 			shares = SeedShareList(seed,count=ssm,id_str='foo',master_idx=1,debug_last_share=True)
 			lsid = shares.last_share.sid
 			collisions = shares.data['long'][lsid][1]