diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index 52a5184..04f4920 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -1153,14 +1153,26 @@ class BasicSwap(BaseApp): raise ValueError('Offer not found') self.loadBidTxns(bid, session) + + coin_from = Coins(offer.coin_from) + coin_to = Coins(offer.coin_to) + if offer.swap_type == SwapTypes.XMR_SWAP: xmr_swap = session.query(XmrSwap).filter_by(bid_id=bid.bid_id).first() self.watchXmrSwap(bid, offer, xmr_swap) + if coin_to.watch_blocks_for_scripts() and bid.xmr_a_lock_tx and bid.xmr_a_lock_tx.chain_height: + if not bid.xmr_b_lock_tx or not bid.xmr_b_lock_tx.txid: + ci_from = self.ci(coin_from) + ci_to = self.ci(coin_to) + chain_a_block_header = ci_from.getBlockHeaderFromHeight(bid.xmr_a_lock_tx.chain_height) + block_time = chain_a_block_header['time'] + chain_b_block_header = ci_to.getBlockHeaderAt(block_time) + dest_script = ci_to.getPkDest(xmr_swap.pkbs) + self.addWatchedScript(ci_to.coin_type(), bid.bid_id, dest_script, TxTypes.XMR_SWAP_B_LOCK) + self.setLastHeightCheckedStart(ci_to.coin_type(), chain_b_block_header['height']) else: self.swaps_in_progress[bid.bid_id] = (bid, offer) - coin_from = Coins(offer.coin_from) - coin_to = Coins(offer.coin_to) if bid.initiate_tx and bid.initiate_tx.txid: self.addWatchedOutput(coin_from, bid.bid_id, bid.initiate_tx.txid.hex(), bid.initiate_tx.vout, BidStates.SWAP_INITIATED) if bid.participate_tx and bid.participate_tx.txid: @@ -1632,16 +1644,12 @@ class BasicSwap(BaseApp): if swap_type == SwapTypes.XMR_SWAP: xmr_offer = XmrOffer() - if reverse_bid: - # Delay before the chain a lock refund tx can be mined - xmr_offer.lock_time_1 = ci_to.getExpectedSequence(lock_type, lock_value) - # Delay before the follower can spend from the chain a lock refund tx - xmr_offer.lock_time_2 = ci_to.getExpectedSequence(lock_type, lock_value) - else: - # Delay before the chain a lock refund tx can be mined - xmr_offer.lock_time_1 = ci_from.getExpectedSequence(lock_type, lock_value) - # Delay before the follower can spend from the chain a lock refund tx - xmr_offer.lock_time_2 = ci_from.getExpectedSequence(lock_type, lock_value) + chain_a_ci = ci_to if reverse_bid else ci_from + lock_value_2 = lock_value + 1000 if (None, DebugTypes.OFFER_LOCK_2_VALUE_INC) in self._debug_cases else lock_value + # Delay before the chain a lock refund tx can be mined + xmr_offer.lock_time_1 = chain_a_ci.getExpectedSequence(lock_type, lock_value) + # Delay before the follower can spend from the chain a lock refund tx + xmr_offer.lock_time_2 = chain_a_ci.getExpectedSequence(lock_type, lock_value_2) xmr_offer.a_fee_rate = msg_buf.fee_rate_from xmr_offer.b_fee_rate = msg_buf.fee_rate_to # Unused: TODO - Set priority? @@ -2643,8 +2651,9 @@ class BasicSwap(BaseApp): txid = ci_from.publishTx(bid.initiate_txn_refund) self.log.error('Submit refund_txn unexpectedly worked: ' + txid) except Exception as ex: - if 'non-BIP68-final' not in str(ex) and 'non-final' not in str(ex): + if ci_from.isTxNonFinalError(str(ex)) is False: self.log.error('Submit refund_txn unexpected error' + str(ex)) + raise ex if txid is not None: msg_buf = BidAcceptMessage() @@ -2654,7 +2663,6 @@ class BasicSwap(BaseApp): # pkh sent in script is hashed with sha256, Decred expects blake256 if bid.pkhash_seller != pkhash_refund: - assert (ci_to.coin_type() == Coins.DCR or ci_from.coin_type() == Coins.DCR) # [rm] msg_buf.pkhash_seller = bid.pkhash_seller bid_bytes = msg_buf.SerializeToString() @@ -2785,7 +2793,7 @@ class BasicSwap(BaseApp): msg_buf.amount_to = amount_to address_out = self.getReceiveAddressFromPool(coin_from, offer_id, TxTypes.XMR_SWAP_A_LOCK) - if coin_from == Coins.PART_BLIND: + if coin_from in (Coins.PART_BLIND, ): addrinfo = ci_from.rpc('getaddressinfo', [address_out]) msg_buf.dest_af = bytes.fromhex(addrinfo['pubkey']) else: @@ -3025,7 +3033,7 @@ class BasicSwap(BaseApp): msg_buf.a_lock_tx = xmr_swap.a_lock_tx msg_buf.a_lock_tx_script = xmr_swap.a_lock_tx_script msg_buf.a_lock_refund_tx = xmr_swap.a_lock_refund_tx - msg_buf.a_lock_refund_tx_script = xmr_swap.a_lock_refund_tx_script + msg_buf.a_lock_refund_tx_script = bytes(xmr_swap.a_lock_refund_tx_script) msg_buf.a_lock_refund_spend_tx = xmr_swap.a_lock_refund_spend_tx msg_buf.al_lock_refund_tx_sig = xmr_swap.al_lock_refund_tx_sig @@ -3327,9 +3335,6 @@ class BasicSwap(BaseApp): secret = self.getContractSecret(bid_date, bid.contract_count) ensure(len(secret) == 32, 'Bad secret length') - self.log.debug('secret {}'.format(secret.hex())) - self.log.debug('sha256(secret) {}'.format(sha256(secret).hex())) - if self.coin_clients[coin_type]['connection_type'] != 'rpc': return None @@ -3475,14 +3480,7 @@ class BasicSwap(BaseApp): else: privkey_wif = self.ci(Coins.PART).encodeKey(privkey) refund_sig = self.callcoinrpc(Coins.PART, 'createsignaturewithkey', [refund_txn, prevout, privkey_wif, 'ALL', options]) - if coin_type in (Coins.DCR, ): - witness_stack = [ - bytes.fromhex(refund_sig), - pubkey, - (OpCodes.OP_0).to_bytes(1), - txn_script] - refund_txn = ci.setTxSignature(bytes.fromhex(refund_txn), witness_stack).hex() - elif coin_type in (Coins.PART, ) or self.coin_clients[coin_type]['use_segwit']: + if coin_type in (Coins.PART, Coins.DCR) or self.coin_clients[coin_type]['use_segwit']: witness_stack = [ bytes.fromhex(refund_sig), pubkey, @@ -3706,8 +3704,18 @@ class BasicSwap(BaseApp): def findTxB(self, ci_to, xmr_swap, bid, session, bid_sender: bool) -> bool: 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, bid.chain_b_height_start, bid_sender) + + found_tx = None + if ci_to.coin_type() in (Coins.DCR, ): + if bid.xmr_b_lock_tx is None or bid.xmr_b_lock_tx.txid is None: + # Watching chain for dest_address with WatchedScript + pass + else: + dest_address = ci_to.pkh_to_address(ci_to.pkh(xmr_swap.pkbs)) + found_tx = ci_to.getLockTxHeight(bid.xmr_b_lock_tx.txid, dest_address, bid.amount_to, bid.chain_b_height_start, vout=bid.xmr_b_lock_tx.vout) + else: + # 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, bid.chain_b_height_start, bid_sender) if isinstance(found_tx, int) and found_tx == -1: if self.countBidEvents(bid, EventLogTypes.LOCK_TX_B_INVALID, session) < 1: @@ -3716,20 +3724,18 @@ class BasicSwap(BaseApp): elif found_tx is not None: if found_tx['height'] != 0 and (bid.xmr_b_lock_tx is None or not bid.xmr_b_lock_tx.chain_height): self.logBidEvent(bid.bid_id, EventLogTypes.LOCK_TX_B_SEEN, '', session) - if bid.xmr_b_lock_tx is None: + + if bid.xmr_b_lock_tx is None or bid.xmr_b_lock_tx.chain_height is None: self.log.debug('Found {} lock tx in chain'.format(ci_to.coin_name())) xmr_swap.b_lock_tx_id = bytes.fromhex(found_tx['txid']) + if bid.xmr_b_lock_tx is None: bid.xmr_b_lock_tx = SwapTx( bid_id=bid.bid_id, tx_type=TxTypes.XMR_SWAP_B_LOCK, txid=xmr_swap.b_lock_tx_id, - chain_height=found_tx['height'], ) - bid_changed = True - bid.xmr_b_lock_tx.setState(TxStates.TX_IN_CHAIN) - else: - bid.xmr_b_lock_tx.chain_height = found_tx['height'] - bid_changed = True + bid.xmr_b_lock_tx.chain_height = found_tx['height'] + bid_changed = True return bid_changed def checkXmrBidState(self, bid_id: bytes, bid, offer): @@ -3785,7 +3791,7 @@ class BasicSwap(BaseApp): self.saveBidInSession(bid_id, bid, session, xmr_swap) session.commit() - if TxTypes.XMR_SWAP_A_LOCK_REFUND_SWIPE not in bid.txns and BidStates(bid.state) != BidStates.BID_STALLED_FOR_TEST: + 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.bid_id, EventLogTypes.LOCK_TX_A_REFUND_SWIPE_TX_PUBLISHED, '', session) @@ -3827,7 +3833,7 @@ class BasicSwap(BaseApp): session.commit() return rv except Exception as ex: - if 'Transaction already in block chain' in str(ex): + if ci_from.isTxExistsError(str(ex)): self.log.info('Found coin a lock refund tx for bid {}'.format(bid_id.hex())) txid = ci_from.getTxid(xmr_swap.a_lock_refund_tx) if TxTypes.XMR_SWAP_A_LOCK_REFUND not in bid.txns: @@ -3882,6 +3888,7 @@ class BasicSwap(BaseApp): if lock_tx_chain_info['depth'] >= ci_from.blocks_confirmed: self.logBidEvent(bid.bid_id, EventLogTypes.LOCK_TX_A_CONFIRMED, '', session) bid.xmr_a_lock_tx.setState(TxStates.TX_CONFIRMED) + bid.setState(BidStates.XMR_SWAP_SCRIPT_COIN_LOCKED) bid_changed = True @@ -3890,6 +3897,13 @@ class BasicSwap(BaseApp): self.log.info('Sending adaptor-sig swap chain B lock tx for bid %s in %d seconds', bid_id.hex(), delay) self.createActionInSession(delay, ActionTypes.SEND_XMR_SWAP_LOCK_TX_B, bid_id, session) # bid.setState(BidStates.SWAP_DELAYING) + elif ci_to.watch_blocks_for_scripts(): + chain_a_block_header = ci_from.getBlockHeaderFromHeight(bid.xmr_a_lock_tx.chain_height) + block_time = chain_a_block_header['time'] + chain_b_block_header = ci_to.getBlockHeaderAt(block_time) + dest_script = ci_to.getPkDest(xmr_swap.pkbs) + self.addWatchedScript(ci_to.coin_type(), bid.bid_id, dest_script, TxTypes.XMR_SWAP_B_LOCK) + self.setLastHeightCheckedStart(ci_to.coin_type(), chain_b_block_header['height']) if bid_changed: self.saveBidInSession(bid_id, bid, session, xmr_swap) @@ -4131,7 +4145,7 @@ class BasicSwap(BaseApp): self.logEvent(Concepts.BID, bid.bid_id, EventLogTypes.ITX_REFUND_PUBLISHED, '', None) # State will update when spend is detected except Exception as ex: - if 'non-BIP68-final' not in str(ex) and 'non-final' not in str(ex): + if ci_from.isTxNonFinalError(str(ex)) is False: self.log.warning('Error trying to submit initiate refund txn: %s', str(ex)) if bid.getPTxState() in (TxStates.TX_SENT, TxStates.TX_CONFIRMED) \ @@ -4142,7 +4156,7 @@ class BasicSwap(BaseApp): self.logEvent(Concepts.BID, bid.bid_id, EventLogTypes.PTX_REFUND_PUBLISHED, '', None) # State will update when spend is detected except Exception as ex: - if 'non-BIP68-final' not in str(ex) and 'non-final' not in str(ex) and 'locks on inputs not met' not in str(ex): + if ci_to.isTxNonFinalError(str(ex)): self.log.warning('Error trying to submit participate refund txn: %s', str(ex)) return False # Bid is still active @@ -4298,6 +4312,7 @@ class BasicSwap(BaseApp): state = BidStates(bid.state) spending_txid = bytes.fromhex(spend_txid_hex) + bid.xmr_a_lock_tx.spend_txid = spending_txid if spending_txid == xmr_swap.a_lock_spend_tx_id: if state == BidStates.XMR_SWAP_LOCK_RELEASED: xmr_swap.a_lock_spend_tx = bytes.fromhex(spend_txn_hex) @@ -4416,6 +4431,16 @@ class BasicSwap(BaseApp): self.saveBid(watched_script.bid_id, bid) else: self.log.warning('Could not find active bid for found watched script: {}'.format(watched_script.bid_id.hex())) + elif watched_script.tx_type == TxTypes.XMR_SWAP_B_LOCK: + bid = self.swaps_in_progress[watched_script.bid_id][0] + bid.xmr_b_lock_tx = SwapTx( + bid_id=watched_script.bid_id, + tx_type=TxTypes.XMR_SWAP_B_LOCK, + txid=txid, + vout=vout, + ) + bid.xmr_b_lock_tx.setState(TxStates.TX_IN_CHAIN) + self.saveBid(watched_script.bid_id, bid) else: self.log.warning('Unknown found watched script tx type for bid {}'.format(watched_script.bid_id.hex())) @@ -4852,12 +4877,12 @@ class BasicSwap(BaseApp): xmr_offer.offer_id = offer_id - if reverse_bid: - xmr_offer.lock_time_1 = ci_to.getExpectedSequence(offer_data.lock_type, offer_data.lock_value) - xmr_offer.lock_time_2 = ci_to.getExpectedSequence(offer_data.lock_type, offer_data.lock_value) - else: - xmr_offer.lock_time_1 = ci_from.getExpectedSequence(offer_data.lock_type, offer_data.lock_value) - xmr_offer.lock_time_2 = ci_from.getExpectedSequence(offer_data.lock_type, offer_data.lock_value) + chain_a_ci = ci_to if reverse_bid else ci_from + lock_value_2 = offer_data.lock_value + if (None, DebugTypes.OFFER_LOCK_2_VALUE_INC) in self._debug_cases: + lock_value_2 += 1000 + xmr_offer.lock_time_1 = chain_a_ci.getExpectedSequence(offer_data.lock_type, offer_data.lock_value) + xmr_offer.lock_time_2 = chain_a_ci.getExpectedSequence(offer_data.lock_type, lock_value_2) xmr_offer.a_fee_rate = offer_data.fee_rate_from xmr_offer.b_fee_rate = offer_data.fee_rate_to @@ -5618,6 +5643,8 @@ class BasicSwap(BaseApp): a_fee_rate, xmr_swap.vkbv) xmr_swap.a_lock_spend_tx_id = ci_from.getTxid(xmr_swap.a_lock_spend_tx) + if bid.xmr_a_lock_tx: + bid.xmr_a_lock_tx.spend_txid = xmr_swap.a_lock_spend_tx_id prevout_amount = ci_from.getLockTxSwapOutputValue(bid, xmr_swap) xmr_swap.al_lock_spend_tx_esig = ci_from.signTxOtVES(kal, xmr_swap.pkasf, xmr_swap.a_lock_spend_tx, 0, xmr_swap.a_lock_tx_script, prevout_amount) ''' @@ -5628,33 +5655,37 @@ class BasicSwap(BaseApp): xmr_swap.a_lock_tx, xmr_swap.a_lock_tx_script, xmr_swap.dest_af, a_fee_rate, xmr_swap.vkbv) ''' - delay = self.get_short_delay_event_seconds() - self.log.info('Sending lock spend tx message for bid %s in %d seconds', bid_id.hex(), delay) - self.createActionInSession(delay, ActionTypes.SEND_XMR_SWAP_LOCK_SPEND_MSG, bid_id, session) + lock_tx_sent: bool = False # publishalocktx if bid.xmr_a_lock_tx and bid.xmr_a_lock_tx.state: if bid.xmr_a_lock_tx.state >= TxStates.TX_SENT: - raise ValueError('Lock tx has already been sent {}'.format(bid.xmr_a_lock_tx.txid.hex())) + self.log.warning('Lock tx has already been sent {}'.format(bid.xmr_a_lock_tx.txid.hex())) + lock_tx_sent = True - lock_tx_signed = ci_from.signTxWithWallet(xmr_swap.a_lock_tx) - txid_hex = ci_from.publishTx(lock_tx_signed) + if lock_tx_sent is False: + lock_tx_signed = ci_from.signTxWithWallet(xmr_swap.a_lock_tx) + txid_hex = ci_from.publishTx(lock_tx_signed) - vout_pos = ci_from.getTxOutputPos(xmr_swap.a_lock_tx, xmr_swap.a_lock_tx_script) - self.log.debug('Submitted lock txn %s to %s chain for bid %s', txid_hex, ci_from.coin_name(), bid_id.hex()) + vout_pos = ci_from.getTxOutputPos(xmr_swap.a_lock_tx, xmr_swap.a_lock_tx_script) + self.log.debug('Submitted lock txn %s to %s chain for bid %s', txid_hex, ci_from.coin_name(), bid_id.hex()) - if bid.xmr_a_lock_tx is None: - bid.xmr_a_lock_tx = SwapTx( - bid_id=bid_id, - tx_type=TxTypes.XMR_SWAP_A_LOCK, - txid=bytes.fromhex(txid_hex), - vout=vout_pos, - ) - bid.xmr_a_lock_tx.setState(TxStates.TX_SENT) + if bid.xmr_a_lock_tx is None: + bid.xmr_a_lock_tx = SwapTx( + bid_id=bid_id, + tx_type=TxTypes.XMR_SWAP_A_LOCK, + txid=bytes.fromhex(txid_hex), + vout=vout_pos, + ) + bid.xmr_a_lock_tx.setState(TxStates.TX_SENT) + self.logBidEvent(bid.bid_id, EventLogTypes.LOCK_TX_A_PUBLISHED, '', session) bid.setState(BidStates.XMR_SWAP_HAVE_SCRIPT_COIN_SPEND_TX) self.watchXmrSwap(bid, offer, xmr_swap) - self.logBidEvent(bid.bid_id, EventLogTypes.LOCK_TX_A_PUBLISHED, '', session) + + delay = self.get_short_delay_event_seconds() + self.log.info('Sending lock spend tx message for bid %s in %d seconds', bid_id.hex(), delay) + self.createActionInSession(delay, ActionTypes.SEND_XMR_SWAP_LOCK_SPEND_MSG, bid_id, session) self.saveBidInSession(bid_id, bid, session, xmr_swap) @@ -5802,8 +5833,10 @@ class BasicSwap(BaseApp): v = ci_from.verifyTxSig(xmr_swap.a_lock_spend_tx, af_lock_spend_sig, xmr_swap.pkaf, 0, xmr_swap.a_lock_tx_script, prevout_amount) ensure(v, 'Invalid coin A lock tx spend tx follower sig') - witness_stack = [ - b'', + witness_stack = [] + if coin_from not in (Coins.DCR,): + witness_stack += [b'',] + witness_stack += [ al_lock_spend_sig, af_lock_spend_sig, xmr_swap.a_lock_tx_script, @@ -5868,7 +5901,8 @@ class BasicSwap(BaseApp): else: address_to = self.getReceiveAddressFromPool(coin_to, bid_id, TxTypes.XMR_SWAP_B_LOCK_SPEND) - txid = ci_to.spendBLockTx(xmr_swap.b_lock_tx_id, address_to, xmr_swap.vkbv, vkbs, bid.amount_to, b_fee_rate, bid.chain_b_height_start) + lock_tx_vout = bid.getLockTXBVout() + txid = ci_to.spendBLockTx(xmr_swap.b_lock_tx_id, address_to, xmr_swap.vkbv, vkbs, bid.amount_to, b_fee_rate, bid.chain_b_height_start, lock_tx_vout=lock_tx_vout) 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: @@ -5933,7 +5967,9 @@ class BasicSwap(BaseApp): address_to = self.getCachedStealthAddressForCoin(coin_to) else: address_to = self.getReceiveAddressFromPool(coin_to, bid_id, TxTypes.XMR_SWAP_B_LOCK_REFUND) - txid = ci_to.spendBLockTx(xmr_swap.b_lock_tx_id, address_to, xmr_swap.vkbv, vkbs, bid.amount_to, b_fee_rate, bid.chain_b_height_start) + + lock_tx_vout = bid.getLockTXBVout() + txid = ci_to.spendBLockTx(xmr_swap.b_lock_tx_id, address_to, xmr_swap.vkbv, vkbs, bid.amount_to, b_fee_rate, bid.chain_b_height_start, lock_tx_vout=lock_tx_vout) 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.bid_id, EventLogTypes.LOCK_TX_B_REFUND_TX_PUBLISHED, '', session) except Exception as ex: @@ -6041,8 +6077,10 @@ class BasicSwap(BaseApp): al_lock_refund_spend_tx_sig = ci_from.signTx(kal, xmr_swap.a_lock_refund_spend_tx, 0, xmr_swap.a_lock_refund_tx_script, prevout_amount) self.log.debug('Setting lock refund spend tx sigs') - witness_stack = [ - b'', + witness_stack = [] + if coin_from not in (Coins.DCR, ): + witness_stack += [b'',] + witness_stack += [ al_lock_refund_spend_tx_sig, xmr_swap.af_lock_refund_spend_tx_sig, bytes((1,)), @@ -6099,6 +6137,8 @@ class BasicSwap(BaseApp): try: xmr_swap.a_lock_spend_tx = msg_data.a_lock_spend_tx xmr_swap.a_lock_spend_tx_id = ci_from.getTxid(xmr_swap.a_lock_spend_tx) + if bid.xmr_a_lock_tx: + bid.xmr_a_lock_tx.spend_txid = xmr_swap.a_lock_spend_tx_id xmr_swap.kal_sig = msg_data.kal_sig ci_from.verifySCLockSpendTx( diff --git a/basicswap/basicswap_util.py b/basicswap/basicswap_util.py index 8f53362..f4d8b3d 100644 --- a/basicswap/basicswap_util.py +++ b/basicswap/basicswap_util.py @@ -203,6 +203,7 @@ class DebugTypes(IntEnum): B_LOCK_TX_MISSED_SEND = auto() DUPLICATE_ACTIONS = auto() DONT_CONFIRM_PTX = auto() + OFFER_LOCK_2_VALUE_INC = auto() class NotificationTypes(IntEnum): diff --git a/basicswap/db.py b/basicswap/db.py index 466283a..943ad76 100644 --- a/basicswap/db.py +++ b/basicswap/db.py @@ -192,6 +192,11 @@ class Bid(Base): else: self.states += pack_state(new_state, now) + def getLockTXBVout(self): + if self.xmr_b_lock_tx: + return self.xmr_b_lock_tx.vout + return None + class SwapTx(Base): __tablename__ = 'transactions' diff --git a/basicswap/interface/base.py b/basicswap/interface/base.py index ff69e1c..7a1ce0c 100644 --- a/basicswap/interface/base.py +++ b/basicswap/interface/base.py @@ -44,6 +44,10 @@ class CoinInterface: def watch_blocks_for_scripts() -> bool: return False + @staticmethod + def compareFeeRates(a, b) -> bool: + return abs(a - b) < 20 + def __init__(self, network): self.setDefaults() self._network = network @@ -149,8 +153,44 @@ class CoinInterface: def use_tx_vsize(self) -> bool: return self._use_segwit + def getLockTxSwapOutputValue(self, bid, xmr_swap): + return bid.amount -class Secp256k1Interface(CoinInterface): + def getLockRefundTxSwapOutputValue(self, bid, xmr_swap): + return xmr_swap.a_swap_refund_value + + def getLockRefundTxSwapOutput(self, xmr_swap): + # Only one prevout exists + return 0 + + +class AdaptorSigInterface(): + def getScriptLockTxDummyWitness(self, script: bytes): + return [ + b'', + bytes(72), + bytes(72), + bytes(len(script)) + ] + + def getScriptLockRefundSpendTxDummyWitness(self, script: bytes): + return [ + b'', + bytes(72), + bytes(72), + bytes((1,)), + bytes(len(script)) + ] + + def getScriptLockRefundSwipeTxDummyWitness(self, script: bytes): + return [ + bytes(72), + b'', + bytes(len(script)) + ] + + +class Secp256k1Interface(CoinInterface, AdaptorSigInterface): @staticmethod def curve_type(): return Curves.secp256k1 @@ -170,3 +210,26 @@ class Secp256k1Interface(CoinInterface): def verifyPubkey(self, pubkey_bytes: bytes) -> bool: return verify_secp256k1_point(pubkey_bytes) + + def isValidAddressHash(self, address_hash: bytes) -> bool: + hash_len = len(address_hash) + if hash_len == 20: + return True + + def isValidPubkey(self, pubkey: bytes) -> bool: + try: + self.verifyPubkey(pubkey) + return True + except Exception: + return False + + def verifySig(self, pubkey: bytes, signed_hash: bytes, sig: bytes) -> bool: + pubkey = PublicKey(pubkey) + return pubkey.verify(sig, signed_hash, hasher=None) + + def sumKeys(self, ka: bytes, kb: bytes) -> bytes: + # TODO: Add to coincurve + return i2b((b2i(ka) + b2i(kb)) % ep.o) + + def sumPubkeys(self, Ka: bytes, Kb: bytes) -> bytes: + return PublicKey.combine_keys([PublicKey(Ka), PublicKey(Kb)]).format() diff --git a/basicswap/interface/btc.py b/basicswap/interface/btc.py index 4f62030..152f57c 100644 --- a/basicswap/interface/btc.py +++ b/basicswap/interface/btc.py @@ -28,7 +28,6 @@ from basicswap.util import ( b2h, i2b, b2i, i2h, ) from basicswap.util.ecc import ( - ep, pointToCPK, CPKToPoint, ) from basicswap.util.script import ( @@ -66,7 +65,6 @@ from basicswap.contrib.test_framework.messages import ( CTxIn, CTxInWitness, CTxOut, - uint256_from_str, ) from basicswap.contrib.test_framework.script import ( CScript, CScriptOp, @@ -120,6 +118,57 @@ def find_vout_for_address_from_txobj(tx_obj, addr: str) -> int: raise RuntimeError("Vout not found for address: txid={}, addr={}".format(tx_obj['txid'], addr)) +def extractScriptLockScriptValues(script_bytes: bytes) -> (bytes, bytes): + script_len = len(script_bytes) + ensure(script_len == 71, 'Bad script length') + o = 0 + ensure_op(script_bytes[o] == OP_2) + ensure_op(script_bytes[o + 1] == 33) + o += 2 + pk1 = script_bytes[o: o + 33] + o += 33 + ensure_op(script_bytes[o] == 33) + o += 1 + pk2 = script_bytes[o: o + 33] + o += 33 + ensure_op(script_bytes[o] == OP_2) + ensure_op(script_bytes[o + 1] == OP_CHECKMULTISIG) + + return pk1, pk2 + + +def extractScriptLockRefundScriptValues(script_bytes: bytes): + script_len = len(script_bytes) + ensure(script_len > 73, 'Bad script length') + ensure_op(script_bytes[0] == OP_IF) + ensure_op(script_bytes[1] == OP_2) + ensure_op(script_bytes[2] == 33) + pk1 = script_bytes[3: 3 + 33] + ensure_op(script_bytes[36] == 33) + pk2 = script_bytes[37: 37 + 33] + ensure_op(script_bytes[70] == OP_2) + ensure_op(script_bytes[71] == OP_CHECKMULTISIG) + ensure_op(script_bytes[72] == OP_ELSE) + o = 73 + csv_val, nb = decodeScriptNum(script_bytes, o) + o += nb + + ensure(script_len == o + 5 + 33, 'Bad script length') # Fails if script too long + ensure_op(script_bytes[o] == OP_CHECKSEQUENCEVERIFY) + o += 1 + ensure_op(script_bytes[o] == OP_DROP) + o += 1 + ensure_op(script_bytes[o] == 33) + o += 1 + pk3 = script_bytes[o: o + 33] + o += 33 + ensure_op(script_bytes[o] == OP_CHECKSIG) + o += 1 + ensure_op(script_bytes[o] == OP_ENDIF) + + return pk1, pk2, csv_val, pk3 + + class BTCInterface(Secp256k1Interface): @staticmethod @@ -157,10 +206,6 @@ class BTCInterface(Secp256k1Interface): rv += output.nValue return rv - @staticmethod - def compareFeeRates(a, b) -> bool: - return abs(a - b) < 20 - @staticmethod def xmr_swap_a_lock_spend_tx_vsize() -> int: return 147 @@ -214,6 +259,23 @@ class BTCInterface(Secp256k1Interface): self._log = self._sc.log if self._sc and self._sc.log else logging self._expect_seedid_hex = None + def open_rpc(self, wallet=None): + return openrpc(self._rpcport, self._rpcauth, wallet=wallet, host=self._rpc_host) + + def json_request(self, rpc_conn, method, params): + try: + v = rpc_conn.json_request(method, params) + r = json.loads(v.decode('utf-8')) + except Exception as ex: + traceback.print_exc() + raise ValueError('RPC Server Error ' + str(ex)) + if 'error' in r and r['error'] is not None: + raise ValueError('RPC error ' + str(r['error'])) + return r['result'] + + def close_rpc(self, rpc_conn): + rpc_conn.close() + def checkWallets(self) -> int: wallets = self.rpc('listwallets') @@ -231,25 +293,6 @@ class BTCInterface(Secp256k1Interface): return len(wallets) - def open_rpc(self, wallet=None): - return openrpc(self._rpcport, self._rpcauth, wallet=wallet, host=self._rpc_host) - - def json_request(self, rpc_conn, method, params): - try: - v = rpc_conn.json_request(method, params) - r = json.loads(v.decode('utf-8')) - except Exception as ex: - traceback.print_exc() - raise ValueError('RPC Server Error ' + str(ex)) - - if 'error' in r and r['error'] is not None: - raise ValueError('RPC error ' + str(r['error'])) - - return r['result'] - - def close_rpc(self, rpc_conn): - rpc_conn.close() - def testDaemonRPC(self, with_wallet=True) -> None: self.rpc_wallet('getwalletinfo' if with_wallet else 'getblockchaininfo') @@ -278,7 +321,7 @@ class BTCInterface(Secp256k1Interface): max_tries = 5000 for i in range(max_tries): - prev_block_header = self.rpc('getblock', [last_block_header['previousblockhash']]) + prev_block_header = self.rpc('getblockheader', [last_block_header['previousblockhash']]) if prev_block_header['time'] <= time: return last_block_header if block_after else prev_block_header @@ -343,18 +386,6 @@ class BTCInterface(Secp256k1Interface): self._log.debug('validateaddress failed: {}'.format(address)) return False - def isValidAddressHash(self, address_hash: bytes) -> bool: - hash_len = len(address_hash) - if hash_len == 20: - return True - - def isValidPubkey(self, pubkey: bytes) -> bool: - try: - self.verifyPubkey(pubkey) - return True - except Exception: - return False - def isAddressMine(self, address: str, or_watch_only: bool = False) -> bool: addr_info = self.rpc_wallet('getaddressinfo', [address]) if not or_watch_only: @@ -468,13 +499,6 @@ class BTCInterface(Secp256k1Interface): def decodeKey(self, k: str) -> bytes: return decodeWif(k) - def sumKeys(self, ka, kb): - # TODO: Add to coincurve - return i2b((b2i(ka) + b2i(kb)) % ep.o) - - def sumPubkeys(self, Ka, Kb): - return PublicKey.combine_keys([PublicKey(Ka), PublicKey(Kb)]).format() - def getScriptForPubkeyHash(self, pkh: bytes) -> CScript: # p2wpkh return CScript([OP_0, pkh]) @@ -485,24 +509,6 @@ class BTCInterface(Secp256k1Interface): tx.deserialize(BytesIO(tx_bytes)) return tx - def extractScriptLockScriptValues(self, script_bytes: bytes): - script_len = len(script_bytes) - ensure(script_len == 71, 'Bad script length') - o = 0 - ensure_op(script_bytes[o] == OP_2) - ensure_op(script_bytes[o + 1] == 33) - o += 2 - pk1 = script_bytes[o: o + 33] - o += 33 - ensure_op(script_bytes[o] == 33) - o += 1 - pk2 = script_bytes[o: o + 33] - o += 33 - ensure_op(script_bytes[o] == OP_2) - ensure_op(script_bytes[o + 1] == OP_CHECKMULTISIG) - - return pk1, pk2 - def createSCLockTx(self, value: int, script: bytearray, vkbv: bytes = None) -> bytes: tx = CTransaction() tx.nVersion = self.txVersion() @@ -512,37 +518,6 @@ class BTCInterface(Secp256k1Interface): def fundSCLockTx(self, tx_bytes, feerate, vkbv=None): return self.fundTx(tx_bytes, feerate) - def extractScriptLockRefundScriptValues(self, script_bytes: bytes): - script_len = len(script_bytes) - ensure(script_len > 73, 'Bad script length') - ensure_op(script_bytes[0] == OP_IF) - ensure_op(script_bytes[1] == OP_2) - ensure_op(script_bytes[2] == 33) - pk1 = script_bytes[3: 3 + 33] - ensure_op(script_bytes[36] == 33) - pk2 = script_bytes[37: 37 + 33] - ensure_op(script_bytes[70] == OP_2) - ensure_op(script_bytes[71] == OP_CHECKMULTISIG) - ensure_op(script_bytes[72] == OP_ELSE) - o = 73 - csv_val, nb = decodeScriptNum(script_bytes, o) - o += nb - - ensure(script_len == o + 5 + 33, 'Bad script length') # Fails if script too long - ensure_op(script_bytes[o] == OP_CHECKSEQUENCEVERIFY) - o += 1 - ensure_op(script_bytes[o] == OP_DROP) - o += 1 - ensure_op(script_bytes[o] == 33) - o += 1 - pk3 = script_bytes[o: o + 33] - o += 33 - ensure_op(script_bytes[o] == OP_CHECKSIG) - o += 1 - ensure_op(script_bytes[o] == OP_ENDIF) - - return pk1, pk2, csv_val, pk3 - def genScriptLockRefundTxScript(self, Kal, Kaf, csv_val) -> CScript: Kal_enc = Kal if len(Kal) == 33 else self.encodePubkey(Kal) @@ -634,7 +609,7 @@ class BTCInterface(Secp256k1Interface): ensure(locked_n is not None, 'Output not found in tx') locked_coin = tx_lock_refund.vout[locked_n].nValue - A, B, lock2_value, C = self.extractScriptLockRefundScriptValues(script_lock_refund) + A, B, lock2_value, C = extractScriptLockRefundScriptValues(script_lock_refund) tx_lock_refund.rehash() tx_lock_refund_hash_int = tx_lock_refund.sha256 @@ -721,7 +696,7 @@ class BTCInterface(Secp256k1Interface): ensure(locked_coin == swap_value, 'Bad locked value') # Check script - A, B = self.extractScriptLockScriptValues(script_out) + A, B = extractScriptLockScriptValues(script_out) ensure(A == Kal, 'Bad script pubkey') ensure(B == Kaf, 'Bad script pubkey') @@ -789,7 +764,7 @@ class BTCInterface(Secp256k1Interface): locked_coin = tx.vout[locked_n].nValue # Check script and values - A, B, csv_val, C = self.extractScriptLockRefundScriptValues(script_out) + A, B, csv_val, C = extractScriptLockRefundScriptValues(script_out) ensure(A == Kal, 'Bad script pubkey') ensure(B == Kaf, 'Bad script pubkey') ensure(csv_val == csv_val_expect, 'Bad script csv value') @@ -922,6 +897,9 @@ class BTCInterface(Secp256k1Interface): def decryptOtVES(self, k: bytes, esig: bytes) -> bytes: return ecdsaotves_dec_sig(k, esig) + bytes((SIGHASH_ALL,)) + def recoverEncKey(self, esig, sig, K): + return ecdsaotves_rec_enc_key(K, esig, sig[:-1]) # Strip sighash type + def verifyTxSig(self, tx_bytes: bytes, sig: bytes, K: bytes, input_n: int, prevout_script: bytes, prevout_value: int) -> bool: tx = self.loadTx(tx_bytes) sig_hash = SegwitV0SignatureHash(prevout_script, tx, input_n, SIGHASH_ALL, prevout_value) @@ -929,11 +907,7 @@ class BTCInterface(Secp256k1Interface): pubkey = PublicKey(K) return pubkey.verify(sig[: -1], sig_hash, hasher=None) # Pop the hashtype byte - def verifySig(self, pubkey, signed_hash, sig): - pubkey = PublicKey(pubkey) - return pubkey.verify(sig, signed_hash, hasher=None) - - def fundTx(self, tx, feerate): + def fundTx(self, tx: bytes, feerate) -> bytes: feerate_str = self.format_amount(feerate) # TODO: unlock unspents if bid cancelled options = { @@ -943,7 +917,7 @@ class BTCInterface(Secp256k1Interface): rv = self.rpc_wallet('fundrawtransaction', [tx.hex(), options]) return bytes.fromhex(rv['hex']) - def listInputs(self, tx_bytes): + def listInputs(self, tx_bytes: bytes): tx = self.loadTx(tx_bytes) all_locked = self.rpc_wallet('listlockunspent') @@ -1049,11 +1023,11 @@ class BTCInterface(Secp256k1Interface): tx.wit.vtxinwit.clear() return tx.serialize() - def extractLeaderSig(self, tx_bytes) -> bytes: + def extractLeaderSig(self, tx_bytes: bytes) -> bytes: tx = self.loadTx(tx_bytes) return tx.wit.vtxinwit[0].scriptWitness.stack[1] - def extractFollowerSig(self, tx_bytes) -> bytes: + def extractFollowerSig(self, tx_bytes: bytes) -> bytes: tx = self.loadTx(tx_bytes) return tx.wit.vtxinwit[0].scriptWitness.stack[2] @@ -1076,9 +1050,6 @@ class BTCInterface(Secp256k1Interface): return bytes.fromhex(self.publishTx(b_lock_tx)) - def recoverEncKey(self, esig, sig, K): - return ecdsaotves_rec_enc_key(K, esig, sig[:-1]) # Strip sighash type - def getTxVSize(self, tx, add_bytes: int = 0, add_witness_bytes: int = 0) -> int: wsf = self.witnessScaleFactor() len_full = len(tx.serialize_with_witness()) + add_bytes + add_witness_bytes @@ -1108,10 +1079,10 @@ class BTCInterface(Secp256k1Interface): witness_bytes = 109 vsize = self.getTxVSize(tx, add_witness_bytes=witness_bytes) pay_fee = round(fee_rate * vsize / 1000) - self._log.info(f'BLockSpendTx fee_rate, vsize, fee: {fee_rate}, {vsize}, {pay_fee}.') + self._log.info(f'BLockSpendTx fee_rate, vsize, fee: {fee_rate}, {vsize}, {pay_fee}.') return pay_fee - def spendBLockTx(self, chain_b_lock_txid: bytes, address_to: str, kbv: bytes, kbs: bytes, cb_swap_value: int, b_fee: int, restore_height: int) -> bytes: + def spendBLockTx(self, chain_b_lock_txid: bytes, address_to: str, kbv: bytes, kbs: bytes, cb_swap_value: int, b_fee: int, restore_height: int, lock_tx_vout=None) -> bytes: self._log.info('spendBLockTx %s:\n', chain_b_lock_txid.hex()) wtx = self.rpc_wallet('gettransaction', [chain_b_lock_txid.hex(), ]) lock_tx = self.loadTx(bytes.fromhex(wtx['hex'])) @@ -1126,7 +1097,7 @@ class BTCInterface(Secp256k1Interface): tx.nVersion = self.txVersion() script_lock = self.getScriptForPubkeyHash(Kbs) - chain_b_lock_txid_int = uint256_from_str(chain_b_lock_txid[::-1]) + chain_b_lock_txid_int = b2i(chain_b_lock_txid) tx.vin.append(CTxIn(COutPoint(chain_b_lock_txid_int, locked_n), nSequence=0, @@ -1148,7 +1119,7 @@ class BTCInterface(Secp256k1Interface): addr_info = self.rpc_wallet('getaddressinfo', [address]) return addr_info['iswatchonly'] - def getSCLockScriptAddress(self, lock_script): + def getSCLockScriptAddress(self, lock_script: bytes) -> str: lock_tx_dest = self.getScriptDest(lock_script) return self.encodeScriptDest(lock_tx_dest) @@ -1225,25 +1196,25 @@ class BTCInterface(Secp256k1Interface): params = [addr_to, value, '', '', subfee, True, self._conf_target] return self.rpc_wallet('sendtoaddress', params) - def signCompact(self, k, message): + def signCompact(self, k, message: str) -> bytes: message_hash = sha256(bytes(message, 'utf-8')) privkey = PrivateKey(k) return privkey.sign_recoverable(message_hash, hasher=None)[:64] - def signRecoverable(self, k, message): + def signRecoverable(self, k, message: str) -> bytes: message_hash = sha256(bytes(message, 'utf-8')) privkey = PrivateKey(k) return privkey.sign_recoverable(message_hash, hasher=None) - def verifyCompactSig(self, K, message, sig): + def verifyCompactSig(self, K, message: str, sig) -> None: message_hash = sha256(bytes(message, 'utf-8')) pubkey = PublicKey(K) rv = pubkey.verify_compact(sig, message_hash, hasher=None) assert (rv is True) - def verifySigAndRecover(self, sig, message): + def verifySigAndRecover(self, sig, message: str) -> bytes: message_hash = sha256(bytes(message, 'utf-8')) pubkey = PublicKey.from_signature_and_message(sig, message_hash, hasher=None) return pubkey.format() @@ -1271,40 +1242,6 @@ class BTCInterface(Secp256k1Interface): def showLockTransfers(self, kbv, Kbs, restore_height): raise ValueError('Unimplemented') - def getLockTxSwapOutputValue(self, bid, xmr_swap): - return bid.amount - - def getLockRefundTxSwapOutputValue(self, bid, xmr_swap): - return xmr_swap.a_swap_refund_value - - def getLockRefundTxSwapOutput(self, xmr_swap): - # Only one prevout exists - return 0 - - def getScriptLockTxDummyWitness(self, script: bytes): - return [ - b'', - bytes(72), - bytes(72), - bytes(len(script)) - ] - - def getScriptLockRefundSpendTxDummyWitness(self, script: bytes): - return [ - b'', - bytes(72), - bytes(72), - bytes((1,)), - bytes(len(script)) - ] - - def getScriptLockRefundSwipeTxDummyWitness(self, script: bytes): - return [ - bytes(72), - b'', - bytes(len(script)) - ] - def getWitnessStackSerialisedLength(self, witness_stack): length = getCompactSizeLen(len(witness_stack)) for e in witness_stack: @@ -1501,7 +1438,7 @@ class BTCInterface(Secp256k1Interface): def createRedeemTxn(self, prevout, output_addr: str, output_value: int, txn_script: bytes = None) -> str: tx = CTransaction() tx.nVersion = self.txVersion() - prev_txid = uint256_from_str(bytes.fromhex(prevout['txid'])[::-1]) + prev_txid = b2i(bytes.fromhex(prevout['txid'])) tx.vin.append(CTxIn(COutPoint(prev_txid, prevout['vout']))) pkh = self.decodeAddress(output_addr) script = self.getScriptForPubkeyHash(pkh) @@ -1513,7 +1450,7 @@ class BTCInterface(Secp256k1Interface): tx = CTransaction() tx.nVersion = self.txVersion() tx.nLockTime = locktime - prev_txid = uint256_from_str(bytes.fromhex(prevout['txid'])[::-1]) + prev_txid = b2i(bytes.fromhex(prevout['txid'])) tx.vin.append(CTxIn(COutPoint(prev_txid, prevout['vout']), nSequence=sequence,)) pkh = self.decodeAddress(output_addr) script = self.getScriptForPubkeyHash(pkh) @@ -1551,6 +1488,12 @@ class BTCInterface(Secp256k1Interface): 'amount': txjs['vout'][n]['value'] } + def isTxExistsError(self, err_str: str) -> bool: + return 'Transaction already in block chain' in err_str + + def isTxNonFinalError(self, err_str: str) -> bool: + return 'non-BIP68-final' in err_str or 'non-final' in err_str + def testBTCInterface(): print('TODO: testBTCInterface') diff --git a/basicswap/interface/dash.py b/basicswap/interface/dash.py index 933ea98..7649e73 100644 --- a/basicswap/interface/dash.py +++ b/basicswap/interface/dash.py @@ -66,7 +66,7 @@ class DASHInterface(BTCInterface): add_bytes = 107 size = len(tx.serialize_with_witness()) + add_bytes pay_fee = round(fee_rate * size / 1000) - self._log.info(f'BLockSpendTx fee_rate, size, fee: {fee_rate}, {size}, {pay_fee}.') + self._log.info(f'BLockSpendTx fee_rate, size, fee: {fee_rate}, {size}, {pay_fee}.') return pay_fee def findTxnByHash(self, txid_hex: str): diff --git a/basicswap/interface/dcr/dcr.py b/basicswap/interface/dcr/dcr.py index c6195bc..a27efaf 100644 --- a/basicswap/interface/dcr/dcr.py +++ b/basicswap/interface/dcr/dcr.py @@ -7,20 +7,29 @@ import base64 import hashlib +import json import logging import random +import traceback from basicswap.basicswap_util import ( getVoutByScriptPubKey, TxLockTypes ) from basicswap.chainparams import Coins -from basicswap.contrib.test_framework.messages import ( - uint256_from_str, +from basicswap.contrib.test_framework.script import ( + CScriptNum, +) +from basicswap.interface.base import ( + Secp256k1Interface, +) +from basicswap.interface.btc import ( + extractScriptLockScriptValues, + extractScriptLockRefundScriptValues, ) -from basicswap.interface.btc import Secp256k1Interface from basicswap.util import ( ensure, + b2h, b2i, i2b, i2h, ) from basicswap.util.address import ( b58decode, @@ -36,28 +45,40 @@ from basicswap.util.script import ( ) from basicswap.util.extkey import ExtKeyPair from basicswap.util.integer import encode_varint -from basicswap.interface.dcr.rpc import make_rpc_func +from basicswap.interface.dcr.rpc import make_rpc_func, openrpc from .messages import ( + COutPoint, CTransaction, CTxIn, CTxOut, - COutPoint, + findOutput, SigHashType, TxSerializeType, ) from .script import ( - push_script_data, - OP_HASH160, - OP_EQUAL, - OP_DUP, - OP_EQUALVERIFY, + OP_CHECKMULTISIG, + OP_CHECKSEQUENCEVERIFY, OP_CHECKSIG, + OP_DROP, + OP_DUP, + OP_ELSE, + OP_ENDIF, + OP_EQUAL, + OP_EQUALVERIFY, + OP_HASH160, + OP_IF, + push_script_data, ) - from coincurve.keys import ( PrivateKey, PublicKey, ) +from coincurve.ecdsaotves import ( + ecdsaotves_enc_sign, + ecdsaotves_enc_verify, + ecdsaotves_dec_sig, + ecdsaotves_rec_enc_key +) SEQUENCE_LOCKTIME_GRANULARITY = 9 # 512 seconds @@ -203,6 +224,10 @@ class DCRInterface(Secp256k1Interface): def watch_blocks_for_scripts() -> bool: return True + @staticmethod + def depth_spendable() -> int: + return 0 + def __init__(self, coin_settings, network, swap_client=None): super().__init__(network) self._rpc_host = coin_settings.get('rpchost', '127.0.0.1') @@ -212,8 +237,10 @@ class DCRInterface(Secp256k1Interface): self._log = self._sc.log if self._sc and self._sc.log else logging self.rpc = make_rpc_func(self._rpcport, self._rpcauth, host=self._rpc_host) if 'walletrpcport' in coin_settings: - self.rpc_wallet = make_rpc_func(coin_settings['walletrpcport'], self._rpcauth, host=self._rpc_host) + self._walletrpcport = coin_settings['walletrpcport'] + self.rpc_wallet = make_rpc_func(self._walletrpcport, self._rpcauth, host=self._rpc_host) else: + self._walletrpcport = None self.rpc_wallet = None self.blocks_confirmed = coin_settings['blocks_confirmed'] self.setConfTarget(coin_settings['conf_target']) @@ -221,6 +248,23 @@ class DCRInterface(Secp256k1Interface): self._use_segwit = True # Decred is natively segwit self._connection_type = coin_settings['connection_type'] + def open_rpc(self): + return openrpc(self._rpcport, self._rpcauth, host=self._rpc_host) + + def json_request(self, rpc_conn, method, params): + try: + v = rpc_conn.json_request(method, params) + r = json.loads(v.decode('utf-8')) + except Exception as ex: + traceback.print_exc() + raise ValueError('RPC Server Error ' + str(ex)) + if 'error' in r and r['error'] is not None: + raise ValueError('RPC error ' + str(r['error'])) + return r['result'] + + def close_rpc(self, rpc_conn): + rpc_conn.close() + def use_tx_vsize(self) -> bool: return False @@ -242,6 +286,7 @@ class DCRInterface(Secp256k1Interface): return b58encode(data + checksum[0:4]) def decode_address(self, address: str) -> bytes: + # Different from decodeAddress returns more prefix bytes addr_data = b58decode(address) if addr_data is None: return None @@ -251,6 +296,9 @@ class DCRInterface(Secp256k1Interface): raise ValueError('Checksum mismatch') return prefixed_data + def decodeAddress(self, address: str) -> bytes: + return self.decode_address(address)[2:] + def testDaemonRPC(self, with_wallet=True) -> None: if with_wallet: self.rpc_wallet('getinfo') @@ -289,6 +337,11 @@ class DCRInterface(Secp256k1Interface): return rv + def getSpendableBalance(self) -> int: + balances = self.rpc_wallet('getbalance') + default_account_bal = balances['balances'][0] # 0 always default? + return self.make_int(default_account_bal['spendable']) + def getSeedHash(self, seed: bytes) -> bytes: # m / purpose' / coin_type' / account' / change / address_index # m/44'/coin_type'/0'/0/0 @@ -339,6 +392,7 @@ class DCRInterface(Secp256k1Interface): def setTxSignature(self, tx_bytes: bytes, stack, txi: int = 0) -> bytes: tx = self.loadTx(tx_bytes) + script_data = bytearray() for data in stack: push_script_data(script_data, data) @@ -390,6 +444,13 @@ class DCRInterface(Secp256k1Interface): assert len(pkh) == 20 return OP_DUP.to_bytes(1) + OP_HASH160.to_bytes(1) + len(pkh).to_bytes(1) + pkh + OP_EQUALVERIFY.to_bytes(1) + OP_CHECKSIG.to_bytes(1) + def getPkDest(self, K: bytes) -> bytearray: + return self.getPubkeyHashDest(self.pkh(K)) + + def getSCLockScriptAddress(self, lock_script: bytes) -> str: + lock_tx_dest = self.getScriptDest(lock_script) + return self.encodeScriptDest(lock_tx_dest) + def get_fee_rate(self, conf_target: int = 2) -> (float, str): chain_client_settings = self._sc.getChainClientSettings(self.coin_type()) # basicswap.json override_feerate = chain_client_settings.get('override_feerate', None) @@ -525,6 +586,29 @@ class DCRInterface(Secp256k1Interface): return sum_value + def signCompact(self, k, message): + message_hash = blake256(bytes(message, 'utf-8')) + + privkey = PrivateKey(k) + return privkey.sign_recoverable(message_hash, hasher=None)[:64] + + def signRecoverable(self, k, message: str) -> bytes: + message_hash = blake256(bytes(message, 'utf-8')) + + privkey = PrivateKey(k) + return privkey.sign_recoverable(message_hash, hasher=None) + + def verifyCompactSig(self, K, message: str, sig) -> None: + message_hash = blake256(bytes(message, 'utf-8')) + pubkey = PublicKey(K) + rv = pubkey.verify_compact(sig, message_hash, hasher=None) + assert (rv is True) + + def verifySigAndRecover(self, sig, message: str) -> bytes: + message_hash = blake256(bytes(message, 'utf-8')) + pubkey = PublicKey.from_signature_and_message(sig, message_hash, hasher=None) + return pubkey.format() + def verifyMessage(self, address: str, message: str, signature: str, message_magic: str = None) -> bool: if message_magic is None: message_magic = self.chainparams()['message_magic'] @@ -546,7 +630,12 @@ class DCRInterface(Secp256k1Interface): return True if address_hash == pubkey_hash else False def signTxWithWallet(self, tx) -> bytes: - return bytes.fromhex(self.rpc('signrawtransaction', [tx.hex()])['hex']) + return bytes.fromhex(self.rpc_wallet('signrawtransaction', [tx.hex()])['hex']) + + def signTxWithKey(self, tx: bytes, key: bytes) -> bytes: + key_wif = self.encodeKey(key) + rv = self.rpc_wallet('signrawtransaction', [tx.hex(), [], [key_wif, ]]) + return bytes.fromhex(rv['hex']) def createRawFundedTransaction(self, addr_to: str, amount: int, sub_fee: bool = False, lock_unspents: bool = True) -> str: @@ -593,6 +682,9 @@ class DCRInterface(Secp256k1Interface): # self._log.warning('gettxout {}'.format(e)) return None + if found_vout is None: + return None + block_height: int = 0 confirmations: int = 0 if 'confirmations' not in txout else txout['confirmations'] @@ -601,6 +693,7 @@ class DCRInterface(Secp256k1Interface): block_height = self.getChainHeight() - confirmations rv = { + 'txid': txid.hex(), 'depth': confirmations, 'index': found_vout, 'height': block_height} @@ -628,7 +721,7 @@ class DCRInterface(Secp256k1Interface): def createRedeemTxn(self, prevout, output_addr: str, output_value: int, txn_script: bytes = None) -> str: tx = CTransaction() tx.version = self.txVersion() - prev_txid = uint256_from_str(bytes.fromhex(prevout['txid'])[::-1]) + prev_txid = b2i(bytes.fromhex(prevout['txid'])) tx.vin.append(CTxIn(COutPoint(prev_txid, prevout['vout'], 0))) pkh = self.decode_address(output_addr)[2:] script = self.getPubkeyHashDest(pkh) @@ -639,7 +732,7 @@ class DCRInterface(Secp256k1Interface): tx = CTransaction() tx.version = self.txVersion() tx.locktime = locktime - prev_txid = uint256_from_str(bytes.fromhex(prevout['txid'])[::-1]) + prev_txid = b2i(bytes.fromhex(prevout['txid'])) tx.vin.append(CTxIn(COutPoint(prev_txid, prevout['vout'], 0), sequence=sequence,)) pkh = self.decode_address(output_addr)[2:] script = self.getPubkeyHashDest(pkh) @@ -678,6 +771,22 @@ class DCRInterface(Secp256k1Interface): block_hash = self.rpc('getblockhash', [height]) return self.rpc('getblockheader', [block_hash]) + def getBlockHeaderAt(self, time: int, block_after=False): + blockchaininfo = self.rpc('getblockchaininfo') + last_block_header = self.rpc('getblockheader', [blockchaininfo['bestblockhash']]) + + max_tries = 5000 + for i in range(max_tries): + prev_block_header = self.rpc('getblockheader', [last_block_header['previousblockhash']]) + if prev_block_header['time'] <= time: + return last_block_header if block_after else prev_block_header + + last_block_header = prev_block_header + raise ValueError(f'Block header not found at time: {time}') + + def getMempoolTx(self, txid): + raise ValueError('TODO') + def getBlockWithTxns(self, block_hash: str): block = self.rpc('getblock', [block_hash, True, True]) @@ -697,3 +806,549 @@ class DCRInterface(Secp256k1Interface): def describeTx(self, tx_hex: str): return self.rpc('decoderawtransaction', [tx_hex]) + + def fundTx(self, tx: bytes, feerate) -> bytes: + feerate_str = float(self.format_amount(feerate)) + # TODO: unlock unspents if bid cancelled + options = { + 'feeRate': feerate_str, + } + rv = self.rpc_wallet('fundrawtransaction', [tx.hex(), 'default', options]) + tx_bytes = bytes.fromhex(rv['hex']) + + tx_obj = self.loadTx(tx_bytes) + for txi in tx_obj.vin: + utxos = [{'amount': float(self.format_amount(txi.value_in)), + 'txid': i2h(txi.prevout.hash), + 'vout': txi.prevout.n, + 'tree': txi.prevout.tree}] + rv = self.rpc_wallet('lockunspent', [False, utxos]) + + return tx_bytes + + def createSCLockTx(self, value: int, script: bytearray, vkbv: bytes = None) -> bytes: + tx = CTransaction() + tx.version = self.txVersion() + tx.vout.append(self.txoType()(value, self.getScriptDest(script))) + return tx.serialize() + + def fundSCLockTx(self, tx_bytes, feerate, vkbv=None): + return self.fundTx(tx_bytes, feerate) + + def genScriptLockRefundTxScript(self, Kal, Kaf, csv_val) -> bytes: + + Kal_enc = Kal if len(Kal) == 33 else self.encodePubkey(Kal) + Kaf_enc = Kaf if len(Kaf) == 33 else self.encodePubkey(Kaf) + + script = bytearray() + script += OP_IF.to_bytes(1) + push_script_data(script, bytes((2,))) + push_script_data(script, Kal_enc) + push_script_data(script, Kaf_enc) + push_script_data(script, bytes((2,))) + script += OP_CHECKMULTISIG.to_bytes(1) + script += OP_ELSE.to_bytes(1) + script += CScriptNum.encode(CScriptNum(csv_val)) + script += OP_CHECKSEQUENCEVERIFY.to_bytes(1) + script += OP_DROP.to_bytes(1) + push_script_data(script, Kaf_enc) + script += OP_CHECKSIG.to_bytes(1) + script += OP_ENDIF.to_bytes(1) + + return script + + def createSCLockSpendTx(self, tx_lock_bytes, script_lock, pkh_dest, tx_fee_rate, vkbv=None, fee_info={}): + tx_lock = self.loadTx(tx_lock_bytes) + output_script = self.getScriptDest(script_lock) + locked_n = findOutput(tx_lock, output_script) + ensure(locked_n is not None, 'Output not found in tx') + locked_coin = tx_lock.vout[locked_n].value + + tx_lock_id_int = b2i(tx_lock.TxHash()) + + tx = CTransaction() + tx.version = self.txVersion() + tx.vin.append(CTxIn(COutPoint(tx_lock_id_int, locked_n, 0))) + tx.vout.append(self.txoType()(locked_coin, self.getPubkeyHashDest(pkh_dest))) + + dummy_witness_stack = self.getScriptLockTxDummyWitness(script_lock) + size = len(self.setTxSignature(tx.serialize(), dummy_witness_stack)) + pay_fee = round(tx_fee_rate * size / 1000) + tx.vout[0].value = locked_coin - pay_fee + + fee_info['fee_paid'] = pay_fee + fee_info['rate_used'] = tx_fee_rate + fee_info['size'] = size + + self._log.info('createSCLockSpendTx %s:\n fee_rate, size, fee: %ld, %ld, %ld.', + tx.TxHash().hex(), tx_fee_rate, size, pay_fee) + + return tx.serialize(TxSerializeType.NoWitness) + + def createSCLockRefundTx(self, tx_lock_bytes, script_lock, Kal, Kaf, lock1_value, csv_val, tx_fee_rate, vkbv=None): + tx_lock = CTransaction() + tx_lock = self.loadTx(tx_lock_bytes) + + output_script = self.getScriptDest(script_lock) + locked_n = findOutput(tx_lock, output_script) + ensure(locked_n is not None, 'Output not found in tx') + locked_coin = tx_lock.vout[locked_n].value + + tx_lock_id_int = b2i(tx_lock.TxHash()) + + refund_script = self.genScriptLockRefundTxScript(Kal, Kaf, csv_val) + tx = CTransaction() + tx.version = self.txVersion() + tx.vin.append(CTxIn(COutPoint(tx_lock_id_int, locked_n, 0), + sequence=lock1_value)) + tx.vout.append(self.txoType()(locked_coin, self.getScriptDest(refund_script))) + + dummy_witness_stack = self.getScriptLockTxDummyWitness(script_lock) + size = len(self.setTxSignature(tx.serialize(), dummy_witness_stack)) + pay_fee = round(tx_fee_rate * size / 1000) + tx.vout[0].value = locked_coin - pay_fee + + self._log.info('createSCLockRefundTx %s:\n fee_rate, size, fee: %ld, %ld, %ld.', + tx.TxHash().hex(), tx_fee_rate, size, pay_fee) + + return tx.serialize(TxSerializeType.NoWitness), refund_script, tx.vout[0].value + + def createSCLockRefundSpendTx(self, tx_lock_refund_bytes, script_lock_refund, pkh_refund_to, tx_fee_rate, vkbv=None): + # 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 + # 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) + + output_script = self.getScriptDest(script_lock_refund) + locked_n = findOutput(tx_lock_refund, output_script) + ensure(locked_n is not None, 'Output not found in tx') + locked_coin = tx_lock_refund.vout[locked_n].value + + tx_lock_refund_hash_int = b2i(tx_lock_refund.TxHash()) + + tx = CTransaction() + tx.version = self.txVersion() + tx.vin.append(CTxIn(COutPoint(tx_lock_refund_hash_int, locked_n, 0), + sequence=0)) + + tx.vout.append(self.txoType()(locked_coin, self.getPubkeyHashDest(pkh_refund_to))) + + dummy_witness_stack = self.getScriptLockRefundSpendTxDummyWitness(script_lock_refund) + size = len(self.setTxSignature(tx.serialize(), dummy_witness_stack)) + pay_fee = round(tx_fee_rate * size / 1000) + tx.vout[0].value = locked_coin - pay_fee + + self._log.info('createSCLockRefundSpendTx %s:\n fee_rate, size, fee: %ld, %ld, %ld.', + tx.TxHash().hex(), tx_fee_rate, size, pay_fee) + + return tx.serialize(TxSerializeType.NoWitness) + + def verifySCLockTx(self, tx_bytes, script_out, + swap_value, + Kal, Kaf, + feerate, + check_lock_tx_inputs, vkbv=None): + # Verify: + # + + # Not necessary to check the lock txn is mineable, as protocol will wait for it to confirm + # However by checking early we can avoid wasting time processing unmineable txns + # Check fee is reasonable + + tx = self.loadTx(tx_bytes) + txid = self.getTxid(tx) + self._log.info('Verifying lock tx: {}.'.format(b2h(txid))) + + ensure(tx.version == self.txVersion(), 'Bad version') + ensure(tx.locktime == 0, 'Bad locktime') + ensure(tx.expiry == 0, 'Bad expiry') + + script_pk = self.getScriptDest(script_out) + locked_n = findOutput(tx, script_pk) + ensure(locked_n is not None, 'Lock output not found in tx') + locked_coin = tx.vout[locked_n].value + + # Check value + ensure(locked_coin == swap_value, 'Bad locked value') + + # Check script + A, B = extractScriptLockScriptValues(script_out) + ensure(A == Kal, 'Bad script pubkey') + ensure(B == Kaf, 'Bad script pubkey') + + if check_lock_tx_inputs: + # TODO: Check that inputs are unspent + # Verify fee rate + inputs_value = 0 + add_bytes = 0 + add_witness_bytes = 0 + for pi in tx.vin: + ptx = self.rpc('getrawtransaction', [i2h(pi.prevout.hash), True]) + prevout = ptx['vout'][pi.prevout.n] + inputs_value += self.make_int(prevout['value']) + self._log.info('prevout: {}.'.format(prevout)) + prevout_type = prevout['scriptPubKey']['type'] + + ''' + if prevout_type == 'witness_v0_keyhash': + #add_witness_bytes += 107 # sig 72, pk 33 and 2 size bytes + #add_witness_bytes += getCompactSizeLen(107) + else: + # Assume P2PKH, TODO more types + add_bytes += 107 # OP_PUSH72 OP_PUSH33 + ''' + + outputs_value = 0 + for txo in tx.vout: + outputs_value += txo.nValue + fee_paid = inputs_value - outputs_value + assert (fee_paid > 0) + + size = len(tx.serialize()) + add_witness_bytes + fee_rate_paid = fee_paid * 1000 // size + + self._log.info('tx amount, size, feerate: %ld, %ld, %ld', locked_coin, size, fee_rate_paid) + + if not self.compareFeeRates(fee_rate_paid, feerate): + self._log.warning('feerate paid doesn\'t match expected: %ld, %ld', fee_rate_paid, feerate) + # TODO: Display warning to user + + return txid, locked_n + + def verifySCLockSpendTx(self, tx_bytes, + lock_tx_bytes, lock_tx_script, + a_pkhash_f, feerate, vkbv=None): + # Verify: + # Must have only one input with correct prevout (n is always 0) and sequence + # Must have only one output with destination and amount + + tx = self.loadTx(tx_bytes) + txid = self.getTxid(tx) + self._log.info('Verifying lock spend tx: {}.'.format(b2h(txid))) + + ensure(tx.version == self.txVersion(), 'Bad version') + ensure(tx.locktime == 0, 'Bad locktime') + ensure(tx.expiry == 0, 'Bad expiry') + ensure(len(tx.vin) == 1, 'tx doesn\'t have one input') + + lock_tx = self.loadTx(lock_tx_bytes) + lock_tx_id = self.getTxid(lock_tx) + + output_script = self.getScriptDest(lock_tx_script) + locked_n = findOutput(lock_tx, output_script) + ensure(locked_n is not None, 'Output not found in tx') + locked_coin = lock_tx.vout[locked_n].value + + ensure(tx.vin[0].sequence == 0, 'Bad input nSequence') + ensure(len(tx.vin[0].signature_script) == 0, 'Input sig not empty') + ensure(i2b(tx.vin[0].prevout.hash) == lock_tx_id and tx.vin[0].prevout.n == locked_n, 'Input prevout mismatch') + + ensure(len(tx.vout) == 1, 'tx doesn\'t have one output') + p2wpkh = self.getPubkeyHashDest(a_pkhash_f) + ensure(tx.vout[0].script_pubkey == p2wpkh, 'Bad output destination') + + # The value of the lock tx output should already be verified, if the fee is as expected the difference will be the correct amount + fee_paid = locked_coin - tx.vout[0].value + assert (fee_paid > 0) + + dummy_witness_stack = self.getScriptLockTxDummyWitness(lock_tx_script) + size = len(self.setTxSignature(tx.serialize(), dummy_witness_stack)) + fee_rate_paid = fee_paid * 1000 // size + + self._log.info('tx amount, size, feerate: %ld, %ld, %ld', tx.vout[0].value, size, fee_rate_paid) + + if not self.compareFeeRates(fee_rate_paid, feerate): + raise ValueError('Bad fee rate, expected: {}'.format(feerate)) + + return True + + def verifySCLockRefundTx(self, tx_bytes, lock_tx_bytes, script_out, + prevout_id, prevout_n, prevout_seq, prevout_script, + Kal, Kaf, csv_val_expect, swap_value, feerate, vkbv=None): + # Verify: + # Must have only one input with correct prevout and sequence + # Must have only one output to the p2wsh of the lock refund script + # Output value must be locked_coin - lock tx fee + + tx = self.loadTx(tx_bytes) + txid = self.getTxid(tx) + self._log.info('Verifying lock refund tx: {}.'.format(b2h(txid))) + + ensure(tx.version == self.txVersion(), 'Bad version') + ensure(tx.locktime == 0, 'locktime not 0') + ensure(tx.expiry == 0, 'Bad expiry') + ensure(len(tx.vin) == 1, 'tx doesn\'t have one input') + + ensure(tx.vin[0].sequence == prevout_seq, 'Bad input sequence') + ensure(i2b(tx.vin[0].prevout.hash) == prevout_id and tx.vin[0].prevout.n == prevout_n and tx.vin[0].prevout.tree == 0, 'Input prevout mismatch') + ensure(len(tx.vin[0].signature_script) == 0, 'Input sig not empty') + + ensure(len(tx.vout) == 1, 'tx doesn\'t have one output') + + script_pk = self.getScriptDest(script_out) + locked_n = findOutput(tx, script_pk) + ensure(locked_n is not None, 'Output not found in tx') + locked_coin = tx.vout[locked_n].value + + # Check script and values + A, B, csv_val, C = extractScriptLockRefundScriptValues(script_out) + ensure(A == Kal, 'Bad script pubkey') + ensure(B == Kaf, 'Bad script pubkey') + ensure(csv_val == csv_val_expect, 'Bad script csv value') + ensure(C == Kaf, 'Bad script pubkey') + + fee_paid = swap_value - locked_coin + assert (fee_paid > 0) + + dummy_witness_stack = self.getScriptLockTxDummyWitness(prevout_script) + size = len(self.setTxSignature(tx.serialize(), dummy_witness_stack)) + fee_rate_paid = fee_paid * 1000 // size + + self._log.info('tx amount, size, feerate: %ld, %ld, %ld', locked_coin, size, fee_rate_paid) + + if not self.compareFeeRates(fee_rate_paid, feerate): + raise ValueError('Bad fee rate, expected: {}'.format(feerate)) + + return txid, locked_coin, locked_n + + def verifySCLockRefundSpendTx(self, tx_bytes, lock_refund_tx_bytes, + lock_refund_tx_id, prevout_script, + Kal, + prevout_n, prevout_value, feerate, vkbv=None): + # Verify: + # Must have only one input with correct prevout (n is always 0) and sequence + # Must have only one output sending lock refund tx value - fee to leader's address, TODO: follower shouldn't need to verify destination addr + tx = self.loadTx(tx_bytes) + txid = self.getTxid(tx) + self._log.info('Verifying lock refund spend tx: {}.'.format(b2h(txid))) + + ensure(tx.version == self.txVersion(), 'Bad version') + ensure(tx.locktime == 0, 'locktime not 0') + ensure(tx.expiry == 0, 'Bad expiry') + ensure(len(tx.vin) == 1, 'tx doesn\'t have one input') + + ensure(tx.vin[0].sequence == 0, 'Bad input sequence') + ensure(len(tx.vin[0].signature_script) == 0, 'Input sig not empty') + ensure(i2b(tx.vin[0].prevout.hash) == lock_refund_tx_id and tx.vin[0].prevout.n == 0 and tx.vin[0].prevout.tree == 0, 'Input prevout mismatch') + + ensure(len(tx.vout) == 1, 'tx doesn\'t have one output') + + # Destination doesn't matter to the follower + ''' + p2wpkh = CScript([OP_0, hash160(Kal)]) + locked_n = findOutput(tx, p2wpkh) + ensure(locked_n is not None, 'Output not found in lock refund spend tx') + ''' + tx_value = tx.vout[0].value + + fee_paid = prevout_value - tx_value + assert (fee_paid > 0) + + dummy_witness_stack = self.getScriptLockRefundSpendTxDummyWitness(prevout_script) + size = len(self.setTxSignature(tx.serialize(), dummy_witness_stack)) + fee_rate_paid = fee_paid * 1000 // size + + self._log.info('tx amount, size, feerate: %ld, %ld, %ld', tx_value, size, fee_rate_paid) + + if not self.compareFeeRates(fee_rate_paid, feerate): + raise ValueError('Bad fee rate, expected: {}'.format(feerate)) + + return True + + def createSCLockRefundSpendToFTx(self, tx_lock_refund_bytes, script_lock_refund, pkh_dest, tx_fee_rate, vkbv=None): + # lock refund swipe tx + # Sends the coinA locked coin to the follower + + tx_lock_refund = self.loadTx(tx_lock_refund_bytes) + + output_script = self.getScriptDest(script_lock_refund) + locked_n = findOutput(tx_lock_refund, output_script) + ensure(locked_n is not None, 'Output not found in tx') + locked_amount = tx_lock_refund.vout[locked_n].value + + A, B, lock2_value, C = extractScriptLockRefundScriptValues(script_lock_refund) + + tx_lock_refund_hash_int = b2i(tx_lock_refund.TxHash()) + + tx = CTransaction() + tx.version = self.txVersion() + tx.vin.append(CTxIn(COutPoint(tx_lock_refund_hash_int, locked_n, 0), + sequence=lock2_value,)) + + tx.vout.append(self.txoType()(locked_amount, self.getPubkeyHashDest(pkh_dest))) + + dummy_witness_stack = self.getScriptLockRefundSwipeTxDummyWitness(script_lock_refund) + size = len(self.setTxSignature(tx.serialize(), dummy_witness_stack)) + pay_fee = round(tx_fee_rate * size / 1000) + tx.vout[0].value = locked_amount - pay_fee + + self._log.info('createSCLockRefundSpendToFTx %s:\n fee_rate, size, fee: %ld, %ld, %ld.', + tx.TxHash().hex(), tx_fee_rate, size, pay_fee) + + return tx.serialize(TxSerializeType.NoWitness) + + def signTxOtVES(self, key_sign: bytes, pubkey_encrypt: bytes, tx_bytes: bytes, input_n: int, prevout_script: bytes, prevout_value: int) -> bytes: + tx = self.loadTx(tx_bytes) + sig_hash = DCRSignatureHash(prevout_script, SigHashType.SigHashAll, tx, input_n) + + return ecdsaotves_enc_sign(key_sign, pubkey_encrypt, sig_hash) + + def verifyTxOtVES(self, tx_bytes: bytes, ct: bytes, Ks: bytes, Ke: bytes, input_n: int, prevout_script: bytes, prevout_value): + tx = self.loadTx(tx_bytes) + sig_hash = DCRSignatureHash(prevout_script, SigHashType.SigHashAll, tx, input_n) + return ecdsaotves_enc_verify(Ks, Ke, sig_hash, ct) + + def decryptOtVES(self, k: bytes, esig: bytes) -> bytes: + return ecdsaotves_dec_sig(k, esig) + bytes((SigHashType.SigHashAll,)) + + def recoverEncKey(self, esig, sig, K): + return ecdsaotves_rec_enc_key(K, esig, sig[:-1]) # Strip sighash type + + def getTxOutputPos(self, tx, script): + if isinstance(tx, bytes): + tx = self.loadTx(tx) + script_pk = self.getScriptDest(script) + return findOutput(tx, script_pk) + + def getScriptLockTxDummyWitness(self, script: bytes): + return [ + bytes(72), + bytes(72), + bytes(len(script)) + ] + + def getScriptLockRefundSpendTxDummyWitness(self, script: bytes): + return [ + bytes(72), + bytes(72), + bytes((1,)), + bytes(len(script)) + ] + + def extractLeaderSig(self, tx_bytes: bytes) -> bytes: + tx = self.loadTx(tx_bytes) + + sig_len = tx.vin[0].signature_script[0] + return tx.vin[0].signature_script[1: 1 + sig_len] + + def extractFollowerSig(self, tx_bytes: bytes) -> bytes: + tx = self.loadTx(tx_bytes) + + sig_len = tx.vin[0].signature_script[0] + ofs = 1 + sig_len + sig_len = tx.vin[0].signature_script[ofs] + ofs += 1 + return tx.vin[0].signature_script[ofs: ofs + sig_len] + + def unlockInputs(self, tx_bytes): + tx = self.loadTx(tx_bytes) + + inputs = [] + for txi in tx.vin: + inputs.append({'amount': float(self.format_amount(txi.value_in)), 'txid': i2h(txi.prevout.hash), 'vout': txi.prevout.n, 'tree': txi.prevout.tree}) + self.rpc_wallet('lockunspent', [True, inputs]) + + def getWalletRestoreHeight(self) -> int: + start_time = self.rpc_wallet('getinfo')['keypoololdest'] + + blockchaininfo = self.getBlockchainInfo() + best_block = blockchaininfo['bestblockhash'] + + chain_synced = round(blockchaininfo['verificationprogress'], 3) + if chain_synced < 1.0: + raise ValueError('{} chain isn\'t synced.'.format(self.coin_name())) + + self._log.debug('Finding block at time: {}'.format(start_time)) + + rpc_conn = self.open_rpc() + try: + block_hash = best_block + while True: + block_header = self.json_request(rpc_conn, 'getblockheader', [block_hash]) + if block_header['time'] < start_time: + return block_header['height'] + # genesis block + if block_header['previousblockhash'] == '0000000000000000000000000000000000000000000000000000000000000000': + return block_header['height'] + + block_hash = block_header['previousblockhash'] + finally: + self.close_rpc(rpc_conn) + raise ValueError('{} wallet restore height not found.'.format(self.coin_name())) + + def createBLockTx(self, Kbs, output_amount, vkbv=None) -> bytes: + tx = CTransaction() + tx.version = self.txVersion() + script_pk = self.getPkDest(Kbs) + tx.vout.append(self.txoType()(output_amount, script_pk)) + return tx.serialize() + + def publishBLockTx(self, kbv, Kbs, output_amount, feerate, unlock_time: int = 0) -> bytes: + b_lock_tx = self.createBLockTx(Kbs, output_amount) + + b_lock_tx = self.fundTx(b_lock_tx, feerate) + b_lock_tx_id = self.getTxid(b_lock_tx) + b_lock_tx = self.signTxWithWallet(b_lock_tx) + + return bytes.fromhex(self.publishTx(b_lock_tx)) + + def getBLockSpendTxFee(self, tx, fee_rate: int) -> int: + witness_bytes = 120 # TODO + size = len(tx.serialize()) + witness_bytes + pay_fee = round(fee_rate * size / 1000) + self._log.info(f'BLockSpendTx fee_rate, vsize, fee: {fee_rate}, {size}, {pay_fee}.') + return pay_fee + + def spendBLockTx(self, chain_b_lock_txid: bytes, address_to: str, kbv: bytes, kbs: bytes, cb_swap_value: int, b_fee: int, restore_height: int, lock_tx_vout=None) -> bytes: + self._log.info('spendBLockTx %s:\n', chain_b_lock_txid.hex()) + locked_n = lock_tx_vout + + Kbs = self.getPubkey(kbs) + script_pk = self.getPkDest(Kbs) + + if locked_n is None: + self._log.debug(f'Unknown lock vout, searching tx: {chain_b_lock_txid.hex()}') + # When refunding a lock tx, it should be in the wallet as a sent tx + wtx = self.rpc_wallet('gettransaction', [chain_b_lock_txid.hex(), ]) + lock_tx = self.loadTx(bytes.fromhex(wtx['hex'])) + locked_n = findOutput(lock_tx, script_pk) + + ensure(locked_n is not None, 'Output not found in tx') + pkh_to = self.decodeAddress(address_to) + + tx = CTransaction() + tx.version = self.txVersion() + + chain_b_lock_txid_int = b2i(chain_b_lock_txid) + + tx.vin.append(CTxIn(COutPoint(chain_b_lock_txid_int, locked_n, 0), + sequence=0)) + tx.vout.append(self.txoType()(cb_swap_value, self.getPubkeyHashDest(pkh_to))) + + pay_fee = self.getBLockSpendTxFee(tx, b_fee) + tx.vout[0].value = cb_swap_value - pay_fee + + b_lock_spend_tx = tx.serialize() + b_lock_spend_tx = self.signTxWithKey(b_lock_spend_tx, kbs) + + return bytes.fromhex(self.publishTx(b_lock_spend_tx)) + + def findTxnByHash(self, txid_hex: str): + try: + txout = self.rpc('gettxout', [txid_hex, 0, 0, True]) + except Exception as e: + # self._log.warning('gettxout {}'.format(e)) + return None + + confirmations: int = 0 if 'confirmations' not in txout else txout['confirmations'] + if confirmations >= self.blocks_confirmed: + block_height = self.getChainHeight() - confirmations # TODO: Better way? + return {'txid': txid_hex, 'amount': 0, 'height': block_height} + return None + + def isTxExistsError(self, err_str: str) -> bool: + return 'transaction already exists' in err_str or 'already have transaction' in err_str + + def isTxNonFinalError(self, err_str: str) -> bool: + return 'locks on inputs not met' in err_str diff --git a/basicswap/interface/dcr/messages.py b/basicswap/interface/dcr/messages.py index 8138c41..2c37aa0 100644 --- a/basicswap/interface/dcr/messages.py +++ b/basicswap/interface/dcr/messages.py @@ -153,7 +153,7 @@ class CTransaction: o += script_bytes def serialize(self, ser_type=TxSerializeType.Full) -> bytes: - data = bytearray() + data = bytes() version = (self.version & 0xffff) | (ser_type << 16) data += version.to_bytes(4, 'little') @@ -195,3 +195,10 @@ class CTransaction: def TxHashFull(self) -> bytes: raise ValueError('todo') + + +def findOutput(tx, script_pk: bytes): + for i in range(len(tx.vout)): + if tx.vout[i].script_pubkey == script_pk: + return i + return None diff --git a/basicswap/interface/dcr/rpc.py b/basicswap/interface/dcr/rpc.py index fd9761b..b536e11 100644 --- a/basicswap/interface/dcr/rpc.py +++ b/basicswap/interface/dcr/rpc.py @@ -27,6 +27,15 @@ def callrpc(rpc_port, auth, method, params=[], host='127.0.0.1'): return r['result'] +def openrpc(rpc_port, auth, host='127.0.0.1'): + try: + url = 'http://{}@{}:{}/'.format(auth, host, rpc_port) + return Jsonrpc(url) + except Exception as ex: + traceback.print_exc() + raise ValueError('RPC error ' + str(ex)) + + def make_rpc_func(port, auth, host='127.0.0.1'): port = port auth = auth diff --git a/basicswap/interface/dcr/script.py b/basicswap/interface/dcr/script.py index bdda286..a40edb2 100644 --- a/basicswap/interface/dcr/script.py +++ b/basicswap/interface/dcr/script.py @@ -9,14 +9,19 @@ OP_0 = 0x00 OP_DATA_1 = 0x01 OP_1NEGATE = 0x4f OP_1 = 0x51 +OP_IF = 0x63 +OP_ELSE = 0x67 +OP_ENDIF = 0x68 +OP_DROP = 0x75 +OP_DUP = 0x76 OP_EQUAL = 0x87 +OP_EQUALVERIFY = 0x88 OP_PUSHDATA1 = 0x4c OP_PUSHDATA2 = 0x4d OP_PUSHDATA4 = 0x4e -OP_DUP = 0x76 -OP_EQUALVERIFY = 0x88 OP_HASH160 = 0xa9 OP_CHECKSIG = 0xac +OP_CHECKMULTISIG = 0xae OP_CHECKSEQUENCEVERIFY = 0xb2 diff --git a/basicswap/interface/firo.py b/basicswap/interface/firo.py index 0be2c3f..d7f7701 100644 --- a/basicswap/interface/firo.py +++ b/basicswap/interface/firo.py @@ -76,7 +76,7 @@ class FIROInterface(BTCInterface): return addr_info['ismine'] return addr_info['ismine'] or addr_info['iswatchonly'] - def getSCLockScriptAddress(self, lock_script): + def getSCLockScriptAddress(self, lock_script: bytes) -> str: lock_tx_dest = self.getScriptDest(lock_script) address = self.encodeScriptDest(lock_tx_dest) @@ -201,7 +201,7 @@ class FIROInterface(BTCInterface): add_bytes = 107 size = len(tx.serialize_with_witness()) + add_bytes pay_fee = round(fee_rate * size / 1000) - self._log.info(f'BLockSpendTx fee_rate, size, fee: {fee_rate}, {size}, {pay_fee}.') + self._log.info(f'BLockSpendTx fee_rate, size, fee: {fee_rate}, {size}, {pay_fee}.') return pay_fee def signTxWithKey(self, tx: bytes, key: bytes) -> bytes: diff --git a/basicswap/interface/nav.py b/basicswap/interface/nav.py index 250d257..ade690e 100644 --- a/basicswap/interface/nav.py +++ b/basicswap/interface/nav.py @@ -13,7 +13,12 @@ from coincurve.keys import ( PublicKey, PrivateKey, ) -from .btc import BTCInterface, find_vout_for_address_from_txobj, findOutput +from basicswap.interface.btc import ( + BTCInterface, + extractScriptLockRefundScriptValues, + findOutput, + find_vout_for_address_from_txobj, +) from basicswap.rpc import make_rpc_func from basicswap.chainparams import Coins from basicswap.interface.contrib.nav_test_framework.mininode import ( @@ -24,7 +29,6 @@ from basicswap.interface.contrib.nav_test_framework.mininode import ( CTransaction, CTxInWitness, FromHex, - uint256_from_str, ) from basicswap.util.crypto import hash160 from basicswap.util.address import ( @@ -33,7 +37,7 @@ from basicswap.util.address import ( encodeAddress, ) from basicswap.util import ( - i2b, i2h, + b2i, i2b, i2h, ensure, ) from basicswap.basicswap_util import ( @@ -305,7 +309,7 @@ class NAVInterface(BTCInterface): def createRedeemTxn(self, prevout, output_addr: str, output_value: int, txn_script: bytes) -> str: tx = CTransaction() tx.nVersion = self.txVersion() - prev_txid = uint256_from_str(bytes.fromhex(prevout['txid'])[::-1]) + prev_txid = b2i(bytes.fromhex(prevout['txid'])) tx.vin.append(CTxIn(COutPoint(prev_txid, prevout['vout']), scriptSig=self.getScriptScriptSig(txn_script))) @@ -319,7 +323,7 @@ class NAVInterface(BTCInterface): tx = CTransaction() tx.nVersion = self.txVersion() tx.nLockTime = locktime - prev_txid = uint256_from_str(bytes.fromhex(prevout['txid'])[::-1]) + prev_txid = b2i(bytes.fromhex(prevout['txid'])) tx.vin.append(CTxIn(COutPoint(prev_txid, prevout['vout']), nSequence=sequence, scriptSig=self.getScriptScriptSig(txn_script))) @@ -512,7 +516,7 @@ class NAVInterface(BTCInterface): tx.vout.append(self.txoType()(output_amount, script_pk)) return tx.serialize() - def spendBLockTx(self, chain_b_lock_txid: bytes, address_to: str, kbv: bytes, kbs: bytes, cb_swap_value: int, b_fee: int, restore_height: int) -> bytes: + def spendBLockTx(self, chain_b_lock_txid: bytes, address_to: str, kbv: bytes, kbs: bytes, cb_swap_value: int, b_fee: int, restore_height: int, lock_tx_vout=None) -> bytes: self._log.info('spendBLockTx %s:\n', chain_b_lock_txid.hex()) wtx = self.rpc('gettransaction', [chain_b_lock_txid.hex(), ]) lock_tx = self.loadTx(bytes.fromhex(wtx['hex'])) @@ -526,7 +530,7 @@ class NAVInterface(BTCInterface): tx = CTransaction() tx.nVersion = self.txVersion() - chain_b_lock_txid_int = uint256_from_str(chain_b_lock_txid[::-1]) + chain_b_lock_txid_int = b2i(chain_b_lock_txid) script_sig = self.getInputScriptForPubkeyHash(self.getPubkeyHash(Kbs)) @@ -678,7 +682,7 @@ class NAVInterface(BTCInterface): ensure(locked_n is not None, 'Output not found in tx') locked_coin = tx_lock_refund.vout[locked_n].nValue - A, B, lock2_value, C = self.extractScriptLockRefundScriptValues(script_lock_refund) + A, B, lock2_value, C = extractScriptLockRefundScriptValues(script_lock_refund) tx_lock_refund.rehash() tx_lock_refund_hash_int = tx_lock_refund.sha256 diff --git a/basicswap/interface/part.py b/basicswap/interface/part.py index db9af17..8d54cf2 100644 --- a/basicswap/interface/part.py +++ b/basicswap/interface/part.py @@ -28,8 +28,13 @@ from basicswap.util.script import ( from basicswap.util.address import ( encodeStealthAddress, ) +from basicswap.interface.btc import ( + BTCInterface, + extractScriptLockScriptValues, + extractScriptLockRefundScriptValues, +) + from basicswap.chainparams import Coins, chainparams -from .btc import BTCInterface class BalanceTypes(IntEnum): @@ -354,7 +359,7 @@ class PARTInterfaceBlind(PARTInterface): lock_txo_scriptpk = bytes.fromhex(lock_tx_obj['vout'][lock_output_n]['scriptPubKey']['hex']) script_pk = CScript([OP_0, hashlib.sha256(script_out).digest()]) ensure(lock_txo_scriptpk == script_pk, 'Bad output script') - A, B = self.extractScriptLockScriptValues(script_out) + A, B = extractScriptLockScriptValues(script_out) ensure(A == Kal, 'Bad script leader pubkey') ensure(B == Kaf, 'Bad script follower pubkey') @@ -402,7 +407,7 @@ class PARTInterfaceBlind(PARTInterface): lock_refund_txo_scriptpk = bytes.fromhex(lock_refund_tx_obj['vout'][lock_refund_output_n]['scriptPubKey']['hex']) script_pk = CScript([OP_0, hashlib.sha256(script_out).digest()]) ensure(lock_refund_txo_scriptpk == script_pk, 'Bad output script') - A, B, csv_val, C = self.extractScriptLockRefundScriptValues(script_out) + A, B, csv_val, C = extractScriptLockRefundScriptValues(script_out) ensure(A == Kal, 'Bad script pubkey') ensure(B == Kaf, 'Bad script pubkey') ensure(csv_val == csv_val_expect, 'Bad script csv value') @@ -632,7 +637,7 @@ class PARTInterfaceBlind(PARTInterface): addr_info = self.rpc_wallet('getaddressinfo', [addr_out]) output_pubkey_hex = addr_info['pubkey'] - A, B, lock2_value, C = self.extractScriptLockRefundScriptValues(script_lock_refund) + A, B, lock2_value, C = extractScriptLockRefundScriptValues(script_lock_refund) # Follower won't be able to decode output to check amount, shouldn't matter as fee is public and output is to leader, sum has to balance @@ -715,7 +720,7 @@ class PARTInterfaceBlind(PARTInterface): return -1 return None - def spendBLockTx(self, chain_b_lock_txid: bytes, address_to: str, kbv: bytes, kbs: bytes, cb_swap_value: int, b_fee: int, restore_height: int, spend_actual_balance: bool = False) -> bytes: + def spendBLockTx(self, chain_b_lock_txid: bytes, address_to: str, kbv: bytes, kbs: bytes, cb_swap_value: int, b_fee: int, restore_height: int, spend_actual_balance: bool = False, lock_tx_vout=None) -> bytes: Kbv = self.getPubkey(kbv) Kbs = self.getPubkey(kbs) sx_addr = self.formatStealthAddress(Kbv, Kbs) @@ -851,7 +856,7 @@ class PARTInterfaceAnon(PARTInterface): return -1 return None - def spendBLockTx(self, chain_b_lock_txid: bytes, address_to: str, kbv: bytes, kbs: bytes, cb_swap_value: int, b_fee: int, restore_height: int, spend_actual_balance: bool = False) -> bytes: + def spendBLockTx(self, chain_b_lock_txid: bytes, address_to: str, kbv: bytes, kbs: bytes, cb_swap_value: int, b_fee: int, restore_height: int, spend_actual_balance: bool = False, lock_tx_vout=None) -> bytes: Kbv = self.getPubkey(kbv) Kbs = self.getPubkey(kbs) sx_addr = self.formatStealthAddress(Kbv, Kbs) diff --git a/basicswap/interface/pivx.py b/basicswap/interface/pivx.py index de420c5..038744c 100644 --- a/basicswap/interface/pivx.py +++ b/basicswap/interface/pivx.py @@ -107,7 +107,7 @@ class PIVXInterface(BTCInterface): add_bytes = 107 size = len(tx.serialize_with_witness()) + add_bytes pay_fee = round(fee_rate * size / 1000) - self._log.info(f'BLockSpendTx fee_rate, size, fee: {fee_rate}, {size}, {pay_fee}.') + self._log.info(f'BLockSpendTx fee_rate, size, fee: {fee_rate}, {size}, {pay_fee}.') return pay_fee def signTxWithKey(self, tx: bytes, key: bytes) -> bytes: diff --git a/basicswap/interface/xmr.py b/basicswap/interface/xmr.py index 104c588..30b587f 100644 --- a/basicswap/interface/xmr.py +++ b/basicswap/interface/xmr.py @@ -409,7 +409,7 @@ class XMRInterface(CoinInterface): return None - def spendBLockTx(self, chain_b_lock_txid: bytes, address_to: str, kbv: bytes, kbs: bytes, cb_swap_value: int, b_fee_rate: int, restore_height: int, spend_actual_balance: bool = False) -> bytes: + def spendBLockTx(self, chain_b_lock_txid: bytes, address_to: str, kbv: bytes, kbs: bytes, cb_swap_value: int, b_fee_rate: int, restore_height: int, spend_actual_balance: bool = False, lock_tx_vout=None) -> bytes: ''' Notes: "Error: No unlocked balance in the specified subaddress(es)" can mean not enough funds after tx fee. diff --git a/basicswap/protocols/xmr_swap_1.py b/basicswap/protocols/xmr_swap_1.py index 70acc53..833449c 100644 --- a/basicswap/protocols/xmr_swap_1.py +++ b/basicswap/protocols/xmr_swap_1.py @@ -21,13 +21,17 @@ from basicswap.basicswap_util import ( from . import ProtocolInterface from basicswap.contrib.test_framework.script import ( CScript, CScriptOp, - OP_CHECKMULTISIG) + OP_CHECKMULTISIG +) def addLockRefundSigs(self, xmr_swap, ci): self.log.debug('Setting lock refund tx sigs') - witness_stack = [ - b'', + + witness_stack = [] + if ci.coin_type() not in (Coins.DCR, ): + witness_stack += [b'', ] + witness_stack += [ xmr_swap.al_lock_refund_tx_sig, xmr_swap.af_lock_refund_tx_sig, xmr_swap.a_lock_tx_script, @@ -74,7 +78,8 @@ def recoverNoScriptTxnWithKey(self, bid_id: bytes, encoded_key): address_to = self.getCachedStealthAddressForCoin(offer.coin_to) amount = bid.amount_to - txid = ci_to.spendBLockTx(xmr_swap.b_lock_tx_id, address_to, xmr_swap.vkbv, vkbs, bid.amount_to, xmr_offer.b_fee_rate, bid.chain_b_height_start, spend_actual_balance=True) + lock_tx_vout = bid.getLockTXBVout() + txid = ci_to.spendBLockTx(xmr_swap.b_lock_tx_id, address_to, xmr_swap.vkbv, vkbs, amount, xmr_offer.b_fee_rate, bid.chain_b_height_start, spend_actual_balance=True, lock_tx_vout=lock_tx_vout) 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, txid.hex(), session) session.commit() diff --git a/tests/basicswap/common.py b/tests/basicswap/common.py index 604acc0..6ad5f89 100644 --- a/tests/basicswap/common.py +++ b/tests/basicswap/common.py @@ -419,9 +419,10 @@ def compare_bid_states(states, expect_states, exact_match: bool = True) -> bool: return True -def compare_bid_states_unordered(states, expect_states) -> bool: +def compare_bid_states_unordered(states, expect_states, ignore_states=[]) -> bool: + ignore_states.append('Bid Delaying') for i in range(len(states) - 1, -1, -1): - if states[i][1] == 'Bid Delaying': + if states[i][1] in ignore_states: del states[i] try: diff --git a/tests/basicswap/extended/test_dcr.py b/tests/basicswap/extended/test_dcr.py index de861a1..49f2a86 100644 --- a/tests/basicswap/extended/test_dcr.py +++ b/tests/basicswap/extended/test_dcr.py @@ -5,9 +5,6 @@ # Distributed under the MIT software license, see the accompanying # file LICENSE or http://www.opensource.org/licenses/mit-license.php. -# TODO -# - Occasionally DCR simnet chain stalls. - import copy import logging import os @@ -27,6 +24,7 @@ from basicswap.basicswap import ( ) from basicswap.basicswap_util import ( TxLockTypes, + TxTypes ) from basicswap.util.crypto import ( hash160 @@ -80,7 +78,25 @@ def make_rpc_func(node_id, base_rpc_port): return rpc_func -def test_success_path(self, coin_from: Coins, coin_to: Coins): +def wait_for_dcr_height(http_port, num_blocks=3): + logging.info('Waiting for DCR chain height %d', num_blocks) + for i in range(60): + if test_delay_event.is_set(): + raise ValueError('Test stopped.') + try: + wallet = read_json_api(http_port, 'wallets/dcr') + decred_blocks = wallet['blocks'] + print('decred_blocks', decred_blocks) + if decred_blocks >= num_blocks: + return + except Exception as e: + print('Error reading wallets', str(e)) + + test_delay_event.wait(1) + raise ValueError(f'wait_for_decred_blocks failed http_port: {http_port}') + + +def run_test_success_path(self, coin_from: Coins, coin_to: Coins): logging.info(f'---------- Test {coin_from.name} to {coin_to.name}') node_from = 0 @@ -106,7 +122,7 @@ def test_success_path(self, coin_from: Coins, coin_to: Coins): swap_clients[node_from].acceptBid(bid_id) wait_for_bid(test_delay_event, swap_clients[node_from], bid_id, BidStates.SWAP_COMPLETED, wait_for=120) - wait_for_bid(test_delay_event, swap_clients[node_to], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=120) + wait_for_bid(test_delay_event, swap_clients[node_to], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=30) # Verify lock tx spends are found in the expected wallets bid, offer = swap_clients[node_from].getBidAndOffer(bid_id) @@ -139,7 +155,7 @@ def test_success_path(self, coin_from: Coins, coin_to: Coins): assert (compare_bid_states(bidder_states, self.states_bidder_sh[0]) is True) -def test_bad_ptx(self, coin_from: Coins, coin_to: Coins): +def run_test_bad_ptx(self, coin_from: Coins, coin_to: Coins): # Invalid PTX sent, swap should stall and ITx and PTx should be reclaimed by senders logging.info(f'---------- Test bad ptx {coin_from.name} to {coin_to.name}') @@ -164,7 +180,7 @@ def test_bad_ptx(self, coin_from: Coins, coin_to: Coins): swap_clients[node_to].setBidDebugInd(bid_id, DebugTypes.MAKE_INVALID_PTX) wait_for_bid(test_delay_event, swap_clients[node_from], bid_id, BidStates.SWAP_COMPLETED, wait_for=120) - wait_for_bid(test_delay_event, swap_clients[node_to], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=120) + wait_for_bid(test_delay_event, swap_clients[node_to], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=30) js_0_bid = read_json_api(1800 + node_from, 'bids/{}'.format(bid_id.hex())) js_1_bid = read_json_api(1800 + node_to, 'bids/{}'.format(bid_id.hex())) @@ -190,6 +206,8 @@ def test_bad_ptx(self, coin_from: Coins, coin_to: Coins): offerer_states = read_json_api(1800 + node_from, path) bidder_states = read_json_api(1800 + node_to, path) + if coin_to not in (Coins.XMR,): + return # Hard to get the timing right assert (compare_bid_states_unordered(offerer_states, self.states_offerer_sh[1]) is True) assert (compare_bid_states_unordered(bidder_states, self.states_bidder_sh[1]) is True) @@ -200,7 +218,7 @@ def test_bad_ptx(self, coin_from: Coins, coin_to: Coins): assert (js_1['num_swapping'] == 0 and js_1['num_watched_outputs'] == 0) -def test_itx_refund(self, coin_from: Coins, coin_to: Coins): +def run_test_itx_refund(self, coin_from: Coins, coin_to: Coins): # Offerer claims PTX and refunds ITX after lock expires # Bidder loses PTX value without gaining ITX value logging.info(f'---------- Test itx refund {coin_from.name} to {coin_to.name}') @@ -250,6 +268,189 @@ def test_itx_refund(self, coin_from: Coins, coin_to: Coins): assert (bid.amount_to - node_from_ci_to.make_int(wtx['details'][0]['amount']) < max_fee) +def run_test_ads_success_path(self, coin_from: Coins, coin_to: Coins): + logging.info(f'---------- Test ADS swap {coin_from.name} to {coin_to.name}') + + # Offerer sends the offer + # Bidder sends the bid + id_offerer: int = 0 + id_bidder: int = 1 + + swap_clients = self.swap_clients + reverse_bid: bool = coin_from in swap_clients[id_offerer].scriptless_coins + ci_from = swap_clients[id_offerer].ci(coin_from) + ci_to = swap_clients[id_bidder].ci(coin_to) + + self.prepare_balance(coin_to, 100.0, 1801, 1800) + self.prepare_balance(coin_from, 100.0, 1800, 1801) + + # Leader sends the initial (chain a) lock tx. + # Follower sends the participate (chain b) lock tx. + id_leader: int = id_bidder if reverse_bid else id_offerer + id_follower: int = id_offerer if reverse_bid else id_bidder + logging.info(f'Offerer, bidder, leader, follower: {id_offerer}, {id_bidder}, {id_leader}, {id_follower}') + + amt_swap = ci_from.make_int(random.uniform(0.1, 2.0), r=1) + rate_swap = ci_to.make_int(random.uniform(0.2, 20.0), r=1) + offer_id = swap_clients[id_offerer].postOffer(coin_from, coin_to, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP) + + wait_for_offer(test_delay_event, swap_clients[id_bidder], offer_id) + bid_id = swap_clients[id_bidder].postXmrBid(offer_id, amt_swap) + wait_for_bid(test_delay_event, swap_clients[id_offerer], bid_id, BidStates.BID_RECEIVED) + + swap_clients[id_offerer].acceptBid(bid_id) + + wait_for_bid(test_delay_event, swap_clients[id_offerer], bid_id, BidStates.SWAP_COMPLETED, wait_for=(180)) + wait_for_bid(test_delay_event, swap_clients[id_bidder], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=(30)) + + if reverse_bid: + return # TODO + + # Verify lock tx spends are found in the expected wallets + bid, xmr_swap = swap_clients[id_offerer].getXmrBid(bid_id) + + node_from_ci_to = swap_clients[0].ci(coin_to) + max_fee: int = 10000 + if node_from_ci_to.coin_type() in (Coins.XMR, ): + pass + else: + wtx = node_from_ci_to.rpc_wallet('gettransaction', [bid.xmr_b_lock_tx.spend_txid.hex(),]) + assert (bid.amount_to - node_from_ci_to.make_int(wtx['details'][0]['amount']) < max_fee) + + node_to_ci_from = swap_clients[1].ci(coin_from) + if node_to_ci_from.coin_type() in (Coins.XMR, ): + pass + else: + wtx = node_to_ci_from.rpc_wallet('gettransaction', [xmr_swap.a_lock_spend_tx_id.hex(),]) + assert (bid.amount - node_to_ci_from.make_int(wtx['details'][0]['amount']) < max_fee) + + bid_id_hex = bid_id.hex() + path = f'bids/{bid_id_hex}/states' + offerer_states = read_json_api(1800 + id_offerer, path) + bidder_states = read_json_api(1800 + id_bidder, path) + + assert (compare_bid_states(offerer_states, self.states_offerer[0]) is True) + assert (compare_bid_states(bidder_states, self.states_bidder[0]) is True) + + +def run_test_ads_both_refund(self, coin_from: Coins, coin_to: Coins, lock_value: int = 32) -> None: + logging.info('---------- Test {} to {} both lock txns refunded'.format(coin_from.name, coin_to.name)) + + id_offerer: int = 0 + id_bidder: int = 1 + + swap_clients = self.swap_clients + reverse_bid: bool = coin_from in swap_clients[id_offerer].scriptless_coins + ci_from = swap_clients[id_offerer].ci(coin_from) + ci_to = swap_clients[id_offerer].ci(coin_to) + + if reverse_bid: + self.prepare_balance(coin_to, 100.0, 1801, 1800) + self.prepare_balance(coin_from, 100.0, 1800, 1801) + + id_leader: int = id_bidder if reverse_bid else id_offerer + id_follower: int = id_offerer if reverse_bid else id_bidder + logging.info(f'Offerer, bidder, leader, follower: {id_offerer}, {id_bidder}, {id_leader}, {id_follower}') + + amt_swap = ci_from.make_int(random.uniform(0.1, 2.0), r=1) + rate_swap = ci_to.make_int(random.uniform(0.2, 20.0), r=1) + + debug_case = (None, DebugTypes.OFFER_LOCK_2_VALUE_INC) + try: + swap_clients[0]._debug_cases.append(debug_case) + swap_clients[1]._debug_cases.append(debug_case) + offer_id = swap_clients[id_offerer].postOffer( + coin_from, coin_to, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP, + lock_type=TxLockTypes.SEQUENCE_LOCK_BLOCKS, lock_value=lock_value) + wait_for_offer(test_delay_event, swap_clients[id_bidder], offer_id) + offer = swap_clients[id_bidder].getOffer(offer_id) + finally: + swap_clients[0]._debug_cases.remove(debug_case) + swap_clients[1]._debug_cases.remove(debug_case) + + 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.CREATE_INVALID_COIN_B_LOCK) + 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.XMR_SWAP_FAILED_REFUNDED, sent=leader_sent_bid, wait_for=(self.extra_wait_time + 180)) + wait_for_bid(test_delay_event, swap_clients[id_follower], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, sent=(not leader_sent_bid), wait_for=(self.extra_wait_time + 40)) + + if reverse_bid: + return # TODO + + # Verify lock tx spends are found in the expected wallets + bid, xmr_swap = swap_clients[id_offerer].getXmrBid(bid_id) + lock_refund_spend_txid = bid.txns[TxTypes.XMR_SWAP_A_LOCK_REFUND_SPEND].txid + + bid, xmr_swap = swap_clients[id_bidder].getXmrBid(bid_id) + + node_from_ci_from = swap_clients[0].ci(coin_from) + max_fee: int = 10000 + if node_from_ci_from.coin_type() in (Coins.XMR, ): + pass + else: + wtx = node_from_ci_from.rpc_wallet('gettransaction', [lock_refund_spend_txid.hex(),]) + assert (bid.amount - node_from_ci_from.make_int(wtx['details'][0]['amount']) < max_fee) + + node_to_ci_to = swap_clients[1].ci(coin_to) + if node_to_ci_to.coin_type() in (Coins.XMR, ): + pass + else: + wtx = node_to_ci_to.rpc_wallet('gettransaction', [bid.xmr_b_lock_tx.spend_txid.hex(),]) + assert (bid.amount_to - node_to_ci_to.make_int(wtx['details'][0]['amount']) < max_fee) + + bid_id_hex = bid_id.hex() + path = f'bids/{bid_id_hex}/states' + offerer_states = read_json_api(1800 + id_offerer, path) + bidder_states = read_json_api(1800 + id_bidder, path) + + assert (compare_bid_states(offerer_states, self.states_offerer[1]) is True) + assert (compare_bid_states(bidder_states, self.states_bidder[1]) is True) + + +def run_test_ads_swipe_refund(self, coin_from: Coins, coin_to: Coins, lock_value: int = 32) -> None: + logging.info('---------- Test {} to {} coin a lock refund tx swiped'.format(coin_from.name, coin_to.name)) + + id_offerer: int = 0 + id_bidder: int = 1 + + swap_clients = self.swap_clients + reverse_bid: bool = coin_from in swap_clients[id_offerer].scriptless_coins + ci_from = swap_clients[id_offerer].ci(coin_from) + ci_to = swap_clients[id_offerer].ci(coin_to) + + id_leader: int = id_bidder if reverse_bid else id_offerer + id_follower: int = id_offerer if reverse_bid else id_bidder + logging.info(f'Offerer, bidder, leader, follower: {id_offerer}, {id_bidder}, {id_leader}, {id_follower}') + + if reverse_bid: + self.prepare_balance(coin_to, 100.0, 1801, 1800) + self.prepare_balance(coin_from, 100.0, 1800, 1801) + + amt_swap = ci_from.make_int(random.uniform(0.1, 2.0), r=1) + rate_swap = ci_to.make_int(random.uniform(0.2, 20.0), r=1) + offer_id = swap_clients[id_offerer].postOffer( + coin_from, coin_to, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP, + lock_type=TxLockTypes.SEQUENCE_LOCK_BLOCKS, lock_value=lock_value) + wait_for_offer(test_delay_event, swap_clients[id_bidder], offer_id) + offer = swap_clients[id_bidder].getOffer(offer_id) + + 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) + + 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) + 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)) + + def prepareDCDDataDir(datadir, node_id, conf_file, dir_prefix, num_nodes=3): node_dir = os.path.join(datadir, dir_prefix + str(node_id)) if not os.path.exists(node_dir): @@ -298,8 +499,9 @@ class Test(BaseTest): test_coin = Coins.DCR dcr_daemons = [] start_ltc_nodes = False - start_xmr_nodes = False + start_xmr_nodes = True dcr_mining_addr = 'SsYbXyjkKAEXXcGdFgr4u4bo4L8RkCxwQpH' + extra_wait_time = 0 hex_seeds = [ 'e8574b2a94404ee62d8acc0258cab4c0defcfab8a5dfc2f4954c1f9d7e09d72a', @@ -331,15 +533,19 @@ class Test(BaseTest): ci0 = cls.swap_clients[0].ci(cls.test_coin) num_passed: int = 0 - for i in range(5): + for i in range(30): try: ci0.rpc_wallet('purchaseticket', [cls.dcr_ticket_account, 0.1, 0]) num_passed += 1 + if num_passed >= 5: + break + test_delay_event.wait(0.1) except Exception as e: if 'double spend' in str(e): pass else: logging.warning('coins_loop purchaseticket {}'.format(e)) + test_delay_event.wait(0.5) try: if num_passed >= 5: @@ -436,6 +642,8 @@ class Test(BaseTest): 'address': js_w[coin_ticker][address_type], 'subfee': False, } + if coin in (Coins.XMR, ): + post_json['sweepall'] = False json_rv = read_json_api(port_take_from_node, 'wallets/{}/withdraw'.format(coin_ticker.lower()), post_json) assert (len(json_rv['txid']) == 64) wait_for_amount: float = amount @@ -763,22 +971,49 @@ class Test(BaseTest): assert (amount_proved >= require_amount) def test_02_part_coin(self): - test_success_path(self, Coins.PART, self.test_coin) + run_test_success_path(self, Coins.PART, self.test_coin) def test_03_coin_part(self): - test_success_path(self, self.test_coin, Coins.PART) + run_test_success_path(self, self.test_coin, Coins.PART) def test_04_part_coin_bad_ptx(self): - test_bad_ptx(self, Coins.PART, self.test_coin) + run_test_bad_ptx(self, Coins.PART, self.test_coin) def test_05_coin_part_bad_ptx(self): - test_bad_ptx(self, self.test_coin, Coins.PART) + run_test_bad_ptx(self, self.test_coin, Coins.PART) def test_06_part_coin_itx_refund(self): - test_itx_refund(self, Coins.PART, self.test_coin) + run_test_itx_refund(self, Coins.PART, self.test_coin) def test_07_coin_part_itx_refund(self): - test_itx_refund(self, self.test_coin, Coins.PART) + run_test_itx_refund(self, self.test_coin, Coins.PART) + + def test_08_ads_coin_xmr(self): + run_test_ads_success_path(self, self.test_coin, Coins.XMR) + + def test_09_ads_xmr_coin(self): + # Reverse bid + run_test_ads_success_path(self, Coins.XMR, self.test_coin) + + def test_10_ads_part_coin(self): + run_test_ads_success_path(self, Coins.PART, self.test_coin) + + def test_11_ads_coin_xmr_both_refund(self): + run_test_ads_both_refund(self, self.test_coin, Coins.XMR, lock_value=20) + + def test_12_ads_xmr_coin_both_refund(self): + # Reverse bid + run_test_ads_both_refund(self, Coins.XMR, self.test_coin, lock_value=20) + + def test_13_ads_part_coin_both_refund(self): + run_test_ads_both_refund(self, Coins.PART, self.test_coin, lock_value=20) + + def test_14_ads_coin_xmr_swipe_refund(self): + run_test_ads_swipe_refund(self, self.test_coin, Coins.XMR, lock_value=20) + + def test_15_ads_xmr_coin_swipe_refund(self): + # Reverse bid + run_test_ads_swipe_refund(self, Coins.XMR, self.test_coin, lock_value=20) if __name__ == '__main__': diff --git a/tests/basicswap/test_btc_xmr.py b/tests/basicswap/test_btc_xmr.py index ed13666..7543df0 100644 --- a/tests/basicswap/test_btc_xmr.py +++ b/tests/basicswap/test_btc_xmr.py @@ -26,7 +26,7 @@ from basicswap.util import ( make_int, format_amount, ) -from basicswap.interface import Curves +from basicswap.interface.base import Curves from tests.basicswap.util import ( read_json_api, ) diff --git a/tests/basicswap/test_run.py b/tests/basicswap/test_run.py index 629aec3..9af3624 100644 --- a/tests/basicswap/test_run.py +++ b/tests/basicswap/test_run.py @@ -337,7 +337,7 @@ class Test(BaseTest): wait_for_in_progress(test_delay_event, swap_clients[1], bid_id, sent=True) wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=80) - wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=80) + wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=30) # Verify lock tx spends are found in the expected wallets bid, offer = swap_clients[0].getBidAndOffer(bid_id) @@ -381,7 +381,7 @@ class Test(BaseTest): wait_for_in_progress(test_delay_event, swap_clients[0], bid_id, sent=True) wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=80) - wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, wait_for=80) + wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, wait_for=30) js_0 = read_json_api(1800) js_1 = read_json_api(1801) @@ -404,7 +404,7 @@ class Test(BaseTest): wait_for_in_progress(test_delay_event, swap_clients[1], bid_id, sent=True) wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=80) - wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=80) + wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=30) js_0 = read_json_api(1800) js_1 = read_json_api(1801) @@ -428,7 +428,7 @@ class Test(BaseTest): swap_clients[0].acceptBid(bid_id) wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=80) - wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.BID_ABANDONED, sent=True, wait_for=80) + wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.BID_ABANDONED, sent=True, wait_for=30) js_0_bid = read_json_api(1800, 'bids/{}'.format(bid_id.hex())) js_1_bid = read_json_api(1801, 'bids/{}'.format(bid_id.hex())) @@ -464,7 +464,7 @@ class Test(BaseTest): swap_clients[0].acceptBid(bid_id) wait_for_bid_tx_state(test_delay_event, swap_clients[0], bid_id, TxStates.TX_REDEEMED, TxStates.TX_REDEEMED, wait_for=80) - wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=80) + wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=30) js_0 = read_json_api(1800) assert (js_0['num_swapping'] == 0 and js_0['num_watched_outputs'] == 0) @@ -513,7 +513,7 @@ class Test(BaseTest): bid_id = swap_clients[1].postBid(offer_id, offer.amount_from) wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=80) - wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=80) + wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=30) def test_10_bad_ptx(self): # Invalid PTX sent, swap should stall and ITx and PTx should be reclaimed by senders @@ -534,7 +534,7 @@ class Test(BaseTest): swap_clients[0].acceptBid(bid_id) wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=120) - wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=120) + wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=30) js_0_bid = read_json_api(1800, 'bids/{}'.format(bid_id.hex())) js_1_bid = read_json_api(1801, 'bids/{}'.format(bid_id.hex())) @@ -574,7 +574,7 @@ class Test(BaseTest): swap_clients[0].acceptBid(bid_id) wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=120) - wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.BID_ABANDONED, sent=True, wait_for=120) + wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.BID_ABANDONED, sent=True, wait_for=30) js_0_bid = read_json_api(1800, 'bids/{}'.format(bid_id.hex())) js_1_bid = read_json_api(1801, 'bids/{}'.format(bid_id.hex())) @@ -727,7 +727,7 @@ class Test(BaseTest): swap_clients[2].acceptBid(bid_id) wait_for_bid(test_delay_event, swap_clients[2], bid_id, BidStates.SWAP_COMPLETED, wait_for=80) - wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=80) + wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=30) # Verify expected inputs were used bid, offer = swap_clients[2].getBidAndOffer(bid_id) @@ -762,7 +762,7 @@ class Test(BaseTest): wait_for_in_progress(test_delay_event, swap_clients[0], bid_id, sent=True) wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=80) - wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, wait_for=80) + wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, wait_for=30) def pass_99_delay(self): logging.info('Delay') diff --git a/tests/basicswap/test_xmr.py b/tests/basicswap/test_xmr.py index b8d67c6..599cbef 100644 --- a/tests/basicswap/test_xmr.py +++ b/tests/basicswap/test_xmr.py @@ -1528,7 +1528,7 @@ class Test(BaseTest): swap_clients[0].acceptXmrBid(bid_id) wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, wait_for=1800) - wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, wait_for=1800, sent=True) + wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, wait_for=30, sent=True) def test_16_new_subaddress(self): logging.info('---------- Test that new subaddresses are created')