From 221d962c12750db7f51fb878911baf9e557aa30d Mon Sep 17 00:00:00 2001 From: gerlofvanek Date: Fri, 29 May 2026 18:36:05 +0200 Subject: [PATCH] Fix: Refund-path / Maturity checks. --- basicswap/basicswap.py | 45 ++++++++++++++++++++++------- basicswap/interface/btc.py | 59 +++++++++++++++++++++++++++++++++++--- 2 files changed, 89 insertions(+), 15 deletions(-) diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index 337d3ba..982891c 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -5613,15 +5613,16 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): ) # Check non-bip68 final - try: - txid = ci_from.publishTx(bid.initiate_txn_refund) - self.log.error( - f"Submit refund_txn unexpectedly worked {self.logIDT(bytes.fromhex(txid))}" - ) - except Exception as ex: - if ci_from.isTxNonFinalError(str(ex)) is False: - self.log.error(f"Submit refund_txn unexpected error: {ex}") - raise ex + if not ci_from.useBackend(): + try: + txid = ci_from.publishTx(bid.initiate_txn_refund) + self.log.error( + f"Submit refund_txn unexpectedly worked {self.logIDT(bytes.fromhex(txid))}" + ) + except Exception as ex: + if ci_from.isTxNonFinalError(str(ex)) is False: + self.log.error(f"Submit refund_txn unexpected error: {ex}") + raise ex if txid is not None: msg_buf = BidAcceptMessage() @@ -7686,7 +7687,16 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): self.saveBidInSession(bid_id, bid, cursor, xmr_swap) self.commitDB() - if TxTypes.XMR_SWAP_A_LOCK_REFUND_SWIPE not in bid.txns: + if ( + TxTypes.XMR_SWAP_A_LOCK_REFUND_SWIPE not in bid.txns + and refund_tx.block_height is not None + and ci_from.isCsvLockMature( + offer.lock_type, + xmr_offer.lock_time_2, + refund_tx.block_height, + refund_tx.block_time, + ) + ): try: if self.haveDebugInd( bid.bid_id, @@ -7782,6 +7792,13 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): if ( len(xmr_swap.al_lock_refund_tx_sig) > 0 and len(xmr_swap.af_lock_refund_tx_sig) > 0 + and bid.xmr_a_lock_tx is not None + and ci_from.isCsvLockMature( + offer.lock_type, + xmr_offer.lock_time_1, + bid.xmr_a_lock_tx.block_height, + bid.xmr_a_lock_tx.block_time, + ) ): try: @@ -8473,6 +8490,9 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): if ( bid.getITxState() in (TxStates.TX_SENT, TxStates.TX_CONFIRMED) and bid.initiate_txn_refund is not None + and ci_from.isAbsLockTimeMature( + ci_from.loadTx(bid.initiate_txn_refund).nLockTime + ) ): try: txid = ci_from.publishTx(bid.initiate_txn_refund) @@ -8496,6 +8516,9 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): if ( bid.getPTxState() in (TxStates.TX_SENT, TxStates.TX_CONFIRMED) and bid.participate_txn_refund is not None + and ci_to.isAbsLockTimeMature( + ci_to.loadTx(bid.participate_txn_refund).nLockTime + ) ): try: txid = ci_to.publishTx(bid.participate_txn_refund) @@ -8511,7 +8534,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): ) # State will update when spend is detected except Exception as ex: - if ci_to.isTxNonFinalError(str(ex)): + if ci_to.isTxNonFinalError(str(ex)) is False: self.log.warning( f"Error trying to submit participate refund txn: {ex}" ) diff --git a/basicswap/interface/btc.py b/basicswap/interface/btc.py index 5704c76..3b4b1c9 100644 --- a/basicswap/interface/btc.py +++ b/basicswap/interface/btc.py @@ -502,6 +502,57 @@ class BTCInterface(Secp256k1Interface): return height return self.rpc("getblockcount") + def getChainMedianTime(self) -> int: + if self.useBackend(): + height = self._backend.getBlockHeight() + times = [] + for h in range(max(0, height - 10), height + 1): + header = self._getBlockHeaderFromHeightElectrum(h) + times.append(header["time"]) + times.sort() + return times[len(times) // 2] + return self.rpc("getblockchaininfo")["mediantime"] + + def isCsvLockMature( + self, + lock_type: int, + encoded_sequence: int, + parent_block_height: Optional[int], + parent_block_time: Optional[int], + chain_height: Optional[int] = None, + chain_mtp: Optional[int] = None, + ) -> bool: + if parent_block_height is None or parent_block_height < 1: + return False + lock_value: int = self.decodeSequence(encoded_sequence) + if lock_type == TxLockTypes.SEQUENCE_LOCK_BLOCKS: + if chain_height is None: + chain_height = self.getChainHeight() + return chain_height + 1 >= parent_block_height + lock_value + if lock_type == TxLockTypes.SEQUENCE_LOCK_TIME: + if parent_block_time is None or parent_block_time < 1: + return False + if chain_mtp is None: + chain_mtp = self.getChainMedianTime() + return chain_mtp >= parent_block_time + lock_value + raise ValueError(f"Unknown lock type {lock_type}") + + def isAbsLockTimeMature( + self, + nlocktime: int, + chain_height: Optional[int] = None, + chain_mtp: Optional[int] = None, + ) -> bool: + if nlocktime == 0: + return True + if nlocktime < 500000000: + if chain_height is None: + chain_height = self.getChainHeight() + return chain_height + 1 >= nlocktime + if chain_mtp is None: + chain_mtp = self.getChainMedianTime() + return chain_mtp >= nlocktime + def getMempoolTx(self, txid): if self._connection_type == "electrum": backend = self.getBackend() @@ -4370,11 +4421,11 @@ class BTCInterface(Secp256k1Interface): return "Transaction already in block chain" in err_str def isTxNonFinalError(self, err_str: str) -> bool: + err_lower = err_str.lower() return ( - "non-BIP68-final" in err_str - or "non-final" in err_str - or "Missing inputs" in err_str - or "bad-txns-inputs-missingorspent" in err_str + "non-bip68-final" in err_lower + or "non-final" in err_lower + or "locktime requirement not satisfied" in err_lower ) def combine_non_segwit_prevouts(self):