From b83f2890133af7706c2b93c61c4fb99c88f17cbd Mon Sep 17 00:00:00 2001
From: mainnet-pat <mainnet_pat@protonmail.com>
Date: Sun, 20 Oct 2024 16:29:50 +0000
Subject: [PATCH] Continue building out the BCH interface Adapt the swap flow
 to BCH specifics - txids change after funding and when signing inputs BCH
 happy path (lock-spend) done

---
 basicswap/basicswap.py          | 253 +++++++++++++-----
 basicswap/interface/bch.py      | 437 ++++++++++++++++++++++++++++----
 basicswap/util/script.py        |  44 ++++
 tests/basicswap/test_bch.py     |  30 ++-
 tests/basicswap/test_bch_xmr.py |  47 +++-
 tests/basicswap/test_xmr.py     |  12 +-
 6 files changed, 678 insertions(+), 145 deletions(-)

diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py
index 4e9ed6e..31f07cc 100644
--- a/basicswap/basicswap.py
+++ b/basicswap/basicswap.py
@@ -41,8 +41,12 @@ from .util import (
     LockedCoinError,
     TemporaryError,
     InactiveCoin,
+    b2h,
+    b2i,
     format_timestamp,
     DeserialiseNum,
+    h2b,
+    i2b,
     zeroIfNone,
     make_int,
     ensure,
@@ -675,7 +679,7 @@ 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
+        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:
@@ -2103,24 +2107,25 @@ class BasicSwap(BaseApp):
             else:
                 raise ValueError('Wallet seed doesn\'t match expected.')
 
-    def getCachedAddressForCoin(self, coin_type):
+    def getCachedAddressForCoin(self, coin_type, session=None):
         self.log.debug('getCachedAddressForCoin %s', Coins(coin_type).name)
         # TODO: auto refresh after used
 
         ci = self.ci(coin_type)
         key_str = 'receive_addr_' + ci.coin_name().lower()
-        session = self.openSession()
+        use_session = self.openSession(session)
         try:
             try:
-                addr = session.query(DBKVString).filter_by(key=key_str).first().value
+                addr = use_session.query(DBKVString).filter_by(key=key_str).first().value
             except Exception:
                 addr = self.getReceiveAddressForCoin(coin_type)
-                session.add(DBKVString(
+                use_session.add(DBKVString(
                     key=key_str,
                     value=addr
                 ))
         finally:
-            self.closeSession(session)
+            if session is None:
+                self.closeSession(use_session)
         return addr
 
     def cacheNewStealthAddressForCoin(self, coin_type):
@@ -2982,28 +2987,27 @@ class BasicSwap(BaseApp):
             refundExtraArgs = dict()
             lockExtraArgs = dict()
             if self.isBchXmrSwap(offer):
-                pkh_refund_to = ci_from.decodeAddress(self.getCachedAddressForCoin(coin_from))
+                pkh_refund_to = ci_from.decodeAddress(self.getCachedAddressForCoin(offer.coin_from, use_session))
                 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)
+                refund_lock_tx_script = ci_from.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_1'] = ci_from.getScriptForPubkeyHash(pkh_dest)
                 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)
+
+            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)
@@ -3016,25 +3020,23 @@ class BasicSwap(BaseApp):
 
             xmr_swap.a_lock_tx_id = ci_from.getTxid(xmr_swap.a_lock_tx)
             a_lock_tx_dest = ci_from.getScriptDest(xmr_swap.a_lock_tx_script)
-
             xmr_swap.a_lock_refund_tx, xmr_swap.a_lock_refund_tx_script, xmr_swap.a_swap_refund_value = ci_from.createSCLockRefundTx(
                 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, refundExtraArgs
+                a_fee_rate, xmr_swap.vkbv, **refundExtraArgs
             )
             xmr_swap.a_lock_refund_tx_id = ci_from.getTxid(xmr_swap.a_lock_refund_tx)
-
             prevout_amount = ci_from.getLockTxSwapOutputValue(bid, xmr_swap)
             xmr_swap.al_lock_refund_tx_sig = ci_from.signTx(kal, xmr_swap.a_lock_refund_tx, 0, xmr_swap.a_lock_tx_script, prevout_amount)
             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.getCachedAddressForCoin(coin_from))
+            pkh_refund_to = ci_from.decodeAddress(self.getCachedAddressForCoin(coin_from, use_session))
             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,
-                a_fee_rate, xmr_swap.vkbv
+                a_fee_rate, xmr_swap.vkbv,
+                **refundExtraArgs
             )
             xmr_swap.a_lock_refund_spend_tx_id = ci_from.getTxid(xmr_swap.a_lock_refund_spend_tx)
 
@@ -3049,7 +3051,8 @@ class BasicSwap(BaseApp):
                 xmr_swap.pkaf,
                 a_fee_rate,
                 check_lock_tx_inputs,
-                xmr_swap.vkbv)
+                xmr_swap.vkbv,
+                **lockExtraArgs)
 
             _, _, lock_refund_vout = ci_from.verifySCLockRefundTx(
                 xmr_swap.a_lock_refund_tx,
@@ -3064,14 +3067,16 @@ class BasicSwap(BaseApp):
                 xmr_offer.lock_time_2,
                 bid.amount,
                 a_fee_rate,
-                xmr_swap.vkbv)
+                xmr_swap.vkbv,
+                **refundExtraArgs)
 
             ci_from.verifySCLockRefundSpendTx(
                 xmr_swap.a_lock_refund_spend_tx, xmr_swap.a_lock_refund_tx,
                 xmr_swap.a_lock_refund_tx_id, xmr_swap.a_lock_refund_tx_script,
                 xmr_swap.pkal,
                 lock_refund_vout, xmr_swap.a_swap_refund_value, a_fee_rate,
-                xmr_swap.vkbv)
+                xmr_swap.vkbv,
+                **refundExtraArgs)
 
             msg_buf = XmrBidAcceptMessage()
             msg_buf.bid_msg_id = bid_id
@@ -3921,7 +3926,6 @@ 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
 
@@ -3933,6 +3937,29 @@ class BasicSwap(BaseApp):
                 if lock_tx_chain_info is None:
                     return rv
 
+                if lock_tx_chain_info['txid'] != b2h(xmr_swap.a_lock_tx_id):
+                    # if we find that txid was changed (by funding or otherwise), we need to update it to track correctly                    
+                    xmr_swap.a_lock_tx_id = h2b(lock_tx_chain_info['txid'])
+                    xmr_swap.a_lock_tx = h2b(lock_tx_chain_info['txhex'])
+
+                    tx = ci_from.loadTx(xmr_swap.a_lock_refund_tx)
+                    tx.vin[0].prevout.hash = b2i(xmr_swap.a_lock_tx_id)
+                    xmr_swap.a_lock_refund_tx = tx.serialize_without_witness()
+                    xmr_swap.a_lock_refund_tx_id = ci_from.getTxid(xmr_swap.a_lock_refund_tx)
+
+                    tx = ci_from.loadTx(xmr_swap.a_lock_spend_tx)
+                    tx.vin[0].prevout.hash = b2i(xmr_swap.a_lock_tx_id)
+                    xmr_swap.a_lock_spend_tx = tx.serialize_without_witness()
+                    xmr_swap.a_lock_spend_tx_id = ci_from.getTxid(xmr_swap.a_lock_spend_tx)
+
+                    if bid.xmr_a_lock_tx:
+                        bid.xmr_a_lock_tx.txid = xmr_swap.a_lock_tx_id
+                        bid.xmr_a_lock_tx.tx_data = xmr_swap.a_lock_tx
+                        bid.xmr_a_lock_tx.spend_txid = xmr_swap.a_lock_spend_tx_id
+
+                    self.addWatchedOutput(offer.coin_from, bid.bid_id, bid.xmr_a_lock_tx.txid.hex(), bid.xmr_a_lock_tx.vout, TxTypes.XMR_SWAP_A_LOCK, SwapTypes.XMR_SWAP)
+                    bid_changed = True
+                    
                 if bid.xmr_a_lock_tx.state == TxStates.TX_NONE and lock_tx_chain_info['height'] == 0:
                     bid.xmr_a_lock_tx.setState(TxStates.TX_IN_MEMPOOL)
 
@@ -4374,8 +4401,15 @@ class BasicSwap(BaseApp):
             state = BidStates(bid.state)
             spending_txid = bytes.fromhex(spend_txid_hex)
 
+            spend_tx = self.ci(coin_from).loadTx(h2b(spend_txn_hex))
+            
             bid.xmr_a_lock_tx.spend_txid = spending_txid
-            if spending_txid == xmr_swap.a_lock_spend_tx_id:
+            if spending_txid == xmr_swap.a_lock_spend_tx_id or i2b(spend_tx.vin[0].prevout.hash) == xmr_swap.a_lock_tx_id:
+                # bch txids change
+                if xmr_swap.a_lock_spend_tx_id != spending_txid:
+                    xmr_swap.a_lock_spend_tx_id = spending_txid
+                    xmr_swap.a_lock_spend_tx = bytes.fromhex(spend_txn_hex)
+
                 if state == BidStates.XMR_SWAP_LOCK_RELEASED:
                     xmr_swap.a_lock_spend_tx = bytes.fromhex(spend_txn_hex)
                     bid.setState(BidStates.XMR_SWAP_SCRIPT_TX_REDEEMED)  # TODO: Wait for confirmation?
@@ -4389,7 +4423,12 @@ class BasicSwap(BaseApp):
                     # Could already be processed if spend was detected in the mempool
                     self.log.warning('Coin a lock tx spend ignored due to bid state for bid {}'.format(bid_id.hex()))
 
-            elif spending_txid == xmr_swap.a_lock_refund_tx_id:
+            elif spending_txid == xmr_swap.a_lock_refund_tx_id or i2b(spend_tx.vin[0].prevout.hash) == xmr_swap.a_lock_refund_tx_id:
+                # bch txids change
+                if xmr_swap.a_lock_refund_tx_id != spending_txid:
+                    xmr_swap.a_lock_refund_tx_id = spending_txid
+                    xmr_swap.a_lock_refund_tx = bytes.fromhex(spend_txn_hex)
+
                 self.log.debug('Coin a lock tx spent by lock refund tx.')
                 bid.setState(BidStates.XMR_SWAP_SCRIPT_TX_PREREFUND)
                 self.logBidEvent(bid.bid_id, EventLogTypes.LOCK_TX_A_REFUND_TX_SEEN, '', use_session)
@@ -4424,14 +4463,22 @@ class BasicSwap(BaseApp):
             state = BidStates(bid.state)
             spending_txid = bytes.fromhex(spend_txid_hex)
 
-            if spending_txid == xmr_swap.a_lock_refund_spend_tx_id:
+            spend_txn_hex = spend_txn['hex']
+            spend_tx = self.ci(coin_from).loadTx(h2b(spend_txn_hex))
+
+            if spending_txid == xmr_swap.a_lock_refund_spend_tx_id or i2b(spend_tx.vin[0].prevout.hash) == xmr_swap.a_lock_refund_spend_tx_id:
+                # bch txids change
+                if xmr_swap.a_lock_refund_spend_tx_id != spending_txid:
+                    xmr_swap.a_lock_refund_spend_tx_id = spending_txid
+                    xmr_swap.a_lock_refund_spend_tx = bytes.fromhex(spend_txn_hex)
+
                 self.log.info('Found coin a lock refund spend tx, bid {}'.format(bid_id.hex()))
                 self.logBidEvent(bid.bid_id, EventLogTypes.LOCK_TX_A_REFUND_SPEND_TX_SEEN, '', session)
                 if bid.xmr_a_lock_tx:
                     bid.xmr_a_lock_tx.setState(TxStates.TX_REFUNDED)
 
                 if was_sent:
-                    xmr_swap.a_lock_refund_spend_tx = bytes.fromhex(spend_txn['hex'])  # Replace with fully signed tx
+                    xmr_swap.a_lock_refund_spend_tx = bytes.fromhex(spend_txn_hex)  # Replace with fully signed tx
                     if TxTypes.XMR_SWAP_A_LOCK_REFUND_SPEND not in bid.txns:
                         bid.txns[TxTypes.XMR_SWAP_A_LOCK_REFUND_SPEND] = SwapTx(
                             bid_id=bid_id,
@@ -5548,6 +5595,33 @@ class BasicSwap(BaseApp):
             xmr_swap.a_lock_refund_spend_tx_id = ci_from.getTxid(xmr_swap.a_lock_refund_spend_tx)
             xmr_swap.al_lock_refund_tx_sig = msg_data.al_lock_refund_tx_sig
 
+            refundExtraArgs = dict()
+            lockExtraArgs = dict()
+            if self.isBchXmrSwap(offer):
+                # perform check that both lock and refund transactions have their outs pointing to correct follower address
+                # and prepare extra args for validation
+
+                bch_ci = self.ci(Coins.BCH)
+
+                mining_fee, out_1, out_2, public_key, timelock = bch_ci.extractScriptLockScriptValues(xmr_swap.a_lock_tx_script)
+                ensure(out_1 == bch_ci.getScriptForPubkeyHash(xmr_swap.dest_af), 'Invalid BCH lock tx script out_1')
+                ensure(out_2 == bch_ci.scriptToP2SH32LockingBytecode(xmr_swap.a_lock_refund_tx_script), 'Invalid BCH lock tx script out_2')
+
+                lockExtraArgs['mining_fee'] = mining_fee
+                lockExtraArgs['out_1'] = out_1
+                lockExtraArgs['out_2'] = out_2
+                lockExtraArgs['public_key'] = public_key
+                lockExtraArgs['timelock'] = timelock
+
+                mining_fee, out_1, out_2, public_key, timelock = bch_ci.extractScriptLockScriptValues(xmr_swap.a_lock_refund_tx_script)
+                ensure(out_2 == bch_ci.getScriptForPubkeyHash(xmr_swap.dest_af), 'Invalid BCH refund tx script out_2')
+
+                refundExtraArgs['mining_fee'] = mining_fee
+                refundExtraArgs['out_1'] = out_1
+                refundExtraArgs['out_2'] = out_2
+                refundExtraArgs['public_key'] = public_key
+                refundExtraArgs['timelock'] = timelock
+
             # TODO: check_lock_tx_inputs without txindex
             check_a_lock_tx_inputs = False
             xmr_swap.a_lock_tx_id, xmr_swap.a_lock_tx_vout = ci_from.verifySCLockTx(
@@ -5555,7 +5629,8 @@ class BasicSwap(BaseApp):
                 bid.amount,
                 xmr_swap.pkal, xmr_swap.pkaf,
                 a_fee_rate,
-                check_a_lock_tx_inputs, xmr_swap.vkbv)
+                check_a_lock_tx_inputs, xmr_swap.vkbv,
+                **lockExtraArgs)
             a_lock_tx_dest = ci_from.getScriptDest(xmr_swap.a_lock_tx_script)
 
             xmr_swap.a_lock_refund_tx_id, xmr_swap.a_swap_refund_value, lock_refund_vout = ci_from.verifySCLockRefundTx(
@@ -5563,13 +5638,15 @@ class BasicSwap(BaseApp):
                 xmr_swap.a_lock_tx_id, xmr_swap.a_lock_tx_vout, xmr_offer.lock_time_1, xmr_swap.a_lock_tx_script,
                 xmr_swap.pkal, xmr_swap.pkaf,
                 xmr_offer.lock_time_2,
-                bid.amount, a_fee_rate, xmr_swap.vkbv)
+                bid.amount, a_fee_rate, xmr_swap.vkbv,
+                **refundExtraArgs)
 
             ci_from.verifySCLockRefundSpendTx(
                 xmr_swap.a_lock_refund_spend_tx, xmr_swap.a_lock_refund_tx,
                 xmr_swap.a_lock_refund_tx_id, xmr_swap.a_lock_refund_tx_script,
                 xmr_swap.pkal,
-                lock_refund_vout, xmr_swap.a_swap_refund_value, a_fee_rate, xmr_swap.vkbv)
+                lock_refund_vout, xmr_swap.a_swap_refund_value, a_fee_rate, xmr_swap.vkbv,
+                **refundExtraArgs)
 
             self.log.info('Checking leader\'s lock refund tx signature')
             prevout_amount = ci_from.getLockTxSwapOutputValue(bid, xmr_swap)
@@ -5724,8 +5801,25 @@ 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)
+            if txid_hex != b2h(xmr_swap.a_lock_tx_id):
+                xmr_swap.a_lock_tx = lock_tx_signed
+                xmr_swap.a_lock_tx_id = bytes.fromhex(txid_hex)
+
+                tx = ci_from.loadTx(xmr_swap.a_lock_refund_tx)
+                tx.vin[0].prevout.hash = b2i(xmr_swap.a_lock_tx_id)
+                xmr_swap.a_lock_refund_tx = tx.serialize_without_witness()
+                xmr_swap.a_lock_refund_tx_id = ci_from.getTxid(xmr_swap.a_lock_refund_tx)
+
+                tx = ci_from.loadTx(xmr_swap.a_lock_spend_tx)
+                tx.vin[0].prevout.hash = b2i(xmr_swap.a_lock_tx_id)
+                xmr_swap.a_lock_spend_tx = tx.serialize_without_witness()
+                xmr_swap.a_lock_spend_tx_id = ci_from.getTxid(xmr_swap.a_lock_spend_tx)
+
+                if bid.xmr_a_lock_tx:
+                    bid.xmr_a_lock_tx.txid = xmr_swap.a_lock_tx_id
+                    bid.xmr_a_lock_tx.tx_data = lock_tx_signed
+                    bid.xmr_a_lock_tx.spend_txid = xmr_swap.a_lock_spend_tx_id
 
             vout_pos = ci_from.getTxOutputPos(xmr_swap.a_lock_tx, xmr_swap.a_lock_tx_script)
             self.log.debug('Submitted lock txn %s to %s chain for bid %s', txid_hex, ci_from.coin_name(), bid_id.hex())
@@ -5794,7 +5888,6 @@ 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)
@@ -5888,25 +5981,40 @@ class BasicSwap(BaseApp):
         kbsf = self.getPathKey(coin_from, coin_to, bid.created_at, xmr_swap.contract_count, KeyTypes.KBSF, for_ed25519)
         kaf = self.getPathKey(coin_from, coin_to, bid.created_at, xmr_swap.contract_count, KeyTypes.KAF)
 
-        al_lock_spend_sig = ci_from.decryptOtVES(kbsf, xmr_swap.al_lock_spend_tx_esig)
-        prevout_amount = ci_from.getLockTxSwapOutputValue(bid, xmr_swap)
-        v = ci_from.verifyTxSig(xmr_swap.a_lock_spend_tx, al_lock_spend_sig, xmr_swap.pkal, 0, xmr_swap.a_lock_tx_script, prevout_amount)
-        ensure(v, 'Invalid coin A lock tx spend tx leader sig')
+        if not self.isBchXmrSwap(offer):
+            # segwit coins sign the transaction
+            al_lock_spend_sig = ci_from.decryptOtVES(kbsf, xmr_swap.al_lock_spend_tx_esig)
+            prevout_amount = ci_from.getLockTxSwapOutputValue(bid, xmr_swap)
+            v = ci_from.verifyTxSig(xmr_swap.a_lock_spend_tx, al_lock_spend_sig, xmr_swap.pkal, 0, xmr_swap.a_lock_tx_script, prevout_amount)
+            ensure(v, 'Invalid coin A lock tx spend tx leader sig')
 
-        af_lock_spend_sig = ci_from.signTx(kaf, xmr_swap.a_lock_spend_tx, 0, xmr_swap.a_lock_tx_script, prevout_amount)
-        v = ci_from.verifyTxSig(xmr_swap.a_lock_spend_tx, af_lock_spend_sig, xmr_swap.pkaf, 0, xmr_swap.a_lock_tx_script, prevout_amount)
-        ensure(v, 'Invalid coin A lock tx spend tx follower sig')
+            af_lock_spend_sig = ci_from.signTx(kaf, xmr_swap.a_lock_spend_tx, 0, xmr_swap.a_lock_tx_script, prevout_amount)
+            v = ci_from.verifyTxSig(xmr_swap.a_lock_spend_tx, af_lock_spend_sig, xmr_swap.pkaf, 0, xmr_swap.a_lock_tx_script, prevout_amount)
+            ensure(v, 'Invalid coin A lock tx spend tx follower sig')
 
-        witness_stack = []
-        if coin_from not in (Coins.DCR,):
-            witness_stack += [b'',]
-        witness_stack += [
-            al_lock_spend_sig,
-            af_lock_spend_sig,
-            xmr_swap.a_lock_tx_script,
-        ]
+            witness_stack = []
+            if coin_from not in (Coins.DCR,):
+                witness_stack += [b'',]
+            witness_stack += [
+                al_lock_spend_sig,
+                af_lock_spend_sig,
+                xmr_swap.a_lock_tx_script,
+            ]
 
-        xmr_swap.a_lock_spend_tx = ci_from.setTxSignature(xmr_swap.a_lock_spend_tx, witness_stack)
+            xmr_swap.a_lock_spend_tx = ci_from.setTxSignature(xmr_swap.a_lock_spend_tx, witness_stack)
+        else:
+            # bch signs the output pkh
+            tx = ci_from.loadTx(xmr_swap.a_lock_spend_tx)
+            out1 = tx.vout[0].scriptPubKey
+            out1_sig = ci_from.decryptOtVES(kbsf, xmr_swap.al_lock_spend_tx_esig)
+            v = ci_from.verifyDataSig(out1, out1_sig, xmr_swap.pkal)
+            ensure(v, 'Invalid signature for lock spend txn')
+
+            # update prevout after tx was signed
+            tx.vin[0].prevout.hash = b2i(xmr_swap.a_lock_tx_id)
+            tx.vin[0].scriptSig = ci_from.getScriptScriptSig(xmr_swap.a_lock_tx_script, out1_sig)
+            xmr_swap.a_lock_spend_tx = tx.serialize_without_witness()
+            xmr_swap.a_lock_spend_tx_id = ci_from.getTxid(xmr_swap.a_lock_spend_tx)
 
         txid = bytes.fromhex(ci_from.publishTx(xmr_swap.a_lock_spend_tx))
         self.log.debug('Submitted lock spend txn %s to %s chain for bid %s', txid.hex(), ci_from.coin_name(), bid_id.hex())
@@ -6136,27 +6244,40 @@ class BasicSwap(BaseApp):
             kbsl = self.getPathKey(coin_from, coin_to, bid.created_at, xmr_swap.contract_count, KeyTypes.KBSL, for_ed25519)
             kal = self.getPathKey(coin_from, coin_to, bid.created_at, xmr_swap.contract_count, KeyTypes.KAL)
 
-            xmr_swap.af_lock_refund_spend_tx_sig = ci_from.decryptOtVES(kbsl, xmr_swap.af_lock_refund_spend_tx_esig)
-            prevout_amount = ci_from.getLockRefundTxSwapOutputValue(bid, xmr_swap)
-            al_lock_refund_spend_tx_sig = ci_from.signTx(kal, xmr_swap.a_lock_refund_spend_tx, 0, xmr_swap.a_lock_refund_tx_script, prevout_amount)
+            if not self.isBchXmrSwap(offer):
+                # segwit coins sign the transaction
+                xmr_swap.af_lock_refund_spend_tx_sig = ci_from.decryptOtVES(kbsl, xmr_swap.af_lock_refund_spend_tx_esig)
+                prevout_amount = ci_from.getLockRefundTxSwapOutputValue(bid, xmr_swap)
+                al_lock_refund_spend_tx_sig = ci_from.signTx(kal, xmr_swap.a_lock_refund_spend_tx, 0, xmr_swap.a_lock_refund_tx_script, prevout_amount)
 
-            self.log.debug('Setting lock refund spend tx sigs')
-            witness_stack = []
-            if coin_from not in (Coins.DCR, ):
-                witness_stack += [b'',]
-            witness_stack += [
-                al_lock_refund_spend_tx_sig,
-                xmr_swap.af_lock_refund_spend_tx_sig,
-                bytes((1,)),
-                xmr_swap.a_lock_refund_tx_script,
-            ]
-            signed_tx = ci_from.setTxSignature(xmr_swap.a_lock_refund_spend_tx, witness_stack)
-            ensure(signed_tx, 'setTxSignature failed')
-            xmr_swap.a_lock_refund_spend_tx = signed_tx
+                self.log.debug('Setting lock refund spend tx sigs')
+                witness_stack = []
+                if coin_from not in (Coins.DCR, ):
+                    witness_stack += [b'',]
+                witness_stack += [
+                    al_lock_refund_spend_tx_sig,
+                    xmr_swap.af_lock_refund_spend_tx_sig,
+                    bytes((1,)),
+                    xmr_swap.a_lock_refund_tx_script,
+                ]
+                signed_tx = ci_from.setTxSignature(xmr_swap.a_lock_refund_spend_tx, witness_stack)
+                ensure(signed_tx, 'setTxSignature failed')
+                xmr_swap.a_lock_refund_spend_tx = signed_tx
 
-            v = ci_from.verifyTxSig(xmr_swap.a_lock_refund_spend_tx, xmr_swap.af_lock_refund_spend_tx_sig, xmr_swap.pkaf, 0, xmr_swap.a_lock_refund_tx_script, prevout_amount)
-            ensure(v, 'Invalid signature for lock refund spend txn')
-            xmr_swap_1.addLockRefundSigs(self, xmr_swap, ci_from)
+                v = ci_from.verifyTxSig(xmr_swap.a_lock_refund_spend_tx, xmr_swap.af_lock_refund_spend_tx_sig, xmr_swap.pkaf, 0, xmr_swap.a_lock_refund_tx_script, prevout_amount)
+                ensure(v, 'Invalid signature for lock refund spend txn')
+                xmr_swap_1.addLockRefundSigs(self, xmr_swap, ci_from)
+            else:
+                # bch signs the output pkh
+                tx = ci_from.loadTx(xmr_swap.a_lock_refund_spend_tx)
+                out1 = tx.vout[0].scriptPubKey
+                out1_sig = ci_from.decryptOtVES(kbsl, xmr_swap.af_lock_refund_spend_tx_esig)
+                v = ci_from.verifyDataSig(out1, out1_sig, xmr_swap.pkaf)
+                ensure(v, 'Invalid signature for lock refund spend txn')
+
+                tx.vin[0].scriptSig = ci_from.getScriptScriptSig(xmr_swap.a_lock_refund_tx_script, out1_sig)
+                xmr_swap.a_lock_refund_spend_tx = tx.serialize_without_witness()
+                xmr_swap.a_lock_refund_spend_tx_id = ci_from.getTxid(xmr_swap.a_lock_refund_spend_tx)
 
             delay = self.get_delay_event_seconds()
             self.log.info('Sending coin A lock tx for adaptor-sig bid %s in %d seconds', bid_id.hex(), delay)
diff --git a/basicswap/interface/bch.py b/basicswap/interface/bch.py
index 959900e..07d2bcc 100644
--- a/basicswap/interface/bch.py
+++ b/basicswap/interface/bch.py
@@ -7,8 +7,9 @@
 
 from typing import Union
 from basicswap.contrib.test_framework.messages import COutPoint, CTransaction, CTxIn, CTxOut
-from basicswap.util import b2h, ensure, i2h
-from .btc import BTCInterface, findOutput
+from basicswap.util import b2h, b2i, ensure, i2h
+from basicswap.util.script import decodePushData, decodeScriptNum
+from .btc import BTCInterface, ensure_op, find_vout_for_address_from_txobj, findOutput
 from basicswap.rpc import make_rpc_func
 from basicswap.chainparams import Coins
 from basicswap.interface.contrib.bch_test_framework.cashaddress import Address
@@ -51,6 +52,12 @@ from coincurve.keys import (
     PrivateKey,
     PublicKey,
 )
+from coincurve.ecdsaotves import (
+    ecdsaotves_enc_sign,
+    ecdsaotves_enc_verify,
+    ecdsaotves_dec_sig,
+    ecdsaotves_rec_enc_key,
+)
 
 def findOutput(tx, script_pk: bytes):
     for i in range(len(tx.vout)):
@@ -80,10 +87,10 @@ class BCHInterface(BTCInterface):
         return bytes(Address.from_string(address).payload)
 
     def encodeSegwitAddress(self, script):
-        raise ValueError('TODO')
+        raise ValueError('Segwit not supported')
 
     def decodeSegwitAddress(self, addr):
-        raise ValueError('TODO')
+        raise ValueError('Segwit not supported')
 
     def getSCLockScriptAddress(self, lock_script: bytes) -> str:
         lock_tx_dest = self.getScriptDest(lock_script)
@@ -125,14 +132,22 @@ class BCHInterface(BTCInterface):
         return self.sh_to_address(script_hash)
     
     def sh_to_address(self, sh: bytes) -> str:
-        assert (len(sh) == 20)
+        assert (len(sh) == 20 or len(sh) == 32)
         network = self._network.upper()
-        address = Address("P2SH20" if network == "MAINNET" else "P2SH20-"+network, sh)
+        address = None
+        if len(sh) == 20:
+            address = Address("P2SH20" if network == "MAINNET" else "P2SH20-"+network, sh)
+        else:
+            address = Address("P2SH32" if network == "MAINNET" else "P2SH32-"+network, sh)
+
         return address.cash_address()
 
     def getDestForScriptHash(self, script_hash):
-        assert len(script_hash) == 20
-        return CScript([OP_HASH160, script_hash, OP_EQUAL])
+        assert (len(script_hash) == 20 or len(script_hash) == 32)
+        if len(script_hash) == 20:
+            return CScript([OP_HASH160, script_hash, OP_EQUAL])
+        else:
+            return CScript([OP_HASH256, script_hash, OP_EQUAL])
 
     def withdrawCoin(self, value: float, addr_to: str, subfee: bool):
         params = [addr_to, value, '', '', subfee, True, True]
@@ -160,9 +175,63 @@ class BCHInterface(BTCInterface):
             return {'txid': txid_hex, 'amount': 0, 'height': block_height}
         return None
 
+    def getLockTxHeight(self, txid, dest_address, bid_amount, rescan_from, find_index: bool = False, vout: int = -1):
+        # Add watchonly address and rescan if required        
+        txid = None
+
+        # first lookup by dest_address
+        if not self.isAddressMine(dest_address, or_watch_only=False):
+            self.importWatchOnlyAddress(dest_address, 'bid')
+            self._log.info('Imported watch-only addr: {}'.format(dest_address))
+            self._log.info('Rescanning {} chain from height: {}'.format(self.coin_name(), rescan_from))
+            self.rpc_wallet('rescanblockchain', [rescan_from])
+
+        return_txid = True
+
+        txns = self.rpc_wallet('listunspent', [0, 9999999, [dest_address, ]])
+
+        for tx in txns:
+            if self.make_int(tx['amount']) == bid_amount:
+                txid = bytes.fromhex(tx['txid'])
+                break
+
+        # try to look up in past transactions
+        if not txid:
+            txns = self.rpc_wallet('listtransactions', ["*", 100000, 0, True])
+            for tx in txns:
+                if self.make_int(tx['amount']) == bid_amount and tx['category'] == 'send' and tx['address'] == dest_address:
+                    txid = bytes.fromhex(tx['txid'])
+                    break
+
+        try:
+            # 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:
+                block_header = self.rpc('getblockheader', [tx['blockhash']])
+                block_height = block_header['height']
+
+            rv = {
+                'depth': 0 if 'confirmations' not in tx else tx['confirmations'],
+                'height': block_height}
+
+        except Exception as e:
+            # self._log.debug('getLockTxHeight gettransaction failed: %s, %s', txid.hex(), str(e))
+            return None
+
+        if find_index:
+            tx_obj = self.rpc('decoderawtransaction', [tx['hex']])
+            rv['index'] = find_vout_for_address_from_txobj(tx_obj, dest_address)
+
+        if return_txid:
+            rv['txid'] = txid.hex()
+            rv['txhex'] = tx['hex']
+
+        return rv
+
     def genScriptLockTxScript(self, ci, Kal: bytes, Kaf: bytes, **kwargs) -> CScript:
-        print("bch genScriptLockTxScript")
-        mining_fee: int = kwargs['mining_fee']
+        mining_fee: int = kwargs['mining_fee'] if 'mining_fee' in kwargs else 1000
         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
@@ -288,7 +357,7 @@ class BCHInterface(BTCInterface):
     def getTxSize(self, tx: CTransaction) -> int:
         return len(tx.serialize_without_witness())
 
-    def getScriptScriptSig(self, script: bytes, ves: bytes) -> bytes:
+    def getScriptScriptSig(self, script: bytes, ves: bytes = None) -> bytes:
         if ves is not None:
             return CScript([ves, script])
         else:
@@ -363,55 +432,25 @@ class BCHInterface(BTCInterface):
         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()
+        # it is not possible to create the refund spend tx without the prior knowledge of the VES which is part of transaction preimage
+        # but it is better and more secure to create a lock spend transaction committing to zero VES than returning static data
+        kwargs['ves'] = bytes(73)
+        return self.createSCLockSpendTx(tx_lock_refund_bytes, script_lock_refund, pkh_refund_to, tx_fee_rate, vkbv, **kwargs)
 
     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)
+        return eck.sign(sha256(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)
+        return self.verifyDataSig(tx_bytes, sig, K)
     
+    def verifyDataSig(self, data: bytes, sig: bytes, K: bytes) -> bool:
+        # simple ecdsa signature verification
+        pubkey = PublicKey(K)
+        return pubkey.verify(sig, sha256(data), hasher=None)
+
     def setTxSignature(self, tx_bytes: bytes, stack) -> bytes:
         return tx_bytes
 
@@ -444,4 +483,296 @@ class BCHInterface(BTCInterface):
 
         # TODO: better script matching, see interfaces/btc.py
 
-        return txid, locked_n
\ No newline at end of file
+        return txid, locked_n
+    
+    def extractScriptLockScriptValuesFromScriptSig(self, script_bytes):
+        signature, nb = decodePushData(script_bytes, 0)
+        unlock_script, _ = decodePushData(script_bytes, nb)
+        mining_fee, out_1, out_2, public_key, timelock = self.extractScriptLockScriptValues(unlock_script)
+
+        return signature, mining_fee, out_1, out_2, public_key, timelock
+
+    def extractScriptLockScriptValues(self, script_bytes):
+        # see BCHInterface.genScriptLockTxScript for reference
+
+        o = 0
+
+        script_len = len(script_bytes)
+        # TODO: stricter script_len checks
+
+        ensure_op(script_bytes[o] == OP_TXINPUTCOUNT); o += 1
+        ensure_op(script_bytes[o] == OP_1); o += 1
+        ensure_op(script_bytes[o] == OP_NUMEQUALVERIFY); o += 1
+        ensure_op(script_bytes[o] == OP_TXOUTPUTCOUNT); o += 1
+        ensure_op(script_bytes[o] == OP_1); o += 1
+        ensure_op(script_bytes[o] == OP_NUMEQUALVERIFY); o += 1
+        mining_fee, nb = decodeScriptNum(script_bytes, o); o += nb
+
+        ensure_op(script_bytes[o] == OP_0); o += 1
+        ensure_op(script_bytes[o] == OP_UTXOVALUE); o += 1
+        ensure_op(script_bytes[o] == OP_0); o += 1
+        ensure_op(script_bytes[o] == OP_OUTPUTVALUE); o += 1
+        ensure_op(script_bytes[o] == OP_SUB); o += 1
+        ensure_op(script_bytes[o] == OP_NUMEQUALVERIFY); o += 1
+
+        ensure_op(script_bytes[o] == OP_0); o += 1
+        ensure_op(script_bytes[o] == OP_UTXOTOKENCATEGORY); o += 1
+        ensure_op(script_bytes[o] == OP_0); o += 1
+        ensure_op(script_bytes[o] == OP_OUTPUTTOKENCATEGORY); o += 1
+
+        ensure_op(script_bytes[o] == OP_EQUALVERIFY); o += 1
+        ensure_op(script_bytes[o] == OP_0); o += 1
+        ensure_op(script_bytes[o] == OP_UTXOTOKENCOMMITMENT); o += 1
+        ensure_op(script_bytes[o] == OP_0); o += 1
+        ensure_op(script_bytes[o] == OP_OUTPUTTOKENCOMMITMENT); o += 1
+        ensure_op(script_bytes[o] == OP_EQUALVERIFY); o += 1
+        ensure_op(script_bytes[o] == OP_0); o += 1
+        ensure_op(script_bytes[o] == OP_UTXOTOKENAMOUNT); o += 1
+        ensure_op(script_bytes[o] == OP_0); o += 1
+        ensure_op(script_bytes[o] == OP_OUTPUTTOKENAMOUNT); o += 1
+        ensure_op(script_bytes[o] == OP_NUMEQUALVERIFY); o += 1
+
+        ensure_op(script_bytes[o] == OP_0); o += 1
+        ensure_op(script_bytes[o] == OP_INPUTSEQUENCENUMBER); o += 1
+        ensure_op(script_bytes[o] == OP_NOTIF); o += 1
+        out_1, nb = decodePushData(script_bytes, o); o += nb
+
+        ensure_op(script_bytes[o] == OP_0); o += 1
+        ensure_op(script_bytes[o] == OP_OUTPUTBYTECODE); o += 1
+        ensure_op(script_bytes[o] == OP_OVER); o += 1
+        ensure_op(script_bytes[o] == OP_EQUALVERIFY); o += 1
+        public_key, nb = decodePushData(script_bytes, o); o += nb
+        ensure_op(script_bytes[o] == OP_CHECKDATASIG); o += 1
+        
+        ensure_op(script_bytes[o] == OP_ELSE); o += 1
+        timelock, nb = decodeScriptNum(script_bytes, o); o += nb
+        ensure_op(script_bytes[o] == OP_CHECKSEQUENCEVERIFY); o += 1
+        ensure_op(script_bytes[o] == OP_DROP); o += 1
+
+        out_2, nb = decodePushData(script_bytes, o); o += nb
+
+        ensure_op(script_bytes[o] == OP_0); o += 1
+        ensure_op(script_bytes[o] == OP_OUTPUTBYTECODE); o += 1
+        ensure_op(script_bytes[o] == OP_EQUAL); o += 1
+
+        ensure_op(script_bytes[o] == OP_ENDIF); o += 1
+
+        ensure(o == script_len, 'Unexpected script length')
+
+        ensure(mining_fee >= 700 and mining_fee <= 10000, 'Bad mining_fee')
+        ensure(len(out_1) == 25, 'Bad out_1')
+        ensure(len(out_2) == 25 or len(out_2) == 35, 'Bad out_2')
+        ensure(len(public_key) == 33, 'Bad public_key')
+        ensure(timelock >= 0, 'Bad timelock')
+
+        return mining_fee, out_1, out_2, public_key, timelock
+
+    def verifySCLockTx(self, tx_bytes, script_out,
+                       swap_value,
+                       Kal, Kaf,
+                       feerate,
+                       check_lock_tx_inputs, vkbv=None,
+                       **kwargs):
+
+        # 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')
+
+        # Check script
+        mining_fee: int = kwargs['mining_fee'] if 'mining_fee' in kwargs else 1000
+        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']
+
+        _mining_fee, _out_1, _out_2, _public_key, _timelock = self.extractScriptLockScriptValues(script_out)
+        ensure(mining_fee == _mining_fee, 'mining mismatch fee')
+        ensure(out_1 == _out_1, 'out_1 mismatch')
+        ensure(out_2 == _out_2, 'out_2 mismatch')
+        ensure(public_key == _public_key, 'public_key mismatch')
+        ensure(timelock == _timelock, 'timelock mismatch')
+
+        return txid, locked_n
+
+    def verifySCLockRefundTx(self, tx_bytes, lock_tx_bytes, script_out,
+                             prevout_id, prevout_n, prevout_seq, prevout_script,
+                             Kal, Kaf, csv_val_expect, swap_value, feerate, vkbv=None, **kwargs):
+        # Verify:
+        #   Must have only one input with correct prevout and sequence
+        #   Must have only one output to the p2wsh of the lock refund script
+        #   Output value must be locked_coin - lock tx fee
+
+        tx = self.loadTx(tx_bytes)
+        txid = self.getTxid(tx)
+        self._log.info('Verifying lock refund tx: {}.'.format(b2h(txid)))
+
+        ensure(tx.nVersion == self.txVersion(), 'Bad version')
+        ensure(tx.nLockTime == 0, 'nLockTime not 0')
+        ensure(len(tx.vin) == 1, 'tx doesn\'t have one input')
+
+        ensure(tx.vin[0].nSequence == prevout_seq, 'Bad input nSequence')
+        ensure(tx.vin[0].scriptSig == self.getScriptScriptSig(prevout_script, None), 'Input scriptsig mismatch')
+        ensure(tx.vin[0].prevout.hash == b2i(prevout_id) and tx.vin[0].prevout.n == prevout_n, 'Input prevout mismatch')
+
+        ensure(len(tx.vout) == 1, 'tx doesn\'t have one output')
+
+        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 script
+        mining_fee: int = kwargs['mining_fee'] if 'mining_fee' in kwargs else 1000
+        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']
+
+        _mining_fee, _out_1, _out_2, _public_key, _timelock = self.extractScriptLockScriptValues(script_out)
+        ensure(mining_fee == _mining_fee, 'mining mismatch fee')
+        ensure(out_1 == _out_1, 'out_1 mismatch')
+        ensure(out_2 == _out_2, 'out_2 mismatch')
+        ensure(public_key == _public_key, 'public_key mismatch')
+        ensure(timelock == _timelock, 'timelock mismatch')
+
+
+        fee_paid = locked_coin - mining_fee
+        assert (fee_paid > 0)
+
+        size = self.getTxSize(tx)
+        vsize = size
+
+        self._log.info('tx amount, vsize, fee: %ld, %ld, %ld', locked_coin, vsize, fee_paid)
+
+        return txid, locked_coin, locked_n
+
+    def verifySCLockRefundSpendTx(self, tx_bytes, lock_refund_tx_bytes,
+                                  lock_refund_tx_id, prevout_script,
+                                  Kal,
+                                  prevout_n, prevout_value, feerate, vkbv=None, **kwargs):
+        # Verify:
+        #   Must have only one input with correct prevout (n is always 0) and sequence
+        #   Must have only one output sending lock refund tx value - fee to leader's address, TODO: follower shouldn't need to verify destination addr
+        tx = self.loadTx(tx_bytes)
+        txid = self.getTxid(tx)
+        self._log.info('Verifying lock refund spend tx: {}.'.format(b2h(txid)))
+
+        ensure(tx.nVersion == self.txVersion(), 'Bad version')
+        ensure(tx.nLockTime == 0, 'nLockTime not 0')
+        ensure(len(tx.vin) == 1, 'tx doesn\'t have one input')
+
+        ensure(tx.vin[0].nSequence == 0, 'Bad input nSequence')
+        ensure(tx.vin[0].scriptSig == self.getScriptScriptSig(prevout_script, bytes(73)), 'Input scriptsig mismatch')
+        ensure(tx.vin[0].prevout.hash == b2i(lock_refund_tx_id) and tx.vin[0].prevout.n == 0, 'Input prevout mismatch')
+
+        ensure(len(tx.vout) == 1, 'tx doesn\'t have one output')
+
+        # Check script
+        mining_fee: int = kwargs['mining_fee'] if 'mining_fee' in kwargs else 1000
+        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']
+
+        _mining_fee, _out_1, _out_2, _public_key, _timelock = self.extractScriptLockScriptValues(prevout_script)
+        ensure(mining_fee == _mining_fee, 'mining mismatch fee')
+        ensure(out_1 == _out_1, 'out_1 mismatch')
+        ensure(out_2 == _out_2, 'out_2 mismatch')
+        ensure(public_key == _public_key, 'public_key mismatch')
+        ensure(timelock == _timelock, 'timelock mismatch')
+
+        tx_value = tx.vout[0].nValue
+        fee_paid = tx_value - mining_fee
+        assert (fee_paid > 0)
+
+        size = self.getTxSize(tx)
+        vsize = size
+
+        self._log.info('tx amount, vsize, fee: %ld, %ld, %ld', tx_value, vsize, fee_paid)
+
+        return True
+
+    def verifySCLockSpendTx(self, tx_bytes,
+                            lock_tx_bytes, lock_tx_script,
+                            a_pkhash_f, feerate, vkbv=None):
+        # Verify:
+        #   Must have only one input with correct prevout (n is always 0) and sequence
+        #   Must have only one output with destination and amount
+
+        tx = self.loadTx(tx_bytes)
+        txid = self.getTxid(tx)
+        self._log.info('Verifying lock spend tx: {}.'.format(b2h(txid)))
+
+        ensure(tx.nVersion == self.txVersion(), 'Bad version')
+        ensure(tx.nLockTime == 0, 'nLockTime not 0')
+        ensure(len(tx.vin) == 1, 'tx doesn\'t have one input')
+
+        lock_tx = self.loadTx(lock_tx_bytes)
+        lock_tx_id = self.getTxid(lock_tx)
+
+        output_script = self.getScriptDest(lock_tx_script)
+        locked_n = findOutput(lock_tx, output_script)
+        ensure(locked_n is not None, 'Output not found in tx')
+        locked_coin = lock_tx.vout[locked_n].nValue
+
+        ensure(tx.vin[0].nSequence == 0, 'Bad input nSequence')
+        ensure(tx.vin[0].scriptSig == self.getScriptScriptSig(lock_tx_script), 'Input scriptsig mismatch')
+
+        # allow for this mismatch in BCH, since the lock txid will get changed after signing
+        # ensure(tx.vin[0].prevout.hash == b2i(lock_tx_id) and tx.vin[0].prevout.n == locked_n, 'Input prevout mismatch')
+
+        ensure(len(tx.vout) == 1, 'tx doesn\'t have one output')
+        p2pkh = self.getScriptForPubkeyHash(a_pkhash_f)
+        ensure(tx.vout[0].scriptPubKey == p2pkh, 'Bad output destination')
+
+        # The value of the lock tx output should already be verified, if the fee is as expected the difference will be the correct amount
+        fee_paid = locked_coin - tx.vout[0].nValue
+        assert (fee_paid > 0)
+
+        size = self.getTxSize(tx)
+        vsize = size
+
+        self._log.info('tx amount, vsize, fee: %ld, %ld, %ld', tx.vout[0].nValue, vsize, fee_paid)
+
+        return True
+
+    def signTxOtVES(self, key_sign: bytes, pubkey_encrypt: bytes, tx_bytes: bytes, input_n: int, prevout_script: bytes, prevout_value: int) -> bytes:
+        _, out_1, _, _, _ = self.extractScriptLockScriptValues(prevout_script)
+        msg = sha256(out_1)
+
+        return ecdsaotves_enc_sign(key_sign, pubkey_encrypt, msg)
+    
+    def decryptOtVES(self, k: bytes, esig: bytes) -> bytes:
+        return ecdsaotves_dec_sig(k, esig)
+
+    def recoverEncKey(self, esig, sig, K):
+        return ecdsaotves_rec_enc_key(K, esig, sig)
+
+    def verifyTxOtVES(self, tx_bytes: bytes, ct: bytes, Ks: bytes, Ke: bytes, input_n: int, prevout_script: bytes, prevout_value):
+        _, out_1, _, _, _ = self.extractScriptLockScriptValues(prevout_script)
+        msg = sha256(out_1)
+
+        return ecdsaotves_enc_verify(Ks, Ke, msg, ct)
+
+    def extractLeaderSig(self, tx_bytes: bytes) -> bytes:
+        tx = self.loadTx(tx_bytes)
+        signature, _, _, _, _, _ = self.extractScriptLockScriptValuesFromScriptSig(tx.vin[0].scriptSig)
+        return signature
diff --git a/basicswap/util/script.py b/basicswap/util/script.py
index e84101c..9e63cf5 100644
--- a/basicswap/util/script.py
+++ b/basicswap/util/script.py
@@ -6,6 +6,7 @@
 
 import struct
 import hashlib
+from basicswap.contrib.test_framework.script import OP_PUSHDATA1, OP_PUSHDATA2, OP_PUSHDATA4, CScriptInvalidError, CScriptTruncatedPushDataError
 from basicswap.script import OpCodes
 
 
@@ -31,6 +32,49 @@ def decodeScriptNum(script_bytes, o):
             v += int(b) << 8 * i
     return (v, 1 + num_len)
 
+def decodePushData(script_bytes, o):
+    datasize = None
+    pushdata_type = None
+    i = o
+    opcode = script_bytes[i]
+    i += 1
+
+    if opcode < OP_PUSHDATA1:
+        pushdata_type = 'PUSHDATA(%d)' % opcode
+        datasize = opcode
+
+    elif opcode == OP_PUSHDATA1:
+        pushdata_type = 'PUSHDATA1'
+        if i >= len(script_bytes):
+            raise CScriptInvalidError('PUSHDATA1: missing data length')
+        datasize = script_bytes[i]
+        i += 1
+
+    elif opcode == OP_PUSHDATA2:
+        pushdata_type = 'PUSHDATA2'
+        if i + 1 >= len(script_bytes):
+            raise CScriptInvalidError('PUSHDATA2: missing data length')
+        datasize = script_bytes[i] + (script_bytes[i + 1] << 8)
+        i += 2
+
+    elif opcode == OP_PUSHDATA4:
+        pushdata_type = 'PUSHDATA4'
+        if i + 3 >= len(script_bytes):
+            raise CScriptInvalidError('PUSHDATA4: missing data length')
+        datasize = script_bytes[i] + (script_bytes[i + 1] << 8) + (script_bytes[i + 2] << 16) + (script_bytes[i + 3] << 24)
+        i += 4
+
+    else:
+        assert False  # shouldn't happen
+
+    data = bytes(script_bytes[i:i + datasize])
+
+    # Check for truncation
+    if len(data) < datasize:
+        raise CScriptTruncatedPushDataError('%s: truncated data' % pushdata_type, data)
+
+    # return data and the number of bytes to skip forward
+    return (data, i + datasize - o)
 
 def getP2SHScriptForHash(p2sh):
     return bytes((OpCodes.OP_HASH160, 0x14)) \
diff --git a/tests/basicswap/test_bch.py b/tests/basicswap/test_bch.py
index 93c732a..dddeea0 100644
--- a/tests/basicswap/test_bch.py
+++ b/tests/basicswap/test_bch.py
@@ -1,11 +1,29 @@
 import unittest
 
-from basicswap.protocols.xmr_swap_1 import XmrBchSwapInterface
+from basicswap.contrib.test_framework.script import CScript
+from basicswap.interface.bch import BCHInterface
 
+bch_lock_spend_tx = '0200000001bfc6bbb47851441c7827059ae337a06aa9064da7f9537eb9243e45766c3dd34c00000000d8473045022100a0161ea14d3b41ed41250c8474fc8ec6ce1cab8df7f401e69ecf77c2ab63d82102207a2a57ddf2ea400e09ea059f3b261da96f5098858b17239931f3cc2fb929bb2a4c8ec3519dc4519d02e80300c600cc949d00ce00d18800cf00d28800d000d39d00cb641976a91481ec21969399d15c26af089d5db437ead066c5ba88ac00cd788821024ffcc0481629866671d89f05f3da813a2aacec1b52e69b8c0c586b665f5d4574ba6752b27523aa20df65a90e9becc316ff5aca44d4e06dfaade56622f32bafa197aba706c5e589758700cd87680000000001251cde06000000001976a91481ec21969399d15c26af089d5db437ead066c5ba88ac00000000'
+bch_lock_script = 'c3519dc4519d02e80300c600cc949d00ce00d18800cf00d28800d000d39d00cb641976a91481ec21969399d15c26af089d5db437ead066c5ba88ac00cd788821024ffcc0481629866671d89f05f3da813a2aacec1b52e69b8c0c586b665f5d4574ba6752b27523aa20df65a90e9becc316ff5aca44d4e06dfaade56622f32bafa197aba706c5e589758700cd8768'
+bch_lock_spend_script = '473045022100a0161ea14d3b41ed41250c8474fc8ec6ce1cab8df7f401e69ecf77c2ab63d82102207a2a57ddf2ea400e09ea059f3b261da96f5098858b17239931f3cc2fb929bb2a4c8ec3519dc4519d02e80300c600cc949d00ce00d18800cf00d28800d000d39d00cb641976a91481ec21969399d15c26af089d5db437ead066c5ba88ac00cd788821024ffcc0481629866671d89f05f3da813a2aacec1b52e69b8c0c586b665f5d4574ba6752b27523aa20df65a90e9becc316ff5aca44d4e06dfaade56622f32bafa197aba706c5e589758700cd8768'
+
+coin_settings = {'rpcport': 0, 'rpcauth': 'none', 'blocks_confirmed': 1, 'conf_target': 1, 'use_segwit': False, 'connection_type': 'rpc'}
 
 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())
+    # def test_generate_script(self):    
+    #     out_1 = bytes.fromhex('a9147171b53baf87efc9c78ffc0e37a78859cebaae4a87')
+    #     out_2 = bytes.fromhex('a9147171b53baf87efc9c78ffc0e37a78859cebaae4a87')
+    #     public_key = bytes.fromhex('03fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556')
+
+    #     ci = BCHInterface(coin_settings, "regtest")
+    #     print(ci.genScriptLockTxScript(None, 1000, out_1, out_2, public_key, 2).hex())
+
+    def test_extractScriptLockScriptValues(self):
+        ci = BCHInterface(coin_settings, "regtest")
+
+        script_bytes = CScript(bytes.fromhex(bch_lock_script))
+        ci.extractScriptLockScriptValues(script_bytes)
+        
+        script_bytes = CScript(bytes.fromhex(bch_lock_spend_script))
+        signature, mining_fee, out_1, out_2, public_key, timelock = ci.extractScriptLockScriptValuesFromScriptSig(script_bytes)
+        print(timelock)
diff --git a/tests/basicswap/test_bch_xmr.py b/tests/basicswap/test_bch_xmr.py
index a410db6..ebd8877 100644
--- a/tests/basicswap/test_bch_xmr.py
+++ b/tests/basicswap/test_bch_xmr.py
@@ -90,7 +90,7 @@ class TestFunctions(BaseTest):
 
         swap_clients = self.swap_clients
         ci = swap_clients[0].ci(Coins.BCH)
-        pi = swap_clients[0].pi(SwapTypes.XMR_BCH_SWAP)
+        pi = swap_clients[0].pi(SwapTypes.XMR_SWAP)
 
         amount: int = ci.make_int(random.uniform(0.1, 2.0), r=1)
 
@@ -108,17 +108,36 @@ class TestFunctions(BaseTest):
 
         mining_fee = 1000
         timelock = 2
-        a_receive = ci.getNewAddress()
-        b_refund = ci.getNewAddress()
-        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()
+        b_receive = ci.getNewAddress()
+        a_refund = 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)
+        refundExtraArgs = dict()
+        lockExtraArgs = dict()
+
+        refundExtraArgs['mining_fee'] = 1000
+        refundExtraArgs['out_1'] = ci.addressToLockingBytecode(a_refund)
+        refundExtraArgs['out_2'] = ci.addressToLockingBytecode(b_receive)
+        refundExtraArgs['public_key'] = B
+        refundExtraArgs['timelock'] = 5
+
+        refund_lock_tx_script = pi.genScriptLockTxScript(ci, A, B, **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.addressToLockingBytecode(b_receive)
+        lockExtraArgs['out_2'] = ci.scriptToP2SH32LockingBytecode(refund_lock_tx_script)
+        lockExtraArgs['public_key'] = A
+        lockExtraArgs['timelock'] = 2
+
+        lock_tx_script = pi.genScriptLockTxScript(ci, A, B, **lockExtraArgs)
 
         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())
+        return
 
         unspents_after = ci.rpc('listunspent')
         assert (len(unspents) > len(unspents_after))
@@ -151,25 +170,25 @@ class TestFunctions(BaseTest):
         assert (wallet_tx_fee == fee_value)
         assert (wallet_tx_fee == expect_fee_int)
 
-        pkh_out = ci.decodeAddress(a_receive)
+        pkh_out = ci.decodeAddress(b_receive)
 
-        msg = sha256(ci.addressToLockingBytecode(a_receive))
+        msg = sha256(ci.addressToLockingBytecode(b_receive))
 
-        # bob creates an adaptor signature for alice and transmits it to her
-        bAdaptorSig = ecdsaotves_enc_sign(b, A, msg)
+        # leader creates an adaptor signature for follower and transmits it to the follower
+        aAdaptorSig = ecdsaotves_enc_sign(a, B, msg)
 
         # alice verifies the adaptor signature
-        assert (ecdsaotves_enc_verify(B, A, msg, bAdaptorSig))
+        assert (ecdsaotves_enc_verify(A, B, msg, aAdaptorSig))
 
         # alice decrypts the adaptor signature
-        bAdaptorSig_dec = ecdsaotves_dec_sig(a, bAdaptorSig)
+        aAdaptorSig_dec = ecdsaotves_dec_sig(b, aAdaptorSig)
 
         fee_info = {}
-        lock_spend_tx = ci.createSCLockSpendTx(lock_tx, lock_tx_script, pkh_out, mining_fee, ves=bAdaptorSig_dec, fee_info=fee_info)
+        lock_spend_tx = ci.createSCLockSpendTx(lock_tx, lock_tx_script, pkh_out, mining_fee, fee_info=fee_info, ves=aAdaptorSig_dec)
         vsize_estimated: int = fee_info['vsize']
 
         tx_decoded = ci.rpc('decoderawtransaction', [lock_spend_tx.hex()])
-        print(tx_decoded)
+        print('lock_spend_tx', lock_spend_tx.hex(), '\n', 'tx_decoded', tx_decoded)
         txid = tx_decoded['txid']
 
         tx_decoded = ci.rpc('decoderawtransaction', [lock_spend_tx.hex()])
diff --git a/tests/basicswap/test_xmr.py b/tests/basicswap/test_xmr.py
index ce8ed05..b80e4eb 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 = 2
-NUM_XMR_NODES = 2
-NUM_BTC_NODES = 2
-NUM_BCH_NODES = 2
-NUM_LTC_NODES = 2
+NUM_NODES = 3
+NUM_XMR_NODES = 3
+NUM_BTC_NODES = 3
+NUM_BCH_NODES = 3
+NUM_LTC_NODES = 3
 TEST_DIR = cfg.TEST_DATADIRS
 
 XMR_BASE_P2P_PORT = 17792
@@ -692,7 +692,7 @@ class BaseTest(unittest.TestCase):
         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))
+            bchCli('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: