From e1528c9d63f85a4b1b897d7f68ac2207bcb5da7e Mon Sep 17 00:00:00 2001 From: tecnovert Date: Thu, 11 Aug 2022 00:02:36 +0200 Subject: [PATCH] coins: Add PIVX No CSV or segwit. sethdseed requires a fully synced chain, manual intervention required to set a key derived from the master mnemonic. Requires a pivxd version with a backported scantxoutset command. --- .cirrus.yml | 4 +- .travis.yml | 4 +- basicswap/basicswap.py | 59 +- basicswap/chainparams.py | 44 +- basicswap/config.py | 5 + basicswap/interface/btc.py | 20 +- basicswap/interface/contrib/__init__.py | 0 .../contrib/pivx_test_framework/__init__.py | 0 .../contrib/pivx_test_framework/authproxy.py | 180 ++ .../contrib/pivx_test_framework/coverage.py | 109 ++ .../contrib/pivx_test_framework/messages.py | 1505 +++++++++++++++++ .../contrib/pivx_test_framework/siphash.py | 63 + .../contrib/pivx_test_framework/util.py | 625 +++++++ basicswap/interface/pivx.py | 56 + bin/basicswap_prepare.py | 73 +- pgp/keys/pivx_fuzzbawls.pgp | 589 +++++++ tests/basicswap/common.py | 10 + tests/basicswap/common_xmr.py | 31 +- tests/basicswap/extended/test_pivx.py | 552 ++++++ tests/basicswap/test_btc_xmr.py | 2 + tests/basicswap/test_other.py | 2 +- tests/basicswap/test_run.py | 1 + tests/basicswap/test_xmr.py | 73 +- 23 files changed, 3969 insertions(+), 38 deletions(-) create mode 100644 basicswap/interface/contrib/__init__.py create mode 100644 basicswap/interface/contrib/pivx_test_framework/__init__.py create mode 100644 basicswap/interface/contrib/pivx_test_framework/authproxy.py create mode 100644 basicswap/interface/contrib/pivx_test_framework/coverage.py create mode 100755 basicswap/interface/contrib/pivx_test_framework/messages.py create mode 100644 basicswap/interface/contrib/pivx_test_framework/siphash.py create mode 100644 basicswap/interface/contrib/pivx_test_framework/util.py create mode 100644 basicswap/interface/pivx.py create mode 100644 pgp/keys/pivx_fuzzbawls.pgp create mode 100644 tests/basicswap/extended/test_pivx.py diff --git a/.cirrus.yml b/.cirrus.yml index 18d52d2..285c5fa 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -7,8 +7,8 @@ lint_task: - pip install codespell script: - flake8 --version - - PYTHONWARNINGS="ignore" flake8 --ignore=E501,F841,W503 --exclude=basicswap/contrib,messages_pb2.py,.eggs,.tox,bin/install_certifi.py - - codespell --check-filenames --disable-colors --quiet-level=7 --ignore-words=tests/lint/spelling.ignore-words.txt -S .git,.eggs,.tox,pgp,*.pyc,*basicswap/contrib,*mnemonics.py,bin/install_certifi.py + - PYTHONWARNINGS="ignore" flake8 --ignore=E501,F841,W503 --exclude=basicswap/contrib,basicswap/interface/contrib,messages_pb2.py,.eggs,.tox,bin/install_certifi.py + - codespell --check-filenames --disable-colors --quiet-level=7 --ignore-words=tests/lint/spelling.ignore-words.txt -S .git,.eggs,.tox,pgp,*.pyc,*basicswap/contrib,*basicswap/interface/contrib,*mnemonics.py,bin/install_certifi.py test_task: environment: diff --git a/.travis.yml b/.travis.yml index 7a36f91..78ceeb4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -52,8 +52,8 @@ jobs: - travis_retry pip install codespell==1.15.0 before_script: script: - - PYTHONWARNINGS="ignore" flake8 --ignore=E501,F841,W503 --exclude=basicswap/contrib,messages_pb2.py,.eggs,.tox,bin/install_certifi.py - - codespell --check-filenames --disable-colors --quiet-level=7 --ignore-words=tests/lint/spelling.ignore-words.txt -S .git,.eggs,.tox,pgp,*.pyc,*basicswap/contrib,*mnemonics.py,bin/install_certifi.py + - PYTHONWARNINGS="ignore" flake8 --ignore=E501,F841,W503 --exclude=basicswap/contrib,basicswap/interface/contrib,messages_pb2.py,.eggs,.tox,bin/install_certifi.py + - codespell --check-filenames --disable-colors --quiet-level=7 --ignore-words=tests/lint/spelling.ignore-words.txt -S .git,.eggs,.tox,pgp,*.pyc,*basicswap/contrib,*basicswap/interface/contrib,*mnemonics.py,bin/install_certifi.py after_success: - echo "End lint" - stage: test diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index 2d50dc9..7afaad2 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -32,6 +32,7 @@ from .interface.btc import BTCInterface from .interface.ltc import LTCInterface from .interface.nmc import NMCInterface from .interface.xmr import XMRInterface +from .interface.pivx import PIVXInterface from .interface.passthrough_btc import PassthroughBTCInterface from . import __version__ @@ -174,7 +175,8 @@ def threadPollChainState(swap_client, coin_type): with swap_client.mxDB: cc['chain_height'] = chain_state['blocks'] cc['chain_best_block'] = chain_state['bestblockhash'] - cc['chain_median_time'] = chain_state['mediantime'] + if 'mediantime' in chain_state: + cc['chain_median_time'] = chain_state['mediantime'] except Exception as e: swap_client.log.warning('threadPollChainState {}, error: {}'.format(str(coin_type), str(e))) swap_client.delay_event.wait(random.randrange(20, 30)) # random to stagger updates @@ -380,21 +382,24 @@ class BasicSwap(BaseApp): session.close() session.remove() + coin_chainparams = chainparams[coin] + default_segwit = coin_chainparams.get('has_segwit', False) + default_csv = coin_chainparams.get('has_csv', True) self.coin_clients[coin] = { 'coin': coin, - 'name': chainparams[coin]['name'], + 'name': coin_chainparams['name'], 'connection_type': connection_type, 'bindir': bindir, 'datadir': datadir, 'rpchost': chain_client_settings.get('rpchost', '127.0.0.1'), - 'rpcport': chain_client_settings.get('rpcport', chainparams[coin][self.chain]['rpcport']), + 'rpcport': chain_client_settings.get('rpcport', coin_chainparams[self.chain]['rpcport']), 'rpcauth': rpcauth, 'blocks_confirmed': chain_client_settings.get('blocks_confirmed', 6), 'conf_target': chain_client_settings.get('conf_target', 2), 'watched_outputs': [], 'last_height_checked': last_height_checked, - 'use_segwit': chain_client_settings.get('use_segwit', False), - 'use_csv': chain_client_settings.get('use_csv', True), + 'use_segwit': chain_client_settings.get('use_segwit', default_segwit), + 'use_csv': chain_client_settings.get('use_csv', default_csv), 'core_version_group': chain_client_settings.get('core_version_group', 0), 'pid': None, 'core_version': None, @@ -482,6 +487,8 @@ class BasicSwap(BaseApp): chain_client_settings = self.getChainClientSettings(coin) xmr_i.setWalletFilename(chain_client_settings['walletfile']) return xmr_i + elif coin == Coins.PIVX: + return PIVXInterface(self.coin_clients[coin], self.chain, self) else: raise ValueError('Unknown coin type') @@ -926,6 +933,8 @@ class BasicSwap(BaseApp): raise ValueError('Invalid swap type for PART_ANON') if (coin_from == Coins.PART_BLIND or coin_to == Coins.PART_BLIND) and swap_type != SwapTypes.XMR_SWAP: raise ValueError('Invalid swap type for PART_BLIND') + if coin_from == Coins.PIVX and swap_type == SwapTypes.XMR_SWAP: + raise ValueError('TODO: PIVX -> XMR') def notify(self, event_type, event_data): if event_type == NT.OFFER_RECEIVED: @@ -958,19 +967,23 @@ class BasicSwap(BaseApp): ensure(amount_to < ci_to.max_amount(), 'To amount above max value for chain') def validateOfferLockValue(self, coin_from, coin_to, lock_type, lock_value): + + coin_from_has_csv = self.coin_clients[coin_from]['use_csv'] + coin_to_has_csv = self.coin_clients[coin_to]['use_csv'] + if lock_type == OfferMessage.SEQUENCE_LOCK_TIME: ensure(lock_value >= self.min_sequence_lock_seconds and lock_value <= self.max_sequence_lock_seconds, 'Invalid lock_value time') - ensure(self.coin_clients[coin_from]['use_csv'] and self.coin_clients[coin_to]['use_csv'], 'Both coins need CSV activated.') + ensure(coin_from_has_csv and coin_to_has_csv, 'Both coins need CSV activated.') elif lock_type == OfferMessage.SEQUENCE_LOCK_BLOCKS: ensure(lock_value >= 5 and lock_value <= 1000, 'Invalid lock_value blocks') - ensure(self.coin_clients[coin_from]['use_csv'] and self.coin_clients[coin_to]['use_csv'], 'Both coins need CSV activated.') + ensure(coin_from_has_csv and coin_to_has_csv, 'Both coins need CSV activated.') elif lock_type == TxLockTypes.ABS_LOCK_TIME: # TODO: range? - ensure(not self.coin_clients[coin_from]['use_csv'] or not self.coin_clients[coin_to]['use_csv'], 'Should use CSV.') + ensure(not coin_from_has_csv or not coin_to_has_csv, 'Should use CSV.') ensure(lock_value >= 4 * 60 * 60 and lock_value <= 96 * 60 * 60, 'Invalid lock_value time') elif lock_type == TxLockTypes.ABS_LOCK_BLOCKS: # TODO: range? - ensure(not self.coin_clients[coin_from]['use_csv'] or not self.coin_clients[coin_to]['use_csv'], 'Should use CSV.') + ensure(not coin_from_has_csv or not coin_to_has_csv, 'Should use CSV.') ensure(lock_value >= 10 and lock_value <= 1000, 'Invalid lock_value blocks') else: raise ValueError('Unknown locktype') @@ -2569,10 +2582,14 @@ class BasicSwap(BaseApp): if self.debug: # Check fee - if self.coin_clients[coin_type]['connection_type'] == 'rpc': + if ci.get_connection_type() == 'rpc': redeem_txjs = self.callcoinrpc(coin_type, 'decoderawtransaction', [redeem_txn]) - self.log.debug('vsize paid, actual vsize %d %d', tx_vsize, redeem_txjs['vsize']) - ensure(tx_vsize >= redeem_txjs['vsize'], 'Underpaid fee') + if ci.using_segwit(): + self.log.debug('vsize paid, actual vsize %d %d', tx_vsize, redeem_txjs['vsize']) + ensure(tx_vsize >= redeem_txjs['vsize'], 'underpaid fee') + else: + self.log.debug('size paid, actual size %d %d', tx_vsize, redeem_txjs['size']) + ensure(tx_vsize >= redeem_txjs['size'], 'underpaid fee') redeem_txjs = self.callcoinrpc(Coins.PART, 'decoderawtransaction', [redeem_txn]) self.log.debug('Have valid redeem txn %s for contract %s tx %s', redeem_txjs['txid'], for_txn_type, prev_txnid) @@ -2669,10 +2686,14 @@ class BasicSwap(BaseApp): if self.debug: # Check fee - if self.coin_clients[coin_type]['connection_type'] == 'rpc': + if ci.get_connection_type() == 'rpc': refund_txjs = self.callcoinrpc(coin_type, 'decoderawtransaction', [refund_txn]) - self.log.debug('vsize paid, actual vsize %d %d', tx_vsize, refund_txjs['vsize']) - ensure(tx_vsize >= refund_txjs['vsize'], 'underpaid fee') + if ci.using_segwit(): + self.log.debug('vsize paid, actual vsize %d %d', tx_vsize, refund_txjs['vsize']) + ensure(tx_vsize >= refund_txjs['vsize'], 'underpaid fee') + else: + self.log.debug('size paid, actual size %d %d', tx_vsize, refund_txjs['size']) + ensure(tx_vsize >= refund_txjs['size'], 'underpaid fee') refund_txjs = self.callcoinrpc(Coins.PART, 'decoderawtransaction', [refund_txn]) self.log.debug('Have valid refund txn %s for contract tx %s', refund_txjs['txid'], txjs['txid']) @@ -3496,13 +3517,14 @@ class BasicSwap(BaseApp): spend_txn = self.callcoinrpc(Coins.PART, 'getrawtransaction', [spend_txid, True]) self.processSpentOutput(coin_type, o, spend_txid, spend_n, spend_txn) else: - chain_blocks = self.callcoinrpc(coin_type, 'getblockcount') + ci = self.ci(coin_type) + chain_blocks = ci.getChainHeight() last_height_checked = c['last_height_checked'] self.log.debug('chain_blocks, last_height_checked %s %s', chain_blocks, last_height_checked) while last_height_checked < chain_blocks: block_hash = self.callcoinrpc(coin_type, 'getblockhash', [last_height_checked + 1]) try: - block = self.callcoinrpc(coin_type, 'getblock', [block_hash, 2]) + block = ci.getBlockWithTxns(block_hash) except Exception as e: if 'Block not available (pruned data)' in str(e): # TODO: Better solution? @@ -3510,6 +3532,9 @@ class BasicSwap(BaseApp): self.log.error('Coin %s last_height_checked %d set to pruneheight %d', self.ci(coin_type).coin_name(), last_height_checked, bci['pruneheight']) last_height_checked = bci['pruneheight'] continue + else: + self.log.error(f'getblock error {e}') + break for tx in block['tx']: for i, inp in enumerate(tx['vin']): diff --git a/basicswap/chainparams.py b/basicswap/chainparams.py index a26de43..1fa4e05 100644 --- a/basicswap/chainparams.py +++ b/basicswap/chainparams.py @@ -26,6 +26,9 @@ class Coins(IntEnum): XMR = 6 PART_BLIND = 7 PART_ANON = 8 + # ZANO = 9 + # NDAU = 10 + PIVX = 11 chainparams = { @@ -206,10 +209,45 @@ chainparams = { 'min_amount': 100000, 'max_amount': 10000 * XMR_COIN, } - } + }, + Coins.PIVX: { + 'name': 'pivx', + 'ticker': 'PIVX', + 'message_magic': 'DarkNet Signed Message:\n', + 'blocks_target': 60 * 1, + 'decimal_places': 8, + 'has_csv': False, + 'has_segwit': False, + 'mainnet': { + 'rpcport': 51473, + 'pubkey_address': 30, + 'script_address': 13, + 'key_prefix': 212, + 'bip44': 119, + 'min_amount': 1000, + 'max_amount': 100000 * COIN, + }, + 'testnet': { + 'rpcport': 51475, + 'pubkey_address': 139, + 'script_address': 19, + 'key_prefix': 239, + 'bip44': 1, + 'min_amount': 1000, + 'max_amount': 100000 * COIN, + 'name': 'testnet4', + }, + 'regtest': { + 'rpcport': 51477, + 'pubkey_address': 139, + 'script_address': 19, + 'key_prefix': 239, + 'bip44': 1, + 'min_amount': 1000, + 'max_amount': 100000 * COIN, + } + }, } - - ticker_map = {} diff --git a/basicswap/config.py b/basicswap/config.py index bf8533f..464634d 100644 --- a/basicswap/config.py +++ b/basicswap/config.py @@ -36,3 +36,8 @@ NAMECOIN_TX = os.getenv('NAMECOIN_TX', 'namecoin-tx' + bin_suffix) XMR_BINDIR = os.path.expanduser(os.getenv('XMR_BINDIR', os.path.join(DEFAULT_TEST_BINDIR, 'monero'))) XMRD = os.getenv('XMRD', 'monerod' + bin_suffix) XMR_WALLET_RPC = os.getenv('XMR_WALLET_RPC', 'monero-wallet-rpc' + bin_suffix) + +PIVX_BINDIR = os.path.expanduser(os.getenv('PIVX_BINDIR', os.path.join(DEFAULT_TEST_BINDIR, 'namecoin'))) +PIVXD = os.getenv('PIVXD', 'pivxd' + bin_suffix) +PIVX_CLI = os.getenv('PIVX_CLI', 'pivx-cli' + bin_suffix) +PIVX_TX = os.getenv('PIVX_TX', 'pivx-tx' + bin_suffix) diff --git a/basicswap/interface/btc.py b/basicswap/interface/btc.py index caa4a24..eea5ae9 100644 --- a/basicswap/interface/btc.py +++ b/basicswap/interface/btc.py @@ -185,9 +185,16 @@ class BTCInterface(CoinInterface): self.blocks_confirmed = coin_settings['blocks_confirmed'] self.setConfTarget(coin_settings['conf_target']) self._use_segwit = coin_settings['use_segwit'] + self._connection_type = coin_settings['connection_type'] self._sc = swap_client self._log = self._sc.log if self._sc and self._sc.log else logging + def using_segwit(self): + return self._use_segwit + + def get_connection_type(self): + return self._connection_type + def open_rpc(self, wallet=None): return openrpc(self._rpcport, self._rpcauth, wallet=wallet, host=self._rpc_host) @@ -285,12 +292,14 @@ class BTCInterface(CoinInterface): def get_fee_rate(self, conf_target=2): try: - return self.rpc_callback('estimatesmartfee', [conf_target])['feerate'], 'estimatesmartfee' + fee_rate = self.rpc_callback('estimatesmartfee', [conf_target])['feerate'] + assert (fee_rate > 0.0), 'Non positive feerate' + return fee_rate, 'estimatesmartfee' except Exception: try: - fee_rate = self.rpc_callback('getwalletinfo')['paytxfee'], 'paytxfee' - assert (fee_rate > 0.0), '0 feerate' - return fee_rate + fee_rate = self.rpc_callback('getwalletinfo')['paytxfee'] + assert (fee_rate > 0.0), 'Non positive feerate' + return fee_rate, 'paytxfee' except Exception: return self.rpc_callback('getnetworkinfo')['relayfee'], 'relayfee' @@ -1161,6 +1170,9 @@ class BTCInterface(CoinInterface): txn_signed = self.rpc_callback('signrawtransactionwithwallet', [txn_funded])['hex'] return txn_signed + def getBlockWithTxns(self, block_hash): + return self.rpc_callback('getblock', [block_hash, 2]) + def testBTCInterface(): print('testBTCInterface') diff --git a/basicswap/interface/contrib/__init__.py b/basicswap/interface/contrib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/basicswap/interface/contrib/pivx_test_framework/__init__.py b/basicswap/interface/contrib/pivx_test_framework/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/basicswap/interface/contrib/pivx_test_framework/authproxy.py b/basicswap/interface/contrib/pivx_test_framework/authproxy.py new file mode 100644 index 0000000..1de4b78 --- /dev/null +++ b/basicswap/interface/contrib/pivx_test_framework/authproxy.py @@ -0,0 +1,180 @@ +# Copyright (c) 2011 Jeff Garzik +# +# Previous copyright, from python-jsonrpc/jsonrpc/proxy.py: +# +# Copyright (c) 2007 Jan-Klaas Kollhof +# +# This file is part of jsonrpc. +# +# jsonrpc is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. +# +# This software 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this software; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +"""HTTP proxy for opening RPC connection to pivxd. + +AuthServiceProxy has the following improvements over python-jsonrpc's +ServiceProxy class: + +- HTTP connections persist for the life of the AuthServiceProxy object + (if server supports HTTP/1.1) +- sends protocol 'version', per JSON-RPC 1.1 +- sends proper, incrementing 'id' +- sends Basic HTTP authentication headers +- parses all JSON numbers that look like floats as Decimal +- uses standard Python json lib +""" + +import base64 +import decimal +import http.client +import json +import logging +import socket +import time +import urllib.parse + +HTTP_TIMEOUT = 300 +USER_AGENT = "AuthServiceProxy/0.1" + +log = logging.getLogger("BitcoinRPC") + +class JSONRPCException(Exception): + def __init__(self, rpc_error): + try: + errmsg = '%(message)s (%(code)i)' % rpc_error + except (KeyError, TypeError): + errmsg = '' + super().__init__(errmsg) + self.error = rpc_error + + +def EncodeDecimal(o): + if isinstance(o, decimal.Decimal): + return str(o) + raise TypeError(repr(o) + " is not JSON serializable") + +class AuthServiceProxy(): + __id_count = 0 + + # ensure_ascii: escape unicode as \uXXXX, passed to json.dumps + def __init__(self, service_url, service_name=None, timeout=HTTP_TIMEOUT, connection=None, ensure_ascii=True): + self.__service_url = service_url + self._service_name = service_name + self.ensure_ascii = ensure_ascii # can be toggled on the fly by tests + self.__url = urllib.parse.urlparse(service_url) + port = 80 if self.__url.port is None else self.__url.port + user = None if self.__url.username is None else self.__url.username.encode('utf8') + passwd = None if self.__url.password is None else self.__url.password.encode('utf8') + authpair = user + b':' + passwd + self.__auth_header = b'Basic ' + base64.b64encode(authpair) + + if connection: + # Callables re-use the connection of the original proxy + self.__conn = connection + elif self.__url.scheme == 'https': + self.__conn = http.client.HTTPSConnection(self.__url.hostname, port, timeout=timeout) + else: + self.__conn = http.client.HTTPConnection(self.__url.hostname, port, timeout=timeout) + + def __getattr__(self, name): + if name.startswith('__') and name.endswith('__'): + # Python internal stuff + raise AttributeError + if self._service_name is not None: + name = "%s.%s" % (self._service_name, name) + return AuthServiceProxy(self.__service_url, name, connection=self.__conn) + + def _request(self, method, path, postdata): + ''' + Do a HTTP request, with retry if we get disconnected (e.g. due to a timeout). + This is a workaround for https://bugs.python.org/issue3566 which is fixed in Python 3.5. + ''' + headers = {'Host': self.__url.hostname, + 'User-Agent': USER_AGENT, + 'Authorization': self.__auth_header, + 'Content-type': 'application/json'} + try: + self.__conn.request(method, path, postdata, headers) + return self._get_response() + except http.client.BadStatusLine as e: + if e.line == "''": # if connection was closed, try again + self.__conn.close() + self.__conn.request(method, path, postdata, headers) + return self._get_response() + else: + raise + except (BrokenPipeError, ConnectionResetError): + # Python 3.5+ raises BrokenPipeError instead of BadStatusLine when the connection was reset + # ConnectionResetError happens on FreeBSD with Python 3.4 + self.__conn.close() + self.__conn.request(method, path, postdata, headers) + return self._get_response() + + def get_request(self, *args, **argsn): + AuthServiceProxy.__id_count += 1 + + log.debug("-%s-> %s %s" % (AuthServiceProxy.__id_count, self._service_name, + json.dumps(args, default=EncodeDecimal, ensure_ascii=self.ensure_ascii))) + if args and argsn: + raise ValueError('Cannot handle both named and positional arguments') + return {'version': '1.1', + 'method': self._service_name, + 'params': args or argsn, + 'id': AuthServiceProxy.__id_count} + + def __call__(self, *args, **argsn): + postdata = json.dumps(self.get_request(*args, **argsn), default=EncodeDecimal, ensure_ascii=self.ensure_ascii) + response = self._request('POST', self.__url.path, postdata.encode('utf-8')) + if response['error'] is not None: + raise JSONRPCException(response['error']) + elif 'result' not in response: + raise JSONRPCException({ + 'code': -343, 'message': 'missing JSON-RPC result'}) + else: + return response['result'] + + def batch(self, rpc_call_list): + postdata = json.dumps(list(rpc_call_list), default=EncodeDecimal, ensure_ascii=self.ensure_ascii) + log.debug("--> " + postdata) + return self._request('POST', self.__url.path, postdata.encode('utf-8')) + + def _get_response(self): + req_start_time = time.time() + try: + http_response = self.__conn.getresponse() + except socket.timeout: + raise JSONRPCException({ + 'code': -344, + 'message': '%r RPC took longer than %f seconds. Consider ' + 'using larger timeout for calls that take ' + 'longer to return.' % (self._service_name, + self.__conn.timeout)}) + if http_response is None: + raise JSONRPCException({ + 'code': -342, 'message': 'missing HTTP response from server'}) + + content_type = http_response.getheader('Content-Type') + if content_type != 'application/json': + raise JSONRPCException({ + 'code': -342, 'message': 'non-JSON HTTP response with \'%i %s\' from server' % (http_response.status, http_response.reason)}) + + responsedata = http_response.read().decode('utf8') + response = json.loads(responsedata, parse_float=decimal.Decimal) + elapsed = time.time() - req_start_time + if "error" in response and response["error"] is None: + log.debug("<-%s- [%.6f] %s" % (response["id"], elapsed, json.dumps(response["result"], default=EncodeDecimal, ensure_ascii=self.ensure_ascii))) + else: + log.debug("<-- [%.6f] %s" % (elapsed, responsedata)) + return response + + def __truediv__(self, relative_uri): + return AuthServiceProxy("{}/{}".format(self.__service_url, relative_uri), self._service_name, connection=self.__conn) diff --git a/basicswap/interface/contrib/pivx_test_framework/coverage.py b/basicswap/interface/contrib/pivx_test_framework/coverage.py new file mode 100644 index 0000000..4d6857e --- /dev/null +++ b/basicswap/interface/contrib/pivx_test_framework/coverage.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +# Copyright (c) 2015-2017 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Utilities for doing coverage analysis on the RPC interface. + +Provides a way to track which RPC commands are exercised during +testing. +""" + +import os + + +REFERENCE_FILENAME = 'rpc_interface.txt' + + +class AuthServiceProxyWrapper(): + """ + An object that wraps AuthServiceProxy to record specific RPC calls. + + """ + def __init__(self, auth_service_proxy_instance, coverage_logfile=None): + """ + Kwargs: + auth_service_proxy_instance (AuthServiceProxy): the instance + being wrapped. + coverage_logfile (str): if specified, write each service_name + out to a file when called. + + """ + self.auth_service_proxy_instance = auth_service_proxy_instance + self.coverage_logfile = coverage_logfile + + def __getattr__(self, name): + return_val = getattr(self.auth_service_proxy_instance, name) + if not isinstance(return_val, type(self.auth_service_proxy_instance)): + # If proxy getattr returned an unwrapped value, do the same here. + return return_val + return AuthServiceProxyWrapper(return_val, self.coverage_logfile) + + def __call__(self, *args, **kwargs): + """ + Delegates to AuthServiceProxy, then writes the particular RPC method + called to a file. + + """ + return_val = self.auth_service_proxy_instance.__call__(*args, **kwargs) + self._log_call() + return return_val + + def _log_call(self): + rpc_method = self.auth_service_proxy_instance._service_name + + if self.coverage_logfile: + with open(self.coverage_logfile, 'a+', encoding='utf8') as f: + f.write("%s\n" % rpc_method) + + def __truediv__(self, relative_uri): + return AuthServiceProxyWrapper(self.auth_service_proxy_instance / relative_uri, + self.coverage_logfile) + + def get_request(self, *args, **kwargs): + self._log_call() + return self.auth_service_proxy_instance.get_request(*args) + +def get_filename(dirname, n_node): + """ + Get a filename unique to the test process ID and node. + + This file will contain a list of RPC commands covered. + """ + pid = str(os.getpid()) + return os.path.join( + dirname, "coverage.pid%s.node%s.txt" % (pid, str(n_node))) + + +def write_all_rpc_commands(dirname, node): + """ + Write out a list of all RPC functions available in `pivx-cli` for + coverage comparison. This will only happen once per coverage + directory. + + Args: + dirname (str): temporary test dir + node (AuthServiceProxy): client + + Returns: + bool. if the RPC interface file was written. + + """ + filename = os.path.join(dirname, REFERENCE_FILENAME) + + if os.path.isfile(filename): + return False + + help_output = node.help().split('\n') + commands = set() + + for line in help_output: + line = line.strip() + + # Ignore blanks and headers + if line and not line.startswith('='): + commands.add("%s\n" % line.split()[0]) + + with open(filename, 'w', encoding='utf8') as f: + f.writelines(list(commands)) + + return True diff --git a/basicswap/interface/contrib/pivx_test_framework/messages.py b/basicswap/interface/contrib/pivx_test_framework/messages.py new file mode 100755 index 0000000..be74041 --- /dev/null +++ b/basicswap/interface/contrib/pivx_test_framework/messages.py @@ -0,0 +1,1505 @@ +#!/usr/bin/env python3 +# Copyright (c) 2010 ArtForz -- public domain half-a-node +# Copyright (c) 2012 Jeff Garzik +# Copyright (c) 2010-2017 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Bitcoin test framework primitive and message strcutures + +CBlock, CTransaction, CBlockHeader, CTxIn, CTxOut, etc....: + data structures that should map to corresponding structures in + bitcoin/primitives + +msg_block, msg_tx, msg_headers, etc.: + data structures that represent network messages + +ser_*, deser_*: functions that handle serialization/deserialization. +""" + +from codecs import encode +import copy +import hashlib +from io import BytesIO +import random +import socket +import struct +import time + +from .siphash import siphash256 +from .util import hex_str_to_bytes, bytes_to_hex_str + +MIN_VERSION_SUPPORTED = 60001 +MY_VERSION = 70925 +MY_SUBVERSION = "/python-mininode-tester:0.0.3/" +MY_RELAY = 1 # from version 70001 onwards, fRelay should be appended to version messages (BIP37) + +MAX_INV_SZ = 50000 +MAX_BLOCK_BASE_SIZE = 2000000 +CURRENT_BLK_VERSION = 11 + +COIN = 100000000 # 1 PIV in satoshis + +NODE_NETWORK = (1 << 0) +# NODE_GETUTXO = (1 << 1) +NODE_BLOOM = (1 << 2) + +MSG_TX = 1 +MSG_BLOCK = 2 +MSG_TYPE_MASK = 0xffffffff >> 2 + +# Serialization/deserialization tools +def sha256(s): + return hashlib.new('sha256', s).digest() + +def ripemd160(s): + return hashlib.new('ripemd160', s).digest() + +def hash256(s): + return sha256(sha256(s)) + +def ser_compact_size(l): + r = b"" + if l < 253: + r = struct.pack("B", l) + elif l < 0x10000: + r = struct.pack(">= 32 + return rs + + +def ser_uint64(u): + rs = b"" + for i in range(2): + rs += struct.pack(">= 32 + return rs + + +def uint256_from_str(s): + r = 0 + t = struct.unpack("> 24) & 0xFF + v = (c & 0xFFFFFF) << (8 * (nbytes - 3)) + return v + + +# deser_function_name: Allow for an alternate deserialization function on the +# entries in the vector. +def deser_vector(f, c, deser_function_name=None): + nit = deser_compact_size(f) + r = [] + for _ in range(nit): + t = c() + if deser_function_name: + getattr(t, deser_function_name)(f) + else: + t.deserialize(f) + r.append(t) + return r + + +# ser_function_name: Allow for an alternate serialization function on the +# entries in the vector (we use this for serializing the vector of transactions +# for a witness block). +def ser_vector(l, ser_function_name=None): + r = ser_compact_size(len(l)) + for i in l: + if ser_function_name: + r += getattr(i, ser_function_name)() + else: + r += i.serialize() + return r + + +def deser_uint256_vector(f): + nit = deser_compact_size(f) + r = [] + for i in range(nit): + t = deser_uint256(f) + r.append(t) + return r + + +def ser_uint256_vector(l): + r = ser_compact_size(len(l)) + for i in l: + r += ser_uint256(i) + return r + + +def deser_string_vector(f): + nit = deser_compact_size(f) + r = [] + for i in range(nit): + t = deser_string(f) + r.append(t) + return r + + +def ser_string_vector(l): + r = ser_compact_size(len(l)) + for sv in l: + r += ser_string(sv) + return r + + +# Deserialize from a hex string representation (eg from RPC) +def FromHex(obj, hex_string): + obj.deserialize(BytesIO(hex_str_to_bytes(hex_string))) + return obj + +# Convert a binary-serializable object to hex (eg for submission via RPC) +def ToHex(obj): + return bytes_to_hex_str(obj.serialize()) + +# Objects that map to pivxd objects, which can be serialized/deserialized + +class CAddress: + __slots__ = ("net", "ip", "nServices", "port", "time") + + # see https://github.com/bitcoin/bips/blob/master/bip-0155.mediawiki + NET_IPV4 = 1 + + ADDRV2_NET_NAME = { + NET_IPV4: "IPv4" + } + + ADDRV2_ADDRESS_LENGTH = { + NET_IPV4: 4 + } + + def __init__(self): + self.time = 0 + self.nServices = 1 + self.net = self.NET_IPV4 + self.ip = "0.0.0.0" + self.port = 0 + + def deserialize(self, f, *, with_time=True): + """Deserialize from addrv1 format (pre-BIP155)""" + if with_time: + # VERSION messages serialize CAddress objects without time + self.time = struct.unpack("H", f.read(2))[0] + + def serialize(self, *, with_time=True): + """Serialize in addrv1 format (pre-BIP155)""" + assert self.net == self.NET_IPV4 + r = b"" + if with_time: + # VERSION messages serialize CAddress objects without time + r += struct.pack("H", self.port) + return r + + def deserialize_v2(self, f): + """Deserialize from addrv2 format (BIP155)""" + self.time = struct.unpack("H", f.read(2))[0] + + def serialize_v2(self): + """Serialize in addrv2 format (BIP155)""" + assert self.net == self.NET_IPV4 + r = b"" + r += struct.pack("H", self.port) + return r + + def __repr__(self): + return ("CAddress(nServices=%i net=%s addr=%s port=%i)" + % (self.nServices, self.ADDRV2_NET_NAME[self.net], self.ip, self.port)) + +class CInv: + typemap = { + 0: "MSG_ERROR", + 1: "MSG_TX", + 2: "MSG_BLOCK", + 3: "MSG_FILTERED_BLOCK", + 4: "MSG_TXLOCK_REQUEST", + 5: "MSG_TXLOCK_VOTE", + 6: "MSG_SPORK", + 7: "MSG_MASTERNODE_WINNER", + 8: "MSG_MASTERNODE_SCANNING_ERROR", + 9: "MSG_BUDGET_VOTE", + 10: "MSG_BUDGET_PROPOSAL", + 11: "MSG_BUDGET_FINALIZED", + 12: "MSG_BUDGET_FINALIZED_VOTE", + 13: "MSG_MASTERNODE_QUORUM", + 15: "MSG_MASTERNODE_ANNOUNCE", + 16: "MSG_MASTERNODE_PING", + 17: "MSG_DSTX" + } + + def __init__(self, t=0, h=0): + self.type = t + self.hash = h + + def deserialize(self, f): + self.type = struct.unpack("= 2: + self.sapData = deser_string(f) + self.sha256 = None + self.hash = None + + def serialize_without_witness(self): + r = b"" + r += struct.pack("= 2: + r += ser_string(self.sapData) + return r + + # Regular serialization is with witness -- must explicitly + # call serialize_without_witness to exclude witness data. + def serialize(self): + return self.serialize_without_witness() + + # Recalculate the txid (transaction hash without witness) + def rehash(self): + self.sha256 = None + self.calc_sha256() + + # We will only cache the serialization without witness in + # self.sha256 and self.hash -- those are expected to be the txid. + def calc_sha256(self, with_witness=False): + if self.sha256 is None: + self.sha256 = uint256_from_str(hash256(self.serialize_without_witness())) + self.hash = encode(hash256(self.serialize_without_witness())[::-1], 'hex_codec').decode('ascii') + + def is_valid(self): + self.calc_sha256() + for tout in self.vout: + if tout.nValue < 0 or tout.nValue > 21000000 * COIN: + return False + return True + + def is_coinbase(self): + return ( + len(self.vin) == 1 and + self.vin[0].prevout == NullOutPoint and + (not self.vin[0].is_zerocoinspend()) + ) + + def is_coinstake(self): + return ( + len(self.vin) == 1 and + len(self.vout) >= 2 and + self.vout[0] == CTxOut() + ) + + def from_hex(self, hexstring): + f = BytesIO(hex_str_to_bytes(hexstring)) + self.deserialize(f) + + def spends(self, outpoint): + return len([x for x in self.vin if + x.prevout.hash == outpoint.hash and x.prevout.n == outpoint.n]) > 0 + + def __repr__(self): + return "CTransaction(nVersion=%i vin=%s vout=%s nLockTime=%i)" \ + % (self.nVersion, repr(self.vin), repr(self.vout), self.nLockTime) + + +class CBlockHeader: + def __init__(self, header=None): + if header is None: + self.set_null() + else: + self.nVersion = header.nVersion + self.hashPrevBlock = header.hashPrevBlock + self.hashMerkleRoot = header.hashMerkleRoot + self.nTime = header.nTime + self.nBits = header.nBits + self.nNonce = header.nNonce + self.hashFinalSaplingRoot = header.hashFinalSaplingRoot + self.sha256 = header.sha256 + self.hash = header.hash + self.calc_sha256() + + def set_null(self): + self.nVersion = CURRENT_BLK_VERSION + self.hashPrevBlock = 0 + self.hashMerkleRoot = 0 + self.nTime = 0 + self.nBits = 0 + self.nNonce = 0 + self.hashFinalSaplingRoot = 0 + self.sha256 = None + self.hash = None + + def deserialize(self, f): + self.nVersion = struct.unpack("= 8: + self.hashFinalSaplingRoot = deser_uint256(f) + self.sha256 = None + self.hash = None + + def serialize(self): + r = b"" + r += struct.pack("= 8: + r += ser_uint256(self.hashFinalSaplingRoot) + return r + + def calc_sha256(self): + if self.sha256 is None: + r = b"" + r += struct.pack("= 8: + r += ser_uint256(self.hashFinalSaplingRoot) + self.sha256 = uint256_from_str(hash256(r)) + self.hash = encode(hash256(r)[::-1], 'hex_codec').decode('ascii') + + def rehash(self): + self.sha256 = None + self.calc_sha256() + return self.sha256 + + # PIVX + def solve_stake(self, stakeInputs, prevModifier): + target0 = uint256_from_compact(self.nBits) + loop = True + while loop: + for uniqueness in stakeInputs: + nvalue, _, prevTime = stakeInputs[uniqueness] + target = int(target0 * nvalue / 100) % 2**256 + data = b"" + # always modifier V2 (256 bits) on regtest + data += ser_uint256(prevModifier) + data += struct.pack(" 1: + newhashes = [] + for i in range(0, len(hashes), 2): + i2 = min(i+1, len(hashes)-1) + newhashes.append(hash256(hashes[i] + hashes[i2])) + hashes = newhashes + return uint256_from_str(hashes[0]) + + def calc_merkle_root(self): + hashes = [] + for tx in self.vtx: + tx.calc_sha256() + hashes.append(ser_uint256(tx.sha256)) + return self.get_merkle_root(hashes) + + def calc_witness_merkle_root(self): + # For witness root purposes, the hash of the + # coinbase, with witness, is defined to be 0...0 + hashes = [ser_uint256(0)] + + for tx in self.vtx[1:]: + # Calculate the hashes with witness data + hashes.append(ser_uint256(tx.calc_sha256(True))) + + return self.get_merkle_root(hashes) + + def is_valid(self): + self.calc_sha256() + target = uint256_from_compact(self.nBits) + if self.sha256 > target: + return False + for tx in self.vtx: + if not tx.is_valid(): + return False + if self.calc_merkle_root() != self.hashMerkleRoot: + return False + return True + + def solve(self): + self.rehash() + target = uint256_from_compact(self.nBits) + while self.sha256 > target: + self.nNonce += 1 + self.rehash() + + def sign_block(self, key, low_s=True): + data = b"" + data += struct.pack("= 8: + data += ser_uint256(self.hashFinalSaplingRoot) + sha256NoSig = hash256(data) + self.vchBlockSig = key.sign(sha256NoSig, low_s=low_s) + self.sig_key = key + self.low_s = low_s + + def re_sign_block(self): + if self.sig_key is None: + raise Exception("Unable to re-sign block. Key Not present, use 'sign_block' first.") + return self.sign_block(self.sig_key, self.low_s) + + def __repr__(self): + return "CBlock(nVersion=%i hashPrevBlock=%064x hashMerkleRoot=%064x nTime=%s nBits=%08x nNonce=%08x vtx=%s)" \ + % (self.nVersion, self.hashPrevBlock, self.hashMerkleRoot, + time.ctime(self.nTime), self.nBits, self.nNonce, repr(self.vtx)) + + +class PrefilledTransaction: + def __init__(self, index=0, tx = None): + self.index = index + self.tx = tx + + def deserialize(self, f): + self.index = deser_compact_size(f) + self.tx = CTransaction() + self.tx.deserialize(f) + + def serialize(self, with_witness=True): + r = b"" + r += ser_compact_size(self.index) + if with_witness: + r += self.tx.serialize_with_witness() + else: + r += self.tx.serialize_without_witness() + return r + + def serialize_without_witness(self): + return self.serialize(with_witness=False) + + def serialize_with_witness(self): + return self.serialize(with_witness=True) + + def __repr__(self): + return "PrefilledTransaction(index=%d, tx=%s)" % (self.index, repr(self.tx)) + +# This is what we send on the wire, in a cmpctblock message. +class P2PHeaderAndShortIDs: + def __init__(self): + self.header = CBlockHeader() + self.nonce = 0 + self.shortids_length = 0 + self.shortids = [] + self.prefilled_txn_length = 0 + self.prefilled_txn = [] + + def deserialize(self, f): + self.header.deserialize(f) + self.nonce = struct.unpack("= 106: + self.addrFrom = CAddress() + self.addrFrom.deserialize(f, with_time=False) + self.nNonce = struct.unpack("= 209: + self.nStartingHeight = struct.unpack("= 70001: + # Relay field is optional for version 70001 onwards + try: + self.nRelay = struct.unpack("= 70925: + try: + self.mn_auth_challenge = deser_uint256(f) + except: + self.mn_auth_challenge = 0 + + + def serialize(self): + r = b"" + r += struct.pack(" +class msg_headers: + command = b"headers" + + def __init__(self, headers=None): + self.headers = headers if headers is not None else [] + + def deserialize(self, f): + # comment in pivxd indicates these should be deserialized as blocks + blocks = deser_vector(f, CBlock) + for x in blocks: + self.headers.append(CBlockHeader(x)) + + def serialize(self): + blocks = [CBlock(x) for x in self.headers] + return ser_vector(blocks) + + def __repr__(self): + return "msg_headers(headers=%s)" % repr(self.headers) + +class msg_feefilter: + command = b"feefilter" + + def __init__(self, feerate=0): + self.feerate = feerate + + def deserialize(self, f): + self.feerate = struct.unpack("> (64 - b) | (n & ((1 << (64 - b)) - 1)) << b + +def siphash_round(v0, v1, v2, v3): + v0 = (v0 + v1) & ((1 << 64) - 1) + v1 = rotl64(v1, 13) + v1 ^= v0 + v0 = rotl64(v0, 32) + v2 = (v2 + v3) & ((1 << 64) - 1) + v3 = rotl64(v3, 16) + v3 ^= v2 + v0 = (v0 + v3) & ((1 << 64) - 1) + v3 = rotl64(v3, 21) + v3 ^= v0 + v2 = (v2 + v1) & ((1 << 64) - 1) + v1 = rotl64(v1, 17) + v1 ^= v2 + v2 = rotl64(v2, 32) + return (v0, v1, v2, v3) + +def siphash256(k0, k1, h): + n0 = h & ((1 << 64) - 1) + n1 = (h >> 64) & ((1 << 64) - 1) + n2 = (h >> 128) & ((1 << 64) - 1) + n3 = (h >> 192) & ((1 << 64) - 1) + v0 = 0x736f6d6570736575 ^ k0 + v1 = 0x646f72616e646f6d ^ k1 + v2 = 0x6c7967656e657261 ^ k0 + v3 = 0x7465646279746573 ^ k1 ^ n0 + v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3) + v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3) + v0 ^= n0 + v3 ^= n1 + v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3) + v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3) + v0 ^= n1 + v3 ^= n2 + v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3) + v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3) + v0 ^= n2 + v3 ^= n3 + v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3) + v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3) + v0 ^= n3 + v3 ^= 0x2000000000000000 + v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3) + v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3) + v0 ^= 0x2000000000000000 + v2 ^= 0xFF + v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3) + v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3) + v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3) + v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3) + return v0 ^ v1 ^ v2 ^ v3 diff --git a/basicswap/interface/contrib/pivx_test_framework/util.py b/basicswap/interface/contrib/pivx_test_framework/util.py new file mode 100644 index 0000000..8e79bdd --- /dev/null +++ b/basicswap/interface/contrib/pivx_test_framework/util.py @@ -0,0 +1,625 @@ +#!/usr/bin/env python3 +# Copyright (c) 2014-2017 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Helpful routines for regression testing.""" + +from base64 import b64encode +from binascii import hexlify, unhexlify +from decimal import Decimal, ROUND_DOWN +import hashlib +import json +import logging +import os +import random +import re +from subprocess import CalledProcessError +import time + +from . import coverage, messages +from .authproxy import AuthServiceProxy, JSONRPCException + +logger = logging.getLogger("TestFramework.utils") + +# Assert functions +################## + +def assert_fee_amount(fee, tx_size, fee_per_kB): + """Assert the fee was in range""" + target_fee = round(tx_size * fee_per_kB / 1000, 8) + if fee < target_fee: + raise AssertionError("Fee of %s PIV too low! (Should be %s PIV)" % (str(fee), str(target_fee))) + # allow the wallet's estimation to be at most 2 bytes off + if fee > (tx_size + 20) * fee_per_kB / 1000: + raise AssertionError("Fee of %s PIV too high! (Should be %s PIV)" % (str(fee), str(target_fee))) + +def assert_equal(thing1, thing2, *args): + if thing1 != thing2 or any(thing1 != arg for arg in args): + raise AssertionError("not(%s)" % " == ".join(str(arg) for arg in (thing1, thing2) + args)) + +def assert_true(condition, message = ""): + if not condition: + raise AssertionError(message) + +def assert_false(condition, message = ""): + assert_true(not condition, message) + +def assert_greater_than(thing1, thing2): + if thing1 <= thing2: + raise AssertionError("%s <= %s" % (str(thing1), str(thing2))) + +def assert_greater_than_or_equal(thing1, thing2): + if thing1 < thing2: + raise AssertionError("%s < %s" % (str(thing1), str(thing2))) + +def assert_raises(exc, fun, *args, **kwds): + assert_raises_message(exc, None, fun, *args, **kwds) + +def assert_raises_message(exc, message, fun, *args, **kwds): + try: + fun(*args, **kwds) + except JSONRPCException: + raise AssertionError("Use assert_raises_rpc_error() to test RPC failures") + except exc as e: + if message is not None and message not in e.error['message']: + raise AssertionError("Expected substring not found:" + e.error['message']) + except Exception as e: + raise AssertionError("Unexpected exception raised: " + type(e).__name__) + else: + raise AssertionError("No exception raised") + +def assert_raises_process_error(returncode, output, fun, *args, **kwds): + """Execute a process and asserts the process return code and output. + + Calls function `fun` with arguments `args` and `kwds`. Catches a CalledProcessError + and verifies that the return code and output are as expected. Throws AssertionError if + no CalledProcessError was raised or if the return code and output are not as expected. + + Args: + returncode (int): the process return code. + output (string): [a substring of] the process output. + fun (function): the function to call. This should execute a process. + args*: positional arguments for the function. + kwds**: named arguments for the function. + """ + try: + fun(*args, **kwds) + except CalledProcessError as e: + if returncode != e.returncode: + raise AssertionError("Unexpected returncode %i" % e.returncode) + if output not in e.output: + raise AssertionError("Expected substring not found:" + e.output) + else: + raise AssertionError("No exception raised") + +def assert_raises_rpc_error(code, message, fun, *args, **kwds): + """Run an RPC and verify that a specific JSONRPC exception code and message is raised. + + Calls function `fun` with arguments `args` and `kwds`. Catches a JSONRPCException + and verifies that the error code and message are as expected. Throws AssertionError if + no JSONRPCException was raised or if the error code/message are not as expected. + + Args: + code (int), optional: the error code returned by the RPC call (defined + in src/rpc/protocol.h). Set to None if checking the error code is not required. + message (string), optional: [a substring of] the error string returned by the + RPC call. Set to None if checking the error string is not required. + fun (function): the function to call. This should be the name of an RPC. + args*: positional arguments for the function. + kwds**: named arguments for the function. + """ + assert try_rpc(code, message, fun, *args, **kwds), "No exception raised" + +def try_rpc(code, message, fun, *args, **kwds): + """Tries to run an rpc command. + + Test against error code and message if the rpc fails. + Returns whether a JSONRPCException was raised.""" + try: + fun(*args, **kwds) + except JSONRPCException as e: + # JSONRPCException was thrown as expected. Check the code and message values are correct. + if (code is not None) and (code != e.error["code"]): + raise AssertionError("Unexpected JSONRPC error code %i" % e.error["code"]) + if (message is not None) and (message not in e.error['message']): + raise AssertionError("Expected substring (%s) not found in: %s" % (message, e.error['message'])) + return True + except Exception as e: + raise AssertionError("Unexpected exception raised: " + type(e).__name__) + else: + return False + +def assert_is_hex_string(string): + try: + int(string, 16) + except Exception as e: + raise AssertionError( + "Couldn't interpret %r as hexadecimal; raised: %s" % (string, e)) + +def assert_is_hash_string(string, length=64): + if not isinstance(string, str): + raise AssertionError("Expected a string, got type %r" % type(string)) + elif length and len(string) != length: + raise AssertionError( + "String of length %d expected; got %d" % (length, len(string))) + elif not re.match('[abcdef0-9]+$', string): + raise AssertionError( + "String %r contains invalid characters for a hash." % string) + +def assert_array_result(object_array, to_match, expected, should_not_find=False): + """ + Pass in array of JSON objects, a dictionary with key/value pairs + to match against, and another dictionary with expected key/value + pairs. + If the should_not_find flag is true, to_match should not be found + in object_array + """ + if should_not_find: + assert_equal(expected, {}) + num_matched = 0 + for item in object_array: + all_match = True + for key, value in to_match.items(): + if item[key] != value: + all_match = False + if not all_match: + continue + elif should_not_find: + num_matched = num_matched + 1 + for key, value in expected.items(): + if item[key] != value: + raise AssertionError("%s : expected %s=%s" % (str(item), str(key), str(value))) + num_matched = num_matched + 1 + if num_matched == 0 and not should_not_find: + raise AssertionError("No objects matched %s" % (str(to_match))) + if num_matched > 0 and should_not_find: + raise AssertionError("Objects were found %s" % (str(to_match))) + +# Utility functions +################### + +def check_json_precision(): + """Make sure json library being used does not lose precision converting BTC values""" + n = Decimal("20000000.00000003") + satoshis = int(json.loads(json.dumps(float(n))) * 1.0e8) + if satoshis != 2000000000000003: + raise RuntimeError("JSON encode/decode loses precision") + +def count_bytes(hex_string): + return len(bytearray.fromhex(hex_string)) + +def bytes_to_hex_str(byte_str): + return hexlify(byte_str).decode('ascii') + +def hash256(byte_str): + sha256 = hashlib.sha256() + sha256.update(byte_str) + sha256d = hashlib.sha256() + sha256d.update(sha256.digest()) + return sha256d.digest()[::-1] + +def hex_str_to_bytes(hex_str): + return unhexlify(hex_str.encode('ascii')) + +def str_to_b64str(string): + return b64encode(string.encode('utf-8')).decode('ascii') + +def satoshi_round(amount): + return Decimal(amount).quantize(Decimal('0.00000001'), rounding=ROUND_DOWN) + +def wait_until(predicate, + *, + attempts=float('inf'), + timeout=float('inf'), + lock=None, + sendpings=None, + mocktime=None): + + if attempts == float('inf') and timeout == float('inf'): + timeout = 60 + attempt = 0 + timeout += time.time() + + while attempt < attempts and time.time() < timeout: + if lock: + with lock: + if predicate(): + return + else: + if predicate(): + return + attempt += 1 + time.sleep(0.5) + if sendpings is not None: + sendpings() + if mocktime is not None: + mocktime(1) + + # Print the cause of the timeout + assert_greater_than(attempts, attempt) + assert_greater_than(timeout, time.time()) + raise RuntimeError('Unreachable') + +# RPC/P2P connection constants and functions +############################################ + +# The maximum number of nodes a single test can spawn +MAX_NODES = 8 +# Don't assign rpc or p2p ports lower than this +PORT_MIN = 11000 +# The number of ports to "reserve" for p2p and rpc, each +PORT_RANGE = 5000 + +class PortSeed: + # Must be initialized with a unique integer for each process + n = None + +def get_rpc_proxy(url, node_number, timeout=None, coveragedir=None): + """ + Args: + url (str): URL of the RPC server to call + node_number (int): the node number (or id) that this calls to + + Kwargs: + timeout (int): HTTP timeout in seconds + + Returns: + AuthServiceProxy. convenience object for making RPC calls. + + """ + proxy_kwargs = {} + if timeout is not None: + proxy_kwargs['timeout'] = timeout + + proxy = AuthServiceProxy(url, **proxy_kwargs) + proxy.url = url # store URL on proxy for info + + coverage_logfile = coverage.get_filename( + coveragedir, node_number) if coveragedir else None + + return coverage.AuthServiceProxyWrapper(proxy, coverage_logfile) + +def p2p_port(n): + assert(n <= MAX_NODES) + return PORT_MIN + n + (MAX_NODES * PortSeed.n) % (PORT_RANGE - 1 - MAX_NODES) + +def rpc_port(n): + return PORT_MIN + PORT_RANGE + n + (MAX_NODES * PortSeed.n) % (PORT_RANGE - 1 - MAX_NODES) + +def rpc_url(datadir, i, rpchost=None): + rpc_u, rpc_p = get_auth_cookie(datadir) + host = '127.0.0.1' + port = rpc_port(i) + if rpchost: + parts = rpchost.split(':') + if len(parts) == 2: + host, port = parts + else: + host = rpchost + return "http://%s:%s@%s:%d" % (rpc_u, rpc_p, host, int(port)) + +# Node functions +################ + +def initialize_datadir(dirname, n): + datadir = get_datadir_path(dirname, n) + if not os.path.isdir(datadir): + os.makedirs(datadir) + with open(os.path.join(datadir, "pivx.conf"), 'w', encoding='utf8') as f: + f.write("regtest=1\n") + f.write("[regtest]\n") + f.write("port=" + str(p2p_port(n)) + "\n") + f.write("rpcport=" + str(rpc_port(n)) + "\n") + f.write("server=1\n") + f.write("keypool=1\n") + f.write("discover=0\n") + f.write("listenonion=0\n") + f.write("spendzeroconfchange=1\n") + f.write("printtoconsole=0\n") + f.write("natpmp=0\n") + return datadir + +def get_datadir_path(dirname, n): + return os.path.join(dirname, "node" + str(n)) + +def append_config(dirname, n, options): + datadir = get_datadir_path(dirname, n) + with open(os.path.join(datadir, "pivx.conf"), 'a', encoding='utf8') as f: + for option in options: + f.write(option + "\n") + +def get_auth_cookie(datadir): + user = None + password = None + if os.path.isfile(os.path.join(datadir, "pivx.conf")): + with open(os.path.join(datadir, "pivx.conf"), 'r', encoding='utf8') as f: + for line in f: + if line.startswith("rpcuser="): + assert user is None # Ensure that there is only one rpcuser line + user = line.split("=")[1].strip("\n") + if line.startswith("rpcpassword="): + assert password is None # Ensure that there is only one rpcpassword line + password = line.split("=")[1].strip("\n") + if os.path.isfile(os.path.join(datadir, "regtest", ".cookie")): + with open(os.path.join(datadir, "regtest", ".cookie"), 'r', encoding="utf8") as f: + userpass = f.read() + split_userpass = userpass.split(':') + user = split_userpass[0] + password = split_userpass[1] + if user is None or password is None: + raise ValueError("No RPC credentials") + return user, password + +# If a cookie file exists in the given datadir, delete it. +def delete_cookie_file(datadir): + if os.path.isfile(os.path.join(datadir, "regtest", ".cookie")): + logger.debug("Deleting leftover cookie file") + os.remove(os.path.join(datadir, "regtest", ".cookie")) + +def get_bip9_status(node, key): + info = node.getblockchaininfo() + return info['bip9_softforks'][key] + +def set_node_times(nodes, t): + for node in nodes: + node.setmocktime(t) + +def disconnect_nodes(from_connection, node_num): + for addr in [peer['addr'] for peer in from_connection.getpeerinfo() if "testnode%d" % node_num in peer['subver']]: + try: + from_connection.disconnectnode(addr) + except JSONRPCException as e: + # If this node is disconnected between calculating the peer id + # and issuing the disconnect, don't worry about it. + # This avoids a race condition if we're mass-disconnecting peers. + if e.error['code'] != -29: # RPC_CLIENT_NODE_NOT_CONNECTED + raise + + # wait to disconnect + wait_until(lambda: [peer['addr'] for peer in from_connection.getpeerinfo() if "testnode%d" % node_num in peer['subver']] == [], timeout=5) + +def connect_nodes(from_connection, node_num): + ip_port = "127.0.0.1:" + str(p2p_port(node_num)) + from_connection.addnode(ip_port, "onetry") + # poll until version handshake complete to avoid race conditions + # with transaction relaying + wait_until(lambda: all(peer['version'] != 0 for peer in from_connection.getpeerinfo())) + +def connect_nodes_clique(nodes): + l = len(nodes) + for a in range(l): + for b in range(a, l): + connect_nodes(nodes[a], b) + connect_nodes(nodes[b], a) + +# Transaction/Block functions +############################# + +def find_output(node, txid, amount): + """ + Return index to output of txid with value amount + Raises exception if there is none. + """ + txdata = node.getrawtransaction(txid, 1) + for i in range(len(txdata["vout"])): + if txdata["vout"][i]["value"] == amount: + return i + raise RuntimeError("find_output txid %s : %s not found" % (txid, str(amount))) + +def gather_inputs(from_node, amount_needed, confirmations_required=1): + """ + Return a random set of unspent txouts that are enough to pay amount_needed + """ + assert(confirmations_required >= 0) + utxo = from_node.listunspent(confirmations_required) + random.shuffle(utxo) + inputs = [] + total_in = Decimal("0.00000000") + while total_in < amount_needed and len(utxo) > 0: + t = utxo.pop() + total_in += t["amount"] + inputs.append({"txid": t["txid"], "vout": t["vout"], "address": t["address"]}) + if total_in < amount_needed: + raise RuntimeError("Insufficient funds: need %d, have %d" % (amount_needed, total_in)) + return (total_in, inputs) + +def make_change(from_node, amount_in, amount_out, fee): + """ + Create change output(s), return them + """ + outputs = {} + amount = amount_out + fee + change = amount_in - amount + if change > amount * 2: + # Create an extra change output to break up big inputs + change_address = from_node.getnewaddress() + # Split change in two, being careful of rounding: + outputs[change_address] = Decimal(change / 2).quantize(Decimal('0.00000001'), rounding=ROUND_DOWN) + change = amount_in - amount - outputs[change_address] + if change > 0: + outputs[from_node.getnewaddress()] = change + return outputs + +def random_transaction(nodes, amount, min_fee, fee_increment, fee_variants): + """ + Create a random transaction. + Returns (txid, hex-encoded-transaction-data, fee) + """ + from_node = random.choice(nodes) + to_node = random.choice(nodes) + fee = min_fee + fee_increment * random.randint(0, fee_variants) + + (total_in, inputs) = gather_inputs(from_node, amount + fee) + outputs = make_change(from_node, total_in, amount, fee) + outputs[to_node.getnewaddress()] = float(amount) + + rawtx = from_node.createrawtransaction(inputs, outputs) + signresult = from_node.signrawtransaction(rawtx) + txid = from_node.sendrawtransaction(signresult["hex"], True) + + return (txid, signresult["hex"], fee) + +# Helper to create at least "count" utxos +# Pass in a fee that is sufficient for relay and mining new transactions. +def create_confirmed_utxos(fee, node, count): + to_generate = int(0.5 * count) + 101 + while to_generate > 0: + node.generate(min(25, to_generate)) + to_generate -= 25 + utxos = node.listunspent() + iterations = count - len(utxos) + addr1 = node.getnewaddress() + addr2 = node.getnewaddress() + if iterations <= 0: + return utxos + for i in range(iterations): + t = utxos.pop() + inputs = [] + inputs.append({"txid": t["txid"], "vout": t["vout"]}) + outputs = {} + send_value = t['amount'] - fee + outputs[addr1] = float(satoshi_round(send_value / 2)) + outputs[addr2] = float(satoshi_round(send_value / 2)) + raw_tx = node.createrawtransaction(inputs, outputs) + signed_tx = node.signrawtransaction(raw_tx)["hex"] + node.sendrawtransaction(signed_tx) + + while (node.getmempoolinfo()['size'] > 0): + node.generate(1) + + utxos = node.listunspent() + assert(len(utxos) >= count) + return utxos + +# Create large OP_RETURN txouts that can be appended to a transaction +# to make it large (helper for constructing large transactions). +def gen_return_txouts(): + # Some pre-processing to create a bunch of OP_RETURN txouts to insert into transactions we create + # So we have big transactions (and therefore can't fit very many into each block) + # create one script_pubkey + script_pubkey = "6a4d0200" # OP_RETURN OP_PUSH2 512 bytes + for i in range(512): + script_pubkey = script_pubkey + "01" + # concatenate 128 txouts of above script_pubkey which we'll insert before the txout for change + txouts = "81" + for k in range(128): + # add txout value + txouts = txouts + "0000000000000000" + # add length of script_pubkey + txouts = txouts + "fd0402" + # add script_pubkey + txouts = txouts + script_pubkey + return txouts + +def create_tx(node, coinbase, to_address, amount): + inputs = [{"txid": coinbase, "vout": 0}] + outputs = {to_address: amount} + rawtx = node.createrawtransaction(inputs, outputs) + signresult = node.signrawtransaction(rawtx) + assert_equal(signresult["complete"], True) + return signresult["hex"] + +# Create a spend of each passed-in utxo, splicing in "txouts" to each raw +# transaction to make it large. See gen_return_txouts() above. +def create_lots_of_big_transactions(node, txouts, utxos, num, fee): + addr = node.getnewaddress() + txids = [] + for _ in range(num): + t = utxos.pop() + inputs = [{"txid": t["txid"], "vout": t["vout"]}] + outputs = {} + change = t['amount'] - fee + outputs[addr] = float(satoshi_round(change)) + rawtx = node.createrawtransaction(inputs, outputs) + newtx = rawtx[0:92] + newtx = newtx + txouts + newtx = newtx + rawtx[94:] + signresult = node.signrawtransaction(newtx, None, None, "NONE") + txid = node.sendrawtransaction(signresult["hex"], True) + txids.append(txid) + return txids + +def mine_large_block(node, utxos=None): + # generate a 66k transaction, + # and 14 of them is close to the 1MB block limit + num = 14 + txouts = gen_return_txouts() + utxos = utxos if utxos is not None else [] + if len(utxos) < num: + utxos.clear() + utxos.extend(node.listunspent()) + fee = 100 * node.getnetworkinfo()["relayfee"] + create_lots_of_big_transactions(node, txouts, utxos, num, fee=fee) + node.generate(1) + +def find_vout_for_address(node, txid, addr): + """ + Locate the vout index of the given transaction sending to the + given address. Raises runtime error exception if not found. + """ + tx = node.getrawtransaction(txid, True) + for i in range(len(tx["vout"])): + if any([addr == a for a in tx["vout"][i]["scriptPubKey"]["addresses"]]): + return i + raise RuntimeError("Vout not found for address: txid=%s, addr=%s" % (txid, addr)) + +# PIVX specific utils +DEFAULT_FEE = 0.01 +SPORK_ACTIVATION_TIME = 1563253447 +SPORK_DEACTIVATION_TIME = 4070908800 + +def DecimalAmt(x): + """Return Decimal from float for equality checks against rpc outputs""" + return Decimal("{:0.8f}".format(x)) + +# Find a coinstake/coinbase address on the node, filtering by the number of UTXOs it has. +# If no filter is provided, returns the coinstake/coinbase address on the node containing +# the greatest number of spendable UTXOs. +# The default cached chain has one address per coinbase output. +def get_coinstake_address(node, expected_utxos=None): + addrs = [utxo['address'] for utxo in node.listunspent() if utxo['generated']] + assert(len(set(addrs)) > 0) + + if expected_utxos is None: + addrs = [(addrs.count(a), a) for a in set(addrs)] + return sorted(addrs, reverse=True)[0][1] + + addrs = [a for a in set(addrs) if addrs.count(a) == expected_utxos] + assert(len(addrs) > 0) + return addrs[0] + +# Deterministic masternodes +def is_coin_locked_by(node, outpoint): + return outpoint.to_json() in node.listlockunspent() + +def get_collateral_vout(json_tx): + funding_txidn = -1 + for o in json_tx["vout"]: + if o["value"] == Decimal('100'): + funding_txidn = o["n"] + break + assert_greater_than(funding_txidn, -1) + return funding_txidn + +# owner and voting keys are created from controller node. +# operator keys are created, if operator_keys is None. +def create_new_dmn(idx, controller, payout_addr, operator_keys): + port = p2p_port(idx) if idx <= MAX_NODES else p2p_port(MAX_NODES) + (idx - MAX_NODES) + ipport = "127.0.0.1:" + str(port) + owner_addr = controller.getnewaddress("mnowner-%d" % idx) + voting_addr = controller.getnewaddress("mnvoting-%d" % idx) + if operator_keys is None: + bls_keypair = controller.generateblskeypair() + operator_pk = bls_keypair["public"] + operator_sk = bls_keypair["secret"] + else: + operator_pk = operator_keys[0] + operator_sk = operator_keys[1] + return messages.Masternode(idx, owner_addr, operator_pk, voting_addr, ipport, payout_addr, operator_sk) + +def spend_mn_collateral(spender, dmn): + inputs = [dmn.collateral.to_json()] + outputs = {spender.getnewaddress(): Decimal('99.99')} + sig_res = spender.signrawtransaction(spender.createrawtransaction(inputs, outputs)) + assert_equal(sig_res['complete'], True) + return spender.sendrawtransaction(sig_res['hex']) diff --git a/basicswap/interface/pivx.py b/basicswap/interface/pivx.py new file mode 100644 index 0000000..870eebe --- /dev/null +++ b/basicswap/interface/pivx.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright (c) 2020 tecnovert +# Distributed under the MIT software license, see the accompanying +# file LICENSE or http://www.opensource.org/licenses/mit-license.php. + +from .btc import BTCInterface +from basicswap.chainparams import Coins +from .contrib.pivx_test_framework.messages import ( + CBlock, + ToHex, + FromHex) + + +class PIVXInterface(BTCInterface): + @staticmethod + def coin_type(): + return Coins.PIVX + + def createRawSignedTransaction(self, addr_to, amount): + txn = self.rpc_callback('createrawtransaction', [[], {addr_to: self.format_amount(amount)}]) + + fee_rate, fee_src = self.get_fee_rate(self._conf_target) + self._log.debug(f'Fee rate: {fee_rate}, source: {fee_src}, block target: {self._conf_target}') + + options = { + 'lockUnspents': True, + 'feeRate': fee_rate, + } + txn_funded = self.rpc_callback('fundrawtransaction', [txn, options])['hex'] + txn_signed = self.rpc_callback('signrawtransaction', [txn_funded])['hex'] + return txn_signed + + def getBlockWithTxns(self, block_hash): + # TODO: Bypass decoderawtransaction and getblockheader + block = self.rpc_callback('getblock', [block_hash, False]) + block_header = self.rpc_callback('getblockheader', [block_hash]) + decoded_block = CBlock() + decoded_block = FromHex(decoded_block, block) + + tx_rv = [] + for tx in decoded_block.vtx: + tx_dec = self.rpc_callback('decoderawtransaction', [ToHex(tx)]) + tx_rv.append(tx_dec) + + block_rv = { + 'hash': block_hash, + 'tx': tx_rv, + 'confirmations': block_header['confirmations'], + 'height': block_header['height'], + 'version': block_header['version'], + 'merkleroot': block_header['merkleroot'], + } + + return block_rv diff --git a/bin/basicswap_prepare.py b/bin/basicswap_prepare.py index 8b0a81d..9cbb53e 100755 --- a/bin/basicswap_prepare.py +++ b/bin/basicswap_prepare.py @@ -46,6 +46,9 @@ MONERO_VERSION = os.getenv('MONERO_VERSION', '0.18.0.0') MONERO_VERSION_TAG = os.getenv('MONERO_VERSION_TAG', '') XMR_SITE_COMMIT = 'f093c0da2219d94e6bef5f3948ac61b4ecdcb95b' # Lock hashes.txt to monero version +PIVX_VERSION = os.getenv('PIVX_VERSION', '5.4.0') +PIVX_VERSION_TAG = os.getenv('PIVX_VERSION_TAG', '') + # version, version tag eg. "rc1", signers known_coins = { 'particl': (PARTICL_VERSION, PARTICL_VERSION_TAG, ('tecnovert',)), @@ -53,6 +56,7 @@ known_coins = { 'bitcoin': (BITCOIN_VERSION, BITCOIN_VERSION_TAG, ('laanwj',)), 'namecoin': ('0.18.0', '', ('JeremyRand',)), 'monero': (MONERO_VERSION, MONERO_VERSION_TAG, ('binaryfate',)), + 'pivx': (PIVX_VERSION, PIVX_VERSION_TAG, ('fuzzbawls',)), } expected_key_ids = { @@ -62,6 +66,7 @@ expected_key_ids = { 'JeremyRand': ('2DBE339E29F6294C',), 'binaryfate': ('F0AF4D462A0BDF92',), 'davidburkett38': ('3620E9D387E55666',), + 'fuzzbawls': ('3BDCDA2D87A881D9',), } if platform.system() == 'Darwin': @@ -114,6 +119,12 @@ BTC_RPC_PWD = os.getenv('BTC_RPC_PWD', '') NMC_RPC_HOST = os.getenv('NMC_RPC_HOST', '127.0.0.1') NMC_RPC_PORT = int(os.getenv('NMC_RPC_PORT', 19698)) +PIVX_RPC_HOST = os.getenv('PIVX_RPC_HOST', '127.0.0.1') +PIVX_RPC_PORT = int(os.getenv('PIVX_RPC_PORT', 51473)) +PIVX_ONION_PORT = int(os.getenv('PIVX_ONION_PORT', 51472)) # nDefaultPort +PIVX_RPC_USER = os.getenv('PIVX_RPC_USER', '') +PIVX_RPC_PWD = os.getenv('PIVX_RPC_PWD', '') + TOR_PROXY_HOST = os.getenv('TOR_PROXY_HOST', '127.0.0.1') TOR_PROXY_PORT = int(os.getenv('TOR_PROXY_PORT', 9050)) TOR_CONTROL_PORT = int(os.getenv('TOR_CONTROL_PORT', 9051)) @@ -205,6 +216,36 @@ def testOnionLink(): logger.info('Onion links work.') +def downloadPIVXParams(output_dir): + # util/fetch-params.sh + + if os.path.exists(output_dir): + logger.info(f'Skipping PIVX params download, path exists: {output_dir}') + return + os.makedirs(output_dir) + + source_url = 'https://download.z.cash/downloads/' + files = { + 'sapling-spend.params': '8e48ffd23abb3a5fd9c5589204f32d9c31285a04b78096ba40a79b75677efc13', + 'sapling-output.params': '2f0ebbcbb9bb0bcffe95a397e7eba89c29eb4dde6191c339db88570e3f3fb0e4', + } + + try: + setConnectionParameters() + for k, v in files.items(): + url = urllib.parse.urljoin(source_url, k) + path = os.path.join(output_dir, k) + downloadFile(url, path) + hasher = hashlib.sha256() + with open(path, 'rb') as fp: + hasher.update(fp.read()) + file_hash = hasher.hexdigest() + logger.info('%s hash: %s', k, file_hash) + assert file_hash == v + finally: + popConnectionParameters() + + def isValidSignature(result): if result.valid is False \ and (result.status == 'signature valid' and result.key_status == 'signing key has expired'): @@ -342,6 +383,10 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}): release_url = 'https://beta.namecoin.org/files/namecoin-core/namecoin-core-{}/{}'.format(version, release_filename) assert_filename = '{}-{}-{}-build.assert'.format(coin, os_name, version.rsplit('.', 1)[0]) assert_url = 'https://raw.githubusercontent.com/namecoin/gitian.sigs/master/%s-%s/%s/%s' % (version, os_dir_name, signing_key_name, assert_filename) + elif coin == 'pivx': + release_url = 'https://github.com/PIVX-Project/PIVX/releases/download/v{}/{}'.format(version + version_tag, release_filename) + assert_filename = '{}-{}-{}-build.assert'.format(coin, os_name, '.'.join(version.split('.')[:2])) + assert_url = 'https://raw.githubusercontent.com/PIVX-Project/gitian.sigs/master/%s-%s/%s/%s' % (version + version_tag, os_dir_name, signing_key_name.capitalize(), assert_filename) else: raise ValueError('Unknown coin') @@ -568,6 +613,14 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}): fp.write('rpcauth={}:{}${}\n'.format(BTC_RPC_USER, salt, password_to_hmac(salt, BTC_RPC_PWD))) elif coin == 'namecoin': fp.write('prune=2000\n') + elif coin == 'pivx': + base_dir = extra_opts.get('data_dir', data_dir) + params_dir = os.path.join(base_dir, 'pivx-params') + downloadPIVXParams(params_dir) + fp.write('prune=4000\n') + fp.write(f'paramsdir={params_dir}\n') + if PIVX_RPC_USER != '': + fp.write('rpcauth={}:{}${}\n'.format(PIVX_RPC_USER, salt, password_to_hmac(salt, PIVX_RPC_PWD))) else: logger.warning('Unknown coin %s', coin) @@ -804,7 +857,7 @@ def initialise_wallets(particl_wallet_mnemonic, with_coins, data_dir, settings, swap_client.setCoinRunParams(c) swap_client.createCoinInterface(c) - if c in (Coins.PART, Coins.BTC, Coins.LTC): + if c in (Coins.PART, Coins.BTC, Coins.LTC, Coins.PIVX): swap_client.waitForDaemonRPC(c, with_wallet=False) # Create wallet if it doesn't exist yet wallets = swap_client.callcoinrpc(c, 'listwallets') @@ -1068,6 +1121,21 @@ def main(): 'bindir': os.path.join(bin_dir, 'monero'), 'restore_height': xmr_restore_height, 'blocks_confirmed': 7, # TODO: 10? + }, + 'pivx': { + 'connection_type': 'rpc' if 'pivx' in with_coins else 'none', + 'manage_daemon': True if ('pivx' in with_coins and PIVX_RPC_HOST == '127.0.0.1') else False, + 'rpchost': PIVX_RPC_HOST, + 'rpcport': PIVX_RPC_PORT + port_offset, + 'onionport': PIVX_ONION_PORT + port_offset, + 'datadir': os.getenv('PIVX_DATA_DIR', os.path.join(data_dir, 'pivx')), + 'bindir': os.path.join(bin_dir, 'pivx'), + 'use_segwit': False, + 'use_csv': False, + 'blocks_confirmed': 1, + 'conf_target': 2, + 'core_version_group': 20, + 'chain_lookups': 'local', } } @@ -1080,6 +1148,9 @@ def main(): if BTC_RPC_USER != '': chainclients['bitcoin']['rpcuser'] = BTC_RPC_USER chainclients['bitcoin']['rpcpassword'] = BTC_RPC_PWD + if PIVX_RPC_USER != '': + chainclients['pivx']['rpcuser'] = PIVX_RPC_USER + chainclients['pivx']['rpcpassword'] = PIVX_RPC_PWD chainclients['monero']['walletsdir'] = os.getenv('XMR_WALLETS_DIR', chainclients['monero']['datadir']) diff --git a/pgp/keys/pivx_fuzzbawls.pgp b/pgp/keys/pivx_fuzzbawls.pgp new file mode 100644 index 0000000..9430078 --- /dev/null +++ b/pgp/keys/pivx_fuzzbawls.pgp @@ -0,0 +1,589 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Comment: Hostname: +Version: Hockeypuck 2.1.0-166-geb2a11b + +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 +LKiMUage5SFJ37FtUGb+8GM8p8TGARPzZ4OMxkagcWtvNJ0OXFUXiN68LqH4Dwwa +6djo5gka0MFo4WdYX/blw2R0xM7to22OsOP2NcSPKrm92SYyotRjEmjQZr4Swy4Z +wSKyouIc0f8AADg5/wAAODQBEAABAQAAAAAAAAAAAAAAAP/Y/9sAhAAIBgYHBgUI +BwcHCQkICgwUDQwLCwwZEhMPFB0aHx4dGhwcICQuJyAiLCMcHCg3KSwwMTQ0NB8n +OT04MjwuMzQyAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIy +MjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAEoASgDASIAAhEBAxEB/8QB +ogAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoLEAACAQMDAgQDBQUEBAAAAX0B +AgMABBEFEiExQQYTUWEHInEUMoGRoQgjQrHBFVLR8CQzYnKCCQoWFxgZGiUmJygp +KjQ1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoOEhYaHiImK +kpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4eLj +5OXm5+jp6vHy8/T19vf4+foBAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKCxEA +AgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAV +YnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hp +anN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPE +xcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDy +q4XdEkiHOz19KrsFki2P91uuO1LpH76Nw4JCkAc1PcWhiBeBTs7qe1a1KbfvIuMu +jM5bEiT53GzqCDyasZRFEa8D0xT0Vpj8vC55Y9astCiwFVUepJqVCU/iBtLREEcs +cQwoJY9Se9L9oAlzyMjkVUIJkwCwHXg0/wAtW5IJ+prRN9CS39pXGSP1pBck8qBg +cnHOe1QpEh52D8a2/D2lx32oqzonkQfvZAe+Og/H/GtYxc5WEdDptr/Y2ghmZftE +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 +tXo= +=lheU +-----END PGP PUBLIC KEY BLOCK----- diff --git a/tests/basicswap/common.py b/tests/basicswap/common.py index 5dea5dc..450934a 100644 --- a/tests/basicswap/common.py +++ b/tests/basicswap/common.py @@ -14,6 +14,7 @@ from urllib.request import urlopen from basicswap.rpc import callrpc from basicswap.contrib.rpcauth import generate_salt, password_to_hmac +from bin.basicswap_prepare import downloadPIVXParams TEST_HTTP_HOST = os.getenv('TEST_HTTP_HOST', '127.0.0.1') # Set to 0.0.0.0 when used in docker @@ -33,6 +34,10 @@ LTC_BASE_PORT = 34792 LTC_BASE_RPC_PORT = 35792 LTC_BASE_ZMQ_PORT = 36792 +PIVX_BASE_PORT = 34892 +PIVX_BASE_RPC_PORT = 35892 +PIVX_BASE_ZMQ_PORT = 36892 + PREFIX_SECRET_KEY_REGTEST = 0x2e @@ -73,6 +78,11 @@ def prepareDataDir(datadir, node_id, conf_file, dir_prefix, base_p2p_port=BASE_P fp.write('stakethreadconddelayms=1000\n') fp.write('smsgsregtestadjust=0\n') + if conf_file == 'pivx.conf': + params_dir = os.path.join(datadir, 'pivx-params') + downloadPIVXParams(params_dir) + fp.write(f'paramsdir={params_dir}\n') + for i in range(0, num_nodes): if node_id == i: continue diff --git a/tests/basicswap/common_xmr.py b/tests/basicswap/common_xmr.py index 271b8be..b979dcc 100644 --- a/tests/basicswap/common_xmr.py +++ b/tests/basicswap/common_xmr.py @@ -27,6 +27,7 @@ from tests.basicswap.common import ( BASE_PORT, BASE_RPC_PORT, BTC_BASE_PORT, BTC_BASE_RPC_PORT, LTC_BASE_PORT, + PIVX_BASE_PORT, ) from basicswap.contrib.rpcauth import generate_salt, password_to_hmac @@ -45,9 +46,6 @@ XMR_BASE_P2P_PORT = 17792 XMR_BASE_RPC_PORT = 29798 XMR_BASE_WALLET_RPC_PORT = 29998 -LTC_BASE_RPC_PORT = 35792 -LTC_BASE_ZMQ_PORT = 36792 - EXTRA_CONFIG_JSON = json.loads(os.getenv('EXTRA_CONFIG_JSON', '{}')) @@ -192,6 +190,33 @@ def run_prepare(node_id, datadir_path, bins_path, with_coins, mnemonic_in=None, for opt in EXTRA_CONFIG_JSON.get('ltc{}'.format(node_id), []): fp.write(opt + '\n') + if 'pivx' in coins_array: + # Pruned nodes don't provide blocks + with open(os.path.join(datadir_path, 'pivx', 'pivx.conf'), 'r') as fp: + lines = fp.readlines() + with open(os.path.join(datadir_path, 'pivx', 'pivx.conf'), 'w') as fp: + for line in lines: + if not line.startswith('prune'): + fp.write(line) + fp.write('port={}\n'.format(PIVX_BASE_PORT + node_id + port_ofs)) + fp.write('bind=127.0.0.1\n') + fp.write('dnsseed=0\n') + fp.write('discover=0\n') + fp.write('listenonion=0\n') + fp.write('upnp=0\n') + if use_rpcauth: + salt = generate_salt(16) + rpc_user = 'test_pivx_' + str(node_id) + rpc_pass = 'test_pivx_pwd_' + str(node_id) + fp.write('rpcauth={}:{}${}\n'.format(rpc_user, salt, password_to_hmac(salt, rpc_pass))) + settings['chainclients']['pivx']['rpcuser'] = rpc_user + settings['chainclients']['pivx']['rpcpassword'] = rpc_pass + for ip in range(num_nodes): + if ip != node_id: + fp.write('connect=127.0.0.1:{}\n'.format(PIVX_BASE_PORT + ip + port_ofs)) + for opt in EXTRA_CONFIG_JSON.get('pivx{}'.format(node_id), []): + fp.write(opt + '\n') + if 'monero' in coins_array: with open(os.path.join(datadir_path, 'monero', 'monerod.conf'), 'a') as fp: fp.write('p2p-bind-ip=127.0.0.1\n') diff --git a/tests/basicswap/extended/test_pivx.py b/tests/basicswap/extended/test_pivx.py new file mode 100644 index 0000000..e2d485f --- /dev/null +++ b/tests/basicswap/extended/test_pivx.py @@ -0,0 +1,552 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# Copyright (c) 2022 tecnovert +# Distributed under the MIT software license, see the accompanying +# file LICENSE or http://www.opensource.org/licenses/mit-license.php. + +""" +basicswap]$ python tests/basicswap/extended/test_pivx.py + +""" + +import os +import sys +import json +import time +import shutil +import signal +import logging +import unittest +import threading + +import basicswap.config as cfg +from basicswap.basicswap import ( + BasicSwap, + Coins, + SwapTypes, + BidStates, + TxStates, +) +from basicswap.util import ( + COIN, +) +from basicswap.basicswap_util import ( + TxLockTypes, +) +from basicswap.util.address import ( + toWIF, +) +from basicswap.rpc import ( + callrpc_cli, + waitForRPC, +) +from basicswap.contrib.key import ( + ECKey, +) +from basicswap.http_server import ( + HttpThread, +) +from tests.basicswap.common import ( + checkForks, + stopDaemons, + wait_for_offer, + wait_for_bid, + wait_for_bid_tx_state, + wait_for_in_progress, + read_json_api, + TEST_HTTP_HOST, + TEST_HTTP_PORT, + BASE_PORT, + BASE_RPC_PORT, + BASE_ZMQ_PORT, + PREFIX_SECRET_KEY_REGTEST, +) +from bin.basicswap_run import startDaemon +from bin.basicswap_prepare import downloadPIVXParams + + +logger = logging.getLogger() +logger.level = logging.DEBUG +if not len(logger.handlers): + logger.addHandler(logging.StreamHandler(sys.stdout)) + +NUM_NODES = 3 +PIVX_NODE = 3 +BTC_NODE = 4 + +delay_event = threading.Event() +stop_test = False + + +def prepareOtherDir(datadir, nodeId, conf_file='pivx.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('fallbackfee=0.01\n') + fp.write('acceptnonstdtxn=0\n') + + if conf_file == 'pivx.conf': + params_dir = os.path.join(datadir, 'pivx-params') + downloadPIVXParams(params_dir) + fp.write(f'paramsdir={params_dir}\n') + + if conf_file == 'bitcoin.conf': + fp.write('wallet=wallet.dat\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('wallet=wallet.dat\n') + fp.write('fallbackfee=0.01\n') + + fp.write('acceptnonstdtxn=0\n') + fp.write('minstakeinterval=5\n') + fp.write('smsgsregtestadjust=0\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) + + pivxdatadir = os.path.join(datadir, str(PIVX_NODE)) + btcdatadir = os.path.join(datadir, str(BTC_NODE)) + settings_path = os.path.join(basicswap_dir, cfg.CONFIG_FILENAME) + settings = { + 'debug': True, + 'zmqhost': 'tcp://127.0.0.1', + 'zmqport': BASE_ZMQ_PORT + nodeId, + 'htmlhost': '127.0.0.1', + '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 + }, + 'pivx': { + 'connection_type': 'rpc', + 'manage_daemon': False, + 'rpcport': BASE_RPC_PORT + PIVX_NODE, + 'datadir': pivxdatadir, + 'bindir': cfg.PIVX_BINDIR, + 'use_csv': False, + 'use_segwit': False, + }, + '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 partRpc(cmd, node_id=0): + return callrpc_cli(cfg.PARTICL_BINDIR, os.path.join(cfg.TEST_DATADIRS, str(node_id)), 'regtest', cmd, cfg.PARTICL_CLI) + + +def btcRpc(cmd): + return callrpc_cli(cfg.BITCOIN_BINDIR, os.path.join(cfg.TEST_DATADIRS, str(BTC_NODE)), 'regtest', cmd, cfg.BITCOIN_CLI) + + +def pivxRpc(cmd): + return callrpc_cli(cfg.PIVX_BINDIR, os.path.join(cfg.TEST_DATADIRS, str(PIVX_NODE)), 'regtest', cmd, cfg.PIVX_CLI) + + +def signal_handler(sig, frame): + global stop_test + print('signal {} detected.'.format(sig)) + stop_test = True + delay_event.set() + + +def run_coins_loop(cls): + while not stop_test: + try: + pivxRpc('generatetoaddress 1 {}'.format(cls.pivx_addr)) + btcRpc('generatetoaddress 1 {}'.format(cls.btc_addr)) + except Exception as e: + logging.warning('run_coins_loop ' + str(e)) + time.sleep(1.0) + + +def run_loop(self): + while not stop_test: + for c in self.swap_clients: + c.update() + time.sleep(1) + + +def make_part_cli_rpc_func(node_id): + node_id = node_id + + def rpc_func(method, params=None, wallet=None): + nonlocal node_id + cmd = method + if params: + for p in params: + cmd += ' "' + p + '"' + return partRpc(cmd, node_id) + return rpc_func + + +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.TEST_DATADIRS): + logging.info('Removing ' + cfg.TEST_DATADIRS) + for name in os.listdir(cfg.TEST_DATADIRS): + if name == 'pivx-params': + continue + fullpath = os.path.join(cfg.TEST_DATADIRS, name) + if os.path.isdir(fullpath): + shutil.rmtree(fullpath) + else: + os.remove(fullpath) + + for i in range(NUM_NODES): + prepareDir(cfg.TEST_DATADIRS, i, cls.network_key, cls.network_pubkey) + + prepareOtherDir(cfg.TEST_DATADIRS, PIVX_NODE) + prepareOtherDir(cfg.TEST_DATADIRS, BTC_NODE, 'bitcoin.conf') + + cls.daemons = [] + cls.swap_clients = [] + cls.http_threads = [] + + btc_data_dir = os.path.join(cfg.TEST_DATADIRS, str(BTC_NODE)) + if os.path.exists(os.path.join(cfg.BITCOIN_BINDIR, 'bitcoin-wallet')): + callrpc_cli(cfg.BITCOIN_BINDIR, btc_data_dir, 'regtest', '-wallet=wallet.dat create', 'bitcoin-wallet') + cls.daemons.append(startDaemon(btc_data_dir, cfg.BITCOIN_BINDIR, cfg.BITCOIND)) + logging.info('Started %s %d', cfg.BITCOIND, cls.daemons[-1].pid) + cls.daemons.append(startDaemon(os.path.join(cfg.TEST_DATADIRS, str(PIVX_NODE)), cfg.PIVX_BINDIR, cfg.PIVXD)) + logging.info('Started %s %d', cfg.PIVXD, cls.daemons[-1].pid) + + for i in range(NUM_NODES): + data_dir = os.path.join(cfg.TEST_DATADIRS, str(i)) + if os.path.exists(os.path.join(cfg.PARTICL_BINDIR, 'particl-wallet')): + callrpc_cli(cfg.PARTICL_BINDIR, data_dir, 'regtest', '-wallet=wallet.dat create', 'particl-wallet') + cls.daemons.append(startDaemon(data_dir, cfg.PARTICL_BINDIR, cfg.PARTICLD)) + logging.info('Started %s %d', cfg.PARTICLD, cls.daemons[-1].pid) + + for i in range(NUM_NODES): + rpc = make_part_cli_rpc_func(i) + waitForRPC(rpc) + if i == 0: + rpc('extkeyimportmaster', ['abandon baby cabbage dad eager fabric gadget habit ice kangaroo lab absorb']) + elif i == 1: + rpc('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']) + rpc('getnewextaddress', ['lblExtTest']) + rpc('rescanblockchain') + else: + rpc('extkeyimportmaster', [rpc('mnemonic', ['new'])['master']]) + + basicswap_dir = os.path.join(os.path.join(cfg.TEST_DATADIRS, str(i)), 'basicswap') + settings_path = os.path.join(basicswap_dir, cfg.CONFIG_FILENAME) + 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].setDaemonPID(Coins.BTC, cls.daemons[0].pid) + cls.swap_clients[-1].setDaemonPID(Coins.PIVX, cls.daemons[1].pid) + cls.swap_clients[-1].setDaemonPID(Coins.PART, cls.daemons[2 + i].pid) + cls.swap_clients[-1].start() + + t = HttpThread(cls.swap_clients[i].fp, TEST_HTTP_HOST, TEST_HTTP_PORT + i, False, cls.swap_clients[i]) + cls.http_threads.append(t) + t.start() + + waitForRPC(pivxRpc) + num_blocks = 1352 # CHECKLOCKTIMEVERIFY soft-fork activates at (regtest) block height 1351. + logging.info('Mining %d pivx blocks', num_blocks) + cls.pivx_addr = pivxRpc('getnewaddress mining_addr') + pivxRpc('generatetoaddress {} {}'.format(num_blocks, cls.pivx_addr)) + + ro = pivxRpc('getblockchaininfo') + try: + assert (ro['bip9_softforks']['csv']['status'] == 'active') + except Exception: + logging.info('pivx: csv is not active') + try: + assert (ro['bip9_softforks']['segwit']['status'] == 'active') + except Exception: + logging.info('pivx: 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') + checkForks(ro) + + ro = pivxRpc('getwalletinfo') + print('pivxRpc', ro) + + signal.signal(signal.SIGINT, signal_handler) + cls.update_thread = threading.Thread(target=run_loop, args=(cls,)) + cls.update_thread.start() + + cls.coins_update_thread = threading.Thread(target=run_coins_loop, args=(cls,)) + cls.coins_update_thread.start() + + # Wait for height, or sequencelock is thrown off by genesis blocktime + num_blocks = 3 + logging.info('Waiting for Particl chain height %d', num_blocks) + for i in range(60): + particl_blocks = cls.swap_clients[0].callrpc('getblockchaininfo')['blocks'] + print('particl_blocks', particl_blocks) + if particl_blocks >= num_blocks: + break + delay_event.wait(1) + assert (particl_blocks >= num_blocks) + + @classmethod + def tearDownClass(cls): + global stop_test + logging.info('Finalising') + stop_test = True + cls.update_thread.join() + cls.coins_update_thread.join() + for t in cls.http_threads: + t.stop() + t.join() + for c in cls.swap_clients: + c.finalise() + c.fp.close() + + stopDaemons(cls.daemons) + + super(Test, cls).tearDownClass() + + def test_02_part_pivx(self): + logging.info('---------- Test PART to PIVX') + swap_clients = self.swap_clients + + offer_id = swap_clients[0].postOffer(Coins.PART, Coins.PIVX, 100 * COIN, 0.1 * COIN, 100 * COIN, SwapTypes.SELLER_FIRST, TxLockTypes.ABS_LOCK_TIME) + + wait_for_offer(delay_event, 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) + + wait_for_bid(delay_event, swap_clients[0], bid_id) + + swap_clients[0].acceptBid(bid_id) + + wait_for_in_progress(delay_event, swap_clients[1], bid_id, sent=True) + + wait_for_bid(delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=60) + wait_for_bid(delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=60) + + js_0 = read_json_api(1800) + js_1 = read_json_api(1801) + 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_pivx_part(self): + logging.info('---------- Test PIVX to PART') + swap_clients = self.swap_clients + + offer_id = swap_clients[1].postOffer(Coins.PIVX, Coins.PART, 10 * COIN, 9.0 * COIN, 10 * COIN, SwapTypes.SELLER_FIRST, TxLockTypes.ABS_LOCK_TIME) + + wait_for_offer(delay_event, 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) + + wait_for_bid(delay_event, swap_clients[1], bid_id) + swap_clients[1].acceptBid(bid_id) + + wait_for_in_progress(delay_event, swap_clients[0], bid_id, sent=True) + + wait_for_bid(delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=60) + wait_for_bid(delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, wait_for=60) + + js_0 = read_json_api(1800) + js_1 = read_json_api(1801) + 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_pivx_btc(self): + logging.info('---------- Test PIVX to BTC') + swap_clients = self.swap_clients + + offer_id = swap_clients[0].postOffer(Coins.PIVX, Coins.BTC, 10 * COIN, 0.1 * COIN, 10 * COIN, SwapTypes.SELLER_FIRST, TxLockTypes.ABS_LOCK_TIME) + + wait_for_offer(delay_event, 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) + + wait_for_bid(delay_event, swap_clients[0], bid_id) + swap_clients[0].acceptBid(bid_id) + + wait_for_in_progress(delay_event, swap_clients[1], bid_id, sent=True) + + wait_for_bid(delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=60) + wait_for_bid(delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=60) + + js_0bid = read_json_api(1800, 'bids/{}'.format(bid_id.hex())) + + js_0 = read_json_api(1800) + js_1 = read_json_api(1801) + + 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 + logging.info('---------- Test refund, PIVX to BTC') + swap_clients = self.swap_clients + + offer_id = swap_clients[0].postOffer(Coins.PIVX, Coins.BTC, 10 * COIN, 0.1 * COIN, 10 * COIN, SwapTypes.SELLER_FIRST, + TxLockTypes.ABS_LOCK_BLOCKS, 10) + + wait_for_offer(delay_event, 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) + + wait_for_bid(delay_event, swap_clients[0], bid_id) + swap_clients[1].abandonBid(bid_id) + swap_clients[0].acceptBid(bid_id) + + wait_for_bid(delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=60) + wait_for_bid(delay_event, swap_clients[1], bid_id, BidStates.BID_ABANDONED, sent=True, wait_for=60) + + js_0 = read_json_api(1800) + js_1 = read_json_api(1801) + 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): + logging.info('---------- Test same client, BTC to PIVX') + swap_clients = self.swap_clients + + js_0_before = read_json_api(1800) + + offer_id = swap_clients[0].postOffer(Coins.PIVX, Coins.BTC, 10 * COIN, 10 * COIN, 10 * COIN, SwapTypes.SELLER_FIRST, TxLockTypes.ABS_LOCK_TIME) + + wait_for_offer(delay_event, 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) + + wait_for_bid(delay_event, swap_clients[0], bid_id) + swap_clients[0].acceptBid(bid_id) + + wait_for_bid_tx_state(delay_event, swap_clients[0], bid_id, TxStates.TX_REDEEMED, TxStates.TX_REDEEMED, wait_for=60) + wait_for_bid(delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=60) + + js_0 = read_json_api(1800) + 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): + logging.info('---------- Test error, BTC to PIVX, set fee above bid value') + swap_clients = self.swap_clients + + js_0_before = read_json_api(1800) + + offer_id = swap_clients[0].postOffer(Coins.PIVX, Coins.BTC, 0.001 * COIN, 1.0 * COIN, 0.001 * COIN, SwapTypes.SELLER_FIRST, TxLockTypes.ABS_LOCK_TIME) + + wait_for_offer(delay_event, 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) + + wait_for_bid(delay_event, swap_clients[0], bid_id) + swap_clients[0].acceptBid(bid_id) + swap_clients[0].getChainClientSettings(Coins.BTC)['override_feerate'] = 10.0 + swap_clients[0].getChainClientSettings(Coins.PIVX)['override_feerate'] = 10.0 + wait_for_bid(delay_event, swap_clients[0], bid_id, BidStates.BID_ERROR, wait_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() diff --git a/tests/basicswap/test_btc_xmr.py b/tests/basicswap/test_btc_xmr.py index 8178e11..ad51d55 100644 --- a/tests/basicswap/test_btc_xmr.py +++ b/tests/basicswap/test_btc_xmr.py @@ -43,6 +43,8 @@ class Test(BaseTest): cls.test_coin_from = Coins.BTC if not hasattr(cls, 'start_ltc_nodes'): cls.start_ltc_nodes = False + if not hasattr(cls, 'start_pivx_nodes'): + cls.start_pivx_nodes = False super(Test, cls).setUpClass() @classmethod diff --git a/tests/basicswap/test_other.py b/tests/basicswap/test_other.py index ff3c652..af10dc4 100644 --- a/tests/basicswap/test_other.py +++ b/tests/basicswap/test_other.py @@ -38,7 +38,7 @@ from basicswap.util import ( class Test(unittest.TestCase): - REQUIRED_SETTINGS = {'blocks_confirmed': 1, 'conf_target': 1, 'use_segwit': True} + REQUIRED_SETTINGS = {'blocks_confirmed': 1, 'conf_target': 1, 'use_segwit': True, 'connection_type': 'rpc'} def test_serialise_num(self): def test_case(v, nb=None): diff --git a/tests/basicswap/test_run.py b/tests/basicswap/test_run.py index 585659a..ef68d38 100644 --- a/tests/basicswap/test_run.py +++ b/tests/basicswap/test_run.py @@ -64,6 +64,7 @@ class Test(BaseTest): def setUpClass(cls): cls.start_ltc_nodes = True cls.start_xmr_nodes = False + cls.start_pivx_nodes = False super(Test, cls).setUpClass() btc_addr1 = callnoderpc(1, 'getnewaddress', ['initial funds', 'bech32'], base_rpc_port=BTC_BASE_RPC_PORT) diff --git a/tests/basicswap/test_xmr.py b/tests/basicswap/test_xmr.py index 0021f62..5d6da61 100644 --- a/tests/basicswap/test_xmr.py +++ b/tests/basicswap/test_xmr.py @@ -80,6 +80,8 @@ from tests.basicswap.common import ( BTC_BASE_RPC_PORT, LTC_BASE_PORT, LTC_BASE_RPC_PORT, + PIVX_BASE_PORT, + PIVX_BASE_RPC_PORT, PREFIX_SECRET_KEY_REGTEST, ) from bin.basicswap_run import startDaemon, startXmrDaemon @@ -91,6 +93,7 @@ NUM_NODES = 3 NUM_XMR_NODES = 3 NUM_BTC_NODES = 3 NUM_LTC_NODES = 3 +NUM_PIVX_NODES = 3 TEST_DIR = cfg.TEST_DATADIRS XMR_BASE_P2P_PORT = 17792 @@ -150,7 +153,7 @@ def startXmrWalletRPC(node_dir, bin_dir, wallet_bin, node_id, opts=[]): return subprocess.Popen(args, stdin=subprocess.PIPE, stdout=wallet_stdout, stderr=wallet_stderr, cwd=data_dir) -def prepare_swapclient_dir(datadir, node_id, network_key, network_pubkey, with_ltc=False, with_xmr=False): +def prepare_swapclient_dir(datadir, node_id, network_key, network_pubkey, with_coins=set()): basicswap_dir = os.path.join(datadir, 'basicswap_' + str(node_id)) if not os.path.exists(basicswap_dir): os.makedirs(basicswap_dir) @@ -201,7 +204,7 @@ def prepare_swapclient_dir(datadir, node_id, network_key, network_pubkey, with_l 'debug_ui': True, } - if with_xmr: + if Coins.XMR in with_coins: settings['chainclients']['monero'] = { 'connection_type': 'rpc', 'manage_daemon': False, @@ -214,7 +217,7 @@ def prepare_swapclient_dir(datadir, node_id, network_key, network_pubkey, with_l 'bindir': cfg.XMR_BINDIR, } - if with_ltc: + if Coins.LTC in with_coins: settings['chainclients']['litecoin'] = { 'connection_type': 'rpc', 'manage_daemon': False, @@ -226,6 +229,18 @@ def prepare_swapclient_dir(datadir, node_id, network_key, network_pubkey, with_l 'use_segwit': True, } + if Coins.PIVX in with_coins: + settings['chainclients']['pivx'] = { + 'connection_type': 'rpc', + 'manage_daemon': False, + 'rpcport': PIVX_BASE_RPC_PORT + node_id, + 'rpcuser': 'test' + str(node_id), + 'rpcpassword': 'test_pass' + str(node_id), + 'datadir': os.path.join(datadir, 'pivx_' + str(node_id)), + 'bindir': cfg.PIVX_BINDIR, + 'use_segwit': False, + } + with open(settings_path, 'w') as fp: json.dump(settings, fp, indent=4) @@ -238,6 +253,10 @@ def ltcCli(cmd, node_id=0): return callrpc_cli(cfg.LITECOIN_BINDIR, os.path.join(TEST_DIR, 'ltc_' + str(node_id)), 'regtest', cmd, cfg.LITECOIN_CLI) +def pivxCli(cmd, node_id=0): + return callrpc_cli(cfg.PIVX_BINDIR, os.path.join(TEST_DIR, 'pivx_' + str(node_id)), 'regtest', cmd, cfg.PIVX_CLI) + + def signal_handler(sig, frame): logging.info('signal {} detected.'.format(sig)) test_delay_event.set() @@ -283,6 +302,8 @@ def run_coins_loop(cls): btcCli('generatetoaddress 1 {}'.format(cls.btc_addr)) if cls.ltc_addr is not None: ltcCli('generatetoaddress 1 {}'.format(cls.ltc_addr)) + if cls.pivx_addr is not None: + pivxCli('generatetoaddress 1 {}'.format(cls.pivx_addr)) if cls.xmr_addr is not None: callrpc_xmr_na(XMR_BASE_RPC_PORT + 1, 'generateblocks', {'wallet_address': cls.xmr_addr, 'amount_of_blocks': 1}) except Exception as e: @@ -304,6 +325,8 @@ class BaseTest(unittest.TestCase): def setUpClass(cls): if not hasattr(cls, 'start_ltc_nodes'): cls.start_ltc_nodes = False + if not hasattr(cls, 'start_pivx_nodes'): + cls.start_pivx_nodes = False if not hasattr(cls, 'start_xmr_nodes'): cls.start_xmr_nodes = True @@ -316,12 +339,14 @@ class BaseTest(unittest.TestCase): cls.part_daemons = [] cls.btc_daemons = [] cls.ltc_daemons = [] + cls.pivx_daemons = [] cls.xmr_daemons = [] cls.xmr_wallet_auth = [] cls.xmr_addr = None cls.btc_addr = None cls.ltc_addr = None + cls.pivx_addr = None logger.propagate = False logger.handlers = [] @@ -333,7 +358,14 @@ class BaseTest(unittest.TestCase): if os.path.isdir(TEST_DIR): logging.info('Removing ' + TEST_DIR) - shutil.rmtree(TEST_DIR) + for name in os.listdir(TEST_DIR): + if name == 'pivx-params': + continue + fullpath = os.path.join(TEST_DIR, name) + if os.path.isdir(fullpath): + shutil.rmtree(fullpath) + else: + os.remove(fullpath) if not os.path.exists(TEST_DIR): os.makedirs(TEST_DIR) @@ -391,6 +423,17 @@ class BaseTest(unittest.TestCase): waitForRPC(make_rpc_func(i, base_rpc_port=LTC_BASE_RPC_PORT)) + if cls.start_pivx_nodes: + for i in range(NUM_PIVX_NODES): + data_dir = prepareDataDir(TEST_DIR, i, 'pivx.conf', 'pivx_', base_p2p_port=PIVX_BASE_PORT, base_rpc_port=PIVX_BASE_RPC_PORT) + if os.path.exists(os.path.join(cfg.PIVX_BINDIR, 'pivx-wallet')): + callrpc_cli(cfg.PIVX_BINDIR, data_dir, 'regtest', '-wallet=wallet.dat create', 'pivx-wallet') + + cls.pivx_daemons.append(startDaemon(os.path.join(TEST_DIR, 'pivx_' + str(i)), cfg.PIVX_BINDIR, cfg.PIVXD)) + logging.info('Started %s %d', cfg.PIVXD, cls.part_daemons[-1].pid) + + waitForRPC(make_rpc_func(i, base_rpc_port=PIVX_BASE_RPC_PORT)) + if cls.start_xmr_nodes: for i in range(NUM_XMR_NODES): prepareXmrDataDir(TEST_DIR, i, 'monerod.conf') @@ -417,7 +460,14 @@ class BaseTest(unittest.TestCase): cls.network_pubkey = eckey.get_pubkey().get_bytes().hex() for i in range(NUM_NODES): - prepare_swapclient_dir(TEST_DIR, i, cls.network_key, cls.network_pubkey, cls.start_ltc_nodes, cls.start_xmr_nodes) + start_nodes = set() + if cls.start_ltc_nodes: + start_nodes.add(Coins.LTC) + if cls.start_xmr_nodes: + start_nodes.add(Coins.XMR) + if cls.start_pivx_nodes: + start_nodes.add(Coins.PIVX) + prepare_swapclient_dir(TEST_DIR, i, cls.network_key, cls.network_pubkey, start_nodes) basicswap_dir = os.path.join(os.path.join(TEST_DIR, 'basicswap_' + str(i))) settings_path = os.path.join(basicswap_dir, cfg.CONFIG_FILENAME) with open(settings_path) as fs: @@ -481,6 +531,18 @@ class BaseTest(unittest.TestCase): checkForks(callnoderpc(0, 'getblockchaininfo', base_rpc_port=LTC_BASE_RPC_PORT)) + if cls.start_pivx_nodes: + num_blocks = 400 + cls.pivx_addr = callnoderpc(0, 'getnewaddress', ['mining_addr'], base_rpc_port=PIVX_BASE_RPC_PORT) + logging.info('Mining %d PIVX blocks to %s', num_blocks, cls.pivx_addr) + callnoderpc(0, 'generatetoaddress', [num_blocks, cls.pivx_addr], base_rpc_port=PIVX_BASE_RPC_PORT) + + # Switch addresses so wallet amounts stay constant + num_blocks = 100 + cls.pivx_addr = cls.swap_clients[0].ci(Coins.PIVX).pubkey_to_address(void_block_rewards_pubkey) + logging.info('Mining %d PIVX blocks to %s', num_blocks, cls.pivx_addr) + callnoderpc(0, 'generatetoaddress', [num_blocks, cls.pivx_addr], base_rpc_port=PIVX_BASE_RPC_PORT) + num_blocks = 100 if cls.start_xmr_nodes: cls.xmr_addr = cls.callxmrnodewallet(cls, 1, 'get_address')['address'] @@ -537,6 +599,7 @@ class BaseTest(unittest.TestCase): stopDaemons(cls.part_daemons) stopDaemons(cls.btc_daemons) stopDaemons(cls.ltc_daemons) + stopDaemons(cls.pivx_daemons) super(BaseTest, cls).tearDownClass()