From eff52352057789ad1ca71414ee4eca1c49a27c86 Mon Sep 17 00:00:00 2001 From: tecnovert Date: Fri, 12 Nov 2021 16:36:10 +0200 Subject: [PATCH] xmr: Check for existing spend of lock tx --- basicswap/basicswap.py | 43 +++++++++++++++++++++++-------------- basicswap/basicswap_util.py | 5 ++++- basicswap/interface_part.py | 4 ++++ basicswap/interface_xmr.py | 24 ++++++++++++++------- doc/release-notes.md | 4 ++++ 5 files changed, 55 insertions(+), 25 deletions(-) diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index d7076c7..c70db9e 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -35,6 +35,7 @@ from .interface_passthrough_btc import PassthroughBTCInterface from . import __version__ from .util import ( + TemporaryError, pubkeyToAddress, format_amount, format_timestamp, @@ -197,7 +198,6 @@ class BasicSwap(BaseApp): self._updating_wallets_info = {} self._last_updated_wallets_info = 0 - # TODO: Adjust ranges self.min_delay_event = self.settings.get('min_delay_event', 10) self.max_delay_event = self.settings.get('max_delay_event', 60) @@ -2903,6 +2903,14 @@ class BasicSwap(BaseApp): self.process_XMR_SWAP_A_LOCK_tx_spend(bid_id, xmr_swap.a_lock_spend_tx_id.hex(), txn_hex) except Exception as e: self.log.debug('getrawtransaction lock spend tx failed: %s', str(e)) + elif state == BidStates.XMR_SWAP_SCRIPT_TX_REDEEMED: + if bid.was_received and self.countQueuedEvents(session, bid_id, EventTypes.REDEEM_XMR_SWAP_LOCK_TX_B) < 1: + bid.setState(BidStates.SWAP_DELAYING) + delay = random.randrange(self.min_delay_event, self.max_delay_event) + self.log.info('Redeeming coin b lock tx for bid %s in %d seconds', bid_id.hex(), delay) + self.createEventInSession(delay, EventTypes.REDEEM_XMR_SWAP_LOCK_TX_B, bid_id, session) + self.saveBidInSession(bid_id, bid, session, xmr_swap) + session.commit() elif state == BidStates.XMR_SWAP_NOSCRIPT_TX_REDEEMED: txid_hex = bid.xmr_b_lock_tx.spend_txid.hex() @@ -3210,18 +3218,13 @@ class BasicSwap(BaseApp): if not bid.was_received: bid.setState(BidStates.SWAP_COMPLETED) - if bid.was_received: - bid.setState(BidStates.SWAP_DELAYING) - delay = random.randrange(self.min_delay_event, self.max_delay_event) - self.log.info('Redeeming coin b lock tx for bid %s in %d seconds', bid_id.hex(), delay) - self.createEventInSession(delay, EventTypes.REDEEM_XMR_SWAP_LOCK_TX_B, bid_id, session) else: # 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: self.log.debug('Coin a lock tx spent by lock refund tx.') - pass + bid.setState(BidStates.XMR_SWAP_SCRIPT_TX_PREREFUND) else: self.setBidError(bid.bid_id, bid, 'Unexpected txn spent coin a lock tx: {}'.format(spend_txid_hex), save_bid=False) @@ -3381,6 +3384,10 @@ class BasicSwap(BaseApp): finally: self.mxDB.release() + def countQueuedEvents(self, session, bid_id, event_type): + q = session.query(EventQueue).filter(sa.and_(EventQueue.active_ind == 1, EventQueue.linked_id == bid_id, EventQueue.event_type == event_type)) + return q.count() + def checkEvents(self): self.mxDB.acquire() now = int(time.time()) @@ -4364,17 +4371,22 @@ class BasicSwap(BaseApp): ci_from = self.ci(coin_from) ci_to = self.ci(coin_to) - # Extract the leader's decrypted signature and use it to recover the follower's privatekey - xmr_swap.al_lock_spend_tx_sig = ci_from.extractLeaderSig(xmr_swap.a_lock_spend_tx) + try: + chain_height = ci_to.getChainHeight() + lock_tx_depth = (chain_height - bid.xmr_b_lock_tx.chain_height) + 1 + if lock_tx_depth < ci_to.depth_spendable(): + raise TemporaryError(f'Chain B lock tx depth {lock_tx_depth} < required for spending.') + + # Extract the leader's decrypted signature and use it to recover the follower's privatekey + xmr_swap.al_lock_spend_tx_sig = ci_from.extractLeaderSig(xmr_swap.a_lock_spend_tx) - kbsf = ci_from.recoverEncKey(xmr_swap.al_lock_spend_tx_esig, xmr_swap.al_lock_spend_tx_sig, xmr_swap.pkasf) - assert(kbsf is not None) + kbsf = ci_from.recoverEncKey(xmr_swap.al_lock_spend_tx_esig, xmr_swap.al_lock_spend_tx_sig, xmr_swap.pkasf) + assert(kbsf is not None) - for_ed25519 = True if coin_to == Coins.XMR else False - kbsl = self.getPathKey(coin_from, coin_to, bid.created_at, xmr_swap.contract_count, 2, for_ed25519) - vkbs = ci_to.sumKeys(kbsl, kbsf) + for_ed25519 = True if coin_to == Coins.XMR else False + kbsl = self.getPathKey(coin_from, coin_to, bid.created_at, xmr_swap.contract_count, 2, for_ed25519) + vkbs = ci_to.sumKeys(kbsl, kbsf) - try: if coin_to == Coins.XMR: address_to = self.getCachedMainWalletAddress(ci_to) else: @@ -4383,7 +4395,6 @@ class BasicSwap(BaseApp): self.log.debug('Submitted lock B spend txn %s to %s chain for bid %s', txid.hex(), ci_to.coin_name(), bid_id.hex()) self.logBidEvent(bid.bid_id, EventLogTypes.LOCK_TX_B_SPEND_TX_PUBLISHED, '', session) except Exception as ex: - # TODO: Make min-conf 10? error_msg = 'spendBLockTx failed for bid {} with error {}'.format(bid_id.hex(), str(ex)) num_retries = self.countBidEvents(bid, EventLogTypes.FAILED_TX_B_SPEND, session) if num_retries > 0: diff --git a/basicswap/basicswap_util.py b/basicswap/basicswap_util.py index e3aa942..526eb5f 100644 --- a/basicswap/basicswap_util.py +++ b/basicswap/basicswap_util.py @@ -73,6 +73,7 @@ class BidStates(IntEnum): XMR_SWAP_NOSCRIPT_COIN_LOCKED = auto() XMR_SWAP_LOCK_RELEASED = auto() XMR_SWAP_SCRIPT_TX_REDEEMED = auto() + XMR_SWAP_SCRIPT_TX_PREREFUND = auto() # script txo moved into pre-refund tx XMR_SWAP_NOSCRIPT_TX_REDEEMED = auto() XMR_SWAP_NOSCRIPT_TX_RECOVERED = auto() XMR_SWAP_FAILED_REFUNDED = auto() @@ -279,7 +280,7 @@ def describeEventEntry(event_type, event_msg): if event_type == EventLogTypes.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: ' + event_msg if event_type == EventLogTypes.LOCK_TX_A_SEEN: return 'Lock tx A seen in chain' if event_type == EventLogTypes.LOCK_TX_A_CONFIRMED: @@ -370,4 +371,6 @@ def isActiveBidState(state): return True if state == BidStates.XMR_SWAP_SCRIPT_TX_REDEEMED: return True + if state == BidStates.XMR_SWAP_SCRIPT_TX_PREREFUND: + return True return False diff --git a/basicswap/interface_part.py b/basicswap/interface_part.py index a4f5ea2..dbeae82 100644 --- a/basicswap/interface_part.py +++ b/basicswap/interface_part.py @@ -607,6 +607,10 @@ class PARTInterfaceAnon(PARTInterface): def balance_type(): return BalanceTypes.ANON + @staticmethod + def depth_spendable() -> int: + return 12 + def coin_name(self): return super().coin_name() + ' Anon' diff --git a/basicswap/interface_xmr.py b/basicswap/interface_xmr.py index 4451974..07f64e2 100644 --- a/basicswap/interface_xmr.py +++ b/basicswap/interface_xmr.py @@ -60,6 +60,10 @@ class XMRInterface(CoinInterface): def nbK() -> int: # No. of bytes requires to encode a public key return 32 + @staticmethod + def depth_spendable() -> int: + return 10 + def __init__(self, coin_settings, network, swap_client=None): super().__init__(network) self.rpc_cb = make_xmr_rpc_func(coin_settings['rpcport'], host=coin_settings.get('rpchost', '127.0.0.1')) @@ -396,15 +400,18 @@ class XMRInterface(CoinInterface): self._log.info('generate_from_keys %s', dumpj(rv)) self.rpc_wallet_cb('open_wallet', {'filename': wallet_filename}) - # For a while after opening the wallet rpc cmds return empty data - for i in range(10): - rv = self.rpc_wallet_cb('get_balance') - print('get_balance', rv) - if rv['balance'] >= cb_swap_value: - break - - time.sleep(1 + i) + self.rpc_wallet_cb('refresh') + rv = self.rpc_wallet_cb('get_balance') if rv['balance'] < cb_swap_value: + self._log.warning('Balance is too low, checking for existing spend.') + txns = self.rpc_wallet_cb('get_transfers', {'out': True})['out'] + print(txns, txns) + if len(txns) > 0: + txid = txns[0]['txid'] + self._log.warning(f'spendBLockTx detected spending tx: {txid}.') + if txns[0]['address'] == address_b58: + return bytes.fromhex(txid) + self._log.error('wallet {} balance {}, expected {}'.format(wallet_filename, rv['balance'], cb_swap_value)) raise TemporaryError('Invalid balance') if rv['unlocked_balance'] < cb_swap_value: @@ -414,6 +421,7 @@ class XMRInterface(CoinInterface): params = {'address': address_to} if self._fee_priority > 0: params['priority'] = self._fee_priority + rv = self.rpc_wallet_cb('sweep_all', params) print('sweep_all', rv) diff --git a/doc/release-notes.md b/doc/release-notes.md index 52f7bf0..aed113c 100644 --- a/doc/release-notes.md +++ b/doc/release-notes.md @@ -5,6 +5,10 @@ - Added protocol version to order and bid messages - Moved chain start heights to bid. - Avoid scantxoutset for decred style swaps +- xmr: spend chain B lock tx will look for existing spends +- xmrswaps: + - Setting state to 'Script tx redeemed' will trigger an attempt to redeem the scriptless lock tx. + - Node will wait for the chain B lock tx to reach a spendable depth before attempting to spend. 0.0.25