diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index 5f55f01..ba4dc65 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -134,6 +134,10 @@ class BidStates(IntEnum): XMR_SWAP_SECRET_SHARED = auto() XMR_SWAP_SCRIPT_TX_REDEEMED = auto() XMR_SWAP_NOSCRIPT_TX_REDEEMED = auto() + XMR_SWAP_NOSCRIPT_TX_RECOVERED = auto() + XMR_SWAP_FAILED_REFUNDED = auto() + XMR_SWAP_FAILED_SWIPED = auto() + XMR_SWAP_FAILED = auto() SWAP_DELAYING = auto() SWAP_TIMEDOUT = auto() BID_ABANDONED = auto() # Bid will no longer be processed @@ -160,6 +164,7 @@ class TxTypes(IntEnum): XMR_SWAP_A_LOCK_SPEND = auto() XMR_SWAP_A_LOCK_REFUND = auto() XMR_SWAP_A_LOCK_REFUND_SPEND = auto() + XMR_SWAP_A_LOCK_REFUND_SWIPE = auto() XMR_SWAP_B_LOCK = auto() @@ -171,6 +176,7 @@ class EventTypes(IntEnum): SEND_XMR_SECRET = auto() REDEEM_XMR_SWAP_LOCK_TX_A = auto() # Follower REDEEM_XMR_SWAP_LOCK_TX_B = auto() # Leader + RECOVER_XMR_SWAP_LOCK_TX_B = auto() class XmrSplitMsgTypes(IntEnum): @@ -178,6 +184,12 @@ class XmrSplitMsgTypes(IntEnum): BID_ACCEPT = auto() +class DebugTypes(IntEnum): + BID_STOP_AFTER_COIN_A_LOCK = auto() + BID_DONT_SPEND_COIN_A_LOCK_REFUND = auto() + CREATE_INVALID_COIN_B_LOCK = auto() + + SEQUENCE_LOCK_BLOCKS = 1 SEQUENCE_LOCK_TIME = 2 ABS_LOCK_BLOCKS = 3 @@ -374,7 +386,7 @@ class BasicSwap(BaseApp): self.sqlite_file = os.path.join(self.data_dir, 'db{}.sqlite'.format('' if self.chain == 'mainnet' else ('_' + self.chain))) db_exists = os.path.exists(self.sqlite_file) - self.engine = sa.create_engine('sqlite:///' + self.sqlite_file) + self.engine = sa.create_engine('sqlite:///' + self.sqlite_file) # , echo=True if not db_exists: Base.metadata.create_all(self.engine) self.session_factory = sessionmaker(bind=self.engine, expire_on_commit=False) @@ -803,8 +815,11 @@ class BasicSwap(BaseApp): if swap_type == SwapTypes.XMR_SWAP: xmr_offer = XmrOffer() - xmr_offer.lock_time_1 = lock_value # Delay before the chain a lock refund tx can be mined - xmr_offer.lock_time_2 = lock_value # Delay before the follower can spend from the chain a lock refund tx + # Delay before the chain a lock refund tx can be mined + xmr_offer.lock_time_1 = getExpectedSequence(lock_type, lock_value, coin_from) + + # Delay before the follower can spend from the chain a lock refund tx + xmr_offer.lock_time_2 = getExpectedSequence(lock_type, lock_value, coin_from) # TODO: Dynamic fee selection xmr_offer.a_fee_rate = make_int(0.00032595, self.ci(coin_from).exp()) @@ -1094,10 +1109,12 @@ class BasicSwap(BaseApp): session.add(bid.participate_tx) if bid.xmr_a_lock_tx: session.add(bid.xmr_a_lock_tx) - if bid.xmr_b_lock_tx: - session.add(bid.xmr_b_lock_tx) if bid.xmr_a_lock_spend_tx: session.add(bid.xmr_a_lock_spend_tx) + if bid.xmr_b_lock_tx: + session.add(bid.xmr_b_lock_tx) + for tx_type, tx in bid.txns.items(): + session.add(tx) if xmr_swap is not None: session.add(xmr_swap) @@ -1231,6 +1248,7 @@ class BasicSwap(BaseApp): self.mxDB.release() def loadBidTxns(self, bid, session): + bid.txns = {} for stx in session.query(SwapTx).filter(sa.and_(SwapTx.bid_id == bid.bid_id)): if stx.tx_type == TxTypes.ITX: bid.initiate_tx = stx @@ -1243,7 +1261,8 @@ class BasicSwap(BaseApp): elif stx.tx_type == TxTypes.XMR_SWAP_B_LOCK: bid.xmr_b_lock_tx = stx else: - self.log.warning('Unknown transaction type: {}'.format(stx.tx_type)) + bid.txns[stx.tx_type] = stx + #self.log.warning('Unknown transaction type: {}'.format(stx.tx_type)) def getXmrBid(self, bid_id, sent=False): self.mxDB.acquire() try: @@ -1585,15 +1604,16 @@ class BasicSwap(BaseApp): ) xmr_swap.al_lock_refund_tx_sig = ci_from.signTx(karl, xmr_swap.a_lock_refund_tx, 0, xmr_swap.a_lock_tx_script, bid.amount) - v = ci_from.verifyTxSig(xmr_swap.a_lock_refund_tx, xmr_swap.al_lock_refund_tx_sig, xmr_swap.pkarl, 0, xmr_swap.a_lock_tx_script, bid.amount) assert(v) + pkh_refund_to = ci_from.decodeAddress(self.getReceiveAddressForCoin(coin_from)) xmr_swap.a_lock_refund_spend_tx = ci_from.createScriptLockRefundSpendTx( xmr_swap.a_lock_refund_tx, xmr_swap.a_lock_refund_tx_script, - xmr_swap.pkal, + pkh_refund_to, xmr_offer.a_fee_rate ) + xmr_swap.a_lock_refund_spend_tx_id = ci_from.getTxHash(xmr_swap.a_lock_refund_spend_tx) msg_buf = XmrBidAcceptMessage() msg_buf.bid_msg_id = bid_id @@ -1699,6 +1719,7 @@ class BasicSwap(BaseApp): self.log.error('Bid %s - Error: %s', bid_id.hex(), error_str) bid.setState(BidStates.BID_ERROR) bid.state_note = 'error msg: ' + error_str + print('[rm] saveBid 5') self.saveBid(bid_id, bid) def createInitiateTxn(self, coin_type, bid_id, bid, initiate_script): @@ -2187,20 +2208,161 @@ class BasicSwap(BaseApp): def checkXmrBidState(self, bid_id, bid, offer): rv = False - state = BidStates(bid.state) - if state == BidStates.XMR_SWAP_HAVE_SCRIPT_COIN_SPEND_TX: - if bid.xmr_a_lock_tx is None: - return - ci_from = self.ci(Coins(offer.coin_from)) - ci_to = self.ci(Coins(offer.coin_to)) - try: - self.mxDB.acquire() - session = scoped_session(self.session_factory) - xmr_offer = session.query(XmrOffer).filter_by(offer_id=offer.offer_id).first() - assert(xmr_offer), 'XMR offer not found: {}.'.format(offer.offer_id.hex()) - xmr_swap = session.query(XmrSwap).filter_by(bid_id=bid.bid_id).first() - assert(xmr_swap), 'XMR swap not found: {}.'.format(bid.bid_id.hex()) + ci_from = self.ci(Coins(offer.coin_from)) + ci_to = self.ci(Coins(offer.coin_to)) + + session = None + try: + self.mxDB.acquire() + session = scoped_session(self.session_factory) + xmr_offer = session.query(XmrOffer).filter_by(offer_id=offer.offer_id).first() + assert(xmr_offer), 'XMR offer not found: {}.'.format(offer.offer_id.hex()) + xmr_swap = session.query(XmrSwap).filter_by(bid_id=bid.bid_id).first() + assert(xmr_swap), 'XMR swap not found: {}.'.format(bid.bid_id.hex()) + + if bid.was_sent and not TxTypes.XMR_SWAP_A_LOCK_REFUND_SPEND in bid.txns: + found_tx = ci_from.getTransaction(xmr_swap.a_lock_refund_spend_tx_id) + if found_tx is not None: + self.log.debug('Found coin a lock refund spend tx') + xmr_swap.a_lock_refund_spend_tx = found_tx # Replace with fully signed tx + + bid.txns[TxTypes.XMR_SWAP_A_LOCK_REFUND_SPEND] = SwapTx( + bid_id=bid_id, + tx_type=TxTypes.XMR_SWAP_A_LOCK_REFUND_SPEND, + txid=xmr_swap.a_lock_refund_spend_tx_id, + ) + + if bid.was_sent: + if bid.xmr_b_lock_tx is not None: + delay = random.randrange(self.min_delay_auto_accept, self.max_delay_auto_accept) + self.log.info('Recovering xmr swap chain B lock 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: + rv = True # Remove from swaps_in_progress + bid.setState(BidStates.XMR_SWAP_FAILED_REFUNDED) + + self.saveBidInSession(bid_id, bid, session, xmr_swap) + session.commit() + return rv + + + if TxTypes.XMR_SWAP_A_LOCK_REFUND_SWIPE in bid.txns: + swipe_tx = bid.txns[TxTypes.XMR_SWAP_A_LOCK_REFUND_SWIPE] + + # TODO: explorer or getrawtransaction for each block after swap begins + found_tx = ci_from.getTransaction(swipe_tx.txid) + if found_tx is not None: + # TODO: Check depth + rv = True + bid.setState(BidStates.XMR_SWAP_FAILED_SWIPED) + self.saveBidInSession(bid_id, bid, session, xmr_swap) + session.commit() + return rv + + if bid.was_received and TxTypes.XMR_SWAP_A_LOCK_REFUND_SPEND in bid.txns: + refund_spend_tx = bid.txns[TxTypes.XMR_SWAP_A_LOCK_REFUND_SPEND] + + # TODO: explorer or getrawtransaction for each block after swap begins + found_tx = ci_from.getTransaction(refund_spend_tx.txid) + if found_tx is not None: + # TODO: Check depth + #if bid.was_received and TxTypes.XMR_SWAP_B_LOCK in bid.txns: + if bid.was_sent and bid.xmr_b_lock_tx is not None: + pass + else: + rv = True + bid.setState(BidStates.XMR_SWAP_FAILED_REFUNDED) + self.saveBidInSession(bid_id, bid, session, xmr_swap) + session.commit() + return rv + + if TxTypes.XMR_SWAP_A_LOCK_REFUND in bid.txns: + refund_tx = bid.txns[TxTypes.XMR_SWAP_A_LOCK_REFUND] + if bid.was_received: + if bid.debug_ind == DebugTypes.BID_DONT_SPEND_COIN_A_LOCK_REFUND: + self.log.debug('XMR bid %s: Abandoning bid for testing: %d.', bid_id.hex(), bid.debug_ind) + bid.setState(BidStates.BID_ABANDONED) + rv = True + self.saveBidInSession(bid_id, bid, session, xmr_swap) + session.commit() + return rv + try: + txid = ci_from.publishTx(xmr_swap.a_lock_refund_spend_tx) + + 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( + bid_id=bid_id, + tx_type=TxTypes.XMR_SWAP_A_LOCK_REFUND_SPEND, + txid=bytes.fromhex(txid), + ) + self.saveBidInSession(bid_id, bid, session, xmr_swap) + session.commit() + except Exception as ex: + logging.debug('Trying to publish coin a lock refund spend tx: %s', str(ex)) + + + if bid.was_sent: + if xmr_swap.a_lock_refund_swipe_tx is None: + self.createCoinALockRefundSwipeTx(ci_from, bid, offer, xmr_swap, xmr_offer) + self.saveBidInSession(bid_id, bid, session, xmr_swap) + session.commit() + + try: + txid = ci_from.publishTx(xmr_swap.a_lock_refund_swipe_tx) + 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, + tx_type=TxTypes.XMR_SWAP_A_LOCK_REFUND_SWIPE, + txid=bytes.fromhex(txid), + ) + self.saveBidInSession(bid_id, bid, session, xmr_swap) + session.commit() + except Exception as ex: + logging.debug('Trying to publish coin a lock refund swipe tx: %s', str(ex)) + + if BidStates(bid.state) == BidStates.XMR_SWAP_NOSCRIPT_TX_RECOVERED: + txid_hex = bid.xmr_b_lock_tx.spend_txid.hex() + + found_tx = ci_to.findTxnByHash(txid_hex) + if found_tx is not None: + self.log.info('Found coin b lock recover tx bid %s', bid_id.hex()) + rv = True # Remove from swaps_in_progress + bid.setState(BidStates.XMR_SWAP_FAILED_REFUNDED) + self.saveBidInSession(bid_id, bid, session, xmr_swap) + session.commit() + return rv + + if len(xmr_swap.al_lock_refund_tx_sig) > 0 and len(xmr_swap.af_lock_refund_tx_sig) > 0: + try: + 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())) + bid.txns[TxTypes.XMR_SWAP_A_LOCK_REFUND] = SwapTx( + bid_id=bid_id, + tx_type=TxTypes.XMR_SWAP_A_LOCK_REFUND, + txid=bytes.fromhex(txid), + ) + self.saveBidInSession(bid_id, bid, session, xmr_swap) + session.commit() + return rv + except Exception as ex: + if 'Transaction already in block chain' in str(ex): + self.log.info('Found coin a lock refund tx for bid {}'.format(bid_id.hex())) + txid = ci_from.getTxHash(xmr_swap.a_lock_refund_tx) + bid.txns[TxTypes.XMR_SWAP_A_LOCK_REFUND] = SwapTx( + bid_id=bid_id, + tx_type=TxTypes.XMR_SWAP_A_LOCK_REFUND, + txid=txid, + ) + self.saveBidInSession(bid_id, bid, session, xmr_swap) + session.commit() + return rv + + state = BidStates(bid.state) + if state == BidStates.XMR_SWAP_HAVE_SCRIPT_COIN_SPEND_TX: + if bid.xmr_a_lock_tx is None: + return rv # TODO: Timeout waiting for transactions @@ -2208,7 +2370,7 @@ class BasicSwap(BaseApp): utxos = ci_from.getOutput(bid.xmr_a_lock_tx.txid, a_lock_tx_dest, bid.amount) if len(utxos) < 1: - return + return rv if len(utxos) > 1: raise ValueError('Too many outputs for chain A lock tx') @@ -2227,41 +2389,27 @@ class BasicSwap(BaseApp): session.commit() - session.close() - session.remove() - except Exception as ex: - session.close() - session.remove() - raise ex - finally: - self.mxDB.release() - elif state == BidStates.XMR_SWAP_SCRIPT_COIN_LOCKED: - if bid.was_sent and bid.xmr_b_lock_tx is None: - return - ci_from = self.ci(Coins(offer.coin_from)) - ci_to = self.ci(Coins(offer.coin_to)) + elif state == BidStates.XMR_SWAP_SCRIPT_COIN_LOCKED: + #if bid.was_sent and TxTypes.XMR_SWAP_B_LOCK not in bid.txns: + if bid.was_sent and bid.xmr_b_lock_tx is None: + return rv - try: - self.mxDB.acquire() - session = scoped_session(self.session_factory) - xmr_offer = session.query(XmrOffer).filter_by(offer_id=offer.offer_id).first() - assert(xmr_offer), 'XMR offer not found: {}.'.format(offer.offer_id.hex()) - xmr_swap = session.query(XmrSwap).filter_by(bid_id=bid.bid_id).first() - assert(xmr_swap), 'XMR swap not found: {}.'.format(bid.bid_id.hex()) - - rv = ci_to.findTxB(xmr_swap.vkbv, xmr_swap.pkbs, bid.amount_to, ci_to.blocks_confirmed, xmr_swap.b_restore_height) - - if rv is not None: + 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 TxTypes.XMR_SWAP_B_LOCK not in bid.txns: if bid.xmr_b_lock_tx is None: - b_lock_tx_id = bytes.fromhex(rv['txid']) + b_lock_tx_id = bytes.fromhex(found_tx['txid']) + #bid.txns[TxTypes.XMR_SWAP_B_LOCK] = SwapTx( bid.xmr_b_lock_tx = SwapTx( bid_id=bid_id, tx_type=TxTypes.XMR_SWAP_B_LOCK, txid=b_lock_tx_id, ) + #bid.txns[TxTypes.XMR_SWAP_B_LOCK].setState(TxStates.TX_CONFIRMED) bid.xmr_b_lock_tx.setState(TxStates.TX_CONFIRMED) + bid.setState(BidStates.XMR_SWAP_NOSCRIPT_COIN_LOCKED) self.saveBidInSession(bid_id, bid, session, xmr_swap) @@ -2271,41 +2419,21 @@ class BasicSwap(BaseApp): self.createEventInSession(delay, EventTypes.SEND_XMR_SECRET, bid_id, session) session.commit() - - session.close() - session.remove() - except Exception as ex: - session.close() - session.remove() - raise ex - finally: - self.mxDB.release() - # Waiting for initiate txn to be confirmed in 'from' chain - #initiate_txnid_hex = bid.initiate_tx.txid.hex() - #p2sh = self.getScriptAddress(coin_from, bid.initiate_tx.script) - elif state == BidStates.XMR_SWAP_SECRET_SHARED: - # Wait for script spend tx to confirm - ci_from = self.ci(Coins(offer.coin_from)) - ci_to = self.ci(Coins(offer.coin_to)) - - try: - self.mxDB.acquire() - session = scoped_session(self.session_factory) - xmr_offer = session.query(XmrOffer).filter_by(offer_id=offer.offer_id).first() - assert(xmr_offer), 'XMR offer not found: {}.'.format(offer.offer_id.hex()) - xmr_swap = session.query(XmrSwap).filter_by(bid_id=bid.bid_id).first() - assert(xmr_swap), 'XMR swap not found: {}.'.format(bid.bid_id.hex()) - + # Waiting for initiate txn to be confirmed in 'from' chain + #initiate_txnid_hex = bid.initiate_tx.txid.hex() + #p2sh = self.getScriptAddress(coin_from, bid.initiate_tx.script) + elif state == BidStates.XMR_SWAP_SECRET_SHARED: + # Wait for script spend tx to confirm # TODO: Use explorer to get tx / block hash for getrawtransaction - rv = ci_from.getTransaction(xmr_swap.a_lock_spend_tx_id) - if rv is not None: - xmr_swap.a_lock_spend_tx = rv + found_tx = ci_from.getTransaction(xmr_swap.a_lock_spend_tx_id) + if found_tx is not None: + xmr_swap.a_lock_spend_tx = found_tx #bid.xmr_a_lock_spend_tx.setState(TxStates.TX_CONFIRMED) bid.setState(BidStates.XMR_SWAP_SCRIPT_TX_REDEEMED) # TODO: Wait for confirmation? if not bid.was_received: - rv = True # Remove from return False + rv = True # Remove from swaps_in_progress bid.setState(BidStates.SWAP_COMPLETED) self.saveBidInSession(bid_id, bid, session, xmr_swap) if bid.was_received: @@ -2314,25 +2442,41 @@ class BasicSwap(BaseApp): self.createEventInSession(delay, EventTypes.REDEEM_XMR_SWAP_LOCK_TX_B, bid_id, session) session.commit() + elif state == BidStates.XMR_SWAP_SCRIPT_TX_REDEEMED: + #txid_hex = bid.txns[TxTypes.XMR_SWAP_B_LOCK].spend_txid.hex() + txid_hex = bid.xmr_b_lock_tx.spend_txid.hex() + found_tx = ci_to.findTxnByHash(txid_hex) + if found_tx is not None: + self.log.info('Found coin b lock spend tx bid %s', bid_id.hex()) + rv = True # Remove from swaps_in_progress + bid.setState(BidStates.SWAP_COMPLETED) + self.saveBidInSession(bid_id, bid, session, xmr_swap) + session.commit() + ''' + elif state == BidStates.XMR_SWAP_NOSCRIPT_TX_RECOVERED: + print('[rm] waiting for coin b lock tx recover tx to confirm') + + txid_hex = bid.xmr_b_lock_tx.spend_txid.hex() + + found_tx = ci_to.findTxnByHash(txid_hex) + if found_tx is not None: + self.log.info('Found coin b lock recover tx bid %s', bid_id.hex()) + rv = True # Remove from swaps_in_progress + bid.setState(BidStates.XMR_SWAP_FAILED_REFUNDED) + print('[rm] saveBidInSession 9.1') + self.saveBidInSession(bid_id, bid, session, xmr_swap) + session.commit() + ''' + + + except Exception as ex: + raise ex + finally: + if session: session.close() session.remove() - except Exception as ex: - session.close() - session.remove() - raise ex - finally: - self.mxDB.release() - elif state == BidStates.XMR_SWAP_SCRIPT_TX_REDEEMED: - ci_to = self.ci(Coins(offer.coin_to)) - txid_hex = bid.xmr_b_lock_tx.spend_txid.hex() - - rv = ci_to.findTxnByHash(txid_hex) - if rv is not None: - rv = True # Remove from return False - bid.setState(BidStates.SWAP_COMPLETED) - self.saveBid(bid_id, bid) - + self.mxDB.release() return rv @@ -2648,6 +2792,7 @@ class BasicSwap(BaseApp): def checkEvents(self): self.mxDB.acquire() now = int(time.time()) + session = None try: session = scoped_session(self.session_factory) @@ -2668,6 +2813,8 @@ class BasicSwap(BaseApp): self.redeemXmrBidCoinALockTx(row.linked_id, session) elif row.event_type == EventTypes.REDEEM_XMR_SWAP_LOCK_TX_B: self.redeemXmrBidCoinBLockTx(row.linked_id, session) + elif row.event_type == EventTypes.RECOVER_XMR_SWAP_LOCK_TX_B: + self.recoverXmrBidCoinBLockTx(row.linked_id, session) else: self.log.warning('Unknown event type: %d', row.event_type) except Exception as ex: @@ -2677,9 +2824,10 @@ class BasicSwap(BaseApp): session.delete(row) session.commit() - session.close() - session.remove() finally: + if session: + session.close() + session.remove() self.mxDB.release() def checkXmrSwaps(self): @@ -2796,8 +2944,8 @@ class BasicSwap(BaseApp): xmr_offer = XmrOffer() xmr_offer.offer_id = offer_id - xmr_offer.lock_time_1 = offer_data.lock_value - xmr_offer.lock_time_2 = offer_data.lock_value + xmr_offer.lock_time_1 = getExpectedSequence(offer_data.lock_type, offer_data.lock_value, coin_from) + xmr_offer.lock_time_2 = getExpectedSequence(offer_data.lock_type, offer_data.lock_value, coin_from) xmr_offer.a_fee_rate = offer_data.fee_rate_from xmr_offer.b_fee_rate = offer_data.fee_rate_to @@ -3012,7 +3160,6 @@ class BasicSwap(BaseApp): bid.setState(BidStates.BID_RECEIVED) self.saveBidInSession(bid.bid_id, bid, session, xmr_swap) - def receiveXmrBidAccept(self, bid, session): # Follower receiving MSG1F and MSG2F self.log.debug('Receiving xmr bid accept %s', bid.bid_id.hex()) @@ -3163,6 +3310,7 @@ class BasicSwap(BaseApp): xmr_swap.a_lock_refund_tx = msg_data.a_lock_refund_tx xmr_swap.a_lock_refund_tx_script = msg_data.a_lock_refund_tx_script xmr_swap.a_lock_refund_spend_tx = msg_data.a_lock_refund_spend_tx + xmr_swap.a_lock_refund_spend_tx_id = ci_from.getTxHash(xmr_swap.a_lock_refund_spend_tx) xmr_swap.al_lock_refund_tx_sig = msg_data.al_lock_refund_tx_sig check_a_lock_tx_inputs = True @@ -3225,6 +3373,8 @@ class BasicSwap(BaseApp): xmr_swap.af_lock_refund_spend_tx_esig = ci_from.signTxOtVES(karf, xmr_swap.pkasl, xmr_swap.a_lock_refund_spend_tx, 0, xmr_swap.a_lock_refund_tx_script, xmr_swap.a_swap_refund_value) xmr_swap.af_lock_refund_tx_sig = ci_from.signTx(karf, xmr_swap.a_lock_refund_tx, 0, xmr_swap.a_lock_tx_script, bid.amount) + self.addLockRefundSigs(xmr_swap, ci_from) + msg_buf = XmrBidLockTxSigsMessage( bid_msg_id=bid_id, af_lock_refund_spend_tx_esig=xmr_swap.af_lock_refund_spend_tx_esig, @@ -3332,16 +3482,27 @@ class BasicSwap(BaseApp): ci_from = self.ci(coin_from) ci_to = self.ci(coin_to) + if bid.debug_ind == DebugTypes.BID_STOP_AFTER_COIN_A_LOCK: + self.log.debug('XMR bid %s: Abandoning bid for testing: %d.', bid_id.hex(), bid.debug_ind) + # bid.setState(BidStates.BID_ABANDONED) # TODO: Retry if state + return + + if bid.debug_ind == DebugTypes.CREATE_INVALID_COIN_B_LOCK: + self.log.debug('XMR bid %s: Debug %d - Reducing lock b txn amount by 10%%.', bid_id.hex(), bid.debug_ind) + bid.amount_to -= int(bid.amount_to * 0.1) b_lock_tx_id = ci_to.publishBLockTx(xmr_swap.pkbv, xmr_swap.pkbs, bid.amount_to, xmr_offer.b_fee_rate) self.log.debug('Submitted lock txn %s to %s chain for bid %s', b_lock_tx_id.hex(), chainparams[coin_to]['name'], bid_id.hex()) bid.xmr_b_lock_tx = SwapTx( + #xmr_b_lock_tx = SwapTx( bid_id=bid_id, tx_type=TxTypes.XMR_SWAP_B_LOCK, txid=b_lock_tx_id, ) bid.xmr_b_lock_tx.setState(TxStates.TX_NONE) + #bid.txns[TxTypes.XMR_SWAP_B_LOCK] = xmr_b_lock_tx + print('[rm] saveBidInSession 23') self.saveBidInSession(bid_id, bid, session, xmr_swap) # Update copy of bid in swaps_in_progress @@ -3377,6 +3538,7 @@ class BasicSwap(BaseApp): xmr_swap.coin_a_lock_refund_spend_tx_msg_id = bytes.fromhex(ro['msgid']) bid.setState(BidStates.XMR_SWAP_SECRET_SHARED) + print('[rm] saveBidInSession 24') self.saveBidInSession(bid_id, bid, session, xmr_swap) # Update copy of bid in swaps_in_progress self.swaps_in_progress[bid_id] = (bid, offer) @@ -3428,6 +3590,7 @@ class BasicSwap(BaseApp): ) bid.xmr_a_lock_spend_tx.setState(TxStates.TX_NONE) + print('[rm] saveBidInSession 25') self.saveBidInSession(bid_id, bid, session, xmr_swap) # Update copy of bid in swaps_in_progress self.swaps_in_progress[bid_id] = (bid, offer) @@ -3451,12 +3614,12 @@ 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) - xmr_swap.kbsf = ci_from.recoverEncKey(xmr_swap.al_lock_spend_tx_esig, xmr_swap.al_lock_spend_tx_sig, xmr_swap.pkasf) - assert(xmr_swap.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) kbsl = self.getPathKey(coin_from, coin_to, bid.created_at, xmr_swap.contract_count, 3, for_xmr=True) - vkbs = ci_to.sumKeys(kbsl, xmr_swap.kbsf) + vkbs = ci_to.sumKeys(kbsl, kbsf) Kbs_test = ci_to.getPubkey(vkbs) print('Kbs_test', Kbs_test.hex()) print('Kbs', xmr_swap.pkbsf.hex()) @@ -3466,10 +3629,57 @@ class BasicSwap(BaseApp): txid = ci_to.spendBLockTx(address_to, xmr_swap.vkbv, vkbs, bid.amount_to, xmr_offer.b_fee_rate, xmr_swap.b_restore_height) bid.xmr_b_lock_tx.spend_txid = txid + #txn = bid.txns[TxTypes.XMR_SWAP_B_LOCK] + #print('[rm] TxTypes.XMR_SWAP_B_LOCK', txn.bid_id.hex()) + #txn.spend_txid = txid + #bid.txns[TxTypes.XMR_SWAP_B_LOCK].spend_txid = txid + print('[rm] saveBidInSession 26') self.saveBidInSession(bid_id, bid, session, xmr_swap) # Update copy of bid in swaps_in_progress self.swaps_in_progress[bid_id] = (bid, offer) + def recoverXmrBidCoinBLockTx(self, bid_id, session): + # Follower recovering B lock tx + self.log.debug('Recovering coin B lock tx for xmr bid %s', bid_id.hex()) + + bid, xmr_swap = self.getXmrBid(bid_id) + assert(bid), 'Bid not found: {}.'.format(bid_id.hex()) + assert(xmr_swap), 'XMR swap not found: {}.'.format(bid_id.hex()) + + offer, xmr_offer = self.getXmrOffer(bid.offer_id, sent=False) + assert(offer), 'Offer not found: {}.'.format(bid.offer_id.hex()) + assert(xmr_offer), 'XMR offer not found: {}.'.format(bid.offer_id.hex()) + coin_from = Coins(offer.coin_from) + coin_to = Coins(offer.coin_to) + ci_from = self.ci(coin_from) + ci_to = self.ci(coin_to) + + # 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) + + kbsf = self.getPathKey(coin_from, coin_to, bid.created_at, xmr_swap.contract_count, 2, for_xmr=True) + + vkbs = ci_to.sumKeys(kbsl, kbsf) + Kbs_test = ci_to.getPubkey(vkbs) + print('Kbs_test', Kbs_test.hex()) + print('Kbs', xmr_swap.pkbsf.hex()) + + 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) + + bid.xmr_b_lock_tx.spend_txid = txid + + bid.setState(BidStates.XMR_SWAP_NOSCRIPT_TX_RECOVERED) + print('[rm] saveBidInSession 32') + self.saveBidInSession(bid_id, bid, session, xmr_swap) + # Update copy of bid in swaps_in_progress + self.swaps_in_progress[bid_id] = (bid, offer) + + def processXmrBidCoinALockSigs(self, msg): # Leader processing MSG3L self.log.debug('Processing xmr coin a follower lock sigs msg %s', msg['msgid']) @@ -3498,16 +3708,33 @@ class BasicSwap(BaseApp): xmr_swap.af_lock_refund_tx_sig = msg_data.af_lock_refund_tx_sig kbsl = self.getPathKey(coin_from, coin_to, bid.created_at, xmr_swap.contract_count, 3, for_xmr=True) + karl = self.getPathKey(coin_from, coin_to, bid.created_at, xmr_swap.contract_count, 5) + xmr_swap.af_lock_refund_spend_tx_sig = ci_from.decryptOtVES(kbsl, xmr_swap.af_lock_refund_spend_tx_esig) + al_lock_refund_spend_tx_sig = ci_from.signTx(karl, xmr_swap.a_lock_refund_spend_tx, 0, xmr_swap.a_lock_refund_tx_script, xmr_swap.a_swap_refund_value) + + self.log.debug('Setting lock refund spend tx sigs') + witness_stack = [ + b'', + 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) + assert(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.pkarf, 0, xmr_swap.a_lock_refund_tx_script, xmr_swap.a_swap_refund_value) assert(v), 'Invalid signature for lock refund spend txn' + self.addLockRefundSigs(xmr_swap, ci_from) delay = random.randrange(self.min_delay_auto_accept, self.max_delay_auto_accept) self.log.info('Sending coin A lock tx for xmr bid %s in %d seconds', bid_id.hex(), delay) self.createEvent(delay, EventTypes.SEND_XMR_SWAP_LOCK_TX_A, bid_id) bid.setState(BidStates.SWAP_DELAYING) + print('[rm] saveBid 27') self.saveBid(bid_id, bid, xmr_swap=xmr_swap) except Exception as ex: if self.debug: @@ -3553,6 +3780,7 @@ class BasicSwap(BaseApp): assert(v), 'verifyTxOtVES failed' bid.setState(BidStates.XMR_SWAP_HAVE_SCRIPT_COIN_SPEND_TX) + print('[rm] saveBid 28') self.saveBid(bid_id, bid, xmr_swap=xmr_swap) except Exception as ex: if self.debug: @@ -3607,6 +3835,7 @@ class BasicSwap(BaseApp): self.createEvent(delay, EventTypes.REDEEM_XMR_SWAP_LOCK_TX_A, bid_id) bid.setState(BidStates.XMR_SWAP_SECRET_SHARED) + print('[rm] saveBid 29') self.saveBid(bid_id, bid, xmr_swap=xmr_swap) self.swaps_in_progress[bid_id] = (bid, offer) @@ -3676,6 +3905,7 @@ class BasicSwap(BaseApp): for bid_id, v in self.swaps_in_progress.items(): try: if self.checkBidState(bid_id, v[0], v[1]) is True: + self.log.debug('[rm] removing state: %s', BidStates(v[0].state)) to_remove.append(bid_id) except Exception as ex: self.log.error('checkBidState %s %s', bid_id.hex(), str(ex)) @@ -3730,6 +3960,7 @@ class BasicSwap(BaseApp): if has_changed: session = scoped_session(self.session_factory) try: + print('[rm] saveBidInSession 30') self.saveBidInSession(bid_id, bid, session) session.commit() if bid.state and bid.state > BidStates.BID_RECEIVED and bid.state < BidStates.SWAP_COMPLETED: @@ -3942,3 +4173,45 @@ class BasicSwap(BaseApp): session.close() session.remove() self.mxDB.release() + + def addLockRefundSigs(self, xmr_swap, ci): + self.log.debug('Setting lock refund tx sigs') + witness_stack = [ + b'', + xmr_swap.al_lock_refund_tx_sig, + xmr_swap.af_lock_refund_tx_sig, + b'', + xmr_swap.a_lock_tx_script, + ] + + signed_tx = ci.setTxSignature(xmr_swap.a_lock_refund_tx, witness_stack) + assert(signed_tx), 'setTxSignature failed' + xmr_swap.a_lock_refund_tx = signed_tx + + def createCoinALockRefundSwipeTx(self, ci, bid, offer, xmr_swap, xmr_offer): + self.log.debug('Creating %s lock refund swipe tx', ci.coin_name()) + + pkh_dest = ci.decodeAddress(self.getReceiveAddressForCoin(ci.coin_type())) + spend_tx = ci.createScriptLockRefundSpendToFTx( + xmr_swap.a_lock_refund_tx, xmr_swap.a_lock_refund_tx_script, + pkh_dest, + xmr_offer.a_fee_rate + ) + + vkaf = self.getPathKey(offer.coin_from, offer.coin_to, bid.created_at, xmr_swap.contract_count, 3) + sig = ci.signTx(vkaf, spend_tx, 0, xmr_swap.a_lock_refund_tx_script, xmr_swap.a_swap_refund_value) + + witness_stack = [ + sig, + b'', + xmr_swap.a_lock_refund_tx_script, + ] + + xmr_swap.a_lock_refund_swipe_tx = ci.setTxSignature(spend_tx, witness_stack) + + def setBidDebugInd(self, bid_id, debug_ind): + self.log.debug('Bid %s Setting debug flag: %s', bid_id.hex(), debug_ind) + bid = self.getBid(bid_id) + bid.debug_ind = debug_ind + print('[rm] saveBid 31') + self.saveBid(bid_id, bid) diff --git a/basicswap/chainparams.py b/basicswap/chainparams.py index 5ea3399..9e6cfc8 100644 --- a/basicswap/chainparams.py +++ b/basicswap/chainparams.py @@ -16,7 +16,7 @@ class Coins(IntEnum): PART = 1 BTC = 2 LTC = 3 - #DCR = 4 + # DCR = 4 NMC = 5 XMR = 6 @@ -190,6 +190,6 @@ chainparams = { } } + class CoinInterface: pass - diff --git a/basicswap/db.py b/basicswap/db.py index c36005b..08f96f1 100644 --- a/basicswap/db.py +++ b/basicswap/db.py @@ -105,11 +105,15 @@ class Bid(Base): state_note = sa.Column(sa.String) + debug_ind = sa.Column(sa.Integer) + initiate_tx = None participate_tx = None xmr_a_lock_tx = None - xmr_b_lock_tx = None xmr_a_lock_spend_tx = None + xmr_b_lock_tx = None # TODO: Can't move to txns due to error: Exception UPDATE statement on table expected to update 1 row(s); 0 were matched + + txns = {} def getITxState(self): if self.initiate_tx is None: @@ -273,18 +277,23 @@ class XmrSwap(Base): a_lock_refund_tx = sa.Column(sa.LargeBinary) a_lock_refund_tx_script = sa.Column(sa.LargeBinary) + a_lock_refund_tx_id = sa.Column(sa.LargeBinary) a_swap_refund_value = sa.Column(sa.BigInteger) + al_lock_refund_tx_sig = sa.Column(sa.LargeBinary) + af_lock_refund_tx_sig = sa.Column(sa.LargeBinary) a_lock_refund_spend_tx = sa.Column(sa.LargeBinary) + a_lock_refund_spend_tx_id = sa.Column(sa.LargeBinary) af_lock_refund_spend_tx_esig = sa.Column(sa.LargeBinary) af_lock_refund_spend_tx_sig = sa.Column(sa.LargeBinary) - af_lock_refund_tx_sig = sa.Column(sa.LargeBinary) a_lock_spend_tx = sa.Column(sa.LargeBinary) a_lock_spend_tx_id = sa.Column(sa.LargeBinary) al_lock_spend_tx_esig = sa.Column(sa.LargeBinary) + a_lock_refund_swipe_tx = sa.Column(sa.LargeBinary) # Follower spends script coin lock refund tx + b_lock_tx_id = sa.Column(sa.LargeBinary) b_restore_height = sa.Column(sa.Integer) # Height of xmr chain before the swap diff --git a/basicswap/interface_btc.py b/basicswap/interface_btc.py index ab45634..8719323 100644 --- a/basicswap/interface_btc.py +++ b/basicswap/interface_btc.py @@ -11,7 +11,6 @@ import logging from io import BytesIO from basicswap.contrib.test_framework import segwit_addr - from .util import ( decodeScriptNum, getCompactSizeLen, @@ -42,8 +41,7 @@ from .contrib.test_framework.messages import ( CTxIn, CTxInWitness, CTxOut, - FromHex, - ToHex) + FromHex) from .contrib.test_framework.script import ( CScript, @@ -110,7 +108,8 @@ class BTCInterface(CoinInterface): rv += output.nValue return rv - def compareFeeRates(self, a, b): + @staticmethod + def compareFeeRates(a, b): return abs(a - b) < 20 def __init__(self, coin_settings, network): @@ -119,6 +118,9 @@ class BTCInterface(CoinInterface): self._network = network self.blocks_confirmed = coin_settings['blocks_confirmed'] + def coin_name(self): + return chainparams[self.coin_type()]['name'] + def testDaemonRPC(self): self.rpc_callback('getwalletinfo', []) @@ -330,10 +332,10 @@ class BTCInterface(CoinInterface): return tx.serialize(), refund_script, tx.vout[0].nValue - def createScriptLockRefundSpendTx(self, tx_lock_refund_bytes, script_lock_refund, Kal, tx_fee_rate): + def createScriptLockRefundSpendTx(self, tx_lock_refund_bytes, script_lock_refund, pkh_refund_to, tx_fee_rate): # 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 - # When the leader publishes the decrypted signature the leader's coinB spend privatekey will be revealed to the follower + # If the leader publishes the decrypted signature the leader's coinB spend privatekey will be revealed to the follower tx_lock_refund = self.loadTx(tx_lock_refund_bytes) @@ -349,9 +351,8 @@ class BTCInterface(CoinInterface): tx.nVersion = self.txVersion() tx.vin.append(CTxIn(COutPoint(tx_lock_refund_hash_int, locked_n), nSequence=0)) - #pubkeyhash = hash160(self.encodePubkey(Kal)) - pubkeyhash = hash160(Kal) - tx.vout.append(self.txoType(locked_coin, CScript([OP_0, pubkeyhash]))) + #pubkeyhash = hash160(Kal) + tx.vout.append(self.txoType(locked_coin, CScript([OP_0, pkh_refund_to]))) witness_bytes = len(script_lock_refund) witness_bytes += 73 * 2 # 2 signatures (72 + 1 byte size) @@ -367,8 +368,11 @@ class BTCInterface(CoinInterface): return tx.serialize() - def createScriptLockRefundSpendToFTx(self, tx_lock_refund, script_lock_refund, pkh_dest, tx_fee_rate): + def createScriptLockRefundSpendToFTx(self, tx_lock_refund_bytes, script_lock_refund, pkh_dest, tx_fee_rate): # Sends the coinA locked coin to the follower + + tx_lock_refund = self.loadTx(tx_lock_refund_bytes) + output_script = CScript([OP_0, hashlib.sha256(script_lock_refund).digest()]) locked_n = findOutput(tx_lock_refund, output_script) assert_cond(locked_n is not None, 'Output not found in tx') @@ -575,10 +579,11 @@ class BTCInterface(CoinInterface): assert_cond(len(tx.vout) == 1, 'tx doesn\'t have one output') - p2wpkh = CScript([OP_0, hash160(Kal)]) - locked_n = findOutput(tx, p2wpkh) - assert_cond(locked_n is not None, 'Output not found in lock refund spend tx') - tx_value = tx.vout[locked_n].nValue + # Destination doesn't matter to the follower + #p2wpkh = CScript([OP_0, hash160(Kal)]) + #locked_n = findOutput(tx, p2wpkh) + #assert_cond(locked_n is not None, 'Output not found in lock refund spend tx') + tx_value = tx.vout[0].nValue fee_paid = prevout_value - tx_value assert(fee_paid > 0) @@ -661,27 +666,28 @@ class BTCInterface(CoinInterface): sig_hash = SegwitV0SignatureHash(prevout_script, tx, prevout_n, SIGHASH_ALL, prevout_value) return ecdsaotves_enc_sign(key_sign, pubkey_encrypt, sig_hash) - #return otves.EncSign(key_sign, key_encrypt, sig_hash) - def verifyTxOtVES(self, tx_bytes, sig, Ks, Ke, prevout_n, prevout_script, prevout_value): + def verifyTxOtVES(self, tx_bytes, ct, Ks, Ke, prevout_n, prevout_script, prevout_value): tx = self.loadTx(tx_bytes) sig_hash = SegwitV0SignatureHash(prevout_script, tx, prevout_n, SIGHASH_ALL, prevout_value) - return ecdsaotves_enc_verify(Ks, Ke, sig_hash, sig) - #return otves.EncVrfy(Ks, Ke, sig_hash, sig) + return ecdsaotves_enc_verify(Ks, Ke, sig_hash, ct) def decryptOtVES(self, k, esig): return ecdsaotves_dec_sig(k, esig) + b'\x01' # 0x1 is SIGHASH_ALL - #return otves.DecSig(k, esig) + b'\x01' # 0x1 is SIGHASH_ALL def verifyTxSig(self, tx_bytes, sig, K, prevout_n, prevout_script, prevout_value): tx = self.loadTx(tx_bytes) sig_hash = SegwitV0SignatureHash(prevout_script, tx, prevout_n, SIGHASH_ALL, prevout_value) ecK = ECPubKey() - #ecK.set_int(K.x(), K.y()) ecK.set(K) return ecK.verify_ecdsa(sig[: -1], sig_hash) # Pop the hashtype byte + def verifySig(self, pubkey, signed_hash, sig): + ecK = ECPubKey() + ecK.set(pubkey) + return ecK.verify_ecdsa(sig, signed_hash) + def fundTx(self, tx, feerate): feerate_str = format_amount(feerate, self.exp()) # TODO: unlock unspents if bid cancelled diff --git a/basicswap/interface_part.py b/basicswap/interface_part.py index 9d39abd..e1e5b57 100644 --- a/basicswap/interface_part.py +++ b/basicswap/interface_part.py @@ -10,7 +10,7 @@ from .contrib.test_framework.messages import ( ) from .interface_btc import BTCInterface -from .chainparams import CoinInterface, Coins +from .chainparams import Coins from .rpc import make_rpc_func diff --git a/basicswap/interface_xmr.py b/basicswap/interface_xmr.py index 05a18cf..dbc7b83 100644 --- a/basicswap/interface_xmr.py +++ b/basicswap/interface_xmr.py @@ -280,7 +280,6 @@ class XMRInterface(CoinInterface): return None - def spendBLockTx(self, address_to, kbv, kbs, cb_swap_value, b_fee_rate, restore_height): Kbv = self.getPubkey(kbv) diff --git a/basicswap/rpc_xmr.py b/basicswap/rpc_xmr.py index 9f15650..199b42e 100644 --- a/basicswap/rpc_xmr.py +++ b/basicswap/rpc_xmr.py @@ -82,4 +82,3 @@ def make_xmr_wallet_rpc_func(port, auth): nonlocal port, auth return callrpc_xmr(port, auth, method, params) return rpc_func - diff --git a/bin/basicswap_prepare.py b/bin/basicswap_prepare.py index a277a55..7f1929a 100755 --- a/bin/basicswap_prepare.py +++ b/bin/basicswap_prepare.py @@ -128,6 +128,8 @@ def prepareCore(coin, version, settings, data_dir): if coin == 'monero': url = 'https://downloads.getmonero.org/cli/monero-linux-x64-v${}.tar.bz2'.format(version) + # TODO + release_path = os.path.join(bin_dir, release_filename) if not os.path.exists(release_path): downloadFile(release_url, release_path) diff --git a/doc/notes.md b/doc/notes.md new file mode 100644 index 0000000..057fef5 --- /dev/null +++ b/doc/notes.md @@ -0,0 +1,6 @@ + +## Run One Test + +``` +python setup.py test -s tests.basicswap.test_xmr.Test.test_02_leader_recover_a_lock_tx +``` diff --git a/tests/basicswap/test_other.py b/tests/basicswap/test_other.py index b1e4fd5..014f6b1 100644 --- a/tests/basicswap/test_other.py +++ b/tests/basicswap/test_other.py @@ -6,6 +6,7 @@ # file LICENSE or http://www.opensource.org/licenses/mit-license.php. import unittest +import secrets import basicswap.contrib.ed25519_fast as edf import basicswap.ed25519_fast_util as edu @@ -13,6 +14,14 @@ import basicswap.ed25519_fast_util as edu from basicswap.ecc_util import i2b from coincurve.ed25519 import ed25519_get_pubkey +from coincurve.ecdsaotves import ( + ecdsaotves_enc_sign, + ecdsaotves_enc_verify, + ecdsaotves_dec_sig, + ecdsaotves_rec_enc_key) + +from basicswap.interface_btc import BTCInterface +from basicswap.interface_xmr import XMRInterface from basicswap.util import ( SerialiseNum, @@ -30,6 +39,9 @@ from basicswap.basicswap import ( SEQUENCE_LOCK_TIME, ) +from basicswap.ecc_util import ( + i2b) + class Test(unittest.TestCase): def test_serialise_num(self): @@ -152,6 +164,36 @@ class Test(unittest.TestCase): pubkey_test = ed25519_get_pubkey(privkey_bytes) assert(pubkey == pubkey_test) + def test_ecdsa_otves(self): + coin_settings = {'rpcport': 0, 'rpcauth': 'none', 'blocks_confirmed': 1} + ci = BTCInterface(coin_settings, 'regtest') + vk_sign = i2b(ci.getNewSecretKey()) + vk_encrypt = i2b(ci.getNewSecretKey()) + + pk_sign = ci.getPubkey(vk_sign) + pk_encrypt = ci.getPubkey(vk_encrypt) + sign_hash = secrets.token_bytes(32) + + cipher_text = ecdsaotves_enc_sign(vk_sign, pk_encrypt, sign_hash) + + assert(ecdsaotves_enc_verify(pk_sign, pk_encrypt, sign_hash, cipher_text)) + + sig = ecdsaotves_dec_sig(vk_encrypt, cipher_text) + + assert(ci.verifySig(pk_sign, sign_hash, sig)) + + recovered_key = ecdsaotves_rec_enc_key(pk_encrypt, cipher_text, sig) + + assert(vk_encrypt == recovered_key) + + def test_dleag(self): + coin_settings = {'rpcport': 0, 'walletrpcport': 0, 'walletrpcauth': 'none', 'blocks_confirmed': 1} + ci = XMRInterface(coin_settings, 'regtest') + + key = i2b(ci.getNewSecretKey()) + proof = ci.proveDLEAG(key) + assert(ci.verifyDLEAG(proof)) + if __name__ == '__main__': unittest.main() diff --git a/tests/basicswap/test_xmr.py b/tests/basicswap/test_xmr.py index 81ad416..cebed46 100644 --- a/tests/basicswap/test_xmr.py +++ b/tests/basicswap/test_xmr.py @@ -6,7 +6,6 @@ # file LICENSE or http://www.opensource.org/licenses/mit-license.php. import os -import sys import json import time import shutil @@ -17,14 +16,6 @@ import traceback import threading import subprocess from urllib.request import urlopen -from coincurve.ecdsaotves import ( - ecdsaotves_enc_sign, - ecdsaotves_enc_verify, - ecdsaotves_dec_sig, - ecdsaotves_rec_enc_key) -from coincurve.dleag import ( - dleag_prove, - dleag_verify) import basicswap.config as cfg from basicswap.basicswap import ( @@ -33,6 +24,7 @@ from basicswap.basicswap import ( SwapTypes, BidStates, TxStates, + DebugTypes, SEQUENCE_LOCK_BLOCKS, ) from basicswap.util import ( @@ -201,7 +193,7 @@ def prepare_swapclient_dir(datadir, node_id, network_key, network_pubkey): 'zmqhost': 'tcp://127.0.0.1', 'zmqport': BASE_ZMQ_PORT + node_id, 'htmlhost': 'localhost', - 'htmlport': 12700 + node_id, + 'htmlport': TEST_HTTP_PORT + node_id, 'network_key': network_key, 'network_pubkey': network_pubkey, 'chainclients': { @@ -303,6 +295,7 @@ def callnoderpc(node_id, method, params=[], wallet=None, base_rpc_port=BASE_RPC_ auth = 'test{0}:test_pass{0}'.format(node_id) return callrpc(base_rpc_port + node_id, auth, method, params, wallet) + def run_coins_loop(cls): global stop_test while not stop_test: @@ -313,6 +306,7 @@ def run_coins_loop(cls): callrpc_xmr_na(XMR_BASE_RPC_PORT + 1, 'generateblocks', {'wallet_address': cls.xmr_addr, 'amount_of_blocks': 1}) time.sleep(1.0) + def run_loop(cls): global stop_test while not stop_test: @@ -547,8 +541,6 @@ class Test(unittest.TestCase): offer_id = swap_clients[0].postOffer(Coins.PART, Coins.XMR, 100 * COIN, 10 * XMR_COIN, 100 * COIN, SwapTypes.XMR_SWAP) self.wait_for_offer(swap_clients[1], offer_id) - offer = swap_clients[1].getOffer(offer_id) - offers = swap_clients[1].listOffers(filters={'offer_id': offer_id}) assert(len(offers) == 1) offer = offers[0] @@ -572,7 +564,106 @@ class Test(unittest.TestCase): self.wait_for_bid(swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=180) self.wait_for_bid(swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True) + def test_02_leader_recover_a_lock_tx(self): + logging.info('---------- Test PART to XMR leader recovers coin a lock tx') + swap_clients = self.swap_clients + js_w0_before = json.loads(urlopen('http://localhost:1800/json/wallets').read()) + + offer_id = swap_clients[0].postOffer(Coins.PART, Coins.XMR, 101 * COIN, 1.1 * XMR_COIN, 101 * COIN, SwapTypes.XMR_SWAP, + lock_type=SEQUENCE_LOCK_BLOCKS, lock_value=12) + self.wait_for_offer(swap_clients[1], offer_id) + offer = swap_clients[1].getOffer(offer_id) + + bid_id = swap_clients[1].postXmrBid(offer_id, offer.amount_from) + + self.wait_for_bid(swap_clients[0], bid_id, BidStates.BID_RECEIVED) + + bid, xmr_swap = swap_clients[0].getXmrBid(bid_id) + assert(xmr_swap) + + swap_clients[1].setBidDebugInd(bid_id, DebugTypes.BID_STOP_AFTER_COIN_A_LOCK) + + swap_clients[0].acceptXmrBid(bid_id) + + self.wait_for_bid(swap_clients[0], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, wait_for=180) + self.wait_for_bid(swap_clients[1], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, sent=True) + + js_w0_after = json.loads(urlopen('http://localhost:1800/json/wallets').read()) + print('[rm] js_w0_before', json.dumps(js_w0_before)) + print('[rm] js_w0_after', json.dumps(js_w0_after)) + + def test_03_follower_recover_a_lock_tx(self): + logging.info('---------- Test PART to XMR follower recovers coin a lock tx') + swap_clients = self.swap_clients + + js_w0_before = json.loads(urlopen('http://localhost:1800/json/wallets').read()) + + offer_id = swap_clients[0].postOffer(Coins.PART, Coins.XMR, 101 * COIN, 1.1 * XMR_COIN, 101 * COIN, SwapTypes.XMR_SWAP, + lock_type=SEQUENCE_LOCK_BLOCKS, lock_value=12) + self.wait_for_offer(swap_clients[1], offer_id) + offer = swap_clients[1].getOffer(offer_id) + + bid_id = swap_clients[1].postXmrBid(offer_id, offer.amount_from) + + self.wait_for_bid(swap_clients[0], bid_id, BidStates.BID_RECEIVED) + + bid, xmr_swap = swap_clients[0].getXmrBid(bid_id) + assert(xmr_swap) + + swap_clients[1].setBidDebugInd(bid_id, DebugTypes.BID_STOP_AFTER_COIN_A_LOCK) + swap_clients[0].setBidDebugInd(bid_id, DebugTypes.BID_DONT_SPEND_COIN_A_LOCK_REFUND) + + swap_clients[0].acceptXmrBid(bid_id) + + self.wait_for_bid(swap_clients[0], bid_id, BidStates.BID_ABANDONED, wait_for=180) + self.wait_for_bid(swap_clients[1], bid_id, BidStates.XMR_SWAP_FAILED_SWIPED, sent=True) + + js_w0_after = json.loads(urlopen('http://localhost:1800/json/wallets').read()) + + def test_04_follower_recover_b_lock_tx(self): + logging.info('---------- Test PART to XMR follower recovers coin b lock tx') + + swap_clients = self.swap_clients + + offer_id = swap_clients[0].postOffer(Coins.PART, Coins.XMR, 101 * COIN, 1.1 * XMR_COIN, 101 * COIN, SwapTypes.XMR_SWAP, + lock_type=SEQUENCE_LOCK_BLOCKS, lock_value=18) + self.wait_for_offer(swap_clients[1], offer_id) + offer = swap_clients[1].getOffer(offer_id) + + bid_id = swap_clients[1].postXmrBid(offer_id, offer.amount_from) + + self.wait_for_bid(swap_clients[0], bid_id, BidStates.BID_RECEIVED) + + bid, xmr_swap = swap_clients[0].getXmrBid(bid_id) + assert(xmr_swap) + + swap_clients[1].setBidDebugInd(bid_id, DebugTypes.CREATE_INVALID_COIN_B_LOCK) + + swap_clients[0].acceptXmrBid(bid_id) + + self.wait_for_bid(swap_clients[0], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, wait_for=180) + self.wait_for_bid(swap_clients[1], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, sent=True) + + def test_05_btc_xmr(self): + logging.info('---------- Test BTC to XMR') + swap_clients = self.swap_clients + offer_id = swap_clients[0].postOffer(Coins.BTC, Coins.XMR, 10 * COIN, 100 * XMR_COIN, 100 * COIN, SwapTypes.XMR_SWAP) + self.wait_for_offer(swap_clients[1], offer_id) + offers = swap_clients[1].listOffers(filters={'offer_id': offer_id}) + offer = offers[0] + + bid_id = swap_clients[1].postXmrBid(offer_id, offer.amount_from) + + self.wait_for_bid(swap_clients[0], bid_id, BidStates.BID_RECEIVED) + + bid, xmr_swap = swap_clients[0].getXmrBid(bid_id) + assert(xmr_swap) + + swap_clients[0].acceptXmrBid(bid_id) + + self.wait_for_bid(swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=180) + self.wait_for_bid(swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True) if __name__ == '__main__':