diff --git a/basicswap/__init__.py b/basicswap/__init__.py index c1ec3f7..40f1c8e 100644 --- a/basicswap/__init__.py +++ b/basicswap/__init__.py @@ -1,3 +1,3 @@ name = "basicswap" -__version__ = "0.0.24" +__version__ = "0.0.25" diff --git a/basicswap/base.py b/basicswap/base.py index 524c97b..02b0b46 100644 --- a/basicswap/base.py +++ b/basicswap/base.py @@ -16,10 +16,8 @@ from .rpc import ( callrpc, ) from .util import ( - pubkeyToAddress, -) -from .basicswap_util import ( TemporaryError, + pubkeyToAddress, ) from .chainparams import ( Coins, diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index 0e35a58..551738f 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -26,7 +26,7 @@ import concurrent.futures from sqlalchemy.orm import sessionmaker, scoped_session from sqlalchemy.orm.session import close_all_sessions -from .interface_part import PARTInterface, PARTInterfaceAnon +from .interface_part import PARTInterface, PARTInterfaceAnon, PARTInterfaceBlind from .interface_btc import BTCInterface from .interface_ltc import LTCInterface from .interface_nmc import NMCInterface @@ -371,6 +371,7 @@ class BasicSwap(BaseApp): if coin == Coins.PART: self.coin_clients[Coins.PART_ANON] = self.coin_clients[coin] + self.coin_clients[Coins.PART_BLIND] = self.coin_clients[coin] if self.coin_clients[coin]['connection_type'] == 'rpc': if coin == Coins.XMR: @@ -384,6 +385,8 @@ class BasicSwap(BaseApp): def ci(self, coin): # Coin interface if coin == Coins.PART_ANON: return self.coin_clients[Coins.PART]['interface_anon'] + if coin == Coins.PART_BLIND: + return self.coin_clients[Coins.PART]['interface_blind'] return self.coin_clients[coin]['interface'] def createInterface(self, coin): @@ -447,6 +450,7 @@ class BasicSwap(BaseApp): self.coin_clients[coin]['interface'] = self.createInterface(coin) if coin == Coins.PART: self.coin_clients[coin]['interface_anon'] = PARTInterfaceAnon(self.coin_clients[coin], self.chain, self) + self.coin_clients[coin]['interface_blind'] = PARTInterfaceBlind(self.coin_clients[coin], self.chain, self) elif self.coin_clients[coin]['connection_type'] == 'passthrough': self.coin_clients[coin]['interface'] = self.createPassthroughInterface(coin) @@ -1843,7 +1847,11 @@ class BasicSwap(BaseApp): msg_buf.amount = int(amount) # Amount of coin_from address_out = self.getReceiveAddressFromPool(coin_from, offer_id, TxTypes.XMR_SWAP_A_LOCK) - msg_buf.dest_af = ci_from.decodeAddress(address_out) + if coin_from == Coins.PART_BLIND: + addrinfo = ci_from.rpc_callback('getaddressinfo', [address_out]) + msg_buf.dest_af = bytes.fromhex(addrinfo['pubkey']) + else: + msg_buf.dest_af = ci_from.decodeAddress(address_out) bid_created_at = int(time.time()) if offer.swap_type != SwapTypes.XMR_SWAP: @@ -1994,6 +2002,7 @@ class BasicSwap(BaseApp): xmr_swap.pkbsl = ci_to.getPubkey(kbsl) xmr_swap.vkbv = ci_to.sumKeys(kbvl, xmr_swap.vkbvf) + ensure(ci_to.verifyKey(xmr_swap.vkbv), 'Invalid key, vkbv') xmr_swap.pkbv = ci_to.sumPubkeys(xmr_swap.pkbvl, xmr_swap.pkbvf) xmr_swap.pkbs = ci_to.sumPubkeys(xmr_swap.pkbsl, xmr_swap.pkbsf) @@ -2007,32 +2016,68 @@ class BasicSwap(BaseApp): # MSG2F xmr_swap.a_lock_tx, xmr_swap.a_lock_tx_script = ci_from.createScriptLockTx( bid.amount, - xmr_swap.pkal, xmr_swap.pkaf, + xmr_swap.pkal, xmr_swap.pkaf, xmr_swap.vkbv ) - xmr_swap.a_lock_tx = ci_from.fundTx(xmr_swap.a_lock_tx, xmr_offer.a_fee_rate) + xmr_swap.a_lock_tx = ci_from.fundScriptLockTx(xmr_swap.a_lock_tx, xmr_offer.a_fee_rate, xmr_swap.vkbv) - xmr_swap.a_lock_tx_id = ci_from.getTxHash(xmr_swap.a_lock_tx) + xmr_swap.a_lock_tx_id = ci_from.getTxid(xmr_swap.a_lock_tx) a_lock_tx_dest = ci_from.getScriptDest(xmr_swap.a_lock_tx_script) xmr_swap.a_lock_refund_tx, xmr_swap.a_lock_refund_tx_script, xmr_swap.a_swap_refund_value = ci_from.createScriptLockRefundTx( xmr_swap.a_lock_tx, xmr_swap.a_lock_tx_script, xmr_swap.pkal, xmr_swap.pkaf, xmr_offer.lock_time_1, xmr_offer.lock_time_2, - xmr_offer.a_fee_rate + xmr_offer.a_fee_rate, xmr_swap.vkbv ) - xmr_swap.a_lock_refund_tx_id = ci_from.getTxHash(xmr_swap.a_lock_refund_tx) + xmr_swap.a_lock_refund_tx_id = ci_from.getTxid(xmr_swap.a_lock_refund_tx) - xmr_swap.al_lock_refund_tx_sig = ci_from.signTx(kal, xmr_swap.a_lock_refund_tx, 0, xmr_swap.a_lock_tx_script, bid.amount) - v = ci_from.verifyTxSig(xmr_swap.a_lock_refund_tx, xmr_swap.al_lock_refund_tx_sig, xmr_swap.pkal, 0, xmr_swap.a_lock_tx_script, bid.amount) - ensure(v, 'Invalid coin A lock tx refund tx leader sig') + prevout_amount = ci_from.getLockTxSwapOutputValue(bid, xmr_swap) + xmr_swap.al_lock_refund_tx_sig = ci_from.signTx(kal, xmr_swap.a_lock_refund_tx, 0, xmr_swap.a_lock_tx_script, prevout_amount) + v = ci_from.verifyTxSig(xmr_swap.a_lock_refund_tx, xmr_swap.al_lock_refund_tx_sig, xmr_swap.pkal, 0, xmr_swap.a_lock_tx_script, prevout_amount) + ensure(v, 'Invalid coin A lock refund tx leader sig') pkh_refund_to = ci_from.decodeAddress(self.getReceiveAddressForCoin(coin_from)) xmr_swap.a_lock_refund_spend_tx = ci_from.createScriptLockRefundSpendTx( xmr_swap.a_lock_refund_tx, xmr_swap.a_lock_refund_tx_script, pkh_refund_to, - xmr_offer.a_fee_rate + xmr_offer.a_fee_rate, xmr_swap.vkbv ) - xmr_swap.a_lock_refund_spend_tx_id = ci_from.getTxHash(xmr_swap.a_lock_refund_spend_tx) + xmr_swap.a_lock_refund_spend_tx_id = ci_from.getTxid(xmr_swap.a_lock_refund_spend_tx) + + # Double check txns before sending + self.log.debug('Bid: {} - Double checking chain A lock txns are valid before sending bid accept.'.format(bid_id.hex())) + check_lock_tx_inputs = False # TODO: check_lock_tx_inputs without txindex + _, xmr_swap.a_lock_tx_vout = ci_from.verifyLockTx( + xmr_swap.a_lock_tx, + xmr_swap.a_lock_tx_script, + bid.amount, + xmr_swap.pkal, + xmr_swap.pkaf, + xmr_offer.a_fee_rate, + check_lock_tx_inputs, + xmr_swap.vkbv) + + _, _, lock_refund_vout = ci_from.verifyLockRefundTx( + xmr_swap.a_lock_refund_tx, + xmr_swap.a_lock_tx, + xmr_swap.a_lock_refund_tx_script, + xmr_swap.a_lock_tx_id, + xmr_swap.a_lock_tx_vout, + xmr_offer.lock_time_1, + xmr_swap.a_lock_tx_script, + xmr_swap.pkal, + xmr_swap.pkaf, + xmr_offer.lock_time_2, + bid.amount, + xmr_offer.a_fee_rate, + xmr_swap.vkbv) + + ci_from.verifyLockRefundSpendTx( + xmr_swap.a_lock_refund_spend_tx, xmr_swap.a_lock_refund_tx, + xmr_swap.a_lock_refund_tx_id, xmr_swap.a_lock_refund_tx_script, + xmr_swap.pkal, + lock_refund_vout, xmr_swap.a_swap_refund_value, xmr_offer.a_fee_rate, + xmr_swap.vkbv) msg_buf = XmrBidAcceptMessage() msg_buf.bid_msg_id = bid_id @@ -2113,12 +2158,12 @@ class BasicSwap(BaseApp): session.remove() self.mxDB.release() - def setBidError(self, bid_id, bid, error_str, save_bid=True): + def setBidError(self, bid_id, bid, error_str, save_bid=True, xmr_swap=None): self.log.error('Bid %s - Error: %s', bid_id.hex(), error_str) bid.setState(BidStates.BID_ERROR) bid.state_note = 'error msg: ' + error_str if save_bid: - self.saveBid(bid_id, bid) + self.saveBid(bid_id, bid, xmr_swap=xmr_swap) def createInitiateTxn(self, coin_type, bid_id, bid, initiate_script): if self.coin_clients[coin_type]['connection_type'] != 'rpc': @@ -2340,7 +2385,8 @@ class BasicSwap(BaseApp): ro = self.callcoinrpc(Coins.PART, 'verifyrawtransaction', [redeem_txn, [prevout]]) ensure(ro['inputs_valid'] is True, 'inputs_valid is false') - ensure(ro['complete'] is True, 'complete is false') + # outputs_valid will be false if not a Particl txn + # ensure(ro['complete'] is True, 'complete is false') ensure(ro['validscripts'] == 1, 'validscripts != 1') if self.debug: @@ -2439,7 +2485,8 @@ class BasicSwap(BaseApp): ro = self.callcoinrpc(Coins.PART, 'verifyrawtransaction', [refund_txn, [prevout]]) ensure(ro['inputs_valid'] is True, 'inputs_valid is false') - ensure(ro['complete'] is True, 'complete is false') + # outputs_valid will be false if not a Particl txn + # ensure(ro['complete'] is True, 'complete is false') ensure(ro['validscripts'] == 1, 'validscripts != 1') if self.debug: @@ -2496,17 +2543,17 @@ class BasicSwap(BaseApp): # Bid saved in checkBidState def setLastHeightChecked(self, coin_type, tx_height): - chain_name = chainparams[coin_type]['name'] + coin_name = self.ci(coin_type).coin_name() if tx_height < 1: tx_height = self.lookupChainHeight(coin_type) if len(self.coin_clients[coin_type]['watched_outputs']) == 0: self.coin_clients[coin_type]['last_height_checked'] = tx_height - self.log.debug('Start checking %s chain at height %d', chain_name, tx_height) + self.log.debug('Start checking %s chain at height %d', coin_name, tx_height) if self.coin_clients[coin_type]['last_height_checked'] > tx_height: self.coin_clients[coin_type]['last_height_checked'] = tx_height - self.log.debug('Rewind checking of %s chain to height %d', chain_name, tx_height) + self.log.debug('Rewind checking of %s chain to height %d', coin_name, tx_height) return tx_height @@ -2652,7 +2699,7 @@ class BasicSwap(BaseApp): txid = ci_from.publishTx(xmr_swap.a_lock_refund_spend_tx) self.logBidEvent(bid.bid_id, EventLogTypes.LOCK_TX_A_REFUND_SPEND_TX_PUBLISHED, '', session) - self.log.info('Submitted coin a lock refund spend tx for bid {}'.format(bid_id.hex())) + self.log.info('Submitted coin a lock refund spend tx for bid {}, txid {}'.format(bid_id.hex(), txid.hex())) bid.txns[TxTypes.XMR_SWAP_A_LOCK_REFUND_SPEND] = SwapTx( bid_id=bid_id, tx_type=TxTypes.XMR_SWAP_A_LOCK_REFUND_SPEND, @@ -2713,7 +2760,7 @@ class BasicSwap(BaseApp): except Exception as ex: if 'Transaction already in block chain' in str(ex): self.log.info('Found coin a lock refund tx for bid {}'.format(bid_id.hex())) - txid = ci_from.getTxHash(xmr_swap.a_lock_refund_tx) + txid = ci_from.getTxid(xmr_swap.a_lock_refund_tx) bid.txns[TxTypes.XMR_SWAP_A_LOCK_REFUND] = SwapTx( bid_id=bid_id, tx_type=TxTypes.XMR_SWAP_A_LOCK_REFUND, @@ -2737,30 +2784,28 @@ class BasicSwap(BaseApp): # TODO: Timeout waiting for transactions bid_changed = False a_lock_tx_dest = ci_from.getScriptDest(xmr_swap.a_lock_tx_script) - utxos, chain_height = ci_from.getOutput(bid.xmr_a_lock_tx.txid, a_lock_tx_dest, bid.amount) + # Changed from ci_from.getOutput(bid.xmr_a_lock_tx.txid, a_lock_tx_dest, bid.amount, xmr_swap) - if len(utxos) < 1: + lock_tx_chain_info = ci_from.getLockTxHeight(bid.xmr_a_lock_tx.txid, a_lock_tx_dest, bid.amount, xmr_swap) + + if lock_tx_chain_info is None: return rv - if len(utxos) > 1: - raise ValueError('Too many outputs for chain A lock tx') - - utxo = utxos[0] - if not bid.xmr_a_lock_tx.chain_height and utxo['height'] != 0: + if not bid.xmr_a_lock_tx.chain_height and lock_tx_chain_info['height'] != 0: self.logBidEvent(bid.bid_id, EventLogTypes.LOCK_TX_A_SEEN, '', session) - block_header = ci_from.getBlockHeaderFromHeight(utxo['height']) + block_header = ci_from.getBlockHeaderFromHeight(lock_tx_chain_info['height']) bid.xmr_a_lock_tx.block_hash = bytes.fromhex(block_header['hash']) bid.xmr_a_lock_tx.block_height = block_header['height'] bid.xmr_a_lock_tx.block_time = block_header['time'] # Or median_time? bid_changed = True - if bid.xmr_a_lock_tx.chain_height != utxo['height'] and utxo['height'] != 0: - bid.xmr_a_lock_tx.chain_height = utxo['height'] + if bid.xmr_a_lock_tx.chain_height != lock_tx_chain_info['height'] and lock_tx_chain_info['height'] != 0: + bid.xmr_a_lock_tx.chain_height = lock_tx_chain_info['height'] bid_changed = True - if utxo['depth'] >= ci_from.blocks_confirmed: + 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) @@ -3233,7 +3278,7 @@ class BasicSwap(BaseApp): # assert(self.mxDB.locked()) self.log.debug('checkForSpends %s', coin_type) - if coin_type == Coins.PART and self.coin_clients[coin_type]['have_spent_index']: + if 'have_spent_index' in self.coin_clients[coin_type] and self.coin_clients[coin_type]['have_spent_index']: # TODO: batch getspentinfo for o in c['watched_outputs']: found_spend = None @@ -3414,7 +3459,6 @@ class BasicSwap(BaseApp): ci_from = self.ci(coin_from) coin_to = Coins(offer_data.coin_to) ci_to = self.ci(coin_to) - chain_from = chainparams[coin_from][self.chain] ensure(offer_data.coin_from != offer_data.coin_to, 'coin_from == coin_to') self.validateSwapType(coin_from, coin_to, offer_data.swap_type) @@ -3742,11 +3786,8 @@ class BasicSwap(BaseApp): raise ValueError('Invalid coin a pubkey.') xmr_swap.pkbsf = xmr_swap.pkasf - if not ci_to.verifyKey(xmr_swap.vkbvf): - raise ValueError('Invalid key.') - - if not ci_from.verifyPubkey(xmr_swap.pkaf): - raise ValueError('Invalid pubkey.') + ensure(ci_to.verifyKey(xmr_swap.vkbvf), 'Invalid key, vkbvf') + ensure(ci_from.verifyPubkey(xmr_swap.pkaf), 'Invalid pubkey, pkaf') self.log.info('Received valid bid %s for xmr offer %s', bid.bid_id.hex(), bid.offer_id.hex()) @@ -3800,10 +3841,7 @@ class BasicSwap(BaseApp): raise ValueError('Invalid coin a pubkey.') xmr_swap.pkbsl = xmr_swap.pkasl - if not ci_to.verifyKey(xmr_swap.vkbvl): - raise ValueError('Invalid key.') - - xmr_swap.vkbv = ci_to.sumKeys(xmr_swap.vkbvl, xmr_swap.vkbvf) + # vkbv and vkbvl are verified in processXmrBidAccept xmr_swap.pkbv = ci_to.sumPubkeys(xmr_swap.pkbvl, xmr_swap.pkbvf) xmr_swap.pkbs = ci_to.sumPubkeys(xmr_swap.pkbsl, xmr_swap.pkbsf) @@ -3930,6 +3968,10 @@ class BasicSwap(BaseApp): try: xmr_swap.pkal = msg_data.pkal xmr_swap.vkbvl = msg_data.kbvl + ensure(ci_to.verifyKey(xmr_swap.vkbvl), 'Invalid key, vkbvl') + xmr_swap.vkbv = ci_to.sumKeys(xmr_swap.vkbvl, xmr_swap.vkbvf) + ensure(ci_to.verifyKey(xmr_swap.vkbv), 'Invalid key, vkbv') + xmr_swap.pkbvl = ci_to.getPubkey(msg_data.kbvl) xmr_swap.kbsl_dleag = msg_data.kbsl_dleag @@ -3938,37 +3980,36 @@ class BasicSwap(BaseApp): xmr_swap.a_lock_refund_tx = msg_data.a_lock_refund_tx xmr_swap.a_lock_refund_tx_script = msg_data.a_lock_refund_tx_script xmr_swap.a_lock_refund_spend_tx = msg_data.a_lock_refund_spend_tx - xmr_swap.a_lock_refund_spend_tx_id = ci_from.getTxHash(xmr_swap.a_lock_refund_spend_tx) + xmr_swap.a_lock_refund_spend_tx_id = ci_from.getTxid(xmr_swap.a_lock_refund_spend_tx) xmr_swap.al_lock_refund_tx_sig = msg_data.al_lock_refund_tx_sig - # TODO: check_a_lock_tx_inputs without txindex + # TODO: check_lock_tx_inputs without txindex check_a_lock_tx_inputs = False xmr_swap.a_lock_tx_id, xmr_swap.a_lock_tx_vout = ci_from.verifyLockTx( xmr_swap.a_lock_tx, xmr_swap.a_lock_tx_script, bid.amount, xmr_swap.pkal, xmr_swap.pkaf, xmr_offer.a_fee_rate, - check_a_lock_tx_inputs - ) + check_a_lock_tx_inputs, xmr_swap.vkbv) a_lock_tx_dest = ci_from.getScriptDest(xmr_swap.a_lock_tx_script) - xmr_swap.a_lock_refund_tx_id, xmr_swap.a_swap_refund_value = ci_from.verifyLockRefundTx( - xmr_swap.a_lock_refund_tx, xmr_swap.a_lock_refund_tx_script, + xmr_swap.a_lock_refund_tx_id, xmr_swap.a_swap_refund_value, lock_refund_vout = ci_from.verifyLockRefundTx( + xmr_swap.a_lock_refund_tx, xmr_swap.a_lock_tx, xmr_swap.a_lock_refund_tx_script, xmr_swap.a_lock_tx_id, xmr_swap.a_lock_tx_vout, xmr_offer.lock_time_1, xmr_swap.a_lock_tx_script, xmr_swap.pkal, xmr_swap.pkaf, xmr_offer.lock_time_2, - bid.amount, xmr_offer.a_fee_rate - ) + bid.amount, xmr_offer.a_fee_rate, xmr_swap.vkbv) ci_from.verifyLockRefundSpendTx( - xmr_swap.a_lock_refund_spend_tx, + xmr_swap.a_lock_refund_spend_tx, xmr_swap.a_lock_refund_tx, xmr_swap.a_lock_refund_tx_id, xmr_swap.a_lock_refund_tx_script, xmr_swap.pkal, - xmr_swap.a_swap_refund_value, xmr_offer.a_fee_rate - ) + lock_refund_vout, xmr_swap.a_swap_refund_value, xmr_offer.a_fee_rate, xmr_swap.vkbv) self.log.info('Checking leader\'s lock refund tx signature') - v = ci_from.verifyTxSig(xmr_swap.a_lock_refund_tx, xmr_swap.al_lock_refund_tx_sig, xmr_swap.pkal, 0, xmr_swap.a_lock_tx_script, bid.amount) + prevout_amount = ci_from.getLockTxSwapOutputValue(bid, xmr_swap) + v = ci_from.verifyTxSig(xmr_swap.a_lock_refund_tx, xmr_swap.al_lock_refund_tx_sig, xmr_swap.pkal, 0, xmr_swap.a_lock_tx_script, prevout_amount) + ensure(v, 'Invalid coin A lock refund tx leader sig') bid.setState(BidStates.BID_RECEIVING_ACC) self.saveBid(bid.bid_id, bid, xmr_swap=xmr_swap) @@ -3985,7 +4026,7 @@ class BasicSwap(BaseApp): except Exception as ex: if self.debug: traceback.print_exc() - self.setBidError(bid.bid_id, bid, str(ex)) + self.setBidError(bid.bid_id, bid, str(ex), xmr_swap=xmr_swap) def watchXmrSwap(self, bid, offer, xmr_swap): self.log.debug('XMR swap in progress, bid %s', bid.bid_id.hex()) @@ -3994,7 +4035,9 @@ class BasicSwap(BaseApp): coin_from = Coins(offer.coin_from) self.setLastHeightChecked(coin_from, xmr_swap.start_chain_a_height) self.addWatchedOutput(coin_from, bid.bid_id, bid.xmr_a_lock_tx.txid.hex(), bid.xmr_a_lock_tx.vout, TxTypes.XMR_SWAP_A_LOCK, SwapTypes.XMR_SWAP) - self.addWatchedOutput(coin_from, bid.bid_id, xmr_swap.a_lock_refund_tx_id.hex(), 0, TxTypes.XMR_SWAP_A_LOCK_REFUND, SwapTypes.XMR_SWAP) + + lock_refund_vout = self.ci(coin_from).getLockRefundTxSwapOutput(xmr_swap) + self.addWatchedOutput(coin_from, bid.bid_id, xmr_swap.a_lock_refund_tx_id.hex(), lock_refund_vout, TxTypes.XMR_SWAP_A_LOCK_REFUND, SwapTypes.XMR_SWAP) bid.in_progress = 1 def sendXmrBidTxnSigsFtoL(self, bid_id, session): @@ -4016,8 +4059,10 @@ class BasicSwap(BaseApp): try: kaf = self.getPathKey(coin_from, coin_to, bid.created_at, xmr_swap.contract_count, 3) - xmr_swap.af_lock_refund_spend_tx_esig = ci_from.signTxOtVES(kaf, xmr_swap.pkasl, xmr_swap.a_lock_refund_spend_tx, 0, xmr_swap.a_lock_refund_tx_script, xmr_swap.a_swap_refund_value) - xmr_swap.af_lock_refund_tx_sig = ci_from.signTx(kaf, xmr_swap.a_lock_refund_tx, 0, xmr_swap.a_lock_tx_script, bid.amount) + prevout_amount = ci_from.getLockRefundTxSwapOutputValue(bid, xmr_swap) + xmr_swap.af_lock_refund_spend_tx_esig = ci_from.signTxOtVES(kaf, xmr_swap.pkasl, xmr_swap.a_lock_refund_spend_tx, 0, xmr_swap.a_lock_refund_tx_script, prevout_amount) + prevout_amount = ci_from.getLockTxSwapOutputValue(bid, xmr_swap) + xmr_swap.af_lock_refund_tx_sig = ci_from.signTx(kaf, xmr_swap.a_lock_refund_tx, 0, xmr_swap.a_lock_tx_script, prevout_amount) self.addLockRefundSigs(xmr_swap, ci_from) @@ -4038,7 +4083,7 @@ class BasicSwap(BaseApp): self.log.info('Sent XMR_BID_TXN_SIGS_FL %s', xmr_swap.coin_a_lock_tx_sigs_l_msg_id.hex()) - a_lock_tx_id = ci_from.getTxHash(xmr_swap.a_lock_tx) + a_lock_tx_id = ci_from.getTxid(xmr_swap.a_lock_tx) a_lock_tx_vout = ci_from.getTxOutputPos(xmr_swap.a_lock_tx, xmr_swap.a_lock_tx_script) self.log.debug('Waiting for lock txn %s to %s chain for bid %s', a_lock_tx_id.hex(), ci_from.coin_name(), bid_id.hex()) bid.xmr_a_lock_tx = SwapTx( @@ -4077,12 +4122,13 @@ class BasicSwap(BaseApp): xmr_swap.a_lock_spend_tx = ci_from.createScriptLockSpendTx( xmr_swap.a_lock_tx, xmr_swap.a_lock_tx_script, xmr_swap.dest_af, - xmr_offer.a_fee_rate) + xmr_offer.a_fee_rate, xmr_swap.vkbv) - xmr_swap.a_lock_spend_tx_id = ci_from.getTxHash(xmr_swap.a_lock_spend_tx) - 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, bid.amount) # self.a_swap_value + xmr_swap.a_lock_spend_tx_id = ci_from.getTxid(xmr_swap.a_lock_spend_tx) + 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) - # Proof leader can sign for kal + # Prove leader can sign for kal xmr_swap.kal_sig = ci_from.signCompact(kal, 'proof key owned for swap') msg_buf = XmrBidLockSpendTxMessage( @@ -4159,8 +4205,7 @@ class BasicSwap(BaseApp): error_msg += ', retry no. {}'.format(num_retries) self.log.error(error_msg) - str_error = str(ex) - if num_retries < 5 and ('not enough unlocked money' in str_error or 'transaction was rejected by daemon' in str_error or self.is_transient_error(ex)): + if num_retries < 5 and (ci_to.is_transient_error(ex) or self.is_transient_error(ex)): delay = random.randrange(self.min_delay_retry, self.max_delay_retry) self.log.info('Retrying sending xmr swap chain B lock tx for bid %s in %d seconds', bid_id.hex(), delay) self.createEventInSession(delay, EventTypes.SEND_XMR_SWAP_LOCK_TX_B, bid_id, session) @@ -4168,7 +4213,7 @@ class BasicSwap(BaseApp): self.setBidError(bid_id, bid, 'publishBLockTx failed: ' + str(ex), save_bid=False) self.saveBidInSession(bid_id, bid, session, xmr_swap, save_in_progress=offer) - self.logBidEvent(bid.bid_id, EventLogTypes.FAILED_TX_B_LOCK_PUBLISH, str_error, session) + self.logBidEvent(bid.bid_id, EventLogTypes.FAILED_TX_B_LOCK_PUBLISH, str(ex), session) return self.log.debug('Submitted lock txn %s to %s chain for bid %s', b_lock_tx_id.hex(), ci_to.coin_name(), bid_id.hex()) @@ -4233,11 +4278,12 @@ class BasicSwap(BaseApp): kaf = self.getPathKey(coin_from, coin_to, bid.created_at, xmr_swap.contract_count, 3) al_lock_spend_sig = ci_from.decryptOtVES(kbsf, xmr_swap.al_lock_spend_tx_esig) - v = ci_from.verifyTxSig(xmr_swap.a_lock_spend_tx, al_lock_spend_sig, xmr_swap.pkal, 0, xmr_swap.a_lock_tx_script, bid.amount) + prevout_amount = ci_from.getLockTxSwapOutputValue(bid, xmr_swap) + v = ci_from.verifyTxSig(xmr_swap.a_lock_spend_tx, al_lock_spend_sig, xmr_swap.pkal, 0, xmr_swap.a_lock_tx_script, prevout_amount) ensure(v, 'Invalid coin A lock tx spend tx leader sig') - af_lock_spend_sig = ci_from.signTx(kaf, xmr_swap.a_lock_spend_tx, 0, xmr_swap.a_lock_tx_script, bid.amount) - v = ci_from.verifyTxSig(xmr_swap.a_lock_spend_tx, af_lock_spend_sig, xmr_swap.pkaf, 0, xmr_swap.a_lock_tx_script, bid.amount) + af_lock_spend_sig = ci_from.signTx(kaf, xmr_swap.a_lock_spend_tx, 0, xmr_swap.a_lock_tx_script, prevout_amount) + 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 = [ @@ -4303,8 +4349,7 @@ class BasicSwap(BaseApp): error_msg += ', retry no. {}'.format(num_retries) self.log.error(error_msg) - str_error = str(ex) - if num_retries < 100 and ('Invalid unlocked_balance' in str_error or self.is_transient_error(ex)): + if num_retries < 100 and (ci_to.is_transient_error(ex) or self.is_transient_error(ex)): delay = random.randrange(self.min_delay_retry, self.max_delay_retry) self.log.info('Retrying sending xmr swap chain B spend tx for bid %s in %d seconds', bid_id.hex(), delay) self.createEventInSession(delay, EventTypes.REDEEM_XMR_SWAP_LOCK_TX_B, bid_id, session) @@ -4312,7 +4357,7 @@ class BasicSwap(BaseApp): self.setBidError(bid_id, bid, 'spendBLockTx failed: ' + str(ex), save_bid=False) self.saveBidInSession(bid_id, bid, session, xmr_swap, save_in_progress=offer) - self.logBidEvent(bid.bid_id, EventLogTypes.FAILED_TX_B_SPEND, str_error, session) + self.logBidEvent(bid.bid_id, EventLogTypes.FAILED_TX_B_SPEND, str(ex), session) return bid.xmr_b_lock_tx.spend_txid = txid @@ -4360,7 +4405,7 @@ class BasicSwap(BaseApp): self.log.error(error_msg) str_error = str(ex) - if num_retries < 100 and ('Invalid unlocked_balance' in str_error or self.is_transient_error(ex)): + if num_retries < 100 and (ci_to.is_transient_error(ex) or self.is_transient_error(ex)): delay = random.randrange(self.min_delay_retry, self.max_delay_retry) self.log.info('Retrying sending xmr swap chain B refund tx for bid %s in %d seconds', bid_id.hex(), delay) self.createEventInSession(delay, EventTypes.RECOVER_XMR_SWAP_LOCK_TX_B, bid_id, session) @@ -4408,7 +4453,8 @@ class BasicSwap(BaseApp): kal = self.getPathKey(coin_from, coin_to, bid.created_at, xmr_swap.contract_count, 3) xmr_swap.af_lock_refund_spend_tx_sig = ci_from.decryptOtVES(kbsl, xmr_swap.af_lock_refund_spend_tx_esig) - al_lock_refund_spend_tx_sig = ci_from.signTx(kal, xmr_swap.a_lock_refund_spend_tx, 0, xmr_swap.a_lock_refund_tx_script, xmr_swap.a_swap_refund_value) + prevout_amount = ci_from.getLockRefundTxSwapOutputValue(bid, xmr_swap) + 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 = [ @@ -4422,7 +4468,7 @@ class BasicSwap(BaseApp): ensure(signed_tx, 'setTxSignature failed') xmr_swap.a_lock_refund_spend_tx = signed_tx - v = ci_from.verifyTxSig(xmr_swap.a_lock_refund_spend_tx, xmr_swap.af_lock_refund_spend_tx_sig, xmr_swap.pkaf, 0, xmr_swap.a_lock_refund_tx_script, xmr_swap.a_swap_refund_value) + v = ci_from.verifyTxSig(xmr_swap.a_lock_refund_spend_tx, xmr_swap.af_lock_refund_spend_tx_sig, xmr_swap.pkaf, 0, xmr_swap.a_lock_refund_tx_script, prevout_amount) ensure(v, 'Invalid signature for lock refund spend txn') self.addLockRefundSigs(xmr_swap, ci_from) @@ -4460,13 +4506,13 @@ 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.getTxHash(xmr_swap.a_lock_spend_tx) + xmr_swap.a_lock_spend_tx_id = ci_from.getTxid(xmr_swap.a_lock_spend_tx) xmr_swap.kal_sig = msg_data.kal_sig ci_from.verifyLockSpendTx( xmr_swap.a_lock_spend_tx, xmr_swap.a_lock_tx, xmr_swap.a_lock_tx_script, - xmr_swap.dest_af, xmr_offer.a_fee_rate) + xmr_swap.dest_af, xmr_offer.a_fee_rate, xmr_swap.vkbv) ci_from.verifyCompact(xmr_swap.pkal, 'proof key owned for swap', xmr_swap.kal_sig) @@ -4532,12 +4578,12 @@ class BasicSwap(BaseApp): ci_from = self.ci(Coins(offer.coin_from)) xmr_swap.al_lock_spend_tx_esig = msg_data.al_lock_spend_tx_esig - try: + prevout_amount = ci_from.getLockTxSwapOutputValue(bid, xmr_swap) v = ci_from.verifyTxOtVES( xmr_swap.a_lock_spend_tx, xmr_swap.al_lock_spend_tx_esig, - xmr_swap.pkal, xmr_swap.pkasf, 0, xmr_swap.a_lock_tx_script, bid.amount) - ensure(v, 'verifyTxOtVES failed') + xmr_swap.pkal, xmr_swap.pkasf, 0, xmr_swap.a_lock_tx_script, prevout_amount) + ensure(v, 'verifyTxOtVES failed for chain a lock tx leader esig') except Exception as ex: if self.debug: traceback.print_exc() @@ -4648,7 +4694,7 @@ class BasicSwap(BaseApp): if now - self._last_checked_watched >= self.check_watched_seconds: for k, c in self.coin_clients.items(): - if k == Coins.PART_ANON: + if k == Coins.PART_ANON or k == Coins.PART_BLIND: continue if len(c['watched_outputs']) > 0: self.checkForSpends(k, c) @@ -5203,11 +5249,11 @@ class BasicSwap(BaseApp): spend_tx = ci.createScriptLockRefundSpendToFTx( xmr_swap.a_lock_refund_tx, xmr_swap.a_lock_refund_tx_script, pkh_dest, - xmr_offer.a_fee_rate - ) + xmr_offer.a_fee_rate, xmr_swap.vkbv) vkaf = self.getPathKey(offer.coin_from, offer.coin_to, bid.created_at, xmr_swap.contract_count, 3) - sig = ci.signTx(vkaf, spend_tx, 0, xmr_swap.a_lock_refund_tx_script, xmr_swap.a_swap_refund_value) + prevout_amount = ci.getLockRefundTxSwapOutputValue(bid, xmr_swap) + sig = ci.signTx(vkaf, spend_tx, 0, xmr_swap.a_lock_refund_tx_script, prevout_amount) witness_stack = [ sig, diff --git a/basicswap/basicswap_util.py b/basicswap/basicswap_util.py index f3265e5..e3aa942 100644 --- a/basicswap/basicswap_util.py +++ b/basicswap/basicswap_util.py @@ -371,7 +371,3 @@ def isActiveBidState(state): if state == BidStates.XMR_SWAP_SCRIPT_TX_REDEEMED: return True return False - - -class TemporaryError(ValueError): - pass diff --git a/basicswap/chainparams.py b/basicswap/chainparams.py index b422fcb..5e3c04b 100644 --- a/basicswap/chainparams.py +++ b/basicswap/chainparams.py @@ -11,6 +11,7 @@ from .util import ( COIN, make_int, format_amount, + TemporaryError, ) XMR_COIN = 10 ** 12 @@ -248,3 +249,21 @@ class CoinInterface: def knownWalletSeed(self): return not self._unknown_wallet_seed + + def chainparams(self): + return chainparams[self.coin_type()] + + def chainparams_network(self): + return chainparams[self.coin_type()][self._network] + + def is_transient_error(self, ex): + if isinstance(ex, TemporaryError): + return True + str_error = str(ex).lower() + if 'not enough unlocked money' in str_error: + return True + if 'transaction was rejected by daemon' in str_error: + return True + if 'invalid unlocked_balance' in str_error: + return True + return False diff --git a/basicswap/contrib/test_framework/messages.py b/basicswap/contrib/test_framework/messages.py index 70e353e..7ae6b11 100755 --- a/basicswap/contrib/test_framework/messages.py +++ b/basicswap/contrib/test_framework/messages.py @@ -340,7 +340,7 @@ class CTxIn: self.nSequence) class CTxOutPart: - __slots__ = ("nVersion", "nValue", "scriptPubKey") + __slots__ = ("nVersion", "nValue", "scriptPubKey", "commitment", "data", "rangeproof") def __init__(self, nValue=0, scriptPubKey=b""): self.nVersion = OUTPUT_TYPE_STANDARD @@ -348,13 +348,37 @@ class CTxOutPart: self.scriptPubKey = scriptPubKey def deserialize(self, f): - self.nValue = struct.unpack(" 73, 'Bad script length') - assert_cond(script_bytes[0] == OP_IF) - assert_cond(script_bytes[1] == OP_2) - assert_cond(script_bytes[2] == 33) + 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] - assert_cond(script_bytes[36] == 33) + ensure_op(script_bytes[36] == 33) pk2 = script_bytes[37: 37 + 33] - assert_cond(script_bytes[70] == OP_2) - assert_cond(script_bytes[71] == OP_CHECKMULTISIG) - assert_cond(script_bytes[72] == OP_ELSE) + 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 - assert_cond(script_len == o + 5 + 33, 'Bad script length') # Fails if script too long - assert_cond(script_bytes[o] == OP_CHECKSEQUENCEVERIFY) + ensure(script_len == o + 5 + 33, 'Bad script length') # Fails if script too long + ensure_op(script_bytes[o] == OP_CHECKSEQUENCEVERIFY) o += 1 - assert_cond(script_bytes[o] == OP_DROP) + ensure_op(script_bytes[o] == OP_DROP) o += 1 - assert_cond(script_bytes[o] == 33) + ensure_op(script_bytes[o] == 33) o += 1 pk3 = script_bytes[o: o + 33] o += 33 - assert_cond(script_bytes[o] == OP_CHECKSIG) + ensure_op(script_bytes[o] == OP_CHECKSIG) o += 1 - assert_cond(script_bytes[o] == OP_ENDIF) + ensure_op(script_bytes[o] == OP_ENDIF) return pk1, pk2, csv_val, pk3 @@ -373,30 +389,28 @@ class BTCInterface(CoinInterface): Kaf_enc, CScriptOp(OP_CHECKSIG), CScriptOp(OP_ENDIF)]) - def createScriptLockRefundTx(self, tx_lock_bytes, script_lock, Kal, Kaf, lock1_value, csv_val, tx_fee_rate): + def createScriptLockRefundTx(self, tx_lock_bytes, script_lock, Kal, Kaf, lock1_value, csv_val, tx_fee_rate, vkbv=None): tx_lock = CTransaction() tx_lock = FromHex(tx_lock, tx_lock_bytes.hex()) output_script = CScript([OP_0, hashlib.sha256(script_lock).digest()]) locked_n = findOutput(tx_lock, output_script) - assert_cond(locked_n is not None, 'Output not found in tx') + ensure(locked_n is not None, 'Output not found in tx') locked_coin = tx_lock.vout[locked_n].nValue tx_lock.rehash() - tx_lock_hash_int = tx_lock.sha256 + tx_lock_id_int = tx_lock.sha256 refund_script = self.genScriptLockRefundTxScript(Kal, Kaf, csv_val) tx = CTransaction() tx.nVersion = self.txVersion() - tx.vin.append(CTxIn(COutPoint(tx_lock_hash_int, locked_n), nSequence=lock1_value)) + tx.vin.append(CTxIn(COutPoint(tx_lock_id_int, locked_n), nSequence=lock1_value)) tx.vout.append(self.txoType()(locked_coin, CScript([OP_0, hashlib.sha256(refund_script).digest()]))) - witness_bytes = len(script_lock) - witness_bytes += 74 * 2 # 2 signatures (72 + 1 byte sighashtype + 1 byte size) - Use maximum txn size for estimate - witness_bytes += 2 # 2 empty witness stack values - witness_bytes += getCompactSizeLen(witness_bytes) + dummy_witness_stack = self.getScriptLockTxDummyWitness(script_lock) + witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack) vsize = self.getTxVSize(tx, add_witness_bytes=witness_bytes) - pay_fee = int(tx_fee_rate * vsize / 1000) + pay_fee = int(tx_fee_rate * vsize // 1000) tx.vout[0].nValue = locked_coin - pay_fee tx.rehash() @@ -405,7 +419,7 @@ class BTCInterface(CoinInterface): return tx.serialize(), refund_script, tx.vout[0].nValue - def createScriptLockRefundSpendTx(self, tx_lock_refund_bytes, script_lock_refund, pkh_refund_to, tx_fee_rate): + def createScriptLockRefundSpendTx(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 @@ -414,7 +428,7 @@ class BTCInterface(CoinInterface): output_script = CScript([OP_0, hashlib.sha256(script_lock_refund).digest()]) locked_n = findOutput(tx_lock_refund, output_script) - assert_cond(locked_n is not None, 'Output not found in tx') + ensure(locked_n is not None, 'Output not found in tx') locked_coin = tx_lock_refund.vout[locked_n].nValue tx_lock_refund.rehash() @@ -426,12 +440,10 @@ class BTCInterface(CoinInterface): tx.vout.append(self.txoType()(locked_coin, self.getScriptForPubkeyHash(pkh_refund_to))) - witness_bytes = len(script_lock_refund) - witness_bytes += 74 * 2 # 2 signatures (72 + 1 byte sighashtype + 1 byte size) - Use maximum txn size for estimate - witness_bytes += 4 # 1 empty, 1 true witness stack values - witness_bytes += getCompactSizeLen(witness_bytes) + dummy_witness_stack = self.getScriptLockRefundSpendTxDummyWitness(script_lock_refund) + witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack) vsize = self.getTxVSize(tx, add_witness_bytes=witness_bytes) - pay_fee = int(tx_fee_rate * vsize / 1000) + pay_fee = int(tx_fee_rate * vsize // 1000) tx.vout[0].nValue = locked_coin - pay_fee tx.rehash() @@ -440,14 +452,15 @@ class BTCInterface(CoinInterface): return tx.serialize() - def createScriptLockRefundSpendToFTx(self, tx_lock_refund_bytes, script_lock_refund, pkh_dest, tx_fee_rate): + def createScriptLockRefundSpendToFTx(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 = CScript([OP_0, hashlib.sha256(script_lock_refund).digest()]) locked_n = findOutput(tx_lock_refund, output_script) - assert_cond(locked_n is not None, 'Output not found in tx') + 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) @@ -461,12 +474,10 @@ class BTCInterface(CoinInterface): tx.vout.append(self.txoType()(locked_coin, self.getScriptForPubkeyHash(pkh_dest))) - witness_bytes = len(script_lock_refund) - witness_bytes += 74 # 2 signatures (72 + 1 byte sighashtype + 1 byte size) - Use maximum txn size for estimate - witness_bytes += 1 # 1 empty stack value - witness_bytes += getCompactSizeLen(witness_bytes) + dummy_witness_stack = self.getScriptLockRefundSwipeTxDummyWitness(script_lock_refund) + witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack) vsize = self.getTxVSize(tx, add_witness_bytes=witness_bytes) - pay_fee = int(tx_fee_rate * vsize / 1000) + pay_fee = int(tx_fee_rate * vsize // 1000) tx.vout[0].nValue = locked_coin - pay_fee tx.rehash() @@ -475,29 +486,26 @@ class BTCInterface(CoinInterface): return tx.serialize() - def createScriptLockSpendTx(self, tx_lock_bytes, script_lock, pkh_dest, tx_fee_rate): + def createScriptLockSpendTx(self, tx_lock_bytes, script_lock, pkh_dest, tx_fee_rate, vkbv=None): tx_lock = self.loadTx(tx_lock_bytes) output_script = CScript([OP_0, hashlib.sha256(script_lock).digest()]) locked_n = findOutput(tx_lock, output_script) - assert_cond(locked_n is not None, 'Output not found in tx') + ensure(locked_n is not None, 'Output not found in tx') locked_coin = tx_lock.vout[locked_n].nValue tx_lock.rehash() - tx_lock_hash_int = tx_lock.sha256 + tx_lock_id_int = tx_lock.sha256 tx = CTransaction() tx.nVersion = self.txVersion() - tx.vin.append(CTxIn(COutPoint(tx_lock_hash_int, locked_n))) + tx.vin.append(CTxIn(COutPoint(tx_lock_id_int, locked_n))) tx.vout.append(self.txoType()(locked_coin, self.getScriptForPubkeyHash(pkh_dest))) - witness_bytes = len(script_lock) - witness_bytes += 33 # sv, size - witness_bytes += 74 * 2 # 2 signatures (72 + 1 byte sighashtype + 1 byte size) - Use maximum txn size for estimate - witness_bytes += 4 # 1 empty, 1 true witness stack values - witness_bytes += getCompactSizeLen(witness_bytes) + dummy_witness_stack = self.getScriptLockTxDummyWitness(script_lock) + witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack) vsize = self.getTxVSize(tx, add_witness_bytes=witness_bytes) - pay_fee = int(tx_fee_rate * vsize / 1000) + pay_fee = int(tx_fee_rate * vsize // 1000) tx.vout[0].nValue = locked_coin - pay_fee tx.rehash() @@ -510,7 +518,7 @@ class BTCInterface(CoinInterface): swap_value, Kal, Kaf, feerate, - check_lock_tx_inputs): + check_lock_tx_inputs, vkbv=None): # Verify: # @@ -519,26 +527,28 @@ class BTCInterface(CoinInterface): # Check fee is reasonable tx = self.loadTx(tx_bytes) - tx_hash = self.getTxHash(tx) - self._log.info('Verifying lock tx: {}.'.format(b2h(tx_hash))) + txid = self.getTxid(tx) + self._log.info('Verifying lock tx: {}.'.format(b2h(txid))) - assert_cond(tx.nVersion == self.txVersion(), 'Bad version') - assert_cond(tx.nLockTime == 0, 'Bad nLockTime') + ensure(tx.nVersion == self.txVersion(), 'Bad version') + ensure(tx.nLockTime == 0, 'Bad nLockTime') # TODO match txns created by cores script_pk = CScript([OP_0, hashlib.sha256(script_out).digest()]) locked_n = findOutput(tx, script_pk) - assert_cond(locked_n is not None, 'Output not found in tx') + ensure(locked_n is not None, 'Output not found in tx') locked_coin = tx.vout[locked_n].nValue - assert_cond(locked_coin == swap_value, 'Bad locked value') + # Check value + ensure(locked_coin == swap_value, 'Bad locked value') - # Check script and values + # Check script A, B = self.extractScriptLockScriptValues(script_out) - assert_cond(A == Kal, 'Bad script pubkey') - assert_cond(B == Kaf, 'Bad script pubkey') + ensure(A == Kal, 'Bad script pubkey') + ensure(B == Kaf, 'Bad script pubkey') if check_lock_tx_inputs: - # Check that inputs are unspent and verify fee rate + # TODO: Check that inputs are unspent + # Verify fee rate inputs_value = 0 add_bytes = 0 add_witness_bytes = getCompactSizeLen(len(tx.vin)) @@ -562,7 +572,7 @@ class BTCInterface(CoinInterface): assert(fee_paid > 0) vsize = self.getTxVSize(tx, add_bytes, add_witness_bytes) - fee_rate_paid = fee_paid * 1000 / vsize + fee_rate_paid = fee_paid * 1000 // vsize self._log.info('tx amount, vsize, feerate: %ld, %ld, %ld', locked_coin, vsize, fee_rate_paid) @@ -570,97 +580,93 @@ class BTCInterface(CoinInterface): self._log.warning('feerate paid doesn\'t match expected: %ld, %ld', fee_rate_paid, feerate) # TODO: Display warning to user - return tx_hash, locked_n + return txid, locked_n - def verifyLockRefundTx(self, tx_bytes, script_out, + def verifyLockRefundTx(self, tx_bytes, lock_tx_bytes, script_out, prevout_id, prevout_n, prevout_seq, prevout_script, - Kal, Kaf, csv_val_expect, swap_value, feerate): + 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) - tx_hash = self.getTxHash(tx) - self._log.info('Verifying lock refund tx: {}.'.format(b2h(tx_hash))) + txid = self.getTxid(tx) + self._log.info('Verifying lock refund tx: {}.'.format(b2h(txid))) - assert_cond(tx.nVersion == self.txVersion(), 'Bad version') - assert_cond(tx.nLockTime == 0, 'nLockTime not 0') - assert_cond(len(tx.vin) == 1, 'tx doesn\'t have one input') + ensure(tx.nVersion == self.txVersion(), 'Bad version') + ensure(tx.nLockTime == 0, 'nLockTime not 0') + ensure(len(tx.vin) == 1, 'tx doesn\'t have one input') - assert_cond(tx.vin[0].nSequence == prevout_seq, 'Bad input nSequence') - assert_cond(len(tx.vin[0].scriptSig) == 0, 'Input scriptsig not empty') - assert_cond(tx.vin[0].prevout.hash == b2i(prevout_id) and tx.vin[0].prevout.n == prevout_n, 'Input prevout mismatch') + ensure(tx.vin[0].nSequence == prevout_seq, 'Bad input nSequence') + ensure(len(tx.vin[0].scriptSig) == 0, 'Input scriptsig not empty') + ensure(tx.vin[0].prevout.hash == b2i(prevout_id) and tx.vin[0].prevout.n == prevout_n, 'Input prevout mismatch') - assert_cond(len(tx.vout) == 1, 'tx doesn\'t have one output') + ensure(len(tx.vout) == 1, 'tx doesn\'t have one output') script_pk = CScript([OP_0, hashlib.sha256(script_out).digest()]) locked_n = findOutput(tx, script_pk) - assert_cond(locked_n is not None, 'Output not found in tx') + ensure(locked_n is not None, 'Output not found in tx') locked_coin = tx.vout[locked_n].nValue # Check script and values A, B, csv_val, C = self.extractScriptLockRefundScriptValues(script_out) - assert_cond(A == Kal, 'Bad script pubkey') - assert_cond(B == Kaf, 'Bad script pubkey') - assert_cond(csv_val == csv_val_expect, 'Bad script csv value') - assert_cond(C == Kaf, 'Bad script pubkey') + 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) - witness_bytes = len(prevout_script) - witness_bytes += 74 * 2 # 2 signatures (72 + 1 byte sighashtype + 1 byte size) - Use maximum txn size for estimate - witness_bytes += 2 # 2 empty witness stack values - witness_bytes += getCompactSizeLen(witness_bytes) + dummy_witness_stack = self.getScriptLockTxDummyWitness(prevout_script) + witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack) vsize = self.getTxVSize(tx, add_witness_bytes=witness_bytes) - fee_rate_paid = fee_paid * 1000 / vsize + fee_rate_paid = fee_paid * 1000 // vsize self._log.info('tx amount, vsize, feerate: %ld, %ld, %ld', locked_coin, vsize, fee_rate_paid) if not self.compareFeeRates(fee_rate_paid, feerate): raise ValueError('Bad fee rate, expected: {}'.format(feerate)) - return tx_hash, locked_coin + return txid, locked_coin, locked_n - def verifyLockRefundSpendTx(self, tx_bytes, + def verifyLockRefundSpendTx(self, tx_bytes, lock_refund_tx_bytes, lock_refund_tx_id, prevout_script, Kal, - prevout_value, feerate): + 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) - tx_hash = self.getTxHash(tx) - self._log.info('Verifying lock refund spend tx: {}.'.format(b2h(tx_hash))) + txid = self.getTxid(tx) + self._log.info('Verifying lock refund spend tx: {}.'.format(b2h(txid))) - assert_cond(tx.nVersion == self.txVersion(), 'Bad version') - assert_cond(tx.nLockTime == 0, 'nLockTime not 0') - assert_cond(len(tx.vin) == 1, 'tx doesn\'t have one input') + ensure(tx.nVersion == self.txVersion(), 'Bad version') + ensure(tx.nLockTime == 0, 'nLockTime not 0') + ensure(len(tx.vin) == 1, 'tx doesn\'t have one input') - assert_cond(tx.vin[0].nSequence == 0, 'Bad input nSequence') - assert_cond(len(tx.vin[0].scriptSig) == 0, 'Input scriptsig not empty') - assert_cond(tx.vin[0].prevout.hash == b2i(lock_refund_tx_id) and tx.vin[0].prevout.n == 0, 'Input prevout mismatch') + ensure(tx.vin[0].nSequence == 0, 'Bad input nSequence') + ensure(len(tx.vin[0].scriptSig) == 0, 'Input scriptsig not empty') + ensure(tx.vin[0].prevout.hash == b2i(lock_refund_tx_id) and tx.vin[0].prevout.n == 0, 'Input prevout mismatch') - assert_cond(len(tx.vout) == 1, 'tx doesn\'t have one output') + 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) - assert_cond(locked_n is not None, 'Output not found in lock refund spend tx') + ensure(locked_n is not None, 'Output not found in lock refund spend tx') ''' tx_value = tx.vout[0].nValue fee_paid = prevout_value - tx_value assert(fee_paid > 0) - witness_bytes = len(prevout_script) - witness_bytes += 74 * 2 # 2 signatures (72 + 1 byte sighashtype + 1 byte size) - Use maximum txn size for estimate - witness_bytes += 4 # 1 empty, 1 true witness stack values - witness_bytes += getCompactSizeLen(witness_bytes) + dummy_witness_stack = self.getScriptLockRefundSpendTxDummyWitness(prevout_script) + witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack) vsize = self.getTxVSize(tx, add_witness_bytes=witness_bytes) - fee_rate_paid = fee_paid * 1000 / vsize + fee_rate_paid = fee_paid * 1000 // vsize self._log.info('tx amount, vsize, feerate: %ld, %ld, %ld', tx_value, vsize, fee_rate_paid) @@ -671,45 +677,43 @@ class BTCInterface(CoinInterface): def verifyLockSpendTx(self, tx_bytes, lock_tx_bytes, lock_tx_script, - a_pkhash_f, feerate): + 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) - tx_hash = self.getTxHash(tx) - self._log.info('Verifying lock spend tx: {}.'.format(b2h(tx_hash))) + txid = self.getTxid(tx) + self._log.info('Verifying lock spend tx: {}.'.format(b2h(txid))) - assert_cond(tx.nVersion == self.txVersion(), 'Bad version') - assert_cond(tx.nLockTime == 0, 'nLockTime not 0') - assert_cond(len(tx.vin) == 1, 'tx doesn\'t have one input') + ensure(tx.nVersion == self.txVersion(), 'Bad version') + ensure(tx.nLockTime == 0, 'nLockTime not 0') + ensure(len(tx.vin) == 1, 'tx doesn\'t have one input') lock_tx = self.loadTx(lock_tx_bytes) - lock_tx_id = self.getTxHash(lock_tx) + lock_tx_id = self.getTxid(lock_tx) output_script = CScript([OP_0, hashlib.sha256(lock_tx_script).digest()]) locked_n = findOutput(lock_tx, output_script) - assert_cond(locked_n is not None, 'Output not found in tx') + ensure(locked_n is not None, 'Output not found in tx') locked_coin = lock_tx.vout[locked_n].nValue - assert_cond(tx.vin[0].nSequence == 0, 'Bad input nSequence') - assert_cond(len(tx.vin[0].scriptSig) == 0, 'Input scriptsig not empty') - assert_cond(tx.vin[0].prevout.hash == b2i(lock_tx_id) and tx.vin[0].prevout.n == locked_n, 'Input prevout mismatch') + ensure(tx.vin[0].nSequence == 0, 'Bad input nSequence') + ensure(len(tx.vin[0].scriptSig) == 0, 'Input scriptsig not empty') + ensure(tx.vin[0].prevout.hash == b2i(lock_tx_id) and tx.vin[0].prevout.n == locked_n, 'Input prevout mismatch') - assert_cond(len(tx.vout) == 1, 'tx doesn\'t have one output') + ensure(len(tx.vout) == 1, 'tx doesn\'t have one output') p2wpkh = self.getScriptForPubkeyHash(a_pkhash_f) - assert_cond(tx.vout[0].scriptPubKey == p2wpkh, 'Bad output destination') + ensure(tx.vout[0].scriptPubKey == 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].nValue assert(fee_paid > 0) - witness_bytes = len(lock_tx_script) - witness_bytes += 33 # sv, size - witness_bytes += 74 * 2 # 2 signatures (72 + 1 byte sighashtype + 1 byte size) - Use maximum txn size for estimate - witness_bytes += 4 # 1 empty, 1 true witness stack values - witness_bytes += getCompactSizeLen(witness_bytes) + dummy_witness_stack = self.getScriptLockTxDummyWitness(lock_tx_script) + witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack) vsize = self.getTxVSize(tx, add_witness_bytes=witness_bytes) - fee_rate_paid = fee_paid * 1000 / vsize + fee_rate_paid = fee_paid * 1000 // vsize self._log.info('tx amount, vsize, feerate: %ld, %ld, %ld', tx.vout[0].nValue, vsize, fee_rate_paid) @@ -718,30 +722,30 @@ class BTCInterface(CoinInterface): return True - def signTx(self, key_bytes, tx_bytes, prevout_n, prevout_script, prevout_value): + def signTx(self, key_bytes, tx_bytes, input_n, prevout_script, prevout_value): tx = self.loadTx(tx_bytes) - sig_hash = SegwitV0SignatureHash(prevout_script, tx, prevout_n, SIGHASH_ALL, prevout_value) + sig_hash = SegwitV0SignatureHash(prevout_script, tx, input_n, SIGHASH_ALL, prevout_value) eck = PrivateKey(key_bytes) return eck.sign(sig_hash, hasher=None) + bytes((SIGHASH_ALL,)) - def signTxOtVES(self, key_sign, pubkey_encrypt, tx_bytes, prevout_n, prevout_script, prevout_value): + def signTxOtVES(self, key_sign, pubkey_encrypt, tx_bytes, input_n, prevout_script, prevout_value): tx = self.loadTx(tx_bytes) - sig_hash = SegwitV0SignatureHash(prevout_script, tx, prevout_n, SIGHASH_ALL, prevout_value) + sig_hash = SegwitV0SignatureHash(prevout_script, tx, input_n, SIGHASH_ALL, prevout_value) return ecdsaotves_enc_sign(key_sign, pubkey_encrypt, sig_hash) - def verifyTxOtVES(self, tx_bytes, ct, Ks, Ke, prevout_n, prevout_script, prevout_value): + def verifyTxOtVES(self, tx_bytes, ct, Ks, Ke, input_n, prevout_script, prevout_value): tx = self.loadTx(tx_bytes) - sig_hash = SegwitV0SignatureHash(prevout_script, tx, prevout_n, SIGHASH_ALL, prevout_value) + sig_hash = SegwitV0SignatureHash(prevout_script, tx, input_n, SIGHASH_ALL, prevout_value) return ecdsaotves_enc_verify(Ks, Ke, sig_hash, ct) def decryptOtVES(self, k, esig): return ecdsaotves_dec_sig(k, esig) + bytes((SIGHASH_ALL,)) - def verifyTxSig(self, tx_bytes, sig, K, prevout_n, prevout_script, prevout_value): + def verifyTxSig(self, tx_bytes, sig, K, input_n, prevout_script, prevout_value): tx = self.loadTx(tx_bytes) - sig_hash = SegwitV0SignatureHash(prevout_script, tx, prevout_n, SIGHASH_ALL, prevout_value) + sig_hash = SegwitV0SignatureHash(prevout_script, tx, input_n, SIGHASH_ALL, prevout_value) pubkey = PublicKey(K) return pubkey.verify(sig[: -1], sig_hash, hasher=None) # Pop the hashtype byte @@ -751,7 +755,7 @@ class BTCInterface(CoinInterface): return pubkey.verify(sig, signed_hash, hasher=None) def fundTx(self, tx, feerate): - feerate_str = format_amount(feerate, self.exp()) + feerate_str = self.format_amount(feerate) # TODO: unlock unspents if bid cancelled options = { 'lockUnspents': True, @@ -784,7 +788,9 @@ class BTCInterface(CoinInterface): tx.deserialize(BytesIO(tx_bytes)) return tx - def getTxHash(self, tx): + def getTxid(self, tx): + if isinstance(tx, str): + tx = bytes.fromhex(tx) if isinstance(tx, bytes): tx = self.loadTx(tx) tx.rehash() @@ -829,6 +835,11 @@ class BTCInterface(CoinInterface): tx.wit.vtxinwit[0].scriptWitness.stack = stack return tx.serialize() + def stripTxSignature(self, tx_bytes): + tx = self.loadTx(tx_bytes) + tx.wit.vtxinwit.clear() + return tx.serialize() + def extractLeaderSig(self, tx_bytes): tx = self.loadTx(tx_bytes) return tx.wit.vtxinwit[0].scriptWitness.stack[1] @@ -851,7 +862,7 @@ class BTCInterface(CoinInterface): b_lock_tx = self.createBLockTx(Kbs, output_amount) b_lock_tx = self.fundTx(b_lock_tx, feerate) - b_lock_tx_id = self.getTxHash(b_lock_tx) + b_lock_tx_id = self.getTxid(b_lock_tx) b_lock_tx = self.signTxWithWallet(b_lock_tx) return self.publishTx(b_lock_tx) @@ -881,7 +892,6 @@ class BTCInterface(CoinInterface): return None def waitForLockTxB(self, kbv, Kbs, cb_swap_value, cb_block_confirmed): - raw_dest = self.getPkDest(Kbs) for i in range(20): @@ -899,9 +909,36 @@ class BTCInterface(CoinInterface): return False def spendBLockTx(self, chain_b_lock_txid, address_to, kbv, kbs, cb_swap_value, b_fee, restore_height): - print('TODO: spendBLockTx') + raise ValueError('TODO') - def getOutput(self, txid, dest_script, expect_value): + def getLockTxHeight(self, txid, dest_script, bid_amount, xmr_swap): + rv = None + p2wsh_addr = self.encode_p2wsh(dest_script) + + addr_info = self.rpc_callback('getaddressinfo', [p2wsh_addr]) + if not addr_info['iswatchonly']: + ro = self.rpc_callback('importaddress', [p2wsh_addr, 'bid', False]) + self._log.info('Imported watch-only addr: {}'.format(p2wsh_addr)) + self._log.info('Rescanning chain from height: {}'.format(xmr_swap.start_chain_a_height)) + self.rpc_callback('rescanblockchain', [xmr_swap.start_chain_a_height]) + + try: + tx = self.rpc_callback('gettransaction', [txid.hex()]) + + block_height = 0 + if 'blockhash' in tx: + block_header = self.rpc_callback('getblockheader', [tx['blockhash']]) + block_height = block_header['height'] + + rv = { + 'depth': 0 if 'confirmations' not in tx else tx['confirmations'], + 'height': block_height} + except Exception as e: + pass + + return rv + + def getOutput(self, txid, dest_script, expect_value, xmr_swap=None): # TODO: Use getrawtransaction if txindex is active utxos = self.rpc_callback('scantxoutset', ['start', ['raw({})'.format(dest_script.hex())]]) if 'height' in utxos: # chain_height not returned by v18 codebase @@ -942,7 +979,7 @@ class BTCInterface(CoinInterface): def verifyMessage(self, address, message, signature, message_magic=None) -> bool: if message_magic is None: - message_magic = chainparams[self.coin_type()]['message_magic'] + message_magic = self.chainparams_network()['message_magic'] message_bytes = SerialiseNumCompact(len(message_magic)) + bytes(message_magic, 'utf-8') + SerialiseNumCompact(len(message)) + bytes(message, 'utf-8') message_hash = hashlib.sha256(hashlib.sha256(message_bytes).digest()).digest() @@ -961,7 +998,51 @@ class BTCInterface(CoinInterface): return True if address_hash == pubkey_hash else False def showLockTransfers(self, Kbv, Kbs): - return 'Unimplemented' + 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): + return [ + b''.hex(), + bytes(72).hex(), + bytes(72).hex(), + bytes(len(script)).hex() + ] + + def getScriptLockRefundSpendTxDummyWitness(self, script): + return [ + b''.hex(), + bytes(72).hex(), + bytes(72).hex(), + bytes((1,)).hex(), + bytes(len(script)).hex() + ] + + def getScriptLockRefundSwipeTxDummyWitness(self, script): + return [ + bytes(72).hex(), + b''.hex(), + bytes(len(script)).hex() + ] + + def getWitnessStackSerialisedLength(self, witness_stack): + length = getCompactSizeLen(len(witness_stack)) + for e in witness_stack: + length += getWitnessElementLen(len(e) // 2) # hex -> bytes + + # See core SerializeTransaction + length += 32 + 4 + 1 + 4 # vinDummy + length += 1 # flags + return length def testBTCInterface(): diff --git a/basicswap/interface_part.py b/basicswap/interface_part.py index 5272e40..2ab0fea 100644 --- a/basicswap/interface_part.py +++ b/basicswap/interface_part.py @@ -5,6 +5,8 @@ # Distributed under the MIT software license, see the accompanying # file LICENSE or http://www.opensource.org/licenses/mit-license.php. +import hashlib +import basicswap.contrib.segwit_addr as segwit_addr from enum import IntEnum from .contrib.test_framework.messages import ( @@ -12,16 +14,20 @@ from .contrib.test_framework.messages import ( ) from .contrib.test_framework.script import ( CScript, + OP_0, OP_DUP, OP_HASH160, OP_EQUALVERIFY, OP_CHECKSIG ) +from .ecc_util import i2b from .util import ( - encodeStealthAddress, toWIF, ensure, - make_int) -from .basicswap_util import ( - TemporaryError) + make_int, + getP2WSH, + TemporaryError, + getCompactSizeLen, + encodeStealthAddress, + getWitnessElementLen) from .chainparams import Coins, chainparams from .interface_btc import BTCInterface @@ -98,12 +104,504 @@ class PARTInterface(BTCInterface): return encodeStealthAddress(prefix_byte, scan_pubkey, spend_pubkey) + def getWitnessStackSerialisedLength(self, witness_stack): + length = getCompactSizeLen(len(witness_stack)) + for e in witness_stack: + length += getWitnessElementLen(len(e) // 2) # hex -> bytes + return length + class PARTInterfaceBlind(PARTInterface): @staticmethod def balance_type(): return BalanceTypes.BLIND + def encodeSegwitP2WSH(self, p2wsh): + return segwit_addr.encode(self.chainparams_network()['hrp'], 0, p2wsh[2:]) + + def getScriptLockTxNonce(self, data): + return hashlib.sha256(data + bytes('locktx', 'utf-8')).digest() + + def getScriptLockRefundTxNonce(self, data): + return hashlib.sha256(data + bytes('lockrefundtx', 'utf-8')).digest() + + def findOutputByNonce(self, tx_obj, nonce): + blinded_info = None + output_n = None + for txo in tx_obj['vout']: + if txo['type'] != 'blind': + continue + try: + blinded_info = self.rpc_callback('rewindrangeproof', [txo['rangeproof'], txo['valueCommitment'], nonce.hex()]) + output_n = txo['n'] + + self.rpc_callback('rewindrangeproof', [txo['rangeproof'], txo['valueCommitment'], nonce.hex()]) + break + except Exception as e: + self._log.debug('Searching for locked output: {}'.format(str(e))) + continue + # Should not be possible for commitment not to match + v = self.rpc_callback('verifycommitment', [txo['valueCommitment'], blinded_info['blind'], blinded_info['amount']]) + ensure(v['result'] is True, 'verifycommitment failed') + return output_n, blinded_info + + def createScriptLockTx(self, value, Kal, Kaf, vkbv): + script = self.genScriptLockTxScript(Kal, Kaf) + + # Nonce is derived from vkbv, ephemeral_key isn't used + ephemeral_key = i2b(self.getNewSecretKey()) + ephemeral_pubkey = self.getPubkey(ephemeral_key) + assert(len(ephemeral_pubkey) == 33) + nonce = self.getScriptLockTxNonce(vkbv) + p2wsh_addr = self.encodeSegwitP2WSH(getP2WSH(script)) + inputs = [] + outputs = [{'type': 'blind', 'amount': self.format_amount(value), 'address': p2wsh_addr, 'nonce': nonce.hex(), 'data': ephemeral_pubkey.hex()}] + params = [inputs, outputs] + rv = self.rpc_callback('createrawparttransaction', params) + + tx_bytes = bytes.fromhex(rv['hex']) + return tx_bytes, script + + def fundScriptLockTx(self, tx_bytes, feerate, vkbv): + feerate_str = self.format_amount(feerate) + # TODO: unlock unspents if bid cancelled + + tx_hex = tx_bytes.hex() + nonce = self.getScriptLockTxNonce(vkbv) + + tx_obj = self.rpc_callback('decoderawtransaction', [tx_hex]) + + assert(len(tx_obj['vout']) == 1) + txo = tx_obj['vout'][0] + blinded_info = self.rpc_callback('rewindrangeproof', [txo['rangeproof'], txo['valueCommitment'], nonce.hex()]) + + outputs_info = {0: {'value': blinded_info['amount'], 'blind': blinded_info['blind'], 'nonce': nonce.hex()}} + + options = { + 'lockUnspents': True, + 'feeRate': feerate_str, + } + rv = self.rpc_callback('fundrawtransactionfrom', ['blind', tx_hex, {}, outputs_info, options]) + return bytes.fromhex(rv['hex']) + + def createScriptLockRefundTx(self, tx_lock_bytes, script_lock, Kal, Kaf, lock1_value, csv_val, tx_fee_rate, vkbv): + lock_tx_obj = self.rpc_callback('decoderawtransaction', [tx_lock_bytes.hex()]) + assert(self.getTxid(tx_lock_bytes).hex() == lock_tx_obj['txid']) + # Nonce is derived from vkbv, ephemeral_key isn't used + ephemeral_key = i2b(self.getNewSecretKey()) + ephemeral_pubkey = self.getPubkey(ephemeral_key) + assert(len(ephemeral_pubkey) == 33) + nonce = self.getScriptLockTxNonce(vkbv) + output_nonce = self.getScriptLockRefundTxNonce(vkbv) + + # Find the output of the lock tx to spend + spend_n, input_blinded_info = self.findOutputByNonce(lock_tx_obj, nonce) + ensure(spend_n is not None, 'Output not found in tx') + + locked_coin = input_blinded_info['amount'] + tx_lock_id = lock_tx_obj['txid'] + refund_script = self.genScriptLockRefundTxScript(Kal, Kaf, csv_val) + p2wsh_addr = self.encodeSegwitP2WSH(getP2WSH(refund_script)) + + inputs = [{'txid': tx_lock_id, 'vout': spend_n, 'sequence': lock1_value, 'blindingfactor': input_blinded_info['blind']}] + outputs = [{'type': 'blind', 'amount': locked_coin, 'address': p2wsh_addr, 'nonce': output_nonce.hex(), 'data': ephemeral_pubkey.hex()}] + params = [inputs, outputs] + rv = self.rpc_callback('createrawparttransaction', params) + lock_refund_tx_hex = rv['hex'] + + # Set dummy witness data for fee estimation + dummy_witness_stack = self.getScriptLockTxDummyWitness(script_lock) + + # Use a junk change pubkey to avoid adding unused keys to the wallet + zero_change_key = i2b(self.getNewSecretKey()) + zero_change_pubkey = self.getPubkey(zero_change_key) + inputs_info = {'0': {'value': input_blinded_info['amount'], 'blind': input_blinded_info['blind'], 'witnessstack': dummy_witness_stack}} + outputs_info = rv['amounts'] + options = { + 'changepubkey': zero_change_pubkey.hex(), + 'feeRate': self.format_amount(tx_fee_rate), + 'subtractFeeFromOutputs': [0, ] + } + rv = self.rpc_callback('fundrawtransactionfrom', ['blind', lock_refund_tx_hex, inputs_info, outputs_info, options]) + lock_refund_tx_hex = rv['hex'] + + for vout, txo in rv['output_amounts'].items(): + if txo['value'] > 0: + refunded_value = txo['value'] + + return bytes.fromhex(lock_refund_tx_hex), refund_script, refunded_value + + def createScriptLockRefundSpendTx(self, tx_lock_refund_bytes, script_lock_refund, pkh_refund_to, tx_fee_rate, vkbv): + # 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 + + lock_refund_tx_obj = self.rpc_callback('decoderawtransaction', [tx_lock_refund_bytes.hex()]) + # Nonce is derived from vkbv + nonce = self.getScriptLockRefundTxNonce(vkbv) + + # Find the output of the lock refund tx to spend + spend_n, input_blinded_info = self.findOutputByNonce(lock_refund_tx_obj, nonce) + ensure(spend_n is not None, 'Output not found in tx') + + tx_lock_refund_id = lock_refund_tx_obj['txid'] + addr_out = self.pkh_to_address(pkh_refund_to) + addr_info = self.rpc_callback('getaddressinfo', [addr_out]) + output_pubkey_hex = addr_info['pubkey'] + + # 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 + + inputs = [{'txid': tx_lock_refund_id, 'vout': spend_n, 'sequence': 0, 'blindingfactor': input_blinded_info['blind']}] + outputs = [{'type': 'blind', 'amount': input_blinded_info['amount'], 'address': addr_out, 'pubkey': output_pubkey_hex}] + params = [inputs, outputs] + rv = self.rpc_callback('createrawparttransaction', params) + lock_refund_spend_tx_hex = rv['hex'] + + # Set dummy witness data for fee estimation + dummy_witness_stack = self.getScriptLockRefundSpendTxDummyWitness(script_lock_refund) + + # Use a junk change pubkey to avoid adding unused keys to the wallet + zero_change_key = i2b(self.getNewSecretKey()) + zero_change_pubkey = self.getPubkey(zero_change_key) + inputs_info = {'0': {'value': input_blinded_info['amount'], 'blind': input_blinded_info['blind'], 'witnessstack': dummy_witness_stack}} + outputs_info = rv['amounts'] + options = { + 'changepubkey': zero_change_pubkey.hex(), + 'feeRate': self.format_amount(tx_fee_rate), + 'subtractFeeFromOutputs': [0, ] + } + + rv = self.rpc_callback('fundrawtransactionfrom', ['blind', lock_refund_spend_tx_hex, inputs_info, outputs_info, options]) + lock_refund_spend_tx_hex = rv['hex'] + + return bytes.fromhex(lock_refund_spend_tx_hex) + + def verifyLockTx(self, tx_bytes, script_out, + swap_value, + Kal, Kaf, + feerate, + check_lock_tx_inputs, vkbv): + lock_tx_obj = self.rpc_callback('decoderawtransaction', [tx_bytes.hex()]) + lock_txid_hex = lock_tx_obj['txid'] + self._log.info('Verifying lock tx: {}.'.format(lock_txid_hex)) + + ensure(lock_tx_obj['version'] == self.txVersion(), 'Bad version') + ensure(lock_tx_obj['locktime'] == 0, 'Bad nLockTime') + + # Find the output of the lock tx to verify + nonce = self.getScriptLockTxNonce(vkbv) + lock_output_n, blinded_info = self.findOutputByNonce(lock_tx_obj, nonce) + ensure(lock_output_n is not None, 'Output not found in tx') + + # Check value + locked_txo_value = make_int(blinded_info['amount']) + ensure(locked_txo_value == swap_value, 'Bad locked value') + + # Check script + 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) + ensure(A == Kal, 'Bad script leader pubkey') + ensure(B == Kaf, 'Bad script follower pubkey') + + # TODO: Check that inputs are unspent, rangeproofs and commitments sum + # Verify fee rate + vsize = lock_tx_obj['vsize'] + fee_paid = make_int(lock_tx_obj['vout'][0]['ct_fee']) + + fee_rate_paid = fee_paid * 1000 // vsize + + self._log.info('tx amount, vsize, feerate: %ld, %ld, %ld', locked_txo_value, vsize, 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 bytes.fromhex(lock_txid_hex), lock_output_n + + def verifyLockRefundTx(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): + lock_refund_tx_obj = self.rpc_callback('decoderawtransaction', [tx_bytes.hex()]) + lock_refund_txid_hex = lock_refund_tx_obj['txid'] + self._log.info('Verifying lock refund tx: {}.'.format(lock_refund_txid_hex)) + + ensure(lock_refund_tx_obj['version'] == self.txVersion(), 'Bad version') + ensure(lock_refund_tx_obj['locktime'] == 0, 'Bad nLockTime') + ensure(len(lock_refund_tx_obj['vin']) == 1, 'tx doesn\'t have one input') + + txin = lock_refund_tx_obj['vin'][0] + ensure(txin['sequence'] == prevout_seq, 'Bad input nSequence') + ensure(txin['scriptSig']['hex'] == '', 'Input scriptsig not empty') + ensure(txin['txid'] == prevout_id.hex() and txin['vout'] == prevout_n, 'Input prevout mismatch') + + ensure(len(lock_refund_tx_obj['vout']) == 3, 'tx doesn\'t have three outputs') + + # Find the output of the lock refund tx to verify + nonce = self.getScriptLockRefundTxNonce(vkbv) + lock_refund_output_n, blinded_info = self.findOutputByNonce(lock_refund_tx_obj, nonce) + ensure(lock_refund_output_n is not None, 'Output not found in tx') + + lock_refund_txo_value = make_int(blinded_info['amount']) + + # Check script + 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) + 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') + + # Check rangeproofs and commitments sum + lock_tx_obj = self.rpc_callback('decoderawtransaction', [lock_tx_bytes.hex()]) + prevout = lock_tx_obj['vout'][prevout_n] + prevtxns = [{'txid': prevout_id.hex(), 'vout': prevout_n, 'scriptPubKey': prevout['scriptPubKey']['hex'], 'amount_commitment': prevout['valueCommitment']}] + rv = self.rpc_callback('verifyrawtransaction', [tx_bytes.hex(), prevtxns]) + ensure(rv['outputs_valid'] is True, 'Invalid outputs') + ensure(rv['inputs_valid'] is True, 'Invalid inputs') + + # Check value + fee_paid = make_int(lock_refund_tx_obj['vout'][0]['ct_fee']) + ensure(swap_value - lock_refund_txo_value == fee_paid, 'Bad output value') + + # Check fee rate + dummy_witness_stack = self.getScriptLockTxDummyWitness(prevout_script) + witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack) + vsize = self.getTxVSize(self.loadTx(tx_bytes), add_witness_bytes=witness_bytes) + fee_rate_paid = fee_paid * 1000 // vsize + self._log.info('vsize, feerate: %ld, %ld', vsize, fee_rate_paid) + + ensure(self.compareFeeRates(fee_rate_paid, feerate), 'Bad fee rate, expected: {}'.format(feerate)) + + return bytes.fromhex(lock_refund_txid_hex), lock_refund_txo_value, lock_refund_output_n + + def verifyLockRefundSpendTx(self, tx_bytes, lock_refund_tx_bytes, + lock_refund_tx_id, prevout_script, + Kal, + prevout_n, prevout_value, feerate, vkbv): + lock_refund_spend_tx_obj = self.rpc_callback('decoderawtransaction', [tx_bytes.hex()]) + lock_refund_spend_txid_hex = lock_refund_spend_tx_obj['txid'] + self._log.info('Verifying lock refund spend tx: {}.'.format(lock_refund_spend_txid_hex)) + + ensure(lock_refund_spend_tx_obj['version'] == self.txVersion(), 'Bad version') + ensure(lock_refund_spend_tx_obj['locktime'] == 0, 'Bad nLockTime') + ensure(len(lock_refund_spend_tx_obj['vin']) == 1, 'tx doesn\'t have one input') + + txin = lock_refund_spend_tx_obj['vin'][0] + ensure(txin['sequence'] == 0, 'Bad input nSequence') + ensure(txin['scriptSig']['hex'] == '', 'Input scriptsig not empty') + ensure(txin['txid'] == lock_refund_tx_id.hex() and txin['vout'] == prevout_n, 'Input prevout mismatch') + + ensure(len(lock_refund_spend_tx_obj['vout']) == 3, 'tx doesn\'t have three outputs') + + # Leader picks output destinations + # Follower is not concerned with them as they pay to leader + + # Check rangeproofs and commitments sum + lock_refund_tx_obj = self.rpc_callback('decoderawtransaction', [lock_refund_tx_bytes.hex()]) + prevout = lock_refund_tx_obj['vout'][prevout_n] + prevtxns = [{'txid': lock_refund_tx_id.hex(), 'vout': prevout_n, 'scriptPubKey': prevout['scriptPubKey']['hex'], 'amount_commitment': prevout['valueCommitment']}] + rv = self.rpc_callback('verifyrawtransaction', [tx_bytes.hex(), prevtxns]) + ensure(rv['outputs_valid'] is True, 'Invalid outputs') + ensure(rv['inputs_valid'] is True, 'Invalid inputs') + + # Check fee rate + dummy_witness_stack = self.getScriptLockRefundSpendTxDummyWitness(prevout_script) + witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack) + vsize = self.getTxVSize(self.loadTx(tx_bytes), add_witness_bytes=witness_bytes) + fee_paid = make_int(lock_refund_spend_tx_obj['vout'][0]['ct_fee']) + fee_rate_paid = fee_paid * 1000 // vsize + ensure(self.compareFeeRates(fee_rate_paid, feerate), 'Bad fee rate, expected: {}'.format(feerate)) + + return True + + def getLockTxSwapOutputValue(self, bid, xmr_swap): + lock_tx_obj = self.rpc_callback('decoderawtransaction', [xmr_swap.a_lock_tx.hex()]) + nonce = self.getScriptLockTxNonce(xmr_swap.vkbv) + output_n, _ = self.findOutputByNonce(lock_tx_obj, nonce) + ensure(output_n is not None, 'Output not found in tx') + return bytes.fromhex(lock_tx_obj['vout'][output_n]['valueCommitment']) + + def getLockRefundTxSwapOutputValue(self, bid, xmr_swap): + lock_refund_tx_obj = self.rpc_callback('decoderawtransaction', [xmr_swap.a_lock_refund_tx.hex()]) + nonce = self.getScriptLockRefundTxNonce(xmr_swap.vkbv) + output_n, _ = self.findOutputByNonce(lock_refund_tx_obj, nonce) + ensure(output_n is not None, 'Output not found in tx') + return bytes.fromhex(lock_refund_tx_obj['vout'][output_n]['valueCommitment']) + + def getLockRefundTxSwapOutput(self, xmr_swap): + lock_refund_tx_obj = self.rpc_callback('decoderawtransaction', [xmr_swap.a_lock_refund_tx.hex()]) + nonce = self.getScriptLockRefundTxNonce(xmr_swap.vkbv) + output_n, _ = self.findOutputByNonce(lock_refund_tx_obj, nonce) + ensure(output_n is not None, 'Output not found in tx') + return output_n + + def createScriptLockSpendTx(self, tx_lock_bytes, script_lock, pk_dest, tx_fee_rate, vkbv): + lock_tx_obj = self.rpc_callback('decoderawtransaction', [tx_lock_bytes.hex()]) + lock_txid_hex = lock_tx_obj['txid'] + + ensure(lock_tx_obj['version'] == self.txVersion(), 'Bad version') + ensure(lock_tx_obj['locktime'] == 0, 'Bad nLockTime') + + # Find the output of the lock tx to verify + nonce = self.getScriptLockTxNonce(vkbv) + spend_n, blinded_info = self.findOutputByNonce(lock_tx_obj, nonce) + ensure(spend_n is not None, 'Output not found in tx') + + addr_out = self.pubkey_to_address(pk_dest) + + inputs = [{'txid': lock_txid_hex, 'vout': spend_n, 'sequence': 0, 'blindingfactor': blinded_info['blind']}] + outputs = [{'type': 'blind', 'amount': blinded_info['amount'], 'address': addr_out, 'pubkey': pk_dest.hex()}] + params = [inputs, outputs] + rv = self.rpc_callback('createrawparttransaction', params) + lock_spend_tx_hex = rv['hex'] + + # Set dummy witness data for fee estimation + dummy_witness_stack = self.getScriptLockTxDummyWitness(script_lock) + + # Use a junk change pubkey to avoid adding unused keys to the wallet + zero_change_key = i2b(self.getNewSecretKey()) + zero_change_pubkey = self.getPubkey(zero_change_key) + inputs_info = {'0': {'value': blinded_info['amount'], 'blind': blinded_info['blind'], 'witnessstack': dummy_witness_stack}} + outputs_info = rv['amounts'] + options = { + 'changepubkey': zero_change_pubkey.hex(), + 'feeRate': self.format_amount(tx_fee_rate), + 'subtractFeeFromOutputs': [0, ] + } + + rv = self.rpc_callback('fundrawtransactionfrom', ['blind', lock_spend_tx_hex, inputs_info, outputs_info, options]) + lock_spend_tx_hex = rv['hex'] + lock_spend_tx_obj = self.rpc_callback('decoderawtransaction', [lock_spend_tx_hex]) + + vsize = lock_spend_tx_obj['vsize'] + pay_fee = make_int(lock_spend_tx_obj['vout'][0]['ct_fee']) + actual_tx_fee_rate = pay_fee * 1000 // vsize + self._log.info('createScriptLockSpendTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.', + lock_spend_tx_obj['txid'], actual_tx_fee_rate, vsize, pay_fee) + + return bytes.fromhex(lock_spend_tx_hex) + + def verifyLockSpendTx(self, tx_bytes, + lock_tx_bytes, lock_tx_script, + a_pk_f, feerate, vkbv): + lock_spend_tx_obj = self.rpc_callback('decoderawtransaction', [tx_bytes.hex()]) + lock_spend_txid_hex = lock_spend_tx_obj['txid'] + self._log.info('Verifying lock spend tx: {}.'.format(lock_spend_txid_hex)) + + ensure(lock_spend_tx_obj['version'] == self.txVersion(), 'Bad version') + ensure(lock_spend_tx_obj['locktime'] == 0, 'Bad nLockTime') + ensure(len(lock_spend_tx_obj['vin']) == 1, 'tx doesn\'t have one input') + + lock_tx_obj = self.rpc_callback('decoderawtransaction', [lock_tx_bytes.hex()]) + lock_txid_hex = lock_tx_obj['txid'] + + # Find the output of the lock tx to verify + nonce = self.getScriptLockTxNonce(vkbv) + spend_n, input_blinded_info = self.findOutputByNonce(lock_tx_obj, nonce) + ensure(spend_n is not None, 'Output not found in tx') + + txin = lock_spend_tx_obj['vin'][0] + ensure(txin['sequence'] == 0, 'Bad input nSequence') + ensure(txin['scriptSig']['hex'] == '', 'Input scriptsig not empty') + ensure(txin['txid'] == lock_txid_hex and txin['vout'] == spend_n, 'Input prevout mismatch') + + ensure(len(lock_spend_tx_obj['vout']) == 3, 'tx doesn\'t have three outputs') + + addr_out = self.pubkey_to_address(a_pk_f) + privkey = self.rpc_callback('dumpprivkey', [addr_out]) + + # Find output: + output_blinded_info = None + output_n = None + for txo in lock_spend_tx_obj['vout']: + if txo['type'] != 'blind': + continue + try: + output_blinded_info = self.rpc_callback('rewindrangeproof', [txo['rangeproof'], txo['valueCommitment'], privkey, txo['data_hex']]) + output_n = txo['n'] + break + except Exception as e: + self._log.debug('Searching for locked output: {}'.format(str(e))) + pass + ensure(output_n is not None, 'Output not found in tx') + + # Commitment + v = self.rpc_callback('verifycommitment', [lock_spend_tx_obj['vout'][output_n]['valueCommitment'], output_blinded_info['blind'], output_blinded_info['amount']]) + ensure(v['result'] is True, 'verifycommitment failed') + + # Check rangeproofs and commitments sum + prevout = lock_tx_obj['vout'][spend_n] + prevtxns = [{'txid': lock_txid_hex, 'vout': spend_n, 'scriptPubKey': prevout['scriptPubKey']['hex'], 'amount_commitment': prevout['valueCommitment']}] + rv = self.rpc_callback('verifyrawtransaction', [tx_bytes.hex(), prevtxns]) + ensure(rv['outputs_valid'] is True, 'Invalid outputs') + ensure(rv['inputs_valid'] is True, 'Invalid inputs') + + # Check amount + fee_paid = make_int(lock_spend_tx_obj['vout'][0]['ct_fee']) + amount_difference = make_int(input_blinded_info['amount']) - make_int(output_blinded_info['amount']) + ensure(fee_paid == amount_difference, 'Invalid output amount') + + # Check fee + dummy_witness_stack = self.getScriptLockTxDummyWitness(lock_tx_script) + witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack) + + vsize = self.getTxVSize(self.loadTx(tx_bytes), add_witness_bytes=witness_bytes) + fee_rate_paid = fee_paid * 1000 // vsize + self._log.info('vsize, feerate: %ld, %ld', vsize, fee_rate_paid) + if not self.compareFeeRates(fee_rate_paid, feerate): + raise ValueError('Bad fee rate, expected: {}'.format(feerate)) + + return True + + def createScriptLockRefundSpendToFTx(self, tx_lock_refund_bytes, script_lock_refund, pkh_dest, tx_fee_rate, vkbv): + # lock refund swipe tx + # Sends the coinA locked coin to the follower + lock_refund_tx_obj = self.rpc_callback('decoderawtransaction', [tx_lock_refund_bytes.hex()]) + nonce = self.getScriptLockRefundTxNonce(vkbv) + + # Find the output of the lock refund tx to spend + spend_n, input_blinded_info = self.findOutputByNonce(lock_refund_tx_obj, nonce) + ensure(spend_n is not None, 'Output not found in tx') + + tx_lock_refund_id = lock_refund_tx_obj['txid'] + addr_out = self.pkh_to_address(pkh_dest) + addr_info = self.rpc_callback('getaddressinfo', [addr_out]) + output_pubkey_hex = addr_info['pubkey'] + + A, B, lock2_value, C = self.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 + + inputs = [{'txid': tx_lock_refund_id, 'vout': spend_n, 'sequence': lock2_value, 'blindingfactor': input_blinded_info['blind']}] + outputs = [{'type': 'blind', 'amount': input_blinded_info['amount'], 'address': addr_out, 'pubkey': output_pubkey_hex}] + params = [inputs, outputs] + rv = self.rpc_callback('createrawparttransaction', params) + + lock_refund_swipe_tx_hex = rv['hex'] + + # Set dummy witness data for fee estimation + dummy_witness_stack = self.getScriptLockRefundSwipeTxDummyWitness(script_lock_refund) + + # Use a junk change pubkey to avoid adding unused keys to the wallet + zero_change_key = i2b(self.getNewSecretKey()) + zero_change_pubkey = self.getPubkey(zero_change_key) + inputs_info = {'0': {'value': input_blinded_info['amount'], 'blind': input_blinded_info['blind'], 'witnessstack': dummy_witness_stack}} + outputs_info = rv['amounts'] + options = { + 'changepubkey': zero_change_pubkey.hex(), + 'feeRate': self.format_amount(tx_fee_rate), + 'subtractFeeFromOutputs': [0, ] + } + + rv = self.rpc_callback('fundrawtransactionfrom', ['blind', lock_refund_swipe_tx_hex, inputs_info, outputs_info, options]) + lock_refund_swipe_tx_hex = rv['hex'] + + return bytes.fromhex(lock_refund_swipe_tx_hex) + class PARTInterfaceAnon(PARTInterface): @staticmethod @@ -134,7 +632,7 @@ class PARTInterfaceAnon(PARTInterface): else: addr_info = self.rpc_callback('getaddressinfo', [sx_addr]) if not addr_info['iswatchonly']: - wif_prefix = chainparams[self.coin_type()][self._network]['key_prefix'] + wif_prefix = self.chainparams_network()['key_prefix'] wif_scan_key = toWIF(wif_prefix, kbv) self.rpc_callback('importstealthaddress', [wif_scan_key, Kbs.hex()]) self._log.info('Imported watch-only sx_addr: {}'.format(sx_addr)) @@ -166,7 +664,7 @@ class PARTInterfaceAnon(PARTInterface): sx_addr = self.formatStealthAddress(Kbv, Kbs) addr_info = self.rpc_callback('getaddressinfo', [sx_addr]) if not addr_info['ismine']: - wif_prefix = chainparams[self.coin_type()][self._network]['key_prefix'] + wif_prefix = self.chainparams_network()['key_prefix'] wif_scan_key = toWIF(wif_prefix, kbv) wif_spend_key = toWIF(wif_prefix, kbs) self.rpc_callback('importstealthaddress', [wif_scan_key, wif_spend_key]) diff --git a/basicswap/interface_xmr.py b/basicswap/interface_xmr.py index c9b11ec..ada3704 100644 --- a/basicswap/interface_xmr.py +++ b/basicswap/interface_xmr.py @@ -26,8 +26,7 @@ from .util import ( ensure, dumpj, make_int, - format_amount) -from .basicswap_util import ( + format_amount, TemporaryError) from .rpc_xmr import ( make_xmr_rpc_func, diff --git a/basicswap/util.py b/basicswap/util.py index 78b0a88..46ccbb5 100644 --- a/basicswap/util.py +++ b/basicswap/util.py @@ -22,9 +22,13 @@ decimal_ctx = decimal.Context() decimal_ctx.prec = 20 -def assert_cond(v, err='Bad opcode'): +class TemporaryError(ValueError): + pass + + +def ensure(v, err_string): if not v: - raise ValueError(err) + raise ValueError(err_string) def toBool(s) -> bool: @@ -222,6 +226,10 @@ def getCompactSizeLen(v): raise ValueError('Value too large') +def getWitnessElementLen(v): + return getCompactSizeLen(v) + v + + def SerialiseNumCompact(v): if v < 253: return bytes((v,)) @@ -339,8 +347,3 @@ def encodeStealthAddress(prefix_byte, scan_pubkey, spend_pubkey): b = bytes((prefix_byte,)) + data b += hashlib.sha256(hashlib.sha256(b).digest()).digest()[:4] return b58encode(b) - - -def ensure(passed, err_string): - if not passed: - raise ValueError(err_string) diff --git a/doc/release-notes.md b/doc/release-notes.md index 4f5e82e..245605b 100644 --- a/doc/release-notes.md +++ b/doc/release-notes.md @@ -1,5 +1,36 @@ + +0.0.25 +============== + +- Fix extra 33 bytes in lock spend fee calculation. +- XMR swaps use watchonly addresses to save the lock tx to the wallet + - Instead of scantxoutset +- Add missing check of leader's lock refund tx signature result +- Blind part -> XMR swaps are possible: + - The sha256 hash of the chain b view private key is used as the nonce for transactions requiring cooperation to sign. + - Follower sends a public key in xmr_swap.dest_af. + - Verify the rangeproofs and commitments of blinded pre-shared txns. +- Add explicit tests for all paths of: + - PARTct -> XMR + - BTC -> XMR + - LTC -> XMR + + +0.0.24 +============== + +- Can swap Particl Anon outputs in place of XMR + + +0.0.23 +============== + +- Enables private offers + + 0.0.22 ============== + - Improved wallets page - Consistent wallet order - Separated RPC calls into threads. @@ -7,6 +38,7 @@ 0.0.21 ============== + - Raised Particl and Monero daemon versions. - Display shared address on bid page if show more info is enabled. - Added View Lock Wallet Transfers button to bid page. @@ -14,5 +46,6 @@ 0.0.6 ============== + - Experimental support for XMR swaps. - Single direction only, scriptless -> XMR diff --git a/tests/basicswap/common.py b/tests/basicswap/common.py index 508a6ba..77443b5 100644 --- a/tests/basicswap/common.py +++ b/tests/basicswap/common.py @@ -29,6 +29,10 @@ BTC_BASE_PORT = 31792 BTC_BASE_RPC_PORT = 32792 BTC_BASE_ZMQ_PORT = 33792 +LTC_BASE_PORT = 34792 +LTC_BASE_RPC_PORT = 35792 +LTC_BASE_ZMQ_PORT = 36792 + PREFIX_SECRET_KEY_REGTEST = 0x2e diff --git a/tests/basicswap/test_btc_xmr.py b/tests/basicswap/test_btc_xmr.py new file mode 100644 index 0000000..96b69b2 --- /dev/null +++ b/tests/basicswap/test_btc_xmr.py @@ -0,0 +1,229 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# Copyright (c) 2021 tecnovert +# Distributed under the MIT software license, see the accompanying +# file LICENSE or http://www.opensource.org/licenses/mit-license.php. + +import json +import random +import logging +import unittest +from urllib.request import urlopen + +from basicswap.basicswap import ( + Coins, + SwapTypes, + BidStates, + DebugTypes, +) +from basicswap.basicswap_util import ( + SEQUENCE_LOCK_BLOCKS, +) +from basicswap.util import ( + make_int, + format_amount, +) +from tests.basicswap.common import ( + wait_for_bid, + wait_for_offer, + wait_for_none_active, +) + +from .test_xmr import BaseTest, test_delay_event + +logger = logging.getLogger() + + +class Test(BaseTest): + __test__ = True + + @classmethod + def setUpClass(cls): + if not hasattr(cls, 'test_coin_from'): + cls.test_coin_from = Coins.BTC + if not hasattr(cls, 'start_ltc_nodes'): + cls.start_ltc_nodes = False + super(Test, cls).setUpClass() + + @classmethod + def tearDownClass(cls): + logging.info('Finalising BTC Test') + super(Test, cls).tearDownClass() + + def getBalance(self, js_wallets): + return float(js_wallets[str(int(self.test_coin_from))]['balance']) + float(js_wallets[str(int(self.test_coin_from))]['unconfirmed']) + + def getXmrBalance(self, js_wallets): + return float(js_wallets[str(int(Coins.XMR))]['unconfirmed']) + float(js_wallets[str(int(Coins.XMR))]['balance']) + + def test_01_full_swap(self): + logging.info('---------- Test {} to XMR'.format(str(self.test_coin_from))) + swap_clients = self.swap_clients + + js_0 = json.loads(urlopen('http://127.0.0.1:1800/json/wallets').read()) + node0_from_before = self.getBalance(js_0) + + js_1 = json.loads(urlopen('http://127.0.0.1:1801/json/wallets').read()) + node1_from_before = self.getBalance(js_1) + + js_0_xmr = json.loads(urlopen('http://127.0.0.1:1800/json/wallets/xmr').read()) + js_1_xmr = json.loads(urlopen('http://127.0.0.1:1801/json/wallets/xmr').read()) + + amt_swap = make_int(random.uniform(0.1, 2.0), scale=8, r=1) + rate_swap = make_int(random.uniform(0.2, 20.0), scale=12, r=1) + offer_id = swap_clients[0].postOffer(self.test_coin_from, Coins.XMR, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP) + wait_for_offer(test_delay_event, swap_clients[1], offer_id) + offers = swap_clients[0].listOffers(filters={'offer_id': offer_id}) + offer = offers[0] + + bid_id = swap_clients[1].postXmrBid(offer_id, offer.amount_from) + + wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.BID_RECEIVED) + + swap_clients[0].acceptXmrBid(bid_id) + + wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=180) + wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True) + + amount_from = float(format_amount(amt_swap, 8)) + js_1 = json.loads(urlopen('http://127.0.0.1:1801/json/wallets').read()) + node1_from_after = self.getBalance(js_1) + assert(node1_from_after > node1_from_before + (amount_from - 0.05)) + + js_0 = json.loads(urlopen('http://127.0.0.1:1800/json/wallets').read()) + node0_from_after = self.getBalance(js_0) + # TODO: Discard block rewards + # assert(node0_from_after < node0_from_before - amount_from) + + js_0_xmr_after = json.loads(urlopen('http://127.0.0.1:1800/json/wallets/xmr').read()) + js_1_xmr_after = json.loads(urlopen('http://127.0.0.1:1801/json/wallets/xmr').read()) + + scale_from = 8 + amount_to = int((amt_swap * rate_swap) // (10 ** scale_from)) + amount_to_float = float(format_amount(amount_to, 12)) + node1_xmr_after = float(js_1_xmr_after['unconfirmed']) + float(js_1_xmr_after['balance']) + node1_xmr_before = float(js_1_xmr['unconfirmed']) + float(js_1_xmr['balance']) + assert(node1_xmr_after > node1_xmr_before + (amount_to_float - 0.02)) + + def test_02_leader_recover_a_lock_tx(self): + logging.info('---------- Test {} to XMR leader recovers coin a lock tx'.format(str(self.test_coin_from))) + swap_clients = self.swap_clients + + js_w0_before = json.loads(urlopen('http://127.0.0.1:1800/json/wallets').read()) + node0_from_before = self.getBalance(js_w0_before) + + amt_swap = make_int(random.uniform(0.1, 2.0), scale=8, r=1) + rate_swap = make_int(random.uniform(0.2, 20.0), scale=12, r=1) + offer_id = swap_clients[0].postOffer( + self.test_coin_from, Coins.XMR, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP, + lock_type=SEQUENCE_LOCK_BLOCKS, lock_value=12) + wait_for_offer(test_delay_event, swap_clients[1], offer_id) + offer = swap_clients[1].getOffer(offer_id) + + bid_id = swap_clients[1].postXmrBid(offer_id, offer.amount_from) + + wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.BID_RECEIVED) + + bid, xmr_swap = swap_clients[0].getXmrBid(bid_id) + assert(xmr_swap) + + swap_clients[1].setBidDebugInd(bid_id, DebugTypes.BID_STOP_AFTER_COIN_A_LOCK) + + swap_clients[0].acceptXmrBid(bid_id) + + wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, wait_for=180) + wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, sent=True) + + js_w0_after = json.loads(urlopen('http://127.0.0.1:1800/json/wallets').read()) + node0_from_after = self.getBalance(js_w0_after) + + # TODO: Discard block rewards + # assert(node0_from_before - node0_from_after < 0.02) + + def test_03_follower_recover_a_lock_tx(self): + logging.info('---------- Test {} to XMR follower recovers coin a lock tx'.format(str(self.test_coin_from))) + swap_clients = self.swap_clients + + js_w0_before = json.loads(urlopen('http://127.0.0.1:1800/json/wallets').read()) + js_w1_before = json.loads(urlopen('http://127.0.0.1:1801/json/wallets').read()) + + amt_swap = make_int(random.uniform(0.1, 2.0), scale=8, r=1) + rate_swap = make_int(random.uniform(0.2, 20.0), scale=12, r=1) + offer_id = swap_clients[0].postOffer( + self.test_coin_from, Coins.XMR, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP, + lock_type=SEQUENCE_LOCK_BLOCKS, lock_value=12) + wait_for_offer(test_delay_event, swap_clients[1], offer_id) + offer = swap_clients[1].getOffer(offer_id) + + bid_id = swap_clients[1].postXmrBid(offer_id, offer.amount_from) + + wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.BID_RECEIVED) + + bid, xmr_swap = swap_clients[0].getXmrBid(bid_id) + assert(xmr_swap) + + swap_clients[1].setBidDebugInd(bid_id, DebugTypes.BID_STOP_AFTER_COIN_A_LOCK) + swap_clients[0].setBidDebugInd(bid_id, DebugTypes.BID_DONT_SPEND_COIN_A_LOCK_REFUND) + + swap_clients[0].acceptXmrBid(bid_id) + + wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.BID_STALLED_FOR_TEST, wait_for=180) + wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.XMR_SWAP_FAILED_SWIPED, wait_for=80, sent=True) + + js_w1_after = json.loads(urlopen('http://127.0.0.1:1801/json/wallets').read()) + + node1_from_before = self.getBalance(js_w1_before) + node1_from_after = self.getBalance(js_w1_after) + amount_from = float(format_amount(amt_swap, 8)) + # TODO: Discard block rewards + # assert(node1_from_after - node1_from_before > (amount_from - 0.02)) + + wait_for_none_active(test_delay_event, 1800) + wait_for_none_active(test_delay_event, 1801) + + def test_04_follower_recover_b_lock_tx(self): + logging.info('---------- Test {} to XMR follower recovers coin b lock tx'.format(str(self.test_coin_from))) + + swap_clients = self.swap_clients + + js_w0_before = json.loads(urlopen('http://127.0.0.1:1800/json/wallets').read()) + js_w1_before = json.loads(urlopen('http://127.0.0.1:1801/json/wallets').read()) + + amt_swap = make_int(random.uniform(0.1, 2.0), scale=8, r=1) + rate_swap = make_int(random.uniform(0.2, 20.0), scale=12, r=1) + offer_id = swap_clients[0].postOffer( + self.test_coin_from, Coins.XMR, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP, + lock_type=SEQUENCE_LOCK_BLOCKS, lock_value=18) + wait_for_offer(test_delay_event, swap_clients[1], offer_id) + offer = swap_clients[1].getOffer(offer_id) + + bid_id = swap_clients[1].postXmrBid(offer_id, offer.amount_from) + + wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.BID_RECEIVED) + + bid, xmr_swap = swap_clients[0].getXmrBid(bid_id) + assert(xmr_swap) + + swap_clients[1].setBidDebugInd(bid_id, DebugTypes.CREATE_INVALID_COIN_B_LOCK) + swap_clients[0].acceptXmrBid(bid_id) + + wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, wait_for=180) + wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, sent=True) + + js_w0_after = json.loads(urlopen('http://127.0.0.1:1800/json/wallets').read()) + js_w1_after = json.loads(urlopen('http://127.0.0.1:1801/json/wallets').read()) + + node0_from_before = self.getBalance(js_w0_before) + node0_from_after = self.getBalance(js_w0_after) + + # TODO: Discard block rewards + # assert(node0_from_before - node0_from_after < 0.02) + + node1_xmr_before = self.getXmrBalance(js_w1_before) + node1_xmr_after = self.getXmrBalance(js_w1_after) + assert(node1_xmr_before - node1_xmr_after < 0.02) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/basicswap/test_ltc_xmr.py b/tests/basicswap/test_ltc_xmr.py new file mode 100644 index 0000000..6a41a87 --- /dev/null +++ b/tests/basicswap/test_ltc_xmr.py @@ -0,0 +1,249 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# Copyright (c) 2021 tecnovert +# Distributed under the MIT software license, see the accompanying +# file LICENSE or http://www.opensource.org/licenses/mit-license.php. + +import json +import random +import logging +import unittest +from urllib.request import urlopen + +from basicswap.basicswap import ( + Coins, + SwapTypes, + BidStates, + DebugTypes, +) +from basicswap.basicswap_util import ( + SEQUENCE_LOCK_BLOCKS, +) +from basicswap.util import ( + make_int, + format_amount, +) +from tests.basicswap.common import ( + wait_for_bid, + wait_for_offer, + wait_for_none_active, +) + +''' +# Should work? + +from .test_btc_xmr import Test, test_delay_event + +logger = logging.getLogger() + + +class TestLTC(Test): + __test__ = True + + @classmethod + def setUpClass(cls): + cls.test_coin_from = Coins.LTC + cls.start_ltc_nodes = True + super(TestLTC, cls).setUpClass() + + @classmethod + def tearDownClass(cls): + logging.info('Finalising LTC Test') + super(TestLTC, cls).tearDownClass() +''' + +from .test_xmr import BaseTest, test_delay_event + +logger = logging.getLogger() + + +class Test(BaseTest): + __test__ = True + + @classmethod + def setUpClass(cls): + cls.test_coin_from = Coins.LTC + cls.start_ltc_nodes = True + super(Test, cls).setUpClass() + + @classmethod + def tearDownClass(cls): + logging.info('Finalising LTC Test') + super(Test, cls).tearDownClass() + + def getBalance(self, js_wallets): + return float(js_wallets[str(int(self.test_coin_from))]['balance']) + float(js_wallets[str(int(self.test_coin_from))]['unconfirmed']) + + def getXmrBalance(self, js_wallets): + return float(js_wallets[str(int(Coins.XMR))]['unconfirmed']) + float(js_wallets[str(int(Coins.XMR))]['balance']) + + def test_01_full_swap(self): + logging.info('---------- Test {} to XMR'.format(str(self.test_coin_from))) + swap_clients = self.swap_clients + + js_0 = json.loads(urlopen('http://127.0.0.1:1800/json/wallets').read()) + node0_from_before = self.getBalance(js_0) + + js_1 = json.loads(urlopen('http://127.0.0.1:1801/json/wallets').read()) + node1_from_before = self.getBalance(js_1) + + js_0_xmr = json.loads(urlopen('http://127.0.0.1:1800/json/wallets/xmr').read()) + js_1_xmr = json.loads(urlopen('http://127.0.0.1:1801/json/wallets/xmr').read()) + + amt_swap = make_int(random.uniform(0.1, 2.0), scale=8, r=1) + rate_swap = make_int(random.uniform(0.2, 20.0), scale=12, r=1) + offer_id = swap_clients[0].postOffer(self.test_coin_from, Coins.XMR, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP) + wait_for_offer(test_delay_event, swap_clients[1], offer_id) + offers = swap_clients[0].listOffers(filters={'offer_id': offer_id}) + offer = offers[0] + + bid_id = swap_clients[1].postXmrBid(offer_id, offer.amount_from) + + wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.BID_RECEIVED) + + swap_clients[0].acceptXmrBid(bid_id) + + wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=180) + wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True) + + amount_from = float(format_amount(amt_swap, 8)) + js_1 = json.loads(urlopen('http://127.0.0.1:1801/json/wallets').read()) + node1_from_after = self.getBalance(js_1) + assert(node1_from_after > node1_from_before + (amount_from - 0.05)) + + js_0 = json.loads(urlopen('http://127.0.0.1:1800/json/wallets').read()) + node0_from_after = self.getBalance(js_0) + # TODO: Discard block rewards + # assert(node0_from_after < node0_from_before - amount_from) + + js_0_xmr_after = json.loads(urlopen('http://127.0.0.1:1800/json/wallets/xmr').read()) + js_1_xmr_after = json.loads(urlopen('http://127.0.0.1:1801/json/wallets/xmr').read()) + + scale_from = 8 + amount_to = int((amt_swap * rate_swap) // (10 ** scale_from)) + amount_to_float = float(format_amount(amount_to, 12)) + node1_xmr_after = float(js_1_xmr_after['unconfirmed']) + float(js_1_xmr_after['balance']) + node1_xmr_before = float(js_1_xmr['unconfirmed']) + float(js_1_xmr['balance']) + assert(node1_xmr_after > node1_xmr_before + (amount_to_float - 0.02)) + + def test_02_leader_recover_a_lock_tx(self): + logging.info('---------- Test {} to XMR leader recovers coin a lock tx'.format(str(self.test_coin_from))) + swap_clients = self.swap_clients + + js_w0_before = json.loads(urlopen('http://127.0.0.1:1800/json/wallets').read()) + node0_from_before = self.getBalance(js_w0_before) + + amt_swap = make_int(random.uniform(0.1, 2.0), scale=8, r=1) + rate_swap = make_int(random.uniform(0.2, 20.0), scale=12, r=1) + offer_id = swap_clients[0].postOffer( + self.test_coin_from, Coins.XMR, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP, + lock_type=SEQUENCE_LOCK_BLOCKS, lock_value=12) + wait_for_offer(test_delay_event, swap_clients[1], offer_id) + offer = swap_clients[1].getOffer(offer_id) + + bid_id = swap_clients[1].postXmrBid(offer_id, offer.amount_from) + + wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.BID_RECEIVED) + + bid, xmr_swap = swap_clients[0].getXmrBid(bid_id) + assert(xmr_swap) + + swap_clients[1].setBidDebugInd(bid_id, DebugTypes.BID_STOP_AFTER_COIN_A_LOCK) + + swap_clients[0].acceptXmrBid(bid_id) + + wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, wait_for=180) + wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, sent=True) + + js_w0_after = json.loads(urlopen('http://127.0.0.1:1800/json/wallets').read()) + node0_from_after = self.getBalance(js_w0_after) + + # TODO: Discard block rewards + # assert(node0_from_before - node0_from_after < 0.02) + + def test_03_follower_recover_a_lock_tx(self): + logging.info('---------- Test {} to XMR follower recovers coin a lock tx'.format(str(self.test_coin_from))) + swap_clients = self.swap_clients + + js_w0_before = json.loads(urlopen('http://127.0.0.1:1800/json/wallets').read()) + js_w1_before = json.loads(urlopen('http://127.0.0.1:1801/json/wallets').read()) + + amt_swap = make_int(random.uniform(0.1, 2.0), scale=8, r=1) + rate_swap = make_int(random.uniform(0.2, 20.0), scale=12, r=1) + offer_id = swap_clients[0].postOffer( + self.test_coin_from, Coins.XMR, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP, + lock_type=SEQUENCE_LOCK_BLOCKS, lock_value=12) + wait_for_offer(test_delay_event, swap_clients[1], offer_id) + offer = swap_clients[1].getOffer(offer_id) + + bid_id = swap_clients[1].postXmrBid(offer_id, offer.amount_from) + + wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.BID_RECEIVED) + + bid, xmr_swap = swap_clients[0].getXmrBid(bid_id) + assert(xmr_swap) + + swap_clients[1].setBidDebugInd(bid_id, DebugTypes.BID_STOP_AFTER_COIN_A_LOCK) + swap_clients[0].setBidDebugInd(bid_id, DebugTypes.BID_DONT_SPEND_COIN_A_LOCK_REFUND) + + swap_clients[0].acceptXmrBid(bid_id) + + wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.BID_STALLED_FOR_TEST, wait_for=180) + wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.XMR_SWAP_FAILED_SWIPED, wait_for=80, sent=True) + + js_w1_after = json.loads(urlopen('http://127.0.0.1:1801/json/wallets').read()) + node1_from_before = self.getBalance(js_w1_before) + node1_from_after = self.getBalance(js_w1_after) + amount_from = float(format_amount(amt_swap, 8)) + # TODO: Discard block rewards + # assert(node1_from_after - node1_from_before > (amount_from - 0.02)) + + wait_for_none_active(test_delay_event, 1800) + wait_for_none_active(test_delay_event, 1801) + + def test_04_follower_recover_b_lock_tx(self): + logging.info('---------- Test {} to XMR follower recovers coin b lock tx'.format(str(self.test_coin_from))) + + swap_clients = self.swap_clients + + js_w0_before = json.loads(urlopen('http://127.0.0.1:1800/json/wallets').read()) + js_w1_before = json.loads(urlopen('http://127.0.0.1:1801/json/wallets').read()) + + amt_swap = make_int(random.uniform(0.1, 2.0), scale=8, r=1) + rate_swap = make_int(random.uniform(0.2, 20.0), scale=12, r=1) + offer_id = swap_clients[0].postOffer( + self.test_coin_from, Coins.XMR, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP, + lock_type=SEQUENCE_LOCK_BLOCKS, lock_value=18) + wait_for_offer(test_delay_event, swap_clients[1], offer_id) + offer = swap_clients[1].getOffer(offer_id) + + bid_id = swap_clients[1].postXmrBid(offer_id, offer.amount_from) + + wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.BID_RECEIVED) + + bid, xmr_swap = swap_clients[0].getXmrBid(bid_id) + assert(xmr_swap) + + swap_clients[1].setBidDebugInd(bid_id, DebugTypes.CREATE_INVALID_COIN_B_LOCK) + swap_clients[0].acceptXmrBid(bid_id) + + wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, wait_for=180) + wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, sent=True) + + js_w0_after = json.loads(urlopen('http://127.0.0.1:1800/json/wallets').read()) + js_w1_after = json.loads(urlopen('http://127.0.0.1:1801/json/wallets').read()) + + node0_from_before = self.getBalance(js_w0_before) + node0_from_after = self.getBalance(js_w0_after) + + # TODO: Discard block rewards + # assert(node0_from_before - node0_from_after < 0.02) + + node1_xmr_before = self.getXmrBalance(js_w1_before) + node1_xmr_after = self.getXmrBalance(js_w1_after) + assert(node1_xmr_before - node1_xmr_after < 0.02) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/basicswap/test_partblind_xmr.py b/tests/basicswap/test_partblind_xmr.py new file mode 100644 index 0000000..6e89616 --- /dev/null +++ b/tests/basicswap/test_partblind_xmr.py @@ -0,0 +1,235 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# Copyright (c) 2021 tecnovert +# Distributed under the MIT software license, see the accompanying +# file LICENSE or http://www.opensource.org/licenses/mit-license.php. + +import json +import random +import logging +import unittest +from urllib.request import urlopen + +from basicswap.basicswap import ( + Coins, + SwapTypes, + BidStates, + DebugTypes, +) +from basicswap.basicswap_util import ( + SEQUENCE_LOCK_BLOCKS, +) +from basicswap.util import ( + make_int, + format_amount, +) +from tests.basicswap.common import ( + wait_for_bid, + wait_for_offer, + wait_for_none_active, + wait_for_balance, + post_json_req, +) + +from .test_xmr import BaseTest, test_delay_event + +logger = logging.getLogger() + + +class Test(BaseTest): + __test__ = True + + @classmethod + def setUpClass(cls): + super(Test, cls).setUpClass() + + js_0 = json.loads(urlopen('http://127.0.0.1:1800/json/wallets/part').read()) + node0_blind_before = js_0['blind_balance'] + js_0['blind_unconfirmed'] + + post_json = { + 'value': 100, + 'address': js_0['stealth_address'], + 'subfee': False, + 'type_to': 'blind', + } + json_rv = json.loads(post_json_req('http://127.0.0.1:1800/json/wallets/part/withdraw', post_json)) + assert(len(json_rv['txid']) == 64) + + logging.info('Waiting for blind balance') + wait_for_balance(test_delay_event, 'http://127.0.0.1:1800/json/wallets/part', 'blind_balance', 100.0 + node0_blind_before) + js_0 = json.loads(urlopen('http://127.0.0.1:1800/json/wallets/part').read()) + node0_blind_before = js_0['blind_balance'] + js_0['blind_unconfirmed'] + + def getBalance(self, js_wallets): + return float(js_wallets[str(int(Coins.PART))]['blind_balance']) + float(js_wallets[str(int(Coins.PART))]['blind_unconfirmed']) + + def getXmrBalance(self, js_wallets): + return float(js_wallets[str(int(Coins.XMR))]['unconfirmed']) + float(js_wallets[str(int(Coins.XMR))]['balance']) + + def test_01_part_xmr(self): + logging.info('---------- Test PARTct to XMR') + swap_clients = self.swap_clients + + js_0 = json.loads(urlopen('http://127.0.0.1:1800/json/wallets/part').read()) + assert(float(js_0['blind_balance']) > 10.0) + node0_blind_before = js_0['blind_balance'] + js_0['blind_unconfirmed'] + + js_1 = json.loads(urlopen('http://127.0.0.1:1801/json/wallets/part').read()) + node1_blind_before = js_1['blind_balance'] + js_1['blind_unconfirmed'] + + js_0_xmr = json.loads(urlopen('http://127.0.0.1:1800/json/wallets/xmr').read()) + js_1_xmr = json.loads(urlopen('http://127.0.0.1:1801/json/wallets/xmr').read()) + + amt_swap = make_int(random.uniform(0.1, 2.0), scale=8, r=1) + rate_swap = make_int(random.uniform(0.2, 20.0), scale=12, r=1) + offer_id = swap_clients[0].postOffer(Coins.PART_BLIND, Coins.XMR, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP) + wait_for_offer(test_delay_event, swap_clients[1], offer_id) + offers = swap_clients[0].listOffers(filters={'offer_id': offer_id}) + offer = offers[0] + + bid_id = swap_clients[1].postXmrBid(offer_id, offer.amount_from) + + wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.BID_RECEIVED) + + swap_clients[0].acceptXmrBid(bid_id) + + wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=180) + wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True) + + amount_from = float(format_amount(amt_swap, 8)) + js_1 = json.loads(urlopen('http://127.0.0.1:1801/json/wallets/part').read()) + node1_blind_after = js_1['blind_balance'] + js_1['blind_unconfirmed'] + assert(node1_blind_after > node1_blind_before + (amount_from - 0.05)) + + js_0 = json.loads(urlopen('http://127.0.0.1:1800/json/wallets/part').read()) + node0_blind_after = js_0['blind_balance'] + js_0['blind_unconfirmed'] + assert(node0_blind_after < node0_blind_before - amount_from) + + js_0_xmr_after = json.loads(urlopen('http://127.0.0.1:1800/json/wallets/xmr').read()) + js_1_xmr_after = json.loads(urlopen('http://127.0.0.1:1801/json/wallets/xmr').read()) + + scale_from = 8 + amount_to = int((amt_swap * rate_swap) // (10 ** scale_from)) + amount_to_float = float(format_amount(amount_to, 12)) + node1_xmr_after = float(js_1_xmr_after['unconfirmed']) + float(js_1_xmr_after['balance']) + node1_xmr_before = float(js_1_xmr['unconfirmed']) + float(js_1_xmr['balance']) + assert(node1_xmr_after > node1_xmr_before + (amount_to_float - 0.02)) + + def test_02_leader_recover_a_lock_tx(self): + logging.info('---------- Test PARTct to XMR leader recovers coin a lock tx') + swap_clients = self.swap_clients + + js_w0_before = json.loads(urlopen('http://127.0.0.1:1800/json/wallets').read()) + node0_blind_before = self.getBalance(js_w0_before) + + amt_swap = make_int(random.uniform(0.1, 2.0), scale=8, r=1) + rate_swap = make_int(random.uniform(0.2, 20.0), scale=12, r=1) + offer_id = swap_clients[0].postOffer( + Coins.PART_BLIND, Coins.XMR, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP, + lock_type=SEQUENCE_LOCK_BLOCKS, lock_value=12) + wait_for_offer(test_delay_event, swap_clients[1], offer_id) + offer = swap_clients[1].getOffer(offer_id) + + bid_id = swap_clients[1].postXmrBid(offer_id, offer.amount_from) + + wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.BID_RECEIVED) + + bid, xmr_swap = swap_clients[0].getXmrBid(bid_id) + assert(xmr_swap) + + swap_clients[1].setBidDebugInd(bid_id, DebugTypes.BID_STOP_AFTER_COIN_A_LOCK) + + swap_clients[0].acceptXmrBid(bid_id) + + wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, wait_for=180) + wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, sent=True) + + js_w0_after = json.loads(urlopen('http://127.0.0.1:1800/json/wallets').read()) + node0_blind_after = self.getBalance(js_w0_after) + assert(node0_blind_before - node0_blind_after < 0.02) + + def test_03_follower_recover_a_lock_tx(self): + logging.info('---------- Test PARTct to XMR follower recovers coin a lock tx') + swap_clients = self.swap_clients + + js_w0_before = json.loads(urlopen('http://127.0.0.1:1800/json/wallets').read()) + js_w1_before = json.loads(urlopen('http://127.0.0.1:1801/json/wallets').read()) + + amt_swap = make_int(random.uniform(0.1, 2.0), scale=8, r=1) + rate_swap = make_int(random.uniform(0.2, 20.0), scale=12, r=1) + offer_id = swap_clients[0].postOffer( + Coins.PART_BLIND, Coins.XMR, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP, + lock_type=SEQUENCE_LOCK_BLOCKS, lock_value=12) + wait_for_offer(test_delay_event, swap_clients[1], offer_id) + offer = swap_clients[1].getOffer(offer_id) + + bid_id = swap_clients[1].postXmrBid(offer_id, offer.amount_from) + + wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.BID_RECEIVED) + + bid, xmr_swap = swap_clients[0].getXmrBid(bid_id) + assert(xmr_swap) + + swap_clients[1].setBidDebugInd(bid_id, DebugTypes.BID_STOP_AFTER_COIN_A_LOCK) + swap_clients[0].setBidDebugInd(bid_id, DebugTypes.BID_DONT_SPEND_COIN_A_LOCK_REFUND) + + swap_clients[0].acceptXmrBid(bid_id) + + wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.BID_STALLED_FOR_TEST, wait_for=180) + wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.XMR_SWAP_FAILED_SWIPED, wait_for=80, sent=True) + + js_w1_after = json.loads(urlopen('http://127.0.0.1:1801/json/wallets').read()) + + node1_blind_before = self.getBalance(js_w1_before) + node1_blind_after = self.getBalance(js_w1_after) + amount_from = float(format_amount(amt_swap, 8)) + assert(node1_blind_after - node1_blind_before > (amount_from - 0.02)) + + wait_for_none_active(test_delay_event, 1800) + wait_for_none_active(test_delay_event, 1801) + + def test_04_follower_recover_b_lock_tx(self): + logging.info('---------- Test PARTct to XMR follower recovers coin b lock tx') + + swap_clients = self.swap_clients + + js_w0_before = json.loads(urlopen('http://127.0.0.1:1800/json/wallets').read()) + js_w1_before = json.loads(urlopen('http://127.0.0.1:1801/json/wallets').read()) + + amt_swap = make_int(random.uniform(0.1, 2.0), scale=8, r=1) + rate_swap = make_int(random.uniform(0.2, 20.0), scale=12, r=1) + offer_id = swap_clients[0].postOffer( + Coins.PART_BLIND, Coins.XMR, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP, + lock_type=SEQUENCE_LOCK_BLOCKS, lock_value=18) + wait_for_offer(test_delay_event, swap_clients[1], offer_id) + offer = swap_clients[1].getOffer(offer_id) + + bid_id = swap_clients[1].postXmrBid(offer_id, offer.amount_from) + + wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.BID_RECEIVED) + + bid, xmr_swap = swap_clients[0].getXmrBid(bid_id) + assert(xmr_swap) + + swap_clients[1].setBidDebugInd(bid_id, DebugTypes.CREATE_INVALID_COIN_B_LOCK) + + swap_clients[0].acceptXmrBid(bid_id) + + wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, wait_for=180) + wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, sent=True) + + js_w0_after = json.loads(urlopen('http://127.0.0.1:1800/json/wallets').read()) + js_w1_after = json.loads(urlopen('http://127.0.0.1:1801/json/wallets').read()) + + node0_blind_before = self.getBalance(js_w0_before) + node0_blind_after = self.getBalance(js_w0_after) + assert(node0_blind_before - node0_blind_after < 0.02) + + node1_xmr_before = self.getXmrBalance(js_w1_before) + node1_xmr_after = self.getXmrBalance(js_w1_after) + assert(node1_xmr_before - node1_xmr_after < 0.02) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/basicswap/test_xmr.py b/tests/basicswap/test_xmr.py index fc2cf76..ebe39aa 100644 --- a/tests/basicswap/test_xmr.py +++ b/tests/basicswap/test_xmr.py @@ -70,6 +70,8 @@ from tests.basicswap.common import ( BASE_ZMQ_PORT, BTC_BASE_PORT, BTC_BASE_RPC_PORT, + LTC_BASE_PORT, + LTC_BASE_RPC_PORT, PREFIX_SECRET_KEY_REGTEST, ) from bin.basicswap_run import startDaemon, startXmrDaemon @@ -80,6 +82,7 @@ logger = logging.getLogger() NUM_NODES = 3 NUM_XMR_NODES = 3 NUM_BTC_NODES = 3 +NUM_LTC_NODES = 3 TEST_DIR = cfg.TEST_DATADIRS XMR_BASE_P2P_PORT = 17792 @@ -139,7 +142,7 @@ def startXmrWalletRPC(node_dir, bin_dir, wallet_bin, node_id, opts=[]): return subprocess.Popen(args, stdin=subprocess.PIPE, stdout=wallet_stdout, stderr=wallet_stderr, cwd=data_dir) -def prepare_swapclient_dir(datadir, node_id, network_key, network_pubkey): +def prepare_swapclient_dir(datadir, node_id, network_key, network_pubkey, with_ltc=False): basicswap_dir = os.path.join(datadir, 'basicswap_' + str(node_id)) if not os.path.exists(basicswap_dir): os.makedirs(basicswap_dir) @@ -198,6 +201,18 @@ def prepare_swapclient_dir(datadir, node_id, network_key, network_pubkey): 'max_delay_retry': 10 } + if with_ltc: + settings['chainclients']['litecoin'] = { + 'connection_type': 'rpc', + 'manage_daemon': False, + 'rpcport': LTC_BASE_RPC_PORT + node_id, + 'rpcuser': 'test' + str(node_id), + 'rpcpassword': 'test_pass' + str(node_id), + 'datadir': os.path.join(datadir, 'ltc_' + str(node_id)), + 'bindir': cfg.LITECOIN_BINDIR, + 'use_segwit': True, + } + with open(settings_path, 'w') as fp: json.dump(settings, fp, indent=4) @@ -206,6 +221,10 @@ def btcRpc(cmd, node_id=0): return callrpc_cli(cfg.BITCOIN_BINDIR, os.path.join(TEST_DIR, 'btc_' + str(node_id)), 'regtest', cmd, cfg.BITCOIN_CLI) +def ltcRpc(cmd, node_id=0): + return callrpc_cli(cfg.LITECOIN_BINDIR, os.path.join(TEST_DIR, 'ltc_' + str(node_id)), 'regtest', cmd, cfg.LITECOIN_CLI) + + def signal_handler(sig, frame): logging.info('signal {} detected.'.format(sig)) test_delay_event.set() @@ -249,6 +268,8 @@ def run_coins_loop(cls): try: if cls.btc_addr is not None: btcRpc('generatetoaddress 1 {}'.format(cls.btc_addr)) + if cls.ltc_addr is not None: + ltcRpc('generatetoaddress 1 {}'.format(cls.ltc_addr)) if cls.xmr_addr is not None: callrpc_xmr_na(XMR_BASE_RPC_PORT + 1, 'generateblocks', {'wallet_address': cls.xmr_addr, 'amount_of_blocks': 1}) except Exception as e: @@ -263,12 +284,13 @@ def run_loop(cls): test_delay_event.wait(1.0) -class Test(unittest.TestCase): +class BaseTest(unittest.TestCase): + __test__ = False @classmethod def setUpClass(cls): - super(Test, cls).setUpClass() - + if not hasattr(cls, 'start_ltc_nodes'): + cls.start_ltc_nodes = False random.seed(time.time()) cls.update_thread = None @@ -277,11 +299,13 @@ class Test(unittest.TestCase): cls.swap_clients = [] cls.part_daemons = [] cls.btc_daemons = [] + cls.ltc_daemons = [] cls.xmr_daemons = [] cls.xmr_wallet_auth = [] cls.xmr_addr = None cls.btc_addr = None + cls.ltc_addr = None logger.propagate = False logger.handlers = [] @@ -336,6 +360,17 @@ class Test(unittest.TestCase): waitForRPC(make_rpc_func(i, base_rpc_port=BTC_BASE_RPC_PORT)) + if cls.start_ltc_nodes: + for i in range(NUM_LTC_NODES): + data_dir = prepareDataDir(TEST_DIR, i, 'litecoin.conf', 'ltc_', base_p2p_port=LTC_BASE_PORT, base_rpc_port=LTC_BASE_RPC_PORT) + if os.path.exists(os.path.join(cfg.LITECOIN_BINDIR, 'litecoin-wallet')): + callrpc_cli(cfg.LITECOIN_BINDIR, data_dir, 'regtest', '-wallet=wallet.dat create', 'litecoin-wallet') + + cls.ltc_daemons.append(startDaemon(os.path.join(TEST_DIR, 'ltc_' + str(i)), cfg.LITECOIN_BINDIR, cfg.LITECOIND)) + logging.info('Started %s %d', cfg.LITECOIND, cls.part_daemons[-1].pid) + + waitForRPC(make_rpc_func(i, base_rpc_port=LTC_BASE_RPC_PORT)) + for i in range(NUM_XMR_NODES): prepareXmrDataDir(TEST_DIR, i, 'monerod.conf') @@ -361,7 +396,7 @@ class Test(unittest.TestCase): cls.network_pubkey = eckey.get_pubkey().get_bytes().hex() for i in range(NUM_NODES): - prepare_swapclient_dir(TEST_DIR, i, cls.network_key, cls.network_pubkey) + prepare_swapclient_dir(TEST_DIR, i, cls.network_key, cls.network_pubkey, cls.start_ltc_nodes) basicswap_dir = os.path.join(os.path.join(TEST_DIR, 'basicswap_' + str(i))) settings_path = os.path.join(basicswap_dir, cfg.CONFIG_FILENAME) with open(settings_path) as fs: @@ -370,6 +405,10 @@ class Test(unittest.TestCase): sc = BasicSwap(fp, basicswap_dir, settings, 'regtest', log_name='BasicSwap{}'.format(i)) sc.setDaemonPID(Coins.BTC, cls.btc_daemons[i].pid) sc.setDaemonPID(Coins.PART, cls.part_daemons[i].pid) + + if cls.start_ltc_nodes: + sc.setDaemonPID(Coins.LTC, cls.ltc_daemons[i].pid) + sc.start() # Set XMR main wallet address xmr_ci = sc.ci(Coins.XMR) @@ -389,6 +428,12 @@ class Test(unittest.TestCase): checkForks(callnoderpc(0, 'getblockchaininfo', base_rpc_port=BTC_BASE_RPC_PORT)) + if cls.start_ltc_nodes: + cls.ltc_addr = callnoderpc(0, 'getnewaddress', ['mining_addr', 'bech32'], base_rpc_port=LTC_BASE_RPC_PORT) + logging.info('Mining %d Litecoin blocks to %s', num_blocks, cls.ltc_addr) + callnoderpc(0, 'generatetoaddress', [num_blocks, cls.ltc_addr], base_rpc_port=LTC_BASE_RPC_PORT) + checkForks(callnoderpc(0, 'getblockchaininfo', base_rpc_port=LTC_BASE_RPC_PORT)) + num_blocks = 100 if callrpc_xmr_na(XMR_BASE_RPC_PORT + 1, 'get_block_count')['count'] < num_blocks: logging.info('Mining %d Monero blocks to %s.', num_blocks, cls.xmr_addr) @@ -441,12 +486,17 @@ class Test(unittest.TestCase): stopDaemons(cls.xmr_daemons) stopDaemons(cls.part_daemons) stopDaemons(cls.btc_daemons) + stopDaemons(cls.ltc_daemons) - super(Test, cls).tearDownClass() + super(BaseTest, cls).tearDownClass() def callxmrnodewallet(self, node_id, method, params=None): return callrpc_xmr(XMR_BASE_WALLET_RPC_PORT + node_id, self.xmr_wallet_auth[node_id], method, params) + +class Test(BaseTest): + __test__ = True + def test_01_part_xmr(self): logging.info('---------- Test PART to XMR') swap_clients = self.swap_clients @@ -601,8 +651,6 @@ class Test(unittest.TestCase): wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, sent=True) js_w0_after = json.loads(urlopen('http://127.0.0.1:1800/json/wallets').read()) - print('[rm] js_w0_before', json.dumps(js_w0_before)) - print('[rm] js_w0_after', json.dumps(js_w0_after)) def test_03_follower_recover_a_lock_tx(self): logging.info('---------- Test PART to XMR follower recovers coin a lock tx') @@ -740,9 +788,6 @@ class Test(unittest.TestCase): js_w0_after = json.loads(urlopen('http://127.0.0.1:1800/json/wallets').read()) js_w1_after = json.loads(urlopen('http://127.0.0.1:1801/json/wallets').read()) - logging.info('[rm] js_w0_after {}'.format(json.dumps(js_w0_after, indent=4))) - logging.info('[rm] js_w1_after {}'.format(json.dumps(js_w1_after, indent=4))) - assert(make_int(js_w1_after['2']['balance'], scale=8, r=1) - (make_int(js_w1_before['2']['balance'], scale=8, r=1) + amt_1) < 1000) def test_07_revoke_offer(self): @@ -875,10 +920,58 @@ class Test(unittest.TestCase): assert(js_1['anon_balance'] < node1_anon_before - amount_to) js_0 = json.loads(urlopen('http://127.0.0.1:1800/json/wallets/part').read()) - assert(js_0['anon_balance'] + js_0['anon_pending'] > node0_anon_before + (amount_to - 0.1)) + assert(js_0['anon_balance'] + js_0['anon_pending'] > node0_anon_before + (amount_to - 0.05)) def test_12_particl_blind(self): - return # TODO + logging.info('---------- Test Particl blind transactions') + swap_clients = self.swap_clients + + js_0 = json.loads(urlopen('http://127.0.0.1:1800/json/wallets/part').read()) + node0_blind_before = js_0['blind_balance'] + js_0['blind_unconfirmed'] + + wait_for_balance(test_delay_event, 'http://127.0.0.1:1801/json/wallets/part', 'balance', 200.0) + js_1 = json.loads(urlopen('http://127.0.0.1:1801/json/wallets/part').read()) + assert(float(js_1['balance']) > 200.0) + node1_blind_before = js_1['blind_balance'] + js_1['blind_unconfirmed'] + + post_json = { + 'value': 100, + 'address': js_0['stealth_address'], + 'subfee': False, + 'type_to': 'blind', + } + json_rv = json.loads(post_json_req('http://127.0.0.1:1800/json/wallets/part/withdraw', post_json)) + assert(len(json_rv['txid']) == 64) + + logging.info('Waiting for blind balance') + wait_for_balance(test_delay_event, 'http://127.0.0.1:1800/json/wallets/part', 'blind_balance', 100.0 + node0_blind_before) + js_0 = json.loads(urlopen('http://127.0.0.1:1800/json/wallets/part').read()) + node0_blind_before = js_0['blind_balance'] + js_0['blind_unconfirmed'] + + amt_swap = make_int(random.uniform(0.1, 2.0), scale=8, r=1) + rate_swap = make_int(random.uniform(2.0, 20.0), scale=8, r=1) + offer_id = swap_clients[0].postOffer(Coins.PART_BLIND, Coins.XMR, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP) + wait_for_offer(test_delay_event, swap_clients[1], offer_id) + offers = swap_clients[0].listOffers(filters={'offer_id': offer_id}) + offer = offers[0] + + bid_id = swap_clients[1].postXmrBid(offer_id, offer.amount_from) + + wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.BID_RECEIVED) + + swap_clients[0].acceptXmrBid(bid_id) + + wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=180) + wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True) + + amount_from = float(format_amount(amt_swap, 8)) + js_1 = json.loads(urlopen('http://127.0.0.1:1801/json/wallets/part').read()) + node1_blind_after = js_1['blind_balance'] + js_1['blind_unconfirmed'] + assert(node1_blind_after > node1_blind_before + (amount_from - 0.05)) + + js_0 = json.loads(urlopen('http://127.0.0.1:1800/json/wallets/part').read()) + node0_blind_after = js_0['blind_balance'] + js_0['blind_unconfirmed'] + assert(node0_blind_after < node0_blind_before - amount_from) def test_98_withdraw_all(self): logging.info('---------- Test XMR withdrawal all')