diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index 8844e6e..4e9ed6e 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -251,7 +251,6 @@ 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): @@ -675,6 +674,9 @@ class BasicSwap(BaseApp): return self.coin_clients[use_coinid][interface_ind] + def isBchXmrSwap(self, offer: Offer): + return (offer['coin_from'] == Coins.BCH or offer['coin_to'] == Coins.BCH) and offer.swap_type == SwapTypes.XMR_SWAP + def pi(self, protocol_ind): if protocol_ind not in self.protocolInterfaces: raise ValueError('Unknown protocol_ind {}'.format(int(protocol_ind))) @@ -2976,7 +2978,32 @@ class BasicSwap(BaseApp): # MSG2F pi = self.pi(SwapTypes.XMR_SWAP) - xmr_swap.a_lock_tx_script = pi.genScriptLockTxScript(ci_from, xmr_swap.pkal, xmr_swap.pkaf) + + refundExtraArgs = dict() + lockExtraArgs = dict() + if self.isBchXmrSwap(offer): + pkh_refund_to = ci_from.decodeAddress(self.getCachedAddressForCoin(coin_from)) + pkh_dest = xmr_swap.dest_af + + # refund script + refundExtraArgs['mining_fee'] = 1000 + refundExtraArgs['out_1'] = ci_from.getScriptForPubkeyHash(pkh_refund_to) + refundExtraArgs['out_2'] = ci_from.getScriptForPubkeyHash(pkh_dest) + refundExtraArgs['public_key'] = xmr_swap.pkaf + refundExtraArgs['timelock'] = xmr_offer.lock_time_2 + + refund_lock_tx_script = pi.genScriptLockTxScript(ci_from, xmr_swap.pkal, xmr_swap.pkaf, refundExtraArgs) + # will make use of this in `createSCLockRefundTx` + refundExtraArgs['refund_lock_tx_script'] = refund_lock_tx_script + + # lock script + lockExtraArgs['mining_fee'] = 1000 + lockExtraArgs['out_1'] = ci_from.getScriptForPubkeyHash(pkh_refund_to) + lockExtraArgs['out_2'] = ci_from.scriptToP2SH32LockingBytecode(refund_lock_tx_script) + lockExtraArgs['public_key'] = xmr_swap.pkal + lockExtraArgs['timelock'] = xmr_offer.lock_time_1 + + xmr_swap.a_lock_tx_script = pi.genScriptLockTxScript(ci_from, xmr_swap.pkal, xmr_swap.pkaf, lockExtraArgs) prefunded_tx = self.getPreFundedTx(Concepts.OFFER, bid.offer_id, TxTypes.ITX_PRE_FUNDED, session=use_session) if prefunded_tx: xmr_swap.a_lock_tx = pi.promoteMockTx(ci_from, prefunded_tx, xmr_swap.a_lock_tx_script) @@ -2994,7 +3021,7 @@ class BasicSwap(BaseApp): xmr_swap.a_lock_tx, xmr_swap.a_lock_tx_script, xmr_swap.pkal, xmr_swap.pkaf, xmr_offer.lock_time_1, xmr_offer.lock_time_2, - a_fee_rate, xmr_swap.vkbv + a_fee_rate, xmr_swap.vkbv, refundExtraArgs ) xmr_swap.a_lock_refund_tx_id = ci_from.getTxid(xmr_swap.a_lock_refund_tx) @@ -3003,7 +3030,7 @@ class BasicSwap(BaseApp): v = ci_from.verifyTxSig(xmr_swap.a_lock_refund_tx, xmr_swap.al_lock_refund_tx_sig, xmr_swap.pkal, 0, xmr_swap.a_lock_tx_script, prevout_amount) ensure(v, 'Invalid coin A lock refund tx leader sig') - pkh_refund_to = ci_from.decodeAddress(self.getReceiveAddressForCoin(coin_from)) + pkh_refund_to = ci_from.decodeAddress(self.getCachedAddressForCoin(coin_from)) xmr_swap.a_lock_refund_spend_tx = ci_from.createSCLockRefundSpendTx( xmr_swap.a_lock_refund_tx, xmr_swap.a_lock_refund_tx_script, pkh_refund_to, @@ -3894,6 +3921,7 @@ class BasicSwap(BaseApp): self.createActionInSession(delay, ActionTypes.RECOVER_XMR_SWAP_LOCK_TX_B, bid_id, session) session.commit() elif state == BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_SPEND_TX: + print(3, bid.xmr_a_lock_tx) if bid.xmr_a_lock_tx is None: return rv @@ -4847,10 +4875,11 @@ class BasicSwap(BaseApp): raise ValueError('TODO') elif offer_data.swap_type == SwapTypes.XMR_SWAP: ensure(offer_data.protocol_version >= MINPROTO_VERSION_ADAPTOR_SIG, 'Invalid protocol version') - if reverse_bid: - ensure(ci_to.has_segwit(), 'Coin-to must support segwit for reverse bid offers') - else: - ensure(ci_from.has_segwit(), 'Coin-from must support segwit') + if Coins.BCH not in (coin_from, coin_to): + if reverse_bid: + ensure(ci_to.has_segwit(), 'Coin-to must support segwit for reverse bid offers') + else: + ensure(ci_from.has_segwit(), 'Coin-from must support segwit') ensure(len(offer_data.proof_address) == 0, 'Unexpected data') ensure(len(offer_data.proof_signature) == 0, 'Unexpected data') ensure(len(offer_data.pkhash_seller) == 0, 'Unexpected data') @@ -5695,6 +5724,7 @@ class BasicSwap(BaseApp): if lock_tx_sent is False: lock_tx_signed = ci_from.signTxWithWallet(xmr_swap.a_lock_tx) + print(1, lock_tx_signed) txid_hex = ci_from.publishTx(lock_tx_signed) vout_pos = ci_from.getTxOutputPos(xmr_swap.a_lock_tx, xmr_swap.a_lock_tx_script) @@ -5764,6 +5794,7 @@ class BasicSwap(BaseApp): try: b_lock_tx_id = ci_to.publishBLockTx(xmr_swap.vkbv, xmr_swap.pkbs, bid.amount_to, b_fee_rate, unlock_time=unlock_time) + print(2, b_lock_tx_id) if bid.debug_ind == DebugTypes.B_LOCK_TX_MISSED_SEND: self.log.debug('Adaptor-sig bid %s: Debug %d - Losing xmr lock tx %s.', bid_id.hex(), bid.debug_ind, b_lock_tx_id.hex()) self.logBidEvent(bid.bid_id, EventLogTypes.DEBUG_TWEAK_APPLIED, 'ind {}'.format(bid.debug_ind), session) diff --git a/basicswap/bin/prepare.py b/basicswap/bin/prepare.py index 369c6ca..63a614c 100644 --- a/basicswap/bin/prepare.py +++ b/basicswap/bin/prepare.py @@ -49,8 +49,8 @@ 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', '') +BITCOINCASH_VERSION = os.getenv('BITCOINCASH_VERSION', '27.1.0') +BITCOINCASH_VERSION_TAG = os.getenv('BITCOINCASH_VERSION_TAG', '') MONERO_VERSION = os.getenv('MONERO_VERSION', '0.18.3.4') MONERO_VERSION_TAG = os.getenv('MONERO_VERSION_TAG', '') diff --git a/basicswap/chainparams.py b/basicswap/chainparams.py index 7aaaf9d..fcfe1ee 100644 --- a/basicswap/chainparams.py +++ b/basicswap/chainparams.py @@ -440,6 +440,9 @@ chainparams = { 'message_magic': 'Bitcoin Signed Message:\n', 'blocks_target': 60 * 2, 'decimal_places': 8, + 'has_cltv': True, + 'has_csv': True, + 'has_segwit': False, 'mainnet': { 'rpcport': 8332, 'pubkey_address': 0, diff --git a/basicswap/interface/bch.py b/basicswap/interface/bch.py index 6743427..959900e 100644 --- a/basicswap/interface/bch.py +++ b/basicswap/interface/bch.py @@ -1,22 +1,62 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -# Copyright (c) 2020-2023 tecnovert +# Copyright (c) 2022-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 basicswap.util import b2h, ensure, i2h from .btc import BTCInterface, findOutput from basicswap.rpc import make_rpc_func -from basicswap.chainparams import Coins, chainparams +from basicswap.chainparams import Coins 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.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, + OP_HASH160, + OP_DUP, + OP_CHECKSIG, + OP_HASH256, +) from basicswap.contrib.test_framework.script import ( CScript, CScriptOp, ) +from coincurve.keys import ( + PrivateKey, + PublicKey, +) + +def findOutput(tx, script_pk: bytes): + for i in range(len(tx.vout)): + if tx.vout[i].scriptPubKey == script_pk: + return i + return None class BCHInterface(BTCInterface): @staticmethod @@ -25,11 +65,184 @@ class BCHInterface(BTCInterface): def __init__(self, coin_settings, network, swap_client=None): super(BCHInterface, self).__init__(coin_settings, network, swap_client) + # No multiwallet support + self.rpc_wallet = make_rpc_func(self._rpcport, self._rpcauth, host=self._rpc_host) + def getExchangeName(self, exchange_name): + return 'bch' + def getNewAddress(self, use_segwit: bool = False, label: str = 'swap_receive') -> str: + args = [label] + return self.rpc_wallet('getnewaddress', args) + + # returns pkh def decodeAddress(self, address: str) -> bytes: return bytes(Address.from_string(address).payload) + def encodeSegwitAddress(self, script): + raise ValueError('TODO') + + def decodeSegwitAddress(self, addr): + raise ValueError('TODO') + + def getSCLockScriptAddress(self, lock_script: bytes) -> str: + lock_tx_dest = self.getScriptDest(lock_script) + address = self.encodeScriptDest(lock_tx_dest) + + if not self.isAddressMine(address, or_watch_only=True): + # Expects P2WSH nested in BIP16_P2SH + ro = self.rpc('importaddress', [lock_tx_dest.hex(), 'bid lock', False, True]) + addr_info = self.rpc('validateaddress', [address]) + + return address + + def createRawFundedTransaction(self, addr_to: str, amount: int, sub_fee: bool = False, lock_unspents: bool = True) -> str: + txn = self.rpc('createrawtransaction', [[], {addr_to: self.format_amount(amount)}]) + + options = { + 'lockUnspents': lock_unspents, + # 'conf_target': self._conf_target, + } + if sub_fee: + options['subtractFeeFromOutputs'] = [0,] + return self.rpc_wallet('fundrawtransaction', [txn, options])['hex'] + + def getScriptForPubkeyHash(self, pkh: bytes) -> bytearray: + # Return P2PKH + return CScript([OP_DUP, OP_HASH160, pkh, OP_EQUALVERIFY, OP_CHECKSIG]) + + # def getScriptDest(self, script: bytearray) -> bytearray: + # # P2SH + + # script_hash = hash160(script) + # assert len(script_hash) == 20 + + # return CScript([OP_HASH160, script_hash, OP_EQUAL]) + + def encodeScriptDest(self, script_dest: bytes) -> str: + # Extract hash from script + script_hash = script_dest[2:-1] + return self.sh_to_address(script_hash) + + def sh_to_address(self, sh: bytes) -> str: + assert (len(sh) == 20) + network = self._network.upper() + address = Address("P2SH20" if network == "MAINNET" else "P2SH20-"+network, sh) + return address.cash_address() + + def getDestForScriptHash(self, script_hash): + assert len(script_hash) == 20 + return CScript([OP_HASH160, script_hash, OP_EQUAL]) + + def withdrawCoin(self, value: float, addr_to: str, subfee: bool): + params = [addr_to, value, '', '', subfee, True, True] + return self.rpc_wallet('sendtoaddress', params) + + def getSpendableBalance(self) -> int: + return self.make_int(self.rpc('getwalletinfo')['unconfirmed_balance']) + + def getBLockSpendTxFee(self, tx, fee_rate: int) -> int: + add_bytes = 107 + size = len(tx.serialize_with_witness()) + add_bytes + pay_fee = round(fee_rate * size / 1000) + self._log.info(f'BLockSpendTx fee_rate, size, fee: {fee_rate}, {size}, {pay_fee}.') + return pay_fee + + def findTxnByHash(self, txid_hex: str): + # Only works for wallet txns + try: + rv = self.rpc('gettransaction', [txid_hex]) + except Exception as ex: + self._log.debug('findTxnByHash getrawtransaction failed: {}'.format(txid_hex)) + return None + if 'confirmations' in rv and rv['confirmations'] >= self.blocks_confirmed: + block_height = self.getBlockHeader(rv['blockhash'])['height'] + return {'txid': txid_hex, 'amount': 0, 'height': block_height} + return None + + def genScriptLockTxScript(self, ci, Kal: bytes, Kaf: bytes, **kwargs) -> CScript: + print("bch genScriptLockTxScript") + mining_fee: int = kwargs['mining_fee'] + out_1: bytes = kwargs['out_1'] + out_2: bytes = kwargs['out_2'] + public_key: bytes = kwargs['public_key'] if 'public_key' in kwargs else Kal + timelock: int = kwargs['timelock'] + + 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) + ]) + def pubkey_to_segwit_address(self, pk: bytes) -> str: raise NotImplementedError() @@ -41,10 +254,6 @@ class BCHInterface(BTCInterface): 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' @@ -83,10 +292,11 @@ class BCHInterface(BTCInterface): if ves is not None: return CScript([ves, script]) else: - return CScript([script]) + return CScript([0, script]) - def createSCLockSpendTx(self, tx_lock_bytes, script_lock, pkh_dest, tx_fee_rate, ves=None, fee_info={}): + def createSCLockSpendTx(self, tx_lock_bytes, script_lock, pkh_dest, tx_fee_rate, vkbv=None, fee_info={}, **kwargs): # tx_fee_rate in this context is equal to `mining_fee` contract param + ves = kwargs['ves'] if 'ves' in kwargs else None tx_lock = self.loadTx(tx_lock_bytes) output_script = self.getScriptDest(script_lock) locked_n = findOutput(tx_lock, output_script) @@ -111,9 +321,127 @@ class BCHInterface(BTCInterface): fee_info['fee_paid'] = pay_fee fee_info['rate_used'] = tx_fee_rate fee_info['size'] = size + # vsize is the same as size for BCH + fee_info['vsize'] = 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() + + def createSCLockRefundTx(self, tx_lock_bytes, script_lock, Kal, Kaf, lock1_value, csv_val, tx_fee_rate, vkbv=None, **kwargs): + tx_lock = CTransaction() + 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 + + refund_script = kwargs['refund_lock_tx_script'] + tx = CTransaction() + tx.nVersion = self.txVersion() + tx.vin.append(CTxIn(COutPoint(tx_lock_id_int, locked_n), + nSequence=kwargs['timelock'] if 'timelock' in kwargs else lock1_value, + scriptSig=self.getScriptScriptSig(script_lock, None))) + tx.vout.append(self.txoType()(locked_coin, self.getScriptDest(refund_script))) + + pay_fee = kwargs['mining_fee'] if 'mining_fee' in kwargs else tx_fee_rate + tx.vout[0].nValue = locked_coin - pay_fee + + size = self.getTxSize(tx) + vsize = size + + tx.rehash() + self._log.info('createSCLockRefundTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.', + i2h(tx.sha256), tx_fee_rate, vsize, pay_fee) + + return tx.serialize_without_witness(), refund_script, tx.vout[0].nValue + + def createSCLockRefundSpendTx(self, tx_lock_refund_bytes, script_lock_refund, pkh_refund_to, tx_fee_rate, vkbv=None, **kwargs): + # Returns the coinA locked coin to the leader + # The follower will sign the multisig path with a signature encumbered by the leader's coinB spend pubkey + # If the leader publishes the decrypted signature the leader's coinB spend privatekey will be revealed to the follower + + # spending the refund contract back to leader requires their adaptor signature to be published, but at the moment of this function call it is too early to share it + # TODO: bettter handling of this case + # allow for template ves for transaction to be signed and verified between parties + ves = kwargs['ves'] if 'ves' in kwargs else bytes(70) + + tx_lock_refund = self.loadTx(tx_lock_refund_bytes) + + output_script = self.getScriptDest(script_lock_refund) + locked_n = findOutput(tx_lock_refund, output_script) + ensure(locked_n is not None, 'Output not found in tx') + locked_coin = tx_lock_refund.vout[locked_n].nValue + + tx_lock_refund.rehash() + tx_lock_refund_hash_int = tx_lock_refund.sha256 + + tx = CTransaction() + tx.nVersion = self.txVersion() + tx.vin.append(CTxIn(COutPoint(tx_lock_refund_hash_int, locked_n), + nSequence=0, + scriptSig=self.getScriptScriptSig(script_lock_refund, ves))) + + tx.vout.append(self.txoType()(locked_coin, self.getScriptForPubkeyHash(pkh_refund_to))) + + pay_fee = tx_fee_rate + tx.vout[0].nValue = locked_coin - pay_fee + + size = self.getTxSize(tx) + vsize = size + + tx.rehash() + self._log.info('createSCLockRefundSpendTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.', + i2h(tx.sha256), tx_fee_rate, vsize, pay_fee) + + return tx.serialize_without_witness() + + def signTx(self, key_bytes: bytes, tx_bytes: bytes, input_n: int, prevout_script: bytes, prevout_value: int) -> bytes: + # simply sign the entire tx data, as this is not a preimage signature + eck = PrivateKey(key_bytes) + return eck.sign(tx_bytes, hasher=None) + + def verifyTxSig(self, tx_bytes: bytes, sig: bytes, K: bytes, input_n: int, prevout_script: bytes, prevout_value: int) -> bool: + # simple ecdsa signature verification + pubkey = PublicKey(K) + return pubkey.verify(sig, tx_bytes, hasher=None) + + def setTxSignature(self, tx_bytes: bytes, stack) -> bytes: + return tx_bytes + + def verifySCLockTx(self, tx_bytes, script_out, + swap_value, + Kal, Kaf, + feerate, + check_lock_tx_inputs, vkbv=None): + # Verify: + # + + # Not necessary to check the lock txn is mineable, as protocol will wait for it to confirm + # However by checking early we can avoid wasting time processing unmineable txns + # Check fee is reasonable + + tx = self.loadTx(tx_bytes) + txid = self.getTxid(tx) + self._log.info('Verifying lock tx: {}.'.format(b2h(txid))) + + ensure(tx.nVersion == self.txVersion(), 'Bad version') + ensure(tx.nLockTime == 0, 'Bad nLockTime') # TODO match txns created by cores + + script_pk = self.getScriptDest(script_out) + locked_n = findOutput(tx, script_pk) + ensure(locked_n is not None, 'Output not found in tx') + locked_coin = tx.vout[locked_n].nValue + + # Check value + ensure(locked_coin == swap_value, 'Bad locked value') + + # TODO: better script matching, see interfaces/btc.py + + return txid, locked_n \ No newline at end of file diff --git a/basicswap/interface/btc.py b/basicswap/interface/btc.py index 05afb35..f91f2fe 100644 --- a/basicswap/interface/btc.py +++ b/basicswap/interface/btc.py @@ -1148,7 +1148,8 @@ class BTCInterface(Secp256k1Interface): return None try: - tx = self.rpc_wallet('gettransaction', [txid.hex()]) + # set `include_watchonly` explicitly to `True` to get transactions for watchonly addresses also in BCH + tx = self.rpc_wallet('gettransaction', [txid.hex(), True]) block_height = 0 if 'blockhash' in tx: diff --git a/basicswap/interface/contrib/bch_test_framework/cashaddress.py b/basicswap/interface/contrib/bch_test_framework/cashaddress.py index d6099de..249e6b1 100644 --- a/basicswap/interface/contrib/bch_test_framework/cashaddress.py +++ b/basicswap/interface/contrib/bch_test_framework/cashaddress.py @@ -189,7 +189,7 @@ class Address: :rtype: ``str`` """ version_bit = Address.VERSIONS[self.version]["version_bit"] - payload = [version_bit] + self.payload + payload = [version_bit] + list(self.payload) payload = convertbits(payload, 8, 5) checksum = calculate_checksum(self.prefix, payload) return self.prefix + ":" + b32encode(payload + checksum) diff --git a/basicswap/protocols/xmr_swap_1.py b/basicswap/protocols/xmr_swap_1.py index 28bc50c..695dcd2 100644 --- a/basicswap/protocols/xmr_swap_1.py +++ b/basicswap/protocols/xmr_swap_1.py @@ -191,7 +191,11 @@ def setDLEAG(xmr_swap, ci_to, kbsf: bytes) -> None: class XmrSwapInterface(ProtocolInterface): swap_type = SwapTypes.XMR_SWAP - def genScriptLockTxScript(self, ci, Kal: bytes, Kaf: bytes) -> CScript: + def genScriptLockTxScript(self, ci, Kal: bytes, Kaf: bytes, **kwargs) -> CScript: + # fallthrough to ci if genScriptLockTxScript is implemented there + if hasattr(ci, 'genScriptLockTxScript') and callable(ci.genScriptLockTxScript): + return ci.genScriptLockTxScript(ci, Kal, Kaf, **kwargs) + Kal_enc = Kal if len(Kal) == 33 else ci.encodePubkey(Kal) Kaf_enc = Kaf if len(Kaf) == 33 else ci.encodePubkey(Kaf) @@ -221,82 +225,3 @@ 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/tests/basicswap/test_bch_xmr.py b/tests/basicswap/test_bch_xmr.py index 6497af3..a410db6 100644 --- a/tests/basicswap/test_bch_xmr.py +++ b/tests/basicswap/test_bch_xmr.py @@ -9,6 +9,7 @@ import random import logging import unittest +from basicswap.chainparams import XMR_COIN from basicswap.db import ( Concepts, ) @@ -23,6 +24,7 @@ from basicswap.basicswap_util import ( EventLogTypes, ) from basicswap.util import ( + COIN, make_int, format_amount, ) @@ -107,9 +109,7 @@ class TestFunctions(BaseTest): 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() @@ -124,11 +124,10 @@ class TestFunctions(BaseTest): 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) + vsize = tx_decoded['size'] + expect_fee_int = round(fee_rate * vsize) expect_fee = ci.format_amount(expect_fee_int) out_value: int = 0 @@ -147,7 +146,6 @@ class TestFunctions(BaseTest): 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) @@ -165,28 +163,24 @@ class TestFunctions(BaseTest): # 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'] + vsize_estimated: int = fee_info['vsize'] 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'] + vsize_actual: int = tx_decoded['size'] - assert (size_actual <= size_estimated and size_estimated - size_actual < 4) + assert (vsize_actual <= vsize_estimated and vsize_estimated - vsize_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) + assert (expect_size >= vsize_actual) + assert (expect_size - vsize_actual < 10) # Test chain b (no-script) lock tx size v = ci.getNewSecretKey() @@ -204,3 +198,27 @@ class TestFunctions(BaseTest): 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) + + def test_05_bch_xmr(self): + logging.info('---------- Test BCH to XMR') + swap_clients = self.swap_clients + offer_id = swap_clients[0].postOffer(Coins.BCH, Coins.XMR, 10 * COIN, 100 * XMR_COIN, 10 * COIN, SwapTypes.XMR_SWAP) + wait_for_offer(test_delay_event, swap_clients[1], offer_id) + offers = swap_clients[1].listOffers(filters={'offer_id': offer_id}) + offer = offers[0] + + swap_clients[1].ci(Coins.XMR).setFeePriority(3) + + bid_id = swap_clients[1].postXmrBid(offer_id, offer.amount_from) + + wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.BID_RECEIVED) + + bid, xmr_swap = swap_clients[0].getXmrBid(bid_id) + assert (xmr_swap) + + swap_clients[0].acceptXmrBid(bid_id) + + wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=180) + wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True) + + swap_clients[1].ci(Coins.XMR).setFeePriority(0) diff --git a/tests/basicswap/test_xmr.py b/tests/basicswap/test_xmr.py index 73d9af8..ce8ed05 100644 --- a/tests/basicswap/test_xmr.py +++ b/tests/basicswap/test_xmr.py @@ -98,11 +98,11 @@ from basicswap.bin.run import startDaemon, startXmrDaemon, startXmrWalletDaemon logger = logging.getLogger() -NUM_NODES = 3 -NUM_XMR_NODES = 3 -NUM_BTC_NODES = 3 -NUM_BCH_NODES = 3 -NUM_LTC_NODES = 3 +NUM_NODES = 2 +NUM_XMR_NODES = 2 +NUM_BTC_NODES = 2 +NUM_BCH_NODES = 2 +NUM_LTC_NODES = 2 TEST_DIR = cfg.TEST_DATADIRS XMR_BASE_P2P_PORT = 17792