diff --git a/basicswap/interface/btc.py b/basicswap/interface/btc.py index c6dd127..46f08dd 100644 --- a/basicswap/interface/btc.py +++ b/basicswap/interface/btc.py @@ -155,9 +155,13 @@ class BTCInterface(CoinInterface): return abs(a - b) < 20 @staticmethod - def xmr_swap_alock_spend_tx_vsize() -> int: + def xmr_swap_a_lock_spend_tx_vsize() -> int: return 147 + @staticmethod + def xmr_swap_b_lock_spend_tx_vsize() -> int: + return 110 + @staticmethod def txoType(): return CTxOut @@ -986,14 +990,14 @@ class BTCInterface(CoinInterface): def scanTxOutset(self, dest): return self.rpc_callback('scantxoutset', ['start', ['raw({})'.format(dest.hex())]]) - def getTransaction(self, txid): + def getTransaction(self, txid: bytes): try: return bytes.fromhex(self.rpc_callback('getrawtransaction', [txid.hex()])) except Exception as ex: # TODO: filter errors return None - def getWalletTransaction(self, txid): + def getWalletTransaction(self, txid: bytes): try: return bytes.fromhex(self.rpc_callback('gettransaction', [txid.hex()])) except Exception as ex: @@ -1460,7 +1464,7 @@ class BTCInterface(CoinInterface): tx.rehash() return tx.serialize().hex() - def ensureFunds(self, amount): + def ensureFunds(self, amount: int) -> None: if self.getSpendableBalance() < amount: raise ValueError('Balance too low') diff --git a/basicswap/interface/firo.py b/basicswap/interface/firo.py index b725c43..2301ed8 100644 --- a/basicswap/interface/firo.py +++ b/basicswap/interface/firo.py @@ -119,7 +119,7 @@ class FIROInterface(BTCInterface): return rv - def createSCLockTx(self, value: int, script: bytearray, vkbv=None) -> bytes: + def createSCLockTx(self, value: int, script: bytearray, vkbv: bytes = None) -> bytes: tx = CTransaction() tx.nVersion = self.txVersion() tx.vout.append(self.txoType()(value, self.getScriptDest(script))) diff --git a/basicswap/interface/part.py b/basicswap/interface/part.py index 65bb947..82f16f2 100644 --- a/basicswap/interface/part.py +++ b/basicswap/interface/part.py @@ -59,8 +59,12 @@ class PARTInterface(BTCInterface): return 0xa0 @staticmethod - def xmr_swap_alock_spend_tx_vsize() -> int: - return 213 + def xmr_swap_a_lock_spend_tx_vsize() -> int: + return 200 + + @staticmethod + def xmr_swap_b_lock_spend_tx_vsize() -> int: + return 138 @staticmethod def txoType(): @@ -111,8 +115,8 @@ class PARTInterface(BTCInterface): return encodeStealthAddress(prefix_byte, scan_pubkey, spend_pubkey) - def getWitnessStackSerialisedLength(self, witness_stack): - length = getCompactSizeLen(len(witness_stack)) + def getWitnessStackSerialisedLength(self, witness_stack) -> int: + length: int = getCompactSizeLen(len(witness_stack)) for e in witness_stack: length += getWitnessElementLen(len(e)) return length @@ -138,7 +142,15 @@ class PARTInterfaceBlind(PARTInterface): def balance_type(): return BalanceTypes.BLIND - def coin_name(self): + @staticmethod + def xmr_swap_a_lock_spend_tx_vsize() -> int: + return 1032 + + @staticmethod + def xmr_swap_b_lock_spend_tx_vsize() -> int: + return 980 + + def coin_name(self) -> str: return super().coin_name() + ' Blind' def getScriptLockTxNonce(self, data): @@ -167,7 +179,7 @@ class PARTInterfaceBlind(PARTInterface): ensure(v['result'] is True, 'verifycommitment failed') return output_n, blinded_info - def createSCLockTx(self, value: int, script: bytearray, vkbv) -> bytes: + def createSCLockTx(self, value: int, script: bytearray, vkbv: bytes) -> bytes: # Nonce is derived from vkbv, ephemeral_key isn't used ephemeral_key = self.getNewSecretKey() @@ -183,7 +195,7 @@ class PARTInterfaceBlind(PARTInterface): tx_bytes = bytes.fromhex(rv['hex']) return tx_bytes - def fundSCLockTx(self, tx_bytes, feerate, vkbv): + def fundSCLockTx(self, tx_bytes: bytes, feerate: int, vkbv: bytes) -> bytes: feerate_str = self.format_amount(feerate) # TODO: unlock unspents if bid cancelled @@ -462,7 +474,7 @@ class PARTInterfaceBlind(PARTInterface): ensure(output_n is not None, 'Output not found in tx') return output_n - def createSCLockSpendTx(self, tx_lock_bytes, script_lock, pk_dest, tx_fee_rate, vkbv): + def createSCLockSpendTx(self, tx_lock_bytes: bytes, script_lock: bytes, pk_dest: bytes, tx_fee_rate: int, vkbv: bytes, fee_info={}) -> bytes: lock_tx_obj = self.rpc_callback('decoderawtransaction', [tx_lock_bytes.hex()]) lock_txid_hex = lock_tx_obj['txid'] @@ -499,13 +511,20 @@ class PARTInterfaceBlind(PARTInterface): 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']) + + # lock_spend_tx_hex does not include the dummy witness stack + witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack) + vsize = self.getTxVSize(self.loadTx(bytes.fromhex(lock_spend_tx_hex)), add_witness_bytes=witness_bytes) actual_tx_fee_rate = pay_fee * 1000 // vsize self._log.info('createSCLockSpendTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.', lock_spend_tx_obj['txid'], actual_tx_fee_rate, vsize, pay_fee) + fee_info['vsize'] = vsize + fee_info['fee_paid'] = pay_fee + fee_info['rate_input'] = tx_fee_rate + fee_info['rate_actual'] = actual_tx_fee_rate + return bytes.fromhex(lock_spend_tx_hex) def verifySCLockSpendTx(self, tx_bytes, @@ -629,7 +648,7 @@ class PARTInterfaceBlind(PARTInterface): def getSpendableBalance(self) -> int: return self.make_int(self.rpc_callback('getbalances')['mine']['blind_trusted']) - def publishBLockTx(self, vkbv, Kbs, output_amount, feerate, delay_for: int = 10, unlock_time: int = 0) -> bytes: + def publishBLockTx(self, vkbv: bytes, Kbs: bytes, output_amount: int, feerate: int, delay_for: int = 10, unlock_time: int = 0) -> bytes: Kbv = self.getPubkey(vkbv) sx_addr = self.formatStealthAddress(Kbv, Kbs) self._log.debug('sx_addr: {}'.format(sx_addr)) @@ -751,14 +770,23 @@ class PARTInterfaceAnon(PARTInterface): def balance_type(): return BalanceTypes.ANON + @staticmethod + def xmr_swap_a_lock_spend_tx_vsize() -> int: + raise ValueError('Not possible') + + @staticmethod + def xmr_swap_b_lock_spend_tx_vsize() -> int: + # TODO: Estimate with ringsize + return 1153 + @staticmethod def depth_spendable() -> int: return 12 - def coin_name(self): + def coin_name(self) -> str: return super().coin_name() + ' Anon' - def publishBLockTx(self, kbv, Kbs, output_amount, feerate, delay_for: int = 10, unlock_time: int = 0) -> bytes: + def publishBLockTx(self, kbv: bytes, Kbs: bytes, output_amount: int, feerate: int, delay_for: int = 10, unlock_time: int = 0) -> bytes: Kbv = self.getPubkey(kbv) sx_addr = self.formatStealthAddress(Kbv, Kbs) diff --git a/basicswap/interface/xmr.py b/basicswap/interface/xmr.py index 3f29cbb..be2e23b 100644 --- a/basicswap/interface/xmr.py +++ b/basicswap/interface/xmr.py @@ -69,6 +69,15 @@ class XMRInterface(CoinInterface): def depth_spendable() -> int: return 10 + @staticmethod + def xmr_swap_a_lock_spend_tx_vsize() -> int: + raise ValueError('Not possible') + + @staticmethod + def xmr_swap_b_lock_spend_tx_vsize() -> int: + # TODO: Estimate with ringsize + return 1507 + def __init__(self, coin_settings, network, swap_client=None): super().__init__(network) daemon_login = None @@ -266,7 +275,7 @@ class XMRInterface(CoinInterface): def encodeSharedAddress(self, Kbv: bytes, Kbs: bytes) -> str: return xmr_util.encode_address(Kbv, Kbs) - def publishBLockTx(self, kbv, Kbs, output_amount, feerate, delay_for: int = 10, unlock_time: int = 0) -> bytes: + def publishBLockTx(self, kbv: bytes, Kbs: bytes, output_amount: int, feerate: int, delay_for: int = 10, unlock_time: int = 0) -> bytes: with self._mx_wallet: self.openWallet(self._wallet_filename) self.rpc_wallet_cb('refresh') @@ -368,7 +377,7 @@ class XMRInterface(CoinInterface): return None - def spendBLockTx(self, chain_b_lock_txid, address_to, kbv, kbs, cb_swap_value, b_fee_rate, restore_height, spend_actual_balance=False): + def spendBLockTx(self, chain_b_lock_txid: bytes, address_to: str, kbv: bytes, kbs: bytes, cb_swap_value: int, b_fee_rate: int, restore_height: int, spend_actual_balance: bool = False) -> bytes: ''' Notes: "Error: No unlocked balance in the specified subaddress(es)" can mean not enough funds after tx fee. @@ -520,6 +529,9 @@ class XMRInterface(CoinInterface): # TODO return True - def ensureFunds(self, amount): + def ensureFunds(self, amount: int) -> None: if self.getSpendableBalance() < amount: raise ValueError('Balance too low') + + def getTransaction(self, txid: bytes): + return self.rpc_cb2('get_transactions', {'txs_hashes': [txid.hex(), ]}) diff --git a/basicswap/js_server.py b/basicswap/js_server.py index 5f133fd..9a28502 100644 --- a/basicswap/js_server.py +++ b/basicswap/js_server.py @@ -203,6 +203,18 @@ def js_offers(self, url_split, post_string, is_json, sent=False) -> bytes: if with_extra_info: offer_data['amount_negotiable'] = o.amount_negotiable offer_data['rate_negotiable'] = o.rate_negotiable + + if o.swap_type == SwapTypes.XMR_SWAP: + _, xmr_offer = swap_client.getXmrOffer(o.offer_id) + offer_data['lock_time_1'] = xmr_offer.lock_time_1 + offer_data['lock_time_2'] = xmr_offer.lock_time_2 + + offer_data['feerate_from'] = xmr_offer.a_fee_rate + offer_data['feerate_to'] = xmr_offer.b_fee_rate + else: + offer_data['feerate_from'] = o.from_feerate + offer_data['feerate_to'] = o.to_feerate + rv.append(offer_data) return bytes(json.dumps(rv), 'UTF-8') diff --git a/basicswap/rpc_xmr.py b/basicswap/rpc_xmr.py index 483b703..69a9d61 100644 --- a/basicswap/rpc_xmr.py +++ b/basicswap/rpc_xmr.py @@ -37,12 +37,15 @@ class JsonrpcDigest(): self.__verbose = verbose self.__allow_none = allow_none - self.__request_id = 1 + self.__request_id = 0 def close(self): if self.__transport is not None: self.__transport.close() + def request_id(self): + return self.__request_id + def post_request(self, method, params, timeout=None): try: connection = self.__transport.make_connection(self.__host) @@ -66,7 +69,7 @@ class JsonrpcDigest(): self.__transport.close() raise - def json_request(self, method, params, username='', password='', timeout=None): + def json_request(self, request_body, username='', password='', timeout=None): try: connection = self.__transport.make_connection(self.__host) if timeout: @@ -74,18 +77,11 @@ class JsonrpcDigest(): headers = self.__transport._extra_headers[:] - request_body = { - 'method': method, - 'params': params, - 'jsonrpc': '2.0', - 'id': self.__request_id - } - connection.putrequest('POST', self.__handler) headers.append(('Content-Type', 'application/json')) headers.append(('Connection', 'keep-alive')) self.__transport.send_headers(connection, headers) - self.__transport.send_content(connection, json.dumps(request_body, default=jsonDecimal).encode('utf-8')) + self.__transport.send_content(connection, json.dumps(request_body, default=jsonDecimal).encode('utf-8') if request_body else '') resp = connection.getresponse() if resp.status == 401: @@ -135,18 +131,11 @@ class JsonrpcDigest(): headers = self.__transport._extra_headers[:] headers.append(('Authorization', header_value)) - request_body = { - 'method': method, - 'params': params, - 'jsonrpc': '2.0', - 'id': self.__request_id - } - connection.putrequest('POST', self.__handler) headers.append(('Content-Type', 'application/json')) headers.append(('Connection', 'keep-alive')) self.__transport.send_headers(connection, headers) - self.__transport.send_content(connection, json.dumps(request_body, default=jsonDecimal).encode('utf-8')) + self.__transport.send_content(connection, json.dumps(request_body, default=jsonDecimal).encode('utf-8') if request_body else '') resp = connection.getresponse() self.__request_id += 1 @@ -168,10 +157,16 @@ def callrpc_xmr(rpc_port, method, params=[], rpc_host='127.0.0.1', path='json_rp url = 'http://{}:{}/{}'.format(rpc_host, rpc_port, path) x = JsonrpcDigest(url) + request_body = { + 'method': method, + 'params': params, + 'jsonrpc': '2.0', + 'id': x.request_id() + } if auth: - v = x.json_request(method, params, username=auth[0], password=auth[1], timeout=timeout) + v = x.json_request(request_body, username=auth[0], password=auth[1], timeout=timeout) else: - v = x.json_request(method, params, timeout=timeout) + v = x.json_request(request_body, timeout=timeout) x.close() r = json.loads(v.decode('utf-8')) except Exception as ex: @@ -183,7 +178,7 @@ def callrpc_xmr(rpc_port, method, params=[], rpc_host='127.0.0.1', path='json_rp return r['result'] -def callrpc_xmr2(rpc_port, method, params=None, auth=None, rpc_host='127.0.0.1', timeout=120): +def callrpc_xmr2(rpc_port: int, method: str, params=None, auth=None, rpc_host='127.0.0.1', timeout=120): try: if rpc_host.count('://') > 0: url = '{}:{}/{}'.format(rpc_host, rpc_port, method) @@ -192,9 +187,9 @@ def callrpc_xmr2(rpc_port, method, params=None, auth=None, rpc_host='127.0.0.1', x = JsonrpcDigest(url) if auth: - v = x.json_request(method, params, username=auth[0], password=auth[1], timeout=timeout) + v = x.json_request(params, username=auth[0], password=auth[1], timeout=timeout) else: - v = x.json_request(method, params, timeout=timeout) + v = x.json_request(params, timeout=timeout) x.close() r = json.loads(v.decode('utf-8')) except Exception as ex: diff --git a/basicswap/templates/offer.html b/basicswap/templates/offer.html index 67dce52..155496e 100644 --- a/basicswap/templates/offer.html +++ b/basicswap/templates/offer.html @@ -420,7 +420,7 @@ Amount you will get {{ data.amt_from }} {{ data.tla_from }} - {% if data.xmr_type == true %} (excluding {{ data.amt_from_lock_spend_tx_fee }} {{ data.tla_from }} in tx fees). + {% if data.xmr_type == true %} (excluding estimated {{ data.amt_from_lock_spend_tx_fee }} {{ data.tla_from }} in tx fees). {% else %} (excluding a tx fee). {% endif %} Amount you will send {{ data.amt_to }} {{ data.tla_to }} diff --git a/basicswap/ui/page_offers.py b/basicswap/ui/page_offers.py index 7ceb787..2dbfac0 100644 --- a/basicswap/ui/page_offers.py +++ b/basicswap/ui/page_offers.py @@ -231,12 +231,12 @@ def parseOfferFormData(swap_client, form_data, page_data, options={}): page_data['from_fee_override'] = ci_from.format_amount(ci_from.make_int(from_fee_override, r=1)) parsed_data['from_fee_override'] = page_data['from_fee_override'] - lock_spend_tx_vsize = ci_leader.xmr_swap_alock_spend_tx_vsize() - lock_spend_tx_fee = ci_leader.make_int(ci_from.make_int(from_fee_override, r=1) * lock_spend_tx_vsize / 1000, r=1) - page_data['amt_from_lock_spend_tx_fee'] = ci_from.format_amount(lock_spend_tx_fee // ci_leader.COIN()) - page_data['tla_from'] = ci_leader.ticker() + lock_spend_tx_vsize = ci_from.xmr_swap_b_lock_spend_tx_vsize() if reverse_bid else ci_from.xmr_swap_a_lock_spend_tx_vsize() + lock_spend_tx_fee = ci_from.make_int(ci_from.make_int(from_fee_override, r=1) * lock_spend_tx_vsize / 1000, r=1) + page_data['amt_from_lock_spend_tx_fee'] = ci_from.format_amount(lock_spend_tx_fee // ci_from.COIN()) + page_data['tla_from'] = ci_from.ticker() - if ci_follower == Coins.XMR: + if ci_to == Coins.XMR: if have_data_entry(form_data, 'fee_rate_to'): page_data['to_fee_override'] = get_data_entry(form_data, 'fee_rate_to') parsed_data['to_fee_override'] = page_data['to_fee_override'] @@ -244,7 +244,7 @@ def parseOfferFormData(swap_client, form_data, page_data, options={}): to_fee_override, page_data['to_fee_src'] = swap_client.getFeeRateForCoin(parsed_data['coin_to'], page_data['fee_to_conf']) if page_data['fee_to_extra'] > 0: to_fee_override += to_fee_override * (float(page_data['fee_to_extra']) / 100.0) - page_data['to_fee_override'] = ci_follower.format_amount(ci_follower.make_int(to_fee_override, r=1)) + page_data['to_fee_override'] = ci_to.format_amount(ci_to.make_int(to_fee_override, r=1)) parsed_data['to_fee_override'] = page_data['to_fee_override'] except Exception as e: print('Error setting fee', str(e)) # Expected if missing fields @@ -612,14 +612,15 @@ def page_offer(self, url_split, post_string): int_fee_rate_now, fee_source = ci_leader.get_fee_rate() data['xmr_type'] = True - data['a_fee_rate'] = ci_leader.format_amount(xmr_offer.a_fee_rate) + data['a_fee_rate'] = ci_from.format_amount(xmr_offer.a_fee_rate) data['a_fee_rate_verify'] = ci_leader.format_amount(int_fee_rate_now, conv_int=True) data['a_fee_rate_verify_src'] = fee_source data['a_fee_warn'] = xmr_offer.a_fee_rate < int_fee_rate_now - lock_spend_tx_vsize = ci_leader.xmr_swap_alock_spend_tx_vsize() - lock_spend_tx_fee = ci_leader.make_int(xmr_offer.a_fee_rate * lock_spend_tx_vsize / 1000, r=1) - data['amt_from_lock_spend_tx_fee'] = ci_leader.format_amount(lock_spend_tx_fee // ci_leader.COIN()) + from_fee_rate = xmr_offer.b_fee_rate if reverse_bid else xmr_offer.a_fee_rate + lock_spend_tx_vsize = ci_from.xmr_swap_b_lock_spend_tx_vsize() if reverse_bid else ci_from.xmr_swap_a_lock_spend_tx_vsize() + lock_spend_tx_fee = ci_from.make_int(from_fee_rate * lock_spend_tx_vsize / 1000, r=1) + data['amt_from_lock_spend_tx_fee'] = ci_from.format_amount(lock_spend_tx_fee // ci_from.COIN()) if offer.was_sent: try: @@ -633,7 +634,9 @@ def page_offer(self, url_split, post_string): formatted_bids = [] amt_swapped = 0 for b in bids: - amt_swapped += b[4] + amount_from = b[4] + rate = b[10] + amt_swapped += amount_from formatted_bids.append((b[2].hex(), ci_from.format_amount(amount_from), strBidState(b[5]), ci_to.format_amount(rate), b[11])) data['amt_swapped'] = ci_from.format_amount(amt_swapped) diff --git a/doc/install.md b/doc/install.md index 615d4ff..f802eac 100644 --- a/doc/install.md +++ b/doc/install.md @@ -69,6 +69,7 @@ To instead use Monero public nodes and not run a local Monero daemon
(it can **Record the mnemonic from the output of the above command.** +**Mnemonics should be stored encrypted and/or air-gapped.** And the output of `echo $CURRENT_XMR_HEIGHT` for use if you need to later restore your wallet. #### Make COINDATA_PATH permanent (optional): diff --git a/tests/basicswap/test_btc_xmr.py b/tests/basicswap/test_btc_xmr.py index c84eec8..d3798de 100644 --- a/tests/basicswap/test_btc_xmr.py +++ b/tests/basicswap/test_btc_xmr.py @@ -143,6 +143,15 @@ class TestFunctions(BaseTest): offer = swap_clients[id_bidder].listOffers(filters={'offer_id': offer_id})[0] assert (offer.offer_id == offer_id) + post_json = {'with_extra_info': True} + offer0 = read_json_api(1800, f'offers/{offer_id.hex()}', post_json)[0] + offer1 = read_json_api(1800, f'offers/{offer_id.hex()}', post_json)[0] + from basicswap.util import dumpj + logging.info('offer0 {} '.format(dumpj(offer0))) + logging.info('offer1 {} '.format(dumpj(offer1))) + assert ('lock_time_1' in offer0) + assert ('lock_time_1' in offer1) + bid_id = swap_clients[1].postXmrBid(offer_id, offer.amount_from) wait_for_bid(test_delay_event, swap_clients[id_offerer], bid_id, BidStates.BID_RECEIVED) @@ -207,6 +216,27 @@ class TestFunctions(BaseTest): assert (node0_sent_messages == (3 + split_msgs if reverse_bid else 4 + split_msgs)) assert (node1_sent_messages == (4 + split_msgs if reverse_bid else 2 + split_msgs)) + post_json = {'show_extra': True} + bid0 = read_json_api(1800, f'bids/{bid_id.hex()}', post_json) + bid1 = read_json_api(1801, f'bids/{bid_id.hex()}', post_json) + logging.info('bid0 {} '.format(dumpj(bid0))) + logging.info('bid1 {} '.format(dumpj(bid1))) + + chain_a_lock_txid = None + chain_b_lock_txid = None + for tx in bid0['txns']: + if tx['type'] == 'Chain A Lock Spend': + chain_a_lock_txid = tx['txid'] + elif tx['type'] == 'Chain B Lock Spend': + chain_b_lock_txid = tx['txid'] + for tx in bid1['txns']: + if not chain_a_lock_txid and tx['type'] == 'Chain A Lock Spend': + chain_a_lock_txid = tx['txid'] + elif not chain_b_lock_txid and tx['type'] == 'Chain B Lock Spend': + chain_b_lock_txid = tx['txid'] + assert (chain_a_lock_txid is not None) + assert (chain_b_lock_txid is not None) + def do_test_02_leader_recover_a_lock_tx(self, coin_from: Coins, coin_to: Coins) -> None: logging.info('---------- Test {} to {} leader recovers coin a lock tx'.format(coin_from.name, coin_to.name)) @@ -656,6 +686,27 @@ class BasicSwapTest(TestFunctions): assert (vsize_actual <= vsize_estimated and vsize_estimated - vsize_actual < 4) assert (self.callnoderpc('sendrawtransaction', [lock_spend_tx.hex()]) == txid) + expect_vsize: int = ci.xmr_swap_a_lock_spend_tx_vsize() + assert (expect_vsize >= vsize_actual) + assert (expect_vsize - vsize_actual < 10) + + # Test chain b (no-script) lock tx size + v = ci.getNewSecretKey() + s = ci.getNewSecretKey() + S = ci.getPubkey(s) + lock_tx_b_txid = ci.publishBLockTx(v, S, amount, fee_rate) + + addr_out = ci.getNewAddress(True) + lock_tx_b_spend_txid = ci.spendBLockTx(lock_tx_b_txid, addr_out, v, s, amount, fee_rate, 0) + lock_tx_b_spend = ci.getTransaction(lock_tx_b_spend_txid) + if lock_tx_b_spend is None: + lock_tx_b_spend = ci.getWalletTransaction(lock_tx_b_spend_txid) + lock_tx_b_spend_decoded = self.callnoderpc('decoderawtransaction', [lock_tx_b_spend.hex()]) + + expect_vsize: int = ci.xmr_swap_b_lock_spend_tx_vsize() + assert (expect_vsize >= lock_tx_b_spend_decoded['vsize']) + assert (expect_vsize - lock_tx_b_spend_decoded['vsize'] < 10) + def test_01_a_full_swap(self): if not self.has_segwit: return diff --git a/tests/basicswap/test_partblind_xmr.py b/tests/basicswap/test_partblind_xmr.py index 9cd5a77..ede543d 100644 --- a/tests/basicswap/test_partblind_xmr.py +++ b/tests/basicswap/test_partblind_xmr.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -# Copyright (c) 2021-2022 tecnovert +# Copyright (c) 2021-2023 tecnovert # Distributed under the MIT software license, see the accompanying # file LICENSE or http://www.opensource.org/licenses/mit-license.php. @@ -66,7 +66,6 @@ class Test(BaseTest): 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 = read_json_api(1800, 'wallets/part') - node0_blind_before = js_0['blind_balance'] + js_0['blind_unconfirmed'] def ensure_balance(self, coin_type, node_id, amount): tla = 'PART' @@ -89,6 +88,122 @@ class Test(BaseTest): def getXmrBalance(self, js_wallets): return float(js_wallets[Coins.XMR.name]['unconfirmed']) + float(js_wallets[Coins.XMR.name]['balance']) + def test_010_txn_size(self): + logging.info('---------- Test {} txn_size'.format(self.test_coin_from.name)) + + self.ensure_balance(self.test_coin_from, 0, 100.0) + + swap_clients = self.swap_clients + ci = swap_clients[0].ci(self.test_coin_from) + pi = swap_clients[0].pi(SwapTypes.XMR_SWAP) + + def wait_for_unspents(delay_event, iterations=20, delay_time=0.5): + nonlocal ci + i = 0 + while not delay_event.is_set(): + unspents = ci.rpc_callback('listunspentblind') + if len(unspents) >= 1: + return + delay_event.wait(delay_time) + i += 1 + if i > iterations: + raise ValueError('wait_for_unspents timed out') + wait_for_unspents(test_delay_event) + + amount: int = ci.make_int(random.uniform(0.1, 2.0), r=1) + + # Record unspents before createSCLockTx as the used ones will be locked + unspents = ci.rpc_callback('listunspentblind') + locked_utxos_before = ci.rpc_callback('listlockunspent') + + # fee_rate is in sats/kvB + fee_rate: int = 1000 + + vkbv = ci.getNewSecretKey() + a = ci.getNewSecretKey() + b = ci.getNewSecretKey() + + A = ci.getPubkey(a) + B = ci.getPubkey(b) + lock_tx_script = pi.genScriptLockTxScript(ci, A, B) + + lock_tx = ci.createSCLockTx(amount, lock_tx_script, vkbv) + lock_tx = ci.fundSCLockTx(lock_tx, fee_rate, vkbv) + lock_tx = ci.signTxWithWallet(lock_tx) + + unspents_after = ci.rpc_callback('listunspentblind') + locked_utxos_after = ci.rpc_callback('listlockunspent') + + assert (len(unspents) > len(unspents_after)) + assert (len(locked_utxos_after) > len(locked_utxos_before)) + lock_tx_decoded = ci.rpc_callback('decoderawtransaction', [lock_tx.hex()]) + txid = lock_tx_decoded['txid'] + + vsize = lock_tx_decoded['vsize'] + expect_fee_int = round(fee_rate * vsize / 1000) + expect_fee = ci.format_amount(expect_fee_int) + + ci.rpc_callback('sendrawtransaction', [lock_tx.hex()]) + rv = ci.rpc_callback('gettransaction', [txid]) + wallet_tx_fee = -ci.make_int(rv['details'][0]['fee']) + + assert (wallet_tx_fee >= expect_fee_int) + assert (wallet_tx_fee - expect_fee_int < 20) + + addr_out = ci.getNewAddress(True) + addrinfo = ci.rpc_callback('getaddressinfo', [addr_out,]) + pk_out = bytes.fromhex(addrinfo['pubkey']) + fee_info = {} + lock_spend_tx = ci.createSCLockSpendTx(lock_tx, lock_tx_script, pk_out, fee_rate, vkbv, fee_info=fee_info) + vsize_estimated: int = fee_info['vsize'] + + spend_tx_decoded = ci.rpc_callback('decoderawtransaction', [lock_spend_tx.hex()]) + txid = spend_tx_decoded['txid'] + + nonce = ci.getScriptLockTxNonce(vkbv) + output_n, _ = ci.findOutputByNonce(lock_tx_decoded, nonce) + assert (output_n is not None) + valueCommitment = bytes.fromhex(lock_tx_decoded['vout'][output_n]['valueCommitment']) + + witness_stack = [ + b'', + ci.signTx(a, lock_spend_tx, 0, lock_tx_script, valueCommitment), + ci.signTx(b, lock_spend_tx, 0, lock_tx_script, valueCommitment), + lock_tx_script, + ] + lock_spend_tx = ci.setTxSignature(lock_spend_tx, witness_stack) + tx_decoded = ci.rpc_callback('decoderawtransaction', [lock_spend_tx.hex()]) + vsize_actual: int = tx_decoded['vsize'] + + # Note: The fee is set allowing 9 bytes for the encoded fee amount, causing a small overestimate + assert (vsize_actual <= vsize_estimated and vsize_estimated - vsize_actual < 10) + assert (ci.rpc_callback('sendrawtransaction', [lock_spend_tx.hex()]) == txid) + + # Test chain b (no-script) lock tx size + v = ci.getNewSecretKey() + s = ci.getNewSecretKey() + S = ci.getPubkey(s) + lock_tx_b_txid = ci.publishBLockTx(v, S, amount, fee_rate) + + addr_out = ci.getNewStealthAddress() + lock_tx_b_spend_txid = None + for i in range(20): + try: + lock_tx_b_spend_txid = ci.spendBLockTx(lock_tx_b_txid, addr_out, v, s, amount, fee_rate, 0) + break + except Exception as e: + print('spendBLockTx failed', str(e)) + test_delay_event.wait(2) + assert (lock_tx_b_spend_txid is not None) + lock_tx_b_spend = ci.getTransaction(lock_tx_b_spend_txid) + if lock_tx_b_spend is None: + lock_tx_b_spend = ci.getWalletTransaction(lock_tx_b_spend_txid) + lock_tx_b_spend_decoded = ci.rpc_callback('decoderawtransaction', [lock_tx_b_spend.hex()]) + + expect_vsize: int = ci.xmr_swap_b_lock_spend_tx_vsize() + assert (expect_vsize >= lock_tx_b_spend_decoded['vsize']) + assert (expect_vsize - lock_tx_b_spend_decoded['vsize'] < 10) + def test_01_part_xmr(self): logging.info('---------- Test PARTct to XMR') swap_clients = self.swap_clients diff --git a/tests/basicswap/test_xmr.py b/tests/basicswap/test_xmr.py index baad3f6..e510306 100644 --- a/tests/basicswap/test_xmr.py +++ b/tests/basicswap/test_xmr.py @@ -669,6 +669,130 @@ class Test(BaseTest): def notest_00_delay(self): test_delay_event.wait(100000) + def test_010_txn_size(self): + logging.info('---------- Test {} txn_size'.format(Coins.PART)) + + swap_clients = self.swap_clients + ci = swap_clients[0].ci(Coins.PART) + pi = swap_clients[0].pi(SwapTypes.XMR_SWAP) + + amount: int = ci.make_int(random.uniform(0.1, 2.0), r=1) + + # Record unspents before createSCLockTx as the used ones will be locked + unspents = ci.rpc_callback('listunspent') + + # fee_rate is in sats/kvB + fee_rate: int = 1000 + + a = ci.getNewSecretKey() + b = ci.getNewSecretKey() + + A = ci.getPubkey(a) + B = ci.getPubkey(b) + lock_tx_script = pi.genScriptLockTxScript(ci, A, B) + + lock_tx = ci.createSCLockTx(amount, lock_tx_script) + lock_tx = ci.fundSCLockTx(lock_tx, fee_rate) + lock_tx = ci.signTxWithWallet(lock_tx) + + unspents_after = ci.rpc_callback('listunspent') + assert (len(unspents) > len(unspents_after)) + + tx_decoded = ci.rpc_callback('decoderawtransaction', [lock_tx.hex()]) + txid = tx_decoded['txid'] + + vsize = tx_decoded['vsize'] + expect_fee_int = round(fee_rate * vsize / 1000) + expect_fee = ci.format_amount(expect_fee_int) + + out_value: int = 0 + for txo in tx_decoded['vout']: + if 'value' in txo: + out_value += ci.make_int(txo['value']) + in_value: int = 0 + for txi in tx_decoded['vin']: + for utxo in unspents: + if 'vout' not in utxo: + continue + if utxo['txid'] == txi['txid'] and utxo['vout'] == txi['vout']: + in_value += ci.make_int(utxo['amount']) + break + fee_value = in_value - out_value + + ci.rpc_callback('sendrawtransaction', [lock_tx.hex()]) + rv = ci.rpc_callback('gettransaction', [txid]) + wallet_tx_fee = -ci.make_int(rv['fee']) + + assert (wallet_tx_fee == fee_value) + assert (wallet_tx_fee == expect_fee_int) + + addr_out = ci.getNewAddress(True) + pkh_out = ci.decodeAddress(addr_out) + fee_info = {} + lock_spend_tx = ci.createSCLockSpendTx(lock_tx, lock_tx_script, pkh_out, fee_rate, fee_info=fee_info) + vsize_estimated: int = fee_info['vsize'] + + tx_decoded = ci.rpc_callback('decoderawtransaction', [lock_spend_tx.hex()]) + txid = tx_decoded['txid'] + + witness_stack = [ + b'', + ci.signTx(a, lock_spend_tx, 0, lock_tx_script, amount), + ci.signTx(b, lock_spend_tx, 0, lock_tx_script, amount), + lock_tx_script, + ] + lock_spend_tx = ci.setTxSignature(lock_spend_tx, witness_stack) + tx_decoded = ci.rpc_callback('decoderawtransaction', [lock_spend_tx.hex()]) + vsize_actual: int = tx_decoded['vsize'] + + assert (vsize_actual <= vsize_estimated and vsize_estimated - vsize_actual < 4) + assert (ci.rpc_callback('sendrawtransaction', [lock_spend_tx.hex()]) == txid) + + expect_vsize: int = ci.xmr_swap_a_lock_spend_tx_vsize() + assert (expect_vsize >= vsize_actual) + assert (expect_vsize - vsize_actual < 10) + + # Test chain b (no-script) lock tx size + v = ci.getNewSecretKey() + s = ci.getNewSecretKey() + S = ci.getPubkey(s) + lock_tx_b_txid = ci.publishBLockTx(v, S, amount, fee_rate) + + addr_out = ci.getNewAddress(True) + lock_tx_b_spend_txid = ci.spendBLockTx(lock_tx_b_txid, addr_out, v, s, amount, fee_rate, 0) + lock_tx_b_spend = ci.getTransaction(lock_tx_b_spend_txid) + if lock_tx_b_spend is None: + lock_tx_b_spend = ci.getWalletTransaction(lock_tx_b_spend_txid) + lock_tx_b_spend_decoded = ci.rpc_callback('decoderawtransaction', [lock_tx_b_spend.hex()]) + + expect_vsize: int = ci.xmr_swap_b_lock_spend_tx_vsize() + assert (expect_vsize >= lock_tx_b_spend_decoded['vsize']) + assert (expect_vsize - lock_tx_b_spend_decoded['vsize'] < 10) + + def test_010_xmr_txn_size(self): + logging.info('---------- Test {} txn_size'.format(Coins.XMR)) + + swap_clients = self.swap_clients + ci = swap_clients[1].ci(Coins.XMR) + pi = swap_clients[1].pi(SwapTypes.XMR_SWAP) + + amount: int = ci.make_int(random.uniform(0.1, 2.0), r=1) + fee_rate: int = 1000 # TODO: How to set feerate for rpc functions? + + v = ci.getNewSecretKey() + s = ci.getNewSecretKey() + S = ci.getPubkey(s) + lock_tx_b_txid = ci.publishBLockTx(v, S, amount, fee_rate) + + addr_out = ci.getNewAddress(True) + lock_tx_b_spend_txid = ci.spendBLockTx(lock_tx_b_txid, addr_out, v, s, amount, fee_rate, 0) + lock_tx_b_spend = ci.getTransaction(lock_tx_b_spend_txid) + + actual_size: int = len(lock_tx_b_spend['txs_as_hex'][0]) // 2 + expect_size: int = ci.xmr_swap_b_lock_spend_tx_vsize() + assert (expect_size >= actual_size) + assert (expect_size - actual_size < 10) + def test_01_part_xmr(self): logging.info('---------- Test PART to XMR') swap_clients = self.swap_clients @@ -1197,6 +1321,36 @@ class Test(BaseTest): js_0 = read_json_api(1800, 'wallets/part') assert (js_0['anon_balance'] + js_0['anon_pending'] > node0_anon_before + (amount_to - 0.05)) + # Test chain b (no-script) lock tx size + ci = swap_clients[1].ci(Coins.PART_ANON) + pi = swap_clients[1].pi(SwapTypes.XMR_SWAP) + amount: int = ci.make_int(random.uniform(0.1, 2.0), r=1) + fee_rate: int = 1000 + v = ci.getNewSecretKey() + s = ci.getNewSecretKey() + S = ci.getPubkey(s) + lock_tx_b_txid = ci.publishBLockTx(v, S, amount, fee_rate) + + addr_out = ci.getNewStealthAddress() + lock_tx_b_spend_txid = None + for i in range(20): + try: + lock_tx_b_spend_txid = ci.spendBLockTx(lock_tx_b_txid, addr_out, v, s, amount, fee_rate, 0) + break + except Exception as e: + print('spendBLockTx failed', str(e)) + test_delay_event.wait(2) + assert (lock_tx_b_spend_txid is not None) + + lock_tx_b_spend = ci.getTransaction(lock_tx_b_spend_txid) + if lock_tx_b_spend is None: + lock_tx_b_spend = ci.getWalletTransaction(lock_tx_b_spend_txid) + lock_tx_b_spend_decoded = ci.rpc_callback('decoderawtransaction', [lock_tx_b_spend.hex()]) + + expect_vsize: int = ci.xmr_swap_b_lock_spend_tx_vsize() + assert (expect_vsize >= lock_tx_b_spend_decoded['vsize']) + assert (expect_vsize - lock_tx_b_spend_decoded['vsize'] < 10) + def test_12_particl_blind(self): logging.info('---------- Test Particl blind transactions') swap_clients = self.swap_clients