diff --git a/.cirrus.yml b/.cirrus.yml index b6682c2..ce6d099 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -16,6 +16,7 @@ test_task: - BIN_DIR: /tmp/cached_bin - PARTICL_BINDIR: ${BIN_DIR}/particl - BITCOIN_BINDIR: ${BIN_DIR}/bitcoin + - BITCOINCASH_BINDIR: ${BIN_DIR}/bitcoincash - LITECOIN_BINDIR: ${BIN_DIR}/litecoin - XMR_BINDIR: ${BIN_DIR}/monero setup_script: @@ -29,7 +30,7 @@ test_task: fingerprint_script: - basicswap-prepare -v populate_script: - - basicswap-prepare --bindir=/tmp/cached_bin --preparebinonly --withcoins=particl,bitcoin,litecoin,monero + - basicswap-prepare --bindir=/tmp/cached_bin --preparebinonly --withcoins=particl,bitcoin,bitcoincash,litecoin,monero script: - cd "${CIRRUS_WORKING_DIR}" - export DATADIRS="${TEST_DIR}" diff --git a/.gitignore b/.gitignore index b91cf81..3a5b40b 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,6 @@ __pycache__ # geckodriver.log *.log docker/.env + +# vscode dev container settings +compose-dev.yaml \ No newline at end of file diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index f3401d8..8844e6e 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -251,6 +251,7 @@ class BasicSwap(BaseApp): protocolInterfaces = { SwapTypes.SELLER_FIRST: atomic_swap_1.AtomicSwapInterface(), SwapTypes.XMR_SWAP: xmr_swap_1.XmrSwapInterface(), + SwapTypes.XMR_BCH_SWAP: xmr_swap_1.XmrBchSwapInterface(), } def __init__(self, fp, data_dir, settings, chain, log_name='BasicSwap', transient_instance=False): @@ -688,6 +689,9 @@ class BasicSwap(BaseApp): elif coin == Coins.BTC: from .interface.btc import BTCInterface return BTCInterface(self.coin_clients[coin], self.chain, self) + elif coin == Coins.BCH: + from .interface.bch import BCHInterface + return BCHInterface(self.coin_clients[coin], self.chain, self) elif coin == Coins.LTC: from .interface.ltc import LTCInterface, LTCInterfaceMWEB interface = LTCInterface(self.coin_clients[coin], self.chain, self) diff --git a/basicswap/basicswap_util.py b/basicswap/basicswap_util.py index 298a3ea..d947ae4 100644 --- a/basicswap/basicswap_util.py +++ b/basicswap/basicswap_util.py @@ -64,6 +64,7 @@ class SwapTypes(IntEnum): SELLER_FIRST_2MSG = auto() BUYER_FIRST_2MSG = auto() XMR_SWAP = auto() + XMR_BCH_SWAP = auto() class OfferStates(IntEnum): diff --git a/basicswap/bin/prepare.py b/basicswap/bin/prepare.py old mode 100755 new mode 100644 index 0c79ed7..369c6ca --- a/basicswap/bin/prepare.py +++ b/basicswap/bin/prepare.py @@ -49,6 +49,9 @@ LITECOIN_VERSION_TAG = os.getenv('LITECOIN_VERSION_TAG', '') BITCOIN_VERSION = os.getenv('BITCOIN_VERSION', '26.0') BITCOIN_VERSION_TAG = os.getenv('BITCOIN_VERSION_TAG', '') +BITCOINCASH_VERSION = os.getenv('BITCOIN_VERSION', '27.1.0') +BITCOINCASH_VERSION_TAG = os.getenv('BITCOIN_VERSION_TAG', '') + MONERO_VERSION = os.getenv('MONERO_VERSION', '0.18.3.4') MONERO_VERSION_TAG = os.getenv('MONERO_VERSION_TAG', '') XMR_SITE_COMMIT = '3751c0d7987a9e78324a718c32c008e2ec91b339' # Lock hashes.txt to monero version @@ -84,6 +87,7 @@ SKIP_GPG_VALIDATION = toBool(os.getenv('SKIP_GPG_VALIDATION', 'false')) known_coins = { 'particl': (PARTICL_VERSION, PARTICL_VERSION_TAG, ('tecnovert',)), 'bitcoin': (BITCOIN_VERSION, BITCOIN_VERSION_TAG, ('laanwj',)), + 'bitcoincash': (BITCOINCASH_VERSION, BITCOINCASH_VERSION_TAG, ('Calin_Culianu',)), 'litecoin': (LITECOIN_VERSION, LITECOIN_VERSION_TAG, ('davidburkett38',)), 'decred': (DCR_VERSION, DCR_VERSION_TAG, ('decred_release',)), 'namecoin': ('0.18.0', '', ('JeremyRand',)), @@ -113,6 +117,7 @@ expected_key_ids = { 'reuben': ('1290A1D0FA7EE109',), 'nav_builder': ('2782262BF6E7FADB',), 'decred_release': ('6D897EDF518A031D',), + 'Calin_Culianu': ('21810A542031C02C',), } USE_PLATFORM = os.getenv('USE_PLATFORM', platform.system()) @@ -186,6 +191,12 @@ BTC_ONION_PORT = int(os.getenv('BTC_ONION_PORT', 8334)) BTC_RPC_USER = os.getenv('BTC_RPC_USER', '') BTC_RPC_PWD = os.getenv('BTC_RPC_PWD', '') +BCH_RPC_HOST = os.getenv('BCH_RPC_HOST', '127.0.0.1') +BCH_RPC_PORT = int(os.getenv('BCH_RPC_PORT', 19997)) +BCH_ONION_PORT = int(os.getenv('BCH_ONION_PORT', 8334)) +BCH_RPC_USER = os.getenv('BCH_RPC_USER', '') +BCH_RPC_PWD = os.getenv('BCH_RPC_PWD', '') + DCR_RPC_HOST = os.getenv('DCR_RPC_HOST', '127.0.0.1') DCR_RPC_PORT = int(os.getenv('DCR_RPC_PORT', 9109)) DCR_WALLET_RPC_HOST = os.getenv('DCR_WALLET_RPC_HOST', '127.0.0.1') @@ -513,8 +524,14 @@ def extractCore(coin, version_data, settings, bin_dir, release_path, extra_opts= return dir_name = 'dashcore' if coin == 'dash' else coin + dir_name = 'bitcoin-cash-node' if coin == 'bitcoincash' else coin if coin == 'decred': bins = ['dcrd', 'dcrwallet'] + elif coin == 'bitcoincash': + bins = ['bitcoind', 'bitcoin-cli', 'bitcoin-tx'] + versions = version.split('.') + if int(versions[0]) >= 22 or int(versions[1]) >= 19: + bins.append('bitcoin-wallet') else: bins = [coin + 'd', coin + '-cli', coin + '-tx'] versions = version.split('.') @@ -696,6 +713,11 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}): assert_url = f'https://raw.githubusercontent.com/bitcoin-core/guix.sigs/main/{version}/{signing_key_name}/all.SHA256SUMS' else: assert_url = 'https://raw.githubusercontent.com/bitcoin-core/gitian.sigs/master/%s-%s/%s/%s' % (version, os_dir_name, signing_key_name, assert_filename) + elif coin == 'bitcoincash': + release_filename = 'bitcoin-cash-node-{}-{}.{}'.format(version, BIN_ARCH, FILE_EXT) + release_url = 'https://github.com/bitcoin-cash-node/bitcoin-cash-node/releases/download/v{}/{}'.format(version, release_filename) + assert_filename = 'SHA256SUMS.{}.asc.Calin_Culianu'.format(version) + assert_url = 'https://gitlab.com/bitcoin-cash-node/announcements/-/raw/master/release-sigs/%s/%s' % (version, assert_filename) elif coin == 'namecoin': 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]) @@ -738,7 +760,7 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}): if not os.path.exists(assert_path): downloadFile(assert_url, assert_path) - if coin not in ('firo', ): + if coin not in ('firo', 'bitcoincash',): assert_sig_url = assert_url + ('.asc' if use_guix else '.sig') if coin not in ('nav', ): assert_sig_filename = '{}-{}-{}-build-{}.assert.sig'.format(coin, os_name, version, signing_key_name) @@ -798,11 +820,13 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}): pubkeyurls.append('https://git.wownero.com/wownero/wownero/raw/branch/master/utils/gpg_keys/wowario.asc') if coin == 'firo': pubkeyurls.append('https://firo.org/reuben.asc') + if coin == 'bitcoincash': + pubkeyurls.append('https://gitlab.com/bitcoin-cash-node/bitcoin-cash-node/-/raw/master/contrib/gitian-signing/pubkeys.txt') if ADD_PUBKEY_URL != '': pubkeyurls.append(ADD_PUBKEY_URL + '/' + pubkey_filename) - if coin in ('monero', 'wownero', 'firo'): + if coin in ('monero', 'wownero', 'firo', 'bitcoincash',): with open(assert_path, 'rb') as fp: verified = gpg.verify_file(fp) @@ -837,7 +861,6 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}): verified = gpg.verify_file(fp, assert_path) ensureValidSignatureBy(verified, signing_key_name) - extractCore(coin, version_data, settings, bin_dir, release_path, extra_opts) @@ -1032,6 +1055,10 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}): fp.write('fallbackfee=0.0002\n') if BTC_RPC_USER != '': fp.write('rpcauth={}:{}${}\n'.format(BTC_RPC_USER, salt, password_to_hmac(salt, BTC_RPC_PWD))) + elif coin == 'bitcoincash': + fp.write('prune=2000\n') + if BCH_RPC_USER != '': + fp.write('rpcauth={}:{}${}\n'.format(BCH_RPC_USER, salt, password_to_hmac(salt, BCH_RPC_PWD))) elif coin == 'namecoin': fp.write('prune=2000\n') elif coin == 'pivx': @@ -1182,6 +1209,8 @@ def modify_tor_config(settings, coin, tor_control_password=None, enable=False, e default_onionport = 0 if coin == 'bitcoin': default_onionport = BTC_ONION_PORT + if coin == 'bitcoincash': + default_onionport = BCH_ONION_PORT elif coin == 'particl': default_onionport = PART_ONION_PORT elif coin == 'litecoin': @@ -1353,7 +1382,7 @@ def initialise_wallets(particl_wallet_mnemonic, with_coins, data_dir, settings, pass else: if coin_settings['manage_daemon']: - filename = coin_name + 'd' + ('.exe' if os.name == 'nt' else '') + filename = (coin_name if not coin_name == "bitcoincash" else "bitcoin") + 'd' + ('.exe' if os.name == 'nt' else '') coin_args = ['-nofindpeers', '-nostaking'] if c == Coins.PART else [] if c == Coins.FIRO: @@ -1755,6 +1784,19 @@ def main(): 'conf_target': 2, 'core_version_group': 22, }, + 'bitcoincash': { + 'connection_type': 'rpc' if 'bitcoincash' in with_coins else 'none', + 'manage_daemon': True if ('bitcoincash' in with_coins and BCH_RPC_HOST == '127.0.0.1') else False, + 'rpchost': BCH_RPC_HOST, + 'rpcport': BCH_RPC_PORT + port_offset, + 'onionport': BCH_ONION_PORT + port_offset, + 'datadir': os.getenv('BCH_DATA_DIR', os.path.join(data_dir, 'bitcoincash')), + 'bindir': os.path.join(bin_dir, 'bitcoincash'), + 'use_segwit': False, + 'blocks_confirmed': 1, + 'conf_target': 2, + 'core_version_group': 22, + }, 'litecoin': { 'connection_type': 'rpc', 'manage_daemon': shouldManageDaemon('LTC'), @@ -1917,6 +1959,9 @@ def main(): if BTC_RPC_USER != '': chainclients['bitcoin']['rpcuser'] = BTC_RPC_USER chainclients['bitcoin']['rpcpassword'] = BTC_RPC_PWD + if BCH_RPC_USER != '': + chainclients['bitcoin']['rpcuser'] = BCH_RPC_USER + chainclients['bitcoin']['rpcpassword'] = BCH_RPC_PWD if XMR_RPC_USER != '': chainclients['monero']['rpcuser'] = XMR_RPC_USER chainclients['monero']['rpcpassword'] = XMR_RPC_PWD diff --git a/basicswap/bin/run.py b/basicswap/bin/run.py index 65baf4f..156301a 100755 --- a/basicswap/bin/run.py +++ b/basicswap/bin/run.py @@ -214,7 +214,7 @@ def runClient(fp, data_dir, chain, start_only_coins): if c in ('monero', 'wownero'): if v['manage_daemon'] is True: swap_client.log.info(f'Starting {display_name} daemon') - filename = c + 'd' + ('.exe' if os.name == 'nt' else '') + filename = (c if not c == "bitcoincash" else "bitcoin") + 'd' + ('.exe' if os.name == 'nt' else '') daemons.append(startXmrDaemon(v['datadir'], v['bindir'], filename)) pid = daemons[-1].handle.pid swap_client.log.info('Started {} {}'.format(filename, pid)) @@ -280,7 +280,7 @@ def runClient(fp, data_dir, chain, start_only_coins): if v['manage_daemon'] is True: swap_client.log.info(f'Starting {display_name} daemon') - filename = c + 'd' + ('.exe' if os.name == 'nt' else '') + filename = (c if not c == "bitcoincash" else "bitcoin") + 'd' + ('.exe' if os.name == 'nt' else '') daemons.append(startDaemon(v['datadir'], v['bindir'], filename)) pid = daemons[-1].handle.pid pids.append((c, pid)) diff --git a/basicswap/chainparams.py b/basicswap/chainparams.py index 36b4f0f..7aaaf9d 100644 --- a/basicswap/chainparams.py +++ b/basicswap/chainparams.py @@ -30,6 +30,7 @@ class Coins(IntEnum): NAV = 14 LTC_MWEB = 15 # ZANO = 16 + BCH = 17 chainparams = { @@ -432,7 +433,45 @@ chainparams = { 'min_amount': 1000, 'max_amount': 100000 * COIN, } - } + }, + Coins.BCH: { + 'name': 'bitcoincash', + 'ticker': 'BCH', + 'message_magic': 'Bitcoin Signed Message:\n', + 'blocks_target': 60 * 2, + 'decimal_places': 8, + 'mainnet': { + 'rpcport': 8332, + 'pubkey_address': 0, + 'script_address': 5, + 'key_prefix': 128, + 'hrp': 'bitcoincash', + 'bip44': 0, + 'min_amount': 1000, + 'max_amount': 100000 * COIN, + }, + 'testnet': { + 'rpcport': 18332, + 'pubkey_address': 111, + 'script_address': 196, + 'key_prefix': 239, + 'hrp': 'bchtest', + 'bip44': 1, + 'min_amount': 1000, + 'max_amount': 100000 * COIN, + 'name': 'testnet3', + }, + 'regtest': { + 'rpcport': 18443, + 'pubkey_address': 111, + 'script_address': 196, + 'key_prefix': 239, + 'hrp': 'bchreg', + 'bip44': 1, + 'min_amount': 1000, + 'max_amount': 100000 * COIN, + } + }, } ticker_map = {} diff --git a/basicswap/config.py b/basicswap/config.py index cad16b1..09f3be2 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) + +BITCOINCASH_BINDIR = os.path.expanduser(os.getenv('BITCOINCASH_BINDIR', os.path.join(DEFAULT_TEST_BINDIR, 'bitcoincash'))) +BITCOINCASHD = os.getenv('BITCOINCASHD', 'bitcoind' + bin_suffix) +BITCOINCASH_CLI = os.getenv('BITCOINCASH_CLI', 'bitcoin-cli' + bin_suffix) +BITCOINCASH_TX = os.getenv('BITCOINCASH_TX', 'bitcoin-tx' + bin_suffix) diff --git a/basicswap/interface/bch.py b/basicswap/interface/bch.py new file mode 100644 index 0000000..6743427 --- /dev/null +++ b/basicswap/interface/bch.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright (c) 2020-2023 tecnovert +# Distributed under the MIT software license, see the accompanying +# file LICENSE or http://www.opensource.org/licenses/mit-license.php. + +from typing import Union +from basicswap.contrib.test_framework.messages import COutPoint, CTransaction, CTxIn, CTxOut +from basicswap.util import ensure, i2h +from .btc import BTCInterface, findOutput +from basicswap.rpc import make_rpc_func +from basicswap.chainparams import Coins, chainparams +from basicswap.interface.contrib.bch_test_framework.cashaddress import Address +from basicswap.util.crypto import hash160, sha256 +from basicswap.interface.contrib.bch_test_framework.script import OP_EQUAL, OP_EQUALVERIFY, OP_HASH256, OP_DUP, OP_HASH160, OP_CHECKSIG +from basicswap.contrib.test_framework.script import ( + CScript, CScriptOp, +) + +class BCHInterface(BTCInterface): + @staticmethod + def coin_type(): + return Coins.BCH + + def __init__(self, coin_settings, network, swap_client=None): + super(BCHInterface, self).__init__(coin_settings, network, swap_client) + + + def decodeAddress(self, address: str) -> bytes: + return bytes(Address.from_string(address).payload) + + def pubkey_to_segwit_address(self, pk: bytes) -> str: + raise NotImplementedError() + + def pkh_to_address(self, pkh: bytes) -> str: + # pkh is ripemd160(sha256(pk)) + assert (len(pkh) == 20) + prefix = self.chainparams_network()['hrp'] + address = Address("P2PKH", b'\x76\xa9\x14' + pkh + b'\x88\xac') + address.prefix = prefix + return address.cash_address() + + def getNewAddress(self, use_segwit: bool = False, label: str = 'swap_receive') -> str: + args = [label] + return self.rpc_wallet('getnewaddress', args) + + def addressToLockingBytecode(self, address: str) -> bytes: + return b'\x76\xa9\x14' + bytes(Address.from_string(address).payload) + b'\x88\xac' + + def getScriptDest(self, script): + return self.scriptToP2SH32LockingBytecode(script) + + def scriptToP2SH32LockingBytecode(self, script: Union[bytes, str]) -> bytes: + if isinstance(script, str): + script = bytes.fromhex(script) + + return CScript([ + CScriptOp(OP_HASH256), + sha256(sha256(script)), + CScriptOp(OP_EQUAL), + ]) + + def createSCLockTx(self, value: int, script: bytearray, vkbv: bytes = None) -> bytes: + tx = CTransaction() + tx.nVersion = self.txVersion() + tx.vout.append(self.txoType()(value, self.getScriptDest(script))) + return tx.serialize_without_witness() + + def getScriptForPubkeyHash(self, pkh: bytes) -> CScript: + return CScript([ + CScriptOp(OP_DUP), + CScriptOp(OP_HASH160), + pkh, + CScriptOp(OP_EQUALVERIFY), + CScriptOp(OP_CHECKSIG), + ]) + + def getTxSize(self, tx: CTransaction) -> int: + return len(tx.serialize_without_witness()) + + def getScriptScriptSig(self, script: bytes, ves: bytes) -> bytes: + if ves is not None: + return CScript([ves, script]) + else: + return CScript([script]) + + def createSCLockSpendTx(self, tx_lock_bytes, script_lock, pkh_dest, tx_fee_rate, ves=None, fee_info={}): + # tx_fee_rate in this context is equal to `mining_fee` contract param + tx_lock = self.loadTx(tx_lock_bytes) + output_script = self.getScriptDest(script_lock) + locked_n = findOutput(tx_lock, output_script) + ensure(locked_n is not None, 'Output not found in tx') + locked_coin = tx_lock.vout[locked_n].nValue + + tx_lock.rehash() + tx_lock_id_int = tx_lock.sha256 + + tx = CTransaction() + tx.nVersion = self.txVersion() + tx.vin.append(CTxIn(COutPoint(tx_lock_id_int, locked_n), + scriptSig=self.getScriptScriptSig(script_lock, ves), + nSequence=0)) + + tx.vout.append(self.txoType()(locked_coin, self.getScriptForPubkeyHash(pkh_dest))) + pay_fee = tx_fee_rate + tx.vout[0].nValue = locked_coin - pay_fee + + size = self.getTxSize(tx) + + fee_info['fee_paid'] = pay_fee + fee_info['rate_used'] = tx_fee_rate + fee_info['size'] = size + + tx.rehash() + self._log.info('createSCLockSpendTx %s:\n fee_rate, size, fee: %ld, %ld, %ld.', + i2h(tx.sha256), tx_fee_rate, size, pay_fee) + + return tx.serialize_without_witness() diff --git a/basicswap/interface/contrib/bch_test_framework/__init__.py b/basicswap/interface/contrib/bch_test_framework/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/basicswap/interface/contrib/bch_test_framework/cashaddress.py b/basicswap/interface/contrib/bch_test_framework/cashaddress.py new file mode 100644 index 0000000..d6099de --- /dev/null +++ b/basicswap/interface/contrib/bch_test_framework/cashaddress.py @@ -0,0 +1,247 @@ +import unittest + + +CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" + +def polymod(values): + chk = 1 + generator = [ + (0x01, 0x98F2BC8E61), + (0x02, 0x79B76D99E2), + (0x04, 0xF33E5FB3C4), + (0x08, 0xAE2EABE2A8), + (0x10, 0x1E4F43E470), + ] + for value in values: + top = chk >> 35 + chk = ((chk & 0x07FFFFFFFF) << 5) ^ value + for i in generator: + if top & i[0] != 0: + chk ^= i[1] + return chk ^ 1 + + +def calculate_checksum(prefix, payload): + poly = polymod(prefix_expand(prefix) + payload + [0, 0, 0, 0, 0, 0, 0, 0]) + out = list() + for i in range(8): + out.append((poly >> 5 * (7 - i)) & 0x1F) + return out + + +def verify_checksum(prefix, payload): + return polymod(prefix_expand(prefix) + payload) == 0 + + +def b32decode(inputs): + out = list() + for letter in inputs: + out.append(CHARSET.find(letter)) + return out + + +def b32encode(inputs): + out = "" + for char_code in inputs: + out += CHARSET[char_code] + return out + + +def convertbits(data, frombits, tobits, pad=True): + acc = 0 + bits = 0 + ret = [] + maxv = (1 << tobits) - 1 + max_acc = (1 << (frombits + tobits - 1)) - 1 + for value in data: + if value < 0 or (value >> frombits): + return None + acc = ((acc << frombits) | value) & max_acc + bits += frombits + while bits >= tobits: + bits -= tobits + ret.append((acc >> bits) & maxv) + if pad: + if bits: + ret.append((acc << (tobits - bits)) & maxv) + elif bits >= frombits or ((acc << (tobits - bits)) & maxv): + return None + return ret + + +def prefix_expand(prefix): + return [ord(x) & 0x1F for x in prefix] + [0] + + +class Address: + """ + Class to handle CashAddr. + + :param version: Version of CashAddr + :type version: ``str`` + :param payload: Payload of CashAddr as int list of the bytearray + :type payload: ``list`` of ``int`` + """ + + VERSIONS = { + "P2SH20": {"prefix": "bitcoincash", "version_bit": 8, "network": "mainnet"}, + "P2SH32": {"prefix": "bitcoincash", "version_bit": 11, "network": "mainnet"}, + "P2PKH": {"prefix": "bitcoincash", "version_bit": 0, "network": "mainnet"}, + "P2SH20-TESTNET": {"prefix": "bchtest", "version_bit": 8, "network": "testnet"}, + "P2SH32-TESTNET": { + "prefix": "bchtest", + "version_bit": 11, + "network": "testnet", + }, + "P2PKH-TESTNET": {"prefix": "bchtest", "version_bit": 0, "network": "testnet"}, + "P2SH20-REGTEST": {"prefix": "bchreg", "version_bit": 8, "network": "regtest"}, + "P2SH32-REGTEST": {"prefix": "bchreg", "version_bit": 11, "network": "regtest"}, + "P2PKH-REGTEST": {"prefix": "bchreg", "version_bit": 0, "network": "regtest"}, + "P2SH20-CATKN": { + "prefix": "bitcoincash", + "version_bit": 24, + "network": "mainnet", + }, + "P2SH32-CATKN": { + "prefix": "bitcoincash", + "version_bit": 27, + "network": "mainnet", + }, + "P2PKH-CATKN": { + "prefix": "bitcoincash", + "version_bit": 16, + "network": "mainnet", + }, + "P2SH20-CATKN-TESTNET": { + "prefix": "bchtest", + "version_bit": 24, + "network": "testnet", + }, + "P2SH32-CATKN-TESTNET": { + "prefix": "bchtest", + "version_bit": 27, + "network": "testnet", + }, + "P2PKH-CATKN-TESTNET": { + "prefix": "bchtest", + "version_bit": 16, + "network": "testnet", + }, + "P2SH20-CATKN-REGTEST": { + "prefix": "bchreg", + "version_bit": 24, + "network": "regtest", + }, + "P2SH32-CATKN-REGTEST": { + "prefix": "bchreg", + "version_bit": 27, + "network": "regtest", + }, + "P2PKH-CATKN-REGTEST": { + "prefix": "bchreg", + "version_bit": 16, + "network": "regtest", + }, + } + + VERSION_SUFFIXES = {"bitcoincash": "", "bchtest": "-TESTNET", "bchreg": "-REGTEST"} + + ADDRESS_TYPES = { + 0: "P2PKH", + 8: "P2SH20", + 11: "P2SH32", + 16: "P2PKH-CATKN", + 24: "P2SH20-CATKN", + 27: "P2SH32-CATKN", + } + + def __init__(self, version, payload): + if version not in Address.VERSIONS: + raise ValueError("Invalid address version provided") + + self.version = version + self.payload = payload + self.prefix = Address.VERSIONS[self.version]["prefix"] + + def __str__(self): + return ( + f"version: {self.version}\npayload: {self.payload}\nprefix: {self.prefix}" + ) + + def __repr__(self): + return f"Address('{self.cash_address()}')" + + def __eq__(self, other): + if isinstance(other, str): + return self.cash_address() == other + elif isinstance(other, Address): + return self.cash_address() == other.cash_address() + else: + raise ValueError( + "Address can be compared to a string address" + " or an instance of Address" + ) + + def cash_address(self): + """ + Generate CashAddr of the Address + + :rtype: ``str`` + """ + version_bit = Address.VERSIONS[self.version]["version_bit"] + payload = [version_bit] + self.payload + payload = convertbits(payload, 8, 5) + checksum = calculate_checksum(self.prefix, payload) + return self.prefix + ":" + b32encode(payload + checksum) + + @staticmethod + def from_string(address): + """ + Generate Address from a cashadress string + + :param scriptcode: The cashaddress string + :type scriptcode: ``str`` + :returns: Instance of :class:~bitcash.cashaddress.Address + """ + try: + address = str(address) + except Exception: + raise ValueError("Expected string as input") + + if address.upper() != address and address.lower() != address: + raise ValueError( + "Cash address contains uppercase and lowercase characters" + ) + + address = address.lower() + colon_count = address.count(":") + if colon_count == 0: + raise ValueError("Cash address is missing prefix") + if colon_count > 1: + raise ValueError("Cash address contains more than one colon character") + + prefix, base32string = address.split(":") + decoded = b32decode(base32string) + + if not verify_checksum(prefix, decoded): + raise ValueError( + "Bad cash address checksum for address {}".format(address) + ) + converted = convertbits(decoded, 5, 8) + + try: + version = Address.ADDRESS_TYPES[converted[0]] + except Exception: + raise ValueError("Could not determine address version") + + version += Address.VERSION_SUFFIXES[prefix] + + payload = converted[1:-6] + return Address(version, payload) + +class TestFrameworkScript(unittest.TestCase): + def test_base58encodedecode(self): + def check_cashaddress(address: str): + self.assertEqual(Address.from_string(address).cash_address(), address) + + check_cashaddress("bitcoincash:qzfyvx77v2pmgc0vulwlfkl3uzjgh5gnmqk5hhyaa6") diff --git a/basicswap/interface/contrib/bch_test_framework/script.py b/basicswap/interface/contrib/bch_test_framework/script.py new file mode 100644 index 0000000..3e7d59d --- /dev/null +++ b/basicswap/interface/contrib/bch_test_framework/script.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2024 tecnovert +# Distributed under the MIT software license, see the accompanying +# file LICENSE or http://www.opensource.org/licenses/mit-license.php. + + +OP_TXINPUTCOUNT = 0xc3 +OP_1 = 0x51 +OP_NUMEQUALVERIFY = 0x9d +OP_TXOUTPUTCOUNT = 0xc4 +OP_0 = 0x00 +OP_UTXOVALUE = 0xc6 +OP_OUTPUTVALUE = 0xcc +OP_SUB = 0x94 +OP_UTXOTOKENCATEGORY = 0xce +OP_OUTPUTTOKENCATEGORY = 0xd1 +OP_EQUALVERIFY = 0x88 +OP_UTXOTOKENCOMMITMENT = 0xcf +OP_OUTPUTTOKENCOMMITMENT = 0xd2 +OP_UTXOTOKENAMOUNT = 0xd0 +OP_OUTPUTTOKENAMOUNT = 0xd3 +OP_INPUTSEQUENCENUMBER = 0xcb +OP_NOTIF = 0x64 +OP_OUTPUTBYTECODE = 0xcd +OP_OVER = 0x78 +OP_CHECKDATASIG = 0xba +OP_CHECKDATASIGVERIFY = 0xbb +OP_ELSE = 0x67 +OP_CHECKSEQUENCEVERIFY = 0xb2 +OP_DROP = 0x75 +OP_EQUAL = 0x87 +OP_ENDIF = 0x68 +OP_HASH256 = 0xaa +OP_PUSHBYTES_32 = 0x20 +OP_DUP = 0x76 +OP_HASH160 = 0xa9 +OP_CHECKSIG = 0xac +OP_SHA256 = 0xa8 +OP_VERIFY = 0x69 \ No newline at end of file diff --git a/basicswap/protocols/xmr_swap_1.py b/basicswap/protocols/xmr_swap_1.py index 22660df..28bc50c 100644 --- a/basicswap/protocols/xmr_swap_1.py +++ b/basicswap/protocols/xmr_swap_1.py @@ -6,6 +6,34 @@ import traceback +import unittest +from basicswap.interface.contrib.bch_test_framework.script import ( + OP_TXINPUTCOUNT, + OP_1, + OP_NUMEQUALVERIFY, + OP_TXOUTPUTCOUNT, + OP_0, + OP_UTXOVALUE, + OP_OUTPUTVALUE, + OP_SUB, + OP_UTXOTOKENCATEGORY, + OP_OUTPUTTOKENCATEGORY, + OP_EQUALVERIFY, + OP_UTXOTOKENCOMMITMENT, + OP_OUTPUTTOKENCOMMITMENT, + OP_UTXOTOKENAMOUNT, + OP_OUTPUTTOKENAMOUNT, + OP_INPUTSEQUENCENUMBER, + OP_NOTIF, + OP_OUTPUTBYTECODE, + OP_OVER, + OP_CHECKDATASIG, + OP_ELSE, + OP_CHECKSEQUENCEVERIFY, + OP_DROP, + OP_EQUAL, + OP_ENDIF, +) from basicswap.util import ( ensure, ) @@ -193,3 +221,82 @@ class XmrSwapInterface(ProtocolInterface): ctx.nLockTime = 0 return ctx.serialize() + +class XmrBchSwapInterface(ProtocolInterface): + swap_type = SwapTypes.XMR_BCH_SWAP + + def genScriptLockTxScript(self, mining_fee: int, out_1: bytes, out_2: bytes, public_key: bytes, timelock: int) -> CScript: + return CScript([ + # // v4.1.0-CashTokens-Optimized + # // Based on swaplock.cash v4.1.0-CashTokens + # + # // Alice has XMR, wants BCH and/or CashTokens. + # // Bob has BCH and/or CashTokens, wants XMR. + # + # // Verify 1-in-1-out TX form + CScriptOp(OP_TXINPUTCOUNT), + CScriptOp(OP_1), CScriptOp(OP_NUMEQUALVERIFY), + CScriptOp(OP_TXOUTPUTCOUNT), + CScriptOp(OP_1), CScriptOp(OP_NUMEQUALVERIFY), + + # // int miningFee + mining_fee, + # // Verify pre-agreed mining fee and that the rest of BCH is forwarded + # // to the output. + CScriptOp(OP_0), CScriptOp(OP_UTXOVALUE), + CScriptOp(OP_0), CScriptOp(OP_OUTPUTVALUE), + CScriptOp(OP_SUB), CScriptOp(OP_NUMEQUALVERIFY), + + # # // Verify that any CashTokens are forwarded to the output. + CScriptOp(OP_0), CScriptOp(OP_UTXOTOKENCATEGORY), + CScriptOp(OP_0), CScriptOp(OP_OUTPUTTOKENCATEGORY), + CScriptOp(OP_EQUALVERIFY), + CScriptOp(OP_0), CScriptOp(OP_UTXOTOKENCOMMITMENT), + CScriptOp(OP_0), CScriptOp(OP_OUTPUTTOKENCOMMITMENT), + CScriptOp(OP_EQUALVERIFY), + CScriptOp(OP_0), CScriptOp(OP_UTXOTOKENAMOUNT), + CScriptOp(OP_0), CScriptOp(OP_OUTPUTTOKENAMOUNT), + CScriptOp(OP_NUMEQUALVERIFY), + + # // If sequence is not used then it is a regular swap TX. + CScriptOp(OP_0), CScriptOp(OP_INPUTSEQUENCENUMBER), + CScriptOp(OP_NOTIF), + # // bytes aliceOutput + out_1, + # // Verify that the BCH and/or CashTokens are forwarded to Alice's + # // output. + CScriptOp(OP_0), CScriptOp(OP_OUTPUTBYTECODE), + CScriptOp(OP_OVER), CScriptOp(OP_EQUALVERIFY), + + # // pubkey bobPubkeyVES + public_key, + # // Require Alice to decrypt and publish Bob's VES signature. + # // The "message" signed is simply a sha256 hash of Alice's output + # // locking bytecode. + # // By decrypting Bob's VES and publishing it, Alice reveals her + # // XMR key share to Bob. + CScriptOp(OP_CHECKDATASIG), + + # // If a TX using this path is mined then Alice gets her BCH. + # // Bob uses the revealed XMR key share to collect his XMR. + + # // Refund will become available when timelock expires, and it would + # // expire because Alice didn't collect on time, either of her own accord + # // or because Bob bailed out and witheld the encrypted signature. + CScriptOp(OP_ELSE), + # // int timelock_0 + timelock, + # // Verify refund timelock. + CScriptOp(OP_CHECKSEQUENCEVERIFY), CScriptOp(OP_DROP), + + # // bytes refundLockingBytecode + out_2, + + # // Verify that the BCH and/or CashTokens are forwarded to Refund + # // contract. + CScriptOp(OP_0), CScriptOp(OP_OUTPUTBYTECODE), + CScriptOp(OP_EQUAL), + + # // BCH and/or CashTokens are simply forwarded to Refund contract. + CScriptOp(OP_ENDIF) + ]) diff --git a/docker/production/bitcoincash/Dockerfile b/docker/production/bitcoincash/Dockerfile new file mode 100644 index 0000000..c9ca712 --- /dev/null +++ b/docker/production/bitcoincash/Dockerfile @@ -0,0 +1,27 @@ +# https://github.com/NicolasDorier/docker-bitcoin/blob/master/README.md + +FROM i_swapclient as install_stage + +RUN basicswap-prepare --preparebinonly --bindir=/coin_bin --withcoin=bitcoincash --withoutcoins=particl && \ + find /coin_bin -name *.tar.gz -delete + +FROM debian:bullseye-slim +COPY --from=install_stage /coin_bin . + +ENV BITCOIN_DATA /data + +RUN groupadd -r bitcoin && useradd -r -m -g bitcoin bitcoin \ + && apt-get update \ + && apt-get install -qq --no-install-recommends gosu \ + && rm -rf /var/lib/apt/lists/* \ + && mkdir "$BITCOIN_DATA" \ + && chown -R bitcoin:bitcoin "$BITCOIN_DATA" \ + && ln -sfn "$BITCOIN_DATA" /home/bitcoin/.bitcoin \ + && chown -h bitcoin:bitcoin /home/bitcoin/.bitcoin +VOLUME /data + +COPY entrypoint.sh /entrypoint.sh +ENTRYPOINT ["/entrypoint.sh"] + +EXPOSE 8332 8333 18332 18333 18443 18444 +CMD ["/bitcoin/bitcoind", "--datadir=/data"] diff --git a/docker/production/bitcoincash/entrypoint.sh b/docker/production/bitcoincash/entrypoint.sh new file mode 100755 index 0000000..925d7d2 --- /dev/null +++ b/docker/production/bitcoincash/entrypoint.sh @@ -0,0 +1,11 @@ +#!/bin/bash +set -e + +if [[ "$1" == "bitcoin-cli" || "$1" == "bitcoin-tx" || "$1" == "bitcoind" || "$1" == "test_bitcoin" ]]; then + mkdir -p "$BITCOIN_DATA" + + chown -h bitcoin:bitcoin /home/bitcoin/.bitcoin + exec gosu bitcoin "$@" +else + exec "$@" +fi diff --git a/docker/production/example.env b/docker/production/example.env index 5e417fd..9f065d8 100644 --- a/docker/production/example.env +++ b/docker/production/example.env @@ -21,6 +21,12 @@ BTC_RPC_PORT=19796 BTC_RPC_USER=bitcoin_user BTC_RPC_PWD=bitcoin_pwd +BCH_DATA_DIR=/data/bitcoincash +BCH_RPC_HOST=bitcoincash_core +BCH_RPC_PORT=19797 +BCH_RPC_USER=bitcoincash_user +BCH_RPC_PWD=bitcoincash_pwd + LTC_DATA_DIR=/data/litecoin LTC_RPC_HOST=litecoin_core LTC_RPC_PORT=19795 diff --git a/tests/basicswap/common.py b/tests/basicswap/common.py index d4a17c3..05439f0 100644 --- a/tests/basicswap/common.py +++ b/tests/basicswap/common.py @@ -31,6 +31,11 @@ BTC_BASE_RPC_PORT = 32792 BTC_BASE_ZMQ_PORT = 33792 BTC_BASE_TOR_PORT = 33732 +BCH_BASE_PORT = 41792 +BCH_BASE_RPC_PORT = 42792 +BCH_BASE_ZMQ_PORT = 43792 +BCH_BASE_TOR_PORT = 43732 + LTC_BASE_PORT = 34792 LTC_BASE_RPC_PORT = 35792 LTC_BASE_ZMQ_PORT = 36792 @@ -75,7 +80,8 @@ def prepareDataDir(datadir, node_id, conf_file, dir_prefix, base_p2p_port=BASE_P fp.write('acceptnonstdtxn=0\n') fp.write('txindex=1\n') fp.write('wallet=wallet.dat\n') - fp.write('findpeers=0\n') + if not base_p2p_port == BCH_BASE_PORT: + fp.write('findpeers=0\n') if base_p2p_port == BTC_BASE_PORT: fp.write('deprecatedrpc=create_bdb\n') diff --git a/tests/basicswap/test_bch.py b/tests/basicswap/test_bch.py new file mode 100644 index 0000000..93c732a --- /dev/null +++ b/tests/basicswap/test_bch.py @@ -0,0 +1,11 @@ +import unittest + +from basicswap.protocols.xmr_swap_1 import XmrBchSwapInterface + + +class TestXmrBchSwapInterface(unittest.TestCase): + def test_generate_script(self): + out_1 = bytes.fromhex('a9147171b53baf87efc9c78ffc0e37a78859cebaae4a87') + out_2 = bytes.fromhex('a9147171b53baf87efc9c78ffc0e37a78859cebaae4a87') + public_key = bytes.fromhex('03fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556') + print(XmrBchSwapInterface().genScriptLockTxScript(None, 1000, out_1, out_2, public_key, 2).hex()) diff --git a/tests/basicswap/test_bch_xmr.py b/tests/basicswap/test_bch_xmr.py new file mode 100644 index 0000000..6497af3 --- /dev/null +++ b/tests/basicswap/test_bch_xmr.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# Copyright (c) 2021-2024 tecnovert +# Distributed under the MIT software license, see the accompanying +# file LICENSE or http://www.opensource.org/licenses/mit-license.php. + +import random +import logging +import unittest + +from basicswap.db import ( + Concepts, +) +from basicswap.basicswap import ( + BidStates, + Coins, + DebugTypes, + SwapTypes, +) +from basicswap.basicswap_util import ( + TxLockTypes, + EventLogTypes, +) +from basicswap.util import ( + make_int, + format_amount, +) +from basicswap.interface.base import Curves +from basicswap.util.crypto import sha256 +from tests.basicswap.util import ( + read_json_api, +) +from tests.basicswap.common import ( + abandon_all_swaps, + wait_for_bid, + wait_for_event, + wait_for_offer, + wait_for_balance, + wait_for_unspent, + wait_for_none_active, + BTC_BASE_RPC_PORT, +) +from basicswap.contrib.test_framework.messages import ( + ToHex, + FromHex, + CTxIn, + COutPoint, + CTransaction, + CTxInWitness, +) +from basicswap.contrib.test_framework.script import ( + CScript, + OP_EQUAL, + OP_CHECKLOCKTIMEVERIFY, + OP_CHECKSEQUENCEVERIFY, +) +from .test_xmr import BaseTest, test_delay_event, callnoderpc + +from coincurve.ecdsaotves import ( + ecdsaotves_enc_sign, + ecdsaotves_enc_verify, + ecdsaotves_dec_sig, + ecdsaotves_rec_enc_key +) + +logger = logging.getLogger() + + +class TestFunctions(BaseTest): + __test__ = True + start_bch_nodes = True + base_rpc_port = None + extra_wait_time = 0 + + def callnoderpc(self, method, params=[], wallet=None, node_id=0): + return callnoderpc(node_id, method, params, wallet, self.base_rpc_port) + + def mineBlock(self, num_blocks=1): + self.callnoderpc('generatetoaddress', [num_blocks, self.btc_addr]) + + def check_softfork_active(self, feature_name): + deploymentinfo = self.callnoderpc('getdeploymentinfo') + assert (deploymentinfo['deployments'][feature_name]['active'] is True) + + def test_010_bch_txn_size(self): + logging.info('---------- Test {} txn_size'.format(Coins.BCH)) + + swap_clients = self.swap_clients + ci = swap_clients[0].ci(Coins.BCH) + pi = swap_clients[0].pi(SwapTypes.XMR_BCH_SWAP) + + amount: int = ci.make_int(random.uniform(0.1, 2.0), r=1) + + # Record unspents before createSCLockTx as the used ones will be locked + unspents = ci.rpc('listunspent') + + # fee_rate is in sats/B + fee_rate: int = 1 + + a = ci.getNewSecretKey() + b = ci.getNewSecretKey() + + A = ci.getPubkey(a) + B = ci.getPubkey(b) + + mining_fee = 1000 + timelock = 2 + a_receive = ci.getNewAddress() + b_receive = ci.getNewAddress() + b_refund = ci.getNewAddress() + print(pi) + refund_lock_tx_script = pi.genScriptLockTxScript(mining_fee=mining_fee, out_1=ci.addressToLockingBytecode(b_refund), out_2=ci.addressToLockingBytecode(a_receive), public_key=A, timelock=timelock) + addr_out = ci.getNewAddress() + + lock_tx_script = pi.genScriptLockTxScript(mining_fee=mining_fee, out_1=ci.addressToLockingBytecode(a_receive), out_2=ci.scriptToP2SH32LockingBytecode(refund_lock_tx_script), public_key=B, timelock=timelock) + + lock_tx = ci.createSCLockTx(amount, lock_tx_script) + lock_tx = ci.fundSCLockTx(lock_tx, fee_rate) + lock_tx = ci.signTxWithWallet(lock_tx) + print(lock_tx.hex()) + + unspents_after = ci.rpc('listunspent') + assert (len(unspents) > len(unspents_after)) + + tx_decoded = ci.rpc('decoderawtransaction', [lock_tx.hex()]) + print(tx_decoded) + txid = tx_decoded['txid'] + + size = tx_decoded['size'] + expect_fee_int = round(fee_rate * size) + expect_fee = ci.format_amount(expect_fee_int) + + out_value: int = 0 + for txo in tx_decoded['vout']: + if 'value' in txo: + out_value += ci.make_int(txo['value']) + in_value: int = 0 + for txi in tx_decoded['vin']: + for utxo in unspents: + if 'vout' not in utxo: + continue + if utxo['txid'] == txi['txid'] and utxo['vout'] == txi['vout']: + in_value += ci.make_int(utxo['amount']) + break + fee_value = in_value - out_value + + ci.rpc('sendrawtransaction', [lock_tx.hex()]) + rv = ci.rpc('gettransaction', [txid]) + print(rv) + wallet_tx_fee = -ci.make_int(rv['fee']) + + assert (wallet_tx_fee == fee_value) + assert (wallet_tx_fee == expect_fee_int) + + pkh_out = ci.decodeAddress(a_receive) + + msg = sha256(ci.addressToLockingBytecode(a_receive)) + + # bob creates an adaptor signature for alice and transmits it to her + bAdaptorSig = ecdsaotves_enc_sign(b, A, msg) + + # alice verifies the adaptor signature + assert (ecdsaotves_enc_verify(B, A, msg, bAdaptorSig)) + + # alice decrypts the adaptor signature + bAdaptorSig_dec = ecdsaotves_dec_sig(a, bAdaptorSig) + print("\nbAdaptorSig_dec", bAdaptorSig_dec.hex()) + + print(ci.addressToLockingBytecode(a_receive).hex(), msg.hex(), bAdaptorSig_dec.hex(), B.hex()) + + fee_info = {} + lock_spend_tx = ci.createSCLockSpendTx(lock_tx, lock_tx_script, pkh_out, mining_fee, ves=bAdaptorSig_dec, fee_info=fee_info) + print(lock_spend_tx.hex()) + size_estimated: int = fee_info['size'] + + tx_decoded = ci.rpc('decoderawtransaction', [lock_spend_tx.hex()]) + print(tx_decoded) + txid = tx_decoded['txid'] + + tx_decoded = ci.rpc('decoderawtransaction', [lock_spend_tx.hex()]) + size_actual: int = tx_decoded['size'] + + assert (size_actual <= size_estimated and size_estimated - size_actual < 4) + assert (ci.rpc('sendrawtransaction', [lock_spend_tx.hex()]) == txid) + + expect_size: int = ci.xmr_swap_a_lock_spend_tx_vsize() + assert (expect_size >= size_actual) + assert (expect_size - size_actual < 10) + + # Test chain b (no-script) lock tx size + v = ci.getNewSecretKey() + s = ci.getNewSecretKey() + S = ci.getPubkey(s) + lock_tx_b_txid = ci.publishBLockTx(v, S, amount, fee_rate) + + addr_out = ci.getNewAddress(True) + lock_tx_b_spend_txid = ci.spendBLockTx(lock_tx_b_txid, addr_out, v, s, amount, fee_rate, 0) + lock_tx_b_spend = ci.getTransaction(lock_tx_b_spend_txid) + if lock_tx_b_spend is None: + lock_tx_b_spend = ci.getWalletTransaction(lock_tx_b_spend_txid) + lock_tx_b_spend_decoded = ci.rpc('decoderawtransaction', [lock_tx_b_spend.hex()]) + + expect_size: int = ci.xmr_swap_b_lock_spend_tx_vsize() + assert (expect_size >= lock_tx_b_spend_decoded['size']) + assert (expect_size - lock_tx_b_spend_decoded['size'] < 10) diff --git a/tests/basicswap/test_xmr.py b/tests/basicswap/test_xmr.py index 53e72ae..73d9af8 100644 --- a/tests/basicswap/test_xmr.py +++ b/tests/basicswap/test_xmr.py @@ -84,6 +84,8 @@ from tests.basicswap.common import ( BASE_ZMQ_PORT, BTC_BASE_PORT, BTC_BASE_RPC_PORT, + BCH_BASE_PORT, + BCH_BASE_RPC_PORT, LTC_BASE_PORT, LTC_BASE_RPC_PORT, PREFIX_SECRET_KEY_REGTEST, @@ -99,6 +101,7 @@ logger = logging.getLogger() NUM_NODES = 3 NUM_XMR_NODES = 3 NUM_BTC_NODES = 3 +NUM_BCH_NODES = 3 NUM_LTC_NODES = 3 TEST_DIR = cfg.TEST_DATADIRS @@ -216,6 +219,18 @@ def prepare_swapclient_dir(datadir, node_id, network_key, network_pubkey, with_c 'use_segwit': True, } + if Coins.BCH in with_coins: + settings['chainclients']['bitcoincash'] = { + 'connection_type': 'rpc', + 'manage_daemon': False, + 'rpcport': BCH_BASE_RPC_PORT + node_id, + 'rpcuser': 'test' + str(node_id), + 'rpcpassword': 'test_pass' + str(node_id), + 'datadir': os.path.join(datadir, 'bch_' + str(node_id)), + 'bindir': cfg.BITCOINCASH_BINDIR, + 'use_segwit': False, + } + if cls: cls.addCoinSettings(settings, datadir, node_id) @@ -226,6 +241,8 @@ def prepare_swapclient_dir(datadir, node_id, network_key, network_pubkey, with_c def btcCli(cmd, node_id=0): return callrpc_cli(cfg.BITCOIN_BINDIR, os.path.join(TEST_DIR, 'btc_' + str(node_id)), 'regtest', cmd, cfg.BITCOIN_CLI) +def bchCli(cmd, node_id=0): + return callrpc_cli(cfg.BITCOINCASH_BINDIR, os.path.join(TEST_DIR, 'bch_' + str(node_id)), 'regtest', cmd, cfg.BITCOINCASH_CLI) 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) @@ -294,17 +311,20 @@ class BaseTest(unittest.TestCase): swap_clients = [] part_daemons = [] btc_daemons = [] + bch_daemons = [] ltc_daemons = [] xmr_daemons = [] xmr_wallet_auth = [] restore_instance = False + start_bch_nodes = False start_ltc_nodes = False start_xmr_nodes = True has_segwit = True xmr_addr = None btc_addr = None + bch_addr = None ltc_addr = None @classmethod @@ -405,6 +425,20 @@ class BaseTest(unittest.TestCase): waitForRPC(make_rpc_func(i, base_rpc_port=BTC_BASE_RPC_PORT), test_delay_event) + for i in range(NUM_BCH_NODES): + if not cls.restore_instance: + data_dir = prepareDataDir(TEST_DIR, i, 'bitcoin.conf', 'bch_', base_p2p_port=BCH_BASE_PORT, base_rpc_port=BCH_BASE_RPC_PORT) + if os.path.exists(os.path.join(cfg.BITCOINCASH_BINDIR, 'bitcoin-wallet')): + try: + callrpc_cli(cfg.BITCOINCASH_BINDIR, data_dir, 'regtest', '-wallet=wallet.dat create', 'bitcoin-wallet') + except Exception as e: + logging.warning('bch: bitcoin-wallet create failed') + raise e + + cls.bch_daemons.append(startDaemon(os.path.join(TEST_DIR, 'bch_' + str(i)), cfg.BITCOINCASH_BINDIR, cfg.BITCOINCASHD)) + logging.info('Bch: Started %s %d', cfg.BITCOINCASHD, cls.part_daemons[-1].handle.pid) + waitForRPC(make_rpc_func(i, base_rpc_port=BCH_BASE_RPC_PORT), test_delay_event) + if cls.start_ltc_nodes: for i in range(NUM_LTC_NODES): if not cls.restore_instance: @@ -466,6 +500,8 @@ class BaseTest(unittest.TestCase): start_nodes.add(Coins.LTC) if cls.start_xmr_nodes: start_nodes.add(Coins.XMR) + if cls.start_bch_nodes: + start_nodes.add(Coins.BCH) if not cls.restore_instance: prepare_swapclient_dir(TEST_DIR, i, cls.network_key, cls.network_pubkey, start_nodes, cls) basicswap_dir = os.path.join(os.path.join(TEST_DIR, 'basicswap_' + str(i))) @@ -483,6 +519,8 @@ class BaseTest(unittest.TestCase): if cls.start_ltc_nodes: sc.setDaemonPID(Coins.LTC, cls.ltc_daemons[i].handle.pid) + if cls.start_bch_nodes: + sc.setDaemonPID(Coins.BCH, cls.bch_daemons[i].handle.pid) cls.addPIDInfo(sc, i) sc.start() @@ -502,6 +540,8 @@ class BaseTest(unittest.TestCase): cls.ltc_addr = cls.swap_clients[0].ci(Coins.LTC).pubkey_to_address(void_block_rewards_pubkey) if cls.start_xmr_nodes: cls.xmr_addr = cls.callxmrnodewallet(cls, 1, 'get_address')['address'] + if cls.start_bch_nodes: + cls.bch_addr = cls.swap_clients[0].ci(Coins.BCH).pubkey_to_address(void_block_rewards_pubkey) else: cls.btc_addr = callnoderpc(0, 'getnewaddress', ['mining_addr', 'bech32'], base_rpc_port=BTC_BASE_RPC_PORT) num_blocks = 400 # Mine enough to activate segwit @@ -550,6 +590,12 @@ class BaseTest(unittest.TestCase): checkForks(callnoderpc(0, 'getblockchaininfo', base_rpc_port=LTC_BASE_RPC_PORT, wallet='wallet.dat')) + if cls.start_bch_nodes: + num_blocks = 200 + cls.bch_addr = callnoderpc(0, 'getnewaddress', ['mining_addr'], base_rpc_port=BCH_BASE_RPC_PORT, wallet='wallet.dat') + logging.info('Mining %d BitcoinCash blocks to %s', num_blocks, cls.bch_addr) + callnoderpc(0, 'generatetoaddress', [num_blocks, cls.bch_addr], base_rpc_port=BCH_BASE_RPC_PORT, wallet='wallet.dat') + num_blocks = 100 if cls.start_xmr_nodes: cls.xmr_addr = cls.callxmrnodewallet(cls, 1, 'get_address')['address'] @@ -612,12 +658,14 @@ class BaseTest(unittest.TestCase): stopDaemons(cls.xmr_daemons) stopDaemons(cls.part_daemons) stopDaemons(cls.btc_daemons) + stopDaemons(cls.bch_daemons) stopDaemons(cls.ltc_daemons) cls.http_threads.clear() cls.swap_clients.clear() cls.part_daemons.clear() cls.btc_daemons.clear() + cls.bch_daemons.clear() cls.ltc_daemons.clear() cls.xmr_daemons.clear() @@ -643,6 +691,8 @@ class BaseTest(unittest.TestCase): def coins_loop(cls): if cls.btc_addr is not None: btcCli('generatetoaddress 1 {}'.format(cls.btc_addr)) + if cls.bch_addr is not None: + ltcCli('generatetoaddress 1 {}'.format(cls.bch_addr)) if cls.ltc_addr is not None: ltcCli('generatetoaddress 1 {}'.format(cls.ltc_addr)) if cls.xmr_addr is not None: