diff --git a/cmds/mmgen-autosign b/cmds/mmgen-autosign index c0964c45..be043fa5 100755 --- a/cmds/mmgen-autosign +++ b/cmds/mmgen-autosign @@ -312,6 +312,7 @@ def set_led(cmd): led_thread.start() def get_insert_status(): + if os.getenv('MMGEN_TEST_SUITE'): return True try: os.stat(os.path.join('/dev/disk/by-label/',part_label)) except: return False else: return True diff --git a/cmds/mmgen-split b/cmds/mmgen-split new file mode 100755 index 00000000..e0161754 --- /dev/null +++ b/cmds/mmgen-split @@ -0,0 +1,24 @@ +#!/usr/bin/env python + +# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution +# Copyright (C)2013-2017 Philemon +# +# 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 . + +""" +mmgen-split: Split funds after a fork using a timelocked transaction +""" + +from mmgen.main import launch +launch("split") diff --git a/mmgen/main_regtest.py b/mmgen/main_regtest.py index e3bd887a..a9554512 100755 --- a/mmgen/main_regtest.py +++ b/mmgen/main_regtest.py @@ -40,32 +40,41 @@ opts_data = lambda: { AVAILABLE COMMANDS - setup - set up system for regtest operation with MMGen - stop - stop the regtest coin daemon - bob - switch to Bob's wallet, starting daemon if necessary - alice - switch to Alice's wallet, starting daemon if necessary - user - show current user - generate - mine a block - send ADDR AMT - send amount AMT to address ADDR - test_daemon - test whether daemon is running - get_balances - get balances of Bob and Alice - show_mempool - show transaction IDs in mempool + setup - set up system for regtest operation with MMGen + fork COIN - create a fork of coin COIN + stop - stop the regtest coin daemon + bob - switch to Bob's wallet, starting daemon if necessary + alice - switch to Alice's wallet, starting daemon if necessary + user - show current user + generate N - mine n blocks (defaults to 1) + send ADDR AMT - send amount AMT of miner funds to address ADDR + test_daemon - test whether daemon is running + get_balances - get balances of Bob and Alice + show_mempool - show transaction IDs in mempool + cli [arguments] - execute an RPC call with arguments """ } cmd_args = opts.init(opts_data) cmds = ('setup','stop','generate','test_daemon','create_data_dir','bob','alice','miner','user','send', - 'wait_for_daemon','wait_for_exit','get_current_user','get_balances','show_mempool') + 'wait_for_daemon','wait_for_exit','get_current_user','get_balances','show_mempool','cli','fork') try: if cmd_args[0] == 'send': assert len(cmd_args) == 3 + elif cmd_args[0] == 'fork': + assert len(cmd_args) == 2 + elif cmd_args[0] == 'generate': + assert len(cmd_args) in (1,2) + if len(cmd_args) == 2: + cmd_args[1] = int(cmd_args[1]) + elif cmd_args[0] == 'cli': + pass else: assert cmd_args[0] in cmds and len(cmd_args) == 1 except: opts.usage() else: - args = cmd_args[1:] from mmgen.regtest import * - globals()[cmd_args[0]](*args) + globals()[cmd_args[0]](*cmd_args[1:]) diff --git a/mmgen/main_split.py b/mmgen/main_split.py new file mode 100755 index 00000000..1353e3aa --- /dev/null +++ b/mmgen/main_split.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python +# +# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution +# Copyright (C)2013-2017 Philemon +# +# 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 . + +# TODO: check that balances of output addrs are zero? + +""" +mmgen-split: Split funds after a replayable chain fork using a timelocked transaction +""" + +import time + +from mmgen.common import * + +opts_data = lambda: { + 'desc': """Split funds in a {pnm} wallet after a chain fork using a + timelocked transaction""".format(pnm=g.proj_name), + 'usage':'[opts] [output addr1] [output addr2]', + 'options': """ +-h, --help Print this help message +--, --longhelp Print help message for long options (common options) +-f, --tx-fees= f The transaction fees for each chain (comma-separated) +-c, --other-coin= c The coin symbol of the other chain (default: {oc}) +-B, --no-blank Don't blank screen before displaying unspent outputs +-d, --outdir= d Specify an alternate directory 'd' for output +-m, --minconf= n Minimum number of confirmations required to spend + outputs (default: 1) +-q, --quiet Suppress warnings; overwrite files without prompting +-r, --rbf Make transaction BIP 125 replaceable (replace-by-fee) +-v, --verbose Produce more verbose output +-y, --yes Answer 'yes' to prompts, suppress non-essential output +-R, --rpc-host2= h Host the other coin daemon is running on (default: none) +-L, --locktime= t Lock time (block height or unix seconds) + (default: {bh}) +""".format(oc=g.proto.forks[-1][2].upper(),bh='current block height'), + 'notes': """\n +This command creates two transactions: one (with the timelock) to be broadcast +on the long chain and one on the short chain after a replayable chain fork. +Only {pnm} addresses may be spent to. + +The command must be run on the longest chain. The user is reponsible for +ensuring that the current chain is the longest. The other chain is specified +on the command line, or it defaults to the most recent replayable fork of the +current chain. + +For the split to have a reasonable chance of succeeding, the long chain should +be well ahead of the short one (by more than 20 blocks or so) and transactions +should have a good chance of confirming quickly on both chains. For this +larger than normal fees may be required. Fees may be specified on the command +line, or network fee estimation may be used. + +If the split fails (i.e. the long-chain TX is broadcast and confirmed on the +short chain), no funds are lost. A new split attempt can be made with the +long-chain transaction's output as an input for the new split transaction. +This process can be repeated as necessary until the split succeeds. + +IMPORTANT: Timelock replay protection offers NO PROTECTION against reorg +attacks on the majority chain or reorg attacks on the minority chain if the +minority chain is ahead of the timelock. If the reorg'd minority chain is +behind the timelock, protection is contingent on getting the non-timelocked +transaction reconfirmed before the timelock expires. Use at your own risk. +""".format(pnm=g.proj_name) +} + +cmd_args = opts.init(opts_data,add_opts=['tx_fee','tx_fee_adj','comment_file']) + +opt.other_coin = opt.other_coin.upper() if opt.other_coin else g.proto.forks[-1][2].upper() +if opt.other_coin.lower() not in [e[2] for e in g.proto.forks if e[3] == True]: + die(1,"'{}': not a replayable fork of {} chain".format(opt.other_coin,g.coin)) + +if len(cmd_args) != 2: + fs = 'This command requires exactly two {} addresses as arguments' + die(1,fs.format(g.proj_name)) + +from mmgen.obj import MMGenID +try: + mmids = [MMGenID(a,on_fail='die') for a in cmd_args] +except: + die(1,'Command line arguments must be valid MMGen IDs') + +if mmids[0] == mmids[1]: + die(2,'Both transactions have the same output! ({})'.format(mmids[0])) + +from mmgen.tx import MMGenSplitTX +from mmgen.protocol import CoinProtocol + +if opt.tx_fees: + g_coin_save = g.coin + for idx,g_coin in ((1,opt.other_coin),(0,g_coin_save)): + g.coin = g_coin + g.proto = CoinProtocol(g.coin,g.testnet) + opt.tx_fee = opt.tx_fees.split(',')[idx] + opts.opt_is_tx_fee(opt.tx_fee,'transaction fee') or sys.exit(1) + +rpc_init(reinit=True) + +tx1 = MMGenSplitTX() +opt.no_blank = True + +gmsg("Creating timelocked transaction for long chain ({})".format(g.coin)) +locktime = int(opt.locktime or 0) or g.rpch.getblockcount() +tx1.create(mmids[0],locktime) + +tx1.format() +tx1.create_fn() + +gmsg("\nCreating transaction for short chain ({})".format(opt.other_coin)) + +if opt.rpc_host2: g.rpc_host = opt.rpc_host2 +if opt.tx_fees: opt.tx_fee = opt.tx_fees.split(',')[1] + +from mmgen.protocol import CoinProtocol +g.coin = opt.other_coin +g.proto = CoinProtocol(g.coin,g.testnet) +reload(sys.modules['mmgen.tx']) + +tx2 = MMGenSplitTX() +tx2.inputs = tx1.inputs +tx2.inputs.convert_coin() + +tx2.create_split(mmids[1]) + +for tx,desc in ((tx1,'Long chain (timelocked)'),(tx2,'Short chain')): + tx.desc = desc + ' transaction' + tx.write_to_file(ask_write=False,ask_overwrite=not opt.yes,ask_write_default_yes=False) diff --git a/mmgen/main_txbump.py b/mmgen/main_txbump.py index 61b5ae1f..46715d50 100755 --- a/mmgen/main_txbump.py +++ b/mmgen/main_txbump.py @@ -133,7 +133,7 @@ if not silent: if seed_files or kl or kal: txsign(tx,seed_files,kl,kal) tx.write_to_file(ask_write=False) - if tx.send(): - tx.write_to_file(ask_write=False) + tx.send(exit_on_fail=True) + tx.write_to_file(ask_write=False) else: tx.write_to_file(ask_write=not opt.yes,ask_write_default_yes=False,ask_overwrite=not opt.yes) diff --git a/mmgen/main_txcreate.py b/mmgen/main_txcreate.py index 062f8055..0fb1593b 100755 --- a/mmgen/main_txcreate.py +++ b/mmgen/main_txcreate.py @@ -39,6 +39,7 @@ opts_data = lambda: { per byte (an integer followed by 's'). If omitted, fee will be calculated using {dn}'s 'estimatefee' call -i, --info Display unspent outputs and exit +-L, --locktime= t Lock time (block height or unix seconds) (default: 0) -m, --minconf= n Minimum number of confirmations required to spend outputs (default: 1) -q, --quiet Suppress warnings; overwrite files without prompting @@ -55,5 +56,5 @@ rpc_init() from mmgen.tx import MMGenTX tx = MMGenTX() -tx.create(cmd_args,do_info=opt.info) +tx.create(cmd_args,int(opt.locktime or 0),do_info=opt.info) tx.write_to_file(ask_write=not opt.yes,ask_overwrite=not opt.yes,ask_write_default_yes=False) diff --git a/mmgen/main_txdo.py b/mmgen/main_txdo.py index 51b57e5e..313ec940 100755 --- a/mmgen/main_txdo.py +++ b/mmgen/main_txdo.py @@ -52,6 +52,7 @@ opts_data = lambda: { -K, --key-generator= m Use method 'm' for public key generation Options: {kgs} (default: {kg}) +-L, --locktime= t Lock time (block height or unix seconds) (default: 0) -m, --minconf=n Minimum number of confirmations required to spend outputs (default: 1) -M, --mmgen-keys-from-file=f Provide keys for {pnm} addresses in a key- @@ -89,9 +90,9 @@ kl = get_keylist(opt) if kl and kal: kl.remove_dup_keys(kal) tx = MMGenTX(caller='txdo') -tx.create(cmd_args) +tx.create(cmd_args,int(opt.locktime or 0)) txsign(tx,seed_files,kl,kal) tx.write_to_file(ask_write=False) -if tx.send(): - tx.write_to_file(ask_overwrite=False,ask_write=False) +tx.send(exit_on_fail=True) +tx.write_to_file(ask_overwrite=False,ask_write=False) diff --git a/mmgen/main_txsend.py b/mmgen/main_txsend.py index fb30305a..8bfe4d15 100755 --- a/mmgen/main_txsend.py +++ b/mmgen/main_txsend.py @@ -64,5 +64,5 @@ if not opt.yes: if tx.add_comment(): # edits an existing comment, returns true if changed tx.write_to_file(ask_write_default_yes=True) -if tx.send(): - tx.write_to_file(ask_overwrite=False,ask_write=False) +tx.send(exit_on_fail=True) +tx.write_to_file(ask_overwrite=False,ask_write=False) diff --git a/mmgen/opts.py b/mmgen/opts.py index dbf33837..ae638c14 100755 --- a/mmgen/opts.py +++ b/mmgen/opts.py @@ -253,7 +253,6 @@ def init(opts_f,add_opts=[],opt_filter=None): setattr(opt,'set_by_user',[]) for k in g.global_sets_opt: if k in opt.__dict__ and getattr(opt,k) != None: -# _typeconvert_from_dfl(k) setattr(opt,k,set_for_type(getattr(opt,k),getattr(g,k),'--'+k)) opt.set_by_user.append(k) else: @@ -277,7 +276,7 @@ def init(opts_f,add_opts=[],opt_filter=None): if g.bob or g.alice: g.testnet = True g.proto = CoinProtocol(g.coin,g.testnet) - g.data_dir = os.path.join(g.data_dir_root,'regtest',('alice','bob')[g.bob]) + g.data_dir = os.path.join(g.data_dir_root,'regtest',g.coin.lower(),('alice','bob')[g.bob]) check_or_create_dir(g.data_dir) import regtest as rt g.rpc_host = 'localhost' @@ -303,6 +302,19 @@ def init(opts_f,add_opts=[],opt_filter=None): return args +def opt_is_tx_fee(val,desc): + from mmgen.tx import MMGenTX + ret = MMGenTX().convert_fee_spec(val,224,on_fail='return') + if ret == False: + msg("'{}': invalid {} (not a {} amount or satoshis-per-byte specification)".format( + val,desc,g.coin.upper())) + elif ret != None and ret > g.proto.max_tx_fee: + msg("'{}': invalid {} (> max_tx_fee ({} {}))".format( + val,desc,g.proto.max_tx_fee,g.coin.upper())) + else: + return True + return False + def check_opts(usr_opts): # Returns false if any check fails def opt_splits(val,sep,n,desc): @@ -332,19 +344,6 @@ def check_opts(usr_opts): # Returns false if any check fails return False return True - def opt_is_tx_fee(val,desc): - from mmgen.tx import MMGenTX - ret = MMGenTX().convert_fee_spec(val,224,on_fail='return') - if ret == False: - msg("'{}': invalid {} (not a {} amount or satoshis-per-byte specification)".format( - val,desc,g.coin.upper())) - elif ret != None and ret > g.proto.max_tx_fee: - msg("'{}': invalid {} (> max_tx_fee ({} {}))".format( - val,desc,g.proto.max_tx_fee,g.coin.upper())) - else: - return True - return False - def opt_is_in_list(val,lst,desc): if val not in lst: q,sep = (('',','),("'","','"))[type(lst[0]) == str] @@ -461,6 +460,9 @@ def check_opts(usr_opts): # Returns false if any check fails m = "Regtest (Bob and Alice) mode not set up yet. Run '{}-regtest setup' to initialize." try: os.stat(daemon_dir) except: die(1,m.format(g.proj_name.lower())) + elif key == 'locktime': + if not opt_is_int(val,desc): return False + if not opt_compares(val,'>',0,desc): return False else: if g.debug: Msg("check_opts(): No test for opt '%s'" % key) diff --git a/mmgen/protocol.py b/mmgen/protocol.py index f325dee5..e3b18880 100755 --- a/mmgen/protocol.py +++ b/mmgen/protocol.py @@ -23,7 +23,7 @@ protocol.py: Coin protocol functions, classes and methods import os,hashlib from binascii import unhexlify from mmgen.util import msg,pmsg -from mmgen.obj import MMGenObject,BTCAmt,LTCAmt,BCHAmt +from mmgen.obj import MMGenObject,BTCAmt,LTCAmt,BCHAmt,B2XAmt from mmgen.globalvars import g def hash160(hexnum): # take hex, return hex - OP_HASH160 @@ -69,9 +69,9 @@ class BitcoinProtocol(MMGenObject): daemon_data_subdir = '' sighash_type = 'ALL' block0 = '000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f' - forks = [ - (478559,'00000000000000000019f112ec0a9982926f1258cdcc558dd7c3b7e5dc7fa148','bch'), - (None,'','b2x') + forks = [ # height, hash, name, replayable + (478559,'00000000000000000019f112ec0a9982926f1258cdcc558dd7c3b7e5dc7fa148','bch',False), + (None,'','b2x',True) ] caps = ('rbf','segwit') base_coin = 'BTC' @@ -160,7 +160,7 @@ class BitcoinCashProtocol(BitcoinProtocol): mmtypes = ('L','C') sighash_type = 'ALL|FORKID' forks = [ - (478559,'000000000000000000651ef99cb9fcbe0dadde1d424bd9f15ff20136191a5eec','btc') + (478559,'000000000000000000651ef99cb9fcbe0dadde1d424bd9f15ff20136191a5eec','btc',False) ] caps = () coin_amt = BCHAmt @@ -178,6 +178,24 @@ class BitcoinCashTestnetProtocol(BitcoinCashProtocol): data_subdir = 'testnet' daemon_data_subdir = 'testnet3' +class B2XProtocol(BitcoinProtocol): + daemon_name = 'bitcoind-2x' + daemon_data_dir = os.path.join(os.getenv('APPDATA'),'Bitcoin_2X') if g.platform == 'win' \ + else os.path.join(g.home_dir,'.bitcoin-2x') + rpc_port = 8338 + coin_amt = B2XAmt + max_tx_fee = B2XAmt('0.1') + forks = [ + (None,'','btc',True) + ] + +class B2XTestnetProtocol(B2XProtocol): + addr_ver_num = { 'p2pkh': ('6f','mn'), 'p2sh': ('c4','2') } + privkey_pfx = 'ef' + data_subdir = 'testnet' + daemon_data_subdir = 'testnet5' + rpc_port = 18338 + class LitecoinProtocol(BitcoinProtocol): block0 = '12a765e31ffd4059bada1e25190f6e98c99d9714d334efa41a195a7e7e04bfe2' name = 'litecoin' @@ -210,6 +228,7 @@ class CoinProtocol(MMGenObject): coins = { 'btc': (BitcoinProtocol,BitcoinTestnetProtocol), 'bch': (BitcoinCashProtocol,BitcoinCashTestnetProtocol), + 'b2x': (B2XProtocol,B2XTestnetProtocol), 'ltc': (LitecoinProtocol,LitecoinTestnetProtocol), # 'eth': (EthereumProtocol,EthereumTestnetProtocol), } diff --git a/mmgen/regtest.py b/mmgen/regtest.py index e51c8253..1b7883df 100755 --- a/mmgen/regtest.py +++ b/mmgen/regtest.py @@ -24,38 +24,40 @@ import os,subprocess,time,shutil from mmgen.common import * PIPE = subprocess.PIPE -data_dir = os.path.join(g.data_dir_root,'regtest') +data_dir = os.path.join(g.data_dir_root,'regtest',g.coin.lower()) daemon_dir = os.path.join(data_dir,'regtest') -rpc_port = 8552 +rpc_ports = { 'btc':8552, 'bch':8553, 'b2x':8554, 'ltc':8555 } +rpc_port = rpc_ports[g.coin.lower()] rpc_user = 'bobandalice' rpc_password = 'hodltothemoon' -init_amt = 500 tr_wallet = lambda user: os.path.join(daemon_dir,'wallet.dat.'+user) -common_args = ( +common_args = lambda: ( '-rpcuser={}'.format(rpc_user), '-rpcpassword={}'.format(rpc_password), '-rpcport={}'.format(rpc_port), '-regtest', '-datadir={}'.format(data_dir)) -def start_daemon(user,quiet=False,daemon=True): +def start_daemon(user,quiet=False,daemon=True,reindex=False): cmd = ( g.proto.daemon_name, + '-listen=0', '-keypool=1', '-wallet={}'.format(os.path.basename(tr_wallet(user))) - ) + common_args + ) + common_args() if daemon: cmd += ('-daemon',) + if reindex: cmd += ('-reindex',) if not g.debug or quiet: vmsg('{}'.format(' '.join(cmd))) p = subprocess.Popen(cmd,stdout=PIPE,stderr=PIPE) err = process_output(p,silent=False)[1] if err: rdie(1,'Error starting the {} daemon:\n{}'.format(g.proto.name.capitalize(),err)) -def start_daemon_mswin(user,quiet=False): +def start_daemon_mswin(user,quiet=False,reindex=False): import threading - t = threading.Thread(target=start_daemon,args=[user,quiet,False]) + t = threading.Thread(target=start_daemon,args=[user,quiet,False,reindex]) t.daemon = True t.start() if not opt.verbose: Msg_r(' \b') # blocks w/o this...crazy @@ -63,7 +65,7 @@ def start_daemon_mswin(user,quiet=False): def start_cmd(*args,**kwargs): cmd = args if args[0] == 'cli': - cmd = (g.proto.name+'-cli',) + common_args + args[1:] + cmd = (g.proto.name+'-cli',) + common_args() + args[1:] if g.debug or not 'quiet' in kwargs: vmsg('{}'.format(' '.join(cmd))) ip = op = ep = (PIPE,None)['no_pipe' in kwargs and kwargs['no_pipe']] @@ -113,15 +115,16 @@ def get_balances(): msg('{:<16} {:12}'.format('Total balance:',tbal)) def create_data_dir(): - try: os.stat(daemon_dir) + try: os.stat(os.path.join(data_dir,'regtest')) # don't use daemon_dir, as data_dir may change except: pass else: - if keypress_confirm('Delete your existing MMGen regtest setup and create a new one?'): + m = "Delete your existing MMGen regtest setup at '{}' and create a new one?" + if keypress_confirm(m.format(data_dir)): shutil.rmtree(data_dir) else: die() - try: os.mkdir(data_dir) + try: os.makedirs(data_dir) except: pass def process_output(p,silent=False): @@ -133,9 +136,9 @@ def process_output(p,silent=False): vmsg('stderr: [{}]'.format(err.strip())) return out,err -def start_and_wait(user,silent=False,nonl=False): +def start_and_wait(user,silent=False,nonl=False,reindex=False): vmsg('Starting {} regtest daemon'.format(g.proto.name)) - (start_daemon_mswin,start_daemon)[g.platform=='linux'](user) + (start_daemon_mswin,start_daemon)[g.platform=='linux'](user,reindex=reindex) wait_for_daemon('ready',silent=silent,nonl=nonl) def stop_and_wait(silent=False,nonl=False,stop_silent=False,ignore_noconnect_error=False): @@ -156,9 +159,53 @@ def show_mempool(): msg(pformat(eval(p.stdout.read()))) p.wait() -def setup(): - try: os.mkdir(data_dir) +def cli(*args): + p = start_cmd(*(('cli',) + args)) + from pprint import pformat + Msg_r(p.stdout.read()) + msg_r(p.stderr.read()) + p.wait() + +def fork(coin): + coin = coin.upper() + from mmgen.protocol import CoinProtocol + forks = CoinProtocol(coin,False).forks + if not [f for f in forks if f[2] == g.coin.lower() and f[3] == True]: + die(1,"Coin {} is not a replayable fork of coin {}".format(g.coin,coin)) + + gmsg('Creating fork from coin {} to coin {}'.format(coin,g.coin)) + source_data_dir = os.path.join(g.data_dir_root,'regtest',coin.lower()) + + try: os.stat(source_data_dir) + except: die(1,"Source directory '{}' does not exist!".format(source_data_dir)) + + # stop the other daemon + global rpc_port,data_dir + rpc_port_save,data_dir_save = rpc_port,data_dir + rpc_port = rpc_ports[coin.lower()] + data_dir = os.path.join(g.data_dir_root,'regtest',coin.lower()) + if test_daemon() != 'stopped': + stop_and_wait(silent=True,stop_silent=True) + rpc_port,data_dir = rpc_port_save,data_dir_save + + try: os.makedirs(data_dir) except: pass + + # stop our daemon + if test_daemon() != 'stopped': + stop_and_wait(silent=True,stop_silent=True) + + create_data_dir() + os.rmdir(data_dir) + shutil.copytree(source_data_dir,data_dir,symlinks=True) + start_and_wait('miner',reindex=True,silent=True) + stop_and_wait(silent=True,stop_silent=True) + gmsg('Fork {} successfully created'.format(g.coin)) + +def setup(): + try: os.makedirs(data_dir) + except: pass + if test_daemon() != 'stopped': stop_and_wait(silent=True,stop_silent=True) create_data_dir() @@ -192,7 +239,7 @@ def get_current_user_win(quiet=False): return None def get_current_user_unix(quiet=False): - p = start_cmd('pgrep','-af','{}.*-rpcuser={}.*'.format(g.proto.daemon_name,rpc_user)) + p = start_cmd('pgrep','-af','{}.*-rpcport={}.*'.format(g.proto.daemon_name,rpc_port)) cmdline = p.stdout.read() if not cmdline: return None for k in ('miner','bob','alice'): @@ -214,20 +261,20 @@ def user(user=None,quiet=False): wait_for_daemon('ready') if test_daemon() == 'ready': if user == get_current_user(quiet=True): - if not quiet: msg('{} is already the current user'.format(user.capitalize())) + if not quiet: msg('{} is already the current user for coin {}'.format(user.capitalize(),g.coin)) return True - gmsg_r('Switching to user {}'.format(user.capitalize())) + gmsg_r('Switching to user {} for coin {}'.format(user.capitalize(),g.coin)) stop_and_wait(silent=False,nonl=True,stop_silent=True) time.sleep(0.1) # file lock has race condition - TODO: test for lock file start_and_wait(user,nonl=True) else: - gmsg_r('Starting regtest daemon with current user {}'.format(user.capitalize())) + gmsg_r('Starting regtest daemon for coin {} with current user {}'.format(g.coin,user.capitalize())) start_and_wait(user,nonl=True) gmsg('done') def stop(silent=False,ignore_noconnect_error=True): if test_daemon() != 'stopped' and not silent: - gmsg('Stopping {} regtest daemon'.format(g.proto.name)) + gmsg('Stopping {} regtest daemon for coin {}'.format(g.proto.name,g.coin)) p = start_cmd('cli','stop') err = process_output(p)[1] if err: diff --git a/mmgen/rpc.py b/mmgen/rpc.py index 19b84a3c..c4e9c62b 100755 --- a/mmgen/rpc.py +++ b/mmgen/rpc.py @@ -35,7 +35,7 @@ class CoinDaemonRPCConnection(object): import socket try: - socket.create_connection((host,port)).close() + socket.create_connection((host,port),timeout=3).close() except: die(1,'Unable to connect to {}:{}'.format(host,port)) diff --git a/mmgen/tx.py b/mmgen/tx.py index 90dd2097..3ae8079b 100755 --- a/mmgen/tx.py +++ b/mmgen/tx.py @@ -60,6 +60,25 @@ only one output, specify a single output address with no {} amount """.strip().format(g.coin), } +def strfmt_locktime(num,terse=False): + # Locktime itself is an unsigned 4-byte integer which can be parsed two ways: + # + # If less than 500 million, locktime is parsed as a block height. The transaction can be + # added to any block which has this height or higher. + # MMGen note: s/this height or higher/a higher block height/ + # + # If greater than or equal to 500 million, locktime is parsed using the Unix epoch time + # format (the number of seconds elapsed since 1970-01-01T00:00 UTC). The transaction can be + # added to any block whose block time is greater than the locktime. + if num >= 5 * 10**6: + return ' '.join(time.strftime('%c',time.gmtime(num)).split()[1:]) + elif num > 0: + return '{}{}'.format(('block height ','')[terse],num) + elif num == None: + return '(None)' + else: + die(2,"'{}': invalid locktime value!".format(num)) + def select_unspent(unspent,prompt): while True: reply = my_raw_input(prompt).strip() @@ -258,10 +277,12 @@ class MMGenTX(MMGenObject): self.timestamp = '' self.chksum = '' self.fmt_data = '' + self.fn = '' self.blockcount = 0 self.chain = None self.coin = None self.caller = caller + self.locktime = None if filename: self.parse_tx_file(filename,md_only=md_only) @@ -276,7 +297,7 @@ class MMGenTX(MMGenObject): die(2,'Transaction is for {}, but current chain is {}!'.format(self.chain,g.chain)) def add_output(self,coinaddr,amt,is_chg=None): - self.outputs.append(self.MMGenTxOutput(addr=coinaddr,amt=amt,is_chg=is_chg)) + self.outputs.append(MMGenTX.MMGenTxOutput(addr=coinaddr,amt=amt,is_chg=is_chg)) def get_chg_output_idx(self): for i in range(len(self.outputs)): @@ -287,7 +308,7 @@ class MMGenTX(MMGenObject): def update_output_amt(self,idx,amt): o = self.outputs[idx].__dict__ o['amt'] = amt - self.outputs[idx] = self.MMGenTxOutput(**o) + self.outputs[idx] = MMGenTX.MMGenTxOutput(**o) def del_output(self,idx): self.outputs.pop(idx) @@ -300,7 +321,8 @@ class MMGenTX(MMGenObject): def add_mmaddrs_to_outputs(self,ad_w,ad_f): a = [e.addr for e in self.outputs] d = ad_w.make_reverse_dict(a) - d.update(ad_f.make_reverse_dict(a)) + if ad_f: + d.update(ad_f.make_reverse_dict(a)) for e in self.outputs: if e.addr and e.addr in d: e.mmid,f = d[e.addr] @@ -316,13 +338,16 @@ class MMGenTX(MMGenObject): die(2,'{}: duplicate address in transaction {}'.format(attr,io_str)) old_attr = attr + def update_txid(self): + self.txid = MMGenTxID(make_chksum_6(unhexlify(self.hex)).upper()) + def create_raw(self): i = [{'txid':e.txid,'vout':e.vout} for e in self.inputs] if self.inputs[0].sequence: i[0]['sequence'] = self.inputs[0].sequence o = dict([(e.addr,e.amt) for e in self.outputs]) self.hex = g.rpch.createrawtransaction(i,o) - self.txid = MMGenTxID(make_chksum_6(unhexlify(self.hex)).upper()) + self.update_txid() # returns true if comment added or changed def add_comment(self,infile=None): @@ -484,8 +509,8 @@ class MMGenTX(MMGenObject): def decode_io(self,desc,data): io,il = ( - (self.MMGenTxOutput,self.MMGenTxOutputList), - (self.MMGenTxInput,self.MMGenTxInputList) + (MMGenTX.MMGenTxOutput,MMGenTX.MMGenTxOutputList), + (MMGenTX.MMGenTxInput,MMGenTX.MMGenTxInputList) )[desc=='inputs'] return il([io(**dict([(k,d[k]) for k in io.__dict__ if k in d and d[k] not in ('',None)])) for d in data]) @@ -519,6 +544,13 @@ class MMGenTX(MMGenObject): def add_timestamp(self): self.timestamp = make_timestamp() + def get_hex_locktime(self): + return int(hexlify(unhexlify(self.hex[-8:])[::-1]),16) + + def set_hex_locktime(self,val): + assert type(val) == int,'locktime value not an integer' + self.hex = self.hex[:-8] + hexlify(unhexlify('{:08x}'.format(val))[::-1]) + def add_blockcount(self): self.blockcount = int(g.rpch.getblockcount()) @@ -526,13 +558,14 @@ class MMGenTX(MMGenObject): self.inputs.check_coin_mismatch() self.outputs.check_coin_mismatch() lines = [ - '{}{} {} {} {} {}'.format( + '{}{} {} {} {} {}{}'.format( (g.coin+' ','')[g.coin=='BTC'], self.chain.upper() if self.chain else 'Unknown', self.txid, self.send_amt, self.timestamp, - self.blockcount + self.blockcount, + ('',' LT={}'.format(self.locktime))[bool(self.locktime)] ), self.hex, repr([e.__dict__ for e in self.inputs]), @@ -624,20 +657,25 @@ class MMGenTX(MMGenObject): ret = self.desc == 'signed transaction' return (red,green)[ret](str(ret)) if color else ret - # protect against an attack where a malicious, compromised or malfunctioning coin daemon could switch - # hex transaction data. + # check that a malicious, compromised or malfunctioning coin daemon hasn't altered hex tx data: def check_hex_tx_matches_mmgen_tx(self,deserial_tx): - m = 'Fatal error: a malicious or malfunctioning coin daemon or other program has altered your data!' + m = 'Fatal error: a malicious or malfunctioning coin daemon or other program may have altered your data!' - if deserial_tx['lock_time'] != 0: - rdie(3,'\nLock time is not zero!\n' + m) + lt = deserial_tx['lock_time'] + if lt != int(self.locktime or 0): + m2 = '\nTransaction hex locktime ({}) does not match MMGen transaction locktime ({})\n{}' + rdie(3,m2.format(lt,self.locktime,m)) - def check_equal(desc,mmio,hexio): + def check_equal(desc,hexio,mmio): if mmio != hexio: msg('\nMMGen {}:\n{}'.format(desc,pformat(mmio))) msg('Hex {}:\n{}'.format(desc,pformat(hexio))) - m2 = '{} in hex transaction data from coin daemon do not match those in MMGen transaction!\n' + m - rdie(3,m2.format(desc.capitalize())) + m2 = '{} in hex transaction data from coin daemon do not match those in MMGen transaction!\n' + rdie(3,(m2+m).format(desc.capitalize())) + + seq_hex = map(lambda i: int(i['nSeq'],16),deserial_tx['txins']) + seq_mmgen = map(lambda i: i.sequence or g.max_int,self.inputs) + check_equal('sequence numbers',seq_hex,seq_mmgen) d_hex = sorted((i['txid'],i['vout']) for i in deserial_tx['txins']) d_mmgen = sorted((i.txid,i.vout) for i in self.inputs) @@ -649,7 +687,7 @@ class MMGenTX(MMGenObject): uh = deserial_tx['unsigned_hex'] if str(self.txid) != make_chksum_6(unhexlify(uh)).upper(): - die(3,'MMGen TxID ({}) does not match hex transaction data!'.format(self.txid)) + rdie(3,'MMGen TxID ({}) does not match hex transaction data!\n{}'.format(self.txid,m)) def check_sigs(self,deserial_tx=None): # return False if no sigs, die on error txins = (deserial_tx or DeserializedTX(self.hex))['txins'] @@ -706,7 +744,7 @@ class MMGenTX(MMGenObject): if ret: die(1,'Transaction has been replaced'+('',', and the replacement TX is confirmed')[ret==2]+'!') - def send(self,prompt_user=True): + def send(self,prompt_user=True,exit_on_fail=False): if not self.marked_signed(): die(1,'Transaction is not signed!') @@ -745,10 +783,14 @@ class MMGenTX(MMGenObject): elif 'Illegal use of SIGHASH_FORKID' in errmsg: m = 'The Aug. 1 2017 UAHF is not yet active on this chain.' m += "\nRe-run the script without the --coin=bch option." + elif '64: non-final' in errmsg: + m2 = "Transaction with locktime '{}' can't be included in this block!" + m = m2.format(strfmt_locktime(self.get_hex_locktime())) else: m = errmsg msg(yellow(m)) msg(red('Send of MMGen transaction {} failed'.format(self.txid))) + if exit_on_fail: sys.exit(1) return False else: if bogus_send: @@ -768,24 +810,28 @@ class MMGenTX(MMGenObject): ask_write=ask_write, ask_write_default_yes=ask_write_default_yes) + def create_fn(self): + tl = self.get_hex_locktime() + self.fn = '{}{}[{!s}{}{}].{}'.format( + self.txid, + ('-'+g.coin,'')[g.coin=='BTC'], + self.send_amt, + ('',',{}'.format(self.btc2spb(self.get_fee())))[self.is_rbf()], + ('',',tl={}'.format(tl))[bool(tl)], + self.ext) + def write_to_file( self, add_desc='', ask_write=True, ask_write_default_yes=False, ask_tty=True, - ask_overwrite=True, - fn=None): - if ask_write == False: - ask_write_default_yes=True - self.format() - if not fn: - fn = '{}{}[{}{}].{}'.format( - self.txid, - ('-'+g.coin,'')[g.coin=='BTC'], - self.send_amt, - ('',',{}'.format(self.btc2spb(self.get_fee())))[self.is_rbf()], - self.ext) - write_data_to_file(fn,self.fmt_data,self.desc+add_desc, + ask_overwrite=True): + + if ask_write == False: ask_write_default_yes = True + if not self.fmt_data: self.format() + if not self.fn: self.create_fn() + + write_data_to_file(self.fn,self.fmt_data,self.desc+add_desc, ask_overwrite=ask_overwrite, ask_write=ask_write, ask_tty=ask_tty, @@ -814,9 +860,6 @@ class MMGenTX(MMGenObject): def is_rbf(self): return self.inputs[0].sequence == g.max_int - 2 - def signal_for_rbf(self): - self.inputs[0].sequence = g.max_int - 2 - def format_view(self,terse=False): try: rpc_init() @@ -824,12 +867,6 @@ class MMGenTX(MMGenObject): except: blockcount = None - hdr_fs = ( - 'TRANSACTION DATA\n\n[ID:{}] [{} {}] [{} UTC] [RBF:{}] [Signed:{}]\n', - 'Transaction {} {} {} ({} UTC) RBF={} Signed={}\n' - )[bool(terse)] - nonmm_str = '(non-{pnm} address)'.format(pnm=g.proj_name) - def get_max_mmwid(io): if io == self.inputs: sel_f = lambda o: len(o.mmid) + 2 # len('()') @@ -837,6 +874,7 @@ class MMGenTX(MMGenObject): sel_f = lambda o: len(o.mmid) + (2,8)[bool(o.is_chg)] # + len(' (chg)') return max(max([sel_f(o) for o in io if o.mmid] or [0]),len(nonmm_str)) + nonmm_str = '(non-{pnm} address)'.format(pnm=g.proj_name) max_mmwid = max(get_max_mmwid(self.inputs),get_max_mmwid(self.outputs)) def format_io(io): @@ -866,8 +904,18 @@ class MMGenTX(MMGenObject): io_out += '\n'.join([('{:>3} {:<8} {}'.format(*d)) for d in items if d[2]]) + '\n\n' return io_out - out = hdr_fs.format(self.txid.hl(),self.send_amt.hl(),g.coin,self.timestamp, - (red('False'),green('True'))[self.is_rbf()],self.marked_signed(color=True)) + hdr_fs = ( + 'TRANSACTION DATA\n\nID={} ({} {}) UTC={} RBF={} Sig={} Locktime={}\n', + 'TX {} ({} {}) UTC={} RBF={} Sig={} Locktime={}\n' + )[bool(terse)] + out = hdr_fs.format(self.txid.hl(), + self.send_amt.hl(), + g.coin, + self.timestamp, + (red('False'), + green('True'))[self.is_rbf()], + self.marked_signed(color=True), + (green('None'),orange(strfmt_locktime(self.locktime,terse=True)))[bool(self.locktime)]) if self.chain in ('testnet','regtest'): out += green('Chain: {}\n'.format(self.chain.upper())) if self.coin_txid: @@ -925,6 +973,10 @@ class MMGenTX(MMGenObject): metadata,self.hex,inputs_data,outputs_data = tx_data metadata = metadata.split() + if metadata[-1].find('LT=') == 0: + desc = 'locktime' + self.locktime = int(metadata.pop()[3:]) + self.coin = metadata.pop(0) if len(metadata) == 6 else 'BTC' if len(metadata) == 5: @@ -955,7 +1007,7 @@ class MMGenTX(MMGenObject): if not self.chain and not self.inputs[0].addr.is_for_chain('testnet'): self.chain = 'mainnet' - def get_fee_from_estimate_or_user(self,estimate_fail_msg_shown=[]): + def get_fee_from_user(self,have_estimate_fail=[]): if opt.tx_fee: desc = 'User-selected' @@ -964,9 +1016,9 @@ class MMGenTX(MMGenObject): desc = 'Network-estimated' ret = g.rpch.estimatefee(opt.tx_confs) if ret == -1: - if not estimate_fail_msg_shown: + if not have_estimate_fail: msg('Network fee estimation for {} confirmations failed'.format(opt.tx_confs)) - estimate_fail_msg_shown.append(True) + have_estimate_fail.append(True) start_fee = None else: start_fee = g.proto.coin_amt(ret) * opt.tx_fee_adj * self.estimate_size() / 1024 @@ -1039,7 +1091,7 @@ class MMGenTX(MMGenObject): self.copy_inputs_from_tw(sel_unspent) # makes self.inputs - change_amt = self.sum_inputs() - self.send_amt - self.get_fee_from_estimate_or_user() + change_amt = self.sum_inputs() - self.send_amt - self.get_fee_from_user() if change_amt >= 0: p = 'Transaction produces {} {} in change'.format(change_amt.hl(),g.coin) @@ -1049,7 +1101,8 @@ class MMGenTX(MMGenObject): else: msg(wmsg['not_enough_coin'].format(abs(change_amt))) - def create(self,cmd_args,do_info=False): + def create(self,cmd_args,locktime,do_info=False): + assert type(locktime) == int if opt.comment_file: self.add_comment(opt.comment_file) @@ -1072,7 +1125,9 @@ class MMGenTX(MMGenObject): change_amt = self.get_inputs_from_user(tw) - if opt.rbf: self.signal_for_rbf() # only after we have inputs + # only after we have inputs + if locktime: self.inputs[0].sequence = g.max_int - 1 + if opt.rbf: self.inputs[0].sequence = g.max_int - 2 chg_idx = self.get_chg_output_idx() @@ -1089,6 +1144,12 @@ class MMGenTX(MMGenObject): self.add_comment() # edits an existing comment self.create_raw() # creates self.hex, self.txid + if locktime: + msg('Setting nlocktime to {}!'.format(strfmt_locktime(locktime))) + self.set_hex_locktime(locktime) + self.update_txid() + self.locktime = locktime + self.add_timestamp() self.add_blockcount() self.chain = g.chain @@ -1165,3 +1226,77 @@ class MMGenBumpTX(MMGenTX): msg('{} {c}: {} fee too large. Maximum fee: <{} {c}'.format(ret,desc,output_amt,c=g.coin)) return False return ret + +class MMGenSplitTX(MMGenTX): + + def __init__(self): + super(type(self),self).__init__() + + def get_fee_from_user(self,have_estimate_fail=[],split_tx=False): + + if split_tx: + try: + rpc_init(reinit=True) + except: + ymsg('Connect to {} daemon failed. Network fee estimation unavailable'.format(g.coin)) + desc = 'User-selected' + try: + # TODO: check in opts.py + start_fee = opt.tx_fees.split(',')[split_tx] + except: + start_fee = None + return self.get_usr_fee_interactive(start_fee,desc=desc) + + return super(type(self),self).get_fee_from_user(have_estimate_fail=have_estimate_fail) + + def get_outputs_from_cmdline(self,mmid): + + from mmgen.addr import AddrData + ad_w = AddrData(source='tw') + + # TODO: check that addr is empty + + if is_mmgen_id(mmid): + coin_addr = mmaddr2coinaddr(mmid,ad_w,None) if is_mmgen_id(mmid) else CoinAddr(mmid) + self.add_output(coin_addr,g.proto.coin_amt('0'),is_chg=True) + else: + die(2,'{}: invalid command-line argument'.format(mmid)) + + self.add_mmaddrs_to_outputs(ad_w,None) + + if not segwit_is_active() and self.has_segwit_outputs(): + fs = '{} Segwit address requested on the command line, but Segwit is not active on this chain' + rdie(2,fs.format(g.proj_name)) + + def create_split(self,mmid): + + self.outputs = self.MMGenTxOutputList() + self.get_outputs_from_cmdline(mmid) + + while True: + change_amt = self.sum_inputs() - self.get_fee_from_user(split_tx=True) + if change_amt >= 0: + p = 'Transaction produces {} {} in change'.format(change_amt.hl(),g.coin) + if opt.yes or keypress_confirm(p+'. OK?',default_yes=True): + if opt.yes: msg(p) + break + else: + msg(wmsg['not_enough_coin'].format(abs(change_amt))) + + self.update_output_amt(0,change_amt) + self.send_amt = change_amt + + if not opt.yes: + self.add_comment() # edits an existing comment + self.create_raw() # creates self.hex, self.txid + + self.add_timestamp() + self.add_blockcount() # TODO + self.chain = g.chain + + assert self.sum_inputs() - self.sum_outputs() <= g.proto.max_tx_fee + + qmsg('Transaction successfully created') + + if not opt.yes: + self.view_with_prompt('View decoded transaction?') diff --git a/scripts/test-release.sh b/scripts/test-release.sh index 5ddff48a..1ea91188 100755 --- a/scripts/test-release.sh +++ b/scripts/test-release.sh @@ -2,7 +2,7 @@ # Tested on Linux, MinGW-64 # MinGW's bash 3.1.17 doesn't do ${var^^} -dfl_tests='obj misc btc btc_tn btc_rt bch bch_rt ltc ltc_tn ltc_rt tool gen' +dfl_tests='obj misc btc btc_tn btc_rt bch bch_rt b2x b2x_rt ltc ltc_tn ltc_rt tool gen' PROGNAME=$(basename $0) while getopts hinPt OPT do @@ -22,6 +22,8 @@ do echo " btc_rt - bitcoin regtest" echo " bch - bitcoin cash (BCH)" echo " bch_rt - bitcoin cash (BCH) regtest" + echo " b2x - bitcoin 2x (B2X)" + echo " b2x_rt - bitcoin 2x (B2X) regtest" echo " ltc - litecoin" echo " ltc_tn - litecoin testnet" echo " ltc_rt - litecoin regtest" @@ -51,7 +53,7 @@ set -e REFDIR=test/ref if uname -a | grep -qi mingw; then SUDO='' MINGW=1; else SUDO='sudo' MINGW=''; fi -function check { +check() { [ "$BRANCH" ] || { echo 'No branch specified. Exiting'; exit; } [ "$(git diff $BRANCH)" == "" ] || { echo "Unmerged changes from branch '$BRANCH'. Exiting" @@ -60,7 +62,7 @@ function check { git diff $BRANCH >/dev/null 2>&1 || exit } -function install { +install() { set -x eval "$SUDO rm -rf .test-release" git clone --branch $BRANCH --single-branch . .test-release @@ -74,7 +76,7 @@ function install { [ "$MINGW" ] && ./setup.py build --compiler=mingw32 eval "$SUDO ./setup.py install" } -function do_test { +do_test() { set +x for i in "$@"; do LS='\n' @@ -93,7 +95,7 @@ t_obj=( 'test/objtest.py --coin=ltc --testnet=1 -S') f_obj='Data object test complete' -i_misc='Miscellaneous operations' +i_misc='Miscellaneous operations' # includes autosign! s_misc='The bitcoin, bitcoin-abc and litecoin (mainnet) daemons must be running for the following tests' t_misc=( 'test/test.py -On misc') @@ -120,7 +122,9 @@ f_btc_tn='You may stop the bitcoin testnet daemon if you wish' i_btc_rt='Bitcoin regtest' s_btc_rt="The following tests will test MMGen's regtest (Bob and Alice) mode" -t_btc_rt=('test/test.py -On regtest') +t_btc_rt=( + 'test/test.py -On regtest' + 'test/test.py -On regtest_split') f_btc_rt="Regtest (Bob and Alice) mode tests for BTC completed" i_bch='Bitcoin cash (BCH)' @@ -133,6 +137,16 @@ s_bch_rt="The following tests will test MMGen's regtest (Bob and Alice) mode" t_bch_rt=('test/test.py --coin=bch -On regtest') f_bch_rt="Regtest (Bob and Alice) mode tests for BCH completed" +i_b2x='Bitcoin 2X (B2X)' +s_b2x='The bitcoin 2X daemon (BTC1) must both be running for the following tests' +t_b2x=('test/test.py -On --coin=b2x dfl_wallet main ref ref_other') +f_b2x='You may stop the Bitcoin 2X daemon if you wish' + +i_b2x_rt='Bitcoin 2X (B2X) regtest' +s_b2x_rt="The following tests will test MMGen's regtest (Bob and Alice) mode" +t_b2x_rt=('test/test.py --coin=b2x -On regtest') +f_b2x_rt="Regtest (Bob and Alice) mode tests for B2X completed" + i_ltc='Litecoin' s_ltc='The litecoin daemon must both be running for the following tests' t_ltc=( @@ -194,12 +208,13 @@ f_gen="gentest tests completed" } [ "$INSTALL_ONLY" ] && exit -function skip_maybe { +skip_maybe() { echo -n "Enter 's' to skip, or ENTER to continue: "; read [ "$REPLY" == 's' ] && return 0 return 1 } -function run_tests { + +run_tests() { for t in $1; do eval echo -e \${GREEN}'###' Running $(echo \$i_$t) tests\$RESET [ "$PAUSE" ] && { eval echo $(echo \$s_$t); skip_maybe && continue; } @@ -209,9 +224,17 @@ function run_tests { done } +check_args() { + for i in $tests; do + echo "$dfl_tests" | grep -q "\<$i\>" || { echo "$i: unrecognized argument"; exit; } + done +} + tests=$dfl_tests [ "$*" ] && tests="$*" [ "$NO_PAUSE" ] || PAUSE=1 + +check_args run_tests "$tests" echo -e "${GREEN}All OK$RESET" diff --git a/setup.py b/setup.py index c0f09432..6e140d45 100755 --- a/setup.py +++ b/setup.py @@ -136,6 +136,7 @@ setup( 'mmgen.main_passgen', 'mmgen.main_addrimport', 'mmgen.main_regtest', + 'mmgen.main_split', 'mmgen.main_txcreate', 'mmgen.main_txbump', 'mmgen.main_txsign', @@ -158,6 +159,7 @@ setup( 'cmds/mmgen-walletchk', 'cmds/mmgen-walletconv', 'cmds/mmgen-walletgen', + 'cmds/mmgen-split', 'cmds/mmgen-txcreate', 'cmds/mmgen-txbump', 'cmds/mmgen-txsign', diff --git a/test/ref/6A52BC-B2X[106.6789,tl=1320969600].rawtx b/test/ref/6A52BC-B2X[106.6789,tl=1320969600].rawtx new file mode 100644 index 00000000..f1db6589 --- /dev/null +++ b/test/ref/6A52BC-B2X[106.6789,tl=1320969600].rawtx @@ -0,0 +1,6 @@ +ff8ec9 +B2X MAINNET 6A52BC 106.6789 20171113_152611 434 LT=1320969600 +020000000321efc7591303a97400ed3bb813258762695f40100825255ab0f9d596310c654a0000000000feffffff21efc7591303a97400ed3bb813258762695f40100825255ab0f9d596310c654a0200000000ffffffff21efc7591303a97400ed3bb813258762695f40100825255ab0f9d596310c654a0100000000ffffffff0400e40b54020000001976a914e9721033b4cd4fae9e5510cdc9d7871dcf2be9e488ac30051d21000000001976a914b57ca511b3e5b54a7ec936b0fc6b44f595a7cffe88ac6c206028090000001976a9142794da6d30fc6adced41313ac3bfab900096b44488ac202cb2060000000017a914f12c150f224f045e75799410d6b6653af33568d8878065bc4e +[{'confs': 1, 'scriptPubKey': '76a9149b1a59d1411e2678a707b371e8e805bbacdde32288ac', 'addr': '1F97Jd89wwmu4ELadesAdGDzg3d8Y6j5iP', 'vout': 0, 'sequence': 4294967294, 'txid': '4a650c3196d5f9b05a25250810405f6962872513b83bed0074a9031359c7ef21', 'mmid': '98831F3A:C:1', 'amt': B2XAmt('100'), 'label': u''}, {'confs': 1, 'scriptPubKey': '76a914abe58e1e45f6176910a4c1ac1ee62328d5cc4fd588ac', 'addr': '1GfuYaKHrhdiVybXMGCcjadSgfjvpdt2x9', 'vout': 2, 'txid': '4a650c3196d5f9b05a25250810405f6962872513b83bed0074a9031359c7ef21', 'mmid': '98831F3A:L:2', 'amt': B2XAmt('200'), 'label': u''}, {'confs': 1, 'scriptPubKey': 'a914dd36f2365178f16504c5c38492a015be59bff33b87', 'addr': '3Mrh5fQwUwV9U2D1VHBrBW6cwqM4ZBdja9', 'vout': 1, 'txid': '4a650c3196d5f9b05a25250810405f6962872513b83bed0074a9031359c7ef21', 'mmid': '98831F3A:S:2', 'amt': B2XAmt('199.9999598'), 'label': u''}] +[{'is_chg': True, 'addr': '14cHfz8dixc3GL3HFZEbicjinguTkaL1BJ', 'amt': B2XAmt('393.3209406'), 'mmid': '98831F3A:L:4'}, {'addr': '1NHM5xG2x1972hGvPqi1X1bcQV4bg4B6zK', 'amt': B2XAmt('100')}, {'mmid': '98831F3A:C:5', 'amt': B2XAmt('5.5555'), 'addr': '1HYcdCFPmWakX2g8mP6ksxDDokDyRbeaAb'}, {'mmid': '98831F3A:S:3', 'amt': B2XAmt('1.1234'), 'addr': '3PgDafUUfbG4MYCXjKgzrfZTRFogLKS4fZ'}] +TvwWgaAnrkQFpAxxjBa4PHvJ8NsJDsurtiv2HuzdnXWjQmY7LHyt6PZn5J7BNtB5VzHtBG7bUosCAMFon8yxUe2mYTZoH9e6dpoAz9E6JDZtUNYz9YnF1Z3jFND1X89RuKAk6YVBrfWseeyHR8vZDdaFzBPK5SPos diff --git a/test/ref/6A52BC-B2X[106.6789,tl=1320969600].testnet.rawtx b/test/ref/6A52BC-B2X[106.6789,tl=1320969600].testnet.rawtx new file mode 100644 index 00000000..aaf673e1 --- /dev/null +++ b/test/ref/6A52BC-B2X[106.6789,tl=1320969600].testnet.rawtx @@ -0,0 +1,6 @@ +c14468 +B2X TESTNET 6A52BC 106.6789 20171113_152611 434 LT=1320969600 +020000000321efc7591303a97400ed3bb813258762695f40100825255ab0f9d596310c654a0000000000feffffff21efc7591303a97400ed3bb813258762695f40100825255ab0f9d596310c654a0200000000ffffffff21efc7591303a97400ed3bb813258762695f40100825255ab0f9d596310c654a0100000000ffffffff0400e40b54020000001976a914e9721033b4cd4fae9e5510cdc9d7871dcf2be9e488ac30051d21000000001976a914b57ca511b3e5b54a7ec936b0fc6b44f595a7cffe88ac6c206028090000001976a9142794da6d30fc6adced41313ac3bfab900096b44488ac202cb2060000000017a914f12c150f224f045e75799410d6b6653af33568d8878065bc4e +[{'confs': 1, 'addr': 'muf4bgD8kyD9qLpCMDqYTBSKY3DqTWZR92', 'vout': 0, 'sequence': 4294967294, 'label': u'', 'mmid': '98831F3A:C:1', 'txid': '4a650c3196d5f9b05a25250810405f6962872513b83bed0074a9031359c7ef21', 'amt': B2XAmt('100'), 'scriptPubKey': '76a9149b1a59d1411e2678a707b371e8e805bbacdde32288ac'}, {'confs': 1, 'addr': 'mwBrqdQGfj4yH6594qAzZVqmYfLdmB1C7W', 'vout': 2, 'label': u'', 'mmid': '98831F3A:L:2', 'txid': '4a650c3196d5f9b05a25250810405f6962872513b83bed0074a9031359c7ef21', 'amt': B2XAmt('200'), 'scriptPubKey': '76a914abe58e1e45f6176910a4c1ac1ee62328d5cc4fd588ac'}, {'confs': 1, 'addr': '2NDQu9QLy6PzVfoqZAQoioT5tABZENihnkH', 'vout': 1, 'label': u'', 'mmid': '98831F3A:S:2', 'txid': '4a650c3196d5f9b05a25250810405f6962872513b83bed0074a9031359c7ef21', 'amt': B2XAmt('199.9999598'), 'scriptPubKey': 'a914dd36f2365178f16504c5c38492a015be59bff33b87'}] +[{'addr': 'mj8Ey3DcXz3J3SWty8CyYXx3egWAapMVMF', 'mmid': '98831F3A:L:4', 'amt': B2XAmt('393.3209406'), 'is_chg': True}, {'addr': 'n2oJP1M1m2aMookY7QgPLvowGUfJYcFbZ7', 'amt': B2XAmt('100')}, {'mmid': '98831F3A:C:5', 'addr': 'mx4ZvFLNaY21J99kUx58hsRYfjpgPYwpHn', 'amt': B2XAmt('5.5555')}, {'mmid': '98831F3A:S:3', 'addr': '2NFEReQQWH3mQZKq5QTJsUcYidc1r35E2Rg', 'amt': B2XAmt('1.1234')}] +TvwWgaAnrkQFpAxxjBa4PHvJ8NsJDsurtiv2HuzdnXWjQmY7LHyt6PZn5J7BNtB5VzHtBG7bUosCAMFon8yxUe2mYTZoH9e6dpoAz9E6JDZtUNYz9YnF1Z3jFND1X89RuKAk6YVBrfWseeyHR8vZDdaFzBPK5SPos diff --git a/test/test.py b/test/test.py index c6bbf6bf..a538a627 100755 --- a/test/test.py +++ b/test/test.py @@ -151,22 +151,25 @@ ref_subdir = '' if g.proto.base_coin == 'BTC' else g.proto.name altcoin_pfx = '' if g.proto.base_coin == 'BTC' else '-'+g.proto.base_coin tn_ext = ('','.testnet')[g.testnet] -fork = {'bch':'btc','btc':'btc','ltc':'ltc'}[g.coin.lower()] -tx_fee = {'btc':'0.0001','bch':'0.001','ltc':'0.01'}[g.coin.lower()] -txbump_fee = {'btc':'123s','bch':'567s','ltc':'12345s'}[g.coin.lower()] +coin_sel = g.coin.lower() +if g.coin == 'B2X': coin_sel = 'btc' -rtFundAmt = {'btc':'500','bch':'500','ltc':'5500'}[g.coin.lower()] +fork = {'bch':'btc','btc':'btc','ltc':'ltc'}[coin_sel] +tx_fee = {'btc':'0.0001','bch':'0.001','ltc':'0.01'}[coin_sel] +txbump_fee = {'btc':'123s','bch':'567s','ltc':'12345s'}[coin_sel] + +rtFundAmt = {'btc':'500','bch':'500','ltc':'5500'}[coin_sel] rtFee = { 'btc': ('20s','10s','60s','0.0001','10s','20s'), 'bch': ('20s','10s','60s','0.0001','10s','20s'), 'ltc': ('1000s','500s','1500s','0.05','400s','1000s') -}[g.coin.lower()] +}[coin_sel] rtBals = { 'btc': ('499.999942','399.9998214','399.9998079','399.9996799','13.00000000','986.99957990','999.99957990'), 'bch': ('499.9999416','399.9999124','399.99989','399.9997616','276.22339397','723.77626763','999.99966160'), 'ltc': ('5499.9971','5399.994085','5399.993545','5399.987145','13.00000000','10986.93714500','10999.93714500'), -}[g.coin.lower()] -rtBobOp3 = {'btc':'S:2','bch':'L:3','ltc':'S:2'}[g.coin.lower()] +}[coin_sel] +rtBobOp3 = {'btc':'S:2','bch':'L:3','ltc':'S:2'}[coin_sel] if opt.segwit and 'S' not in g.proto.mmtypes: die(1,'--segwit option incompatible with {}'.format(g.proto.__name__)) @@ -201,12 +204,9 @@ cfgs = { }, 'segwit': get_segwit_val() }, - '17': { - 'tmpdir': os.path.join('test','tmp17'), - }, - '18': { - 'tmpdir': os.path.join('test','tmp18'), - }, + '17': { 'tmpdir': os.path.join('test','tmp17') }, + '18': { 'tmpdir': os.path.join('test','tmp18') }, + '19': { 'tmpdir': os.path.join('test','tmp19'), 'wpasswd':'abc' }, '1': { 'tmpdir': os.path.join('test','tmp1'), 'wpasswd': 'Dorian', @@ -452,6 +452,7 @@ cfgs = { 'ref_tx_file': { 'btc': 'FFB367[1.234]{}.rawtx', 'bch': '99BE60-BCH[106.6789]{}.rawtx', + 'b2x': '6A52BC-B2X[106.6789,tl=1320969600]{}.rawtx', 'ltc': '75F455-LTC[106.6789]{}.rawtx', }, 'ic_wallet': '98831F3A-5482381C-18460FB1[256,1].mmincog', @@ -671,6 +672,30 @@ cmd_group['regtest'] = ( ('regtest_stop', 'stopping regtest daemon'), ) +cmd_group['regtest_split'] = ( + ('regtest_split_setup', 'regtest forking scenario setup'), + ('regtest_walletgen_bob', "generating Bob's wallet"), + ('regtest_addrgen_bob', "generating Bob's addresses"), + ('regtest_addrimport_bob', "importing Bob's addresses"), + ('regtest_fund_bob', "funding Bob's wallet"), + ('regtest_split_fork', 'regtest split fork'), + ('regtest_split_start_btc', 'start regtest daemon (BTC)'), + ('regtest_split_start_b2x', 'start regtest daemon (B2X)'), + ('regtest_split_gen_btc', 'mining a block (BTC)'), + ('regtest_split_gen_b2x', 'mining 100 blocks (B2X)'), + ('regtest_split_do_split', 'creating coin splitting transactions'), + ('regtest_split_sign_b2x', 'signing B2X split transaction'), + ('regtest_split_sign_btc', 'signing BTC split transaction'), + ('regtest_split_send_b2x', 'sending B2X split transaction'), + ('regtest_split_send_btc', 'sending BTC split transaction'), + ('regtest_split_gen_btc', 'mining a block (BTC)'), + ('regtest_split_gen_b2x2', 'mining a block (B2X)'), + ('regtest_split_txdo_timelock_bad_btc', 'sending transaction with bad locktime (BTC)'), + ('regtest_split_txdo_timelock_good_btc','sending transaction with good locktime (BTC)'), + ('regtest_split_txdo_timelock_bad_b2x', 'sending transaction with bad locktime (B2X)'), + ('regtest_split_txdo_timelock_good_b2x','sending transaction with good locktime (B2X)'), +) + cmd_group['misc'] = ( ('autosign', 'transaction autosigning (BTC,BCH,LTC)'), ) @@ -743,6 +768,11 @@ for a,b in cmd_group['regtest']: cmd_list['regtest'].append(a) cmd_data[a] = (17,b,[[[],17]]) +cmd_data['info_regtest_split'] = 'regtest mode with fork and coin split',[17] +for a,b in cmd_group['regtest_split']: + cmd_list['regtest_split'].append(a) + cmd_data[a] = (19,b,[[[],19]]) + cmd_data['info_misc'] = 'miscellaneous operations',[18] for a,b in cmd_group['misc']: cmd_list['misc'].append(a) @@ -935,7 +965,7 @@ def create_fake_unspent_entry(coinaddr,al_id=None,idx=None,lbl=None,non_mmgen=Fa if 'S' not in g.proto.mmtypes: segwit = False if lbl: lbl = ' ' + lbl spk1,spk2 = (('76a914','88ac'),('a914','87'))[segwit and coinaddr.addr_fmt=='p2sh'] - amt1,amt2 = {'btc':(10,40),'bch':(10,40),'ltc':(1000,4000)}[g.coin.lower()] + amt1,amt2 = {'btc':(10,40),'bch':(10,40),'ltc':(1000,4000)}[coin_sel] return { 'account': '{}:{}'.format(g.proto.base_coin.lower(),coinaddr) if non_mmgen \ else (u'{}:{}{}'.format(al_id,idx,lbl.decode('utf8'))), @@ -1032,7 +1062,7 @@ def make_txcreate_cmdline(tx_data): coinaddr = AddrGenerator(t).to_addr(KeyGenerator().to_pubhex(privkey)) # total of two outputs must be < 10 BTC (<1000 LTC) - mods = {'btc':(6,4),'bch':(6,4),'ltc':(600,400)}[g.coin.lower()] + mods = {'btc':(6,4),'bch':(6,4),'ltc':(600,400)}[coin_sel] for k in cfgs: cfgs[k]['amts'] = [None,None] for idx,mod in enumerate(mods): @@ -1501,11 +1531,11 @@ class MMGenTestSuite(object): add = ' #' + tnum if tnum else '' t.written_to_file('Signed transaction' + add, oo=True) - def txsign(self,name,txfile,wf,pf='',bumpf='',save=True,has_label=False,txdo_handle=None): + def txsign(self,name,txfile,wf,pf='',bumpf='',save=True,has_label=False,txdo_handle=None,extra_opts=[]): if txdo_handle: t = txdo_handle else: - t = MMGenExpect(name,'mmgen-txsign', ['-d',cfg['tmpdir'],txfile]+([],[wf])[bool(wf)]) + t = MMGenExpect(name,'mmgen-txsign', extra_opts + ['-d',cfg['tmpdir'],txfile]+([],[wf])[bool(wf)]) t.license() t.tx_view() t.passphrase('MMGen wallet',cfg['wpasswd']) @@ -1522,18 +1552,24 @@ class MMGenTestSuite(object): def txsign_dfl_wallet(self,name,txfile,pf='',save=True,has_label=False): return self.txsign(name,txfile,wf=None,pf=pf,save=save,has_label=has_label) - def txsend(self,name,sigfile,txdo_handle=None): + def txsend(self,name,sigfile,txdo_handle=None,really_send=False,extra_opts=[]): if txdo_handle: t = txdo_handle else: - t = MMGenExpect(name,'mmgen-txsend', ['-d',cfg['tmpdir'],sigfile]) + if really_send: os.environ['MMGEN_BOGUS_SEND'] = '' + t = MMGenExpect(name,'mmgen-txsend', extra_opts + ['-d',cfg['tmpdir'],sigfile]) + if really_send: os.environ['MMGEN_BOGUS_SEND'] = '1' t.license() t.tx_view() t.expect('Add a comment to transaction? (y/N): ','\n') t.expect('Are you sure you want to broadcast this') m = 'YES, I REALLY WANT TO DO THIS' t.expect("'%s' to confirm: " % m,m+'\n') - t.expect('BOGUS transaction NOT sent') + if really_send: + txid = t.expect_getend('Transaction sent: ') + assert len(txid) == 64 + else: + t.expect('BOGUS transaction NOT sent') t.written_to_file('Sent transaction') t.ok() @@ -1818,7 +1854,7 @@ class MMGenTestSuite(object): cmp_or_die(hincog_offset,int(o)) # Miscellaneous tests - def autosign(self,name): + def autosign(self,name): # tests everything except device detection, mount/unmount if g.platform == 'win': msg('Skipping {} (not supported)'.format(name)); return fdata = (('btc',''),('bch',''),('ltc','litecoin')) @@ -2102,8 +2138,8 @@ class MMGenTestSuite(object): def regtest_walletgen_alice(self,name): return self.regtest_walletgen(name,'alice') @staticmethod - def regtest_user_dir(user): - return os.path.join(data_dir,'regtest',user) + def regtest_user_dir(user,coin=None): + return os.path.join(data_dir,'regtest',coin or g.coin.lower(),user) def regtest_user_sid(self,user): return os.path.basename(get_file_with_ext('mmdat',self.regtest_user_dir(user)))[:8] @@ -2183,7 +2219,15 @@ class MMGenTestSuite(object): cmp_or_die(ret,rtBals[6],skip_ok=True) t.ok() - def regtest_user_txdo(self,name,user,fee,outputs_cl,outputs_prompt,extra_args=[],wf=None,pw='abc',no_send=False,do_label=False): + def regtest_user_txdo( self,name,user,fee, + outputs_cl, + outputs_prompt, + extra_args=[], + wf=None, + pw='abc', + no_send=False, + do_label=False, + bad_locktime=False): os.environ['MMGEN_BOGUS_SEND'] = '' t = MMGenExpect(name,'mmgen-txdo', ['-d',cfg['tmpdir'],'-B','--'+user,'--tx-fee='+fee] @@ -2205,11 +2249,14 @@ class MMGenTestSuite(object): t.expect('to continue: ','\n') t.passphrase('MMGen wallet',pw) t.written_to_file('Signed transaction') - if not no_send: + if no_send: + t.read() + exit_val = 0 + else: t.expect('to confirm: ','YES, I REALLY WANT TO DO THIS\n') - t.expect('Transaction sent') - t.read() - t.ok() + s,exit_val = (('Transaction sent',0),("can't be included",1))[bad_locktime] + t.expect(s) + t.ok(exit_val) def regtest_bob_split1(self,name): sid = self.regtest_user_sid('bob') @@ -2269,9 +2316,11 @@ class MMGenTestSuite(object): txfile = get_file_with_ext(',{}].sigtx'.format(rtFee[1][:-1]),cfg['tmpdir'],delete=False,no_dot=True) return self.regtest_user_txbump(name,'bob',txfile,rtFee[2],'c') - def regtest_generate(self,name): - t = MMGenExpect(name,'mmgen-regtest',['generate']) - t.expect('Mined 1 block') + def regtest_generate(self,name,coin=None,num_blocks=1): + int(num_blocks) + if coin: opt.coin = coin + t = MMGenExpect(name,'mmgen-regtest',['generate',str(num_blocks)]) + t.expect('Mined {} block'.format(num_blocks)) t.ok() def regtest_get_mempool(self,name): @@ -2389,7 +2438,95 @@ class MMGenTestSuite(object): t = MMGenExpect(name,'mmgen-regtest',['stop']) t.ok() - # regtest undocumented admin commands + def regtest_split_setup(self,name): + if g.coin != 'BTC': die(1,'Test valid only for coin BTC') + opt.coin = 'BTC' + return self.regtest_setup(name) + + def regtest_split_fork(self,name): + opt.coin = 'B2X' + t = MMGenExpect(name,'mmgen-regtest',['fork','btc']) + t.expect('Creating fork from coin') + t.expect('successfully created') + t.ok() + + def regtest_split_start(self,name,coin): + opt.coin = coin + t = MMGenExpect(name,'mmgen-regtest',['bob']) + t.expect('Starting') + t.expect('done') + t.ok() + + def regtest_split_start_btc(self,name): self.regtest_split_start(name,coin='BTC') + def regtest_split_start_b2x(self,name): self.regtest_split_start(name,coin='B2X') + def regtest_split_gen_btc(self,name): self.regtest_generate(name,coin='BTC') + def regtest_split_gen_b2x(self,name): self.regtest_generate(name,coin='B2X',num_blocks=100) + def regtest_split_gen_b2x2(self,name): self.regtest_generate(name,coin='B2X') + + def regtest_split_do_split(self,name): + opt.coin = 'B2X' + sid = self.regtest_user_sid('bob') + t = MMGenExpect(name,'mmgen-split',[ + '--bob', + '--outdir='+cfg['tmpdir'], + '--tx-fees=0.0001,0.0003', + sid+':S:1',sid+':S:2']) + t.expect(r"'q'=quit view, .*?:.",'q', regex=True) + t.expect('outputs to spend: ','1\n') + + for tx in ('timelocked','split'): + for q in ('fee','change'): t.expect('OK? (Y/n): ','y') + t.expect('Add a comment to transaction? (y/N): ','n') + t.expect('View decoded transaction\? .*?: ','t',regex=True) + t.expect('to continue: ','\n') + + t.written_to_file('Long chain (timelocked) transaction') + t.written_to_file('Short chain transaction') + t.ok() + + def regtest_split_sign(self,name,coin,ext): + wf = get_file_with_ext('mmdat',self.regtest_user_dir('bob',coin=coin.lower())) + txfile = get_file_with_ext(ext,cfg['tmpdir'],no_dot=True) + opt.coin = coin + self.txsign(name,txfile,wf,extra_opts=['--bob']) + + def regtest_split_sign_b2x(self,name): + return self.regtest_split_sign(name,coin='B2X',ext='533].rawtx') + + def regtest_split_sign_btc(self,name): + return self.regtest_split_sign(name,coin='BTC',ext='9997].rawtx') + + def regtest_split_send(self,name,coin,ext): + opt.coin = coin + txfile = get_file_with_ext(ext,cfg['tmpdir'],no_dot=True) + self.txsend(name,txfile,really_send=True,extra_opts=['--bob']) + + def regtest_split_send_b2x(self,name): + return self.regtest_split_send(name,coin='B2X',ext='533].sigtx') + + def regtest_split_send_btc(self,name): + return self.regtest_split_send(name,coin='BTC',ext='9997].sigtx') + + def regtest_split_txdo_timelock(self,name,coin,locktime,bad_locktime): + opt.coin = coin + sid = self.regtest_user_sid('bob') + self.regtest_user_txdo( + name,'bob','0.0001',[sid+':S:5'],'1',pw='abc', + extra_args=['--locktime='+str(locktime)], + bad_locktime=bad_locktime) + + def regtest_split_txdo_timelock_bad_btc(self,name): + self.regtest_split_txdo_timelock(name,'BTC',locktime=8888,bad_locktime=True) + def regtest_split_txdo_timelock_good_btc(self,name): + self.regtest_split_txdo_timelock(name,'BTC',locktime=1321009871,bad_locktime=False) + def regtest_split_txdo_timelock_bad_b2x(self,name): + self.regtest_split_txdo_timelock(name,'B2X',locktime=8888,bad_locktime=True) + def regtest_split_txdo_timelock_good_b2x(self,name): + self.regtest_split_txdo_timelock(name,'B2X',locktime=1321009871,bad_locktime=False) + +# def regtest_user_txdo(self,name,user,fee,outputs_cl,outputs_prompt,extra_args=[],wf=None,pw='abc',no_send=False,do_label=False): + + # undocumented admin commands ref_tx_setup = regtest_setup ref_tx_generate = regtest_generate @@ -2420,7 +2557,12 @@ class MMGenTestSuite(object): outputs_cl = [sid+':{}:3,1.1234'.format(g.proto.mmtypes[-1]), sid+':C:5,5.5555',sid+':L:4',addr+',100'] pw = cfg['wpasswd'] # create tx in cwd - t = MMGenExpect(name,'mmgen-txcreate',['-B','--bob','--tx-fee='+rtFee[0]] + outputs_cl) + t = MMGenExpect(name,'mmgen-txcreate',[ + '-B', + '--bob', + '--tx-fee='+rtFee[0], + '--locktime=1320969600' + ] + outputs_cl) # [os.path.join(ref_dir,cfg['ref_wallet'])]) t.expect(r"'q'=quit view, .*?:.",'M',regex=True) # sort by mmid t.expect(r"'q'=quit view, .*?:.",'q',regex=True) @@ -2441,7 +2583,7 @@ class MMGenTestSuite(object): with open(fn) as f: lines = f.read().splitlines() - from mmgen.obj import BTCAmt,LTCAmt,BCHAmt + from mmgen.obj import BTCAmt,LTCAmt,BCHAmt,B2XAmt tx = {} for k,i in (('in',3),('out',4)): tx[k] = eval(lines[i])