mirror of
https://github.com/basicswap/basicswap.git
synced 2024-11-16 15:58:17 +00:00
Add bitcoincash support for prepare and run scripts, add bitcoincash to testing suite, groundwork for bch-xmr atomic swap protocol
This commit is contained in:
parent
745d1460e5
commit
1b43806d51
20 changed files with 937 additions and 9 deletions
|
@ -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}"
|
||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -13,3 +13,6 @@ __pycache__
|
|||
# geckodriver.log
|
||||
*.log
|
||||
docker/.env
|
||||
|
||||
# vscode dev container settings
|
||||
compose-dev.yaml
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
53
basicswap/bin/prepare.py
Executable file → Normal file
53
basicswap/bin/prepare.py
Executable file → Normal file
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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 = {}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
119
basicswap/interface/bch.py
Normal file
119
basicswap/interface/bch.py
Normal file
|
@ -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()
|
247
basicswap/interface/contrib/bch_test_framework/cashaddress.py
Normal file
247
basicswap/interface/contrib/bch_test_framework/cashaddress.py
Normal file
|
@ -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")
|
40
basicswap/interface/contrib/bch_test_framework/script.py
Normal file
40
basicswap/interface/contrib/bch_test_framework/script.py
Normal file
|
@ -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
|
|
@ -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)
|
||||
])
|
||||
|
|
27
docker/production/bitcoincash/Dockerfile
Normal file
27
docker/production/bitcoincash/Dockerfile
Normal file
|
@ -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"]
|
11
docker/production/bitcoincash/entrypoint.sh
Executable file
11
docker/production/bitcoincash/entrypoint.sh
Executable file
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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,6 +80,7 @@ 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')
|
||||
if not base_p2p_port == BCH_BASE_PORT:
|
||||
fp.write('findpeers=0\n')
|
||||
|
||||
if base_p2p_port == BTC_BASE_PORT:
|
||||
|
|
11
tests/basicswap/test_bch.py
Normal file
11
tests/basicswap/test_bch.py
Normal file
|
@ -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())
|
206
tests/basicswap/test_bch_xmr.py
Normal file
206
tests/basicswap/test_bch_xmr.py
Normal file
|
@ -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)
|
|
@ -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:
|
||||
|
|
Loading…
Reference in a new issue