From ca264db0d0705938add0ea80dbffd21d1b7c73a5 Mon Sep 17 00:00:00 2001 From: tecnovert Date: Mon, 7 Nov 2022 22:31:10 +0200 Subject: [PATCH] Add non-segwit Firo support. Rework tests to combine atomic and xmr test cases. Modify btc interface to support P2WSH_nested_in_BIP16_P2SH Add coin feature tests to test_btc_xmr.py --- basicswap/__init__.py | 2 +- basicswap/base.py | 7 - basicswap/basicswap.py | 116 ++--- basicswap/basicswap_util.py | 2 +- basicswap/chainparams.py | 40 ++ basicswap/config.py | 5 + basicswap/interface/btc.py | 185 +++++-- basicswap/interface/firo.py | 172 +++++++ basicswap/interface/part.py | 44 +- basicswap/ui/page_wallet.py | 2 +- bin/basicswap_prepare.py | 148 ++++-- pgp/keys/firo_reuben.pgp | 13 + ...o_binaryfate.asc => monero_binaryfate.pgp} | 0 tests/basicswap/common.py | 4 + tests/basicswap/extended/test_dash.py | 8 +- tests/basicswap/extended/test_firo.py | 465 ++++++++++++++++++ tests/basicswap/extended/test_pivx.py | 8 +- .../basicswap/extended/test_xmr_persistent.py | 5 +- tests/basicswap/test_btc_xmr.py | 224 ++++++++- tests/basicswap/test_other.py | 2 +- tests/basicswap/test_xmr.py | 359 +++++++------- 21 files changed, 1400 insertions(+), 411 deletions(-) create mode 100644 basicswap/interface/firo.py create mode 100644 pgp/keys/firo_reuben.pgp rename pgp/keys/{monero_binaryfate.asc => monero_binaryfate.pgp} (100%) create mode 100644 tests/basicswap/extended/test_firo.py diff --git a/basicswap/__init__.py b/basicswap/__init__.py index daff959..f616645 100644 --- a/basicswap/__init__.py +++ b/basicswap/__init__.py @@ -1,3 +1,3 @@ name = "basicswap" -__version__ = "0.11.44" +__version__ = "0.11.45" diff --git a/basicswap/base.py b/basicswap/base.py index eee3dee..60df37f 100644 --- a/basicswap/base.py +++ b/basicswap/base.py @@ -15,7 +15,6 @@ import traceback import subprocess import basicswap.config as cfg -import basicswap.contrib.segwit_addr as segwit_addr from .rpc import ( callrpc, @@ -112,12 +111,6 @@ class BaseApp: return c raise ValueError('Unknown coin: {}'.format(coin_name)) - def encodeSegwit(self, coin_type, raw): - return segwit_addr.encode(chainparams[coin_type][self.chain]['hrp'], 0, raw) - - def decodeSegwit(self, coin_type, addr): - return bytes(segwit_addr.decode(chainparams[coin_type][self.chain]['hrp'], addr)[1]) - def callrpc(self, method, params=[], wallet=None): cc = self.coin_clients[Coins.PART] return callrpc(cc['rpcport'], cc['rpcauth'], method, params, wallet, cc['rpchost']) diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index 99ce727..ac11c27 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -34,6 +34,7 @@ from .interface.nmc import NMCInterface from .interface.xmr import XMRInterface from .interface.pivx import PIVXInterface from .interface.dash import DASHInterface +from .interface.firo import FIROInterface from .interface.passthrough_btc import PassthroughBTCInterface from . import __version__ @@ -58,7 +59,6 @@ from .util.address import ( getKeyID, decodeWif, decodeAddress, - encodeAddress, pubkeyToAddress, ) from .chainparams import ( @@ -531,6 +531,8 @@ class BasicSwap(BaseApp): return PIVXInterface(self.coin_clients[coin], self.chain, self) elif coin == Coins.DASH: return DASHInterface(self.coin_clients[coin], self.chain, self) + elif coin == Coins.FIRO: + return FIROInterface(self.coin_clients[coin], self.chain, self) else: raise ValueError('Unknown coin type') @@ -549,7 +551,7 @@ class BasicSwap(BaseApp): authcookiepath = os.path.join(self.getChainDatadirPath(coin), '.cookie') pidfilename = cc['name'] - if cc['name'] in ('bitcoin', 'litecoin', 'namecoin', 'dash'): + if cc['name'] in ('bitcoin', 'litecoin', 'namecoin', 'dash', 'firo'): pidfilename += 'd' pidfilepath = os.path.join(self.getChainDatadirPath(coin), pidfilename + '.pid') @@ -975,10 +977,8 @@ class BasicSwap(BaseApp): raise ValueError('Invalid swap type for PART_ANON') if (coin_from == Coins.PART_BLIND or coin_to == Coins.PART_BLIND) and swap_type != SwapTypes.XMR_SWAP: raise ValueError('Invalid swap type for PART_BLIND') - if coin_from == Coins.PIVX and swap_type == SwapTypes.XMR_SWAP: - raise ValueError('TODO: PIVX -> XMR') - if coin_from == Coins.DASH and swap_type == SwapTypes.XMR_SWAP: - raise ValueError('TODO: DASH -> XMR') + if coin_from in (Coins.PIVX, Coins.DASH, Coins.FIRO, Coins.NMC) and swap_type == SwapTypes.XMR_SWAP: + raise ValueError('TODO: {} -> XMR'.format(coin_from.name)) def notify(self, event_type, event_data, session=None): @@ -1053,7 +1053,6 @@ class BasicSwap(BaseApp): ensure(amount_to < ci_to.max_amount(), 'To amount above max value for chain') def validateOfferLockValue(self, coin_from, coin_to, lock_type, lock_value): - coin_from_has_csv = self.coin_clients[coin_from]['use_csv'] coin_to_has_csv = self.coin_clients[coin_to]['use_csv'] @@ -1609,18 +1608,6 @@ class BasicSwap(BaseApp): self.mxDB.release() return self._contract_count - def getUnspentsByAddr(self, coin_type): - ci = self.ci(coin_type) - - unspent_addr = dict() - unspent = self.callcoinrpc(coin_type, 'listunspent') - for u in unspent: - if u['spendable'] is not True: - continue - unspent_addr[u['address']] = unspent_addr.get(u['address'], 0) + ci.make_int(u['amount'], r=1) - - return unspent_addr - def getProofOfFunds(self, coin_type, amount_for, extra_commit_bytes): ci = self.ci(coin_type) self.log.debug('getProofOfFunds %s %s', ci.coin_name(), ci.format_amount(amount_for)) @@ -1628,28 +1615,7 @@ class BasicSwap(BaseApp): if self.coin_clients[coin_type]['connection_type'] != 'rpc': return (None, None) - # TODO: Lock unspent and use same output/s to fund bid - unspent_addr = self.getUnspentsByAddr(coin_type) - - sign_for_addr = None - for addr, value in unspent_addr.items(): - if value >= amount_for: - sign_for_addr = addr - break - - ensure(sign_for_addr is not None, 'Could not find address with enough funds for proof') - - self.log.debug('sign_for_addr %s', sign_for_addr) - if self.coin_clients[coin_type]['use_segwit']: # TODO: Use isSegwitAddress when scantxoutset can use combo - # 'Address does not refer to key' for non p2pkh - addrinfo = self.callcoinrpc(coin_type, 'getaddressinfo', [sign_for_addr]) - pkh = addrinfo['scriptPubKey'][4:] - sign_for_addr = encodeAddress(bytes((chainparams[coin_type][self.chain]['pubkey_address'],)) + bytes.fromhex(pkh)) - self.log.debug('sign_for_addr converted %s', sign_for_addr) - - signature = self.callcoinrpc(coin_type, 'signmessage', [sign_for_addr, sign_for_addr + '_swap_proof_' + extra_commit_bytes.hex()]) - - return (sign_for_addr, signature) + return ci.getProofOfFunds(amount_for, extra_commit_bytes) def saveBidInSession(self, bid_id, bid, session, xmr_swap=None, save_in_progress=None): session.add(bid) @@ -2293,16 +2259,16 @@ class BasicSwap(BaseApp): xmr_swap.kbsl_dleag = xmr_swap.pkbsl # MSG2F - xmr_swap.a_lock_tx, xmr_swap.a_lock_tx_script = ci_from.createScriptLockTx( + xmr_swap.a_lock_tx, xmr_swap.a_lock_tx_script = ci_from.createSCLockTx( bid.amount, xmr_swap.pkal, xmr_swap.pkaf, xmr_swap.vkbv ) - 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 = ci_from.fundSCLockTx(xmr_swap.a_lock_tx, xmr_offer.a_fee_rate, xmr_swap.vkbv) 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_refund_tx, xmr_swap.a_lock_refund_tx_script, xmr_swap.a_swap_refund_value = ci_from.createSCLockRefundTx( 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, @@ -2316,7 +2282,7 @@ class BasicSwap(BaseApp): 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_spend_tx = ci_from.createSCLockRefundSpendTx( xmr_swap.a_lock_refund_tx, xmr_swap.a_lock_refund_tx_script, pkh_refund_to, xmr_offer.a_fee_rate, xmr_swap.vkbv @@ -2326,7 +2292,7 @@ class BasicSwap(BaseApp): # 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_vout = ci_from.verifySCLockTx( xmr_swap.a_lock_tx, xmr_swap.a_lock_tx_script, bid.amount, @@ -2336,7 +2302,7 @@ class BasicSwap(BaseApp): check_lock_tx_inputs, xmr_swap.vkbv) - _, _, lock_refund_vout = ci_from.verifyLockRefundTx( + _, _, lock_refund_vout = ci_from.verifySCLockRefundTx( xmr_swap.a_lock_refund_tx, xmr_swap.a_lock_tx, xmr_swap.a_lock_refund_tx_script, @@ -2351,7 +2317,7 @@ class BasicSwap(BaseApp): xmr_offer.a_fee_rate, xmr_swap.vkbv) - ci_from.verifyLockRefundSpendTx( + ci_from.verifySCLockRefundSpendTx( 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, @@ -2602,8 +2568,8 @@ class BasicSwap(BaseApp): assert (addr_redeem_out is not None) if self.coin_clients[coin_type]['use_segwit']: - # Change to btc hrp - addr_redeem_out = self.encodeSegwit(Coins.PART, self.decodeSegwit(coin_type, addr_redeem_out)) + # Change to part hrp + addr_redeem_out = self.ci(Coins.PART).encodeSegwitAddress(ci.decodeSegwitAddress(addr_redeem_out)) else: addr_redeem_out = replaceAddrPrefix(addr_redeem_out, Coins.PART, self.chain) self.log.debug('addr_redeem_out %s', addr_redeem_out) @@ -2704,8 +2670,8 @@ class BasicSwap(BaseApp): addr_refund_out = self.getReceiveAddressFromPool(coin_type, bid.bid_id, tx_type) ensure(addr_refund_out is not None, 'addr_refund_out is null') if self.coin_clients[coin_type]['use_segwit']: - # Change to btc hrp - addr_refund_out = self.encodeSegwit(Coins.PART, self.decodeSegwit(coin_type, addr_refund_out)) + # Change to part hrp + addr_refund_out = self.ci(Coins.PART).encodeSegwitAddress(ci.decodeSegwitAddress(addr_refund_out)) else: addr_refund_out = replaceAddrPrefix(addr_refund_out, Coins.PART, self.chain) self.log.debug('addr_refund_out %s', addr_refund_out) @@ -2861,7 +2827,6 @@ class BasicSwap(BaseApp): # TODO: random offset into explorers, try blocks for exp in explorers: - # TODO: ExplorerBitAps use only gettransaction if assert_txid is set rv = exp.lookupUnspentByAddress(address) @@ -3033,11 +2998,11 @@ class BasicSwap(BaseApp): # TODO: Timeout waiting for transactions bid_changed = False - a_lock_tx_dest = ci_from.getScriptDest(xmr_swap.a_lock_tx_script) - # Changed from ci_from.getOutput(bid.xmr_a_lock_tx.txid, a_lock_tx_dest, bid.amount, xmr_swap) - - p2wsh_addr = ci_from.encode_p2wsh(a_lock_tx_dest) - lock_tx_chain_info = ci_from.getLockTxHeight(bid.xmr_a_lock_tx.txid, p2wsh_addr, bid.amount, bid.chain_a_height_start) + if offer.coin_from == Coins.FIRO: + lock_tx_chain_info = ci_from.getLockTxHeightFiro(bid.xmr_a_lock_tx.txid, xmr_swap.a_lock_tx_script, bid.amount, bid.chain_a_height_start) + else: + a_lock_tx_addr = ci_from.getSCLockScriptAddress(xmr_swap.a_lock_tx_script) + lock_tx_chain_info = ci_from.getLockTxHeight(bid.xmr_a_lock_tx.txid, a_lock_tx_addr, bid.amount, bid.chain_a_height_start) if lock_tx_chain_info is None: return rv @@ -3149,9 +3114,11 @@ class BasicSwap(BaseApp): if TxTypes.XMR_SWAP_A_LOCK_REFUND in bid.txns: refund_tx = bid.txns[TxTypes.XMR_SWAP_A_LOCK_REFUND] if refund_tx.block_time is None: - a_lock_refund_tx_dest = ci_from.getScriptDest(xmr_swap.a_lock_refund_tx_script) - p2wsh_addr = ci_from.encode_p2wsh(a_lock_refund_tx_dest) - lock_refund_tx_chain_info = ci_from.getLockTxHeight(refund_tx.txid, p2wsh_addr, 0, bid.chain_a_height_start) + if offer.coin_from == Coins.FIRO: + lock_refund_tx_chain_info = ci_from.getLockTxHeightFiro(refund_tx.txid, xmr_swap.a_lock_refund_tx_script, 0, bid.chain_a_height_start) + else: + refund_tx_addr = ci_from.getSCLockScriptAddress(xmr_swap.a_lock_refund_tx_script) + lock_refund_tx_chain_info = ci_from.getLockTxHeight(refund_tx.txid, refund_tx_addr, 0, bid.chain_a_height_start) if lock_refund_tx_chain_info is not None and lock_refund_tx_chain_info.get('height', 0) > 0: block_header = ci_from.getBlockHeaderFromHeight(lock_refund_tx_chain_info['height']) @@ -4015,18 +3982,7 @@ class BasicSwap(BaseApp): if swap_type == SwapTypes.SELLER_FIRST: ensure(len(bid_data.pkhash_buyer) == 20, 'Bad pkhash_buyer length') - # Verify proof of funds - bid_proof_address = replaceAddrPrefix(bid_data.proof_address, Coins.PART, self.chain) - mm = chainparams[coin_to]['message_magic'] - passed = self.ci(Coins.PART).verifyMessage(bid_proof_address, bid_data.proof_address + '_swap_proof_' + offer_id.hex(), bid_data.proof_signature, mm) - ensure(passed is True, 'Proof of funds signature invalid') - - if self.coin_clients[coin_to]['use_segwit']: - addr_search = self.encodeSegwit(coin_to, decodeAddress(bid_data.proof_address)[1:]) - else: - addr_search = bid_data.proof_address - - sum_unspent = self.getAddressBalance(coin_to, addr_search) + sum_unspent = ci_to.verifyProofOfFunds(bid_data.proof_address, bid_data.proof_signature, offer_id) self.log.debug('Proof of funds %s %s', bid_data.proof_address, self.ci(coin_to).format_amount(sum_unspent)) ensure(sum_unspent >= amount_to, 'Proof of funds failed') @@ -4382,7 +4338,7 @@ class BasicSwap(BaseApp): # 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_id, xmr_swap.a_lock_tx_vout = ci_from.verifySCLockTx( xmr_swap.a_lock_tx, xmr_swap.a_lock_tx_script, bid.amount, xmr_swap.pkal, xmr_swap.pkaf, @@ -4390,14 +4346,14 @@ class BasicSwap(BaseApp): 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, lock_refund_vout = ci_from.verifyLockRefundTx( + xmr_swap.a_lock_refund_tx_id, xmr_swap.a_swap_refund_value, lock_refund_vout = ci_from.verifySCLockRefundTx( 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( + ci_from.verifySCLockRefundSpendTx( 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, @@ -4517,7 +4473,7 @@ class BasicSwap(BaseApp): xmr_swap.kal_sig = ci_from.signCompact(kal, 'proof key owned for swap') # Create Script lock spend tx - xmr_swap.a_lock_spend_tx = ci_from.createScriptLockSpendTx( + xmr_swap.a_lock_spend_tx = ci_from.createSCLockSpendTx( xmr_swap.a_lock_tx, xmr_swap.a_lock_tx_script, xmr_swap.dest_af, xmr_offer.a_fee_rate, xmr_swap.vkbv) @@ -4923,12 +4879,12 @@ class BasicSwap(BaseApp): 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( + ci_from.verifySCLockSpendTx( 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.vkbv) - ci_from.verifyCompact(xmr_swap.pkal, 'proof key owned for swap', xmr_swap.kal_sig) + ci_from.verifyCompactSig(xmr_swap.pkal, 'proof key owned for swap', xmr_swap.kal_sig) bid.setState(BidStates.XMR_SWAP_HAVE_SCRIPT_COIN_SPEND_TX) bid.setState(BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_SPEND_TX) @@ -5865,7 +5821,7 @@ class BasicSwap(BaseApp): self.log.debug('Creating %s lock refund swipe tx', ci.coin_name()) pkh_dest = ci.decodeAddress(self.getReceiveAddressForCoin(ci.coin_type())) - spend_tx = ci.createScriptLockRefundSpendToFTx( + spend_tx = ci.createSCLockRefundSpendToFTx( xmr_swap.a_lock_refund_tx, xmr_swap.a_lock_refund_tx_script, pkh_dest, xmr_offer.a_fee_rate, xmr_swap.vkbv) diff --git a/basicswap/basicswap_util.py b/basicswap/basicswap_util.py index 71d44be..bc25a45 100644 --- a/basicswap/basicswap_util.py +++ b/basicswap/basicswap_util.py @@ -9,8 +9,8 @@ import struct import hashlib from enum import IntEnum, auto from .util.address import ( - decodeAddress, encodeAddress, + decodeAddress, ) from .chainparams import ( chainparams, diff --git a/basicswap/chainparams.py b/basicswap/chainparams.py index 7a61910..f4e6959 100644 --- a/basicswap/chainparams.py +++ b/basicswap/chainparams.py @@ -30,6 +30,7 @@ class Coins(IntEnum): # NDAU = 10 PIVX = 11 DASH = 12 + FIRO = 13 chainparams = { @@ -287,6 +288,45 @@ chainparams = { 'min_amount': 1000, 'max_amount': 100000 * COIN, } + }, + Coins.FIRO: { + 'name': 'firo', + 'ticker': 'FIRO', + 'message_magic': 'Zcoin Signed Message:\n', + 'blocks_target': 60 * 10, + 'decimal_places': 8, + 'has_csv': True, + 'has_segwit': True, + 'mainnet': { + 'rpcport': 8888, + 'pubkey_address': 82, + 'script_address': 7, + 'key_prefix': 210, + 'hrp': '', + 'bip44': 136, + 'min_amount': 1000, + 'max_amount': 100000 * COIN, + }, + 'testnet': { + 'rpcport': 18888, + 'pubkey_address': 65, + 'script_address': 178, + 'key_prefix': 185, + 'hrp': '', + 'bip44': 1, + 'min_amount': 1000, + 'max_amount': 100000 * COIN, + }, + 'regtest': { + 'rpcport': 28888, + 'pubkey_address': 65, + 'script_address': 178, + 'key_prefix': 185, + 'hrp': '', + 'bip44': 1, + 'min_amount': 1000, + 'max_amount': 100000 * COIN, + } } } ticker_map = {} diff --git a/basicswap/config.py b/basicswap/config.py index 61010e5..33483f9 100644 --- a/basicswap/config.py +++ b/basicswap/config.py @@ -46,3 +46,8 @@ DASH_BINDIR = os.path.expanduser(os.getenv('DASH_BINDIR', os.path.join(DEFAULT_T DASHD = os.getenv('DASHD', 'dashd' + bin_suffix) DASH_CLI = os.getenv('DASH_CLI', 'dash-cli' + bin_suffix) DASH_TX = os.getenv('DASH_TX', 'dash-tx' + bin_suffix) + +FIRO_BINDIR = os.path.expanduser(os.getenv('FIRO_BINDIR', os.path.join(DEFAULT_TEST_BINDIR, 'firo'))) +FIROD = os.getenv('FIROD', 'firod' + bin_suffix) +FIRO_CLI = os.getenv('FIRO_CLI', 'firo-cli' + bin_suffix) +FIRO_TX = os.getenv('FIRO_TX', 'firo-tx' + bin_suffix) diff --git a/basicswap/interface/btc.py b/basicswap/interface/btc.py index d60b15d..aeff269 100644 --- a/basicswap/interface/btc.py +++ b/basicswap/interface/btc.py @@ -320,7 +320,7 @@ class BTCInterface(CoinInterface): def decodeAddress(self, address): bech32_prefix = self.chainparams_network()['hrp'] - if address.startswith(bech32_prefix + '1'): + if len(bech32_prefix) > 0 and address.startswith(bech32_prefix + '1'): return bytes(segwit_addr.decode(bech32_prefix, address)[1]) return decodeAddress(address)[1:] @@ -338,12 +338,22 @@ class BTCInterface(CoinInterface): checksum = hashlib.sha256(hashlib.sha256(data).digest()).digest() return b58encode(data + checksum[0:4]) + def sh_to_address(self, sh): + assert (len(sh) == 20) + prefix = self.chainparams_network()['script_address'] + data = bytes((prefix,)) + sh + checksum = hashlib.sha256(hashlib.sha256(data).digest()).digest() + return b58encode(data + checksum[0:4]) + def encode_p2wsh(self, script): bech32_prefix = self.chainparams_network()['hrp'] version = 0 program = script[2:] # strip version and length return segwit_addr.encode(bech32_prefix, version, program) + def encodeScriptDest(self, script): + return self.encode_p2wsh(script) + def encode_p2sh(self, script): return pubkeyToAddress(self.chainparams_network()['script_address'], script) @@ -375,6 +385,12 @@ class BTCInterface(CoinInterface): def encodePubkey(self, pk): return pointToCPK(pk) + def encodeSegwitAddress(self, key_hash): + return segwit_addr.encode(self.chainparams_network()['hrp'], 0, key_hash) + + def decodeSegwitAddress(self, addr): + return bytes(segwit_addr.decode(self.chainparams_network()['hrp'], addr)[1]) + def decodePubkey(self, pke): return CPKToPoint(pke) @@ -415,7 +431,7 @@ class BTCInterface(CoinInterface): return CScript([2, Kal_enc, Kaf_enc, 2, CScriptOp(OP_CHECKMULTISIG)]) - def createScriptLockTx(self, value, Kal, Kaf, vkbv=None): + def createSCLockTx(self, value, Kal, Kaf, vkbv=None): script = self.genScriptLockTxScript(Kal, Kaf) tx = CTransaction() tx.nVersion = self.txVersion() @@ -423,7 +439,7 @@ class BTCInterface(CoinInterface): return tx.serialize(), script - def fundScriptLockTx(self, tx_bytes, feerate, vkbv=None): + def fundSCLockTx(self, tx_bytes, feerate, vkbv=None): return self.fundTx(tx_bytes, feerate) def extractScriptLockRefundScriptValues(self, script_bytes): @@ -470,11 +486,11 @@ 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, vkbv=None): + def createSCLockRefundTx(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()]) + output_script = self.getScriptDest(script_lock) locked_n = findOutput(tx_lock, output_script) ensure(locked_n is not None, 'Output not found in tx') locked_coin = tx_lock.vout[locked_n].nValue @@ -485,8 +501,10 @@ class BTCInterface(CoinInterface): refund_script = self.genScriptLockRefundTxScript(Kal, Kaf, csv_val) tx = CTransaction() tx.nVersion = self.txVersion() - 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()]))) + tx.vin.append(CTxIn(COutPoint(tx_lock_id_int, locked_n), + nSequence=lock1_value, + scriptSig=self.getScriptScriptSig(script_lock))) + tx.vout.append(self.txoType()(locked_coin, self.getScriptDest(refund_script))) dummy_witness_stack = self.getScriptLockTxDummyWitness(script_lock) witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack) @@ -495,19 +513,19 @@ class BTCInterface(CoinInterface): tx.vout[0].nValue = locked_coin - pay_fee tx.rehash() - self._log.info('createScriptLockRefundTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.', + self._log.info('createSCLockRefundTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.', i2h(tx.sha256), tx_fee_rate, vsize, pay_fee) 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, vkbv=None): + def createSCLockRefundSpendTx(self, tx_lock_refund_bytes, script_lock_refund, pkh_refund_to, tx_fee_rate, vkbv=None): # Returns the coinA locked coin to the leader # The follower will sign the multisig path with a signature encumbered by the leader's coinB spend pubkey # If the leader publishes the decrypted signature the leader's coinB spend privatekey will be revealed to the follower tx_lock_refund = self.loadTx(tx_lock_refund_bytes) - output_script = CScript([OP_0, hashlib.sha256(script_lock_refund).digest()]) + output_script = self.getScriptDest(script_lock_refund) locked_n = findOutput(tx_lock_refund, output_script) ensure(locked_n is not None, 'Output not found in tx') locked_coin = tx_lock_refund.vout[locked_n].nValue @@ -517,7 +535,9 @@ class BTCInterface(CoinInterface): tx = CTransaction() tx.nVersion = self.txVersion() - tx.vin.append(CTxIn(COutPoint(tx_lock_refund_hash_int, locked_n), nSequence=0)) + tx.vin.append(CTxIn(COutPoint(tx_lock_refund_hash_int, locked_n), + nSequence=0, + scriptSig=self.getScriptScriptSig(script_lock_refund))) tx.vout.append(self.txoType()(locked_coin, self.getScriptForPubkeyHash(pkh_refund_to))) @@ -528,18 +548,18 @@ class BTCInterface(CoinInterface): tx.vout[0].nValue = locked_coin - pay_fee tx.rehash() - self._log.info('createScriptLockRefundSpendTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.', + self._log.info('createSCLockRefundSpendTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.', i2h(tx.sha256), tx_fee_rate, vsize, pay_fee) return tx.serialize() - def createScriptLockRefundSpendToFTx(self, tx_lock_refund_bytes, script_lock_refund, pkh_dest, tx_fee_rate, vkbv=None): + def createSCLockRefundSpendToFTx(self, tx_lock_refund_bytes, script_lock_refund, pkh_dest, tx_fee_rate, vkbv=None): # lock refund swipe tx # Sends the coinA locked coin to the follower tx_lock_refund = self.loadTx(tx_lock_refund_bytes) - output_script = CScript([OP_0, hashlib.sha256(script_lock_refund).digest()]) + output_script = self.getScriptDest(script_lock_refund) locked_n = findOutput(tx_lock_refund, output_script) ensure(locked_n is not None, 'Output not found in tx') locked_coin = tx_lock_refund.vout[locked_n].nValue @@ -551,7 +571,9 @@ class BTCInterface(CoinInterface): tx = CTransaction() tx.nVersion = self.txVersion() - tx.vin.append(CTxIn(COutPoint(tx_lock_refund_hash_int, locked_n), nSequence=lock2_value)) + tx.vin.append(CTxIn(COutPoint(tx_lock_refund_hash_int, locked_n), + nSequence=lock2_value, + scriptSig=self.getScriptScriptSig(script_lock_refund))) tx.vout.append(self.txoType()(locked_coin, self.getScriptForPubkeyHash(pkh_dest))) @@ -562,14 +584,14 @@ class BTCInterface(CoinInterface): tx.vout[0].nValue = locked_coin - pay_fee tx.rehash() - self._log.info('createScriptLockRefundSpendToFTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.', + self._log.info('createSCLockRefundSpendToFTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.', i2h(tx.sha256), tx_fee_rate, vsize, pay_fee) return tx.serialize() - def createScriptLockSpendTx(self, tx_lock_bytes, script_lock, pkh_dest, tx_fee_rate, vkbv=None): + def createSCLockSpendTx(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()]) + output_script = self.getScriptDest(script_lock) locked_n = findOutput(tx_lock, output_script) ensure(locked_n is not None, 'Output not found in tx') locked_coin = tx_lock.vout[locked_n].nValue @@ -579,7 +601,8 @@ class BTCInterface(CoinInterface): tx = CTransaction() tx.nVersion = self.txVersion() - tx.vin.append(CTxIn(COutPoint(tx_lock_id_int, locked_n))) + tx.vin.append(CTxIn(COutPoint(tx_lock_id_int, locked_n), + scriptSig=self.getScriptScriptSig(script_lock))) tx.vout.append(self.txoType()(locked_coin, self.getScriptForPubkeyHash(pkh_dest))) @@ -590,16 +613,16 @@ class BTCInterface(CoinInterface): tx.vout[0].nValue = locked_coin - pay_fee tx.rehash() - self._log.info('createScriptLockSpendTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.', + self._log.info('createSCLockSpendTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.', i2h(tx.sha256), tx_fee_rate, vsize, pay_fee) return tx.serialize() - def verifyLockTx(self, tx_bytes, script_out, - swap_value, - Kal, Kaf, - feerate, - check_lock_tx_inputs, vkbv=None): + def verifySCLockTx(self, tx_bytes, script_out, + swap_value, + Kal, Kaf, + feerate, + check_lock_tx_inputs, vkbv=None): # Verify: # @@ -614,7 +637,7 @@ class BTCInterface(CoinInterface): 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()]) + script_pk = self.getScriptDest(script_out) locked_n = findOutput(tx, script_pk) ensure(locked_n is not None, 'Output not found in tx') locked_coin = tx.vout[locked_n].nValue @@ -663,9 +686,9 @@ class BTCInterface(CoinInterface): return txid, locked_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=None): + def verifySCLockRefundTx(self, tx_bytes, lock_tx_bytes, script_out, + prevout_id, prevout_n, prevout_seq, prevout_script, + Kal, Kaf, csv_val_expect, swap_value, feerate, vkbv=None): # Verify: # Must have only one input with correct prevout and sequence # Must have only one output to the p2wsh of the lock refund script @@ -680,12 +703,12 @@ class BTCInterface(CoinInterface): ensure(len(tx.vin) == 1, 'tx doesn\'t have one input') 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].scriptSig == self.getScriptScriptSig(prevout_script), 'Input scriptsig mismatch') ensure(tx.vin[0].prevout.hash == b2i(prevout_id) and tx.vin[0].prevout.n == prevout_n, 'Input prevout mismatch') ensure(len(tx.vout) == 1, 'tx doesn\'t have one output') - script_pk = CScript([OP_0, hashlib.sha256(script_out).digest()]) + script_pk = self.getScriptDest(script_out) locked_n = findOutput(tx, script_pk) ensure(locked_n is not None, 'Output not found in tx') locked_coin = tx.vout[locked_n].nValue @@ -712,10 +735,10 @@ class BTCInterface(CoinInterface): return txid, locked_coin, locked_n - def verifyLockRefundSpendTx(self, tx_bytes, lock_refund_tx_bytes, - lock_refund_tx_id, prevout_script, - Kal, - prevout_n, prevout_value, feerate, vkbv=None): + def verifySCLockRefundSpendTx(self, tx_bytes, lock_refund_tx_bytes, + lock_refund_tx_id, prevout_script, + Kal, + prevout_n, prevout_value, feerate, vkbv=None): # Verify: # Must have only one input with correct prevout (n is always 0) and sequence # Must have only one output sending lock refund tx value - fee to leader's address, TODO: follower shouldn't need to verify destination addr @@ -728,7 +751,7 @@ class BTCInterface(CoinInterface): ensure(len(tx.vin) == 1, 'tx doesn\'t have one input') ensure(tx.vin[0].nSequence == 0, 'Bad input nSequence') - ensure(len(tx.vin[0].scriptSig) == 0, 'Input scriptsig not empty') + ensure(tx.vin[0].scriptSig == self.getScriptScriptSig(prevout_script), 'Input scriptsig mismatch') ensure(tx.vin[0].prevout.hash == b2i(lock_refund_tx_id) and tx.vin[0].prevout.n == 0, 'Input prevout mismatch') ensure(len(tx.vout) == 1, 'tx doesn\'t have one output') @@ -756,9 +779,9 @@ class BTCInterface(CoinInterface): return True - def verifyLockSpendTx(self, tx_bytes, - lock_tx_bytes, lock_tx_script, - a_pkhash_f, feerate, vkbv=None): + def verifySCLockSpendTx(self, tx_bytes, + lock_tx_bytes, lock_tx_script, + a_pkhash_f, feerate, vkbv=None): # Verify: # Must have only one input with correct prevout (n is always 0) and sequence # Must have only one output with destination and amount @@ -774,13 +797,13 @@ class BTCInterface(CoinInterface): lock_tx = self.loadTx(lock_tx_bytes) lock_tx_id = self.getTxid(lock_tx) - output_script = CScript([OP_0, hashlib.sha256(lock_tx_script).digest()]) + output_script = self.getScriptDest(lock_tx_script) locked_n = findOutput(lock_tx, output_script) ensure(locked_n is not None, 'Output not found in tx') locked_coin = lock_tx.vout[locked_n].nValue ensure(tx.vin[0].nSequence == 0, 'Bad input nSequence') - ensure(len(tx.vin[0].scriptSig) == 0, 'Input scriptsig not empty') + ensure(tx.vin[0].scriptSig == self.getScriptScriptSig(lock_tx_script), 'Input scriptsig mismatch') ensure(tx.vin[0].prevout.hash == b2i(lock_tx_id) and tx.vin[0].prevout.n == locked_n, 'Input prevout mismatch') ensure(len(tx.vout) == 1, 'tx doesn\'t have one output') @@ -891,7 +914,7 @@ class BTCInterface(CoinInterface): def getTxOutputPos(self, tx, script): if isinstance(tx, bytes): tx = self.loadTx(tx) - script_pk = CScript([OP_0, hashlib.sha256(script).digest()]) + script_pk = self.getScriptDest(script) return findOutput(tx, script_pk) def getPubkeyHash(self, K): @@ -900,6 +923,9 @@ class BTCInterface(CoinInterface): def getScriptDest(self, script): return CScript([OP_0, hashlib.sha256(script).digest()]) + def getScriptScriptSig(self, script): + return bytes() + def getPkDest(self, K): return self.getScriptForPubkeyHash(self.getPubkeyHash(K)) @@ -1003,11 +1029,22 @@ class BTCInterface(CoinInterface): def spendBLockTx(self, chain_b_lock_txid, address_to, kbv, kbs, cb_swap_value, b_fee, restore_height): raise ValueError('TODO') + def importWatchOnlyAddress(self, address, label): + self.rpc_callback('importaddress', [address, label, False]) + + def isWatchOnlyAddress(self, address): + addr_info = self.rpc_callback('getaddressinfo', [address]) + return addr_info['iswatchonly'] + + def getSCLockScriptAddress(self, lock_script): + lock_tx_dest = self.getScriptDest(lock_script) + return self.encodeScriptDest(lock_tx_dest) + def getLockTxHeight(self, txid, dest_address, bid_amount, rescan_from, find_index=False): # Add watchonly address and rescan if required - addr_info = self.rpc_callback('getaddressinfo', [dest_address]) - if not addr_info['iswatchonly']: - ro = self.rpc_callback('importaddress', [dest_address, 'bid', False]) + + if not self.isWatchOnlyAddress(dest_address): + self.importWatchOnlyAddress(dest_address, 'bid') self._log.info('Imported watch-only addr: {}'.format(dest_address)) self._log.info('Rescanning {} chain from height: {}'.format(self.coin_name(), rescan_from)) self.rpc_callback('rescanblockchain', [rescan_from]) @@ -1082,7 +1119,7 @@ class BTCInterface(CoinInterface): privkey = PrivateKey(k) return privkey.sign_recoverable(message_hash, hasher=None)[:64] - def verifyCompact(self, K, message, sig): + def verifyCompactSig(self, K, message, sig): message_hash = hashlib.sha256(bytes(message, 'utf-8')).digest() pubkey = PublicKey(K) rv = pubkey.verify_compact(sig, message_hash, hasher=None) @@ -1090,7 +1127,7 @@ class BTCInterface(CoinInterface): def verifyMessage(self, address, message, signature, message_magic=None) -> bool: if message_magic is None: - message_magic = self.chainparams_network()['message_magic'] + message_magic = self.chainparams()['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() @@ -1185,9 +1222,61 @@ class BTCInterface(CoinInterface): def getBlockWithTxns(self, block_hash): return self.rpc_callback('getblock', [block_hash, 2]) + def getUnspentsByAddr(self): + unspent_addr = dict() + unspent = self.rpc_callback('listunspent') + for u in unspent: + if u['spendable'] is not True: + continue + unspent_addr[u['address']] = unspent_addr.get(u['address'], 0) + self.make_int(u['amount'], r=1) + return unspent_addr + + def getUTXOBalance(self, address): + num_blocks = self.rpc_callback('getblockcount') + + sum_unspent = 0 + self._log.debug('[rm] scantxoutset start') # scantxoutset is slow + ro = self.rpc_callback('scantxoutset', ['start', ['addr({})'.format(address)]]) # TODO: Use combo(address) where possible + self._log.debug('[rm] scantxoutset end') + for o in ro['unspents']: + sum_unspent += self.make_int(o['amount']) + return sum_unspent + + def getProofOfFunds(self, amount_for, extra_commit_bytes): + # TODO: Lock unspent and use same output/s to fund bid + unspent_addr = self.getUnspentsByAddr() + + sign_for_addr = None + for addr, value in unspent_addr.items(): + if value >= amount_for: + sign_for_addr = addr + break + + ensure(sign_for_addr is not None, 'Could not find address with enough funds for proof') + + self._log.debug('sign_for_addr %s', sign_for_addr) + if self._use_segwit: # TODO: Use isSegwitAddress when scantxoutset can use combo + # 'Address does not refer to key' for non p2pkh + pkh = self.decodeAddress(sign_for_addr) + sign_for_addr = self.pkh_to_address(pkh) + self._log.debug('sign_for_addr converted %s', sign_for_addr) + + signature = self.rpc_callback('signmessage', [sign_for_addr, sign_for_addr + '_swap_proof_' + extra_commit_bytes.hex()]) + + return (sign_for_addr, signature) + + def verifyProofOfFunds(self, address, signature, extra_commit_bytes): + passed = self.verifyMessage(address, address + '_swap_proof_' + extra_commit_bytes.hex(), signature) + ensure(passed is True, 'Proof of funds signature invalid') + + if self._use_segwit: + address = self.encodeSegwitAddress(decodeAddress(address)[1:]) + + return self.getUTXOBalance(address) + def testBTCInterface(): - print('testBTCInterface') + print('TODO: testBTCInterface') if __name__ == "__main__": diff --git a/basicswap/interface/firo.py b/basicswap/interface/firo.py new file mode 100644 index 0000000..a6e46d8 --- /dev/null +++ b/basicswap/interface/firo.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright (c) 2022 tecnovert +# Distributed under the MIT software license, see the accompanying +# file LICENSE or http://www.opensource.org/licenses/mit-license.php. + +import hashlib +from .btc import BTCInterface, find_vout_for_address_from_txobj +from basicswap.chainparams import Coins + +from basicswap.util.address import ( + decodeAddress, +) +from basicswap.contrib.test_framework.script import ( + CScript, + OP_0, + OP_DUP, + OP_EQUAL, + OP_HASH160, + OP_CHECKSIG, + OP_EQUALVERIFY, + hash160, +) +from basicswap.contrib.test_framework.messages import ( + CTransaction, +) + + +class FIROInterface(BTCInterface): + @staticmethod + def coin_type(): + return Coins.FIRO + + def initialiseWallet(self, key): + # load with -hdseed= parameter + pass + + def getNewAddress(self, use_segwit, label='swap_receive'): + return self.rpc_callback('getnewaddress', [label]) + # addr_plain = self.rpc_callback('getnewaddress', [label]) + # return self.rpc_callback('addwitnessaddress', [addr_plain]) + + def decodeAddress(self, address): + return decodeAddress(address)[1:] + + def encodeSegwitAddress(self, script): + raise ValueError('TODO') + + def decodeSegwitAddress(self, addr): + raise ValueError('TODO') + + def isWatchOnlyAddress(self, address): + addr_info = self.rpc_callback('validateaddress', [address]) + return addr_info['iswatchonly'] + + def getSCLockScriptAddress(self, lock_script): + lock_tx_dest = self.getScriptDest(lock_script) + address = self.encodeScriptDest(lock_tx_dest) + + if not self.isWatchOnlyAddress(address): + # Expects P2WSH nested in BIP16_P2SH + ro = self.rpc_callback('importaddress', [lock_tx_dest.hex(), 'bid lock', False, True]) + addr_info = self.rpc_callback('validateaddress', [address]) + + return address + + def getLockTxHeightFiro(self, txid, lock_script, bid_amount, rescan_from, find_index=False): + # Add watchonly address and rescan if required + lock_tx_dest = self.getScriptDest(lock_script) + dest_address = self.encodeScriptDest(lock_tx_dest) + if not self.isWatchOnlyAddress(dest_address): + self.rpc_callback('importaddress', [lock_tx_dest.hex(), 'bid lock', False, True]) + self._log.info('Imported watch-only addr: {}'.format(dest_address)) + self._log.info('Rescanning {} chain from height: {}'.format(self.coin_name(), rescan_from)) + self.rpc_callback('rescanblockchain', [rescan_from]) + + return_txid = True if txid is None else False + if txid is None: + txns = self.rpc_callback('listunspent', [0, 9999999, [dest_address, ]]) + + for tx in txns: + if self.make_int(tx['amount']) == bid_amount: + txid = bytes.fromhex(tx['txid']) + break + + if txid is None: + return None + + 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: + self._log.debug('getLockTxHeight gettransaction failed: %s, %s', txid.hex(), str(e)) + return None + + if find_index: + tx_obj = self.rpc_callback('decoderawtransaction', [tx['hex']]) + rv['index'] = find_vout_for_address_from_txobj(tx_obj, dest_address) + + if return_txid: + rv['txid'] = txid.hex() + + return rv + + def createSCLockTx(self, value, Kal, Kaf, vkbv=None): + script = self.genScriptLockTxScript(Kal, Kaf) + tx = CTransaction() + tx.nVersion = self.txVersion() + tx.vout.append(self.txoType()(value, self.getScriptDest(script))) + + return tx.serialize(), script + + def fundSCLockTx(self, tx_bytes, feerate, vkbv=None): + return self.fundTx(tx_bytes, feerate) + + def signTxWithWallet(self, tx): + rv = self.rpc_callback('signrawtransaction', [tx.hex()]) + return bytes.fromhex(rv['hex']) + + def createRawSignedTransaction(self, addr_to, amount): + txn = self.rpc_callback('createrawtransaction', [[], {addr_to: self.format_amount(amount)}]) + + fee_rate, fee_src = self.get_fee_rate(self._conf_target) + self._log.debug(f'Fee rate: {fee_rate}, source: {fee_src}, block target: {self._conf_target}') + + options = { + 'lockUnspents': True, + 'feeRate': fee_rate, + } + txn_funded = self.rpc_callback('fundrawtransaction', [txn, options])['hex'] + txn_signed = self.rpc_callback('signrawtransaction', [txn_funded])['hex'] + return txn_signed + + def getScriptForPubkeyHash(self, pkh): + # Return P2WPKH nested in BIP16 P2SH + + return CScript([OP_DUP, OP_HASH160, pkh, OP_EQUALVERIFY, OP_CHECKSIG]) + + def getScriptDest(self, script): + # P2WSH nested in BIP16_P2SH + + script_hash = hashlib.sha256(script).digest() + assert len(script_hash) == 32 + script_hash_hash = hash160(script_hash) + assert len(script_hash_hash) == 20 + + return CScript([OP_HASH160, script_hash_hash, OP_EQUAL]) + + def encodeScriptDest(self, script): + # Extract hash from script + script_hash = script[2:-1] + return self.sh_to_address(script_hash) + + def getScriptScriptSig(self, script): + return CScript([OP_0, hashlib.sha256(script).digest()]) + + def withdrawCoin(self, value, addr_to, subfee): + params = [addr_to, value, '', '', subfee] + return self.rpc_callback('sendtoaddress', params) + + def getWalletSeedID(self): + return self.rpc_callback('getwalletinfo')['hdmasterkeyid'] diff --git a/basicswap/interface/part.py b/basicswap/interface/part.py index 06b28dc..cddf673 100644 --- a/basicswap/interface/part.py +++ b/basicswap/interface/part.py @@ -166,7 +166,7 @@ class PARTInterfaceBlind(PARTInterface): ensure(v['result'] is True, 'verifycommitment failed') return output_n, blinded_info - def createScriptLockTx(self, value, Kal, Kaf, vkbv): + def createSCLockTx(self, value, Kal, Kaf, vkbv): script = self.genScriptLockTxScript(Kal, Kaf) # Nonce is derived from vkbv, ephemeral_key isn't used @@ -183,7 +183,7 @@ class PARTInterfaceBlind(PARTInterface): tx_bytes = bytes.fromhex(rv['hex']) return tx_bytes, script - def fundScriptLockTx(self, tx_bytes, feerate, vkbv): + def fundSCLockTx(self, tx_bytes, feerate, vkbv): feerate_str = self.format_amount(feerate) # TODO: unlock unspents if bid cancelled @@ -205,7 +205,7 @@ class PARTInterfaceBlind(PARTInterface): 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): + def createSCLockRefundTx(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 @@ -252,7 +252,7 @@ class PARTInterfaceBlind(PARTInterface): 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): + def createSCLockRefundSpendTx(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 @@ -297,11 +297,11 @@ class PARTInterfaceBlind(PARTInterface): 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): + def verifySCLockTx(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)) @@ -341,9 +341,9 @@ class PARTInterfaceBlind(PARTInterface): 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): + def verifySCLockRefundTx(self, tx_bytes, lock_tx_bytes, script_out, + prevout_id, prevout_n, prevout_seq, prevout_script, + Kal, Kaf, csv_val_expect, swap_value, feerate, vkbv): 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)) @@ -399,10 +399,10 @@ class PARTInterfaceBlind(PARTInterface): 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): + def verifySCLockRefundSpendTx(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)) @@ -460,7 +460,7 @@ class PARTInterfaceBlind(PARTInterface): 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): + def createSCLockSpendTx(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'] @@ -501,14 +501,14 @@ class PARTInterfaceBlind(PARTInterface): 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.', + 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) return bytes.fromhex(lock_spend_tx_hex) - def verifyLockSpendTx(self, tx_bytes, - lock_tx_bytes, lock_tx_script, - a_pk_f, feerate, vkbv): + def verifySCLockSpendTx(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)) @@ -578,7 +578,7 @@ class PARTInterfaceBlind(PARTInterface): return True - def createScriptLockRefundSpendToFTx(self, tx_lock_refund_bytes, script_lock_refund, pkh_dest, tx_fee_rate, vkbv): + def createSCLockRefundSpendToFTx(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()]) diff --git a/basicswap/ui/page_wallet.py b/basicswap/ui/page_wallet.py index 1f08f88..47d2489 100644 --- a/basicswap/ui/page_wallet.py +++ b/basicswap/ui/page_wallet.py @@ -296,7 +296,7 @@ def page_wallet(self, url_split, post_string): if show_utxo_groups: utxo_groups = '' - unspent_by_addr = swap_client.getUnspentsByAddr(k) + unspent_by_addr = ci.getUnspentsByAddr() sorted_unspent_by_addr = sorted(unspent_by_addr.items(), key=lambda x: x[1], reverse=True) for kv in sorted_unspent_by_addr: diff --git a/bin/basicswap_prepare.py b/bin/basicswap_prepare.py index 3f2a444..3551e8b 100755 --- a/bin/basicswap_prepare.py +++ b/bin/basicswap_prepare.py @@ -53,6 +53,9 @@ PIVX_VERSION_TAG = os.getenv('PIVX_VERSION_TAG', '_scantxoutset') DASH_VERSION = os.getenv('DASH_VERSION', '18.1.0') DASH_VERSION_TAG = os.getenv('DASH_VERSION_TAG', '') +FIRO_VERSION = os.getenv('FIRO_VERSION', '0.14.11.1') +FIRO_VERSION_TAG = os.getenv('FIRO_VERSION_TAG', '') + known_coins = { 'particl': (PARTICL_VERSION, PARTICL_VERSION_TAG, ('tecnovert',)), @@ -62,6 +65,7 @@ known_coins = { 'monero': (MONERO_VERSION, MONERO_VERSION_TAG, ('binaryfate',)), 'pivx': (PIVX_VERSION, PIVX_VERSION_TAG, ('tecnovert',)), 'dash': (DASH_VERSION, DASH_VERSION_TAG, ('pasta',)), + 'firo': (FIRO_VERSION, FIRO_VERSION_TAG, ('reuben',)), } expected_key_ids = { @@ -73,6 +77,7 @@ expected_key_ids = { 'davidburkett38': ('3620E9D387E55666',), 'fuzzbawls': ('3BDCDA2D87A881D9',), 'pasta': ('52527BEDABE87984',), + 'reuben': ('1290A1D0FA7EE109',), } if platform.system() == 'Darwin': @@ -137,6 +142,12 @@ DASH_ONION_PORT = int(os.getenv('DASH_ONION_PORT', 9999)) # nDefaultPort DASH_RPC_USER = os.getenv('DASH_RPC_USER', '') DASH_RPC_PWD = os.getenv('DASH_RPC_PWD', '') +FIRO_RPC_HOST = os.getenv('FIRO_RPC_HOST', '127.0.0.1') +FIRO_RPC_PORT = int(os.getenv('FIRO_RPC_PORT', 8888)) +FIRO_ONION_PORT = int(os.getenv('FIRO_ONION_PORT', 8168)) # nDefaultPort +FIRO_RPC_USER = os.getenv('FIRO_RPC_USER', '') +FIRO_RPC_PWD = os.getenv('FIRO_RPC_PWD', '') + TOR_PROXY_HOST = os.getenv('TOR_PROXY_HOST', '127.0.0.1') TOR_PROXY_PORT = int(os.getenv('TOR_PROXY_PORT', 9050)) TOR_CONTROL_PORT = int(os.getenv('TOR_CONTROL_PORT', 9051)) @@ -211,6 +222,19 @@ def downloadBytes(url): popConnectionParameters() +def importPubkeyFromUrls(gpg, pubkeyurls): + for url in pubkeyurls: + try: + logger.info('Importing public key from url: ' + url) + rv = gpg.import_keys(downloadBytes(url)) + break + except Exception as e: + logging.warning('Import from url failed: %s', str(e)) + + for key in rv.fingerprints: + gpg.trust_keys(key, 'TRUST_FULLY') + + def testTorConnection(): test_url = 'https://check.torproject.org/' logger.info('Testing TOR connection at: ' + test_url) @@ -278,8 +302,14 @@ def extractCore(coin, version_data, settings, bin_dir, release_path, extra_opts= logger.info('extractCore %s v%s%s', coin, version, version_tag) extract_core_overwrite = extra_opts.get('extract_core_overwrite', True) - if coin == 'monero': - bins = ['monerod', 'monero-wallet-rpc'] + if coin in ('monero', 'firo'): + if coin == 'monero': + bins = ['monerod', 'monero-wallet-rpc'] + elif coin == 'firo': + bins = [coin + 'd', coin + '-cli', coin + '-tx'] + else: + raise ValueError('Unknown coin') + num_exist = 0 for b in bins: out_path = os.path.join(bin_dir, b) @@ -412,12 +442,23 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}): release_url = 'https://github.com/dashpay/dash/releases/download/v{}/{}'.format(version + version_tag, release_filename) assert_filename = '{}-{}-{}-build.assert'.format(coin, os_name, major_version) assert_url = 'https://raw.githubusercontent.com/dashpay/gitian.sigs/master/%s-%s/%s/%s' % (version + version_tag, os_dir_name, signing_key_name, assert_filename) + elif coin == 'firo': + raise ValueError('TODO: scantxoutset release') + if BIN_ARCH == 'x86_64-linux-gnu': + arch_name = 'linux64' + file_ext = 'tar.gz' + elif BIN_ARCH == 'osx64': + arch_name = 'macos' + file_ext = 'dmg' + raise ValueError('TODO: Firo - Extract .dmg') + else: + raise ValueError('Firo: Unknown architecture') + release_filename = '{}-{}-{}{}.{}'.format('firo', version + version_tag, arch_name, filename_extra, file_ext) + release_url = 'https://github.com/firoorg/firo/releases/download/v{}/{}'.format(version + version_tag, release_filename) + assert_url = 'https://github.com/firoorg/firo/releases/download/v%s/SHA256SUMS' % (version + version_tag) else: raise ValueError('Unknown coin') - assert_sig_filename = assert_filename + '.sig' - assert_sig_url = assert_url + ('.asc' if major_version >= 22 else '.sig') - release_path = os.path.join(bin_dir, release_filename) if not os.path.exists(release_path): downloadFile(release_url, release_path) @@ -428,10 +469,12 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}): if not os.path.exists(assert_path): downloadFile(assert_url, assert_path) - assert_sig_filename = '{}-{}-{}-build-{}.assert.sig'.format(coin, os_name, version, signing_key_name) - assert_sig_path = os.path.join(bin_dir, assert_sig_filename) - if not os.path.exists(assert_sig_path): - downloadFile(assert_sig_url, assert_sig_path) + if coin not in ('firo', ): + assert_sig_url = assert_url + ('.asc' if major_version >= 22 else '.sig') + assert_sig_filename = '{}-{}-{}-build-{}.assert.sig'.format(coin, os_name, version, signing_key_name) + assert_sig_path = os.path.join(bin_dir, assert_sig_filename) + if not os.path.exists(assert_sig_path): + downloadFile(assert_sig_url, assert_sig_path) hasher = hashlib.sha256() with open(release_path, 'rb') as fp: @@ -462,17 +505,28 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}): for key in rv.fingerprints: gpg.trust_keys(rv.fingerprints[0], 'TRUST_FULLY') + if coin == 'pivx': + pubkey_filename = '{}_{}.pgp'.format('particl', signing_key_name) + else: + pubkey_filename = '{}_{}.pgp'.format(coin, signing_key_name) + pubkeyurls = [ + 'https://raw.githubusercontent.com/tecnovert/basicswap/master/pgp/keys/' + pubkey_filename, + 'https://gitlab.com/particl/basicswap/-/raw/master/pgp/keys/' + pubkey_filename, + ] + if coin == 'dash': + pubkeyurls.append('https://raw.githubusercontent.com/dashpay/dash/master/contrib/gitian-keys/pasta.pgp') if coin == 'monero': + pubkeyurls.append('https://raw.githubusercontent.com/monero-project/monero/master/utils/gpg_keys/binaryfate.asc') + if coin == 'firo': + pubkeyurls.append('https://firo.org/reuben.asc') + + if coin in ('monero', 'firo'): with open(assert_path, 'rb') as fp: verified = gpg.verify_file(fp) if not isValidSignature(verified) and verified.username is None: logger.warning('Signature made by unknown key.') - - pubkeyurl = 'https://raw.githubusercontent.com/monero-project/monero/master/utils/gpg_keys/binaryfate.asc' - logger.info('Importing public key from url: ' + pubkeyurl) - rv = gpg.import_keys(downloadBytes(pubkeyurl)) - gpg.trust_keys(rv.fingerprints[0], 'TRUST_FULLY') + importPubkeyFromUrls(gpg, pubkeyurls) with open(assert_path, 'rb') as fp: verified = gpg.verify_file(fp) else: @@ -481,28 +535,7 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}): if not isValidSignature(verified) and verified.username is None: logger.warning('Signature made by unknown key.') - - if coin == 'pivx': - filename = '{}_{}.pgp'.format('particl', signing_key_name) - else: - filename = '{}_{}.pgp'.format(coin, signing_key_name) - pubkeyurls = [ - 'https://raw.githubusercontent.com/tecnovert/basicswap/master/pgp/keys/' + filename, - 'https://gitlab.com/particl/basicswap/-/raw/master/pgp/keys/' + filename, - ] - if coin == 'dash': - pubkeyurls.append('https://raw.githubusercontent.com/dashpay/dash/master/contrib/gitian-keys/pasta.pgp') - for url in pubkeyurls: - try: - logger.info('Importing public key from url: ' + url) - rv = gpg.import_keys(downloadBytes(url)) - break - except Exception as e: - logging.warning('Import from url failed: %s', str(e)) - - for key in rv.fingerprints: - gpg.trust_keys(key, 'TRUST_FULLY') - + importPubkeyFromUrls(gpg, pubkeyurls) with open(assert_sig_path, 'rb') as fp: verified = gpg.verify_file(fp, assert_path) @@ -600,12 +633,13 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}): with open(core_conf_path, 'w') as fp: if chain != 'mainnet': fp.write(chain + '=1\n') - if chain == 'testnet': - fp.write('[test]\n\n') - if chain == 'regtest': - fp.write('[regtest]\n\n') - else: - logger.warning('Unknown chain %s', chain) + if coin != 'firo': + if chain == 'testnet': + fp.write('[test]\n\n') + elif chain == 'regtest': + fp.write('[regtest]\n\n') + else: + logger.warning('Unknown chain %s', chain) if COINS_RPCBIND_IP != '127.0.0.1': fp.write('rpcallowip=127.0.0.1\n') @@ -656,6 +690,13 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}): fp.write('fallbackfee=0.0002\n') if DASH_RPC_USER != '': fp.write('rpcauth={}:{}${}\n'.format(DASH_RPC_USER, salt, password_to_hmac(salt, DASH_RPC_PWD))) + elif coin == 'firo': + fp.write('prune=4000\n') + fp.write('fallbackfee=0.0002\n') + fp.write('txindex=0\n') + fp.write('usehd=1\n') + if FIRO_RPC_USER != '': + fp.write('rpcauth={}:{}${}\n'.format(FIRO_RPC_USER, salt, password_to_hmac(salt, FIRO_RPC_PWD))) else: logger.warning('Unknown coin %s', coin) @@ -886,6 +927,9 @@ def initialise_wallets(particl_wallet_mnemonic, with_coins, data_dir, settings, filename = coin_name + 'd' + ('.exe' if os.name == 'nt' else '') coin_args = ['-nofindpeers', '-nostaking'] if c == Coins.PART else [] + if c == Coins.FIRO: + coin_args += ['-hdseed={}'.format(swap_client.getWalletKey(Coins.FIRO, 1).hex())] + daemons.append(startDaemon(coin_settings['datadir'], coin_settings['bindir'], filename, daemon_args + coin_args)) swap_client.setDaemonPID(c, daemons[-1].pid) swap_client.setCoinRunParams(c) @@ -1173,7 +1217,7 @@ def main(): }, 'dash': { 'connection_type': 'rpc' if 'dash' in with_coins else 'none', - 'manage_daemon': True if ('dash' in with_coins and PIVX_RPC_HOST == '127.0.0.1') else False, + 'manage_daemon': True if ('dash' in with_coins and DASH_RPC_HOST == '127.0.0.1') else False, 'rpchost': DASH_RPC_HOST, 'rpcport': DASH_RPC_PORT + port_offset, 'onionport': DASH_ONION_PORT + port_offset, @@ -1185,6 +1229,21 @@ def main(): 'conf_target': 2, 'core_version_group': 18, 'chain_lookups': 'local', + }, + 'firo': { + 'connection_type': 'rpc' if 'firo' in with_coins else 'none', + 'manage_daemon': True if ('firo' in with_coins and FIRO_RPC_HOST == '127.0.0.1') else False, + 'rpchost': FIRO_RPC_HOST, + 'rpcport': FIRO_RPC_PORT + port_offset, + 'onionport': FIRO_ONION_PORT + port_offset, + 'datadir': os.getenv('FIRO_DATA_DIR', os.path.join(data_dir, 'firo')), + 'bindir': os.path.join(bin_dir, 'firo'), + 'use_segwit': False, + 'use_csv': True, + 'blocks_confirmed': 1, + 'conf_target': 2, + 'core_version_group': 18, + 'chain_lookups': 'local', } } @@ -1203,6 +1262,9 @@ def main(): if DASH_RPC_USER != '': chainclients['dash']['rpcuser'] = DASH_RPC_USER chainclients['dash']['rpcpassword'] = DASH_RPC_PWD + if FIRO_RPC_USER != '': + chainclients['firo']['rpcuser'] = FIRO_RPC_USER + chainclients['firo']['rpcpassword'] = FIRO_RPC_PWD chainclients['monero']['walletsdir'] = os.getenv('XMR_WALLETS_DIR', chainclients['monero']['datadir']) diff --git a/pgp/keys/firo_reuben.pgp b/pgp/keys/firo_reuben.pgp new file mode 100644 index 0000000..709dcb6 --- /dev/null +++ b/pgp/keys/firo_reuben.pgp @@ -0,0 +1,13 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mDMEX7UxaxYJKwYBBAHaRw8BAQdAjb4i983N4ysLmcn6RyeTwctpB2EppSc7qJ6l +yb0pezm0IXJldWJlbkBmaXJvLm9yZyA8cmV1YmVuQGZpcm8ub3JnPoiPBBAWCgAg +BQJftTFrBgsJBwgDAgQVCAoCBBYCAQACGQECGwMCHgEAIQkQEpCh0Pp+4QkWIQQB +hkVNY+g9he+R3k4SkKHQ+n7hCaMKAP9pYkzGWBNRZyvLnUVob9mV+1rOQfNM0T8p +Pmj9rIl+fgEAw8ae8Suhotv9DawP90ehFNUNUwxKz4b2zJgzz5Y7ewO4OARftTFr +EgorBgEEAZdVAQUBAQdAi86WcrR9ArfA48pJ6hFiPilSfYLd7vZJ4UgeN/I6kB4D +AQgHiHgEGBYIAAkFAl+1MWsCGwwAIQkQEpCh0Pp+4QkWIQQBhkVNY+g9he+R3k4S +kKHQ+n7hCdfMAP49okBRnhH7n4VLmfdygZUDJyfMSPG4CBC+wL2igQMqBAEAjnAf +VjbDqrZ5bf6eBbSEblyyQBtKvlARiNUu1oNsogw= +=PWmb +-----END PGP PUBLIC KEY BLOCK----- diff --git a/pgp/keys/monero_binaryfate.asc b/pgp/keys/monero_binaryfate.pgp similarity index 100% rename from pgp/keys/monero_binaryfate.asc rename to pgp/keys/monero_binaryfate.pgp diff --git a/tests/basicswap/common.py b/tests/basicswap/common.py index 22cf30a..4c8cb77 100644 --- a/tests/basicswap/common.py +++ b/tests/basicswap/common.py @@ -308,6 +308,10 @@ def delay_for(delay_event, delay_for=60): delay_event.wait(delay_for) +def make_boolean(s): + return s.lower() in ['1', 'true'] + + def make_rpc_func(node_id, base_rpc_port=BASE_RPC_PORT): node_id = node_id auth = 'test{0}:test_pass{0}'.format(node_id) diff --git a/tests/basicswap/extended/test_dash.py b/tests/basicswap/extended/test_dash.py index 5a659d4..2b12a4d 100644 --- a/tests/basicswap/extended/test_dash.py +++ b/tests/basicswap/extended/test_dash.py @@ -528,13 +528,13 @@ class Test(unittest.TestCase): def test_08_withdrawal(self): logging.info('---------- Test DASH withdrawals') - pivx_addr = dashRpc('getnewaddress \"Withdrawal test\"') - wallets0 = read_json_api(TEST_HTTP_PORT + 0, 'wallets') - assert (float(wallets0['DASH']['balance']) > 100) + addr = dashRpc('getnewaddress \"Withdrawal test\"') + wallets = read_json_api(TEST_HTTP_PORT + 0, 'wallets') + assert (float(wallets['DASH']['balance']) > 100) post_json = { 'value': 100, - 'address': pivx_addr, + 'address': addr, 'subfee': False, } json_rv = json.loads(post_json_req('http://127.0.0.1:{}/json/wallets/dash/withdraw'.format(TEST_HTTP_PORT + 0), post_json)) diff --git a/tests/basicswap/extended/test_firo.py b/tests/basicswap/extended/test_firo.py new file mode 100644 index 0000000..638beb8 --- /dev/null +++ b/tests/basicswap/extended/test_firo.py @@ -0,0 +1,465 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# Copyright (c) 2022 tecnovert +# Distributed under the MIT software license, see the accompanying +# file LICENSE or http://www.opensource.org/licenses/mit-license.php. + +import os +import json +import random +import logging +import unittest + +import basicswap.config as cfg +from basicswap.basicswap import ( + Coins, + TxStates, + SwapTypes, + BidStates, +) +from basicswap.basicswap_util import ( + TxLockTypes, +) +from basicswap.util import ( + COIN, + make_int, + format_amount, +) +from basicswap.rpc import ( + callrpc_cli, + waitForRPC, +) +from tests.basicswap.common import ( + stopDaemons, + wait_for_bid, + make_rpc_func, + read_json_api, + post_json_req, + TEST_HTTP_PORT, + wait_for_offer, + wait_for_in_progress, + wait_for_bid_tx_state, +) +from basicswap.contrib.test_framework.messages import ( + FromHex, + CTransaction, +) +from bin.basicswap_run import startDaemon +from basicswap.contrib.rpcauth import generate_salt, password_to_hmac +from tests.basicswap.test_xmr import BaseTest, test_delay_event, callnoderpc + +logger = logging.getLogger() + +FIRO_BASE_PORT = 34832 +FIRO_BASE_RPC_PORT = 35832 +FIRO_BASE_ZMQ_PORT = 36832 + + +def firoCli(cmd, node_id=0): + return callrpc_cli(cfg.FIRO_BINDIR, os.path.join(cfg.TEST_DATADIRS, 'firo_' + str(node_id)), 'regtest', cmd, cfg.FIRO_CLI) + + +def prepareDataDir(datadir, node_id, conf_file, dir_prefix, base_p2p_port, base_rpc_port, num_nodes=3): + node_dir = os.path.join(datadir, dir_prefix + str(node_id)) + if not os.path.exists(node_dir): + os.makedirs(node_dir) + cfg_file_path = os.path.join(node_dir, conf_file) + if os.path.exists(cfg_file_path): + return + with open(cfg_file_path, 'w+') as fp: + fp.write('regtest=1\n') + fp.write('port=' + str(base_p2p_port + node_id) + '\n') + fp.write('rpcport=' + str(base_rpc_port + node_id) + '\n') + + salt = generate_salt(16) + fp.write('rpcauth={}:{}${}\n'.format('test' + str(node_id), salt, password_to_hmac(salt, 'test_pass' + str(node_id)))) + + fp.write('daemon=0\n') + fp.write('dandelion=0\n') + fp.write('printtoconsole=0\n') + fp.write('server=1\n') + fp.write('discover=0\n') + fp.write('listenonion=0\n') + fp.write('bind=127.0.0.1\n') + fp.write('findpeers=0\n') + fp.write('debug=1\n') + fp.write('debugexclude=libevent\n') + + fp.write('fallbackfee=0.01\n') + fp.write('acceptnonstdtxn=0\n') + + # qa/rpc-tests/segwit.py + fp.write('prematurewitness=1\n') + fp.write('walletprematurewitness=1\n') + fp.write('blockversion=4\n') + fp.write('promiscuousmempoolflags=517\n') + + for i in range(0, num_nodes): + if node_id == i: + continue + fp.write('addnode=127.0.0.1:{}\n'.format(base_p2p_port + i)) + + return node_dir + + +class Test(BaseTest): + __test__ = True + firo_daemons = [] + firo_addr = None + test_coin_from = Coins.FIRO + + test_atomic = True + test_xmr = False + + @classmethod + def setUpClass(cls): + cls.start_ltc_nodes = False + cls.start_xmr_nodes = False + super(Test, cls).setUpClass() + + @classmethod + def prepareExtraDataDir(cls, i): + if not cls.restore_instance: + data_dir = prepareDataDir(cfg.TEST_DATADIRS, i, 'firo.conf', 'firo_', base_p2p_port=FIRO_BASE_PORT, base_rpc_port=FIRO_BASE_RPC_PORT) + if os.path.exists(os.path.join(cfg.FIRO_BINDIR, 'firo-wallet')): + callrpc_cli(cfg.FIRO_BINDIR, data_dir, 'regtest', '-wallet=wallet.dat create', 'firo-wallet') + + cls.firo_daemons.append(startDaemon(os.path.join(cfg.TEST_DATADIRS, 'firo_' + str(i)), cfg.FIRO_BINDIR, cfg.FIROD)) + logging.info('Started %s %d', cfg.FIROD, cls.part_daemons[-1].pid) + + waitForRPC(make_rpc_func(i, base_rpc_port=FIRO_BASE_RPC_PORT)) + + @classmethod + def addPIDInfo(cls, sc, i): + sc.setDaemonPID(Coins.FIRO, cls.firo_daemons[i].pid) + + @classmethod + def prepareExtraCoins(cls): + if cls.restore_instance: + void_block_rewards_pubkey = cls.getRandomPubkey() + cls.firo_addr = cls.swap_clients[0].ci(Coins.FIRO).pubkey_to_address(void_block_rewards_pubkey) + else: + num_blocks = 400 + cls.firo_addr = callnoderpc(0, 'getnewaddress', ['mining_addr'], base_rpc_port=FIRO_BASE_RPC_PORT) + # cls.firo_addr = callnoderpc(0, 'addwitnessaddress', [cls.firo_addr], base_rpc_port=FIRO_BASE_RPC_PORT) + logging.info('Mining %d Firo blocks to %s', num_blocks, cls.firo_addr) + callnoderpc(0, 'generatetoaddress', [num_blocks, cls.firo_addr], base_rpc_port=FIRO_BASE_RPC_PORT) + + firo_addr1 = callnoderpc(1, 'getnewaddress', ['initial addr'], base_rpc_port=FIRO_BASE_RPC_PORT) + # firo_addr1 = callnoderpc(1, 'addwitnessaddress', [firo_addr1], base_rpc_port=FIRO_BASE_RPC_PORT) + for i in range(5): + callnoderpc(0, 'sendtoaddress', [firo_addr1, 1000], base_rpc_port=FIRO_BASE_RPC_PORT) + + # Set future block rewards to nowhere (a random address), so wallet amounts stay constant + void_block_rewards_pubkey = cls.getRandomPubkey() + cls.firo_addr = cls.swap_clients[0].ci(Coins.FIRO).pubkey_to_address(void_block_rewards_pubkey) + num_blocks = 100 + logging.info('Mining %d Firo blocks to %s', num_blocks, cls.firo_addr) + callnoderpc(0, 'generatetoaddress', [num_blocks, cls.firo_addr], base_rpc_port=FIRO_BASE_RPC_PORT) + + @classmethod + def tearDownClass(cls): + logging.info('Finalising FIRO Test') + super(Test, cls).tearDownClass() + + stopDaemons(cls.firo_daemons) + + @classmethod + def addCoinSettings(cls, settings, datadir, node_id): + settings['chainclients']['firo'] = { + 'connection_type': 'rpc', + 'manage_daemon': False, + 'rpcport': FIRO_BASE_RPC_PORT + node_id, + 'rpcuser': 'test' + str(node_id), + 'rpcpassword': 'test_pass' + str(node_id), + 'datadir': os.path.join(datadir, 'firo_' + str(node_id)), + 'bindir': cfg.FIRO_BINDIR, + 'use_csv': True, + 'use_segwit': False, + } + + @classmethod + def coins_loop(cls): + super(Test, cls).coins_loop() + callnoderpc(0, 'generatetoaddress', [1, cls.firo_addr], base_rpc_port=FIRO_BASE_RPC_PORT) + + def getBalance(self, js_wallets): + return float(js_wallets[self.test_coin_from.name]['balance']) + float(js_wallets[self.test_coin_from.name]['unconfirmed']) + + def getXmrBalance(self, js_wallets): + return float(js_wallets[Coins.XMR.name]['unconfirmed']) + float(js_wallets[Coins.XMR.name]['balance']) + + def callnoderpc(self, method, params=[], wallet=None, node_id=0): + return callnoderpc(node_id, method, params, wallet, base_rpc_port=FIRO_BASE_RPC_PORT) + + def test_01_firo(self): + logging.info('---------- Test {} segwit'.format(self.test_coin_from.name)) + + ''' + Segwit is not currently enabled: + https://github.com/firoorg/firo/blob/master/src/validation.cpp#L4425 + + Txns spending segwit utxos don't get mined. + ''' + + swap_clients = self.swap_clients + + addr_plain = firoCli('getnewaddress \"segwit test\"') + addr_witness = firoCli(f'addwitnessaddress {addr_plain}') + addr_witness_info = firoCli(f'validateaddress {addr_witness}') + txid = firoCli(f'sendtoaddress {addr_witness} 1.0') + assert len(txid) == 64 + + self.callnoderpc('generatetoaddress', [1, self.firo_addr]) + ''' + TODO: Add back when segwit is active + ro = self.callnoderpc('scantxoutset', ['start', ['addr({})'.format(addr_witness)]]) + assert (len(ro['unspents']) == 1) + assert (ro['unspents'][0]['txid'] == txid) + ''' + + tx_wallet = firoCli(f'gettransaction {txid}') + tx_hex = tx_wallet['hex'] + tx = firoCli(f'decoderawtransaction {tx_hex}') + + prevout_n = -1 + for txo in tx['vout']: + if addr_witness in txo['scriptPubKey']['addresses']: + prevout_n = txo['n'] + break + assert prevout_n > -1 + + tx_funded = firoCli(f'createrawtransaction [{{\\"txid\\":\\"{txid}\\",\\"vout\\":{prevout_n}}}] {{\\"{addr_plain}\\":0.99}}') + tx_signed = firoCli(f'signrawtransaction {tx_funded}')['hex'] + + # Add scriptsig for txids to match + decoded_tx = CTransaction() + decoded_tx = FromHex(decoded_tx, tx_funded) + decoded_tx.vin[0].scriptSig = bytes.fromhex('16' + addr_witness_info['hex']) + txid_with_scriptsig = decoded_tx.rehash() + + tx_funded_decoded = firoCli(f'decoderawtransaction {tx_funded}') + tx_signed_decoded = firoCli(f'decoderawtransaction {tx_signed}') + assert tx_funded_decoded['txid'] != tx_signed_decoded['txid'] + assert txid_with_scriptsig == tx_signed_decoded['txid'] + + def test_02_part_coin(self): + logging.info('---------- Test PART to {}'.format(self.test_coin_from.name)) + if not self.test_atomic: + logging.warning('Skipping test') + return + swap_clients = self.swap_clients + + offer_id = swap_clients[0].postOffer(Coins.PART, self.test_coin_from, 100 * COIN, 0.1 * COIN, 100 * COIN, SwapTypes.SELLER_FIRST) + + wait_for_offer(test_delay_event, swap_clients[1], offer_id) + offers = swap_clients[1].listOffers() + assert (len(offers) == 1) + for offer in offers: + if offer.offer_id == offer_id: + bid_id = swap_clients[1].postBid(offer_id, offer.amount_from) + + wait_for_bid(test_delay_event, swap_clients[0], bid_id) + swap_clients[0].acceptBid(bid_id) + + wait_for_in_progress(test_delay_event, swap_clients[1], bid_id, sent=True) + wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=60) + wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=60) + + js_0 = read_json_api(1800) + js_1 = read_json_api(1801) + assert (js_0['num_swapping'] == 0 and js_0['num_watched_outputs'] == 0) + assert (js_1['num_swapping'] == 0 and js_1['num_watched_outputs'] == 0) + + def test_03_coin_part(self): + logging.info('---------- Test {} to PART'.format(self.test_coin_from.name)) + swap_clients = self.swap_clients + + offer_id = swap_clients[1].postOffer(self.test_coin_from, Coins.PART, 10 * COIN, 9.0 * COIN, 10 * COIN, SwapTypes.SELLER_FIRST) + + wait_for_offer(test_delay_event, swap_clients[0], offer_id) + offers = swap_clients[0].listOffers() + for offer in offers: + if offer.offer_id == offer_id: + bid_id = swap_clients[0].postBid(offer_id, offer.amount_from) + + wait_for_bid(test_delay_event, swap_clients[1], bid_id) + swap_clients[1].acceptBid(bid_id) + + wait_for_in_progress(test_delay_event, swap_clients[0], bid_id, sent=True) + + wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=60) + wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, wait_for=60) + + js_0 = read_json_api(1800) + js_1 = read_json_api(1801) + assert (js_0['num_swapping'] == 0 and js_0['num_watched_outputs'] == 0) + assert (js_1['num_swapping'] == 0 and js_1['num_watched_outputs'] == 0) + + def test_04_coin_btc(self): + logging.info('---------- Test {} to BTC'.format(self.test_coin_from.name)) + swap_clients = self.swap_clients + + offer_id = swap_clients[0].postOffer(self.test_coin_from, Coins.BTC, 10 * COIN, 0.1 * COIN, 10 * COIN, SwapTypes.SELLER_FIRST) + + wait_for_offer(test_delay_event, swap_clients[1], offer_id) + offers = swap_clients[1].listOffers() + for offer in offers: + if offer.offer_id == offer_id: + bid_id = swap_clients[1].postBid(offer_id, offer.amount_from) + + wait_for_bid(test_delay_event, swap_clients[0], bid_id) + swap_clients[0].acceptBid(bid_id) + + wait_for_in_progress(test_delay_event, swap_clients[1], bid_id, sent=True) + + wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=60) + wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=60) + + js_0bid = read_json_api(1800, 'bids/{}'.format(bid_id.hex())) + + js_0 = read_json_api(1800) + js_1 = read_json_api(1801) + + assert (js_0['num_swapping'] == 0 and js_0['num_watched_outputs'] == 0) + assert (js_1['num_swapping'] == 0 and js_1['num_watched_outputs'] == 0) + + def test_05_refund(self): + # Seller submits initiate txn, buyer doesn't respond + logging.info('---------- Test refund, {} to BTC'.format(self.test_coin_from.name)) + swap_clients = self.swap_clients + + offer_id = swap_clients[0].postOffer(self.test_coin_from, Coins.BTC, 10 * COIN, 0.1 * COIN, 10 * COIN, SwapTypes.SELLER_FIRST, + TxLockTypes.SEQUENCE_LOCK_BLOCKS, 10) + + wait_for_offer(test_delay_event, swap_clients[1], offer_id) + offers = swap_clients[1].listOffers() + for offer in offers: + if offer.offer_id == offer_id: + bid_id = swap_clients[1].postBid(offer_id, offer.amount_from) + + wait_for_bid(test_delay_event, swap_clients[0], bid_id) + swap_clients[1].abandonBid(bid_id) + swap_clients[0].acceptBid(bid_id) + + wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=60) + wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.BID_ABANDONED, sent=True, wait_for=60) + + js_0 = read_json_api(1800) + js_1 = read_json_api(1801) + assert (js_0['num_swapping'] == 0 and js_0['num_watched_outputs'] == 0) + assert (js_1['num_swapping'] == 0 and js_1['num_watched_outputs'] == 0) + + def test_06_self_bid(self): + logging.info('---------- Test same client, BTC to {}'.format(self.test_coin_from.name)) + swap_clients = self.swap_clients + + js_0_before = read_json_api(1800) + + offer_id = swap_clients[0].postOffer(self.test_coin_from, Coins.BTC, 10 * COIN, 10 * COIN, 10 * COIN, SwapTypes.SELLER_FIRST) + + wait_for_offer(test_delay_event, swap_clients[0], offer_id) + offers = swap_clients[0].listOffers() + for offer in offers: + if offer.offer_id == offer_id: + bid_id = swap_clients[0].postBid(offer_id, offer.amount_from) + + wait_for_bid(test_delay_event, swap_clients[0], bid_id) + swap_clients[0].acceptBid(bid_id) + + wait_for_bid_tx_state(test_delay_event, swap_clients[0], bid_id, TxStates.TX_REDEEMED, TxStates.TX_REDEEMED, wait_for=60) + wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=60) + + js_0 = read_json_api(1800) + assert (js_0['num_swapping'] == 0 and js_0['num_watched_outputs'] == 0) + assert (js_0['num_recv_bids'] == js_0_before['num_recv_bids'] + 1 and js_0['num_sent_bids'] == js_0_before['num_sent_bids'] + 1) + + def test_07_error(self): + logging.info('---------- Test error, BTC to {}, set fee above bid value'.format(self.test_coin_from.name)) + swap_clients = self.swap_clients + + js_0_before = read_json_api(1800) + + offer_id = swap_clients[0].postOffer(self.test_coin_from, Coins.BTC, 0.001 * COIN, 1.0 * COIN, 0.001 * COIN, SwapTypes.SELLER_FIRST) + + wait_for_offer(test_delay_event, swap_clients[0], offer_id) + offers = swap_clients[0].listOffers() + for offer in offers: + if offer.offer_id == offer_id: + bid_id = swap_clients[0].postBid(offer_id, offer.amount_from) + + wait_for_bid(test_delay_event, swap_clients[0], bid_id) + swap_clients[0].acceptBid(bid_id) + swap_clients[0].getChainClientSettings(Coins.BTC)['override_feerate'] = 10.0 + swap_clients[0].getChainClientSettings(Coins.FIRO)['override_feerate'] = 10.0 + wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.BID_ERROR, wait_for=60) + + def test_08_withdrawal(self): + logging.info('---------- Test {} withdrawals'.format(self.test_coin_from.name)) + + addr = self.callnoderpc('getnewaddress', ['Withdrawal test', ]) + wallets = read_json_api(TEST_HTTP_PORT + 0, 'wallets') + assert (float(wallets[self.test_coin_from.name]['balance']) > 100) + + post_json = { + 'value': 100, + 'address': addr, + 'subfee': False, + } + json_rv = json.loads(post_json_req('http://127.0.0.1:{}/json/wallets/{}/withdraw'.format(TEST_HTTP_PORT + 0, self.test_coin_from.name.lower()), post_json)) + assert (len(json_rv['txid']) == 64) + + def test_101_full_swap(self): + logging.info('---------- Test {} to XMR'.format(self.test_coin_from.name)) + if not self.test_xmr: + logging.warning('Skipping test') + return + swap_clients = self.swap_clients + + js_0 = read_json_api(1800, 'wallets') + node0_from_before = self.getBalance(js_0) + + js_1 = read_json_api(1801, 'wallets') + node1_from_before = self.getBalance(js_1) + + js_0_xmr = read_json_api(1800, 'wallets/xmr') + js_1_xmr = read_json_api(1801, 'wallets/xmr') + + 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 = read_json_api(1801, 'wallets') + node1_from_after = self.getBalance(js_1) + assert (node1_from_after > node1_from_before + (amount_from - 0.05)) + + js_0 = read_json_api(1800, 'wallets') + node0_from_after = self.getBalance(js_0) + # TODO: Discard block rewards + # assert (node0_from_after < node0_from_before - amount_from) + + js_0_xmr_after = read_json_api(1800, 'wallets/xmr') + js_1_xmr_after = read_json_api(1801, 'wallets/xmr') + + 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)) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/basicswap/extended/test_pivx.py b/tests/basicswap/extended/test_pivx.py index 141823f..5ddb897 100644 --- a/tests/basicswap/extended/test_pivx.py +++ b/tests/basicswap/extended/test_pivx.py @@ -538,13 +538,13 @@ class Test(unittest.TestCase): def test_08_withdrawal(self): logging.info('---------- Test PIVX withdrawals') - pivx_addr = pivxRpc('getnewaddress \"Withdrawal test\"') - wallets0 = read_json_api(TEST_HTTP_PORT + 0, 'wallets') - assert (float(wallets0['PIVX']['balance']) > 100) + addr = pivxRpc('getnewaddress \"Withdrawal test\"') + wallets = read_json_api(TEST_HTTP_PORT + 0, 'wallets') + assert (float(wallets['PIVX']['balance']) > 100) post_json = { 'value': 100, - 'address': pivx_addr, + 'address': addr, 'subfee': False, } json_rv = json.loads(post_json_req('http://127.0.0.1:{}/json/wallets/pivx/withdraw'.format(TEST_HTTP_PORT + 0), post_json)) diff --git a/tests/basicswap/extended/test_xmr_persistent.py b/tests/basicswap/extended/test_xmr_persistent.py index 8ed1d17..a398a20 100644 --- a/tests/basicswap/extended/test_xmr_persistent.py +++ b/tests/basicswap/extended/test_xmr_persistent.py @@ -34,6 +34,7 @@ from basicswap.rpc import ( callrpc, ) from tests.basicswap.common import ( + make_boolean, read_json_api, waitForServer, BASE_RPC_PORT, @@ -46,10 +47,6 @@ from tests.basicswap.common_xmr import ( import bin.basicswap_run as runSystem -def make_boolean(s): - return s.lower() in ['1', 'true'] - - test_path = os.path.expanduser(os.getenv('TEST_PATH', '/tmp/test_persistent')) RESET_TEST = make_boolean(os.getenv('RESET_TEST', 'false')) diff --git a/tests/basicswap/test_btc_xmr.py b/tests/basicswap/test_btc_xmr.py index ad51d55..9a48cb7 100644 --- a/tests/basicswap/test_btc_xmr.py +++ b/tests/basicswap/test_btc_xmr.py @@ -27,39 +27,217 @@ from tests.basicswap.common import ( read_json_api, wait_for_offer, wait_for_none_active, + BTC_BASE_RPC_PORT, ) - -from .test_xmr import BaseTest, test_delay_event +from basicswap.contrib.test_framework.messages import ( + ToHex, + FromHex, + CTxIn, + COutPoint, + CTransaction, + CTxInWitness, +) +from basicswap.contrib.test_framework.script import ( + CScript, + OP_CHECKLOCKTIMEVERIFY, + OP_CHECKSEQUENCEVERIFY, +) +from .test_xmr import BaseTest, test_delay_event, callnoderpc 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 - if not hasattr(cls, 'start_pivx_nodes'): - cls.start_pivx_nodes = False - super(Test, cls).setUpClass() - - @classmethod - def tearDownClass(cls): - logging.info('Finalising BTC Test') - super(Test, cls).tearDownClass() + test_coin_from = Coins.BTC + start_ltc_nodes = False def getBalance(self, js_wallets): return float(js_wallets[self.test_coin_from.name]['balance']) + float(js_wallets[self.test_coin_from.name]['unconfirmed']) - def getXmrBalance(self, js_wallets): - return float(js_wallets[Coins.XMR.name]['unconfirmed']) + float(js_wallets[Coins.XMR.name]['balance']) + def callnoderpc(self, method, params=[], wallet=None, node_id=0): + return callnoderpc(node_id, method, params, wallet, base_rpc_port=BTC_BASE_RPC_PORT) + + def test_001_nested_segwit(self): + logging.info('---------- Test {} p2sh nested segwit'.format(self.test_coin_from.name)) + + addr_p2sh_segwit = self.callnoderpc('getnewaddress', ['segwit test', 'p2sh-segwit']) + addr_info = self.callnoderpc('getaddressinfo', [addr_p2sh_segwit, ]) + assert addr_info['script'] == 'witness_v0_keyhash' + + txid = self.callnoderpc('sendtoaddress', [addr_p2sh_segwit, 1.0]) + assert len(txid) == 64 + + self.callnoderpc('generatetoaddress', [1, self.btc_addr]) + ro = self.callnoderpc('scantxoutset', ['start', ['addr({})'.format(addr_p2sh_segwit)]]) + assert (len(ro['unspents']) == 1) + assert (ro['unspents'][0]['txid'] == txid) + + tx_wallet = self.callnoderpc('gettransaction', [txid, ])['hex'] + tx = self.callnoderpc('decoderawtransaction', [tx_wallet, ]) + + prevout_n = -1 + for txo in tx['vout']: + if addr_p2sh_segwit == txo['scriptPubKey']['address']: + prevout_n = txo['n'] + break + assert prevout_n > -1 + + tx_funded = self.callnoderpc('createrawtransaction', [[{'txid': txid, 'vout': prevout_n}], {addr_p2sh_segwit: 0.99}]) + tx_signed = self.callnoderpc('signrawtransactionwithwallet', [tx_funded, ])['hex'] + tx_funded_decoded = self.callnoderpc('decoderawtransaction', [tx_funded, ]) + tx_signed_decoded = self.callnoderpc('decoderawtransaction', [tx_signed, ]) + assert tx_funded_decoded['txid'] != tx_signed_decoded['txid'] + + # Add scriptsig for txids to match + addr_p2sh_segwit_info = self.callnoderpc('getaddressinfo', [addr_p2sh_segwit, ]) + decoded_tx = FromHex(CTransaction(), tx_funded) + decoded_tx.vin[0].scriptSig = bytes.fromhex('16' + addr_p2sh_segwit_info['hex']) + txid_with_scriptsig = decoded_tx.rehash() + assert txid_with_scriptsig == tx_signed_decoded['txid'] + + def test_002_native_segwit(self): + logging.info('---------- Test {} p2sh native segwit'.format(self.test_coin_from.name)) + + addr_segwit = self.callnoderpc('getnewaddress', ['segwit test', 'bech32']) + addr_info = self.callnoderpc('getaddressinfo', [addr_segwit, ]) + assert addr_info['iswitness'] is True + + txid = self.callnoderpc('sendtoaddress', [addr_segwit, 1.0]) + assert len(txid) == 64 + tx_wallet = self.callnoderpc('gettransaction', [txid, ])['hex'] + tx = self.callnoderpc('decoderawtransaction', [tx_wallet, ]) + + self.callnoderpc('generatetoaddress', [1, self.btc_addr]) + ro = self.callnoderpc('scantxoutset', ['start', ['addr({})'.format(addr_segwit)]]) + assert (len(ro['unspents']) == 1) + assert (ro['unspents'][0]['txid'] == txid) + + prevout_n = -1 + for txo in tx['vout']: + if addr_segwit == txo['scriptPubKey']['address']: + prevout_n = txo['n'] + break + assert prevout_n > -1 + + tx_funded = self.callnoderpc('createrawtransaction', [[{'txid': txid, 'vout': prevout_n}], {addr_segwit: 0.99}]) + tx_signed = self.callnoderpc('signrawtransactionwithwallet', [tx_funded, ])['hex'] + tx_funded_decoded = self.callnoderpc('decoderawtransaction', [tx_funded, ]) + tx_signed_decoded = self.callnoderpc('decoderawtransaction', [tx_signed, ]) + assert tx_funded_decoded['txid'] == tx_signed_decoded['txid'] + + def test_003_cltv(self): + logging.info('---------- Test {} cltv'.format(self.test_coin_from.name)) + ci = self.swap_clients[0].ci(self.test_coin_from) + + chain_height = self.callnoderpc('getblockcount') + script = CScript([chain_height + 3, OP_CHECKLOCKTIMEVERIFY, ]) + + script_dest = ci.getScriptDest(script) + tx = CTransaction() + tx.nVersion = ci.txVersion() + tx.vout.append(ci.txoType()(ci.make_int(1.1), script_dest)) + tx_hex = ToHex(tx) + tx_funded = self.callnoderpc('fundrawtransaction', [tx_hex]) + utxo_pos = 0 if tx_funded['changepos'] == 1 else 1 + tx_signed = self.callnoderpc('signrawtransactionwithwallet', [tx_funded['hex'], ])['hex'] + txid = self.callnoderpc('sendrawtransaction', [tx_signed, ]) + + addr_out = self.callnoderpc('getnewaddress', ['csv test', 'bech32']) + pkh = ci.decodeSegwitAddress(addr_out) + script_out = ci.getScriptForPubkeyHash(pkh) + + tx_spend = CTransaction() + tx_spend.nVersion = ci.txVersion() + tx_spend.nLockTime = chain_height + 3 + tx_spend.vin.append(CTxIn(COutPoint(int(txid, 16), utxo_pos))) + tx_spend.vout.append(ci.txoType()(ci.make_int(1.0999), script_out)) + tx_spend.wit.vtxinwit.append(CTxInWitness()) + tx_spend.wit.vtxinwit[0].scriptWitness.stack = [script, ] + tx_spend_hex = ToHex(tx_spend) + try: + txid = self.callnoderpc('sendrawtransaction', [tx_spend_hex, ]) + assert False, 'Should fail' + except Exception as e: + assert ('non-final' in str(e)) + + self.callnoderpc('generatetoaddress', [49, self.btc_addr]) + txid = self.callnoderpc('sendrawtransaction', [tx_spend_hex, ]) + self.callnoderpc('generatetoaddress', [1, self.btc_addr]) + ro = self.callnoderpc('listreceivedbyaddress', [0, ]) + sum_addr = 0 + for entry in ro: + if entry['address'] == addr_out: + sum_addr += entry['amount'] + assert (sum_addr == 1.0999) + + def test_004_csv(self): + logging.info('---------- Test {} csv'.format(self.test_coin_from.name)) + swap_clients = self.swap_clients + ci = self.swap_clients[0].ci(self.test_coin_from) + + script = CScript([3, OP_CHECKSEQUENCEVERIFY, ]) + + script_dest = ci.getScriptDest(script) + tx = CTransaction() + tx.nVersion = ci.txVersion() + tx.vout.append(ci.txoType()(ci.make_int(1.1), script_dest)) + tx_hex = ToHex(tx) + tx_funded = self.callnoderpc('fundrawtransaction', [tx_hex]) + utxo_pos = 0 if tx_funded['changepos'] == 1 else 1 + tx_signed = self.callnoderpc('signrawtransactionwithwallet', [tx_funded['hex'], ])['hex'] + txid = self.callnoderpc('sendrawtransaction', [tx_signed, ]) + + addr_out = self.callnoderpc('getnewaddress', ['csv test', 'bech32']) + pkh = ci.decodeSegwitAddress(addr_out) + script_out = ci.getScriptForPubkeyHash(pkh) + + tx_spend = CTransaction() + tx_spend.nVersion = ci.txVersion() + tx_spend.vin.append(CTxIn(COutPoint(int(txid, 16), utxo_pos), + nSequence=3)) + tx_spend.vout.append(ci.txoType()(ci.make_int(1.0999), script_out)) + tx_spend.wit.vtxinwit.append(CTxInWitness()) + tx_spend.wit.vtxinwit[0].scriptWitness.stack = [script, ] + tx_spend_hex = ToHex(tx_spend) + try: + txid = self.callnoderpc('sendrawtransaction', [tx_spend_hex, ]) + assert False, 'Should fail' + except Exception as e: + assert ('non-BIP68-final' in str(e)) + + self.callnoderpc('generatetoaddress', [3, self.btc_addr]) + txid = self.callnoderpc('sendrawtransaction', [tx_spend_hex, ]) + self.callnoderpc('generatetoaddress', [1, self.btc_addr]) + ro = self.callnoderpc('listreceivedbyaddress', [0, ]) + sum_addr = 0 + for entry in ro: + if entry['address'] == addr_out: + sum_addr += entry['amount'] + assert (sum_addr == 1.0999) + + def test_005_watchonly(self): + logging.info('---------- Test {} watchonly'.format(self.test_coin_from.name)) + + addr = self.callnoderpc('getnewaddress', ['watchonly test', 'bech32']) + ro = self.callnoderpc('importaddress', [addr, '', False], node_id=1) + txid = self.callnoderpc('sendtoaddress', [addr, 1.0]) + tx_hex = self.callnoderpc('getrawtransaction', [txid, ]) + self.callnoderpc('sendrawtransaction', [tx_hex, ], node_id=1) + ro = self.callnoderpc('gettransaction', [txid, ], node_id=1) + assert (ro['txid'] == txid) + balances = self.callnoderpc('getbalances', node_id=1) + assert (balances['watchonly']['trusted'] + balances['watchonly']['untrusted_pending'] >= 1.0) + + def test_006_getblock_verbosity(self): + logging.info('---------- Test {} getblock verbosity'.format(self.test_coin_from.name)) + + best_hash = self.callnoderpc('getbestblockhash') + block = self.callnoderpc('getblock', [best_hash, 2]) + assert ('vin' in block['tx'][0]) def test_01_full_swap(self): - logging.info('---------- Test {} to XMR'.format(str(self.test_coin_from))) + logging.info('---------- Test {} to XMR'.format(self.test_coin_from.name)) swap_clients = self.swap_clients js_0 = read_json_api(1800, 'wallets') @@ -108,7 +286,7 @@ class Test(BaseTest): 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))) + logging.info('---------- Test {} to XMR leader recovers coin a lock tx'.format(self.test_coin_from.name)) swap_clients = self.swap_clients js_w0_before = read_json_api(1800, 'wallets') @@ -143,7 +321,7 @@ class Test(BaseTest): # 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))) + logging.info('---------- Test {} to XMR follower recovers coin a lock tx'.format(self.test_coin_from.name)) swap_clients = self.swap_clients js_w0_before = read_json_api(1800, 'wallets') @@ -186,7 +364,7 @@ class Test(BaseTest): 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))) + logging.info('---------- Test {} to XMR follower recovers coin b lock tx'.format(self.test_coin_from.name)) swap_clients = self.swap_clients diff --git a/tests/basicswap/test_other.py b/tests/basicswap/test_other.py index af10dc4..6f7d716 100644 --- a/tests/basicswap/test_other.py +++ b/tests/basicswap/test_other.py @@ -212,7 +212,7 @@ class Test(unittest.TestCase): pk = ci.getPubkey(vk) sig = ci.signCompact(vk, 'test signing message') assert (len(sig) == 64) - ci.verifyCompact(pk, 'test signing message', sig) + ci.verifyCompactSig(pk, 'test signing message', sig) def test_pubkey_to_address(self): coin_settings = {'rpcport': 0, 'rpcauth': 'none'} diff --git a/tests/basicswap/test_xmr.py b/tests/basicswap/test_xmr.py index 5d6da61..061f04b 100644 --- a/tests/basicswap/test_xmr.py +++ b/tests/basicswap/test_xmr.py @@ -59,6 +59,7 @@ from basicswap.http_server import ( ) from tests.basicswap.common import ( prepareDataDir, + make_boolean, make_rpc_func, checkForks, stopDaemons, @@ -80,8 +81,6 @@ from tests.basicswap.common import ( BTC_BASE_RPC_PORT, LTC_BASE_PORT, LTC_BASE_RPC_PORT, - PIVX_BASE_PORT, - PIVX_BASE_RPC_PORT, PREFIX_SECRET_KEY_REGTEST, ) from bin.basicswap_run import startDaemon, startXmrDaemon @@ -93,7 +92,6 @@ NUM_NODES = 3 NUM_XMR_NODES = 3 NUM_BTC_NODES = 3 NUM_LTC_NODES = 3 -NUM_PIVX_NODES = 3 TEST_DIR = cfg.TEST_DATADIRS XMR_BASE_P2P_PORT = 17792 @@ -102,6 +100,7 @@ XMR_BASE_ZMQ_PORT = 22792 XMR_BASE_WALLET_RPC_PORT = 23792 test_delay_event = threading.Event() +RESET_TEST = make_boolean(os.getenv('RESET_TEST', 'true')) def prepareXmrDataDir(datadir, node_id, conf_file): @@ -153,7 +152,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, with_coins=set()): +def prepare_swapclient_dir(datadir, node_id, network_key, network_pubkey, with_coins=set(), cls=None): basicswap_dir = os.path.join(datadir, 'basicswap_' + str(node_id)) if not os.path.exists(basicswap_dir): os.makedirs(basicswap_dir) @@ -229,17 +228,8 @@ def prepare_swapclient_dir(datadir, node_id, network_key, network_pubkey, with_c 'use_segwit': True, } - if Coins.PIVX in with_coins: - settings['chainclients']['pivx'] = { - 'connection_type': 'rpc', - 'manage_daemon': False, - 'rpcport': PIVX_BASE_RPC_PORT + node_id, - 'rpcuser': 'test' + str(node_id), - 'rpcpassword': 'test_pass' + str(node_id), - 'datadir': os.path.join(datadir, 'pivx_' + str(node_id)), - 'bindir': cfg.PIVX_BINDIR, - 'use_segwit': False, - } + if cls: + cls.addCoinSettings(settings, datadir, node_id) with open(settings_path, 'w') as fp: json.dump(settings, fp, indent=4) @@ -253,10 +243,6 @@ def ltcCli(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 pivxCli(cmd, node_id=0): - return callrpc_cli(cfg.PIVX_BINDIR, os.path.join(TEST_DIR, 'pivx_' + str(node_id)), 'regtest', cmd, cfg.PIVX_CLI) - - def signal_handler(sig, frame): logging.info('signal {} detected.'.format(sig)) test_delay_event.set() @@ -298,14 +284,7 @@ def run_coins_loop(cls): while not test_delay_event.is_set(): pause_event.wait() try: - if cls.btc_addr is not None: - btcCli('generatetoaddress 1 {}'.format(cls.btc_addr)) - if cls.ltc_addr is not None: - ltcCli('generatetoaddress 1 {}'.format(cls.ltc_addr)) - if cls.pivx_addr is not None: - pivxCli('generatetoaddress 1 {}'.format(cls.pivx_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}) + cls.coins_loop() except Exception as e: logging.warning('run_coins_loop ' + str(e)) test_delay_event.wait(1.0) @@ -320,34 +299,34 @@ def run_loop(cls): class BaseTest(unittest.TestCase): __test__ = False + update_thread = None + coins_update_thread = None + http_threads = [] + swap_clients = [] + part_daemons = [] + btc_daemons = [] + ltc_daemons = [] + xmr_daemons = [] + xmr_wallet_auth = [] + restore_instance = False + + start_ltc_nodes = False + start_xmr_nodes = True + + xmr_addr = None + btc_addr = None + ltc_addr = None + + @classmethod + def getRandomPubkey(cls): + eckey = ECKey() + eckey.generate() + return eckey.get_pubkey().get_bytes() @classmethod def setUpClass(cls): - if not hasattr(cls, 'start_ltc_nodes'): - cls.start_ltc_nodes = False - if not hasattr(cls, 'start_pivx_nodes'): - cls.start_pivx_nodes = False - if not hasattr(cls, 'start_xmr_nodes'): - cls.start_xmr_nodes = True - random.seed(time.time()) - cls.update_thread = None - cls.coins_update_thread = None - cls.http_threads = [] - cls.swap_clients = [] - cls.part_daemons = [] - cls.btc_daemons = [] - cls.ltc_daemons = [] - cls.pivx_daemons = [] - cls.xmr_daemons = [] - cls.xmr_wallet_auth = [] - - cls.xmr_addr = None - cls.btc_addr = None - cls.ltc_addr = None - cls.pivx_addr = None - logger.propagate = False logger.handlers = [] logger.setLevel(logging.INFO) # DEBUG shows many messages from requests.post @@ -356,16 +335,24 @@ class BaseTest(unittest.TestCase): stream_stdout.setFormatter(formatter) logger.addHandler(stream_stdout) + diagrams_dir = 'doc/protocols/sequence_diagrams' + cls.states_bidder = extract_states_from_xu_file(os.path.join(diagrams_dir, 'xmr.bidder.alt.xu'), 'B') + cls.states_offerer = extract_states_from_xu_file(os.path.join(diagrams_dir, 'xmr.offerer.alt.xu'), 'O') + if os.path.isdir(TEST_DIR): - logging.info('Removing ' + TEST_DIR) - for name in os.listdir(TEST_DIR): - if name == 'pivx-params': - continue - fullpath = os.path.join(TEST_DIR, name) - if os.path.isdir(fullpath): - shutil.rmtree(fullpath) - else: - os.remove(fullpath) + if RESET_TEST: + logging.info('Removing ' + TEST_DIR) + for name in os.listdir(TEST_DIR): + if name == 'pivx-params': + continue + fullpath = os.path.join(TEST_DIR, name) + if os.path.isdir(fullpath): + shutil.rmtree(fullpath) + else: + os.remove(fullpath) + else: + logging.info('Restoring instance from ' + TEST_DIR) + cls.restore_instance = True if not os.path.exists(TEST_DIR): os.makedirs(TEST_DIR) @@ -373,39 +360,38 @@ class BaseTest(unittest.TestCase): cls.stream_fp.setFormatter(formatter) logger.addHandler(cls.stream_fp) - diagrams_dir = 'doc/protocols/sequence_diagrams' - cls.states_bidder = extract_states_from_xu_file(os.path.join(diagrams_dir, 'xmr.bidder.alt.xu'), 'B') - cls.states_offerer = extract_states_from_xu_file(os.path.join(diagrams_dir, 'xmr.offerer.alt.xu'), 'O') - try: logging.info('Preparing coin nodes.') for i in range(NUM_NODES): - data_dir = prepareDataDir(TEST_DIR, i, 'particl.conf', 'part_') - if os.path.exists(os.path.join(cfg.PARTICL_BINDIR, 'particl-wallet')): - callrpc_cli(cfg.PARTICL_BINDIR, data_dir, 'regtest', '-wallet=wallet.dat create', 'particl-wallet') + if not cls.restore_instance: + data_dir = prepareDataDir(TEST_DIR, i, 'particl.conf', 'part_') + if os.path.exists(os.path.join(cfg.PARTICL_BINDIR, 'particl-wallet')): + callrpc_cli(cfg.PARTICL_BINDIR, data_dir, 'regtest', '-wallet=wallet.dat create', 'particl-wallet') cls.part_daemons.append(startDaemon(os.path.join(TEST_DIR, 'part_' + str(i)), cfg.PARTICL_BINDIR, cfg.PARTICLD)) logging.info('Started %s %d', cfg.PARTICLD, cls.part_daemons[-1].pid) - for i in range(NUM_NODES): - # Load mnemonics after all nodes have started to avoid staking getting stuck in TryToSync - rpc = make_rpc_func(i) - waitForRPC(rpc) - if i == 0: - rpc('extkeyimportmaster', ['abandon baby cabbage dad eager fabric gadget habit ice kangaroo lab absorb']) - elif i == 1: - rpc('extkeyimportmaster', ['pact mammal barrel matrix local final lecture chunk wasp survey bid various book strong spread fall ozone daring like topple door fatigue limb olympic', '', 'true']) - rpc('getnewextaddress', ['lblExtTest']) - rpc('rescanblockchain') - else: - rpc('extkeyimportmaster', [rpc('mnemonic', ['new'])['master']]) - # Lower output split threshold for more stakeable outputs - rpc('walletsettings', ['stakingoptions', {'stakecombinethreshold': 100, 'stakesplitthreshold': 200}]) + if not cls.restore_instance: + for i in range(NUM_NODES): + # Load mnemonics after all nodes have started to avoid staking getting stuck in TryToSync + rpc = make_rpc_func(i) + waitForRPC(rpc) + if i == 0: + rpc('extkeyimportmaster', ['abandon baby cabbage dad eager fabric gadget habit ice kangaroo lab absorb']) + elif i == 1: + rpc('extkeyimportmaster', ['pact mammal barrel matrix local final lecture chunk wasp survey bid various book strong spread fall ozone daring like topple door fatigue limb olympic', '', 'true']) + rpc('getnewextaddress', ['lblExtTest']) + rpc('rescanblockchain') + else: + rpc('extkeyimportmaster', [rpc('mnemonic', ['new'])['master']]) + # Lower output split threshold for more stakeable outputs + rpc('walletsettings', ['stakingoptions', {'stakecombinethreshold': 100, 'stakesplitthreshold': 200}]) for i in range(NUM_BTC_NODES): - data_dir = prepareDataDir(TEST_DIR, i, 'bitcoin.conf', 'btc_', base_p2p_port=BTC_BASE_PORT, base_rpc_port=BTC_BASE_RPC_PORT) - if os.path.exists(os.path.join(cfg.BITCOIN_BINDIR, 'bitcoin-wallet')): - callrpc_cli(cfg.BITCOIN_BINDIR, data_dir, 'regtest', '-wallet=wallet.dat create', 'bitcoin-wallet') + if not cls.restore_instance: + data_dir = prepareDataDir(TEST_DIR, i, 'bitcoin.conf', 'btc_', base_p2p_port=BTC_BASE_PORT, base_rpc_port=BTC_BASE_RPC_PORT) + if os.path.exists(os.path.join(cfg.BITCOIN_BINDIR, 'bitcoin-wallet')): + callrpc_cli(cfg.BITCOIN_BINDIR, data_dir, 'regtest', '-wallet=wallet.dat create', 'bitcoin-wallet') cls.btc_daemons.append(startDaemon(os.path.join(TEST_DIR, 'btc_' + str(i)), cfg.BITCOIN_BINDIR, cfg.BITCOIND)) logging.info('Started %s %d', cfg.BITCOIND, cls.part_daemons[-1].pid) @@ -414,29 +400,20 @@ class BaseTest(unittest.TestCase): 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') + if not cls.restore_instance: + 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)) - if cls.start_pivx_nodes: - for i in range(NUM_PIVX_NODES): - data_dir = prepareDataDir(TEST_DIR, i, 'pivx.conf', 'pivx_', base_p2p_port=PIVX_BASE_PORT, base_rpc_port=PIVX_BASE_RPC_PORT) - if os.path.exists(os.path.join(cfg.PIVX_BINDIR, 'pivx-wallet')): - callrpc_cli(cfg.PIVX_BINDIR, data_dir, 'regtest', '-wallet=wallet.dat create', 'pivx-wallet') - - cls.pivx_daemons.append(startDaemon(os.path.join(TEST_DIR, 'pivx_' + str(i)), cfg.PIVX_BINDIR, cfg.PIVXD)) - logging.info('Started %s %d', cfg.PIVXD, cls.part_daemons[-1].pid) - - waitForRPC(make_rpc_func(i, base_rpc_port=PIVX_BASE_RPC_PORT)) - if cls.start_xmr_nodes: for i in range(NUM_XMR_NODES): - prepareXmrDataDir(TEST_DIR, i, 'monerod.conf') + if not cls.restore_instance: + prepareXmrDataDir(TEST_DIR, i, 'monerod.conf') cls.xmr_daemons.append(startXmrDaemon(os.path.join(TEST_DIR, 'xmr_' + str(i)), cfg.XMR_BINDIR, cfg.XMRD)) logging.info('Started %s %d', cfg.XMRD, cls.xmr_daemons[-1].pid) @@ -450,14 +427,20 @@ class BaseTest(unittest.TestCase): waitForXMRWallet(i, cls.xmr_wallet_auth[i]) - cls.callxmrnodewallet(cls, i, 'create_wallet', {'filename': 'testwallet', 'language': 'English'}) + if not cls.restore_instance: + cls.callxmrnodewallet(cls, i, 'create_wallet', {'filename': 'testwallet', 'language': 'English'}) cls.callxmrnodewallet(cls, i, 'open_wallet', {'filename': 'testwallet'}) + for i in range(NUM_NODES): + # Hook for descendant classes + cls.prepareExtraDataDir(i) + logging.info('Preparing swap clients.') - eckey = ECKey() - eckey.generate() - cls.network_key = toWIF(PREFIX_SECRET_KEY_REGTEST, eckey.get_bytes()) - cls.network_pubkey = eckey.get_pubkey().get_bytes().hex() + if not cls.restore_instance: + eckey = ECKey() + eckey.generate() + cls.network_key = toWIF(PREFIX_SECRET_KEY_REGTEST, eckey.get_bytes()) + cls.network_pubkey = eckey.get_pubkey().get_bytes().hex() for i in range(NUM_NODES): start_nodes = set() @@ -465,13 +448,15 @@ class BaseTest(unittest.TestCase): start_nodes.add(Coins.LTC) if cls.start_xmr_nodes: start_nodes.add(Coins.XMR) - if cls.start_pivx_nodes: - start_nodes.add(Coins.PIVX) - prepare_swapclient_dir(TEST_DIR, i, cls.network_key, cls.network_pubkey, start_nodes) + if not cls.restore_instance: + prepare_swapclient_dir(TEST_DIR, i, cls.network_key, cls.network_pubkey, start_nodes, cls) 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: settings = json.load(fs) + if cls.restore_instance and i == 1: + cls.network_key = settings['network_key'] + cls.network_pubkey = settings['network_pubkey'] fp = open(os.path.join(basicswap_dir, 'basicswap.log'), 'w') sc = BasicSwap(fp, basicswap_dir, settings, 'regtest', log_name='BasicSwap{}'.format(i)) sc.setDaemonPID(Coins.BTC, cls.btc_daemons[i].pid) @@ -479,6 +464,7 @@ class BaseTest(unittest.TestCase): if cls.start_ltc_nodes: sc.setDaemonPID(Coins.LTC, cls.ltc_daemons[i].pid) + cls.addPIDInfo(sc, i) sc.start() if cls.start_xmr_nodes: @@ -490,74 +476,75 @@ class BaseTest(unittest.TestCase): t = HttpThread(cls.swap_clients[i].fp, TEST_HTTP_HOST, TEST_HTTP_PORT + i, False, cls.swap_clients[i]) cls.http_threads.append(t) t.start() - # Set future block rewards to nowhere (a random address), so wallet amounts stay constant - eckey = ECKey() - eckey.generate() - void_block_rewards_pubkey = eckey.get_pubkey().get_bytes() + void_block_rewards_pubkey = cls.getRandomPubkey() + if cls.restore_instance: + cls.btc_addr = cls.swap_clients[0].ci(Coins.BTC).pubkey_to_segwit_address(void_block_rewards_pubkey) + if cls.start_ltc_nodes: + cls.ltc_addr = cls.swap_clients[0].ci(Coins.LTC).pubkey_to_address(void_block_rewards_pubkey) + if cls.start_xmr_nodes: + cls.xmr_addr = cls.callxmrnodewallet(cls, 1, 'get_address')['address'] + else: + cls.btc_addr = callnoderpc(0, 'getnewaddress', ['mining_addr', 'bech32'], base_rpc_port=BTC_BASE_RPC_PORT) + num_blocks = 400 # Mine enough to activate segwit + logging.info('Mining %d Bitcoin blocks to %s', num_blocks, cls.btc_addr) + callnoderpc(0, 'generatetoaddress', [num_blocks, cls.btc_addr], base_rpc_port=BTC_BASE_RPC_PORT) - cls.btc_addr = callnoderpc(0, 'getnewaddress', ['mining_addr', 'bech32'], base_rpc_port=BTC_BASE_RPC_PORT) - num_blocks = 400 # Mine enough to activate segwit - logging.info('Mining %d Bitcoin blocks to %s', num_blocks, cls.btc_addr) - callnoderpc(0, 'generatetoaddress', [num_blocks, cls.btc_addr], base_rpc_port=BTC_BASE_RPC_PORT) - - # Switch addresses so wallet amounts stay constant - num_blocks = 100 - cls.btc_addr = cls.swap_clients[0].ci(Coins.BTC).pubkey_to_segwit_address(void_block_rewards_pubkey) - logging.info('Mining %d Bitcoin blocks to %s', num_blocks, cls.btc_addr) - callnoderpc(0, 'generatetoaddress', [num_blocks, cls.btc_addr], base_rpc_port=BTC_BASE_RPC_PORT) - - checkForks(callnoderpc(0, 'getblockchaininfo', base_rpc_port=BTC_BASE_RPC_PORT)) - - if cls.start_ltc_nodes: - num_blocks = 400 - 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) - - num_blocks = 31 - cls.ltc_addr = cls.swap_clients[0].ci(Coins.LTC).pubkey_to_address(void_block_rewards_pubkey) - 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) - - # https://github.com/litecoin-project/litecoin/issues/807 - # Block 432 is when MWEB activates. It requires a peg-in. You'll need to generate an mweb address and send some coins to it. Then it will allow you to mine the next block. - mweb_addr = callnoderpc(2, 'getnewaddress', ['mweb_addr', 'mweb'], base_rpc_port=LTC_BASE_RPC_PORT) - callnoderpc(0, 'sendtoaddress', [mweb_addr, 1], base_rpc_port=LTC_BASE_RPC_PORT) - - num_blocks = 69 - cls.ltc_addr = cls.swap_clients[0].ci(Coins.LTC).pubkey_to_address(void_block_rewards_pubkey) - 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)) - - if cls.start_pivx_nodes: - num_blocks = 400 - cls.pivx_addr = callnoderpc(0, 'getnewaddress', ['mining_addr'], base_rpc_port=PIVX_BASE_RPC_PORT) - logging.info('Mining %d PIVX blocks to %s', num_blocks, cls.pivx_addr) - callnoderpc(0, 'generatetoaddress', [num_blocks, cls.pivx_addr], base_rpc_port=PIVX_BASE_RPC_PORT) + btc_addr1 = callnoderpc(1, 'getnewaddress', ['initial addr'], base_rpc_port=BTC_BASE_RPC_PORT) + for i in range(5): + callnoderpc(0, 'sendtoaddress', [btc_addr1, 100], base_rpc_port=BTC_BASE_RPC_PORT) # Switch addresses so wallet amounts stay constant num_blocks = 100 - cls.pivx_addr = cls.swap_clients[0].ci(Coins.PIVX).pubkey_to_address(void_block_rewards_pubkey) - logging.info('Mining %d PIVX blocks to %s', num_blocks, cls.pivx_addr) - callnoderpc(0, 'generatetoaddress', [num_blocks, cls.pivx_addr], base_rpc_port=PIVX_BASE_RPC_PORT) + cls.btc_addr = cls.swap_clients[0].ci(Coins.BTC).pubkey_to_segwit_address(void_block_rewards_pubkey) + logging.info('Mining %d Bitcoin blocks to %s', num_blocks, cls.btc_addr) + callnoderpc(0, 'generatetoaddress', [num_blocks, cls.btc_addr], base_rpc_port=BTC_BASE_RPC_PORT) - num_blocks = 100 - if cls.start_xmr_nodes: - cls.xmr_addr = cls.callxmrnodewallet(cls, 1, 'get_address')['address'] - 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) - callrpc_xmr_na(XMR_BASE_RPC_PORT + 1, 'generateblocks', {'wallet_address': cls.xmr_addr, 'amount_of_blocks': num_blocks}) - logging.info('XMR blocks: %d', callrpc_xmr_na(XMR_BASE_RPC_PORT + 1, 'get_block_count')['count']) + checkForks(callnoderpc(0, 'getblockchaininfo', base_rpc_port=BTC_BASE_RPC_PORT)) - logging.info('Adding anon outputs') - outputs = [] - for i in range(8): - sx_addr = callnoderpc(1, 'getnewstealthaddress') - outputs.append({'address': sx_addr, 'amount': 0.5}) - for i in range(6): - callnoderpc(0, 'sendtypeto', ['part', 'anon', outputs]) + if cls.start_ltc_nodes: + num_blocks = 400 + 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) + + num_blocks = 31 + cls.ltc_addr = cls.swap_clients[0].ci(Coins.LTC).pubkey_to_address(void_block_rewards_pubkey) + 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) + + # https://github.com/litecoin-project/litecoin/issues/807 + # Block 432 is when MWEB activates. It requires a peg-in. You'll need to generate an mweb address and send some coins to it. Then it will allow you to mine the next block. + mweb_addr = callnoderpc(2, 'getnewaddress', ['mweb_addr', 'mweb'], base_rpc_port=LTC_BASE_RPC_PORT) + callnoderpc(0, 'sendtoaddress', [mweb_addr, 1], base_rpc_port=LTC_BASE_RPC_PORT) + + num_blocks = 69 + cls.ltc_addr = cls.swap_clients[0].ci(Coins.LTC).pubkey_to_address(void_block_rewards_pubkey) + 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 cls.start_xmr_nodes: + cls.xmr_addr = cls.callxmrnodewallet(cls, 1, 'get_address')['address'] + 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) + callrpc_xmr_na(XMR_BASE_RPC_PORT + 1, 'generateblocks', {'wallet_address': cls.xmr_addr, 'amount_of_blocks': num_blocks}) + logging.info('XMR blocks: %d', callrpc_xmr_na(XMR_BASE_RPC_PORT + 1, 'get_block_count')['count']) + + logging.info('Adding anon outputs') + outputs = [] + for i in range(8): + sx_addr = callnoderpc(1, 'getnewstealthaddress') + outputs.append({'address': sx_addr, 'amount': 0.5}) + for i in range(6): + callnoderpc(0, 'sendtypeto', ['part', 'anon', outputs]) + + part_addr1 = callnoderpc(1, 'getnewaddress', ['initial addr']) + part_addr2 = callnoderpc(1, 'getnewaddress', ['initial addr 2']) + callnoderpc(0, 'sendtypeto', ['part', 'part', [{'address': part_addr1, 'amount': 100}, {'address': part_addr2, 'amount': 100}]]) + + cls.prepareExtraCoins() logging.info('Starting update thread.') signal.signal(signal.SIGINT, signal_handler) @@ -599,13 +586,40 @@ class BaseTest(unittest.TestCase): stopDaemons(cls.part_daemons) stopDaemons(cls.btc_daemons) stopDaemons(cls.ltc_daemons) - stopDaemons(cls.pivx_daemons) super(BaseTest, cls).tearDownClass() + @classmethod + def addCoinSettings(cls, settings, datadir, node_id): + pass + + @classmethod + def prepareExtraDataDir(cls, i): + pass + + @classmethod + def addPIDInfo(cls, sc, i): + pass + + @classmethod + def prepareExtraCoins(cls): + pass + + @classmethod + def coins_loop(cls): + if cls.btc_addr is not None: + btcCli('generatetoaddress 1 {}'.format(cls.btc_addr)) + if cls.ltc_addr is not None: + ltcCli('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}) + 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) + def getXmrBalance(self, js_wallets): + return float(js_wallets[Coins.XMR.name]['unconfirmed']) + float(js_wallets[Coins.XMR.name]['balance']) + class Test(BaseTest): __test__ = True @@ -617,9 +631,9 @@ class Test(BaseTest): logging.info('---------- Test PART to XMR') swap_clients = self.swap_clients + start_xmr_amount = self.getXmrBalance(read_json_api(1800, 'wallets')) js_1 = read_json_api(1801, 'wallets') - assert (make_int(js_1[Coins.XMR.name]['balance'], scale=12) > 0) - assert (make_int(js_1[Coins.XMR.name]['unconfirmed'], scale=12) > 0) + assert (self.getXmrBalance(js_1) > 0.0) offer_id = swap_clients[0].postOffer(Coins.PART, Coins.XMR, 100 * COIN, 0.11 * XMR_COIN, 100 * COIN, SwapTypes.XMR_SWAP) wait_for_offer(test_delay_event, swap_clients[1], offer_id) @@ -640,8 +654,9 @@ class Test(BaseTest): wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True) js_0_end = read_json_api(1800, 'wallets') - end_xmr = float(js_0_end['XMR']['balance']) + float(js_0_end['XMR']['unconfirmed']) - assert (end_xmr > 10.9 and end_xmr < 11.0) + end_xmr_amount = self.getXmrBalance(js_0_end) + xmr_amount_diff = end_xmr_amount - start_xmr_amount + assert (xmr_amount_diff > 10.9 and xmr_amount_diff < 11.0) bid_id_hex = bid_id.hex() path = f'bids/{bid_id_hex}/states'