B2X support, locktime-based coin splitting utility

- full support for B2X fork
- `mmgen-split` coin splitting utility for replayable forks
- `mmgen-regtest fork` command for testing forking scenarios
- `regtest_split` command added to `test/test.py` test suite
- timelock support for `txcreate` and `txdo`
- nlocktime and nsequence checks after signing and before sending
This commit is contained in:
philemon 2017-11-13 22:50:35 +03:00
commit 420d0e9699
Signed by untrusted user who does not match committer: mmgen
GPG key ID: 62DBE9E5212F05BE
18 changed files with 709 additions and 152 deletions

View file

@ -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

24
cmds/mmgen-split Executable file
View file

@ -0,0 +1,24 @@
#!/usr/bin/env python
# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
# Copyright (C)2013-2017 Philemon <mmgen-py@yandex.com>
#
# 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/>.
"""
mmgen-split: Split funds after a fork using a timelocked transaction
"""
from mmgen.main import launch
launch("split")

View file

@ -41,31 +41,40 @@ opts_data = lambda: {
AVAILABLE COMMANDS
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 - mine a block
send ADDR AMT - send amount AMT to address ADDR
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:])

139
mmgen/main_split.py Executable file
View file

@ -0,0 +1,139 @@
#!/usr/bin/env python
#
# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
# Copyright (C)2013-2017 Philemon <mmgen-py@yandex.com>
#
# 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/>.
# 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)

View file

@ -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.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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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),
}

View file

@ -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:

View file

@ -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))

View file

@ -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,6 +321,7 @@ 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)
if ad_f:
d.update(ad_f.make_reverse_dict(a))
for e in self.outputs:
if e.addr and e.addr in d:
@ -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?')

View file

@ -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"

View file

@ -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',

View file

@ -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

View file

@ -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

View file

@ -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,17 +1552,23 @@ 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')
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:
t.expect('to confirm: ','YES, I REALLY WANT TO DO THIS\n')
t.expect('Transaction sent')
if no_send:
t.read()
t.ok()
exit_val = 0
else:
t.expect('to confirm: ','YES, I REALLY WANT TO DO THIS\n')
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])