From c66160fb09e16a15489e7cba017eb29a7e465850 Mon Sep 17 00:00:00 2001 From: tecnovert Date: Sun, 31 Jan 2021 14:26:32 +0200 Subject: [PATCH] Add more event log types. Auto accept only bids of the exact offer amount. Retry sending lock B refund tx. --- basicswap/__init__.py | 2 +- basicswap/basicswap.py | 90 ++++++++++++++++--- basicswap/interface_btc.py | 4 + basicswap/interface_xmr.py | 2 +- .../basicswap/extended/test_xmr_persistent.py | 29 +++++- tests/basicswap/test_xmr.py | 1 - 6 files changed, 110 insertions(+), 18 deletions(-) diff --git a/basicswap/__init__.py b/basicswap/__init__.py index 2dd6ee7..bea1cf8 100644 --- a/basicswap/__init__.py +++ b/basicswap/__init__.py @@ -1,3 +1,3 @@ name = "basicswap" -__version__ = "0.0.15" +__version__ = "0.0.16" diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index 0db6c61..996acbd 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -200,6 +200,12 @@ class EventLogTypes(IntEnum): LOCK_TX_B_SEEN = auto() LOCK_TX_B_CONFIRMED = auto() DEBUG_TWEAK_APPLIED = auto() + FAILED_TX_B_REFUND = auto() + LOCK_TX_B_INVALID = auto() + LOCK_TX_A_REFUND_TX_PUBLISHED = auto() + LOCK_TX_A_REFUND_SPEND_TX_PUBLISHED = auto() + LOCK_TX_A_REFUND_SWIPE_TX_PUBLISHED = auto() + LOCK_TX_B_REFUND_TX_PUBLISHED = auto() class XmrSplitMsgTypes(IntEnum): @@ -327,25 +333,37 @@ def getLockName(lock_type): def describeEventEntry(event_type, event_msg): if event_type == EventLogTypes.FAILED_TX_B_LOCK_PUBLISH: - return 'Failed to publish lock tx b' + return 'Failed to publish lock tx B' if event_type == EventLogTypes.FAILED_TX_B_LOCK_PUBLISH: - return 'Failed to publish lock tx b' + return 'Failed to publish lock tx B' if event_type == EventLogTypes.LOCK_TX_A_PUBLISHED: - return 'Lock tx a published' + return 'Lock tx A published' if event_type == EventLogTypes.LOCK_TX_B_PUBLISHED: - return 'Lock tx b published' + return 'Lock tx B published' if event_type == EventLogTypes.FAILED_TX_B_SPEND: - return 'Failed to publish lock tx b spend' + return 'Failed to publish lock tx B spend' if event_type == EventLogTypes.LOCK_TX_A_SEEN: - return 'Lock tx a seen in chain' + return 'Lock tx A seen in chain' if event_type == EventLogTypes.LOCK_TX_A_CONFIRMED: - return 'Lock tx a confirmed in chain' + return 'Lock tx A confirmed in chain' if event_type == EventLogTypes.LOCK_TX_B_SEEN: - return 'Lock tx b seen in chain' + return 'Lock tx B seen in chain' if event_type == EventLogTypes.LOCK_TX_B_CONFIRMED: - return 'Lock tx b confirmed in chain' + return 'Lock tx B confirmed in chain' if event_type == EventLogTypes.DEBUG_TWEAK_APPLIED: return 'Debug tweak applied ' + event_msg + if event_type == EventLogTypes.FAILED_TX_B_REFUND: + return 'Failed to publish lock tx B refund' + if event_type == EventLogTypes.LOCK_TX_B_INVALID: + return 'Detected invalid lock Tx B' + if event_type == EventLogTypes.LOCK_TX_A_REFUND_TX_PUBLISHED: + return 'Lock tx A refund tx published' + if event_type == EventLogTypes.LOCK_TX_A_REFUND_SPEND_TX_PUBLISHED: + return 'Lock tx A refund spend tx published' + if event_type == EventLogTypes.LOCK_TX_A_REFUND_SWIPE_TX_PUBLISHED: + return 'Lock tx A refund swipe tx published' + if event_type == EventLogTypes.LOCK_TX_B_REFUND_TX_PUBLISHED: + return 'Lock tx B refund tx published' def getExpectedSequence(lockType, lockVal, coin_type): @@ -2693,6 +2711,7 @@ class BasicSwap(BaseApp): if TxTypes.XMR_SWAP_A_LOCK_REFUND_SPEND not in bid.txns: try: txid = ci_from.publishTx(xmr_swap.a_lock_refund_spend_tx) + self.logBidEvent(bid, EventLogTypes.LOCK_TX_A_REFUND_SPEND_TX_PUBLISHED, '', session) self.log.info('Submitted coin a lock refund spend tx for bid {}'.format(bid_id.hex())) bid.txns[TxTypes.XMR_SWAP_A_LOCK_REFUND_SPEND] = SwapTx( @@ -2714,6 +2733,7 @@ class BasicSwap(BaseApp): if TxTypes.XMR_SWAP_A_LOCK_REFUND_SWIPE not in bid.txns: try: txid = ci_from.publishTx(xmr_swap.a_lock_refund_swipe_tx) + self.logBidEvent(bid, EventLogTypes.LOCK_TX_A_REFUND_SWIPE_TX_PUBLISHED, '', session) self.log.info('Submitted coin a lock refund swipe tx for bid {}'.format(bid_id.hex())) bid.txns[TxTypes.XMR_SWAP_A_LOCK_REFUND_SWIPE] = SwapTx( bid_id=bid_id, @@ -2742,6 +2762,7 @@ class BasicSwap(BaseApp): txid = ci_from.publishTx(xmr_swap.a_lock_refund_tx) self.log.info('Submitted coin a lock refund tx for bid {}'.format(bid_id.hex())) + self.logBidEvent(bid, EventLogTypes.LOCK_TX_A_REFUND_TX_PUBLISHED, '', session) bid.txns[TxTypes.XMR_SWAP_A_LOCK_REFUND] = SwapTx( bid_id=bid_id, tx_type=TxTypes.XMR_SWAP_A_LOCK_REFUND, @@ -2789,6 +2810,13 @@ class BasicSwap(BaseApp): utxo = utxos[0] if not bid.xmr_a_lock_tx.chain_height and utxo['height'] != 0: self.logBidEvent(bid, EventLogTypes.LOCK_TX_A_SEEN, '', session) + + block_header = ci_from.getBlockHeaderFromHeight(utxo['height']) + + bid.xmr_a_lock_tx.block_hash = bytes.fromhex(block_header['hash']) + bid.xmr_a_lock_tx.block_height = block_header['height'] + bid.xmr_a_lock_tx.block_time = block_header['time'] # Or median_time? + bid_changed = True if bid.xmr_a_lock_tx.chain_height != utxo['height'] and utxo['height'] != 0: bid.xmr_a_lock_tx.chain_height = utxo['height'] @@ -2817,7 +2845,12 @@ class BasicSwap(BaseApp): bid_changed = False # Have to use findTxB instead of relying on the first seen height to detect chain reorgs found_tx = ci_to.findTxB(xmr_swap.vkbv, xmr_swap.pkbs, bid.amount_to, ci_to.blocks_confirmed, xmr_swap.b_restore_height) - if found_tx is not None: + + if isinstance(found_tx, int) and found_tx == -1: + if self.countBidEvents(bid, EventLogTypes.LOCK_TX_B_INVALID, session) < 1: + self.logBidEvent(bid, EventLogTypes.LOCK_TX_B_INVALID, 'Detected invalid lock tx B', session) + bid_changed = True + elif found_tx is not None: if bid.xmr_b_lock_tx is None or not bid.xmr_b_lock_tx.chain_height: self.logBidEvent(bid, EventLogTypes.LOCK_TX_B_SEEN, '', session) if bid.xmr_b_lock_tx is None: @@ -3581,6 +3614,7 @@ class BasicSwap(BaseApp): assert(msg['to'] == offer.addr_from), 'Received on incorrect address' assert(now <= offer.expire_at), 'Offer expired' assert(bid_data.amount >= offer.min_bid_amount), 'Bid amount below minimum' + assert(bid_data.amount <= offer.amount_from), 'Bid amount above offer amount' assert(now <= msg['sent'] + bid_data.time_valid), 'Bid expired' # TODO: Allow higher bids @@ -3646,6 +3680,8 @@ class BasicSwap(BaseApp): if offer.auto_accept_bids: if self.countAcceptedBids(offer_id) > 0: self.log.info('Not auto accepting bid %s, already have', bid_id.hex()) + elif bid_data.amount != offer.amount_from: + self.log.info('Not auto accepting bid %s, want exact amount match', bid_id.hex()) else: delay = random.randrange(self.min_delay_event, self.max_delay_event) self.log.info('Auto accepting bid %s in %d seconds', bid_id.hex(), delay) @@ -3766,6 +3802,8 @@ class BasicSwap(BaseApp): if offer.auto_accept_bids: if self.countAcceptedBids(bid.offer_id) > 0: self.log.info('Not auto accepting bid %s, already have', bid.bid_id.hex()) + elif bid.amount != offer.amount_from: + self.log.info('Not auto accepting bid %s, want exact amount match', bid_id.hex()) else: delay = random.randrange(self.min_delay_event, self.max_delay_event) self.log.info('Auto accepting xmr bid %s in %d seconds', bid.bid_id.hex(), delay) @@ -3842,7 +3880,15 @@ class BasicSwap(BaseApp): ci_from = self.ci(Coins(offer.coin_from)) ci_to = self.ci(Coins(offer.coin_to)) + assert(offer.state == OfferStates.OFFER_RECEIVED), 'Bad offer state' + assert(msg['to'] == offer.addr_from), 'Received on incorrect address' + assert(now <= offer.expire_at), 'Offer expired' + assert(bid_data.amount >= offer.min_bid_amount), 'Bid amount below minimum' + assert(bid_data.amount <= offer.amount_from), 'Bid amount above offer amount' + assert(now <= msg['sent'] + bid_data.time_valid), 'Bid expired' + self.log.debug('TODO: xmr bid validation') + assert(ci_to.verifyKey(bid_data.kbvf)) assert(ci_from.verifyPubkey(bid_data.pkaf)) @@ -4310,7 +4356,29 @@ class BasicSwap(BaseApp): address_to = ci_to.getMainWalletAddress() - txid = ci_to.spendBLockTx(address_to, xmr_swap.vkbv, vkbs, bid.amount_to, xmr_offer.b_fee_rate, xmr_swap.b_restore_height) + try: + txid = ci_to.spendBLockTx(address_to, xmr_swap.vkbv, vkbs, bid.amount_to, xmr_offer.b_fee_rate, xmr_swap.b_restore_height) + self.log.debug('Submitted lock B refund txn %s to %s chain for bid %s', txid.hex(), ci_to.coin_name(), bid_id.hex()) + self.logBidEvent(bid, EventLogTypes.LOCK_TX_B_REFUND_TX_PUBLISHED, '', session) + except Exception as ex: + # TODO: Make min-conf 10? + error_msg = 'spendBLockTx refund failed for bid {} with error {}'.format(bid_id.hex(), str(ex)) + num_retries = self.countBidEvents(bid, EventLogTypes.FAILED_TX_B_REFUND, session) + if num_retries > 0: + error_msg += ', retry no. {}'.format(num_retries) + self.log.error(error_msg) + + str_error = str(ex) + if num_retries < 100 and 'Invalid unlocked_balance' in str_error: + delay = random.randrange(self.min_delay_retry, self.max_delay_retry) + self.log.info('Retrying sending xmr swap chain B refund tx for bid %s in %d seconds', bid_id.hex(), delay) + self.createEventInSession(delay, EventTypes.RECOVER_XMR_SWAP_LOCK_TX_B, bid_id, session) + else: + self.setBidError(bid_id, bid, 'spendBLockTx for refund failed: ' + str(ex), save_bid=False) + self.saveBidInSession(bid_id, bid, session, xmr_swap, save_in_progress=offer) + + self.logBidEvent(bid, EventLogTypes.FAILED_TX_B_REFUND, str_error, session) + return bid.xmr_b_lock_tx.spend_txid = txid diff --git a/basicswap/interface_btc.py b/basicswap/interface_btc.py index bfc1f12..1db63da 100644 --- a/basicswap/interface_btc.py +++ b/basicswap/interface_btc.py @@ -150,6 +150,10 @@ class BTCInterface(CoinInterface): def getMempoolTx(self, txid): return self.rpc_callback('getrawtransaction', [txid.hex()]) + def getBlockHeaderFromHeight(self, height): + block_hash = self.rpc_callback('getblockhash', [height]) + return self.rpc_callback('getblockheader', [block_hash]) + def initialiseWallet(self, key_bytes): wif_prefix = chainparams[self.coin_type()][self._network]['key_prefix'] key_wif = toWIF(wif_prefix, key_bytes) diff --git a/basicswap/interface_xmr.py b/basicswap/interface_xmr.py index cf9f54c..e84c6da 100644 --- a/basicswap/interface_xmr.py +++ b/basicswap/interface_xmr.py @@ -268,7 +268,7 @@ class XMRInterface(CoinInterface): return {'txid': transfer['tx_hash'], 'amount': transfer['amount'], 'height': 0 if 'block_height' not in transfer else transfer['block_height']} else: self._log.warning('Incorrect amount detected for coin b lock txn: {}'.format(transfer['tx_hash'])) - + return -1 return None def waitForLockTxB(self, kbv, Kbs, cb_swap_value, cb_block_confirmed, restore_height): diff --git a/tests/basicswap/extended/test_xmr_persistent.py b/tests/basicswap/extended/test_xmr_persistent.py index a2d2753..9589e05 100644 --- a/tests/basicswap/extended/test_xmr_persistent.py +++ b/tests/basicswap/extended/test_xmr_persistent.py @@ -101,11 +101,19 @@ def updateThread(cls): try: if cls.btc_addr is not None: callbtcrpc(0, 'generatetoaddress', [1, cls.btc_addr]) + except Exception as e: + print('updateThread error', str(e)) + cls.delay_event.wait(random.randrange(cls.update_min, cls.update_max)) + + +def updateThreadXmr(cls): + while not cls.delay_event.is_set(): + try: if cls.xmr_addr is not None: callrpc_xmr_na(XMR_BASE_RPC_PORT + 1, 'generateblocks', {'wallet_address': cls.xmr_addr, 'amount_of_blocks': 1}) except Exception as e: - print('updateThread error', str(e)) - cls.delay_event.wait(random.randrange(1, 4)) + print('updateThreadXmr error', str(e)) + cls.delay_event.wait(random.randrange(cls.xmr_update_min, cls.xmr_update_max)) class Test(unittest.TestCase): @@ -113,8 +121,15 @@ class Test(unittest.TestCase): def setUpClass(cls): super(Test, cls).setUpClass() + cls.update_min = int(os.getenv('UPDATE_THREAD_MIN_WAIT', '1')) + cls.update_max = cls.update_min * 4 + + cls.xmr_update_min = int(os.getenv('XMR_UPDATE_THREAD_MIN_WAIT', '1')) + cls.xmr_update_max = cls.xmr_update_min * 4 + cls.delay_event = threading.Event() cls.update_thread = None + cls.update_thread_xmr = None cls.processes = [] cls.btc_addr = None cls.xmr_addr = None @@ -204,8 +219,8 @@ class Test(unittest.TestCase): settings['min_delay_event'] = 1 settings['max_delay_event'] = 4 - settings['min_delay_retry'] = 10 - settings['max_delay_retry'] = 20 + settings['min_delay_retry'] = 15 + settings['max_delay_retry'] = 30 settings['min_sequence_lock_seconds'] = 60 settings['check_progress_seconds'] = 5 @@ -270,6 +285,9 @@ class Test(unittest.TestCase): self.update_thread = threading.Thread(target=updateThread, args=(self,)) self.update_thread.start() + self.update_thread_xmr = threading.Thread(target=updateThreadXmr, args=(self,)) + self.update_thread_xmr.start() + # Wait for height, or sequencelock is thrown off by genesis blocktime num_blocks = 3 logging.info('Waiting for Particl chain height %d', num_blocks) @@ -291,11 +309,14 @@ class Test(unittest.TestCase): cls.delay_event.set() if cls.update_thread: cls.update_thread.join() + if cls.update_thread_xmr: + cls.update_thread_xmr.join() for p in cls.processes: p.terminate() for p in cls.processes: p.join() cls.update_thread = None + cls.update_thread_xmr = None cls.processes = [] def test_persistent(self): diff --git a/tests/basicswap/test_xmr.py b/tests/basicswap/test_xmr.py index 18d4190..e11c773 100644 --- a/tests/basicswap/test_xmr.py +++ b/tests/basicswap/test_xmr.py @@ -632,7 +632,6 @@ class Test(unittest.TestCase): assert(make_int(js_w1_after['2']['balance'], scale=8, r=1) - (make_int(js_w1_before['2']['balance'], scale=8, r=1) + amt_1) < 1000) - def test_07_revoke_offer(self): logging.info('---------- Test offer revocaction') swap_clients = self.swap_clients