NMC and CLTV, abs lock values still to be verified

This commit is contained in:
tecnovert 2019-07-25 00:59:40 +02:00
parent 222fbb492f
commit 307b8ab8bf
No known key found for this signature in database
GPG key ID: 8ED6D8750C4E3F93
6 changed files with 638 additions and 45 deletions

View file

@ -126,6 +126,9 @@ class TxTypes(IntEnum): # For PooledAddress
SEQUENCE_LOCK_BLOCKS = 1
SEQUENCE_LOCK_TIME = 2
ABS_LOCK_BLOCKS = 3
ABS_LOCK_TIME = 4
SEQUENCE_LOCKTIME_GRANULARITY = 9 # 512 seconds
SEQUENCE_LOCKTIME_TYPE_FLAG = (1 << 22)
SEQUENCE_LOCKTIME_MASK = 0x0000ffff
@ -183,6 +186,10 @@ def getLockName(lock_type):
return 'Sequence lock, blocks'
if lock_type == SEQUENCE_LOCK_TIME:
return 'Sequence lock, time'
if lock_type == ABS_LOCK_BLOCKS:
return 'blocks'
if lock_type == ABS_LOCK_TIME:
return 'time'
def getExpectedSequence(lockType, lockVal, coin_type):
@ -206,7 +213,7 @@ def decodeSequence(lock_value):
return lock_value & SEQUENCE_LOCKTIME_MASK
def buildContractScriptCSV(sequence, secret_hash, pkh_redeem, pkh_refund):
def buildContractScript(lock_val, secret_hash, pkh_redeem, pkh_refund, op_lock=OpCodes.OP_CHECKSEQUENCEVERIFY):
script = bytearray([
OpCodes.OP_IF,
OpCodes.OP_SIZE,
@ -222,9 +229,9 @@ def buildContractScriptCSV(sequence, secret_hash, pkh_redeem, pkh_refund):
0x14]) \
+ pkh_redeem \
+ bytearray([OpCodes.OP_ELSE, ]) \
+ SerialiseNum(sequence) \
+ SerialiseNum(lock_val) \
+ bytearray([
OpCodes.OP_CHECKSEQUENCEVERIFY,
op_lock,
OpCodes.OP_DROP,
OpCodes.OP_DUP,
OpCodes.OP_HASH160,
@ -590,6 +597,7 @@ class BasicSwap():
'watched_outputs': [],
'last_height_checked': last_height_checked,
'use_segwit': use_segwit,
'use_csv': chain_client_settings.get('use_csv', True),
}
def start(self):
@ -710,8 +718,16 @@ class BasicSwap():
def validateOfferLockValue(self, coin_from, coin_to, lock_type, lock_value):
if lock_type == OfferMessage.SEQUENCE_LOCK_TIME:
assert(lock_value >= 2 * 60 * 60 and lock_value <= 96 * 60 * 60), 'Invalid lock_value time'
assert(self.coin_clients[coin_from]['use_csv'] and self.coin_clients[coin_to]['use_csv']), 'Both coins need CSV activated.'
elif lock_type == OfferMessage.SEQUENCE_LOCK_BLOCKS:
assert(lock_value >= 5 and lock_value <= 1000), 'Invalid lock_value blocks'
assert(self.coin_clients[coin_from]['use_csv'] and self.coin_clients[coin_to]['use_csv']), 'Both coins need CSV activated.'
elif lock_type == ABS_LOCK_TIME:
# TODO: range?
assert(lock_value >= 4 * 60 * 60 and lock_value <= 96 * 60 * 60), 'Invalid lock_value time'
elif lock_type == ABS_LOCK_BLOCKS:
# TODO: range?
assert(lock_value >= 10 and lock_value <= 1000), 'Invalid lock_value blocks'
else:
raise ValueError('Unknown locktype')
@ -872,7 +888,7 @@ class BasicSwap():
def getReceiveAddressForCoin(self, coin_type):
if coin_type == Coins.PART:
new_addr = self.callcoinrpc(Coins.PART, 'getnewaddress')
elif coin_type == Coins.LTC or coin_type == Coins.BTC:
elif coin_type == Coins.LTC or coin_type == Coins.BTC or coin_type == Coins.NMC:
args = []
if self.coin_clients[coin_type]['use_segwit']:
args = ['swap_receive', 'bech32']
@ -1123,15 +1139,22 @@ class BasicSwap():
coin_from = Coins(offer.coin_from)
bid_date = dt.datetime.fromtimestamp(bid.created_at).date()
# TODO: Use CLTV for coins without CSV
sequence = getExpectedSequence(offer.lock_type, offer.lock_value, coin_from)
secret = self.getContractSecret(bid_date, bid.contract_count)
secret_hash = hashlib.sha256(secret).digest()
pubkey_refund = self.getContractPubkey(bid_date, bid.contract_count)
pkhash_refund = getKeyID(pubkey_refund)
script = buildContractScriptCSV(sequence, secret_hash, bid.pkhash_buyer, pkhash_refund)
if offer.lock_type < ABS_LOCK_BLOCKS:
sequence = getExpectedSequence(offer.lock_type, offer.lock_value, coin_from)
script = buildContractScript(sequence, secret_hash, bid.pkhash_buyer, pkhash_refund)
else:
if offer.lock_type == ABS_LOCK_BLOCKS:
lock_value = self.callcoinrpc(coin_from, 'getblockchaininfo')['blocks'] + offer.lock_value
else:
lock_value = int(time.time()) + offer.lock_value
logging.debug('initiate %s lock_value %d %d', coin_from, offer.lock_value, lock_value)
script = buildContractScript(lock_value, secret_hash, bid.pkhash_buyer, pkhash_refund, OpCodes.OP_CHECKLOCKTIMEVERIFY)
p2sh = self.callcoinrpc(Coins.PART, 'decodescript', [script.hex()])['p2sh']
@ -1141,7 +1164,7 @@ class BasicSwap():
txn = self.createInitiateTxn(coin_from, bid_id, bid)
# Store the signed refund txn in case wallet is locked when refund is possible
refund_txn = self.createRefundTxn(coin_from, txn, bid, script)
refund_txn = self.createRefundTxn(coin_from, txn, offer, bid, script)
bid.initiate_txn_refund = bytes.fromhex(refund_txn)
txid = self.submitTxn(coin_from, txn)
@ -1153,7 +1176,7 @@ class BasicSwap():
txid = self.submitTxn(coin_from, bid.initiate_txn_refund.hex())
self.log.error('Submit refund_txn unexpectedly worked: ' + txid)
except Exception as e:
if 'non-BIP68-final' not in str(e):
if 'non-BIP68-final' not in str(e) and 'non-final' not in str(e):
self.log.error('Submit refund_txn unexpected error' + str(e))
if txid is not None:
@ -1269,14 +1292,22 @@ class BasicSwap():
bid_date = dt.datetime.fromtimestamp(bid.created_at).date()
# Participate txn is locked for half the time of the initiate txn
lock_value = decodeSequence(offer.lock_value) // 2
sequence = getExpectedSequence(offer.lock_type, lock_value, coin_to)
secret_hash = extractScriptSecretHash(bid.initiate_script)
pkhash_seller = bid.pkhash_seller
pkhash_buyer_refund = bid.pkhash_buyer
bid.participate_script = buildContractScriptCSV(sequence, secret_hash, pkhash_seller, pkhash_buyer_refund)
# Participate txn is locked for half the time of the initiate txn
lock_value = offer.lock_value // 2
if offer.lock_type < ABS_LOCK_BLOCKS:
sequence = getExpectedSequence(offer.lock_type, lock_value, coin_to)
bid.participate_script = buildContractScript(sequence, secret_hash, pkhash_seller, pkhash_buyer_refund)
else:
if offer.lock_type == ABS_LOCK_BLOCKS:
contract_lock_value = self.callcoinrpc(coin_to, 'getblockchaininfo')['blocks'] + lock_value
else:
contract_lock_value = int(time.time()) + lock_value
logging.debug('participate %s lock_value %d %d', coin_to, lock_value, contract_lock_value)
bid.participate_script = buildContractScript(contract_lock_value, secret_hash, pkhash_seller, pkhash_buyer_refund, OpCodes.OP_CHECKLOCKTIMEVERIFY)
def createParticipateTxn(self, bid_id, bid, offer):
self.log.debug('createParticipateTxn')
@ -1302,7 +1333,7 @@ class BasicSwap():
txn_signed = self.callcoinrpc(coin_to, 'signrawtransactionwithwallet', [txn_funded])['hex']
refund_txn = self.createRefundTxn(coin_to, txn_signed, bid, bid.participate_script, tx_type=TxTypes.PTX_REFUND)
refund_txn = self.createRefundTxn(coin_to, txn_signed, offer, bid, bid.participate_script, tx_type=TxTypes.PTX_REFUND)
bid.participate_txn_refund = bytes.fromhex(refund_txn)
chain_height = self.callcoinrpc(coin_to, 'getblockchaininfo')['blocks']
@ -1433,7 +1464,7 @@ class BasicSwap():
return redeem_txn
def createRefundTxn(self, coin_type, txn, bid, txn_script, addr_refund_out=None, tx_type=TxTypes.ITX_REFUND):
def createRefundTxn(self, coin_type, txn, offer, bid, txn_script, addr_refund_out=None, tx_type=TxTypes.ITX_REFUND):
self.log.debug('createRefundTxn')
if self.coin_clients[coin_type]['connection_type'] != 'rpc':
return None
@ -1459,7 +1490,11 @@ class BasicSwap():
'redeemScript': txn_script.hex(),
'amount': prev_amount}
sequence = DeserialiseNum(txn_script, 64)
lock_value = DeserialiseNum(txn_script, 64)
if offer.lock_type < ABS_LOCK_BLOCKS:
sequence = lock_value
else:
sequence = 1
prevout_s = ' in={}:{}:{}'.format(txjs['txid'], vout, sequence)
fee_rate = self.getFeeRateForCoin(coin_type)
@ -1488,6 +1523,9 @@ class BasicSwap():
else:
refund_txn = self.calltx('-btcmode -create nversion=2' + prevout_s + output_to)
if offer.lock_type == ABS_LOCK_BLOCKS or offer.lock_type == ABS_LOCK_TIME:
refund_txn = self.calltx('{} locktime={}'.format(refund_txn, lock_value))
options = {}
if self.coin_clients[coin_type]['use_segwit']:
options['force_segwit'] = True
@ -1691,7 +1729,7 @@ class BasicSwap():
self.initiateTxnConfirmed(bid_id, bid, offer)
save_bid = True
# Bid times out if buyer doesn't see tx in chain within
# Bid times out if buyer doesn't see tx in chain within INITIATE_TX_TIMEOUT seconds
if bid.state_time + INITIATE_TX_TIMEOUT < int(time.time()):
self.log.info('Swap timed out waiting for initiate tx for bid %s', bid_id.hex())
bid.setState(BidStates.SWAP_TIMEDOUT)
@ -1757,7 +1795,7 @@ class BasicSwap():
self.log.debug('Submitted initiate refund txn %s to %s chain for bid %s', txid, chainparams[coin_from]['name'], bid_id.hex())
# State will update when spend is detected
except Exception as e:
if 'non-BIP68-final (code 64)' not in str(e):
if 'non-BIP68-final (code 64)' not in str(e) and 'non-final' not in str(e):
self.log.warning('Error trying to submit initiate refund txn: %s', str(e))
if (bid.participate_txn_state == TxStates.TX_SENT or bid.participate_txn_state == TxStates.TX_CONFIRMED) \
and bid.participate_txn_refund is not None:
@ -1766,7 +1804,7 @@ class BasicSwap():
self.log.debug('Submitted participate refund txn %s to %s chain for bid %s', txid, chainparams[coin_to]['name'], bid_id.hex())
# State will update when spend is detected
except Exception as e:
if 'non-BIP68-final (code 64)' not in str(e):
if 'non-BIP68-final (code 64)' not in str(e) and 'non-final' not in str(e):
self.log.warning('Error trying to submit participate refund txn: %s', str(e))
return False # Bid is still active
@ -2090,19 +2128,11 @@ class BasicSwap():
self.log.debug('for bid %s', bid_accept_data.bid_msg_id.hex())
decoded_script = self.callcoinrpc(Coins.PART, 'decodescript', [bid_accept_data.contract_script.hex()])
# TODO: Verify script without decoding?
prog = re.compile('OP_IF OP_SIZE 32 OP_EQUALVERIFY OP_SHA256 (\w+) OP_EQUALVERIFY OP_DUP OP_HASH160 (\w+) OP_ELSE (\d+) OP_CHECKSEQUENCEVERIFY OP_DROP OP_DUP OP_HASH160 (\w+) OP_ENDIF OP_EQUALVERIFY OP_CHECKSIG')
rr = prog.match(decoded_script['asm'])
if not rr:
raise ValueError('Bad script')
scriptvalues = rr.groups()
bid_id = bid_accept_data.bid_msg_id
bid, offer = self.getBidAndOffer(bid_id)
assert(bid is not None and bid.was_sent is True), 'Unknown bidid'
assert(offer), 'Offer not found ' + bid.offer_id.hex()
coin_from = Coins(offer.coin_from)
# assert(bid.expire_at > now), 'Bid expired' # How much time over to accept
@ -2112,12 +2142,26 @@ class BasicSwap():
return
raise ValueError('Wrong bid state: {}'.format(str(BidStates(bid.state))))
coin_from = Coins(offer.coin_from)
expect_sequence = getExpectedSequence(offer.lock_type, offer.lock_value, coin_from)
use_csv = True if offer.lock_type < ABS_LOCK_BLOCKS else False
# TODO: Verify script without decoding?
decoded_script = self.callcoinrpc(Coins.PART, 'decodescript', [bid_accept_data.contract_script.hex()])
lock_check_op = 'OP_CHECKSEQUENCEVERIFY' if use_csv else 'OP_CHECKLOCKTIMEVERIFY'
prog = re.compile('OP_IF OP_SIZE 32 OP_EQUALVERIFY OP_SHA256 (\w+) OP_EQUALVERIFY OP_DUP OP_HASH160 (\w+) OP_ELSE (\d+) {} OP_DROP OP_DUP OP_HASH160 (\w+) OP_ENDIF OP_EQUALVERIFY OP_CHECKSIG'.format(lock_check_op))
rr = prog.match(decoded_script['asm'])
if not rr:
raise ValueError('Bad script')
scriptvalues = rr.groups()
assert(len(scriptvalues[0]) == 64), 'Bad secret_hash length'
assert(bytes.fromhex(scriptvalues[1]) == bid.pkhash_buyer), 'pkhash_buyer mismatch'
assert(int(scriptvalues[2]) == expect_sequence), 'sequence mismatch'
if use_csv:
expect_sequence = getExpectedSequence(offer.lock_type, offer.lock_value, coin_from)
assert(int(scriptvalues[2]) == expect_sequence), 'sequence mismatch'
else:
self.log.warning('TODO: validate absolute lock values')
assert(len(scriptvalues[3]) == 40), 'pkhash_refund bad length'
assert(bid.accept_msg_id is None), 'Bid already accepted'

View file

@ -22,3 +22,8 @@ LITECOIN_BINDIR = os.path.expanduser(os.getenv('LITECOIN_BINDIR', ''))
LITECOIND = os.getenv('LITECOIND', 'litecoind' + ('.exe' if os.name == 'nt' else ''))
LITECOIN_CLI = os.getenv('LITECOIN_CLI', 'litecoin-cli' + ('.exe' if os.name == 'nt' else ''))
LITECOIN_TX = os.getenv('LITECOIN_TX', 'litecoin-tx' + ('.exe' if os.name == 'nt' else ''))
NAMECOIN_BINDIR = os.path.expanduser(os.getenv('NAMECOIN_BINDIR', ''))
NAMECOIND = os.getenv('NAMECOIND', 'namecoind' + ('.exe' if os.name == 'nt' else ''))
NAMECOIN_CLI = os.getenv('NAMECOIN_CLI', 'namecoin-cli' + ('.exe' if os.name == 'nt' else ''))
NAMECOIN_TX = os.getenv('NAMECOIN_TX', 'namecoin-tx' + ('.exe' if os.name == 'nt' else ''))

View file

@ -14,6 +14,8 @@ message OfferMessage {
NOT_SET = 0;
SEQUENCE_LOCK_BLOCKS = 1;
SEQUENCE_LOCK_TIME = 2;
ABS_LOCK_BLOCKS = 3;
ABS_LOCK_TIME = 4;
}
LockType lock_type = 7;
uint32 lock_value = 8;

View file

@ -23,6 +23,7 @@ import time
from urllib.request import urlretrieve
import urllib.parse
import logging
import platform
import gnupg
@ -30,7 +31,10 @@ import basicswap.config as cfg
from basicswap.util import callrpc_cli
from bin.basicswap_run import startDaemon
BIN_ARCH = 'x86_64-linux-gnu.tar.gz'
if platform.system() == 'Darwin':
BIN_ARCH = 'osx64.tar.gz'
else:
BIN_ARCH = 'x86_64-linux-gnu.tar.gz'
known_coins = {
'particl': '0.18.1.0',
@ -358,6 +362,7 @@ def main():
'datadir': os.path.join(data_dir, 'namecoin'),
'bindir': os.path.join(data_dir, 'bins', 'namecoin'),
'use_segwit': False,
'use_csv': False,
'blocks_confirmed': 1
}
}

View file

@ -22,7 +22,6 @@ import signal
import subprocess
import logging
import basicswap.config as cfg
from basicswap import __version__
from basicswap.basicswap import BasicSwap
from basicswap.http_server import HttpThread
@ -66,17 +65,10 @@ def runClient(fp, dataDir, chain):
for c, v in settings['chainclients'].items():
if v['manage_daemon'] is True:
logger.info('Starting {} daemon'.format(c.capitalize()))
if c == 'particl':
daemons.append(startDaemon(v['datadir'], v['bindir'], cfg.PARTICLD))
logger.info('Started {} {}'.format(cfg.PARTICLD, daemons[-1].pid))
elif c == 'bitcoin':
daemons.append(startDaemon(v['datadir'], v['bindir'], cfg.BITCOIND))
logger.info('Started {} {}'.format(cfg.BITCOIND, daemons[-1].pid))
elif c == 'litecoin':
daemons.append(startDaemon(v['datadir'], v['bindir'], cfg.LITECOIND))
logger.info('Started {} {}'.format(cfg.LITECOIND, daemons[-1].pid))
else:
logger.warning('Unknown chain', c)
filename = c + 'd' + ('.exe' if os.name == 'nt' else '')
daemons.append(startDaemon(v['datadir'], v['bindir'], filename))
logger.info('Started {} {}'.format(filename, daemons[-1].pid))
swap_client = BasicSwap(fp, dataDir, settings, chain)

545
tests/test_nmc.py Normal file
View file

@ -0,0 +1,545 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (c) 2019 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE.txt or http://www.opensource.org/licenses/mit-license.php.
"""
basicswap]$ python tests/test_nmc.py
"""
import os
import sys
import unittest
import json
import logging
import shutil
import subprocess
import time
import signal
import threading
from urllib.request import urlopen
from basicswap.basicswap import (
BasicSwap,
Coins,
SwapTypes,
BidStates,
TxStates,
ABS_LOCK_BLOCKS,
ABS_LOCK_TIME,
)
from basicswap.util import (
COIN,
toWIF,
callrpc_cli,
)
from basicswap.key import (
ECKey,
)
from basicswap.http_server import (
HttpThread,
)
import basicswap.config as cfg
logger = logging.getLogger()
logger.level = logging.DEBUG
if not len(logger.handlers):
logger.addHandler(logging.StreamHandler(sys.stdout))
NUM_NODES = 3
BASE_PORT = 14792
BASE_RPC_PORT = 19792
BASE_ZMQ_PORT = 20792
PREFIX_SECRET_KEY_REGTEST = 0x2e
TEST_HTML_PORT = 1800
NMC_NODE = 3
BTC_NODE = 4
stop_test = False
def prepareOtherDir(datadir, nodeId, conf_file='namecoin.conf'):
node_dir = os.path.join(datadir, str(nodeId))
if not os.path.exists(node_dir):
os.makedirs(node_dir)
filePath = os.path.join(node_dir, conf_file)
with open(filePath, 'w+') as fp:
fp.write('regtest=1\n')
fp.write('[regtest]\n')
fp.write('port=' + str(BASE_PORT + nodeId) + '\n')
fp.write('rpcport=' + str(BASE_RPC_PORT + nodeId) + '\n')
fp.write('daemon=0\n')
fp.write('printtoconsole=0\n')
fp.write('server=1\n')
fp.write('discover=0\n')
fp.write('listenonion=0\n')
fp.write('bind=127.0.0.1\n')
fp.write('findpeers=0\n')
fp.write('debug=1\n')
fp.write('debugexclude=libevent\n')
fp.write('acceptnonstdtxn=0\n')
def prepareDir(datadir, nodeId, network_key, network_pubkey):
node_dir = os.path.join(datadir, str(nodeId))
if not os.path.exists(node_dir):
os.makedirs(node_dir)
filePath = os.path.join(node_dir, 'particl.conf')
with open(filePath, 'w+') as fp:
fp.write('regtest=1\n')
fp.write('[regtest]\n')
fp.write('port=' + str(BASE_PORT + nodeId) + '\n')
fp.write('rpcport=' + str(BASE_RPC_PORT + nodeId) + '\n')
fp.write('daemon=0\n')
fp.write('printtoconsole=0\n')
fp.write('server=1\n')
fp.write('discover=0\n')
fp.write('listenonion=0\n')
fp.write('bind=127.0.0.1\n')
fp.write('findpeers=0\n')
fp.write('debug=1\n')
fp.write('debugexclude=libevent\n')
fp.write('zmqpubsmsg=tcp://127.0.0.1:' + str(BASE_ZMQ_PORT + nodeId) + '\n')
fp.write('acceptnonstdtxn=0\n')
fp.write('minstakeinterval=5\n')
for i in range(0, NUM_NODES):
if nodeId == i:
continue
fp.write('addnode=127.0.0.1:%d\n' % (BASE_PORT + i))
if nodeId < 2:
fp.write('spentindex=1\n')
fp.write('txindex=1\n')
basicswap_dir = os.path.join(datadir, str(nodeId), 'basicswap')
if not os.path.exists(basicswap_dir):
os.makedirs(basicswap_dir)
nmcdatadir = os.path.join(datadir, str(NMC_NODE))
btcdatadir = os.path.join(datadir, str(BTC_NODE))
settings_path = os.path.join(basicswap_dir, 'basicswap.json')
settings = {
'zmqhost': 'tcp://127.0.0.1',
'zmqport': BASE_ZMQ_PORT + nodeId,
'htmlhost': 'localhost',
'htmlport': 12700 + nodeId,
'network_key': network_key,
'network_pubkey': network_pubkey,
'chainclients': {
'particl': {
'connection_type': 'rpc',
'manage_daemon': False,
'rpcport': BASE_RPC_PORT + nodeId,
'datadir': node_dir,
'bindir': cfg.PARTICL_BINDIR,
'blocks_confirmed': 2, # Faster testing
},
'namecoin': {
'connection_type': 'rpc',
'manage_daemon': False,
'rpcport': BASE_RPC_PORT + NMC_NODE,
'datadir': nmcdatadir,
'bindir': cfg.NAMECOIN_BINDIR,
'use_csv': False,
# 'use_segwit': True,
},
'bitcoin': {
'connection_type': 'rpc',
'manage_daemon': False,
'rpcport': BASE_RPC_PORT + BTC_NODE,
'datadir': btcdatadir,
'bindir': cfg.BITCOIN_BINDIR,
'use_segwit': True,
}
},
'check_progress_seconds': 2,
'check_watched_seconds': 4,
'check_expired_seconds': 60
}
with open(settings_path, 'w') as fp:
json.dump(settings, fp, indent=4)
def startDaemon(nodeId, bin_dir=cfg.PARTICL_BINDIR, daemon_bin=cfg.PARTICLD):
node_dir = os.path.join(cfg.DATADIRS, str(nodeId))
daemon_bin = os.path.join(bin_dir, daemon_bin)
args = [daemon_bin, '-datadir=' + node_dir]
logging.info('Starting node ' + str(nodeId) + ' ' + daemon_bin + ' ' + '-datadir=' + node_dir)
return subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
def partRpc(cmd, node_id=0):
return callrpc_cli(cfg.PARTICL_BINDIR, os.path.join(cfg.DATADIRS, str(node_id)), 'regtest', cmd, cfg.PARTICL_CLI)
def btcRpc(cmd):
return callrpc_cli(cfg.BITCOIN_BINDIR, os.path.join(cfg.DATADIRS, str(BTC_NODE)), 'regtest', cmd, cfg.BITCOIN_CLI)
def nmcRpc(cmd):
return callrpc_cli(cfg.NAMECOIN_BINDIR, os.path.join(cfg.DATADIRS, str(NMC_NODE)), 'regtest', cmd, cfg.NAMECOIN_CLI)
def signal_handler(sig, frame):
global stop_test
print('signal {} detected.'.format(sig))
stop_test = True
def run_loop(self):
while not stop_test:
time.sleep(1)
for c in self.swap_clients:
c.update()
nmcRpc('generatetoaddress 1 {}'.format(self.nmc_addr))
btcRpc('generatetoaddress 1 {}'.format(self.btc_addr))
def waitForRPC(rpc_func, wallet=None):
for i in range(5):
try:
rpc_func('getwalletinfo')
return
except Exception as ex:
logging.warning('Can\'t connect to daemon RPC: %s. Trying again in %d second/s.', str(ex), (1 + i))
time.sleep(1 + i)
raise ValueError('waitForRPC failed')
class Test(unittest.TestCase):
@classmethod
def setUpClass(cls):
super(Test, cls).setUpClass()
eckey = ECKey()
eckey.generate()
cls.network_key = toWIF(PREFIX_SECRET_KEY_REGTEST, eckey.get_bytes())
cls.network_pubkey = eckey.get_pubkey().get_bytes().hex()
if os.path.isdir(cfg.DATADIRS):
logging.info('Removing ' + cfg.DATADIRS)
shutil.rmtree(cfg.DATADIRS)
for i in range(NUM_NODES):
prepareDir(cfg.DATADIRS, i, cls.network_key, cls.network_pubkey)
prepareOtherDir(cfg.DATADIRS, NMC_NODE)
prepareOtherDir(cfg.DATADIRS, BTC_NODE, 'bitcoin.conf')
cls.daemons = []
cls.swap_clients = []
cls.daemons.append(startDaemon(BTC_NODE, cfg.BITCOIN_BINDIR, cfg.BITCOIND))
logging.info('Started %s %d', cfg.BITCOIND, cls.daemons[-1].pid)
cls.daemons.append(startDaemon(NMC_NODE, cfg.NAMECOIN_BINDIR, cfg.NAMECOIND))
logging.info('Started %s %d', cfg.NAMECOIND, cls.daemons[-1].pid)
for i in range(NUM_NODES):
cls.daemons.append(startDaemon(i))
logging.info('Started %s %d', cfg.PARTICLD, cls.daemons[-1].pid)
time.sleep(1)
for i in range(NUM_NODES):
basicswap_dir = os.path.join(os.path.join(cfg.DATADIRS, str(i)), 'basicswap')
settings_path = os.path.join(basicswap_dir, 'basicswap.json')
with open(settings_path) as fs:
settings = json.load(fs)
fp = open(os.path.join(basicswap_dir, 'basicswap.log'), 'w')
cls.swap_clients.append(BasicSwap(fp, basicswap_dir, settings, 'regtest', log_name='BasicSwap{}'.format(i)))
cls.swap_clients[-1].start()
cls.swap_clients[0].callrpc('extkeyimportmaster', ['abandon baby cabbage dad eager fabric gadget habit ice kangaroo lab absorb'])
cls.swap_clients[1].callrpc('extkeyimportmaster', ['pact mammal barrel matrix local final lecture chunk wasp survey bid various book strong spread fall ozone daring like topple door fatigue limb olympic', '', 'true'])
cls.swap_clients[1].callrpc('getnewextaddress', ['lblExtTest'])
cls.swap_clients[1].callrpc('rescanblockchain')
waitForRPC(nmcRpc)
num_blocks = 500
logging.info('Mining %d namecoin blocks', num_blocks)
cls.nmc_addr = nmcRpc('getnewaddress mining_addr legacy')
nmcRpc('generatetoaddress {} {}'.format(num_blocks, cls.nmc_addr))
ro = nmcRpc('getblockchaininfo')
try:
assert(ro['bip9_softforks']['csv']['status'] == 'active')
except Exception:
logging.info('nmc: csv is not active')
try:
assert(ro['bip9_softforks']['segwit']['status'] == 'active')
except Exception:
logging.info('nmc: segwit is not active')
waitForRPC(btcRpc)
cls.btc_addr = btcRpc('getnewaddress mining_addr bech32')
logging.info('Mining %d bitcoin blocks to %s', num_blocks, cls.btc_addr)
btcRpc('generatetoaddress {} {}'.format(num_blocks, cls.btc_addr))
ro = btcRpc('getblockchaininfo')
assert(ro['bip9_softforks']['csv']['status'] == 'active')
assert(ro['bip9_softforks']['segwit']['status'] == 'active')
ro = nmcRpc('getwalletinfo')
print('nmcRpc', ro)
cls.http_threads = []
host = '0.0.0.0' # All interfaces (docker)
for i in range(3):
t = HttpThread(cls.swap_clients[i].fp, host, TEST_HTML_PORT + i, False, cls.swap_clients[i])
cls.http_threads.append(t)
t.start()
signal.signal(signal.SIGINT, signal_handler)
cls.update_thread = threading.Thread(target=run_loop, args=(cls,))
cls.update_thread.start()
@classmethod
def tearDownClass(cls):
global stop_test
logging.info('Finalising')
stop_test = True
cls.update_thread.join()
for t in cls.http_threads:
t.stop()
t.join()
for c in cls.swap_clients:
c.fp.close()
for d in cls.daemons:
logging.info('Terminating %d', d.pid)
d.terminate()
d.wait(timeout=10)
if d.stdout:
d.stdout.close()
if d.stderr:
d.stderr.close()
if d.stdin:
d.stdin.close()
super(Test, cls).tearDownClass()
def wait_for_offer(self, swap_client, offer_id):
logging.info('wait_for_offer %s', offer_id.hex())
for i in range(20):
time.sleep(1)
offers = swap_client.listOffers()
for offer in offers:
if offer.offer_id == offer_id:
return
raise ValueError('wait_for_offer timed out.')
def wait_for_bid(self, swap_client, bid_id):
logging.info('wait_for_bid %s', bid_id.hex())
for i in range(20):
time.sleep(1)
bids = swap_client.listBids()
for bid in bids:
if bid.bid_id == bid_id and bid.was_received:
return
raise ValueError('wait_for_bid timed out.')
def wait_for_in_progress(self, swap_client, bid_id, sent=False):
logging.info('wait_for_in_progress %s', bid_id.hex())
for i in range(20):
time.sleep(1)
swaps = swap_client.listSwapsInProgress()
for b in swaps:
if b[0] == bid_id:
return
raise ValueError('wait_for_in_progress timed out.')
def wait_for_bid_state(self, swap_client, bid_id, state, sent=False, seconds_for=30):
logging.info('wait_for_bid_state %s %s', bid_id.hex(), str(state))
for i in range(seconds_for):
time.sleep(1)
bid = swap_client.getBid(bid_id)
if bid.state >= state:
return
raise ValueError('wait_for_bid_state timed out.')
def wait_for_bid_tx_state(self, swap_client, bid_id, initiate_state, participate_state, seconds_for=30):
logging.info('wait_for_bid_tx_state %s %s %s', bid_id.hex(), str(initiate_state), str(participate_state))
for i in range(seconds_for):
time.sleep(1)
bid = swap_client.getBid(bid_id)
if (initiate_state is None or bid.initiate_txn_state == initiate_state) \
and (participate_state is None or bid.participate_txn_state == participate_state):
return
raise ValueError('wait_for_bid_tx_state timed out.')
def test_02_part_ltc(self):
swap_clients = self.swap_clients
logging.info('---------- Test PART to NMC')
offer_id = swap_clients[0].postOffer(Coins.PART, Coins.NMC, 100 * COIN, 0.1 * COIN, 100 * COIN, SwapTypes.SELLER_FIRST, ABS_LOCK_TIME)
self.wait_for_offer(swap_clients[1], offer_id)
offers = swap_clients[1].listOffers()
assert(len(offers) == 1)
for offer in offers:
if offer.offer_id == offer_id:
bid_id = swap_clients[1].postBid(offer_id, offer.amount_from)
self.wait_for_bid(swap_clients[0], bid_id)
swap_clients[0].acceptBid(bid_id)
self.wait_for_in_progress(swap_clients[1], bid_id, sent=True)
self.wait_for_bid_state(swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, seconds_for=60)
self.wait_for_bid_state(swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True, seconds_for=60)
js_0 = json.loads(urlopen('http://localhost:1800/json').read())
js_1 = json.loads(urlopen('http://localhost:1801/json').read())
assert(js_0['num_swapping'] == 0 and js_0['num_watched_outputs'] == 0)
assert(js_1['num_swapping'] == 0 and js_1['num_watched_outputs'] == 0)
def test_03_nmc_part(self):
swap_clients = self.swap_clients
logging.info('---------- Test NMC to PART')
offer_id = swap_clients[1].postOffer(Coins.NMC, Coins.PART, 10 * COIN, 9.0 * COIN, 10 * COIN, SwapTypes.SELLER_FIRST, ABS_LOCK_TIME)
self.wait_for_offer(swap_clients[0], offer_id)
offers = swap_clients[0].listOffers()
for offer in offers:
if offer.offer_id == offer_id:
bid_id = swap_clients[0].postBid(offer_id, offer.amount_from)
self.wait_for_bid(swap_clients[1], bid_id)
swap_clients[1].acceptBid(bid_id)
self.wait_for_in_progress(swap_clients[0], bid_id, sent=True)
self.wait_for_bid_state(swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, sent=True, seconds_for=60)
self.wait_for_bid_state(swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, seconds_for=60)
js_0 = json.loads(urlopen('http://localhost:1800/json').read())
js_1 = json.loads(urlopen('http://localhost:1801/json').read())
assert(js_0['num_swapping'] == 0 and js_0['num_watched_outputs'] == 0)
assert(js_1['num_swapping'] == 0 and js_1['num_watched_outputs'] == 0)
def test_04_nmc_btc(self):
swap_clients = self.swap_clients
logging.info('---------- Test NMC to BTC')
offer_id = swap_clients[0].postOffer(Coins.NMC, Coins.BTC, 10 * COIN, 0.1 * COIN, 10 * COIN, SwapTypes.SELLER_FIRST, ABS_LOCK_TIME)
self.wait_for_offer(swap_clients[1], offer_id)
offers = swap_clients[1].listOffers()
for offer in offers:
if offer.offer_id == offer_id:
bid_id = swap_clients[1].postBid(offer_id, offer.amount_from)
self.wait_for_bid(swap_clients[0], bid_id)
swap_clients[0].acceptBid(bid_id)
self.wait_for_in_progress(swap_clients[1], bid_id, sent=True)
self.wait_for_bid_state(swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, seconds_for=60)
self.wait_for_bid_state(swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True, seconds_for=60)
js_0bid = json.loads(urlopen('http://localhost:1800/json/bids/{}'.format(bid_id.hex())).read())
js_0 = json.loads(urlopen('http://localhost:1800/json').read())
js_1 = json.loads(urlopen('http://localhost:1801/json').read())
assert(js_0['num_swapping'] == 0 and js_0['num_watched_outputs'] == 0)
assert(js_1['num_swapping'] == 0 and js_1['num_watched_outputs'] == 0)
def test_05_refund(self):
# Seller submits initiate txn, buyer doesn't respond
swap_clients = self.swap_clients
logging.info('---------- Test refund, NMC to BTC')
# Note the lock value is absolute.
offer_id = swap_clients[0].postOffer(Coins.NMC, Coins.BTC, 10 * COIN, 0.1 * COIN, 10 * COIN, SwapTypes.SELLER_FIRST,
ABS_LOCK_BLOCKS, 20)
self.wait_for_offer(swap_clients[1], offer_id)
offers = swap_clients[1].listOffers()
for offer in offers:
if offer.offer_id == offer_id:
bid_id = swap_clients[1].postBid(offer_id, offer.amount_from)
self.wait_for_bid(swap_clients[0], bid_id)
swap_clients[1].abandonBid(bid_id)
swap_clients[0].acceptBid(bid_id)
self.wait_for_bid_state(swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, seconds_for=60)
self.wait_for_bid_state(swap_clients[1], bid_id, BidStates.BID_ABANDONED, sent=True, seconds_for=60)
js_0 = json.loads(urlopen('http://localhost:1800/json').read())
js_1 = json.loads(urlopen('http://localhost:1801/json').read())
assert(js_0['num_swapping'] == 0 and js_0['num_watched_outputs'] == 0)
assert(js_1['num_swapping'] == 0 and js_1['num_watched_outputs'] == 0)
def test_06_self_bid(self):
swap_clients = self.swap_clients
logging.info('---------- Test same client, BTC to NMC')
js_0_before = json.loads(urlopen('http://localhost:1800/json').read())
offer_id = swap_clients[0].postOffer(Coins.NMC, Coins.BTC, 10 * COIN, 10 * COIN, 10 * COIN, SwapTypes.SELLER_FIRST, ABS_LOCK_TIME)
self.wait_for_offer(swap_clients[0], offer_id)
offers = swap_clients[0].listOffers()
for offer in offers:
if offer.offer_id == offer_id:
bid_id = swap_clients[0].postBid(offer_id, offer.amount_from)
self.wait_for_bid(swap_clients[0], bid_id)
swap_clients[0].acceptBid(bid_id)
self.wait_for_bid_tx_state(swap_clients[0], bid_id, TxStates.TX_REDEEMED, TxStates.TX_REDEEMED, seconds_for=60)
self.wait_for_bid_state(swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, seconds_for=60)
js_0 = json.loads(urlopen('http://localhost:1800/json').read())
assert(js_0['num_swapping'] == 0 and js_0['num_watched_outputs'] == 0)
assert(js_0['num_recv_bids'] == js_0_before['num_recv_bids'] + 1 and js_0['num_sent_bids'] == js_0_before['num_sent_bids'] + 1)
def test_07_error(self):
swap_clients = self.swap_clients
logging.info('---------- Test error, BTC to NMC, set fee above bid value')
js_0_before = json.loads(urlopen('http://localhost:1800/json').read())
offer_id = swap_clients[0].postOffer(Coins.NMC, Coins.BTC, 0.001 * COIN, 1.0 * COIN, 0.001 * COIN, SwapTypes.SELLER_FIRST, ABS_LOCK_TIME)
self.wait_for_offer(swap_clients[0], offer_id)
offers = swap_clients[0].listOffers()
for offer in offers:
if offer.offer_id == offer_id:
bid_id = swap_clients[0].postBid(offer_id, offer.amount_from)
self.wait_for_bid(swap_clients[0], bid_id)
swap_clients[0].acceptBid(bid_id)
swap_clients[0].coin_clients[Coins.BTC]['override_feerate'] = 10.0
swap_clients[0].coin_clients[Coins.NMC]['override_feerate'] = 10.0
self.wait_for_bid_state(swap_clients[0], bid_id, BidStates.BID_ERROR, seconds_for=60)
def pass_99_delay(self):
global stop_test
logging.info('Delay')
for i in range(60 * 5):
if stop_test:
break
time.sleep(1)
print('delay', i)
stop_test = True
if __name__ == '__main__':
unittest.main()