diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index 42a6e9d..ccd6be8 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -3828,10 +3828,13 @@ class BasicSwap(BaseApp): if TxTypes.XMR_SWAP_A_LOCK_REFUND in bid.txns: refund_tx = bid.txns[TxTypes.XMR_SWAP_A_LOCK_REFUND] if was_received: - if bid.debug_ind == DebugTypes.BID_DONT_SPEND_COIN_A_LOCK_REFUND: + if bid.debug_ind in (DebugTypes.BID_DONT_SPEND_COIN_A_LOCK_REFUND, DebugTypes.BID_DONT_SPEND_COIN_A_LOCK_REFUND2): self.log.debug('Adaptor-sig bid %s: Stalling bid for testing: %d.', bid_id.hex(), bid.debug_ind) - bid.setState(BidStates.BID_STALLED_FOR_TEST) - rv = True + if bid.debug_ind == DebugTypes.BID_DONT_SPEND_COIN_A_LOCK_REFUND: + bid.setState(BidStates.BID_STALLED_FOR_TEST) + rv = True + else: + bid.setState(BidStates.BID_STALLED_FOR_TEST_TYPE2) self.saveBidInSession(bid_id, bid, session, xmr_swap) self.logBidEvent(bid.bid_id, EventLogTypes.DEBUG_TWEAK_APPLIED, 'ind {}'.format(bid.debug_ind), session) session.commit() @@ -4055,10 +4058,16 @@ class BasicSwap(BaseApp): self.log.debug('getrawtransaction lock spend tx failed: %s', str(e)) elif state == BidStates.XMR_SWAP_SCRIPT_TX_REDEEMED: if was_received and self.countQueuedActions(session, bid_id, ActionTypes.REDEEM_XMR_SWAP_LOCK_TX_B) < 1: - bid.setState(BidStates.SWAP_DELAYING) - delay = self.get_delay_event_seconds() - self.log.info('Redeeming coin b lock tx for bid %s in %d seconds', bid_id.hex(), delay) - self.createActionInSession(delay, ActionTypes.REDEEM_XMR_SWAP_LOCK_TX_B, bid_id, session) + if bid.debug_ind == DebugTypes.BID_DONT_SPEND_COIN_B_LOCK: + self.log.debug('Adaptor-sig bid %s: Stalling bid for testing: %d.', bid_id.hex(), bid.debug_ind) + bid.setState(BidStates.BID_STALLED_FOR_TEST_TYPE2) # If BID_STALLED_FOR_TEST is set process_XMR_SWAP_A_LOCK_tx_spend would fail + + self.logBidEvent(bid.bid_id, EventLogTypes.DEBUG_TWEAK_APPLIED, 'ind {}'.format(bid.debug_ind), session) + else: + bid.setState(BidStates.SWAP_DELAYING) + delay = self.get_delay_event_seconds() + self.log.info('Redeeming coin b lock tx for bid %s in %d seconds', bid_id.hex(), delay) + self.createActionInSession(delay, ActionTypes.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: @@ -4464,6 +4473,10 @@ class BasicSwap(BaseApp): 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) + if was_received: + _, out_1, _, _, _ = ci_from.extractScriptLockScriptValues(xmr_swap.a_lock_refund_tx_script) + self.addWatchedScript(ci_from.coin_type(), bid_id, out_1, TxTypes.BCH_MERCY) + bid.setState(BidStates.XMR_SWAP_SCRIPT_TX_PREREFUND) self.logBidEvent(bid.bid_id, EventLogTypes.LOCK_TX_A_REFUND_TX_SEEN, '', use_session) else: @@ -4587,6 +4600,34 @@ class BasicSwap(BaseApp): self.removeWatchedScript(coin_type, watched_script.bid_id, watched_script.script) + def processMercyTx(self, coin_type, watched_script, txid: bytes, vout: int, tx) -> None: + bid_id = watched_script.bid_id + self.log.warning('Found mercy tx for bid: {}'.format(bid_id.hex())) + + self.logBidEvent(bid_id, EventLogTypes.BCH_MERCY_TX_FOUND, txid.hex(), session=None) + + if bid_id not in self.swaps_in_progress: + self.log.warning('Could not find active bid for found mercy tx: {}'.format(bid_id.hex())) + else: + remote_keyshare = bytes.fromhex(tx['vout'][0]['scriptPubKey']['asm'].split(' ')[2]) + ci = self.ci(coin_type) + ensure(ci.verifyKey(remote_keyshare), 'Invalid keyshare') + + bid = self.swaps_in_progress[bid_id][0] + bid.txns[TxTypes.BCH_MERCY] = SwapTx( + bid_id=bid_id, + tx_type=TxTypes.BCH_MERCY, + txid=txid, + tx_data=remote_keyshare, + ) + self.saveBid(bid_id, bid) + + delay = self.get_delay_event_seconds() + self.log.info('Redeeming coin b lock tx for bid %s in %d seconds', bid_id.hex(), delay) + self.createAction(delay, ActionTypes.REDEEM_XMR_SWAP_LOCK_TX_B, bid_id) + + self.removeWatchedScript(coin_type, bid_id, watched_script.script) + def checkNewBlock(self, coin_type, c): pass @@ -4690,7 +4731,10 @@ class BasicSwap(BaseApp): # TODO: Optimise by loading rawtx in CTransaction if bytes.fromhex(txo['scriptPubKey']['hex']) == s.script: self.log.debug('Found script from search for bid %s: %s %d', s.bid_id.hex(), tx['txid'], i) - self.processFoundScript(coin_type, s, bytes.fromhex(tx['txid']), i) + if s.tx_type == TxTypes.BCH_MERCY: + self.processMercyTx(coin_type, s, bytes.fromhex(tx['txid']), i, tx) + else: + self.processFoundScript(coin_type, s, bytes.fromhex(tx['txid']), i, tx) for o in c['watched_outputs']: for i, inp in enumerate(tx['vin']): @@ -6105,7 +6149,13 @@ class BasicSwap(BaseApp): # 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) + if TxTypes.BCH_MERCY in bid.txns: + self.log.info('Using keyshare from mercy tx.') + kbsf = bid.txns[TxTypes.BCH_MERCY].tx_data + pkbsf = ci_to.getPubkey(kbsf) + ensure(pkbsf == xmr_swap.pkbsf, 'Keyshare from mercy tx does not match expected pubkey') + else: + 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: bool = True if ci_to.curve_type() == Curves.ed25519 else False @@ -6170,7 +6220,6 @@ class BasicSwap(BaseApp): # Extract the follower's decrypted signature and use it to recover the leader's privatekey af_lock_refund_spend_tx_sig = ci_from.extractFollowerSig(xmr_swap.a_lock_refund_spend_tx) - kbsl = ci_from.recoverEncKey(xmr_swap.af_lock_refund_spend_tx_esig, af_lock_refund_spend_tx_sig, xmr_swap.pkasl) assert (kbsl is not None) @@ -6443,7 +6492,7 @@ class BasicSwap(BaseApp): ensure(bid, 'Bid not found: {}.'.format(bid_id.hex())) ensure(xmr_swap, 'Adaptor-sig swap not found: {}.'.format(bid_id.hex())) - if BidStates(bid.state) == BidStates.BID_STALLED_FOR_TEST: + if BidStates(bid.state) in (BidStates.BID_STALLED_FOR_TEST, BidStates.BID_STALLED_FOR_TEST_TYPE2): self.log.debug('Bid stalled %s', bid_id.hex()) return diff --git a/basicswap/basicswap_util.py b/basicswap/basicswap_util.py index d947ae4..065931c 100644 --- a/basicswap/basicswap_util.py +++ b/basicswap/basicswap_util.py @@ -106,6 +106,7 @@ class BidStates(IntEnum): BID_REQUEST_SENT = 29 BID_REQUEST_ACCEPTED = 30 BID_EXPIRED = 31 + BID_STALLED_FOR_TEST_TYPE2 = 32 class TxStates(IntEnum): @@ -137,6 +138,8 @@ class TxTypes(IntEnum): ITX_PRE_FUNDED = auto() + BCH_MERCY = auto() + class ActionTypes(IntEnum): ACCEPT_BID = auto() @@ -184,6 +187,7 @@ class EventLogTypes(IntEnum): PTX_REDEEM_PUBLISHED = auto() PTX_REFUND_PUBLISHED = auto() LOCK_TX_B_IN_MEMPOOL = auto() + BCH_MERCY_TX_FOUND = auto() class XmrSplitMsgTypes(IntEnum): @@ -195,6 +199,7 @@ class DebugTypes(IntEnum): NONE = 0 BID_STOP_AFTER_COIN_A_LOCK = auto() BID_DONT_SPEND_COIN_A_LOCK_REFUND = auto() + BID_DONT_SPEND_COIN_A_LOCK_REFUND2 = auto() # continues CREATE_INVALID_COIN_B_LOCK = auto() BUYER_STOP_AFTER_ITX = auto() MAKE_INVALID_PTX = auto() @@ -206,6 +211,7 @@ class DebugTypes(IntEnum): DONT_CONFIRM_PTX = auto() OFFER_LOCK_2_VALUE_INC = auto() BID_STOP_AFTER_COIN_B_LOCK = auto() + BID_DONT_SPEND_COIN_B_LOCK = auto() class NotificationTypes(IntEnum): @@ -445,6 +451,8 @@ def describeEventEntry(event_type, event_msg): return 'Participate tx redeem tx published' if event_type == EventLogTypes.PTX_REFUND_PUBLISHED: return 'Participate tx refund tx published' + if event_type == EventLogTypes.BCH_MERCY_TX_FOUND: + return 'BCH mercy tx found' def getVoutByAddress(txjs, p2sh): diff --git a/basicswap/interface/bch.py b/basicswap/interface/bch.py index fa5073a..b0fb233 100644 --- a/basicswap/interface/bch.py +++ b/basicswap/interface/bch.py @@ -201,7 +201,7 @@ class BCHInterface(BTCInterface): 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: + if self.make_int(tx['amount']) == bid_amount and tx['category'] == 'send' and tx.get('address', '_NONE_') == dest_address: txid = bytes.fromhex(tx['txid']) break diff --git a/tests/basicswap/test_bch_xmr.py b/tests/basicswap/test_bch_xmr.py index 495b983..55aa8ab 100644 --- a/tests/basicswap/test_bch_xmr.py +++ b/tests/basicswap/test_bch_xmr.py @@ -564,7 +564,7 @@ class TestBCH(BasicSwapTest): super().test_02_a_leader_recover_a_lock_tx() def test_03_a_follower_recover_a_lock_tx(self): - super().test_03_a_follower_recover_a_lock_tx() + self.do_test_03_follower_recover_a_lock_tx(self.test_coin_from, Coins.XMR, with_mercy=True) def test_03_b_follower_recover_a_lock_tx_reverse(self): self.prepare_balance(Coins.BCH, 100.0, 1801, 1800) diff --git a/tests/basicswap/test_btc_xmr.py b/tests/basicswap/test_btc_xmr.py index 5f258e2..4b90b47 100644 --- a/tests/basicswap/test_btc_xmr.py +++ b/tests/basicswap/test_btc_xmr.py @@ -264,8 +264,8 @@ class TestFunctions(BaseTest): # TODO: Discard block rewards # assert (node0_from_before - node0_from_after < 0.02) - def do_test_03_follower_recover_a_lock_tx(self, coin_from, coin_to, lock_value: int = 32): - logging.info('---------- Test {} to {} follower recovers coin a lock tx'.format(coin_from.name, coin_to.name)) + def do_test_03_follower_recover_a_lock_tx(self, coin_from, coin_to, lock_value: int = 32, with_mercy: bool = False): + logging.info('---------- Test {} to {} follower recovers coin a lock tx{}'.format(coin_from.name, coin_to.name, ' (with mercy tx)' if with_mercy else '')) # Leader is too slow to recover the coin a lock tx and follower swipes it # coin b lock tx remains unspent @@ -296,13 +296,17 @@ class TestFunctions(BaseTest): bid_id = swap_clients[id_bidder].postXmrBid(offer_id, offer.amount_from) wait_for_bid(test_delay_event, swap_clients[id_offerer], bid_id, BidStates.BID_RECEIVED) - swap_clients[id_follower].setBidDebugInd(bid_id, DebugTypes.BID_STOP_AFTER_COIN_A_LOCK) - swap_clients[id_leader].setBidDebugInd(bid_id, DebugTypes.BID_DONT_SPEND_COIN_A_LOCK_REFUND) + debug_type = DebugTypes.BID_DONT_SPEND_COIN_B_LOCK if with_mercy else DebugTypes.BID_STOP_AFTER_COIN_A_LOCK + swap_clients[id_follower].setBidDebugInd(bid_id, debug_type) + debug_type = DebugTypes.BID_DONT_SPEND_COIN_A_LOCK_REFUND2 if with_mercy else DebugTypes.BID_DONT_SPEND_COIN_A_LOCK_REFUND + swap_clients[id_leader].setBidDebugInd(bid_id, debug_type) swap_clients[id_offerer].acceptBid(bid_id) leader_sent_bid: bool = True if reverse_bid else False - wait_for_bid(test_delay_event, swap_clients[id_leader], bid_id, BidStates.BID_STALLED_FOR_TEST, wait_for=(self.extra_wait_time + 180), sent=leader_sent_bid) + + expect_state = BidStates.XMR_SWAP_NOSCRIPT_TX_REDEEMED if with_mercy else BidStates.BID_STALLED_FOR_TEST + wait_for_bid(test_delay_event, swap_clients[id_leader], bid_id, expect_state, wait_for=(self.extra_wait_time + 180), sent=leader_sent_bid) wait_for_bid(test_delay_event, swap_clients[id_follower], bid_id, BidStates.XMR_SWAP_FAILED_SWIPED, wait_for=(self.extra_wait_time + 80), sent=(not leader_sent_bid)) js_w1_after = read_json_api(1800 + id_bidder, 'wallets')