diff --git a/basicswap/__init__.py b/basicswap/__init__.py index ef1fd25..7e6d1e5 100644 --- a/basicswap/__init__.py +++ b/basicswap/__init__.py @@ -1,3 +1,3 @@ name = "basicswap" -__version__ = "0.11.66" +__version__ = "0.11.68" diff --git a/basicswap/base.py b/basicswap/base.py index 4f10904..b947d28 100644 --- a/basicswap/base.py +++ b/basicswap/base.py @@ -37,7 +37,6 @@ class BaseApp: def __init__(self, fp, data_dir, settings, chain, log_name='BasicSwap'): self.log_name = log_name self.fp = fp - self.is_running = True self.fail_code = 0 self.mock_time_offset = 0 @@ -49,6 +48,8 @@ class BaseApp: self.mxDB = threading.RLock() self.debug = self.settings.get('debug', False) self.delay_event = threading.Event() + self.chainstate_delay_event = threading.Event() + self._network = None self.prepareLogging() self.log.info('Network: {}'.format(self.chain)) @@ -65,7 +66,7 @@ class BaseApp: def stopRunning(self, with_code=0): self.fail_code = with_code with self.mxDB: - self.is_running = False + self.chainstate_delay_event.set() self.delay_event.set() def prepareLogging(self): diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index e8f3d23..2c4c51b 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -34,6 +34,7 @@ from .interface import Curves from .interface.part import PARTInterface, PARTInterfaceAnon, PARTInterfaceBlind from . import __version__ +from .rpc import escape_rpcauth from .rpc_xmr import make_xmr_rpc2_func from .ui.util import getCoinName from .util import ( @@ -49,7 +50,6 @@ from .util import ( ensure, ) from .util.script import ( - getP2WSH, getP2SHScriptForHash, ) from .util.address import ( @@ -133,7 +133,7 @@ from .basicswap_util import ( strBidState, describeEventEntry, getVoutByAddress, - getVoutByP2WSH, + getVoutByScriptPubKey, getOfferProofOfFundsHash, getLastBidState, isActiveBidState, @@ -146,8 +146,8 @@ from basicswap.db_util import ( remove_expired_data, ) -PROTOCOL_VERSION_SECRET_HASH = 1 -MINPROTO_VERSION_SECRET_HASH = 1 +PROTOCOL_VERSION_SECRET_HASH = 2 +MINPROTO_VERSION_SECRET_HASH = 2 PROTOCOL_VERSION_ADAPTOR_SIG = 3 MINPROTO_VERSION_ADAPTOR_SIG = 3 @@ -164,7 +164,7 @@ def validOfferStateToReceiveBid(offer_state): def threadPollXMRChainState(swap_client, coin_type): ci = swap_client.ci(coin_type) cc = swap_client.coin_clients[coin_type] - while not swap_client.delay_event.is_set(): + while not swap_client.chainstate_delay_event.is_set(): try: new_height = ci.getChainHeight() if new_height != cc['chain_height']: @@ -173,13 +173,13 @@ def threadPollXMRChainState(swap_client, coin_type): cc['chain_height'] = new_height except Exception as e: swap_client.log.warning('threadPollXMRChainState {}, error: {}'.format(ci.ticker(), str(e))) - swap_client.delay_event.wait(random.randrange(20, 30)) # random to stagger updates + swap_client.chainstate_delay_event.wait(random.randrange(20, 30)) # random to stagger updates def threadPollChainState(swap_client, coin_type): ci = swap_client.ci(coin_type) cc = swap_client.coin_clients[coin_type] - while not swap_client.delay_event.is_set(): + while not swap_client.chainstate_delay_event.is_set(): try: chain_state = ci.getBlockchainInfo() if chain_state['bestblockhash'] != cc['chain_best_block']: @@ -191,7 +191,7 @@ def threadPollChainState(swap_client, coin_type): cc['chain_median_time'] = chain_state['mediantime'] except Exception as e: swap_client.log.warning('threadPollChainState {}, error: {}'.format(ci.ticker(), str(e))) - swap_client.delay_event.wait(random.randrange(20, 30)) # random to stagger updates + swap_client.chainstate_delay_event.wait(random.randrange(20, 30)) # random to stagger updates class WatchedOutput(): # Watch for spends @@ -372,7 +372,6 @@ class BasicSwap(BaseApp): self.log.info('Finalise') with self.mxDB: - self.is_running = False self.delay_event.set() if self._network: @@ -593,6 +592,9 @@ class BasicSwap(BaseApp): elif coin == Coins.FIRO: from .interface.firo import FIROInterface return FIROInterface(self.coin_clients[coin], self.chain, self) + elif coin == Coins.NAV: + from .interface.nav import NAVInterface + return NAVInterface(self.coin_clients[coin], self.chain, self) else: raise ValueError('Unknown coin type') @@ -622,16 +624,20 @@ class BasicSwap(BaseApp): datadir_pid = -1 for i in range(20): try: - # Workaround for mismatched pid file name in litecoin 0.21.2 - # Also set with pid= in .conf - # TODO: Remove - if cc['name'] == 'litecoin' and (not os.path.exists(pidfilepath)) and \ - os.path.exists(os.path.join(self.getChainDatadirPath(coin), 'bitcoind.pid')): - pidfilepath = os.path.join(self.getChainDatadirPath(coin), 'bitcoind.pid') + if os.name == 'nt' and cc['core_version_group'] <= 17: + # Older core versions don't write a pid file on windows + pass + else: + # Workaround for mismatched pid file name in litecoin 0.21.2 + # Also set with pid= in .conf + # TODO: Remove + if cc['name'] == 'litecoin' and (not os.path.exists(pidfilepath)) and \ + os.path.exists(os.path.join(self.getChainDatadirPath(coin), 'bitcoind.pid')): + pidfilepath = os.path.join(self.getChainDatadirPath(coin), 'bitcoind.pid') - with open(pidfilepath, 'rb') as fp: - datadir_pid = int(fp.read().decode('utf-8')) - assert (datadir_pid == cc['pid']), 'Mismatched pid' + with open(pidfilepath, 'rb') as fp: + datadir_pid = int(fp.read().decode('utf-8')) + assert (datadir_pid == cc['pid']), 'Mismatched pid' assert (os.path.exists(authcookiepath)) break except Exception as e: @@ -642,9 +648,9 @@ class BasicSwap(BaseApp): if os.name != 'nt' or cc['core_version_group'] > 17: # Litecoin on windows doesn't write a pid file assert (datadir_pid == cc['pid']), 'Mismatched pid' with open(authcookiepath, 'rb') as fp: - cc['rpcauth'] = fp.read().decode('utf-8') + cc['rpcauth'] = escape_rpcauth(fp.read().decode('utf-8')) except Exception as e: - self.log.error('Unable to read authcookie for %s, %s, datadir pid %d, daemon pid %s. Error: %s', str(coin), authcookiepath, datadir_pid, cc['pid'], str(e)) + self.log.error('Unable to read authcookie for %s, %s, datadir pid %d, daemon pid %s. Error: %s', Coins(coin).name, authcookiepath, datadir_pid, cc['pid'], str(e)) raise ValueError('Error, terminating') def createCoinInterface(self, coin): @@ -671,7 +677,7 @@ class BasicSwap(BaseApp): self.createCoinInterface(c) if self.coin_clients[c]['connection_type'] == 'rpc': - if c == Coins.BTC: + if c in (Coins.BTC, ): self.waitForDaemonRPC(c, with_wallet=False) if len(self.callcoinrpc(c, 'listwallets')) >= 1: self.waitForDaemonRPC(c) @@ -760,8 +766,9 @@ class BasicSwap(BaseApp): try: for i in range(num_tries): rv = self.callcoincli(coin, 'stop', timeout=10) - self.log.debug('Trying to stop %s', str(coin)) + self.log.debug('Trying to stop %s', Coins(coin).name) stopping = True + # self.delay_event will be set here time.sleep(i + 1) except Exception as ex: str_ex = str(ex) @@ -772,13 +779,13 @@ class BasicSwap(BaseApp): # Using .cookie is a temporary workaround, will only work if rpc password is unset. # TODO: Query lock on .lock properly if os.path.exists(authcookiepath): - self.log.debug('Waiting on .cookie file %s', str(coin)) + self.log.debug('Waiting on .cookie file %s', Coins(coin).name) time.sleep(i + 1) time.sleep(4) # Extra time to settle return self.log.error('stopDaemon %s', str(ex)) self.log.error(traceback.format_exc()) - raise ValueError('Could not stop {}'.format(str(coin))) + raise ValueError('Could not stop {}'.format(Coins(coin).name)) def stopDaemons(self) -> None: for c in self.activeCoins(): @@ -787,16 +794,20 @@ class BasicSwap(BaseApp): self.stopDaemon(c) def waitForDaemonRPC(self, coin_type, with_wallet: bool = True) -> None: - for i in range(self.startup_tries): - if not self.is_running: + startup_tries = self.startup_tries + chain_client_settings = self.getChainClientSettings(coin_type) + if 'startup_tries' in chain_client_settings: + startup_tries = chain_client_settings['startup_tries'] + for i in range(startup_tries): + if self.delay_event.is_set(): return try: self.coin_clients[coin_type]['interface'].testDaemonRPC(with_wallet) return except Exception as ex: - self.log.warning('Can\'t connect to %s RPC: %s. Trying again in %d second/s.', coin_type, str(ex), (1 + i)) - time.sleep(1 + i) - self.log.error('Can\'t connect to %s RPC, exiting.', coin_type) + self.log.warning('Can\'t connect to %s RPC: %s. Trying again in %d second/s, %d/%d.', Coins(coin_type).name, str(ex), (1 + i), i + 1, startup_tries) + self.delay_event.wait(1 + i) + self.log.error('Can\'t connect to %s RPC, exiting.', Coins(coin_type).name) self.stopRunning(1) # systemd will try to restart the process if fail_code != 0 def checkCoinsReady(self, coin_from, coin_to) -> None: @@ -1762,7 +1773,7 @@ class BasicSwap(BaseApp): def getReceiveAddressForCoin(self, coin_type): new_addr = self.ci(coin_type).getNewAddress(self.coin_clients[coin_type]['use_segwit']) - self.log.debug('Generated new receive address %s for %s', new_addr, str(coin_type)) + self.log.debug('Generated new receive address %s for %s', new_addr, Coins(coin_type).name) return new_addr def getRelayFeeRateForCoin(self, coin_type): @@ -1772,7 +1783,7 @@ class BasicSwap(BaseApp): chain_client_settings = self.getChainClientSettings(coin_type) override_feerate = chain_client_settings.get('override_feerate', None) if override_feerate: - self.log.debug('Fee rate override used for %s: %f', str(coin_type), override_feerate) + self.log.debug('Fee rate override used for %s: %f', Coins(coin_type).name, override_feerate) return override_feerate, 'override_feerate' return self.ci(coin_type).get_fee_rate(conf_target) @@ -1781,7 +1792,7 @@ class BasicSwap(BaseApp): if coin_type == Coins.XMR: self.log.error('TODO: estimateWithdrawFee XMR') return None - tx_vsize = self.getContractSpendTxVSize(coin_type) + tx_vsize = self.ci(coin_type).getHTLCSpendTxVSize() est_fee = (fee_rate * tx_vsize) / 1000 return est_fee @@ -2328,7 +2339,7 @@ class BasicSwap(BaseApp): # Ensure bid is still valid now: int = self.getTime() ensure(bid.expire_at > now, 'Bid expired') - ensure(bid.state in (BidStates.BID_RECEIVED, ), 'Wrong bid state: {}'.format(str(BidStates(bid.state)))) + ensure(bid.state in (BidStates.BID_RECEIVED, ), 'Wrong bid state: {}'.format(BidStates(bid.state).name)) if offer.swap_type == SwapTypes.XMR_SWAP: reverse_bid: bool = Coins(offer.coin_from) in self.scriptless_coins @@ -2932,11 +2943,12 @@ class BasicSwap(BaseApp): return None ci = self.ci(coin_type) - if self.coin_clients[coin_type]['use_segwit']: - addr_to = ci.encode_p2wsh(getP2WSH(initiate_script)) + if ci.using_segwit(): + p2wsh = ci.getScriptDest(initiate_script) + addr_to = ci.encodeScriptDest(p2wsh) else: addr_to = ci.encode_p2sh(initiate_script) - self.log.debug('Create initiate txn for coin %s to %s for bid %s', str(coin_type), addr_to, bid_id.hex()) + self.log.debug('Create initiate txn for coin %s to %s for bid %s', Coins(coin_type).name, addr_to, bid_id.hex()) if prefunded_tx: pi = self.pi(SwapTypes.SELLER_FIRST) @@ -2999,9 +3011,9 @@ class BasicSwap(BaseApp): self.log.debug('bid %s: Make invalid PTx for testing: %d.', bid_id.hex(), bid.debug_ind) self.logBidEvent(bid.bid_id, EventLogTypes.DEBUG_TWEAK_APPLIED, 'ind {}'.format(bid.debug_ind), None) - if self.coin_clients[coin_to]['use_segwit']: - p2wsh = getP2WSH(participate_script) - addr_to = ci.encode_p2wsh(p2wsh) + if ci.using_segwit(): + p2wsh = ci.getScriptDest(participate_script) + addr_to = ci.encodeScriptDest(p2wsh) else: addr_to = ci.encode_p2sh(participate_script) @@ -3014,8 +3026,8 @@ class BasicSwap(BaseApp): txjs = self.callcoinrpc(coin_to, 'decoderawtransaction', [txn_signed]) txid = txjs['txid'] - if self.coin_clients[coin_to]['use_segwit']: - vout = getVoutByP2WSH(txjs, p2wsh.hex()) + if ci.using_segwit(): + vout = getVoutByScriptPubKey(txjs, p2wsh.hex()) else: vout = getVoutByAddress(txjs, addr_to) self.addParticipateTxn(bid_id, bid, coin_to, txid, vout, chain_height) @@ -3024,18 +3036,8 @@ class BasicSwap(BaseApp): return txn_signed - def getContractSpendTxVSize(self, coin_type, redeem: bool = True) -> int: - tx_vsize = 5 # Add a few bytes, sequence in script takes variable amount of bytes - if coin_type == Coins.PART: - tx_vsize += 204 if redeem else 187 - if self.coin_clients[coin_type]['use_segwit']: - tx_vsize += 143 if redeem else 134 - else: - tx_vsize += 323 if redeem else 287 - return tx_vsize - def createRedeemTxn(self, coin_type, bid, for_txn_type='participate', addr_redeem_out=None, fee_rate=None): - self.log.debug('createRedeemTxn for coin %s', str(coin_type)) + self.log.debug('createRedeemTxn for coin %s', Coins(coin_type).name) ci = self.ci(coin_type) if for_txn_type == 'participate': @@ -3049,8 +3051,8 @@ class BasicSwap(BaseApp): txn_script = bid.initiate_tx.script prev_amount = bid.amount - if self.coin_clients[coin_type]['use_segwit']: - prev_p2wsh = getP2WSH(txn_script) + if ci.using_segwit(): + prev_p2wsh = ci.getScriptDest(txn_script) script_pub_key = prev_p2wsh.hex() else: script_pub_key = getP2SHScriptForHash(getKeyID(txn_script)).hex() @@ -3063,7 +3065,10 @@ class BasicSwap(BaseApp): 'amount': ci.format_amount(prev_amount)} bid_date = dt.datetime.fromtimestamp(bid.created_at).date() - wif_prefix = chainparams[Coins.PART][self.chain]['key_prefix'] + if coin_type in (Coins.NAV, ): + wif_prefix = chainparams[coin_type][self.chain]['key_prefix'] + else: + wif_prefix = chainparams[Coins.PART][self.chain]['key_prefix'] pubkey = self.getContractPubkey(bid_date, bid.contract_count) privkey = toWIF(wif_prefix, self.getContractPrivkey(bid_date, bid.contract_count)) @@ -3078,7 +3083,7 @@ class BasicSwap(BaseApp): if fee_rate is None: fee_rate, fee_src = self.getFeeRateForCoin(coin_type) - tx_vsize = self.getContractSpendTxVSize(coin_type) + tx_vsize = ci.getHTLCSpendTxVSize() tx_fee = (fee_rate * tx_vsize) / 1000 self.log.debug('Redeem tx fee %s, rate %s', ci.format_amount(tx_fee, conv_int=True, r=1), str(fee_rate)) @@ -3092,14 +3097,20 @@ class BasicSwap(BaseApp): self.log.debug('addr_redeem_out %s', addr_redeem_out) - redeem_txn = ci.createRedeemTxn(prevout, addr_redeem_out, amount_out) + if ci.use_p2shp2wsh(): + redeem_txn = ci.createRedeemTxn(prevout, addr_redeem_out, amount_out, txn_script) + else: + redeem_txn = ci.createRedeemTxn(prevout, addr_redeem_out, amount_out) options = {} - if self.coin_clients[coin_type]['use_segwit']: + if ci.using_segwit(): options['force_segwit'] = True - redeem_sig = self.callcoinrpc(Coins.PART, 'createsignaturewithkey', [redeem_txn, prevout, privkey, 'ALL', options]) + if coin_type in (Coins.NAV, ): + redeem_sig = ci.getTxSignature(redeem_txn, prevout, privkey) + else: + redeem_sig = self.callcoinrpc(Coins.PART, 'createsignaturewithkey', [redeem_txn, prevout, privkey, 'ALL', options]) - if coin_type == Coins.PART or self.coin_clients[coin_type]['use_segwit']: + if coin_type == Coins.PART or ci.using_segwit(): witness_stack = [ bytes.fromhex(redeem_sig), pubkey, @@ -3115,7 +3126,11 @@ class BasicSwap(BaseApp): script += format(OpCodes.OP_PUSHDATA1, '02x') + format(len(txn_script), '02x') + txn_script.hex() redeem_txn = ci.setTxScriptSig(bytes.fromhex(redeem_txn), 0, bytes.fromhex(script)).hex() - ro = self.callcoinrpc(Coins.PART, 'verifyrawtransaction', [redeem_txn, [prevout]]) + if coin_type in (Coins.NAV, ): + # Only checks signature + ro = ci.verifyRawTransaction(redeem_txn, [prevout]) + else: + ro = self.callcoinrpc(Coins.PART, 'verifyrawtransaction', [redeem_txn, [prevout]]) ensure(ro['inputs_valid'] is True, 'inputs_valid is false') # outputs_valid will be false if not a Particl txn # ensure(ro['complete'] is True, 'complete is false') @@ -3125,16 +3140,15 @@ class BasicSwap(BaseApp): # Check fee if ci.get_connection_type() == 'rpc': redeem_txjs = self.callcoinrpc(coin_type, 'decoderawtransaction', [redeem_txn]) - if ci.using_segwit(): + if ci.using_segwit() or coin_type in (Coins.PART, ): self.log.debug('vsize paid, actual vsize %d %d', tx_vsize, redeem_txjs['vsize']) ensure(tx_vsize >= redeem_txjs['vsize'], 'underpaid fee') else: self.log.debug('size paid, actual size %d %d', tx_vsize, redeem_txjs['size']) ensure(tx_vsize >= redeem_txjs['size'], 'underpaid fee') - redeem_txjs = self.callcoinrpc(Coins.PART, 'decoderawtransaction', [redeem_txn]) - self.log.debug('Have valid redeem txn %s for contract %s tx %s', redeem_txjs['txid'], for_txn_type, prev_txnid) - + redeem_txid = ci.getTxid(bytes.fromhex(redeem_txn)) + self.log.debug('Have valid redeem txn %s for contract %s tx %s', redeem_txid.hex(), for_txn_type, prev_txnid) return redeem_txn def createRefundTxn(self, coin_type, txn, offer, bid, txn_script: bytearray, addr_refund_out=None, tx_type=TxTypes.ITX_REFUND): @@ -3142,27 +3156,32 @@ class BasicSwap(BaseApp): if self.coin_clients[coin_type]['connection_type'] != 'rpc': return None - txjs = self.callcoinrpc(Coins.PART, 'decoderawtransaction', [txn]) - if self.coin_clients[coin_type]['use_segwit']: - p2wsh = getP2WSH(txn_script) - vout = getVoutByP2WSH(txjs, p2wsh.hex()) + ci = self.ci(coin_type) + if coin_type in (Coins.NAV, ): + wif_prefix = chainparams[coin_type][self.chain]['key_prefix'] + prevout = ci.find_prevout_info(txn, txn_script) else: - addr_to = self.ci(Coins.PART).encode_p2sh(txn_script) - vout = getVoutByAddress(txjs, addr_to) + wif_prefix = chainparams[Coins.PART][self.chain]['key_prefix'] + txjs = self.callcoinrpc(Coins.PART, 'decoderawtransaction', [txn]) + if ci.using_segwit(): + p2wsh = ci.getScriptDest(txn_script) + vout = getVoutByScriptPubKey(txjs, p2wsh.hex()) + else: + addr_to = self.ci(Coins.PART).encode_p2sh(txn_script) + vout = getVoutByAddress(txjs, addr_to) + + prevout = { + 'txid': txjs['txid'], + 'vout': vout, + 'scriptPubKey': txjs['vout'][vout]['scriptPubKey']['hex'], + 'redeemScript': txn_script.hex(), + 'amount': txjs['vout'][vout]['value'] + } bid_date = dt.datetime.fromtimestamp(bid.created_at).date() - wif_prefix = chainparams[Coins.PART][self.chain]['key_prefix'] pubkey = self.getContractPubkey(bid_date, bid.contract_count) privkey = toWIF(wif_prefix, self.getContractPrivkey(bid_date, bid.contract_count)) - prev_amount = txjs['vout'][vout]['value'] - prevout = { - 'txid': txjs['txid'], - 'vout': vout, - 'scriptPubKey': txjs['vout'][vout]['scriptPubKey']['hex'], - 'redeemScript': txn_script.hex(), - 'amount': prev_amount} - lock_value = DeserialiseNum(txn_script, 64) sequence: int = 1 if offer.lock_type < TxLockTypes.ABS_LOCK_BLOCKS: @@ -3170,13 +3189,12 @@ class BasicSwap(BaseApp): fee_rate, fee_src = self.getFeeRateForCoin(coin_type) - tx_vsize = self.getContractSpendTxVSize(coin_type, False) + tx_vsize = ci.getHTLCSpendTxVSize(False) tx_fee = (fee_rate * tx_vsize) / 1000 - ci = self.ci(coin_type) self.log.debug('Refund tx fee %s, rate %s', ci.format_amount(tx_fee, conv_int=True, r=1), str(fee_rate)) - amount_out = ci.make_int(prev_amount, r=1) - ci.make_int(tx_fee, r=1) + amount_out = ci.make_int(prevout['amount'], r=1) - ci.make_int(tx_fee, r=1) if amount_out <= 0: raise ValueError('Refund amount out <= 0') @@ -3188,12 +3206,19 @@ class BasicSwap(BaseApp): locktime: int = 0 if offer.lock_type == TxLockTypes.ABS_LOCK_BLOCKS or offer.lock_type == TxLockTypes.ABS_LOCK_TIME: locktime = lock_value - refund_txn = ci.createRefundTxn(prevout, addr_refund_out, amount_out, locktime, sequence) + + if ci.use_p2shp2wsh(): + refund_txn = ci.createRefundTxn(prevout, addr_refund_out, amount_out, locktime, sequence, txn_script) + else: + refund_txn = ci.createRefundTxn(prevout, addr_refund_out, amount_out, locktime, sequence) options = {} if self.coin_clients[coin_type]['use_segwit']: options['force_segwit'] = True - refund_sig = self.callcoinrpc(Coins.PART, 'createsignaturewithkey', [refund_txn, prevout, privkey, 'ALL', options]) + if coin_type in (Coins.NAV, ): + refund_sig = ci.getTxSignature(refund_txn, prevout, privkey) + else: + refund_sig = self.callcoinrpc(Coins.PART, 'createsignaturewithkey', [refund_txn, prevout, privkey, 'ALL', options]) if coin_type == Coins.PART or self.coin_clients[coin_type]['use_segwit']: witness_stack = [ bytes.fromhex(refund_sig), @@ -3208,7 +3233,12 @@ class BasicSwap(BaseApp): script += format(OpCodes.OP_PUSHDATA1, '02x') + format(len(txn_script), '02x') + txn_script.hex() refund_txn = ci.setTxScriptSig(bytes.fromhex(refund_txn), 0, bytes.fromhex(script)).hex() - ro = self.callcoinrpc(Coins.PART, 'verifyrawtransaction', [refund_txn, [prevout]]) + if coin_type in (Coins.NAV, ): + # Only checks signature + ro = ci.verifyRawTransaction(refund_txn, [prevout]) + else: + ro = self.callcoinrpc(Coins.PART, 'verifyrawtransaction', [refund_txn, [prevout]]) + ensure(ro['inputs_valid'] is True, 'inputs_valid is false') # outputs_valid will be false if not a Particl txn # ensure(ro['complete'] is True, 'complete is false') @@ -3218,15 +3248,16 @@ class BasicSwap(BaseApp): # Check fee if ci.get_connection_type() == 'rpc': refund_txjs = self.callcoinrpc(coin_type, 'decoderawtransaction', [refund_txn]) - if ci.using_segwit(): + if ci.using_segwit() or coin_type in (Coins.PART, ): self.log.debug('vsize paid, actual vsize %d %d', tx_vsize, refund_txjs['vsize']) ensure(tx_vsize >= refund_txjs['vsize'], 'underpaid fee') else: self.log.debug('size paid, actual size %d %d', tx_vsize, refund_txjs['size']) ensure(tx_vsize >= refund_txjs['size'], 'underpaid fee') - refund_txjs = self.callcoinrpc(Coins.PART, 'decoderawtransaction', [refund_txn]) - self.log.debug('Have valid refund txn %s for contract tx %s', refund_txjs['txid'], txjs['txid']) + refund_txid = ci.getTxid(bytes.fromhex(refund_txn)) + prev_txid = ci.getTxid(bytes.fromhex(txn)) + self.log.debug('Have valid refund txn %s for contract tx %s', refund_txid.hex(), prev_txid.hex()) return refund_txn @@ -3344,10 +3375,10 @@ class BasicSwap(BaseApp): return rv - raise ValueError('No explorer for lookupUnspentByAddress {}'.format(str(coin_type))) + raise ValueError('No explorer for lookupUnspentByAddress {}'.format(Coins(coin_type).name)) if self.coin_clients[coin_type]['connection_type'] != 'rpc': - raise ValueError('No RPC connection for lookupUnspentByAddress {}'.format(str(coin_type))) + raise ValueError('No RPC connection for lookupUnspentByAddress {}'.format(Coins(coin_type).name)) if assert_txid is not None: try: @@ -3704,8 +3735,9 @@ class BasicSwap(BaseApp): except Exception: pass else: - if self.coin_clients[coin_from]['use_segwit']: - addr = ci_from.encode_p2wsh(getP2WSH(bid.initiate_tx.script)) + if ci_from.using_segwit(): + dest_script = ci_from.getScriptDest(bid.initiate_tx.script) + addr = ci_from.encodeScriptDest(dest_script) else: addr = p2sh @@ -3745,8 +3777,9 @@ class BasicSwap(BaseApp): return True # Mark bid for archiving elif state == BidStates.SWAP_INITIATED: # Waiting for participate txn to be confirmed in 'to' chain - if self.coin_clients[coin_to]['use_segwit']: - addr = ci_to.encode_p2wsh(getP2WSH(bid.participate_tx.script)) + if ci_to.using_segwit(): + p2wsh = ci_to.getScriptDest(bid.participate_tx.script) + addr = ci_to.encodeScriptDest(p2wsh) else: addr = ci_to.encode_p2sh(bid.participate_tx.script) @@ -3851,13 +3884,13 @@ class BasicSwap(BaseApp): def removeWatchedOutput(self, coin_type, bid_id: bytes, txid_hex: str) -> None: # Remove all for bid if txid is None - self.log.debug('removeWatchedOutput %s %s %s', str(coin_type), bid_id.hex(), txid_hex) + self.log.debug('removeWatchedOutput %s %s %s', Coins(coin_type).name, bid_id.hex(), txid_hex) old_len = len(self.coin_clients[coin_type]['watched_outputs']) for i in range(old_len - 1, -1, -1): wo = self.coin_clients[coin_type]['watched_outputs'][i] if wo.bid_id == bid_id and (txid_hex is None or wo.txid_hex == txid_hex): del self.coin_clients[coin_type]['watched_outputs'][i] - self.log.debug('Removed watched output %s %s %s', str(coin_type), bid_id.hex(), wo.txid_hex) + self.log.debug('Removed watched output %s %s %s', Coins(coin_type).name, bid_id.hex(), wo.txid_hex) def initiateTxnSpent(self, bid_id: bytes, spend_txid: str, spend_n: int, spend_txn): self.log.debug('Bid %s initiate txn spent by %s %d', bid_id.hex(), spend_txid, spend_n) @@ -4632,7 +4665,7 @@ class BasicSwap(BaseApp): chain_b_height_start=ci_to.getChainHeight(), ) else: - ensure(bid.state == BidStates.BID_SENT, 'Wrong bid state: {}'.format(str(BidStates(bid.state)))) + ensure(bid.state == BidStates.BID_SENT, 'Wrong bid state: {}'.format(BidStates(bid.state).name)) bid.created_at = msg['sent'] bid.expire_at = msg['sent'] + bid_data.time_valid bid.was_received = True @@ -4680,7 +4713,7 @@ class BasicSwap(BaseApp): self.log.info('Received valid bid accept %s for bid %s sent to self', accept_msg_id.hex(), bid_id.hex()) return - raise ValueError('Wrong bid state: {}'.format(str(BidStates(bid.state)))) + raise ValueError('Wrong bid state: {}'.format(BidStates(bid.state).name)) use_csv = True if offer.lock_type < TxLockTypes.ABS_LOCK_BLOCKS else False @@ -4936,7 +4969,7 @@ class BasicSwap(BaseApp): bid.chain_b_height_start = wallet_restore_height self.log.warning('Adaptor-sig swap restore height clamped to {}'.format(wallet_restore_height)) else: - ensure(bid.state == BidStates.BID_SENT, 'Wrong bid state: {}'.format(str(BidStates(bid.state)))) + ensure(bid.state == BidStates.BID_SENT, 'Wrong bid state: {}'.format(BidStates(bid.state).name)) # Don't update bid.created_at, it's been used to derive kaf bid.expire_at = msg['sent'] + bid_data.time_valid bid.was_received = True @@ -5178,7 +5211,6 @@ class BasicSwap(BaseApp): txid_hex = ci_from.publishTx(lock_tx_signed) vout_pos = ci_from.getTxOutputPos(xmr_swap.a_lock_tx, xmr_swap.a_lock_tx_script) - self.log.debug('Submitted lock txn %s to %s chain for bid %s', txid_hex, ci_from.coin_name(), bid_id.hex()) if bid.xmr_a_lock_tx is None: @@ -5806,7 +5838,7 @@ class BasicSwap(BaseApp): bid.chain_b_height_start = wallet_restore_height self.log.warning('Adaptor-sig swap restore height clamped to {}'.format(wallet_restore_height)) else: - ensure(bid.state == BidStates.BID_REQUEST_SENT, 'Wrong bid state: {}'.format(str(BidStates(bid.state)))) + ensure(bid.state == BidStates.BID_REQUEST_SENT, 'Wrong bid state: {}'.format(BidStates(bid.state).name)) # Don't update bid.created_at, it's been used to derive kaf bid.expire_at = msg['sent'] + bid_data.time_valid bid.was_received = True @@ -5948,7 +5980,7 @@ class BasicSwap(BaseApp): break except Exception as e: if 'Unknown message id' in str(e) and i < num_tries: - time.sleep(1) + self.delay_event.wait(1) else: raise e @@ -6364,6 +6396,8 @@ class BasicSwap(BaseApp): rv['blind_unconfirmed'] = walletinfo['unconfirmed_blind'] elif coin == Coins.XMR: rv['main_address'] = self.getCachedMainWalletAddress(ci) + elif coin == Coins.NAV: + rv['immature'] = walletinfo['immature_balance'] return rv except Exception as e: diff --git a/basicswap/basicswap_util.py b/basicswap/basicswap_util.py index 535023e..5828c37 100644 --- a/basicswap/basicswap_util.py +++ b/basicswap/basicswap_util.py @@ -446,14 +446,14 @@ def getVoutByAddress(txjs, p2sh): raise ValueError('Address output not found in txn') -def getVoutByP2WSH(txjs, p2wsh_hex): +def getVoutByScriptPubKey(txjs, scriptPubKey_hex: str) -> int: for o in txjs['vout']: try: - if p2wsh_hex == o['scriptPubKey']['hex']: + if scriptPubKey_hex == o['scriptPubKey']['hex']: return o['n'] except Exception: pass - raise ValueError('P2WSH output not found in txn') + raise ValueError('scriptPubKey output not found in txn') def replaceAddrPrefix(addr, coin_type, chain_name, addr_type='pubkey_address'): diff --git a/basicswap/chainparams.py b/basicswap/chainparams.py index fd9ad58..04bde1b 100644 --- a/basicswap/chainparams.py +++ b/basicswap/chainparams.py @@ -31,6 +31,7 @@ class Coins(IntEnum): PIVX = 11 DASH = 12 FIRO = 13 + NAV = 14 chainparams = { @@ -327,6 +328,45 @@ chainparams = { 'min_amount': 1000, 'max_amount': 100000 * COIN, } + }, + Coins.NAV: { + 'name': 'navcoin', + 'ticker': 'NAV', + 'message_magic': 'Navcoin Signed Message:\n', + 'blocks_target': 30, + 'decimal_places': 8, + 'has_csv': True, + 'has_segwit': True, + 'mainnet': { + 'rpcport': 44444, + 'pubkey_address': 53, + 'script_address': 85, + 'key_prefix': 150, + 'hrp': '', + 'bip44': 130, + 'min_amount': 1000, + 'max_amount': 100000 * COIN, + }, + 'testnet': { + 'rpcport': 44445, + 'pubkey_address': 111, + 'script_address': 196, + 'key_prefix': 239, + 'hrp': '', + 'bip44': 1, + 'min_amount': 1000, + 'max_amount': 100000 * COIN, + }, + 'regtest': { + 'rpcport': 44446, + 'pubkey_address': 111, + 'script_address': 196, + 'key_prefix': 239, + 'hrp': '', + 'bip44': 1, + 'min_amount': 1000, + 'max_amount': 100000 * COIN, + } } } ticker_map = {} diff --git a/basicswap/interface/btc.py b/basicswap/interface/btc.py index 46f08dd..aad1cdf 100644 --- a/basicswap/interface/btc.py +++ b/basicswap/interface/btc.py @@ -55,7 +55,6 @@ from basicswap.contrib.test_framework.messages import ( CTxIn, CTxInWitness, CTxOut, - FromHex, uint256_from_str) from basicswap.contrib.test_framework.script import ( @@ -209,6 +208,10 @@ class BTCInterface(CoinInterface): # Using btc native segwit return self._use_segwit + def use_p2shp2wsh(self) -> bool: + # p2sh-p2wsh + return False + def get_connection_type(self): return self._connection_type @@ -287,7 +290,7 @@ class BTCInterface(CoinInterface): def getWalletRestoreHeight(self) -> int: start_time = self.rpc_callback('getwalletinfo')['keypoololdest'] - blockchaininfo = self.rpc_callback('getblockchaininfo') + blockchaininfo = self.getBlockchainInfo() best_block = blockchaininfo['bestblockhash'] chain_synced = round(blockchaininfo['verificationprogress'], 3) @@ -450,7 +453,7 @@ class BTCInterface(CoinInterface): def decodePubkey(self, pke): return CPKToPoint(pke) - def decodeKey(self, k): + def decodeKey(self, k: str) -> bytes: return decodeWif(k) def sumKeys(self, ka, kb): @@ -461,8 +464,15 @@ class BTCInterface(CoinInterface): return PublicKey.combine_keys([PublicKey(Ka), PublicKey(Kb)]).format() def getScriptForPubkeyHash(self, pkh: bytes) -> CScript: + # p2wpkh return CScript([OP_0, pkh]) + def loadTx(self, tx_bytes: bytes) -> CTransaction: + # Load tx from bytes to internal representation + tx = CTransaction() + tx.deserialize(BytesIO(tx_bytes)) + return tx + def extractScriptLockScriptValues(self, script_bytes: bytes): script_len = len(script_bytes) ensure(script_len == 71, 'Bad script length') @@ -536,7 +546,7 @@ class BTCInterface(CoinInterface): 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()) + tx_lock = self.loadTx(tx_lock_bytes) output_script = self.getScriptDest(script_lock) locked_n = findOutput(tx_lock, output_script) @@ -900,7 +910,7 @@ class BTCInterface(CoinInterface): def decryptOtVES(self, k, esig): return ecdsaotves_dec_sig(k, esig) + bytes((SIGHASH_ALL,)) - def verifyTxSig(self, tx_bytes, sig, K, input_n, prevout_script, prevout_value): + def verifyTxSig(self, tx_bytes: bytes, sig: bytes, K: bytes, input_n: int, prevout_script: bytes, prevout_value: int) -> bool: tx = self.loadTx(tx_bytes) sig_hash = SegwitV0SignatureHash(prevout_script, tx, input_n, SIGHASH_ALL, prevout_value) @@ -955,12 +965,6 @@ class BTCInterface(CoinInterface): def encodeTx(self, tx) -> bytes: return tx.serialize() - def loadTx(self, tx_bytes: bytes) -> CTransaction: - # Load tx from bytes to internal representation - tx = CTransaction() - tx.deserialize(BytesIO(tx_bytes)) - return tx - def getTxid(self, tx) -> bytes: if isinstance(tx, str): tx = bytes.fromhex(tx) @@ -984,6 +988,18 @@ class BTCInterface(CoinInterface): def getScriptScriptSig(self, script: bytes) -> bytes: return bytes() + def getP2SHP2WSHDest(self, script): + script_hash = hashlib.sha256(script).digest() + assert len(script_hash) == 32 + p2wsh_hash = hash160(CScript([OP_0, script_hash])) + assert len(p2wsh_hash) == 20 + return CScript([OP_HASH160, p2wsh_hash, OP_EQUAL]) + + def getP2SHP2WSHScriptSig(self, script): + script_hash = hashlib.sha256(script).digest() + assert len(script_hash) == 32 + return CScript([CScript([OP_0, script_hash, ]), ]) + def getPkDest(self, K: bytes) -> bytearray: return self.getScriptForPubkeyHash(self.getPubkeyHash(K)) @@ -1468,6 +1484,14 @@ class BTCInterface(CoinInterface): if self.getSpendableBalance() < amount: raise ValueError('Balance too low') + def getHTLCSpendTxVSize(self, redeem: bool = True) -> int: + tx_vsize = 5 # Add a few bytes, sequence in script takes variable amount of bytes + if self.using_segwit(): + tx_vsize += 143 if redeem else 134 + else: + tx_vsize += 323 if redeem else 287 + return tx_vsize + def testBTCInterface(): print('TODO: testBTCInterface') diff --git a/basicswap/interface/contrib/nav_test_framework/__init__.py b/basicswap/interface/contrib/nav_test_framework/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/basicswap/interface/contrib/nav_test_framework/authproxy.py b/basicswap/interface/contrib/nav_test_framework/authproxy.py new file mode 100755 index 0000000..e8a6f9c --- /dev/null +++ b/basicswap/interface/contrib/nav_test_framework/authproxy.py @@ -0,0 +1,175 @@ + +""" + Copyright 2011 Jeff Garzik + + AuthServiceProxy has the following improvements over python-jsonrpc's + ServiceProxy class: + + - HTTP connections persist for the life of the AuthServiceProxy object + (if server supports HTTP/1.1) + - sends protocol 'version', per JSON-RPC 1.1 + - sends proper, incrementing 'id' + - sends Basic HTTP authentication headers + - parses all JSON numbers that look like floats as Decimal + - uses standard Python json lib + + Previous copyright, from python-jsonrpc/jsonrpc/proxy.py: + + Copyright (c) 2007 Jan-Klaas Kollhof + + This file is part of jsonrpc. + + jsonrpc is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2.1 of the License, or + (at your option) any later version. + + This software is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with this software; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" + +try: + import http.client as httplib +except ImportError: + import httplib +import base64 +import decimal +import json +import logging +try: + import urllib.parse as urlparse +except ImportError: + import urlparse + +USER_AGENT = "AuthServiceProxy/0.1" + +HTTP_TIMEOUT = 30 + +log = logging.getLogger("NavcoinRPC") + +class JSONRPCException(Exception): + def __init__(self, rpc_error): + Exception.__init__(self) + self.error = rpc_error + + +def EncodeDecimal(o): + if isinstance(o, decimal.Decimal): + return str(o) + raise TypeError(repr(o) + " is not JSON serializable") + +class AuthServiceProxy(object): + __id_count = 0 + + # ensure_ascii: escape unicode as \uXXXX, passed to json.dumps + def __init__(self, service_url, service_name=None, timeout=HTTP_TIMEOUT, connection=None, ensure_ascii=True): + self.__service_url = service_url + self._service_name = service_name + self.ensure_ascii = ensure_ascii # can be toggled on the fly by tests + self.__url = urlparse.urlparse(service_url) + if self.__url.port is None: + port = 80 + else: + port = self.__url.port + (user, passwd) = (self.__url.username, self.__url.password) + try: + user = user.encode('utf8') + except AttributeError: + pass + try: + passwd = passwd.encode('utf8') + except AttributeError: + pass + authpair = user + b':' + passwd + self.__auth_header = b'Basic ' + base64.b64encode(authpair) + + if connection: + # Callables re-use the connection of the original proxy + self.__conn = connection + elif self.__url.scheme == 'https': + self.__conn = httplib.HTTPSConnection(self.__url.hostname, port, + timeout=timeout) + else: + self.__conn = httplib.HTTPConnection(self.__url.hostname, port, + timeout=timeout) + + def __getattr__(self, name): + if name.startswith('__') and name.endswith('__'): + # Python internal stuff + raise AttributeError + if self._service_name is not None: + name = "%s.%s" % (self._service_name, name) + return AuthServiceProxy(self.__service_url, name, connection=self.__conn) + + def _request(self, method, path, postdata): + ''' + Do a HTTP request, with retry if we get disconnected (e.g. due to a timeout). + This is a workaround for https://bugs.python.org/issue3566 which is fixed in Python 3.5. + ''' + headers = {'Host': self.__url.hostname, + 'User-Agent': USER_AGENT, + 'Authorization': self.__auth_header, + 'Content-type': 'application/json'} + try: + self.__conn.request(method, path, postdata, headers) + return self._get_response() + except httplib.BadStatusLine as e: + if e.line == "''": # if connection was closed, try again + self.__conn.close() + self.__conn.request(method, path, postdata, headers) + return self._get_response() + else: + raise + except BrokenPipeError: + # Python 3.5+ raises this instead of BadStatusLine when the connection was reset + self.__conn.close() + self.__conn.request(method, path, postdata, headers) + return self._get_response() + + def __call__(self, *args): + AuthServiceProxy.__id_count += 1 + + log.debug("-%s-> %s %s"%(AuthServiceProxy.__id_count, self._service_name, + json.dumps(args, default=EncodeDecimal, ensure_ascii=self.ensure_ascii))) + postdata = json.dumps({'version': '1.1', + 'method': self._service_name, + 'params': args, + 'id': AuthServiceProxy.__id_count}, default=EncodeDecimal, ensure_ascii=self.ensure_ascii) + response = self._request('POST', self.__url.path, postdata.encode('utf-8')) + if response['error'] is not None: + raise JSONRPCException(response['error']) + elif 'result' not in response: + raise JSONRPCException({ + 'code': -343, 'message': 'missing JSON-RPC result'}) + else: + return response['result'] + + def _batch(self, rpc_call_list): + postdata = json.dumps(list(rpc_call_list), default=EncodeDecimal, ensure_ascii=self.ensure_ascii) + log.debug("--> "+postdata) + return self._request('POST', self.__url.path, postdata.encode('utf-8')) + + def _get_response(self): + http_response = self.__conn.getresponse() + if http_response is None: + raise JSONRPCException({ + 'code': -342, 'message': 'missing HTTP response from server'}) + + content_type = http_response.getheader('Content-Type') + if content_type != 'application/json': + raise JSONRPCException({ + 'code': -342, 'message': 'non-JSON HTTP response with \'%i %s\' from server' % (http_response.status, http_response.reason)}) + + responsedata = http_response.read().decode('utf8') + response = json.loads(responsedata, parse_float=decimal.Decimal) + if "error" in response and response["error"] is None: + log.debug("<-%s- %s"%(response["id"], json.dumps(response["result"], default=EncodeDecimal, ensure_ascii=self.ensure_ascii))) + else: + log.debug("<-- "+responsedata) + return response diff --git a/basicswap/interface/contrib/nav_test_framework/bignum.py b/basicswap/interface/contrib/nav_test_framework/bignum.py new file mode 100755 index 0000000..bdbc4b9 --- /dev/null +++ b/basicswap/interface/contrib/nav_test_framework/bignum.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +# +# bignum.py +# +# This file is copied from python-navcoinlib. +# +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +# + +"""Bignum routines""" + + +import struct + + +# generic big endian MPI format + +def bn_bytes(v, have_ext=False): + ext = 0 + if have_ext: + ext = 1 + return ((v.bit_length()+7)//8) + ext + +def bn2bin(v): + s = bytearray() + i = bn_bytes(v) + while i > 0: + s.append((v >> ((i-1) * 8)) & 0xff) + i -= 1 + return s + +def bin2bn(s): + l = 0 + for ch in s: + l = (l << 8) | ch + return l + +def bn2mpi(v): + have_ext = False + if v.bit_length() > 0: + have_ext = (v.bit_length() & 0x07) == 0 + + neg = False + if v < 0: + neg = True + v = -v + + s = struct.pack(b">I", bn_bytes(v, have_ext)) + ext = bytearray() + if have_ext: + ext.append(0) + v_bin = bn2bin(v) + if neg: + if have_ext: + ext[0] |= 0x80 + else: + v_bin[0] |= 0x80 + return s + ext + v_bin + +def mpi2bn(s): + if len(s) < 4: + return None + s_size = bytes(s[:4]) + v_len = struct.unpack(b">I", s_size)[0] + if len(s) != (v_len + 4): + return None + if v_len == 0: + return 0 + + v_str = bytearray(s[4:]) + neg = False + i = v_str[0] + if i & 0x80: + neg = True + i &= ~0x80 + v_str[0] = i + + v = bin2bn(v_str) + + if neg: + return -v + return v + +# navcoin-specific little endian format, with implicit size +def mpi2vch(s): + r = s[4:] # strip size + r = r[::-1] # reverse string, converting BE->LE + return r + +def bn2vch(v): + return bytes(mpi2vch(bn2mpi(v))) + +def vch2mpi(s): + r = struct.pack(b">I", len(s)) # size + r += s[::-1] # reverse string, converting LE->BE + return r + +def vch2bn(s): + return mpi2bn(vch2mpi(s)) + diff --git a/basicswap/interface/contrib/nav_test_framework/coverage.py b/basicswap/interface/contrib/nav_test_framework/coverage.py new file mode 100755 index 0000000..a282400 --- /dev/null +++ b/basicswap/interface/contrib/nav_test_framework/coverage.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +# Copyright (c) 2015-2016 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +""" +This module contains utilities for doing coverage analysis on the RPC +interface. + +It provides a way to track which RPC commands are exercised during +testing. + +""" +import os + + +REFERENCE_FILENAME = 'rpc_interface.txt' + + +class AuthServiceProxyWrapper(object): + """ + An object that wraps AuthServiceProxy to record specific RPC calls. + + """ + def __init__(self, auth_service_proxy_instance, coverage_logfile=None): + """ + Kwargs: + auth_service_proxy_instance (AuthServiceProxy): the instance + being wrapped. + coverage_logfile (str): if specified, write each service_name + out to a file when called. + + """ + self.auth_service_proxy_instance = auth_service_proxy_instance + self.coverage_logfile = coverage_logfile + + def __getattr__(self, *args, **kwargs): + return_val = self.auth_service_proxy_instance.__getattr__( + *args, **kwargs) + + return AuthServiceProxyWrapper(return_val, self.coverage_logfile) + + def __call__(self, *args, **kwargs): + """ + Delegates to AuthServiceProxy, then writes the particular RPC method + called to a file. + + """ + return_val = self.auth_service_proxy_instance.__call__(*args, **kwargs) + rpc_method = self.auth_service_proxy_instance._service_name + + if self.coverage_logfile: + with open(self.coverage_logfile, 'a+') as f: + f.write("%s\n" % rpc_method) + + return return_val + + @property + def url(self): + return self.auth_service_proxy_instance.url + + +def get_filename(dirname, n_node): + """ + Get a filename unique to the test process ID and node. + + This file will contain a list of RPC commands covered. + """ + pid = str(os.getpid()) + return os.path.join( + dirname, "coverage.pid%s.node%s.txt" % (pid, str(n_node))) + + +def write_all_rpc_commands(dirname, node): + """ + Write out a list of all RPC functions available in `navcoin-cli` for + coverage comparison. This will only happen once per coverage + directory. + + Args: + dirname (str): temporary test dir + node (AuthServiceProxy): client + + Returns: + bool. if the RPC interface file was written. + + """ + filename = os.path.join(dirname, REFERENCE_FILENAME) + + if os.path.isfile(filename): + return False + + help_output = node.help().split('\n') + commands = set() + + for line in help_output: + line = line.strip() + + # Ignore blanks and headers + if line and not line.startswith('='): + commands.add("%s\n" % line.split()[0]) + + with open(filename, 'w') as f: + f.writelines(list(commands)) + + return True diff --git a/basicswap/interface/contrib/nav_test_framework/mininode.py b/basicswap/interface/contrib/nav_test_framework/mininode.py new file mode 100755 index 0000000..ff476ee --- /dev/null +++ b/basicswap/interface/contrib/nav_test_framework/mininode.py @@ -0,0 +1,1497 @@ +#!/usr/bin/env python3 +# Copyright (c) 2010 ArtForz -- public domain half-a-node +# Copyright (c) 2012 Jeff Garzik +# Copyright (c) 2010-2016 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +# +# mininode.py - Bitcoin P2P network half-a-node +# +# This python code was modified from ArtForz' public domain half-a-node, as +# found in the mini-node branch of http://github.com/jgarzik/pynode. +# +# NodeConn: an object which manages p2p connectivity to a bitcoin node +# NodeConnCB: a base class that describes the interface for receiving +# callbacks with network messages from a NodeConn +# CBlock, CTransaction, CBlockHeader, CTxIn, CTxOut, etc....: +# data structures that should map to corresponding structures in +# bitcoin/primitives +# msg_block, msg_tx, msg_headers, etc.: +# data structures that represent network messages +# ser_*, deser_*: functions that handle serialization/deserialization + + +import struct +import socket +import time +import sys +import random +from .util import hex_str_to_bytes, bytes_to_hex_str +from io import BytesIO +from codecs import encode +import hashlib +from threading import RLock +from threading import Thread +import logging +import copy +from .siphash import siphash256 + +BIP0031_VERSION = 70029 +MY_VERSION = 70029 # past bip-31 for ping/pong +MY_SUBVERSION = b"/python-mininode-tester:0.0.3/" + +MAX_INV_SZ = 50000 +MAX_BLOCK_SIZE = 1000000 + +COIN = 100000000 # 1 btc in satoshis + +NODE_NETWORK = (1 << 0) +NODE_GETUTXO = (1 << 1) +NODE_BLOOM = (1 << 2) +NODE_WITNESS = (1 << 3) + +# Keep our own socket map for asyncore, so that we can track disconnects +# ourselves (to workaround an issue with closing an asyncore socket when +# using select) +mininode_socket_map = dict() + +# One lock for synchronizing all data access between the networking thread (see +# NetworkThread below) and the thread running the test logic. For simplicity, +# NodeConn acquires this lock whenever delivering a message to to a NodeConnCB, +# and whenever adding anything to the send buffer (in send_message()). This +# lock should be acquired in the thread running the test logic to synchronize +# access to any data shared with the NodeConnCB or NodeConn. +mininode_lock = RLock() + +# Serialization/deserialization tools +def sha256(s): + return hashlib.new('sha256', s).digest() + +def ripemd160(s): + return hashlib.new('ripemd160', s).digest() + +def hash256(s): + return sha256(sha256(s)) + +def ser_compact_size(l): + r = b"" + if l < 253: + r = struct.pack("B", l) + elif l < 0x10000: + r = struct.pack(">= 32 + return rs + + +def uint256_from_str(s): + r = 0 + t = struct.unpack("> 24) & 0xFF + v = (c & 0xFFFFFF) << (8 * (nbytes - 3)) + return v + + +def deser_vector(f, c): + nit = deser_compact_size(f) + r = [] + for i in range(nit): + t = c() + t.deserialize(f) + r.append(t) + return r + + +# ser_function_name: Allow for an alternate serialization function on the +# entries in the vector (we use this for serializing the vector of transactions +# for a witness block). +def ser_vector(l, ser_function_name=None): + r = ser_compact_size(len(l)) + for i in l: + if ser_function_name: + r += getattr(i, ser_function_name)() + else: + r += i.serialize() + return r + + +def deser_uint256_vector(f): + nit = deser_compact_size(f) + r = [] + for i in range(nit): + t = deser_uint256(f) + r.append(t) + return r + + +def ser_uint256_vector(l): + r = ser_compact_size(len(l)) + for i in l: + r += ser_uint256(i) + return r + + +def deser_string_vector(f): + nit = deser_compact_size(f) + r = [] + for i in range(nit): + t = deser_string(f) + r.append(t) + return r + + +def ser_string_vector(l): + r = ser_compact_size(len(l)) + for sv in l: + r += ser_string(sv) + return r + + +def deser_int_vector(f): + nit = deser_compact_size(f) + r = [] + for i in range(nit): + t = struct.unpack("H", f.read(2))[0] + + def serialize(self): + r = b"" + r += struct.pack("H", self.port) + return r + + def __repr__(self): + return "CAddress(nServices=%i ip=%s port=%i)" % (self.nServices, + self.ip, self.port) + +MSG_WITNESS_FLAG = 1<<30 + +class CInv(object): + typemap = { + 0: "Error", + 1: "TX", + 2: "Block", + 1|MSG_WITNESS_FLAG: "WitnessTx", + 2|MSG_WITNESS_FLAG : "WitnessBlock", + 4: "CompactBlock", + 5: "DandelionTx" + } + + def __init__(self, t=0, h=0): + self.type = t + self.hash = h + + def deserialize(self, f): + self.type = struct.unpack(" 2: + self.strDZeel = deser_string(f) + + def serialize_without_witness(self): + r = b"" + r += struct.pack("= 2: + r += ser_string(self.strDZeel) + return r + + # Only serialize with witness when explicitly called for + def serialize_with_witness(self): + flags = 0 + if not self.wit.is_null(): + flags |= 1 + r = b"" + r += struct.pack("= 2: + r += ser_string(self.strDZeel) + return r + + # Regular serialization is without witness -- must explicitly + # call serialize_with_witness to include witness data. + def serialize(self): + return self.serialize_without_witness() + + # Recalculate the txid (transaction hash without witness) + def rehash(self): + self.sha256 = None + self.calc_sha256() + + # We will only cache the serialization without witness in + # self.sha256 and self.hash -- those are expected to be the txid. + def calc_sha256(self, with_witness=False): + if with_witness: + # Don't cache the result, just return it + return uint256_from_str(hash256(self.serialize_with_witness())) + + if self.sha256 is None: + self.sha256 = uint256_from_str(hash256(self.serialize_without_witness())) + self.hash = encode(hash256(self.serialize())[::-1], 'hex_codec').decode('ascii') + + def is_valid(self): + self.calc_sha256() + for tout in self.vout: + if tout.nValue < 0 or tout.nValue > 21000000 * COIN: + return False + return True + + def __repr__(self): + return "CTransaction(nVersion=%i nTime=%i vin=%s vout=%s wit=%s nLockTime=%i)" \ + % (self.nVersion, self.nTime, repr(self.vin), repr(self.vout), repr(self.wit), self.nLockTime) + + +class msg_dandeliontx(): + command = b"dandeliontx" + + def __init__(self, tx=CTransaction()): + self.tx = tx + + def deserialize(self, f): + self.tx.deserialize(f) + + def serialize(self): + return self.tx.serialize_without_witness() + + def __repr__(self): + return "msg_dandeliontx(tx=%s)" % (repr(self.tx)) + + +class CBlockHeader(object): + def __init__(self, header=None): + if header is None: + self.set_null() + else: + self.nVersion = header.nVersion + self.hashPrevBlock = header.hashPrevBlock + self.hashMerkleRoot = header.hashMerkleRoot + self.nTime = header.nTime + self.nBits = header.nBits + self.nNonce = header.nNonce + self.sha256 = header.sha256 + self.hash = header.hash + self.calc_sha256() + + def set_null(self): + self.nVersion = 1 + self.hashPrevBlock = 0 + self.hashMerkleRoot = 0 + self.nTime = 0 + self.nBits = 0 + self.nNonce = 0 + self.sha256 = None + self.hash = None + + def deserialize(self, f): + self.nVersion = struct.unpack(" 1: + newhashes = [] + for i in range(0, len(hashes), 2): + i2 = min(i+1, len(hashes)-1) + newhashes.append(hash256(hashes[i] + hashes[i2])) + hashes = newhashes + return uint256_from_str(hashes[0]) + + def calc_merkle_root(self): + hashes = [] + for tx in self.vtx: + tx.calc_sha256() + hashes.append(ser_uint256(tx.sha256)) + return self.get_merkle_root(hashes) + + def calc_witness_merkle_root(self): + # For witness root purposes, the hash of the + # coinbase, with witness, is defined to be 0...0 + hashes = [ser_uint256(0)] + + for tx in self.vtx[1:]: + # Calculate the hashes with witness data + hashes.append(ser_uint256(tx.calc_sha256(True))) + + return self.get_merkle_root(hashes) + + def is_valid(self): + self.calc_sha256() + target = uint256_from_compact(self.nBits) + if self.sha256 > target: + return False + for tx in self.vtx: + if not tx.is_valid(): + return False + if self.calc_merkle_root() != self.hashMerkleRoot: + return False + return True + + def solve(self): + self.rehash() + target = uint256_from_compact(self.nBits) + while self.sha256 > target: + self.nNonce += 1 + self.rehash() + + def __repr__(self): + return "CBlock(nVersion=%i hashPrevBlock=%064x hashMerkleRoot=%064x nTime=%s nBits=%08x nNonce=%08x vtx=%s)" \ + % (self.nVersion, self.hashPrevBlock, self.hashMerkleRoot, + time.ctime(self.nTime), self.nBits, self.nNonce, repr(self.vtx)) + + +class CUnsignedAlert(object): + def __init__(self): + self.nVersion = 1 + self.nRelayUntil = 0 + self.nExpiration = 0 + self.nID = 0 + self.nCancel = 0 + self.setCancel = [] + self.nMinVer = 0 + self.nMaxVer = 0 + self.setSubVer = [] + self.nPriority = 0 + self.strComment = b"" + self.strStatusBar = b"" + self.strReserved = b"" + + def deserialize(self, f): + self.nVersion = struct.unpack("= 106: + self.addrFrom = CAddress() + self.addrFrom.deserialize(f) + self.nNonce = struct.unpack("= 209: + self.nStartingHeight = struct.unpack(" +class msg_headers(object): + command = b"headers" + + def __init__(self): + self.headers = [] + + def deserialize(self, f): + # comment in bitcoind indicates these should be deserialized as blocks + blocks = deser_vector(f, CBlock) + for x in blocks: + self.headers.append(CBlockHeader(x)) + + def serialize(self): + blocks = [CBlock(x) for x in self.headers] + return ser_vector(blocks) + + def __repr__(self): + return "msg_headers(headers=%s)" % repr(self.headers) + + +class msg_reject(object): + command = b"reject" + REJECT_MALFORMED = 1 + + def __init__(self): + self.message = b"" + self.code = 0 + self.reason = b"" + self.data = 0 + + def deserialize(self, f): + self.message = deser_string(f) + self.code = struct.unpack(" '3': + long = int + bchr = lambda x: bytes([x]) + bord = lambda x: x + +import struct + +from .bignum import bn2vch + +MAX_SCRIPT_SIZE = 10000 +MAX_SCRIPT_ELEMENT_SIZE = 520 +MAX_SCRIPT_OPCODES = 201 + +OPCODE_NAMES = {} + +def hash160(s): + return hashlib.new('ripemd160', sha256(s)).digest() + + +_opcode_instances = [] +class CScriptOp(int): + """A single script opcode""" + __slots__ = [] + + @staticmethod + def encode_op_pushdata(d): + """Encode a PUSHDATA op, returning bytes""" + if len(d) < 0x4c: + return b'' + bchr(len(d)) + d # OP_PUSHDATA + elif len(d) <= 0xff: + return b'\x4c' + bchr(len(d)) + d # OP_PUSHDATA1 + elif len(d) <= 0xffff: + return b'\x4d' + struct.pack(b'>= 8 + if r[-1] & 0x80: + r.append(0x80 if neg else 0) + elif neg: + r[-1] |= 0x80 + return bytes(bchr(len(r)) + r) + + +class CScript(bytes): + """Serialized script + + A bytes subclass, so you can use this directly whenever bytes are accepted. + Note that this means that indexing does *not* work - you'll get an index by + byte rather than opcode. This format was chosen for efficiency so that the + general case would not require creating a lot of little CScriptOP objects. + + iter(script) however does iterate by opcode. + """ + @classmethod + def __coerce_instance(cls, other): + # Coerce other into bytes + if isinstance(other, CScriptOp): + other = bchr(other) + elif isinstance(other, CScriptNum): + if (other.value == 0): + other = bchr(CScriptOp(OP_0)) + else: + other = CScriptNum.encode(other) + elif isinstance(other, int): + if 0 <= other <= 16: + other = bytes(bchr(CScriptOp.encode_op_n(other))) + elif other == -1: + other = bytes(bchr(OP_1NEGATE)) + else: + other = CScriptOp.encode_op_pushdata(bn2vch(other)) + elif isinstance(other, (bytes, bytearray)): + other = CScriptOp.encode_op_pushdata(other) + return other + + def __add__(self, other): + # Do the coercion outside of the try block so that errors in it are + # noticed. + other = self.__coerce_instance(other) + + try: + # bytes.__add__ always returns bytes instances unfortunately + return CScript(super(CScript, self).__add__(other)) + except TypeError: + raise TypeError('Can not add a %r instance to a CScript' % other.__class__) + + def join(self, iterable): + # join makes no sense for a CScript() + raise NotImplementedError + + def __new__(cls, value=b''): + if isinstance(value, bytes) or isinstance(value, bytearray): + return super(CScript, cls).__new__(cls, value) + else: + def coerce_iterable(iterable): + for instance in iterable: + yield cls.__coerce_instance(instance) + # Annoyingly on both python2 and python3 bytes.join() always + # returns a bytes instance even when subclassed. + return super(CScript, cls).__new__(cls, b''.join(coerce_iterable(value))) + + def raw_iter(self): + """Raw iteration + + Yields tuples of (opcode, data, sop_idx) so that the different possible + PUSHDATA encodings can be accurately distinguished, as well as + determining the exact opcode byte indexes. (sop_idx) + """ + i = 0 + while i < len(self): + sop_idx = i + opcode = bord(self[i]) + i += 1 + + if opcode > OP_PUSHDATA4: + yield (opcode, None, sop_idx) + else: + datasize = None + pushdata_type = None + if opcode < OP_PUSHDATA1: + pushdata_type = 'PUSHDATA(%d)' % opcode + datasize = opcode + + elif opcode == OP_PUSHDATA1: + pushdata_type = 'PUSHDATA1' + if i >= len(self): + raise CScriptInvalidError('PUSHDATA1: missing data length') + datasize = bord(self[i]) + i += 1 + + elif opcode == OP_PUSHDATA2: + pushdata_type = 'PUSHDATA2' + if i + 1 >= len(self): + raise CScriptInvalidError('PUSHDATA2: missing data length') + datasize = bord(self[i]) + (bord(self[i+1]) << 8) + i += 2 + + elif opcode == OP_PUSHDATA4: + pushdata_type = 'PUSHDATA4' + if i + 3 >= len(self): + raise CScriptInvalidError('PUSHDATA4: missing data length') + datasize = bord(self[i]) + (bord(self[i+1]) << 8) + (bord(self[i+2]) << 16) + (bord(self[i+3]) << 24) + i += 4 + + else: + assert False # shouldn't happen + + + data = bytes(self[i:i+datasize]) + + # Check for truncation + if len(data) < datasize: + raise CScriptTruncatedPushDataError('%s: truncated data' % pushdata_type, data) + + i += datasize + + yield (opcode, data, sop_idx) + + def __iter__(self): + """'Cooked' iteration + + Returns either a CScriptOP instance, an integer, or bytes, as + appropriate. + + See raw_iter() if you need to distinguish the different possible + PUSHDATA encodings. + """ + for (opcode, data, sop_idx) in self.raw_iter(): + if data is not None: + yield data + else: + opcode = CScriptOp(opcode) + + if opcode.is_small_int(): + yield opcode.decode_op_n() + else: + yield CScriptOp(opcode) + + def __repr__(self): + # For Python3 compatibility add b before strings so testcases don't + # need to change + def _repr(o): + if isinstance(o, bytes): + return b"x('%s')" % hexlify(o).decode('ascii') + else: + return repr(o) + + ops = [] + i = iter(self) + while True: + op = None + try: + op = _repr(next(i)) + except CScriptTruncatedPushDataError as err: + op = '%s...' % (_repr(err.data), err) + break + except CScriptInvalidError as err: + op = '' % err + break + except StopIteration: + break + finally: + if op is not None: + ops.append(op) + + return "CScript([%s])" % ', '.join(ops) + + def GetSigOpCount(self, fAccurate): + """Get the SigOp count. + + fAccurate - Accurately count CHECKMULTISIG, see BIP16 for details. + + Note that this is consensus-critical. + """ + n = 0 + lastOpcode = OP_INVALIDOPCODE + for (opcode, data, sop_idx) in self.raw_iter(): + if opcode in (OP_CHECKSIG, OP_CHECKSIGVERIFY): + n += 1 + elif opcode in (OP_CHECKMULTISIG, OP_CHECKMULTISIGVERIFY): + if fAccurate and (OP_1 <= lastOpcode <= OP_16): + n += opcode.decode_op_n() + else: + n += 20 + lastOpcode = opcode + return n + + +SIGHASH_ALL = 1 +SIGHASH_NONE = 2 +SIGHASH_SINGLE = 3 +SIGHASH_ANYONECANPAY = 0x80 + +def FindAndDelete(script, sig): + """Consensus critical, see FindAndDelete() in Satoshi codebase""" + r = b'' + last_sop_idx = sop_idx = 0 + skip = True + for (opcode, data, sop_idx) in script.raw_iter(): + if not skip: + r += script[last_sop_idx:sop_idx] + last_sop_idx = sop_idx + if script[sop_idx:sop_idx + len(sig)] == sig: + skip = True + else: + skip = False + if not skip: + r += script[last_sop_idx:] + return CScript(r) + + +def SignatureHash(script, txTo, inIdx, hashtype): + """Consensus-correct SignatureHash + + Returns (hash, err) to precisely match the consensus-critical behavior of + the SIGHASH_SINGLE bug. (inIdx is *not* checked for validity) + """ + HASH_ONE = b'\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + + if inIdx >= len(txTo.vin): + return (HASH_ONE, "inIdx %d out of range (%d)" % (inIdx, len(txTo.vin))) + txtmp = CTransaction(txTo) + + for txin in txtmp.vin: + txin.scriptSig = b'' + txtmp.vin[inIdx].scriptSig = FindAndDelete(script, CScript([OP_CODESEPARATOR])) + + if (hashtype & 0x1f) == SIGHASH_NONE: + txtmp.vout = [] + + for i in range(len(txtmp.vin)): + if i != inIdx: + txtmp.vin[i].nSequence = 0 + + elif (hashtype & 0x1f) == SIGHASH_SINGLE: + outIdx = inIdx + if outIdx >= len(txtmp.vout): + return (HASH_ONE, "outIdx %d out of range (%d)" % (outIdx, len(txtmp.vout))) + + tmp = txtmp.vout[outIdx] + txtmp.vout = [] + for i in range(outIdx): + txtmp.vout.append(CTxOut()) + txtmp.vout.append(tmp) + + for i in range(len(txtmp.vin)): + if i != inIdx: + txtmp.vin[i].nSequence = 0 + + if hashtype & SIGHASH_ANYONECANPAY: + tmp = txtmp.vin[inIdx] + txtmp.vin = [] + txtmp.vin.append(tmp) + + s = txtmp.serialize() + s += struct.pack(b"> (64 - b) | (n & ((1 << (64 - b)) - 1)) << b + +def siphash_round(v0, v1, v2, v3): + v0 = (v0 + v1) & ((1 << 64) - 1) + v1 = rotl64(v1, 13) + v1 ^= v0 + v0 = rotl64(v0, 32) + v2 = (v2 + v3) & ((1 << 64) - 1) + v3 = rotl64(v3, 16) + v3 ^= v2 + v0 = (v0 + v3) & ((1 << 64) - 1) + v3 = rotl64(v3, 21) + v3 ^= v0 + v2 = (v2 + v1) & ((1 << 64) - 1) + v1 = rotl64(v1, 17) + v1 ^= v2 + v2 = rotl64(v2, 32) + return (v0, v1, v2, v3) + +def siphash256(k0, k1, h): + n0 = h & ((1 << 64) - 1) + n1 = (h >> 64) & ((1 << 64) - 1) + n2 = (h >> 128) & ((1 << 64) - 1) + n3 = (h >> 192) & ((1 << 64) - 1) + v0 = 0x736f6d6570736575 ^ k0 + v1 = 0x646f72616e646f6d ^ k1 + v2 = 0x6c7967656e657261 ^ k0 + v3 = 0x7465646279746573 ^ k1 ^ n0 + v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3) + v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3) + v0 ^= n0 + v3 ^= n1 + v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3) + v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3) + v0 ^= n1 + v3 ^= n2 + v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3) + v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3) + v0 ^= n2 + v3 ^= n3 + v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3) + v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3) + v0 ^= n3 + v3 ^= 0x2000000000000000 + v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3) + v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3) + v0 ^= 0x2000000000000000 + v2 ^= 0xFF + v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3) + v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3) + v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3) + v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3) + return v0 ^ v1 ^ v2 ^ v3 diff --git a/basicswap/interface/contrib/nav_test_framework/util.py b/basicswap/interface/contrib/nav_test_framework/util.py new file mode 100755 index 0000000..d14c602 --- /dev/null +++ b/basicswap/interface/contrib/nav_test_framework/util.py @@ -0,0 +1,700 @@ +#!/usr/bin/env python3 +# Copyright (c) 2014-2016 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + + +# +# Helpful routines for regression testing +# + +import os +import sys + +from binascii import hexlify, unhexlify +from base64 import b64encode +from decimal import Decimal, ROUND_DOWN +import json +import http.client +import random +import shutil +import subprocess +import time +import re +import errno + +from . import coverage +from .authproxy import AuthServiceProxy, JSONRPCException + +COVERAGE_DIR = None + +# The maximum number of nodes a single test can spawn +MAX_NODES = 8 +# Don't assign rpc or p2p ports lower than this +PORT_MIN = 11000 +# The number of ports to "reserve" for p2p and rpc, each +PORT_RANGE = 5000 + +NAVCOIND_PROC_WAIT_TIMEOUT = 60 + + +class PortSeed: + # Must be initialized with a unique integer for each process + n = None + +#Set Mocktime default to OFF. +#MOCKTIME is only needed for scripts that use the +#cached version of the blockchain. If the cached +#version of the blockchain is used without MOCKTIME +#then the mempools will not sync due to IBD. +MOCKTIME = 0 + +def enable_mocktime(): + #For backwared compatibility of the python scripts + #with previous versions of the cache, set MOCKTIME + #to Jan 1, 2014 + (201 * 10 * 60) + global MOCKTIME + MOCKTIME = 1388534400 + (201 * 10 * 60) + +def disable_mocktime(): + global MOCKTIME + MOCKTIME = 0 + +def get_mocktime(): + return MOCKTIME + +def enable_coverage(dirname): + """Maintain a log of which RPC calls are made during testing.""" + global COVERAGE_DIR + COVERAGE_DIR = dirname + +def get_rpc_proxy(url, node_number, timeout=None): + """ + Args: + url (str): URL of the RPC server to call + node_number (int): the node number (or id) that this calls to + + Kwargs: + timeout (int): HTTP timeout in seconds + + Returns: + AuthServiceProxy. convenience object for making RPC calls. + + """ + proxy_kwargs = {} + if timeout is not None: + proxy_kwargs['timeout'] = timeout + + proxy = AuthServiceProxy(url, **proxy_kwargs) + proxy.url = url # store URL on proxy for info + + coverage_logfile = coverage.get_filename( + COVERAGE_DIR, node_number) if COVERAGE_DIR else None + + return coverage.AuthServiceProxyWrapper(proxy, coverage_logfile) + + +def p2p_port(n): + assert(n <= MAX_NODES) + return PORT_MIN + n + (MAX_NODES * PortSeed.n) % (PORT_RANGE - 1 - MAX_NODES) + +def rpc_port(n): + return PORT_MIN + PORT_RANGE + n + (MAX_NODES * PortSeed.n) % (PORT_RANGE - 1 - MAX_NODES) + +def check_json_precision(): + """Make sure json library being used does not lose precision converting NAV values""" + n = Decimal("20000000.00000003") + satoshis = int(json.loads(json.dumps(float(n)))*1.0e8) + if satoshis != 2000000000000003: + raise RuntimeError("JSON encode/decode loses precision") + +def count_bytes(hex_string): + return len(bytearray.fromhex(hex_string)) + +def bytes_to_hex_str(byte_str): + return hexlify(byte_str).decode('ascii') + +def hex_str_to_bytes(hex_str): + return unhexlify(hex_str.encode('ascii')) + +def str_to_b64str(string): + return b64encode(string.encode('utf-8')).decode('ascii') + +def sync_blocks(rpc_connections, wait=1, timeout=60): + """ + Wait until everybody has the same tip + """ + while timeout > 0: + tips = [ x.getbestblockhash() for x in rpc_connections ] + if tips == [ tips[0] ]*len(tips): + #if all x.getblockhash() in tips are the same return True + return True + time.sleep(wait) + timeout -= wait + raise AssertionError("Block sync failed") + +def sync_mempools(rpc_connections, wait=1, timeout=60): + """ + Wait until everybody has the same transactions in their memory + pools + """ + while timeout > 0: + pool = set(rpc_connections[0].getrawmempool()) + num_match = 1 + for i in range(1, len(rpc_connections)): + if set(rpc_connections[i].getrawmempool()) == pool: + num_match = num_match+1 + if num_match == len(rpc_connections): + return True + time.sleep(wait) + timeout -= wait + raise AssertionError("Mempool sync failed") + +navcoind_processes = {} + +def initialize_datadir(dirname, n): + datadir = os.path.join(dirname, "node"+str(n)) + if not os.path.isdir(datadir): + os.makedirs(datadir) + rpc_u, rpc_p = rpc_auth_pair(n) + with open(os.path.join(datadir, "navcoin.conf"), 'w') as f: + f.write("devnet=1\n") + f.write("rpcuser=" + rpc_u + "\n") + f.write("rpcpassword=" + rpc_p + "\n") + f.write("port="+str(p2p_port(n))+"\n") + f.write("rpcport="+str(rpc_port(n))+"\n") + f.write("listenonion=0\n") + f.write("dandelion=0\n") + f.write("ntpminmeasures=-1\n") + f.write("torserver=0\n") + f.write("suppressblsctwarning=1\n") + return datadir + +def rpc_auth_pair(n): + return 'rpcuser💻' + str(n), 'rpcpass🔑' + str(n) + +def rpc_url(i, rpchost=None): + rpc_u, rpc_p = rpc_auth_pair(i) + return "http://%s:%s@%s:%d" % (rpc_u, rpc_p, rpchost or '127.0.0.1', rpc_port(i)) + +def wait_for_navcoind_start(process, url, i): + ''' + Wait for navcoind to start. This means that RPC is accessible and fully initialized. + Raise an exception if navcoind exits during initialization. + ''' + polls_interval = 1.0 / 4 + runtime = 60 + while runtime > 0: + if process.poll() is not None: + raise Exception('navcoind exited with status %i during initialization' % process.returncode) + try: + # print('Checking RPC') + rpc = get_rpc_proxy(url, i) + blocks = rpc.getblockcount() + # print('RPC replied with blocks: %i' % blocks) + return # break out of loop on success + except IOError as e: + if e.errno != errno.ECONNREFUSED: # Port not yet open? + raise # unknown IO error + # else: + # print('Waiting for port') + except JSONRPCException as e: # Initialization phase + if e.error['code'] != -28: # RPC in warmup? + raise # unkown JSON RPC exception + # else: + # print('RPC in warmup') + time.sleep(polls_interval) + runtime -= polls_interval + raise Exception('navcoind RPC timeout') + +def initialize_chain(test_dir, num_nodes): + """ + Create a cache of a 200-block-long chain (with wallet) for MAX_NODES + Afterward, create num_nodes copies from the cache + """ + + assert num_nodes <= MAX_NODES + create_cache = False + for i in range(MAX_NODES): + if not os.path.isdir(os.path.join('cache', 'node'+str(i))): + create_cache = True + break + + if create_cache: + + #find and delete old cache directories if any exist + for i in range(MAX_NODES): + if os.path.isdir(os.path.join("cache","node"+str(i))): + shutil.rmtree(os.path.join("cache","node"+str(i))) + + # Create cache directories, run navcoinds: + for i in range(MAX_NODES): + datadir=initialize_datadir("cache", i) + args = [ os.getenv("NAVCOIND", "navcoind"), "-server", "-keypool=1", "-datadir="+datadir, "-discover=0" ] + if i > 0: + args.append("-connect=127.0.0.1:"+str(p2p_port(0))) + navcoind_processes[i] = subprocess.Popen(args) + if os.getenv("PYTHON_DEBUG", ""): + print("initialize_chain: navcoind started, waiting for RPC to come up") + wait_for_navcoind_start(navcoind_processes[i], rpc_url(i), i) + if os.getenv("PYTHON_DEBUG", ""): + print("initialize_chain: RPC succesfully started") + + rpcs = [] + for i in range(MAX_NODES): + try: + rpcs.append(get_rpc_proxy(rpc_url(i), i)) + except: + sys.stderr.write("Error connecting to "+url+"\n") + sys.exit(1) + + # Create a 200-block-long chain; each of the 4 first nodes + # gets 25 mature blocks and 25 immature. + # Note: To preserve compatibility with older versions of + # initialize_chain, only 4 nodes will generate coins. + # + # blocks are created with timestamps 10 minutes apart + # starting from 2010 minutes in the past + enable_mocktime() + block_time = get_mocktime() - (201 * 10 * 60) + for i in range(2): + for peer in range(4): + for j in range(25): + set_node_times(rpcs, block_time) + slow_gen(rpcs[peer], 1) + block_time += 10*60 + # Must sync before next peer starts generating blocks + sync_blocks(rpcs) + + # Shut them down, and clean up cache directories: + stop_nodes(rpcs) + wait_navcoinds() + disable_mocktime() + for i in range(MAX_NODES): + os.remove(log_filename("cache", i, "debug.log")) + os.remove(log_filename("cache", i, "db.log")) + os.remove(log_filename("cache", i, "peers.dat")) + os.remove(log_filename("cache", i, "fee_estimates.dat")) + + for i in range(num_nodes): + from_dir = os.path.join("cache", "node"+str(i)) + to_dir = os.path.join(test_dir, "node"+str(i)) + shutil.copytree(from_dir, to_dir) + initialize_datadir(test_dir, i) # Overwrite port/rpcport in navcoin.conf + +def initialize_chain_clean(test_dir, num_nodes): + """ + Create an empty blockchain and num_nodes wallets. + Useful if a test case wants complete control over initialization. + """ + for i in range(num_nodes): + datadir=initialize_datadir(test_dir, i) + + +def _rpchost_to_args(rpchost): + '''Convert optional IP:port spec to rpcconnect/rpcport args''' + if rpchost is None: + return [] + + match = re.match('(\[[0-9a-fA-f:]+\]|[^:]+)(?::([0-9]+))?$', rpchost) + if not match: + raise ValueError('Invalid RPC host spec ' + rpchost) + + rpcconnect = match.group(1) + rpcport = match.group(2) + + if rpcconnect.startswith('['): # remove IPv6 [...] wrapping + rpcconnect = rpcconnect[1:-1] + + rv = ['-rpcconnect=' + rpcconnect] + if rpcport: + rv += ['-rpcport=' + rpcport] + return rv + +def start_node(i, dirname, extra_args=None, rpchost=None, timewait=None, binary=None): + """ + Start a navcoind and return RPC connection to it + """ + datadir = os.path.join(dirname, "node"+str(i)) + if binary is None: + binary = os.getenv("NAVCOIND", "navcoind") + args = [ binary, "-datadir="+datadir, "-server", "-keypool=1", "-discover=0", "-rest", "-mocktime="+str(get_mocktime()) ] + if extra_args is not None: args.extend(extra_args) + navcoind_processes[i] = subprocess.Popen(args) + if os.getenv("PYTHON_DEBUG", ""): + print("start_node: navcoind started, waiting for RPC to come up") + url = rpc_url(i, rpchost) + wait_for_navcoind_start(navcoind_processes[i], url, i) + if os.getenv("PYTHON_DEBUG", ""): + print("start_node: RPC succesfully started") + proxy = get_rpc_proxy(url, i, timeout=timewait) + + if COVERAGE_DIR: + coverage.write_all_rpc_commands(COVERAGE_DIR, proxy) + + return proxy + +def start_nodes(num_nodes, dirname, extra_args=None, rpchost=None, binary=None): + """ + Start multiple navcoinds, return RPC connections to them + """ + if extra_args is None: extra_args = [ None for _ in range(num_nodes) ] + if binary is None: binary = [ None for _ in range(num_nodes) ] + rpcs = [] + try: + for i in range(num_nodes): + rpcs.append(start_node(i, dirname, extra_args[i], rpchost, binary=binary[i])) + except: # If one node failed to start, stop the others + stop_nodes(rpcs) + raise + return rpcs + +def log_filename(dirname, n_node, logname): + return os.path.join(dirname, "node"+str(n_node), "devnet", logname) + +def stop_node(node, i): + try: + node.stop() + except http.client.CannotSendRequest as e: + print("WARN: Unable to stop node: " + repr(e)) + navcoind_processes[i].wait(timeout=NAVCOIND_PROC_WAIT_TIMEOUT) + del navcoind_processes[i] + +def stop_nodes(nodes): + for node in nodes: + try: + node.stop() + except http.client.CannotSendRequest as e: + print("WARN: Unable to stop node: " + repr(e)) + del nodes[:] # Emptying array closes connections as a side effect + +def set_node_times(nodes, t): + for node in nodes: + node.setmocktime(t) + +def wait_navcoinds(): + # Wait for all navcoinds to cleanly exit + for navcoind in navcoind_processes.values(): + navcoind.wait(timeout=NAVCOIND_PROC_WAIT_TIMEOUT) + navcoind_processes.clear() + +def connect_nodes(from_connection, node_num): + ip_port = "127.0.0.1:"+str(p2p_port(node_num)) + from_connection.addnode(ip_port, "onetry") + # poll until version handshake complete to avoid race conditions + # with transaction relaying + while any(peer['version'] == 0 for peer in from_connection.getpeerinfo()): + time.sleep(0.1) + +def connect_nodes_bi(nodes, a, b): + connect_nodes(nodes[a], b) + connect_nodes(nodes[b], a) + +def find_output(node, txid, amount): + """ + Return index to output of txid with value amount + Raises exception if there is none. + """ + txdata = node.getrawtransaction(txid, 1) + for i in range(len(txdata["vout"])): + if txdata["vout"][i]["value"] == amount: + return i + raise RuntimeError("find_output txid %s : %s not found"%(txid,str(amount))) + + +def gather_inputs(from_node, amount_needed, confirmations_required=1): + """ + Return a random set of unspent txouts that are enough to pay amount_needed + """ + assert(confirmations_required >=0) + utxo = from_node.listunspent(confirmations_required) + random.shuffle(utxo) + inputs = [] + total_in = Decimal("0.00000000") + while total_in < amount_needed and len(utxo) > 0: + t = utxo.pop() + total_in += t["amount"] + inputs.append({ "txid" : t["txid"], "vout" : t["vout"], "address" : t["address"] } ) + if total_in < amount_needed: + raise RuntimeError("Insufficient funds: need %d, have %d"%(amount_needed, total_in)) + return (total_in, inputs) + +def make_change(from_node, amount_in, amount_out, fee): + """ + Create change output(s), return them + """ + outputs = {} + amount = amount_out+fee + change = amount_in - amount + if change > amount*2: + # Create an extra change output to break up big inputs + change_address = from_node.getnewaddress() + # Split change in two, being careful of rounding: + outputs[change_address] = Decimal(change/2).quantize(Decimal('0.00000001'), rounding=ROUND_DOWN) + change = amount_in - amount - outputs[change_address] + if change > 0: + outputs[from_node.getnewaddress()] = change + return outputs + +def send_zeropri_transaction(from_node, to_node, amount, fee): + """ + Create&broadcast a zero-priority transaction. + Returns (txid, hex-encoded-txdata) + Ensures transaction is zero-priority by first creating a send-to-self, + then using its output + """ + + # Create a send-to-self with confirmed inputs: + self_address = from_node.getnewaddress() + (total_in, inputs) = gather_inputs(from_node, amount+fee*2) + outputs = make_change(from_node, total_in, amount+fee, fee) + outputs[self_address] = float(amount+fee) + + self_rawtx = from_node.createrawtransaction(inputs, outputs) + self_signresult = from_node.signrawtransaction(self_rawtx) + self_txid = from_node.sendrawtransaction(self_signresult["hex"], True) + + vout = find_output(from_node, self_txid, amount+fee) + # Now immediately spend the output to create a 1-input, 1-output + # zero-priority transaction: + inputs = [ { "txid" : self_txid, "vout" : vout } ] + outputs = { to_node.getnewaddress() : float(amount) } + + rawtx = from_node.createrawtransaction(inputs, outputs) + signresult = from_node.signrawtransaction(rawtx) + txid = from_node.sendrawtransaction(signresult["hex"], True) + + return (txid, signresult["hex"]) + +def random_zeropri_transaction(nodes, amount, min_fee, fee_increment, fee_variants): + """ + Create a random zero-priority transaction. + Returns (txid, hex-encoded-transaction-data, fee) + """ + from_node = random.choice(nodes) + to_node = random.choice(nodes) + fee = min_fee + fee_increment*random.randint(0,fee_variants) + (txid, txhex) = send_zeropri_transaction(from_node, to_node, amount, fee) + return (txid, txhex, fee) + +def random_transaction(nodes, amount, min_fee, fee_increment, fee_variants): + """ + Create a random transaction. + Returns (txid, hex-encoded-transaction-data, fee) + """ + from_node = random.choice(nodes) + to_node = random.choice(nodes) + fee = min_fee + fee_increment*random.randint(0,fee_variants) + + (total_in, inputs) = gather_inputs(from_node, amount+fee) + outputs = make_change(from_node, total_in, amount, fee) + outputs[to_node.getnewaddress()] = float(amount) + + rawtx = from_node.createrawtransaction(inputs, outputs) + signresult = from_node.signrawtransaction(rawtx) + txid = from_node.sendrawtransaction(signresult["hex"], True) + + return (txid, signresult["hex"], fee) + +def assert_fee_amount(fee, tx_size, fee_per_kB): + """Assert the fee was in range""" + target_fee = tx_size * fee_per_kB / 1000 + if fee < target_fee: + raise AssertionError("Fee of %s NAV too low! (Should be %s NAV)"%(str(fee), str(target_fee))) + # allow the wallet's estimation to be at most 2 bytes off + if fee > (tx_size + 2) * fee_per_kB / 1000: + raise AssertionError("Fee of %s NAV too high! (Should be %s NAV)"%(str(fee), str(target_fee))) + +def assert_equal(thing1, thing2): + if thing1 != thing2: + raise AssertionError("%s != %s"%(str(thing1),str(thing2))) + +def assert_greater_than(thing1, thing2): + if thing1 <= thing2: + raise AssertionError("%s <= %s"%(str(thing1),str(thing2))) + +def assert_raises(exc, fun, *args, **kwds): + try: + fun(*args, **kwds) + except exc: + pass + except Exception as e: + raise AssertionError("Unexpected exception raised: "+type(e).__name__) + else: + raise AssertionError("No exception raised") + +def assert_is_hex_string(string): + try: + int(string, 16) + except Exception as e: + raise AssertionError( + "Couldn't interpret %r as hexadecimal; raised: %s" % (string, e)) + +def assert_is_hash_string(string, length=64): + if not isinstance(string, str): + raise AssertionError("Expected a string, got type %r" % type(string)) + elif length and len(string) != length: + raise AssertionError( + "String of length %d expected; got %d" % (length, len(string))) + elif not re.match('[abcdef0-9]+$', string): + raise AssertionError( + "String %r contains invalid characters for a hash." % string) + +def assert_array_result(object_array, to_match, expected, should_not_find = False): + """ + Pass in array of JSON objects, a dictionary with key/value pairs + to match against, and another dictionary with expected key/value + pairs. + If the should_not_find flag is true, to_match should not be found + in object_array + """ + if should_not_find == True: + assert_equal(expected, { }) + num_matched = 0 + for item in object_array: + all_match = True + for key,value in to_match.items(): + if item[key] != value: + all_match = False + if not all_match: + continue + elif should_not_find == True: + num_matched = num_matched+1 + for key,value in expected.items(): + if item[key] != value: + raise AssertionError("%s : expected %s=%s"%(str(item), str(key), str(value))) + num_matched = num_matched+1 + if num_matched == 0 and should_not_find != True: + raise AssertionError("No objects matched %s"%(str(to_match))) + if num_matched > 0 and should_not_find == True: + raise AssertionError("Objects were found %s"%(str(to_match))) + +def assert_raises_rpc_error(code, message, fun, *args, **kwds): + """Run an RPC and verify that a specific JSONRPC exception code and message is raised. + Calls function `fun` with arguments `args` and `kwds`. Catches a JSONRPCException + and verifies that the error code and message are as expected. Throws AssertionError if + no JSONRPCException was raised or if the error code/message are not as expected. + Args: + code (int), optional: the error code returned by the RPC call (defined + in src/rpc/protocol.h). Set to None if checking the error code is not required. + message (string), optional: [a substring of] the error string returned by the + RPC call. Set to None if checking the error string is not required. + fun (function): the function to call. This should be the name of an RPC. + args*: positional arguments for the function. + kwds**: named arguments for the function. + """ + assert try_rpc(code, message, fun, *args, **kwds), "No exception raised" + +def try_rpc(code, message, fun, *args, **kwds): + """Tries to run an rpc command. + Test against error code and message if the rpc fails. + Returns whether a JSONRPCException was raised.""" + try: + fun(*args, **kwds) + except JSONRPCException as e: + # JSONRPCException was thrown as expected. Check the code and message values are correct. + if (code is not None) and (code != e.error["code"]): + raise AssertionError("Unexpected JSONRPC error code %i" % e.error["code"]) + if (message is not None) and (message not in e.error['message']): + raise AssertionError("Expected substring not found:" + e.error['message']) + return True + except Exception as e: + raise AssertionError("Unexpected exception raised: " + type(e).__name__) + else: + return False + +def satoshi_round(amount): + return Decimal(amount).quantize(Decimal('0.00000001'), rounding=ROUND_DOWN) + +# Helper to create at least "count" utxos +# Pass in a fee that is sufficient for relay and mining new transactions. +def create_confirmed_utxos(fee, node, count): + node.generate(int(0.5*count)+101) + utxos = node.listunspent() + iterations = count - len(utxos) + addr1 = node.getnewaddress() + addr2 = node.getnewaddress() + if iterations <= 0: + return utxos + for i in range(iterations): + t = utxos.pop() + inputs = [] + inputs.append({ "txid" : t["txid"], "vout" : t["vout"]}) + outputs = {} + send_value = t['amount'] - fee + outputs[addr1] = satoshi_round(send_value/2) + outputs[addr2] = satoshi_round(send_value/2) + raw_tx = node.createrawtransaction(inputs, outputs) + signed_tx = node.signrawtransaction(raw_tx)["hex"] + txid = node.sendrawtransaction(signed_tx) + + while (node.getmempoolinfo()['size'] > 0): + node.generate(1) + + utxos = node.listunspent() + assert(len(utxos) >= count) + return utxos + +# Create large OP_RETURN txouts that can be appended to a transaction +# to make it large (helper for constructing large transactions). +def gen_return_txouts(): + # Some pre-processing to create a bunch of OP_RETURN txouts to insert into transactions we create + # So we have big transactions (and therefore can't fit very many into each block) + # create one script_pubkey + script_pubkey = "6a4d0200" #OP_RETURN OP_PUSH2 512 bytes + for i in range (512): + script_pubkey = script_pubkey + "01" + # concatenate 128 txouts of above script_pubkey which we'll insert before the txout for change + txouts = "81" + for k in range(128): + # add txout value + txouts = txouts + "0000000000000000" + # add length of script_pubkey + txouts = txouts + "fd0402" + # add script_pubkey + txouts = txouts + script_pubkey + return txouts + +def create_tx(node, coinbase, to_address, amount): + inputs = [{ "txid" : coinbase, "vout" : 0}] + outputs = { to_address : amount } + rawtx = node.createrawtransaction(inputs, outputs) + signresult = node.signrawtransaction(rawtx) + assert_equal(signresult["complete"], True) + return signresult["hex"] + +# Create a spend of each passed-in utxo, splicing in "txouts" to each raw +# transaction to make it large. See gen_return_txouts() above. +def create_lots_of_big_transactions(node, txouts, utxos, fee): + addr = node.getnewaddress() + txids = [] + for i in range(len(utxos)): + t = utxos.pop() + inputs = [] + inputs.append({ "txid" : t["txid"], "vout" : t["vout"]}) + outputs = {} + send_value = t['amount'] - fee + outputs[addr] = satoshi_round(send_value) + rawtx = node.createrawtransaction(inputs, outputs) + newtx = rawtx[0:92] + newtx = newtx + txouts + newtx = newtx + rawtx[94:] + signresult = node.signrawtransaction(newtx, None, None, "NONE") + txid = node.sendrawtransaction(signresult["hex"], True) + txids.append(txid) + return txids + +def get_bip9_status(node, key): + info = node.getblockchaininfo() + return info['bip9_softforks'][key] + + +def slow_gen(node, count, sleep = 0.1): + total = count + blocks = [] + while total > 0: + now = min(total, 10) + blocks.extend(node.generate(now)) + total -= now + time.sleep(sleep) + return blocks diff --git a/basicswap/interface/dash.py b/basicswap/interface/dash.py index 5e6835b..3929300 100644 --- a/basicswap/interface/dash.py +++ b/basicswap/interface/dash.py @@ -25,10 +25,10 @@ class DASHInterface(BTCInterface): self._wallet_passphrase = '' self._have_checked_seed = False - def seedToMnemonic(self, key): + def seedToMnemonic(self, key: bytes) -> str: return Mnemonic('english').to_mnemonic(key) - def initialiseWallet(self, key): + def initialiseWallet(self, key: bytes): words = self.seedToMnemonic(key) mnemonic_passphrase = '' @@ -37,10 +37,10 @@ class DASHInterface(BTCInterface): if self._wallet_passphrase != '': self.unlockWallet(self._wallet_passphrase) - def decodeAddress(self, address): + def decodeAddress(self, address: str) -> bytes: return decodeAddress(address)[1:] - def checkExpectedSeed(self, key_hash): + def checkExpectedSeed(self, key_hash: str): try: rv = self.rpc_callback('dumphdinfo') entropy = Mnemonic('english').to_entropy(rv['mnemonic'].split(' ')) diff --git a/basicswap/interface/firo.py b/basicswap/interface/firo.py index 2301ed8..092ba20 100644 --- a/basicswap/interface/firo.py +++ b/basicswap/interface/firo.py @@ -163,15 +163,15 @@ class FIROInterface(BTCInterface): return CScript([OP_HASH160, script_hash_hash, OP_EQUAL]) - def getSeedHash(self, seed) -> bytes: + def getSeedHash(self, seed: bytes) -> bytes: return hash160(seed)[::-1] - def encodeScriptDest(self, script): + def encodeScriptDest(self, script_dest: bytes) -> str: # Extract hash from script - script_hash = script[2:-1] + script_hash = script_dest[2:-1] return self.sh_to_address(script_hash) - def getScriptScriptSig(self, script): + def getScriptScriptSig(self, script: bytes) -> bytearray: return CScript([OP_0, hashlib.sha256(script).digest()]) def withdrawCoin(self, value, addr_to, subfee): diff --git a/basicswap/interface/nav.py b/basicswap/interface/nav.py new file mode 100644 index 0000000..7608732 --- /dev/null +++ b/basicswap/interface/nav.py @@ -0,0 +1,666 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright (c) 2023 tecnovert +# Distributed under the MIT software license, see the accompanying +# file LICENSE or http://www.opensource.org/licenses/mit-license.php. + +from io import BytesIO +from coincurve.keys import ( + PublicKey, + PrivateKey, +) +from .btc import BTCInterface, find_vout_for_address_from_txobj, findOutput +from basicswap.chainparams import Coins +from basicswap.interface.contrib.nav_test_framework.mininode import ( + CTxIn, + CTxOut, + CBlock, + COutPoint, + CTransaction, + CTxInWitness, + FromHex, + uint256_from_str, +) +from basicswap.util.address import ( + decodeWif, + pubkeyToAddress, + encodeAddress, +) +from basicswap.util import ( + i2b, i2h, + ensure, +) +from basicswap.basicswap_util import ( + getVoutByScriptPubKey, +) + +from basicswap.interface.contrib.nav_test_framework.script import ( + hash160, + CScript, + OP_0, + OP_EQUAL, + OP_DUP, OP_HASH160, OP_EQUALVERIFY, OP_CHECKSIG, + SIGHASH_ALL, + SegwitVersion1SignatureHash, +) +from mnemonic import Mnemonic + + +class NAVInterface(BTCInterface): + @staticmethod + def coin_type(): + return Coins.NAV + + @staticmethod + def txVersion() -> int: + return 3 + + @staticmethod + def txoType(): + return CTxOut + + def use_p2shp2wsh(self) -> bool: + # p2sh-p2wsh + return True + + def seedToMnemonic(self, key): + return Mnemonic('english').to_mnemonic(key) + + def initialiseWallet(self, key): + # load with -importmnemonic= parameter + pass + + def getWalletSeedID(self): + return self.rpc_callback('getwalletinfo')['hdmasterkeyid'] + + def withdrawCoin(self, value, addr_to: str, subfee: bool): + strdzeel = '' + params = [addr_to, value, '', '', strdzeel, subfee] + return self.rpc_callback('sendtoaddress', params) + + def getSpendableBalance(self) -> int: + return self.make_int(self.rpc_callback('getwalletinfo')['balance']) + + def signTxWithWallet(self, tx: bytes) -> bytes: + rv = self.rpc_callback('signrawtransaction', [tx.hex()]) + + return bytes.fromhex(rv['hex']) + + def checkExpectedSeed(self, key_hash: str): + try: + rv = self.rpc_callback('dumpmnemonic') + entropy = Mnemonic('english').to_entropy(rv.split(' ')) + + entropy_hash = self.getAddressHashFromKey(entropy)[::-1].hex() + self._have_checked_seed = True + return entropy_hash == key_hash + except Exception as e: + self._log.warning('checkExpectedSeed failed: {}'.format(str(e))) + return False + + def getScriptForP2PKH(self, pkh: bytes) -> bytearray: + # Return P2PKH + return CScript([OP_DUP, OP_HASH160, pkh, OP_EQUALVERIFY, OP_CHECKSIG]) + + def getScriptForPubkeyHash(self, pkh: bytes) -> bytearray: + # Return P2SH-p2wpkh + + script = CScript([OP_0, pkh]) + script_hash = hash160(script) + assert len(script_hash) == 20 + + return CScript([OP_HASH160, script_hash, OP_EQUAL]) + + def getInputScriptForPubkeyHash(self, pkh: bytes) -> bytearray: + script = CScript([OP_0, pkh]) + return bytes((len(script),)) + script + + def encodeSegwitAddress(self, pkh: bytes) -> str: + # P2SH-p2wpkh + script = CScript([OP_0, pkh]) + script_hash = hash160(script) + assert len(script_hash) == 20 + return encodeAddress(bytes((self.chainparams_network()['script_address'],)) + script_hash) + + def encodeSegwitAddressScript(self, script: bytes) -> str: + if len(script) == 23 and script[0] == OP_HASH160 and script[1] == 20 and script[22] == OP_EQUAL: + script_hash = script[2:22] + return encodeAddress(bytes((self.chainparams_network()['script_address'],)) + script_hash) + raise ValueError('Unknown Script') + + def loadTx(self, tx_bytes: bytes) -> CTransaction: + # Load tx from bytes to internal representation + tx = CTransaction() + tx.deserialize(BytesIO(tx_bytes)) + return tx + + def signTx(self, key_bytes: bytes, tx_bytes: bytes, input_n: int, prevout_script, prevout_value: int): + tx = self.loadTx(tx_bytes) + sig_hash = SegwitVersion1SignatureHash(prevout_script, tx, input_n, SIGHASH_ALL, prevout_value) + eck = PrivateKey(key_bytes) + return eck.sign(sig_hash, hasher=None) + bytes((SIGHASH_ALL,)) + + def setTxSignature(self, tx_bytes: bytes, stack) -> bytes: + tx = self.loadTx(tx_bytes) + tx.wit.vtxinwit.clear() + tx.wit.vtxinwit.append(CTxInWitness()) + tx.wit.vtxinwit[0].scriptWitness.stack = stack + return tx.serialize_with_witness() + + def verifyProofOfFunds(self, address, signature, extra_commit_bytes): + self._log.warning('verifyProofOfFunds TODO') + # TODO: Port scantxoutset or external lookup or read utxodb directly + return 999999 * self.COIN() + + def createRawFundedTransaction(self, addr_to: str, amount: int, sub_fee: bool = False, lock_unspents: bool = True) -> str: + 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}') + if sub_fee: + raise ValueError('Navcoin fundrawtransaction is missing the subtractFeeFromOutputs parameter') + # options['subtractFeeFromOutputs'] = [0,] + + return self.fundTx(txn, fee_rate, lock_unspents) + + def isAddressMine(self, address: str, or_watch_only: bool = False) -> bool: + addr_info = self.rpc_callback('validateaddress', [address]) + if not or_watch_only: + return addr_info['ismine'] + return addr_info['ismine'] or addr_info['iswatchonly'] + + def createRawSignedTransaction(self, addr_to, amount) -> str: + txn_funded = self.createRawFundedTransaction(addr_to, amount) + return self.rpc_callback('signrawtransaction', [txn_funded])['hex'] + + def getBlockchainInfo(self): + rv = self.rpc_callback('getblockchaininfo') + synced = round(rv['verificationprogress'], 3) + if synced >= 0.997: + rv['verificationprogress'] = 1.0 + return rv + + def encodeScriptDest(self, script_dest: bytes) -> str: + script_hash = script_dest[2:-1] # Extract hash from script + return self.sh_to_address(script_hash) + + def encode_p2wsh(self, script: bytes) -> str: + return pubkeyToAddress(self.chainparams_network()['script_address'], script) + + def find_prevout_info(self, txn_hex: str, txn_script: bytes): + txjs = self.rpc_callback('decoderawtransaction', [txn_hex]) + n = getVoutByScriptPubKey(txjs, self.getScriptDest(txn_script).hex()) + + return { + 'txid': txjs['txid'], + 'vout': n, + 'scriptPubKey': txjs['vout'][n]['scriptPubKey']['hex'], + 'redeemScript': txn_script.hex(), + 'amount': txjs['vout'][n]['value'] + } + + 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.using_segwit(): # TODO: Use isSegwitAddress when scantxoutset can use combo + # 'Address does not refer to key' for non p2pkh + addr_info = self.rpc_callback('validateaddress', [addr, ]) + if 'isscript' in addr_info and addr_info['isscript'] and 'hex' in addr_info: + pkh = bytes.fromhex(addr_info['hex'])[2:] + 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 getNewAddress(self, use_segwit: bool, label: str = 'swap_receive') -> str: + address: str = self.rpc_callback('getnewaddress', [label,]) + if use_segwit: + return self.rpc_callback('addwitnessaddress', [address,]) + return address + + def createRedeemTxn(self, prevout, output_addr: str, output_value: int, txn_script: bytes) -> str: + tx = CTransaction() + tx.nVersion = self.txVersion() + prev_txid = uint256_from_str(bytes.fromhex(prevout['txid'])[::-1]) + + tx.vin.append(CTxIn(COutPoint(prev_txid, prevout['vout']), + scriptSig=self.getScriptScriptSig(txn_script))) + pkh = self.decodeAddress(output_addr) + script = self.getScriptForPubkeyHash(pkh) + tx.vout.append(self.txoType()(output_value, script)) + tx.rehash() + return tx.serialize().hex() + + def createRefundTxn(self, prevout, output_addr: str, output_value: int, locktime: int, sequence: int, txn_script: bytes) -> str: + tx = CTransaction() + tx.nVersion = self.txVersion() + tx.nLockTime = locktime + prev_txid = uint256_from_str(bytes.fromhex(prevout['txid'])[::-1]) + tx.vin.append(CTxIn(COutPoint(prev_txid, prevout['vout']), + nSequence=sequence, + scriptSig=self.getScriptScriptSig(txn_script))) + pkh = self.decodeAddress(output_addr) + script = self.getScriptForPubkeyHash(pkh) + tx.vout.append(self.txoType()(output_value, script)) + tx.rehash() + return tx.serialize().hex() + + def getTxSignature(self, tx_hex: str, prevout_data, key_wif: str) -> str: + key = decodeWif(key_wif) + redeem_script = bytes.fromhex(prevout_data['redeemScript']) + sig = self.signTx(key, bytes.fromhex(tx_hex), 0, redeem_script, self.make_int(prevout_data['amount'])) + + return sig.hex() + + def verifyTxSig(self, tx_bytes: bytes, sig: bytes, K: bytes, input_n: int, prevout_script: bytes, prevout_value: int) -> bool: + tx = self.loadTx(tx_bytes) + sig_hash = SegwitVersion1SignatureHash(prevout_script, tx, input_n, SIGHASH_ALL, prevout_value) + + pubkey = PublicKey(K) + return pubkey.verify(sig[: -1], sig_hash, hasher=None) # Pop the hashtype byte + + def verifyRawTransaction(self, tx_hex: str, prevouts): + # Only checks signature + # verifyrawtransaction + self._log.warning('NAV verifyRawTransaction only checks signature') + inputs_valid: bool = False + validscripts: int = 0 + + tx_bytes = bytes.fromhex(tx_hex) + tx = self.loadTx(bytes.fromhex(tx_hex)) + + signature = tx.wit.vtxinwit[0].scriptWitness.stack[0] + pubkey = tx.wit.vtxinwit[0].scriptWitness.stack[1] + + input_n: int = 0 + prevout_data = prevouts[input_n] + redeem_script = bytes.fromhex(prevout_data['redeemScript']) + prevout_value = self.make_int(prevout_data['amount']) + + if self.verifyTxSig(tx_bytes, signature, pubkey, input_n, redeem_script, prevout_value): + validscripts += 1 + + # TODO: validate inputs + inputs_valid = True + + return { + 'inputs_valid': inputs_valid, + 'validscripts': validscripts, + } + + def getHTLCSpendTxVSize(self, redeem: bool = True) -> int: + tx_vsize = 5 # Add a few bytes, sequence in script takes variable amount of bytes + + tx_vsize += 184 if redeem else 187 + return tx_vsize + + def getTxid(self, tx) -> bytes: + if isinstance(tx, str): + tx = bytes.fromhex(tx) + if isinstance(tx, bytes): + tx = self.loadTx(tx) + tx.rehash() + return i2b(tx.sha256) + + def rescanBlockchainForAddress(self, height_start: int, addr_find: str): + # Very ugly workaround for missing `rescanblockchain` rpc command + + chain_blocks: int = self.getChainHeight() + + current_height: int = chain_blocks + block_hash = self.rpc_callback('getblockhash', [current_height]) + + script_hash: bytes = self.decodeAddress(addr_find) + find_scriptPubKey = self.getDestForScriptHash(script_hash) + + while current_height > height_start: + block_hash = self.rpc_callback('getblockhash', [current_height]) + + block = self.rpc_callback('getblock', [block_hash, False]) + decoded_block = CBlock() + decoded_block = FromHex(decoded_block, block) + for tx in decoded_block.vtx: + for txo in tx.vout: + if txo.scriptPubKey == find_scriptPubKey: + tx.rehash() + txid = i2b(tx.sha256) + self._log.info('Found output to addr: {} in tx {} in block {}'.format(addr_find, txid.hex(), block_hash)) + self._log.info('rescanblockchain hack invalidateblock {}'.format(block_hash)) + self.rpc_callback('invalidateblock', [block_hash]) + self.rpc_callback('reconsiderblock', [block_hash]) + return + current_height -= 1 + + def getLockTxHeight(self, txid, dest_address, bid_amount, rescan_from, find_index: bool = False): + # Add watchonly address and rescan if required + + if not self.isAddressMine(dest_address, or_watch_only=True): + self.importWatchOnlyAddress(dest_address, 'bid') + self._log.info('Imported watch-only addr: {}'.format(dest_address)) + # Importing triggers a rescan + self._log.info('Rescanning {} chain from height: {}'.format(self.coin_name(), rescan_from)) + self.rescanBlockchainForAddress(rescan_from, dest_address) + + 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 getBlockWithTxns(self, block_hash): + # TODO: Bypass decoderawtransaction and getblockheader + block = self.rpc_callback('getblock', [block_hash, False]) + block_header = self.rpc_callback('getblockheader', [block_hash]) + decoded_block = CBlock() + decoded_block = FromHex(decoded_block, block) + + tx_rv = [] + for tx in decoded_block.vtx: + tx_hex = tx.serialize_with_witness().hex() + tx_dec = self.rpc_callback('decoderawtransaction', [tx_hex]) + if 'hex' not in tx_dec: + tx_dec['hex'] = tx_hex + + tx_rv.append(tx_dec) + + block_rv = { + 'hash': block_hash, + 'tx': tx_rv, + 'confirmations': block_header['confirmations'], + 'height': block_header['height'], + 'version': block_header['version'], + 'merkleroot': block_header['merkleroot'], + } + + return block_rv + + def getScriptScriptSig(self, script: bytes) -> bytearray: + return self.getP2SHP2WSHScriptSig(script) + + def getScriptDest(self, script): + return self.getP2SHP2WSHDest(script) + + def getDestForScriptHash(self, script_hash): + assert len(script_hash) == 20 + return CScript([OP_HASH160, script_hash, OP_EQUAL]) + + def pubkey_to_segwit_address(self, pk: bytes) -> str: + pkh = hash160(pk) + script_out = self.getScriptForPubkeyHash(pkh) + return self.encodeSegwitAddressScript(script_out) + + def createBLockTx(self, Kbs: bytes, output_amount: int, vkbv=None) -> bytes: + tx = CTransaction() + tx.nVersion = self.txVersion() + script_pk = self.getPkDest(Kbs) + tx.vout.append(self.txoType()(output_amount, script_pk)) + return tx.serialize() + + def spendBLockTx(self, chain_b_lock_txid: bytes, address_to: str, kbv: bytes, kbs: bytes, cb_swap_value: int, b_fee: int, restore_height: int) -> bytes: + self._log.info('spendBLockTx %s:\n', chain_b_lock_txid.hex()) + wtx = self.rpc_callback('gettransaction', [chain_b_lock_txid.hex(), ]) + lock_tx = self.loadTx(bytes.fromhex(wtx['hex'])) + + Kbs = self.getPubkey(kbs) + script_pk = self.getPkDest(Kbs) + locked_n = findOutput(lock_tx, script_pk) + ensure(locked_n is not None, 'Output not found in tx') + pkh_to = self.decodeAddress(address_to) + + tx = CTransaction() + tx.nVersion = self.txVersion() + + chain_b_lock_txid_int = uint256_from_str(chain_b_lock_txid[::-1]) + + script_sig = self.getInputScriptForPubkeyHash(self.getPubkeyHash(Kbs)) + + tx.vin.append(CTxIn(COutPoint(chain_b_lock_txid_int, locked_n), + nSequence=0, + scriptSig=script_sig)) + tx.vout.append(self.txoType()(cb_swap_value, self.getScriptForPubkeyHash(pkh_to))) + + pay_fee = self.getBLockSpendTxFee(tx, b_fee) + tx.vout[0].nValue = cb_swap_value - pay_fee + + b_lock_spend_tx = tx.serialize() + b_lock_spend_tx = self.signTxWithKey(b_lock_spend_tx, kbs, cb_swap_value) + + return bytes.fromhex(self.publishTx(b_lock_spend_tx)) + + def signTxWithKey(self, tx: bytes, key: bytes, prev_amount: int) -> bytes: + Key = self.getPubkey(key) + pkh = self.getPubkeyHash(Key) + script = self.getScriptForP2PKH(pkh) + + sig = self.signTx(key, tx, 0, script, prev_amount) + + stack = [ + sig, + Key, + ] + return self.setTxSignature(tx, stack) + + def findTxnByHash(self, txid_hex: str): + # Only works for wallet txns + try: + rv = self.rpc_callback('gettransaction', [txid_hex]) + except Exception as ex: + self._log.debug('findTxnByHash getrawtransaction failed: {}'.format(txid_hex)) + return None + if 'confirmations' in rv and rv['confirmations'] >= self.blocks_confirmed: + block_height = self.getBlockHeader(rv['blockhash'])['height'] + return {'txid': txid_hex, 'amount': 0, 'height': block_height} + return None + + def createSCLockTx(self, value: int, script: bytearray, vkbv: bytes = None) -> bytes: + tx = CTransaction() + tx.nVersion = self.txVersion() + tx.vout.append(self.txoType()(value, self.getScriptDest(script))) + + return tx.serialize() + + def fundTx(self, tx, feerate, lock_unspents: bool = True): + feerate_str = self.format_amount(feerate) + # TODO: unlock unspents if bid cancelled + options = { + 'lockUnspents': lock_unspents, + 'feeRate': feerate_str, + } + rv = self.rpc_callback('fundrawtransaction', [tx.hex(), options]) + + # Sign transaction then strip witness data to fill scriptsig + rv = self.rpc_callback('signrawtransaction', [rv['hex']]) + + tx_signed = self.loadTx(bytes.fromhex(rv['hex'])) + if len(tx_signed.vin) != len(tx_signed.wit.vtxinwit): + raise ValueError('txn has non segwit input') + for witness_data in tx_signed.wit.vtxinwit: + if len(witness_data.scriptWitness.stack) < 2: + raise ValueError('txn has non segwit input') + + return tx_signed.serialize_without_witness() + + def fundSCLockTx(self, tx_bytes: bytes, feerate, vkbv=None): + tx_funded = self.fundTx(tx_bytes, feerate) + return tx_funded + + def createSCLockRefundTx(self, tx_lock_bytes, script_lock, Kal, Kaf, lock1_value, csv_val, tx_fee_rate, vkbv=None): + tx_lock = CTransaction() + tx_lock = self.loadTx(tx_lock_bytes) + + output_script = self.getScriptDest(script_lock) + locked_n = findOutput(tx_lock, output_script) + ensure(locked_n is not None, 'Output not found in tx') + locked_coin = tx_lock.vout[locked_n].nValue + + tx_lock.rehash() + tx_lock_id_int = tx_lock.sha256 + + refund_script = self.genScriptLockRefundTxScript(Kal, Kaf, csv_val) + tx = CTransaction() + tx.nVersion = self.txVersion() + tx.vin.append(CTxIn(COutPoint(tx_lock_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) + vsize = self.getTxVSize(tx, add_witness_bytes=witness_bytes) + pay_fee = round(tx_fee_rate * vsize / 1000) + tx.vout[0].nValue = locked_coin - pay_fee + + tx.rehash() + 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 createSCLockRefundSpendTx(self, tx_lock_refund_bytes, script_lock_refund, pkh_refund_to, tx_fee_rate, vkbv=None): + # Returns the coinA locked coin to the leader + # The follower will sign the multisig path with a signature encumbered by the leader's coinB spend pubkey + # If the leader publishes the decrypted signature the leader's coinB spend privatekey will be revealed to the follower + + tx_lock_refund = self.loadTx(tx_lock_refund_bytes) + + output_script = self.getScriptDest(script_lock_refund) + locked_n = findOutput(tx_lock_refund, output_script) + ensure(locked_n is not None, 'Output not found in tx') + locked_coin = tx_lock_refund.vout[locked_n].nValue + + tx_lock_refund.rehash() + tx_lock_refund_hash_int = tx_lock_refund.sha256 + + tx = CTransaction() + tx.nVersion = self.txVersion() + 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))) + + dummy_witness_stack = self.getScriptLockRefundSpendTxDummyWitness(script_lock_refund) + witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack) + vsize = self.getTxVSize(tx, add_witness_bytes=witness_bytes) + pay_fee = round(tx_fee_rate * vsize / 1000) + tx.vout[0].nValue = locked_coin - pay_fee + + tx.rehash() + 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 createSCLockRefundSpendToFTx(self, tx_lock_refund_bytes, script_lock_refund, pkh_dest, tx_fee_rate, vkbv=None): + # lock refund swipe tx + # Sends the coinA locked coin to the follower + + tx_lock_refund = self.loadTx(tx_lock_refund_bytes) + + output_script = self.getScriptDest(script_lock_refund) + locked_n = findOutput(tx_lock_refund, output_script) + ensure(locked_n is not None, 'Output not found in tx') + locked_coin = tx_lock_refund.vout[locked_n].nValue + + A, B, lock2_value, C = self.extractScriptLockRefundScriptValues(script_lock_refund) + + tx_lock_refund.rehash() + tx_lock_refund_hash_int = tx_lock_refund.sha256 + + tx = CTransaction() + tx.nVersion = self.txVersion() + 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))) + + dummy_witness_stack = self.getScriptLockRefundSwipeTxDummyWitness(script_lock_refund) + witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack) + vsize = self.getTxVSize(tx, add_witness_bytes=witness_bytes) + pay_fee = round(tx_fee_rate * vsize / 1000) + tx.vout[0].nValue = locked_coin - pay_fee + + tx.rehash() + 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 createSCLockSpendTx(self, tx_lock_bytes, script_lock, pkh_dest, tx_fee_rate, vkbv=None, fee_info={}): + tx_lock = self.loadTx(tx_lock_bytes) + output_script = self.getScriptDest(script_lock) + locked_n = findOutput(tx_lock, output_script) + ensure(locked_n is not None, 'Output not found in tx') + locked_coin = tx_lock.vout[locked_n].nValue + + tx_lock.rehash() + tx_lock_id_int = tx_lock.sha256 + + tx = CTransaction() + tx.nVersion = self.txVersion() + 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))) + + dummy_witness_stack = self.getScriptLockTxDummyWitness(script_lock) + witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack) + vsize = self.getTxVSize(tx, add_witness_bytes=witness_bytes) + pay_fee = round(tx_fee_rate * vsize / 1000) + tx.vout[0].nValue = locked_coin - pay_fee + + fee_info['fee_paid'] = pay_fee + fee_info['rate_used'] = tx_fee_rate + fee_info['witness_bytes'] = witness_bytes + fee_info['vsize'] = vsize + + tx.rehash() + 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() diff --git a/basicswap/interface/part.py b/basicswap/interface/part.py index 82f16f2..d7f48b0 100644 --- a/basicswap/interface/part.py +++ b/basicswap/interface/part.py @@ -124,7 +124,7 @@ class PARTInterface(BTCInterface): def getWalletRestoreHeight(self) -> int: start_time = self.rpc_callback('getwalletinfo')['keypoololdest'] - blockchaininfo = self.rpc_callback('getblockchaininfo') + blockchaininfo = self.getBlockchainInfo() best_block = blockchaininfo['bestblockhash'] chain_synced = round(blockchaininfo['verificationprogress'], 3) @@ -136,6 +136,11 @@ class PARTInterface(BTCInterface): block_header = self.rpc_callback('getblockheader', [block_hash]) return block_header['height'] + def getHTLCSpendTxVSize(self, redeem: bool = True) -> int: + tx_vsize = 5 # Add a few bytes, sequence in script takes variable amount of bytes + tx_vsize += 204 if redeem else 187 + return tx_vsize + class PARTInterfaceBlind(PARTInterface): @staticmethod diff --git a/basicswap/protocols/__init__.py b/basicswap/protocols/__init__.py index 2ac2312..04fd842 100644 --- a/basicswap/protocols/__init__.py +++ b/basicswap/protocols/__init__.py @@ -1,15 +1,12 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2022 tecnovert +# Copyright (c) 2022-2023 tecnovert # Distributed under the MIT software license, see the accompanying # file LICENSE or http://www.opensource.org/licenses/mit-license.php. from basicswap.script import ( OpCodes, ) -from basicswap.util.script import ( - getP2WSH, -) from basicswap.interface.btc import ( find_vout_for_address_from_txobj, ) @@ -27,11 +24,11 @@ class ProtocolInterface: def getMockScriptScriptPubkey(self, ci) -> bytearray: script = self.getMockScript() - return ci.get_p2wsh_script_pubkey(script) if ci._use_segwit else ci.get_p2sh_script_pubkey(script) + return ci.getScriptDest(script) if ci._use_segwit else ci.get_p2sh_script_pubkey(script) def getMockAddrTo(self, ci): script = self.getMockScript() - return ci.encode_p2wsh(getP2WSH(script)) if ci._use_segwit else ci.encode_p2sh(script) + return ci.encodeScriptDest(ci.getScriptDest(script)) if ci._use_segwit else ci.encode_p2sh(script) def findMockVout(self, ci, itx_decoded): mock_addr = self.getMockAddrTo(ci) diff --git a/basicswap/protocols/atomic_swap_1.py b/basicswap/protocols/atomic_swap_1.py index 9b12886..d0a8765 100644 --- a/basicswap/protocols/atomic_swap_1.py +++ b/basicswap/protocols/atomic_swap_1.py @@ -81,7 +81,7 @@ class AtomicSwapInterface(ProtocolInterface): def promoteMockTx(self, ci, mock_tx: bytes, script: bytearray) -> bytearray: mock_txo_script = self.getMockScriptScriptPubkey(ci) - real_txo_script = ci.get_p2wsh_script_pubkey(script) if ci._use_segwit else ci.get_p2sh_script_pubkey(script) + real_txo_script = ci.getScriptDest(script) if ci._use_segwit else ci.get_p2sh_script_pubkey(script) found: int = 0 ctx = ci.loadTx(mock_tx) diff --git a/basicswap/rpc.py b/basicswap/rpc.py index d95a065..f545d20 100644 --- a/basicswap/rpc.py +++ b/basicswap/rpc.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2020-2022 tecnovert +# Copyright (c) 2020-2023 tecnovert # Distributed under the MIT software license, see the accompanying # file LICENSE or http://www.opensource.org/licenses/mit-license.php. @@ -165,3 +165,9 @@ def make_rpc_func(port, auth, wallet=None, host='127.0.0.1'): nonlocal port, auth, wallet, host return callrpc(port, auth, method, params, wallet if wallet_override is None else wallet_override, host) return rpc_func + + +def escape_rpcauth(auth_str: str) -> str: + username, password = auth_str.split(':', 1) + password = urllib.parse.quote(password, safe='') + return f'{username}:{password}' diff --git a/basicswap/util/address.py b/basicswap/util/address.py index e3b93a0..9dcfd7e 100644 --- a/basicswap/util/address.py +++ b/basicswap/util/address.py @@ -87,7 +87,7 @@ def toWIF(prefix_byte: int, b: bytes, compressed: bool = True) -> str: return b58encode(b) -def getKeyID(key_data: bytes) -> str: +def getKeyID(key_data: bytes) -> bytes: sha256_hash = hashlib.sha256(key_data).digest() return ripemd160(sha256_hash) diff --git a/bin/basicswap_prepare.py b/bin/basicswap_prepare.py index 76fe0ed..b3480e5 100755 --- a/bin/basicswap_prepare.py +++ b/bin/basicswap_prepare.py @@ -61,8 +61,14 @@ DASH_VERSION_TAG = os.getenv('DASH_VERSION_TAG', '') FIRO_VERSION = os.getenv('FIRO_VERSION', '0.14.99.1') FIRO_VERSION_TAG = os.getenv('FIRO_VERSION_TAG', '') +NAV_VERSION = os.getenv('NAV_VERSION', '7.0.3') +NAV_VERSION_TAG = os.getenv('NAV_VERSION', '') + GUIX_SSL_CERT_DIR = None +ADD_PUBKEY_URL = os.getenv('ADD_PUBKEY_URL', '') +OVERRIDE_DISABLED_COINS = toBool(os.getenv('OVERRIDE_DISABLED_COINS', 'false')) + known_coins = { 'particl': (PARTICL_VERSION, PARTICL_VERSION_TAG, ('tecnovert',)), @@ -74,8 +80,13 @@ known_coins = { 'dash': (DASH_VERSION, DASH_VERSION_TAG, ('pasta',)), # 'firo': (FIRO_VERSION, FIRO_VERSION_TAG, ('reuben',)), 'firo': (FIRO_VERSION, FIRO_VERSION_TAG, ('tecnovert',)), + 'navcoin': (NAV_VERSION, NAV_VERSION_TAG, ('nav_builder',)), } +disabled_coins = [ + 'navcoin', +] + expected_key_ids = { 'tecnovert': ('13F13651C9CF0D6B',), 'thrasher': ('FE3348877809386C',), @@ -86,6 +97,7 @@ expected_key_ids = { 'fuzzbawls': ('3BDCDA2D87A881D9',), 'pasta': ('52527BEDABE87984',), 'reuben': ('1290A1D0FA7EE109',), + 'nav_builder': ('2782262BF6E7FADB',), } USE_PLATFORM = os.getenv('USE_PLATFORM', platform.system()) @@ -159,6 +171,12 @@ 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', '') +NAV_RPC_HOST = os.getenv('NAV_RPC_HOST', '127.0.0.1') +NAV_RPC_PORT = int(os.getenv('NAV_RPC_PORT', 44444)) +NAV_ONION_PORT = int(os.getenv('NAV_ONION_PORT', 8334)) # TODO? +NAV_RPC_USER = os.getenv('NAV_RPC_USER', '') +NAV_RPC_PWD = os.getenv('NAV_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)) @@ -198,9 +216,13 @@ def make_reporthook(read_start=0): dl_complete: bool = totalsize > 0 and read >= use_size time_now = time.time() time_delta = time_now - time_last - if time_delta < 4 and not dl_complete: + if time_delta < 4.0 and not dl_complete: return + # Avoid division by zero by picking a value + if time_delta <= 0.0: + time_delta = 0.01 + bytes_delta = read - read_last time_last = time_now read_last = read @@ -612,8 +634,14 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}): release_filename = 'firo-0.14.99.1-x86_64-apple-darwin18.tar.gz' else: raise ValueError('Firo: Unknown architecture') - release_url = 'https://github.com/tecnovert/particl-core/releases/download/v{}/{}'.format(version + version_tag, release_filename) + release_url = 'https://github.com/tecnovert/particl-core/releases/download/{}/{}'.format(version + version_tag, release_filename) assert_url = 'https://github.com/tecnovert/particl-core/releases/download/v%s/SHA256SUMS.asc' % (version + version_tag) + elif coin == 'navcoin': + release_filename = '{}-{}-{}.{}'.format(coin, version, BIN_ARCH, FILE_EXT) + release_url = 'https://github.com/navcoin/navcoin-core/releases/download/{}/{}'.format(version + version_tag, release_filename) + assert_filename = 'SHA256SUM_7.0.3.asc' + assert_sig_filename = 'SHA256SUM_7.0.3.asc.sig' + assert_url = 'https://github.com/navcoin/navcoin-core/releases/download/{}/{}'.format(version + version_tag, assert_filename) else: raise ValueError('Unknown coin') @@ -629,7 +657,8 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}): 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) + if coin not in ('nav', ): + 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) @@ -665,6 +694,8 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}): if coin in ('firo', ): pubkey_filename = '{}_{}.pgp'.format('particl', signing_key_name) + elif coin in ('navcoin', ): + pubkey_filename = '{}_builder.pgp'.format(coin) else: pubkey_filename = '{}_{}.pgp'.format(coin, signing_key_name) pubkeyurls = [ @@ -678,6 +709,9 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}): if coin == 'firo': pubkeyurls.append('https://firo.org/reuben.asc') + if ADD_PUBKEY_URL != '': + pubkeyurls.append(ADD_PUBKEY_URL + '/' + pubkey_filename) + if coin in ('monero', 'firo'): with open(assert_path, 'rb') as fp: verified = gpg.verify_file(fp) @@ -687,6 +721,21 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}): importPubkeyFromUrls(gpg, pubkeyurls) with open(assert_path, 'rb') as fp: verified = gpg.verify_file(fp) + elif coin in ('navcoin'): + with open(assert_sig_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.') + importPubkeyFromUrls(gpg, pubkeyurls) + with open(assert_sig_path, 'rb') as fp: + verified = gpg.verify_file(fp) + + # .sig file is not a detached signature, recheck release hash in decrypted data + logger.warning('Double checking Navcoin release hash.') + with open(assert_sig_path, 'rb') as fp: + decrypted = gpg.decrypt_file(fp) + assert (release_hash.hex() in str(decrypted)) else: with open(assert_sig_path, 'rb') as fp: verified = gpg.verify_file(fp, assert_path) @@ -793,8 +842,12 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}): exitWithError('{} exists'.format(core_conf_path)) with open(core_conf_path, 'w') as fp: if chain != 'mainnet': - fp.write(chain + '=1\n') - if coin != 'firo': + if coin in ('navcoin',): + chainname = 'devnet' if chain == 'regtest' else chain + fp.write(chainname + '=1\n') + else: + fp.write(chain + '=1\n') + if coin not in ('firo', 'navcoin'): if chain == 'testnet': fp.write('[test]\n\n') elif chain == 'regtest': @@ -856,6 +909,11 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}): 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))) + elif coin == 'navcoin': + fp.write('prune=4000\n') + fp.write('fallbackfee=0.0002\n') + if NAV_RPC_USER != '': + fp.write('rpcauth={}:{}${}\n'.format(NAV_RPC_USER, salt, password_to_hmac(salt, NAV_RPC_PWD))) else: logger.warning('Unknown coin %s', coin) @@ -997,7 +1055,8 @@ def printVersion(): logger.info('Core versions:') for coin, version in known_coins.items(): - logger.info('\t%s: %s%s', coin, version[0], version[1]) + postfix = ' (Disabled)' if coin in disabled_coins else '' + logger.info('\t%s: %s%s%s', coin.capitalize(), version[0], version[1], postfix) def printHelp(): @@ -1033,7 +1092,11 @@ def printHelp(): print('--initwalletsonly Setup coin wallets only.') print('--keysdirpath Speed up tests by preloading all PGP keys in directory.') - print('\n' + 'Known coins: {}'.format(', '.join(known_coins.keys()))) + active_coins = [] + for coin_name in known_coins.keys(): + if coin_name not in disabled_coins: + active_coins.append(coin_name) + print('\n' + 'Known coins: {}'.format(', '.join(active_coins))) def finalise_daemon(d): @@ -1042,7 +1105,7 @@ def finalise_daemon(d): d.send_signal(signal.CTRL_C_EVENT if os.name == 'nt' else signal.SIGINT) d.wait(timeout=120) except Exception as e: - logging.info(f'Error {e}'.format(d.pid)) + logging.info(f'Error {e} for process {d.pid}') for fp in (d.stdout, d.stderr, d.stdin): if fp: fp.close() @@ -1213,6 +1276,13 @@ def check_btc_fastsync_data(base_dir, sync_file_path): ensureValidSignatureBy(verified, 'tecnovert') +def ensure_coin_valid(coin: str, test_disabled: bool = True) -> None: + if coin not in known_coins: + exitWithError(f'Unknown coin {coin.capitalize()}') + if test_disabled and not OVERRIDE_DISABLED_COINS and coin in disabled_coins: + exitWithError(f'{coin.capitalize()} is disabled') + + def main(): global use_tor_proxy data_dir = None @@ -1312,28 +1382,24 @@ def main(): continue if name == 'withcoin' or name == 'withcoins': for coin in [s.lower() for s in s[1].split(',')]: - if coin not in known_coins: - exitWithError('Unknown coin {}'.format(coin)) + ensure_coin_valid(coin) with_coins.add(coin) coins_changed = True continue if name == 'withoutcoin' or name == 'withoutcoins': for coin in [s.lower() for s in s[1].split(',')]: - if coin not in known_coins: - exitWithError('Unknown coin {}'.format(coin)) + ensure_coin_valid(coin, test_disabled=False) with_coins.discard(coin) coins_changed = True continue if name == 'addcoin': add_coin = s[1].lower() - if add_coin not in known_coins: - exitWithError('Unknown coin {}'.format(s[1])) + ensure_coin_valid(add_coin) with_coins = {add_coin, } continue if name == 'disablecoin': disable_coin = s[1].lower() - if disable_coin not in known_coins: - exitWithError('Unknown coin {}'.format(s[1])) + ensure_coin_valid(disable_coin, test_disabled=False) continue if name == 'htmlhost': htmlhost = s[1].strip('"') @@ -1376,7 +1442,14 @@ def main(): os.makedirs(data_dir) config_path = os.path.join(data_dir, cfg.CONFIG_FILENAME) + should_download_btc_fastsync = False if extra_opts.get('use_btc_fastsync', False) is True: + if 'bitcoin' in with_coins or add_coin == 'bitcoin': + should_download_btc_fastsync = True + else: + logger.warning('Ignoring usebtcfastsync option without Bitcoin selected.') + + if should_download_btc_fastsync: logger.info(f'Preparing BTC Fastsync file {BITCOIN_FASTSYNC_FILE}') sync_file_path = os.path.join(data_dir, BITCOIN_FASTSYNC_FILE) sync_file_url = os.path.join(BITCOIN_FASTSYNC_URL, BITCOIN_FASTSYNC_FILE) @@ -1487,7 +1560,7 @@ def main(): 'use_csv': False, 'blocks_confirmed': 1, 'conf_target': 2, - 'core_version_group': 20, + 'core_version_group': 17, 'chain_lookups': 'local', }, 'dash': { @@ -1519,6 +1592,22 @@ def main(): 'conf_target': 2, 'core_version_group': 18, 'chain_lookups': 'local', + }, + 'navcoin': { + 'connection_type': 'rpc' if 'navcoin' in with_coins else 'none', + 'manage_daemon': True if ('navcoin' in with_coins and NAV_RPC_HOST == '127.0.0.1') else False, + 'rpchost': NAV_RPC_HOST, + 'rpcport': NAV_RPC_PORT + port_offset, + 'onionport': NAV_ONION_PORT + port_offset, + 'datadir': os.getenv('NAV_DATA_DIR', os.path.join(data_dir, 'navcoin')), + 'bindir': os.path.join(bin_dir, 'navcoin'), + 'use_segwit': True, + 'use_csv': True, + 'blocks_confirmed': 1, + 'conf_target': 2, + 'core_version_group': 18, + 'chain_lookups': 'local', + 'startup_tries': 40, } } @@ -1543,6 +1632,9 @@ def main(): if FIRO_RPC_USER != '': chainclients['firo']['rpcuser'] = FIRO_RPC_USER chainclients['firo']['rpcpassword'] = FIRO_RPC_PWD + if NAV_RPC_USER != '': + chainclients['nav']['rpcuser'] = NAV_RPC_USER + chainclients['nav']['rpcpassword'] = NAV_RPC_PWD chainclients['monero']['walletsdir'] = os.getenv('XMR_WALLETS_DIR', chainclients['monero']['datadir']) @@ -1598,9 +1690,13 @@ def main(): settings = load_config(config_path) if disable_coin not in settings['chainclients']: - exitWithError('{} has not been prepared'.format(disable_coin)) - settings['chainclients'][disable_coin]['connection_type'] = 'none' - settings['chainclients'][disable_coin]['manage_daemon'] = False + exitWithError(f'{disable_coin} not configured') + + coin_settings = settings['chainclients'][disable_coin] + if coin_settings['connection_type'] == 'none' and coin_settings['manage_daemon'] is False: + exitWithError(f'{disable_coin} is already disabled') + coin_settings['connection_type'] = 'none' + coin_settings['manage_daemon'] = False with open(config_path, 'w') as fp: json.dump(settings, fp, indent=4) diff --git a/bin/basicswap_run.py b/bin/basicswap_run.py index 625916e..bc56074 100755 --- a/bin/basicswap_run.py +++ b/bin/basicswap_run.py @@ -1,14 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -# Copyright (c) 2019-2022 tecnovert +# Copyright (c) 2019-2023 tecnovert # Distributed under the MIT software license, see the accompanying # file LICENSE or http://www.opensource.org/licenses/mit-license.php. import os import sys import json -import time import shutil import signal import logging @@ -210,8 +209,7 @@ def runClient(fp, data_dir, chain): swap_client.ws_server.run_forever(threaded=True) logger.info('Exit with Ctrl + c.') - while swap_client.is_running: - time.sleep(0.5) + while not swap_client.delay_event.wait(0.5): swap_client.update() except Exception as ex: diff --git a/doc/release-notes.md b/doc/release-notes.md index 7fef925..ed00ac3 100644 --- a/doc/release-notes.md +++ b/doc/release-notes.md @@ -1,6 +1,23 @@ +0.0.68 +============== + +- Temporarily disabled Navcoin. + - Untested on mainnet. + + +0.0.67 +============== + +- Added support for p2sh-p2wsh coins +- Added Navcoin +- Fixed Particl fee estimation in secret hash swaps. +- Raised adaptor signature swap protocol version to 2 + - Not backwards compatible with previous versions. + 0.0.66 ============== + - Fixed bugs in getLinkedMessageId and validateSwapType. diff --git a/pgp/keys/navcoin_builder.pgp b/pgp/keys/navcoin_builder.pgp new file mode 100644 index 0000000..361d13c --- /dev/null +++ b/pgp/keys/navcoin_builder.pgp @@ -0,0 +1,40 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +xsDNBF/8KgoBDADpq1pIJh2u7J6eX1u8awKFA/k0M826KejUFIMY/B25MlYaBaQm +DMy2aCX9NuLCXtnA+Ys5UGlHb70KrsGLmlvA6Jk+nQZhjCM9RXTJtbMOrq84uTYc +zRgWBfAkMU+HIj2svkdjrmDdCjdbip6myBbketgg9UP09GA2TxdLweghDwNjjz/a +mVr2eaIoYWq13OFY7qoe8eKXQO0yGUv/T1abtJPiRFWpTwU4M7a8BqG2aFlJtzT2 +WGDWn1pFkGEQMJqf+TKugRtDwoCv/TPJzATlCD9gzPUf3Xpbhv+f6Nj08WewBwhv +tqdpVFPTkp4xQP5QgN+JsplfQcEgWNczIYGNrna0RG3KjL4a+mHUGFi14NwFioIa +l4aqfBwbEOU5M+wLWfTsTAch7Pi42UeYEVpbEFZKQxxP3NeTere6sdwPJZg193nF +cb/rjnvuPUlGJlZ+TJbGptlkjkH/mNRDB04iTR8XbSHdYgo+rKo0EDQoxkRTeTWw +d1vGFOH9keg8Xq0AEQEAAc0kTmF2Q29pbiBDb3JlIDxidWlsZGVyQG5hdi5jb21t +dW5pdHk+wsEUBBMBCgA+FiEEG/m1G67VG6CzoXTuJ4ImK/bn+tsFAl/8KgoCGwMF +CQPCZwAFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQJ4ImK/bn+ttpxQv6AyWV +2mKyaWWxjsZTVBt0wJeajT+CaNZ0jU/zvEUOnpsr0/r3mbVsyyxWNRUdIcfceYrB +729x8B15GUE1RwpbRJwoKT5WjGeFd/sHkA+j6PRVg7x3pyJPzLYOB61rJsPTAOOM +06og0Kj+qbzIlcwDQdCgxObGuoHGXQvTFsxrOYiGRca4x3qHbIYlNEemgO41RHu+ +zZCmHwfPocIljr6/FMHOuZQ7zbdRqXNmZtIBOYhjhEK7qypKEQwYXoUNJBkzdfHl +H8Mz/rSQ68jBcJ6fjXIs2XfUZgUaJAH/Xar9/52iApLr2AyBzHaXIGy+CUUWa+kR +Tpo6OqEkk92O2mNGwTx4I1T6ZED4ABkGWdKZS0rU7MlcNSe000pV6IQPrPoDw9CU +2PkJCyqZe1QqTdLtlHLoSlnLhLAd3vWPmw+T0MfMMSyuVMvqgC5vuCRJKDFs//uE +LuP58CiC+DmNyLFMCgjqv3GKsaDag21gNp8IRDnmgrJn/UrRRwEOMcDUYLaKzsDN +BF/8KgoBDADjAlvNASpQbzEQVOIUaJxXRP+09IvVavotS40LqOjvPuDWyOnA9+AP +VOrmFo/+HndicMjQzasXnkc249uiYiLyshlyrme6ebbMp+4aDcSxQRfx2+oXLR1l +0oBrchMiAJ+6xe0N6Itc6EhjAsPwG2IGfsFSmg9YYsm5NjHnujyvcQwcGG0OUBsy +ydJWtL5AU1z3H5fegN6DKFaUW/IMcxNN6el8NRZkHLwNh+JLyulHWKYDs/1YJ5sY +sknCvnicPAOpLqgdGopdu9ORnvOZ5wsJq8IFP6SBjdSjFj71eXam5dyWfMvMew1v +IVkBHi4y/G1J0Rlzi2f8j38htGa2H9A4eG4WUDO/uDJ/g76sWTAVPnWNNG3BQLgN +m2LDVNszGJNBTwFYlzEHrPipEA0foePHOLXc24o0/LZZYunP+zNvJiqqLEYCpvK/ +nU7HozvHJvV8r+b/FK/vDLQbmp4HvvSL5ho/Dwn+yWU+Z+DPM+qIgukn4JrUUuZK +c553H1U2LtsAEQEAAcLA/AQYAQoAJhYhBBv5tRuu1Rugs6F07ieCJiv25/rbBQJf +/CoKAhsMBQkDwmcAAAoJECeCJiv25/rbN2gMAL28b3ou3c9aV99F8fEniZLsR2t7 +EcZ93kBd9ozgeVnabSDsaRvlQ1uJDabemhcLyRY5fCCBAAXGCZ6jtxicOgt0cb+S +MHcrM7EUHLfxguM296V633svaFSUCwk3kBLMv9ukIrWu3oflE9MUyM/J92A0/TP/ +PgBzD31xbiEEcSEqKt/CBP/pQbTSEgIa+JjGKYVPrN+n8kitY/Vu3yUNpATSH3j/ +0cl/f5IXhg4uwqCzmopkU8lH/WGP90kIvG6ZwCstNJ4GN9sKRYKN++19PdDUmi++ +z9FC0cu6GgSuWIWd2CyhGhOqMkFlwOnN5U5svH+wFlBK5+z6MgEkPCR6L2XPMonE +bd6JCKuw7SUHN4pUCZ2lnEStjiGM/cziuZ01U1TesH4W6CdOSl9+/sI4wwO//Qj/ +/QYSjNwXNexgFA7pX90boRjQl1LNMocHVaYriGzw7n9fKJPwYI5oUam5By3sYmTT +i0WBmStFWb3bvqfkdjbe86g7ilbbLIoAlUS03w== +=Y9TC +-----END PGP PUBLIC KEY BLOCK----- diff --git a/tests/basicswap/extended/test_firo.py b/tests/basicswap/extended/test_firo.py index 14f8988..0683a3f 100644 --- a/tests/basicswap/extended/test_firo.py +++ b/tests/basicswap/extended/test_firo.py @@ -122,7 +122,7 @@ class Test(BaseTest): test_atomic = True test_xmr = False - # Particl node mnemonics are set in test/basicswap/mnemonics.py + # Particl node mnemonics are test_xmr.py, node 2 is set randomly firo_seeds = [ 'd90b7ed1be614e1c172653aee1f3b6230f43b7fa99cf07fa984a17966ad81de7', '6c81d6d74ba33a0db9e41518c2b6789fbe938e98018a4597dac661cfc5f2dfc1', diff --git a/tests/basicswap/extended/test_nav.py b/tests/basicswap/extended/test_nav.py new file mode 100644 index 0000000..a6e0627 --- /dev/null +++ b/tests/basicswap/extended/test_nav.py @@ -0,0 +1,1030 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# Copyright (c) 2023 tecnovert +# Distributed under the MIT software license, see the accompanying +# file LICENSE or http://www.opensource.org/licenses/mit-license.php. + +import os +import random +import logging +import unittest + +import basicswap.config as cfg +from basicswap.basicswap import ( + Coins, + TxStates, + SwapTypes, + BidStates, + DebugTypes, +) +from basicswap.basicswap_util import ( + TxLockTypes, +) +from basicswap.util import ( + COIN, + make_int, + format_amount, +) +from basicswap.util.address import ( + decodeWif, +) +from basicswap.rpc import ( + waitForRPC, +) +from tests.basicswap.util import ( + read_json_api, +) +from tests.basicswap.common import ( + stopDaemons, + wait_for_bid, + make_rpc_func, + TEST_HTTP_PORT, + wait_for_offer, + wait_for_balance, + wait_for_unspent, + wait_for_in_progress, + wait_for_bid_tx_state, +) +from basicswap.interface.contrib.nav_test_framework.mininode import ( + ToHex, + FromHex, + CTxIn, + COutPoint, + CTransaction, + CTxInWitness, +) +from basicswap.interface.contrib.nav_test_framework.script import ( + CScript, + OP_EQUAL, + OP_CHECKSEQUENCEVERIFY +) + +from bin.basicswap_run import startDaemon +from basicswap.contrib.rpcauth import generate_salt, password_to_hmac +from tests.basicswap.test_xmr import test_delay_event, callnoderpc +from mnemonic import Mnemonic + +from tests.basicswap.test_btc_xmr import TestFunctions + +logger = logging.getLogger() + +NAV_BINDIR = os.path.expanduser(os.getenv('NAV_BINDIR', os.path.join(cfg.DEFAULT_TEST_BINDIR, 'navcoin'))) +NAVD = os.getenv('NAVD', 'navcoind' + cfg.bin_suffix) +NAV_CLI = os.getenv('NAV_CLI', 'navcoin-cli' + cfg.bin_suffix) +NAV_TX = os.getenv('NAV_TX', 'navcoin-tx' + cfg.bin_suffix) + +NAV_BASE_PORT = 44832 +NAV_BASE_RPC_PORT = 45832 +NAV_BASE_ZMQ_PORT = 46832 + + +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('devnet=1\n') # regtest=1 ? + 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('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') + + # test/rpc-tests/segwit.py + fp.write('prematurewitness=1\n') + fp.write('walletprematurewitness=1\n') + fp.write('blockversion=4\n') + fp.write('promiscuousmempoolflags=517\n') + + fp.write('listenonion=0\n') + fp.write('dandelion=0\n') + fp.write('ntpminmeasures=-1\n') + fp.write('torserver=0\n') + fp.write('suppressblsctwarning=1\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(TestFunctions): + __test__ = True + test_coin_from = Coins.NAV + nav_daemons = [] + nav_addr = None + start_ltc_nodes = False + start_xmr_nodes = True + + test_atomic = True + test_xmr = True + + extra_wait_time = 100 + + # Particl node mnemonics are test_xmr.py, node 2 is set randomly + # Get the expected seeds from BasicSwap::initialiseWallet + nav_seeds = [ + '516b471da2a67bcfd42a1da7f7ae8f9a1b02c34f6a2d6a943ceec5dca68e7fa1', + 'a8c0911fba070d5cc2784703afeb0f7c3b9b524b8a53466c04e01933d9fede78', + '7b3b533ac3a27114ae17c8cca0d2cd9f736e7519ae52b8ec8f1f452e8223d082', + ] + + @classmethod + def prepareExtraDataDir(cls, i): + extra_opts = [] + if not cls.restore_instance: + seed_hex = cls.nav_seeds[i] + mnemonic = Mnemonic('english').to_mnemonic(bytes.fromhex(seed_hex)) + print('mnemonic', mnemonic) + extra_opts.append(f'-importmnemonic={mnemonic}') + data_dir = prepareDataDir(cfg.TEST_DATADIRS, i, 'navcoin.conf', 'nav_', base_p2p_port=NAV_BASE_PORT, base_rpc_port=NAV_BASE_RPC_PORT) + + cls.nav_daemons.append(startDaemon(os.path.join(cfg.TEST_DATADIRS, 'nav_' + str(i)), NAV_BINDIR, NAVD, opts=extra_opts)) + logging.info('Started %s %d', NAVD, cls.nav_daemons[-1].pid) + + waitForRPC(make_rpc_func(i, base_rpc_port=NAV_BASE_RPC_PORT), max_tries=12) + + @classmethod + def addPIDInfo(cls, sc, i): + sc.setDaemonPID(Coins.NAV, cls.nav_daemons[i].pid) + + @classmethod + def sync_blocks(cls, wait_for: int = 20, num_nodes: int = 3) -> None: + logging.info('Syncing blocks') + for i in range(wait_for): + if test_delay_event.is_set(): + raise ValueError('Test stopped.') + block_hash0 = callnoderpc(0, 'getbestblockhash', base_rpc_port=NAV_BASE_RPC_PORT) + matches: int = 0 + for i in range(1, num_nodes): + block_hash = callnoderpc(i, 'getbestblockhash', base_rpc_port=NAV_BASE_RPC_PORT) + if block_hash == block_hash0: + matches += 1 + if matches == num_nodes - 1: + return + test_delay_event.wait(1) + raise ValueError('sync_blocks timed out.') + + @classmethod + def prepareExtraCoins(cls): + if cls.restore_instance: + void_block_rewards_pubkey = cls.getRandomPubkey() + cls.nav_addr = cls.swap_clients[0].ci(Coins.NAV).pubkey_to_address(void_block_rewards_pubkey) + else: + num_blocks = 400 + cls.nav_addr = callnoderpc(0, 'getnewaddress', ['mining_addr'], base_rpc_port=NAV_BASE_RPC_PORT) + # cls.nav_addr = addwitnessaddress doesn't work with generatetoaddress + + logging.info('Mining %d NAV blocks to %s', num_blocks, cls.nav_addr) + callnoderpc(0, 'generatetoaddress', [num_blocks, cls.nav_addr], base_rpc_port=NAV_BASE_RPC_PORT) + + nav_addr1 = callnoderpc(1, 'getnewaddress', ['initial addr'], base_rpc_port=NAV_BASE_RPC_PORT) + nav_addr1 = callnoderpc(1, 'addwitnessaddress', [nav_addr1], base_rpc_port=NAV_BASE_RPC_PORT) + for i in range(5): + callnoderpc(0, 'sendtoaddress', [nav_addr1, 1000], base_rpc_port=NAV_BASE_RPC_PORT) + + # Set future block rewards to nowhere (a random address), so wallet amounts stay constant + void_block_rewards_pubkey = cls.getRandomPubkey() + cls.nav_addr = cls.swap_clients[0].ci(Coins.NAV).pubkey_to_address(void_block_rewards_pubkey) + num_blocks = 100 + logging.info('Mining %d NAV blocks to %s', num_blocks, cls.nav_addr) + callnoderpc(0, 'generatetoaddress', [num_blocks, cls.nav_addr], base_rpc_port=NAV_BASE_RPC_PORT) + + cls.sync_blocks() + + @classmethod + def tearDownClass(cls): + logging.info('Finalising NAV Test') + super(Test, cls).tearDownClass() + + stopDaemons(cls.nav_daemons) + + @classmethod + def addCoinSettings(cls, settings, datadir, node_id): + settings['chainclients']['navcoin'] = { + 'connection_type': 'rpc', + 'manage_daemon': False, + 'rpcport': NAV_BASE_RPC_PORT + node_id, + 'rpcuser': 'test' + str(node_id), + 'rpcpassword': 'test_pass' + str(node_id), + 'datadir': os.path.join(datadir, 'nav_' + str(node_id)), + 'bindir': NAV_BINDIR, + 'use_csv': True, + 'use_segwit': True, + 'blocks_confirmed': 1, + } + + @classmethod + def coins_loop(cls): + super(Test, cls).coins_loop() + chain_height: int = callnoderpc(0, 'getblockcount', [], base_rpc_port=NAV_BASE_RPC_PORT) + staking_info = callnoderpc(0, 'getstakinginfo', [], base_rpc_port=NAV_BASE_RPC_PORT) + print('Staking loop: NAV node 0 chain_height {}, staking {}, currentblocktx {}'.format(chain_height, staking_info['staking'], staking_info['currentblocktx'])) + + 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=NAV_BASE_RPC_PORT) + + def mineBlock(self, num_blocks: int = 1): + self.callnoderpc('generatetoaddress', [num_blocks, self.nav_addr]) + + def stake_block(self, num_blocks: int = 1, node_id: int = 0, wait_for: int = 360): + print(f'Trying to stake {num_blocks} blocks') + blockcount = self.callnoderpc('getblockcount', node_id=node_id) + + try: + # Turn staking on + self.callnoderpc('staking', [True,], node_id=node_id) + + # Wait for a new block to be mined + for i in range(wait_for): + if test_delay_event.is_set(): + raise ValueError('Test stopped.') + if self.callnoderpc('getblockcount', node_id=node_id) >= blockcount + num_blocks: + return + test_delay_event.wait(1) + raise ValueError('stake_block timed out.') + finally: + # Turn staking off + self.callnoderpc('staking', [False,], node_id=node_id) + + def test_001_segwit(self): + logging.info('---------- Test {} segwit'.format(self.test_coin_from.name)) + + swap_clients = self.swap_clients + + ci = swap_clients[0].ci(self.test_coin_from) + assert (ci.using_segwit() is True) + + addr_plain = self.callnoderpc('getnewaddress', ['segwit test', ]) + addr_witness = self.callnoderpc('addwitnessaddress', [addr_plain, ]) + addr_witness_info = self.callnoderpc('validateaddress', [addr_witness, ]) + txid = self.callnoderpc('sendtoaddress', [addr_witness, 1.0]) + assert len(txid) == 64 + self.mineBlock() + + tx_wallet = self.callnoderpc('gettransaction', [txid, ]) + tx_hex = tx_wallet['hex'] + tx = self.callnoderpc('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 + + inputs = [{'txid': txid, 'vout': prevout_n}, ] + outputs = {addr_plain: 0.99} + tx_funded = self.callnoderpc('createrawtransaction', [inputs, outputs]) + tx_signed = self.callnoderpc('signrawtransaction', [tx_funded, ])['hex'] + + # Add scriptsig for txids to match + decoded_tx = CTransaction() + decoded_tx = FromHex(decoded_tx, tx_funded) + + tx_funded_with_scriptsig = ToHex(decoded_tx) + decoded_tx.vin[0].scriptSig = bytes.fromhex('16' + addr_witness_info['hex']) + decoded_tx.rehash() + txid_with_scriptsig = decoded_tx.hash + + 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'] + assert txid_with_scriptsig == tx_signed_decoded['txid'] + ci = swap_clients[0].ci(self.test_coin_from) + assert tx_signed_decoded['version'] == ci.txVersion() + + # Ensure txn can get into the chain + txid = self.callnoderpc('sendrawtransaction', [tx_signed, ]) + # Block must be staked, witness merkle root mismatch if mined + self.stake_block(1) + + tx_wallet = self.callnoderpc('gettransaction', [txid, ]) + assert (len(tx_wallet['blockhash']) == 64) + + def test_002_scantxoutset(self): + logging.info('---------- Test {} scantxoutset'.format(self.test_coin_from.name)) + logging.warning('Skipping test') + return # TODO + addr_plain = self.callnoderpc('getnewaddress', ['scantxoutset test', ]) + addr_witness = self.callnoderpc('addwitnessaddress', [addr_plain, ]) + addr_witness_info = self.callnoderpc('validateaddress', [addr_witness, ]) + txid = self.callnoderpc('sendtoaddress', [addr_witness, 1.0]) + assert len(txid) == 64 + + self.mineBlock() + + ro = self.callnoderpc('scantxoutset', ['start', ['addr({})'.format(addr_witness)]]) + assert (len(ro['unspents']) == 1) + assert (ro['unspents'][0]['txid'] == txid) + + def test_003_signature_hash(self): + logging.info('---------- Test {} signature_hash'.format(self.test_coin_from.name)) + # Test that signing a transaction manually produces the same result when signed with the wallet + + swap_clients = self.swap_clients + + addr_plain = self.callnoderpc('getnewaddress', ['address test',]) + addr_witness = self.callnoderpc('addwitnessaddress', [addr_plain,]) + validate_plain = self.callnoderpc('validateaddress', [addr_plain]) + validate_witness = self.callnoderpc('validateaddress', [addr_witness]) + assert (validate_plain['ismine'] is True) + assert (validate_witness['script'] == 'witness_v0_keyhash') + assert (validate_witness['ismine'] is True) + + ci = swap_clients[0].ci(self.test_coin_from) + pkh = ci.decodeAddress(addr_plain) + script_out = ci.getScriptForPubkeyHash(pkh) + + addr_out = ci.encodeSegwitAddressScript(script_out) + assert (addr_out == addr_witness) + + # Test address from pkh + test_addr = ci.encodeSegwitAddress(pkh) + assert (addr_out == test_addr) + + txid = self.callnoderpc('sendtoaddress', [addr_out, 1.0]) + assert len(txid) == 64 + + self.mineBlock() + + tx_wallet = self.callnoderpc('gettransaction', [txid, ]) + tx_hex = tx_wallet['hex'] + tx = self.callnoderpc('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 + + inputs = [{'txid': txid, 'vout': prevout_n}, ] + outputs = {addr_witness: 0.99} + tx_wallet_funded = self.callnoderpc('createrawtransaction', [inputs, outputs]) + tx_wallet_signed = self.callnoderpc('signrawtransaction', [tx_wallet_funded, ])['hex'] + tx_wallet_decoded = self.callnoderpc('decoderawtransaction', [tx_wallet_signed, ]) + script_in = ci.getInputScriptForPubkeyHash(pkh) + + # TODO: Are there more restrictions on tx.nTime? + # - tx.nTime can't be greater than the blocktime + tx_spend = CTransaction() + tx_spend.nVersion = ci.txVersion() + tx_spend.nTime = tx_wallet_decoded['time'] + tx_spend.vin.append(CTxIn(COutPoint(int(txid, 16), prevout_n), + scriptSig=script_in, + nSequence=tx_wallet_decoded['vin'][0]['sequence'])) + tx_spend.vout.append(ci.txoType()(ci.make_int(0.99), script_out)) + tx_spend_bytes = tx_spend.serialize_with_witness() + tx_spend_hex = tx_spend_bytes.hex() + + script = ci.getScriptForP2PKH(pkh) + key_wif = self.callnoderpc('dumpprivkey', [addr_plain, ]) + key = decodeWif(key_wif) + + sig = ci.signTx(key, tx_spend_bytes, 0, script, ci.make_int(1.0)) + + stack = [ + sig, + ci.getPubkey(key), + ] + tx_spend_signed = ci.setTxSignature(tx_spend_bytes, stack) + assert (tx_spend_signed.hex() == tx_wallet_signed) + + def test_004_csv(self): + logging.info('---------- Test {} csv'.format(self.test_coin_from.name)) + swap_clients = self.swap_clients + ci = 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('signrawtransaction', [tx_funded['hex'], ])['hex'] + self.sync_blocks() + txid = self.callnoderpc('sendrawtransaction', [tx_signed, ]) + + self.callnoderpc('getnewaddress', ['used?',]) # First generated address has a positive balance + addr_out = self.callnoderpc('getnewaddress', ['csv test',]) + addr_witness = self.callnoderpc('addwitnessaddress', [addr_out,]) + + # Test switching address from p2pkh to p2sh-p2wsh + pkh = ci.decodeAddress(addr_out) + script_out = ci.getScriptForPubkeyHash(pkh) + # Convert to p2sh-p2wsh + addr_out = ci.encodeSegwitAddressScript(script_out) + assert (addr_out == addr_witness) + + p2wsh = ci.getP2SHP2WSHDest(script) + assert (p2wsh == script_dest) + addr_out_test = ci.encodeScriptDest(p2wsh) + + tx_decoded = self.callnoderpc('decoderawtransaction', [tx_signed, ]) + assert (addr_out_test in tx_decoded['vout'][utxo_pos]['scriptPubKey']['addresses']) + + tx_spend = CTransaction() + tx_spend.nVersion = ci.txVersion() + + tx_spend.vin.append(CTxIn(COutPoint(int(txid, 16), utxo_pos), + nSequence=3, + scriptSig=ci.getScriptScriptSig(script))) + 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 = tx_spend.serialize_with_witness().hex() + + txid_spent = txid + try: + txid = self.callnoderpc('sendrawtransaction', [tx_spend_hex, ]) + except Exception as e: + assert ('non-BIP68-final' in str(e)) + else: + assert False, 'Should fail' + + self.stake_block(3) + + tx_spend_decoded = self.callnoderpc('decoderawtransaction', [tx_spend_hex, ]) + txid = self.callnoderpc('sendrawtransaction', [tx_spend_hex, ]) + self.stake_block(1) + + ro = self.callnoderpc('listtransactions') + sum_addr = 0 + for entry in ro: + if 'address' in entry and entry['address'] == addr_out: + if 'category' in entry and entry['category'] == 'receive': + sum_addr += entry['amount'] + assert (sum_addr == 1.0999) + + # listreceivedbyaddress doesn't seem to find witness utxos + ''' + 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']) + ro = self.callnoderpc('importaddress', [addr, '', False], node_id=1) + + ro = self.callnoderpc('validateaddress', [addr,], node_id=1) + assert (ro['iswatchonly'] is True) + + txid = self.callnoderpc('sendtoaddress', [addr, 1.0]) + tx_hex = self.callnoderpc('getrawtransaction', [txid, ]) + + self.sync_blocks() + + try: + self.callnoderpc('sendrawtransaction', [tx_hex, ], node_id=1) + except Exception as e: + if 'transaction already in block chain' not in str(e): + raise (e) + ro = self.callnoderpc('gettransaction', [txid, True], node_id=1) + assert (ro['txid'] == txid) + assert (ro['details'][0]['involvesWatchonly'] is True) + assert (ro['details'][0]['amount'] == 1.0) + + # No watchonly balance in getwalletinfo + ro = self.callnoderpc('listreceivedbyaddress', [0, False, True], node_id=1) + sum_addr = 0 + for entry in ro: + if entry['address'] == addr: + sum_addr += entry['amount'] + assert (sum_addr == 1.0) + + def test_007_hdwallet(self): + logging.info('---------- Test {} hdwallet'.format(self.test_coin_from.name)) + + # Run initialiseWallet to set 'main_wallet_seedid_' + for i, sc in enumerate(self.swap_clients): + if i > 1: + # node 2 is set from a random seed + continue + sc.initialiseWallet(self.test_coin_from) + ci = sc.ci(self.test_coin_from) + if i == 0: + assert ('19ac5fdb423421b7f9a33cf319715742be5f4caa' == ci.getWalletSeedID()) + assert sc.checkWalletSeed(self.test_coin_from) is True + + def test_012_p2sh_p2wsh(self): + logging.info('---------- Test {} p2sh-p2wsh'.format(self.test_coin_from.name)) + + swap_clients = self.swap_clients + ci = self.swap_clients[0].ci(self.test_coin_from) + + script = CScript([2, 2, OP_EQUAL, ]) + + script_dest = ci.getP2SHP2WSHDest(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('signrawtransaction', [tx_funded['hex'], ])['hex'] + txid = self.callnoderpc('sendrawtransaction', [tx_signed, ]) + + self.stake_block(1) + + addr_out = self.callnoderpc('getnewaddress', ['used?',]) + addr_out = self.callnoderpc('getnewaddress', ['csv test']) + addr_witness = self.callnoderpc('addwitnessaddress', [addr_out,]) + pkh = ci.decodeAddress(addr_out) + script_out = ci.getScriptForPubkeyHash(pkh) + addr_out = ci.encodeSegwitAddressScript(script_out) + + # Double check output type + prev_tx = self.callnoderpc('decoderawtransaction', [tx_signed, ]) + assert (prev_tx['vout'][utxo_pos]['scriptPubKey']['type'] == 'scripthash') + + tx_spend = CTransaction() + tx_spend.nVersion = ci.txVersion() + tx_spend.vin.append(CTxIn(COutPoint(int(txid, 16), utxo_pos), + scriptSig=ci.getP2SHP2WSHScriptSig(script))) + 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 = tx_spend.serialize_with_witness().hex() + + txid = self.callnoderpc('sendrawtransaction', [tx_spend_hex, ]) + self.stake_block(1) + ro = self.callnoderpc('listtransactions') + sum_addr = 0 + for entry in ro: + if 'address' in entry and entry['address'] == addr_out: + if 'category' in entry and entry['category'] == 'receive': + sum_addr += entry['amount'] + assert (sum_addr == 1.0999) + + # Ensure tx was mined + tx_wallet = self.callnoderpc('gettransaction', [txid, ]) + assert (len(tx_wallet['blockhash']) == 64) + + 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 + + self.callnoderpc('staking', [True,]) + + 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) + offer = swap_clients[1].getOffer(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=260) + wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=260) + + 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) + + self.callnoderpc('staking', [False,]) + + def test_03_coin_part(self): + logging.info('---------- Test {} to PART'.format(self.test_coin_from.name)) + swap_clients = self.swap_clients + + self.callnoderpc('staking', [True,]) + + 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) + offer = swap_clients[0].getOffer(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=260) + wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, wait_for=260) + + 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) + + self.callnoderpc('staking', [False,]) + + def test_04_coin_btc(self): + logging.info('---------- Test {} to BTC'.format(self.test_coin_from.name)) + + self.callnoderpc('staking', [True,]) + + 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) + offer = swap_clients[1].getOffer(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=260) + wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=260) + + 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) + + self.callnoderpc('staking', [False,]) + + 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 + + self.callnoderpc('staking', [True,]) + + 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, 5) + + wait_for_offer(test_delay_event, swap_clients[1], offer_id) + offer = swap_clients[1].getOffer(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=260) + wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.BID_ABANDONED, sent=True, wait_for=260) + + 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) + + self.callnoderpc('staking', [False,]) + + def test_05_bad_ptx(self): + # Invalid PTX sent, swap should stall and ITx and PTx should be reclaimed by senders + logging.info('---------- Test bad PTx, BTC to {}'.format(self.test_coin_from.name)) + + self.callnoderpc('staking', [True,]) + + swap_clients = self.swap_clients + + swap_value = make_int(random.uniform(0.001, 10.0), scale=8, r=1) + logging.info('swap_value {}'.format(format_amount(swap_value, 8))) + offer_id = swap_clients[0].postOffer(Coins.BTC, self.test_coin_from, swap_value, 0.1 * COIN, swap_value, SwapTypes.SELLER_FIRST, + TxLockTypes.SEQUENCE_LOCK_BLOCKS, 5) + + wait_for_offer(test_delay_event, swap_clients[1], offer_id) + offer = swap_clients[1].getOffer(offer_id) + bid_id = swap_clients[1].postBid(offer_id, offer.amount_from) + swap_clients[1].setBidDebugInd(bid_id, DebugTypes.MAKE_INVALID_PTX) + + wait_for_bid(test_delay_event, swap_clients[0], bid_id) + swap_clients[0].acceptBid(bid_id) + + wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=320) + wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=320) + + js_0_bid = read_json_api(1800, 'bids/{}'.format(bid_id.hex())) + js_1_bid = read_json_api(1801, 'bids/{}'.format(bid_id.hex())) + assert (js_0_bid['itx_state'] == 'Refunded') + assert (js_1_bid['ptx_state'] == 'Refunded') + + 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) + + self.callnoderpc('staking', [False,]) + + def test_06_self_bid(self): + logging.info('---------- Test same client, BTC to {}'.format(self.test_coin_from.name)) + + self.callnoderpc('staking', [True,]) + + 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) + offer = swap_clients[0].getOffer(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=260) + wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=260) + + 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) + self.callnoderpc('staking', [False,]) + + def test_07_error(self): + logging.info('---------- Test error, BTC to {}, set fee above bid value'.format(self.test_coin_from.name)) + + self.callnoderpc('staking', [True,]) + + 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) + offer = swap_clients[0].getOffer(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) + try: + swap_clients[0].getChainClientSettings(Coins.BTC)['override_feerate'] = 10.0 + swap_clients[0].getChainClientSettings(Coins.NAV)['override_feerate'] = 10.0 + wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.BID_ERROR, wait_for=260) + swap_clients[0].abandonBid(bid_id) + finally: + del swap_clients[0].getChainClientSettings(Coins.BTC)['override_feerate'] + del swap_clients[0].getChainClientSettings(Coins.NAV)['override_feerate'] + self.callnoderpc('staking', [False,]) + + def test_08_wallet(self): + logging.info('---------- Test {} wallet'.format(self.test_coin_from.name)) + + logging.info('Test withdrawal') + 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 = read_json_api(TEST_HTTP_PORT + 0, 'wallets/{}/withdraw'.format(self.test_coin_from.name.lower()), post_json) + assert (len(json_rv['txid']) == 64) + + logging.info('Test createutxo') + post_json = { + 'value': 10, + } + json_rv = read_json_api(TEST_HTTP_PORT + 0, 'wallets/{}/createutxo'.format(self.test_coin_from.name.lower()), post_json) + assert (len(json_rv['txid']) == 64) + + def ensure_balance(self, coin_type, node_id, amount): + tla = coin_type.name + js_w = read_json_api(1800 + node_id, 'wallets') + if float(js_w[tla]['balance']) < amount: + post_json = { + 'value': amount, + 'address': js_w[tla]['deposit_address'], + 'subfee': False, + } + json_rv = read_json_api(1800, 'wallets/{}/withdraw'.format(tla.lower()), post_json) + assert (len(json_rv['txid']) == 64) + wait_for_balance(test_delay_event, 'http://127.0.0.1:{}/json/wallets/{}'.format(1800 + node_id, tla.lower()), 'balance', amount, iterations=120, delay_time=5) + + def test_10_prefunded_itx(self): + logging.info('---------- Test prefunded itx offer') + + self.callnoderpc('staking', [True,]) + + swap_clients = self.swap_clients + coin_from = Coins.NAV + coin_to = Coins.BTC + swap_type = SwapTypes.SELLER_FIRST + ci_from = swap_clients[2].ci(coin_from) + ci_to = swap_clients[1].ci(coin_to) + tla_from = coin_from.name + + # Prepare balance + self.ensure_balance(coin_from, 2, 10.0) + self.ensure_balance(coin_to, 1, 100.0) + + js_w2 = read_json_api(1802, 'wallets') + post_json = { + 'value': 10.0, + 'address': read_json_api(1802, 'wallets/{}/nextdepositaddr'.format(tla_from.lower())), + 'subfee': True, + } + json_rv = read_json_api(1802, 'wallets/{}/withdraw'.format(tla_from.lower()), post_json) + wait_for_balance(test_delay_event, 'http://127.0.0.1:1802/json/wallets/{}'.format(tla_from.lower()), 'balance', 9.0) + assert (len(json_rv['txid']) == 64) + + # Create prefunded ITX + pi = swap_clients[2].pi(SwapTypes.XMR_SWAP) + js_w2 = read_json_api(1802, 'wallets') + swap_value = 9.5 + if float(js_w2[tla_from]['balance']) < swap_value: + swap_value = js_w2[tla_from]['balance'] + swap_value = ci_from.make_int(swap_value) + assert (swap_value > ci_from.make_int(9)) + + # Missing fundrawtransaction subtractFeeFromOutputs parameter + try: + itx = pi.getFundedInitiateTxTemplate(ci_from, swap_value, True) + except Exception as e: + assert ('subtractFeeFromOutputs' in str(e)) + else: + assert False, 'Should fail' + itx = pi.getFundedInitiateTxTemplate(ci_from, swap_value, False) + + itx_decoded = ci_from.describeTx(itx.hex()) + n = pi.findMockVout(ci_from, itx_decoded) + value_after = ci_from.make_int(itx_decoded['vout'][n]['value']) + assert (value_after == swap_value) + swap_value = value_after + wait_for_unspent(test_delay_event, ci_from, swap_value) + + extra_options = {'prefunded_itx': itx} + rate_swap = ci_to.make_int(random.uniform(0.2, 10.0), r=1) + offer_id = swap_clients[2].postOffer(coin_from, coin_to, swap_value, rate_swap, swap_value, swap_type, extra_options=extra_options) + + wait_for_offer(test_delay_event, swap_clients[1], offer_id) + offer = swap_clients[1].getOffer(offer_id) + bid_id = swap_clients[1].postBid(offer_id, offer.amount_from) + + wait_for_bid(test_delay_event, swap_clients[2], bid_id, BidStates.BID_RECEIVED) + swap_clients[2].acceptBid(bid_id) + + wait_for_bid(test_delay_event, swap_clients[2], bid_id, BidStates.SWAP_COMPLETED, wait_for=320) + wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=320) + + # Verify expected inputs were used + bid, offer = swap_clients[2].getBidAndOffer(bid_id) + assert (bid.initiate_tx) + wtx = ci_from.rpc_callback('gettransaction', [bid.initiate_tx.txid.hex(),]) + itx_after = ci_from.describeTx(wtx['hex']) + assert (len(itx_after['vin']) == len(itx_decoded['vin'])) + for i, txin in enumerate(itx_decoded['vin']): + assert (txin['txid'] == itx_after['vin'][i]['txid']) + assert (txin['vout'] == itx_after['vin'][i]['vout']) + self.callnoderpc('staking', [False,]) + + def test_11_xmrswap_to(self): + logging.info('---------- Test xmr swap protocol to') + + self.callnoderpc('staking', [True,]) + + swap_clients = self.swap_clients + coin_from = Coins.BTC + coin_to = Coins.NAV + swap_type = SwapTypes.XMR_SWAP + ci_from = swap_clients[0].ci(coin_from) + ci_to = swap_clients[1].ci(coin_to) + + swap_value = ci_from.make_int(random.uniform(0.2, 20.0), r=1) + rate_swap = ci_to.make_int(random.uniform(0.2, 20.0), r=1) + offer_id = swap_clients[0].postOffer(coin_from, coin_to, swap_value, rate_swap, swap_value, swap_type) + + wait_for_offer(test_delay_event, swap_clients[1], offer_id) + offer = swap_clients[1].getOffer(offer_id) + bid_id = swap_clients[1].postBid(offer_id, offer.amount_from) + + wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.BID_RECEIVED) + swap_clients[0].acceptBid(bid_id) + + wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=320) + wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=320) + + self.callnoderpc('staking', [False,]) + + def test_12_xmrswap_to_recover_b_lock_tx(self): + coin_from = Coins.BTC + coin_to = Coins.NAV + logging.info('---------- Test {} to {} follower recovers coin b lock tx'.format(coin_from.name, coin_to.name)) + + self.callnoderpc('staking', [True,]) + + swap_clients = self.swap_clients + ci_from = swap_clients[0].ci(coin_from) + ci_to = swap_clients[1].ci(coin_to) + + amt_swap = ci_from.make_int(random.uniform(0.1, 2.0), r=1) + rate_swap = ci_to.make_int(random.uniform(0.2, 20.0), r=1) + offer_id = swap_clients[0].postOffer( + coin_from, coin_to, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP, + lock_type=TxLockTypes.SEQUENCE_LOCK_BLOCKS, lock_value=12) + wait_for_offer(test_delay_event, swap_clients[1], offer_id) + offer = swap_clients[1].getOffer(offer_id) + + bid_id = swap_clients[1].postXmrBid(offer_id, offer.amount_from) + wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.BID_RECEIVED) + + bid, xmr_swap = swap_clients[0].getXmrBid(bid_id) + swap_clients[1].setBidDebugInd(bid_id, DebugTypes.CREATE_INVALID_COIN_B_LOCK) + swap_clients[0].acceptXmrBid(bid_id) + + wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, wait_for=380) + wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, sent=True, wait_for=180) + + self.callnoderpc('staking', [False,]) + + # Adaptor sig swap tests + def test_01_a_full_swap(self): + self.node_a_id = 2 + self.sync_blocks() + self.callnoderpc('staking', [True,]) + self.do_test_01_full_swap(self.test_coin_from, Coins.XMR) + + self.callnoderpc('staking', [False,]) + + def test_01_b_full_swap_reverse(self): + self.node_a_id = 0 + self.sync_blocks() + self.callnoderpc('staking', [True,]) + + self.prepare_balance(Coins.XMR, 100.0, 1800, 1801) + self.do_test_01_full_swap(Coins.XMR, self.test_coin_from) + + self.callnoderpc('staking', [False,]) + + def test_02_a_leader_recover_a_lock_tx(self): + self.node_a_id = 2 + self.sync_blocks() + self.prepare_balance(Coins.NAV, 1000.0, 1802, 1800) + self.do_test_02_leader_recover_a_lock_tx(self.test_coin_from, Coins.XMR, lock_value=5) + + def test_02_b_leader_recover_a_lock_tx_reverse(self): + self.sync_blocks() + self.prepare_balance(Coins.XMR, 100.0, 1800, 1801) + self.do_test_02_leader_recover_a_lock_tx(Coins.XMR, self.test_coin_from, lock_value=5) + + def test_03_a_follower_recover_a_lock_tx(self): + self.node_a_id = 2 + self.sync_blocks() + self.prepare_balance(Coins.NAV, 1000.0, 1802, 1800) + self.do_test_03_follower_recover_a_lock_tx(self.test_coin_from, Coins.XMR, lock_value=5) + + def test_03_b_follower_recover_a_lock_tx_reverse(self): + self.sync_blocks() + self.prepare_balance(Coins.XMR, 100.0, 1800, 1801) + self.do_test_03_follower_recover_a_lock_tx(Coins.XMR, self.test_coin_from, lock_value=5) + + def test_04_a_follower_recover_b_lock_tx(self): + self.node_a_id = 2 + self.sync_blocks() + self.prepare_balance(Coins.NAV, 1000.0, 1802, 1800) + self.do_test_04_follower_recover_b_lock_tx(self.test_coin_from, Coins.XMR, lock_value=5) + + def test_04_b_follower_recover_b_lock_tx_reverse(self): + self.sync_blocks() + self.prepare_balance(Coins.XMR, 100.0, 1800, 1801) + self.do_test_04_follower_recover_b_lock_tx(Coins.XMR, self.test_coin_from, lock_value=5) + + def test_05_self_bid(self): + self.sync_blocks() + self.do_test_05_self_bid(self.test_coin_from, Coins.XMR) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/basicswap/test_btc_xmr.py b/tests/basicswap/test_btc_xmr.py index 3cc1cf2..3e8cc72 100644 --- a/tests/basicswap/test_btc_xmr.py +++ b/tests/basicswap/test_btc_xmr.py @@ -44,6 +44,7 @@ from basicswap.contrib.test_framework.messages import ( ) from basicswap.contrib.test_framework.script import ( CScript, + OP_EQUAL, OP_CHECKLOCKTIMEVERIFY, OP_CHECKSEQUENCEVERIFY, ) @@ -54,6 +55,10 @@ logger = logging.getLogger() class TestFunctions(BaseTest): base_rpc_port = None + extra_wait_time = 0 + + node_a_id = 0 + node_b_id = 1 def getBalance(self, js_wallets, coin) -> float: if coin == Coins.PART_BLIND: @@ -64,6 +69,9 @@ class TestFunctions(BaseTest): coin_ticker: str = 'PART' balance_type: str = 'anon_balance' unconfirmed_name: str = 'anon_pending' + elif coin == Coins.NAV: + coin_wallet = js_wallets[coin.name] + return float(coin_wallet['balance']) + float(coin_wallet['unconfirmed']) + float(coin_wallet['immature']) else: coin_ticker: str = coin.name balance_type: str = 'balance' @@ -77,7 +85,9 @@ class TestFunctions(BaseTest): def mineBlock(self, num_blocks=1): self.callnoderpc('generatetoaddress', [num_blocks, self.btc_addr]) - def prepare_balance(self, coin, amount: float, port_target_node: int, port_take_from_node: int) -> None: + def prepare_balance(self, coin, amount: float, port_target_node: int, port_take_from_node: int, test_balance: bool = True) -> None: + delay_iterations = 100 if coin == Coins.NAV else 20 + delay_time = 5 if coin == Coins.NAV else 3 if coin == Coins.PART_BLIND: coin_ticker: str = 'PART' balance_type: str = 'blind_balance' @@ -93,7 +103,8 @@ class TestFunctions(BaseTest): balance_type: str = 'balance' address_type: str = 'deposit_address' js_w = read_json_api(port_target_node, 'wallets') - if float(js_w[coin_ticker][balance_type]) >= amount: + current_balance: float = float(js_w[coin_ticker][balance_type]) + if test_balance and current_balance >= amount: return post_json = { 'value': amount, @@ -104,33 +115,38 @@ class TestFunctions(BaseTest): post_json['type_to'] = type_to json_rv = read_json_api(port_take_from_node, 'wallets/{}/withdraw'.format(coin_ticker.lower()), post_json) assert (len(json_rv['txid']) == 64) - wait_for_balance(test_delay_event, 'http://127.0.0.1:{}/json/wallets/{}'.format(port_target_node, coin_ticker.lower()), balance_type, amount) + wait_for_amount: float = amount + if not test_balance: + wait_for_amount += current_balance + wait_for_balance(test_delay_event, 'http://127.0.0.1:{}/json/wallets/{}'.format(port_target_node, coin_ticker.lower()), balance_type, wait_for_amount, iterations=delay_iterations, delay_time=delay_time) def do_test_01_full_swap(self, coin_from: Coins, coin_to: Coins) -> None: logging.info('---------- Test {} to {}'.format(coin_from.name, coin_to.name)) - swap_clients = self.swap_clients - reverse_bid: bool = coin_from in swap_clients[0].scriptless_coins - ci_from = swap_clients[0].ci(coin_from) - ci_to = swap_clients[1].ci(coin_to) - ci_part0 = swap_clients[0].ci(Coins.PART) - ci_part1 = swap_clients[1].ci(Coins.PART) - # Offerer sends the offer # Bidder sends the bid - id_offerer: int = 0 - id_bidder: int = 1 + id_offerer: int = self.node_a_id + id_bidder: int = self.node_b_id + + swap_clients = self.swap_clients + reverse_bid: bool = coin_from in swap_clients[id_offerer].scriptless_coins + ci_from = swap_clients[id_offerer].ci(coin_from) + ci_to = swap_clients[id_bidder].ci(coin_to) + ci_part0 = swap_clients[id_offerer].ci(Coins.PART) + ci_part1 = swap_clients[id_bidder].ci(Coins.PART) + + self.prepare_balance(coin_from, 100.0, 1800 + id_offerer, 1801 if reverse_bid else 1800) # Leader sends the initial (chain a) lock tx. # Follower sends the participate (chain b) lock tx. - id_leader: int = 1 if reverse_bid else 0 - id_follower: int = 0 if reverse_bid else 1 + id_leader: int = id_bidder if reverse_bid else id_offerer + id_follower: int = id_offerer if reverse_bid else id_bidder logging.info(f'Offerer, bidder, leader, follower: {id_offerer}, {id_bidder}, {id_leader}, {id_follower}') - js_0 = read_json_api(1800, 'wallets') + js_0 = read_json_api(1800 + id_offerer, 'wallets') node0_from_before: float = self.getBalance(js_0, coin_from) - js_1 = read_json_api(1801, 'wallets') + js_1 = read_json_api(1800 + id_bidder, 'wallets') node1_from_before: float = self.getBalance(js_1, coin_from) node0_sent_messages_before: int = ci_part0.rpc_callback('smsgoutbox', ['count',])['num_messages'] @@ -144,20 +160,20 @@ class TestFunctions(BaseTest): assert (offer.offer_id == offer_id) post_json = {'with_extra_info': True} - offer0 = read_json_api(1800, f'offers/{offer_id.hex()}', post_json)[0] - offer1 = read_json_api(1800, f'offers/{offer_id.hex()}', post_json)[0] + offer0 = read_json_api(1800 + id_offerer, f'offers/{offer_id.hex()}', post_json)[0] + offer1 = read_json_api(1800 + id_offerer, f'offers/{offer_id.hex()}', post_json)[0] from basicswap.util import dumpj logging.info('offer0 {} '.format(dumpj(offer0))) logging.info('offer1 {} '.format(dumpj(offer1))) assert ('lock_time_1' in offer0) assert ('lock_time_1' in offer1) - bid_id = swap_clients[1].postXmrBid(offer_id, offer.amount_from) + bid_id = swap_clients[id_bidder].postXmrBid(offer_id, offer.amount_from) wait_for_bid(test_delay_event, swap_clients[id_offerer], bid_id, BidStates.BID_RECEIVED) - bid0 = read_json_api(1800, f'bids/{bid_id.hex()}') - bid1 = read_json_api(1801, f'bids/{bid_id.hex()}') + bid0 = read_json_api(1800 + id_offerer, f'bids/{bid_id.hex()}') + bid1 = read_json_api(1800 + id_bidder, f'bids/{bid_id.hex()}') tolerance = 20 if reverse_bid else 0 assert (bid0['ticker_from'] == ci_from.ticker()) @@ -172,7 +188,7 @@ class TestFunctions(BaseTest): assert (bid1['reverse_bid'] == reverse_bid) found: bool = False - bids0 = read_json_api(1800, 'bids') + bids0 = read_json_api(1800 + id_offerer, 'bids') logging.info('bids0 {} '.format(bids0)) for bid in bids0: logging.info('bid {} '.format(bid)) @@ -186,16 +202,16 @@ class TestFunctions(BaseTest): swap_clients[id_offerer].acceptBid(bid_id) - wait_for_bid(test_delay_event, swap_clients[id_offerer], bid_id, BidStates.SWAP_COMPLETED, wait_for=180) - wait_for_bid(test_delay_event, swap_clients[id_bidder], bid_id, BidStates.SWAP_COMPLETED, sent=True) + wait_for_bid(test_delay_event, swap_clients[id_offerer], bid_id, BidStates.SWAP_COMPLETED, wait_for=(self.extra_wait_time + 180)) + wait_for_bid(test_delay_event, swap_clients[id_bidder], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=(self.extra_wait_time + 30)) amount_from = float(ci_from.format_amount(amt_swap)) - js_1_after = read_json_api(1801, 'wallets') + js_1_after = read_json_api(1800 + id_bidder, 'wallets') node1_from_after = self.getBalance(js_1_after, coin_from) if coin_from is not Coins.PART: # TODO: staking assert (node1_from_after > node1_from_before + (amount_from - 0.05)) - js_0_after = read_json_api(1800, 'wallets') + js_0_after = read_json_api(1800 + id_offerer, 'wallets') node0_from_after: float = self.getBalance(js_0_after, coin_from) # TODO: Discard block rewards # assert (node0_from_after < node0_from_before - amount_from) @@ -217,8 +233,8 @@ class TestFunctions(BaseTest): assert (node1_sent_messages == (4 + split_msgs if reverse_bid else 2 + split_msgs)) post_json = {'show_extra': True} - bid0 = read_json_api(1800, f'bids/{bid_id.hex()}', post_json) - bid1 = read_json_api(1801, f'bids/{bid_id.hex()}', post_json) + bid0 = read_json_api(1800 + id_offerer, f'bids/{bid_id.hex()}', post_json) + bid1 = read_json_api(1800 + id_bidder, f'bids/{bid_id.hex()}', post_json) logging.info('bid0 {} '.format(dumpj(bid0))) logging.info('bid1 {} '.format(dumpj(bid1))) @@ -237,18 +253,19 @@ class TestFunctions(BaseTest): assert (chain_a_lock_txid is not None) assert (chain_b_lock_txid is not None) - def do_test_02_leader_recover_a_lock_tx(self, coin_from: Coins, coin_to: Coins) -> None: + def do_test_02_leader_recover_a_lock_tx(self, coin_from: Coins, coin_to: Coins, lock_value: int = 32) -> None: logging.info('---------- Test {} to {} leader recovers coin a lock tx'.format(coin_from.name, coin_to.name)) - swap_clients = self.swap_clients - reverse_bid: bool = coin_from in swap_clients[0].scriptless_coins - ci_from = swap_clients[0].ci(coin_from) - ci_to = swap_clients[0].ci(coin_to) + id_offerer: int = self.node_a_id + id_bidder: int = self.node_b_id - id_offerer: int = 0 - id_bidder: int = 1 - id_leader: int = 1 if reverse_bid else 0 - id_follower: int = 0 if reverse_bid else 1 + swap_clients = self.swap_clients + reverse_bid: bool = coin_from in swap_clients[id_offerer].scriptless_coins + ci_from = swap_clients[id_offerer].ci(coin_from) + ci_to = swap_clients[id_offerer].ci(coin_to) + + id_leader: int = id_bidder if reverse_bid else id_offerer + id_follower: int = id_offerer if reverse_bid else id_bidder logging.info(f'Offerer, bidder, leader, follower: {id_offerer}, {id_bidder}, {id_leader}, {id_follower}') js_wl_before = read_json_api(1800 + id_leader, 'wallets') @@ -258,7 +275,7 @@ class TestFunctions(BaseTest): rate_swap = ci_to.make_int(random.uniform(0.2, 20.0), r=1) offer_id = swap_clients[id_offerer].postOffer( coin_from, coin_to, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP, - lock_type=TxLockTypes.SEQUENCE_LOCK_BLOCKS, lock_value=32) + lock_type=TxLockTypes.SEQUENCE_LOCK_BLOCKS, lock_value=lock_value) wait_for_offer(test_delay_event, swap_clients[id_bidder], offer_id) offer = swap_clients[id_bidder].getOffer(offer_id) @@ -269,8 +286,8 @@ class TestFunctions(BaseTest): swap_clients[id_offerer].acceptBid(bid_id) leader_sent_bid: bool = True if reverse_bid else False - wait_for_bid(test_delay_event, swap_clients[id_leader], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, sent=leader_sent_bid, wait_for=180) - wait_for_bid(test_delay_event, swap_clients[id_follower], bid_id, [BidStates.BID_STALLED_FOR_TEST, BidStates.XMR_SWAP_FAILED], sent=(not leader_sent_bid)) + wait_for_bid(test_delay_event, swap_clients[id_leader], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, sent=leader_sent_bid, wait_for=(self.extra_wait_time + 180)) + wait_for_bid(test_delay_event, swap_clients[id_follower], bid_id, [BidStates.BID_STALLED_FOR_TEST, BidStates.XMR_SWAP_FAILED], sent=(not leader_sent_bid), wait_for=(self.extra_wait_time + 30)) js_wl_after = read_json_api(1800 + id_leader, 'wallets') wl_from_after = self.getBalance(js_wl_after, coin_from) @@ -278,32 +295,33 @@ class TestFunctions(BaseTest): # TODO: Discard block rewards # assert (node0_from_before - node0_from_after < 0.02) - def do_test_03_follower_recover_a_lock_tx(self, coin_from, coin_to, lock_value=32): + def do_test_03_follower_recover_a_lock_tx(self, coin_from, coin_to, lock_value: int = 32): logging.info('---------- Test {} to {} follower recovers coin a lock tx'.format(coin_from.name, coin_to.name)) # Leader is too slow to recover the coin a lock tx and follower swipes it # coin b lock tx remains unspent - swap_clients = self.swap_clients - reverse_bid: bool = coin_from in swap_clients[0].scriptless_coins - ci_from = swap_clients[0].ci(coin_from) - ci_to = swap_clients[0].ci(coin_to) + id_offerer: int = self.node_a_id + id_bidder: int = self.node_b_id - id_offerer: int = 0 - id_bidder: int = 1 - id_leader: int = 1 if reverse_bid else 0 - id_follower: int = 0 if reverse_bid else 1 + swap_clients = self.swap_clients + reverse_bid: bool = coin_from in swap_clients[id_offerer].scriptless_coins + ci_from = swap_clients[id_offerer].ci(coin_from) + ci_to = swap_clients[id_offerer].ci(coin_to) + + id_leader: int = id_bidder if reverse_bid else id_offerer + id_follower: int = id_offerer if reverse_bid else id_bidder logging.info(f'Offerer, bidder, leader, follower: {id_offerer}, {id_bidder}, {id_leader}, {id_follower}') - js_w0_before = read_json_api(1800, 'wallets') - js_w1_before = read_json_api(1801, 'wallets') + js_w0_before = read_json_api(1800 + id_offerer, 'wallets') + js_w1_before = read_json_api(1800 + id_bidder, 'wallets') amt_swap = ci_from.make_int(random.uniform(0.1, 2.0), r=1) rate_swap = ci_to.make_int(random.uniform(0.2, 20.0), r=1) offer_id = swap_clients[id_offerer].postOffer( coin_from, coin_to, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP, lock_type=TxLockTypes.SEQUENCE_LOCK_BLOCKS, lock_value=lock_value) - wait_for_offer(test_delay_event, swap_clients[1], offer_id) + wait_for_offer(test_delay_event, swap_clients[id_bidder], offer_id) offer = swap_clients[id_bidder].getOffer(offer_id) bid_id = swap_clients[id_bidder].postXmrBid(offer_id, offer.amount_from) @@ -315,10 +333,10 @@ class TestFunctions(BaseTest): swap_clients[id_offerer].acceptBid(bid_id) leader_sent_bid: bool = True if reverse_bid else False - wait_for_bid(test_delay_event, swap_clients[id_leader], bid_id, BidStates.BID_STALLED_FOR_TEST, wait_for=180, sent=leader_sent_bid) - wait_for_bid(test_delay_event, swap_clients[id_follower], bid_id, BidStates.XMR_SWAP_FAILED_SWIPED, wait_for=80, sent=(not leader_sent_bid)) + wait_for_bid(test_delay_event, swap_clients[id_leader], bid_id, BidStates.BID_STALLED_FOR_TEST, wait_for=(self.extra_wait_time + 180), sent=leader_sent_bid) + wait_for_bid(test_delay_event, swap_clients[id_follower], bid_id, BidStates.XMR_SWAP_FAILED_SWIPED, wait_for=(self.extra_wait_time + 80), sent=(not leader_sent_bid)) - js_w1_after = read_json_api(1801, 'wallets') + js_w1_after = read_json_api(1800 + id_bidder, 'wallets') node1_from_before = self.getBalance(js_w1_before, coin_from) node1_from_after = self.getBalance(js_w1_after, coin_from) @@ -326,33 +344,36 @@ class TestFunctions(BaseTest): # TODO: Discard block rewards # assert (node1_from_after - node1_from_before > (amount_from - 0.02)) - swap_clients[0].abandonBid(bid_id) + swap_clients[id_offerer].abandonBid(bid_id) - wait_for_none_active(test_delay_event, 1800) - wait_for_none_active(test_delay_event, 1801) + wait_for_none_active(test_delay_event, 1800 + id_offerer) + wait_for_none_active(test_delay_event, 1800 + id_bidder) - def do_test_04_follower_recover_b_lock_tx(self, coin_from, coin_to): + def do_test_04_follower_recover_b_lock_tx(self, coin_from, coin_to, lock_value: int = 32): logging.info('---------- Test {} to {} follower recovers coin b lock tx'.format(coin_from.name, coin_to.name)) - swap_clients = self.swap_clients - reverse_bid: bool = coin_from in swap_clients[0].scriptless_coins - ci_from = swap_clients[0].ci(coin_from) - ci_to = swap_clients[0].ci(coin_to) + id_offerer: int = self.node_a_id + id_bidder: int = self.node_b_id - id_offerer: int = 0 - id_bidder: int = 1 - id_leader: int = 1 if reverse_bid else 0 - id_follower: int = 0 if reverse_bid else 1 + swap_clients = self.swap_clients + reverse_bid: bool = coin_from in swap_clients[id_offerer].scriptless_coins + ci_from = swap_clients[id_offerer].ci(coin_from) + ci_to = swap_clients[id_offerer].ci(coin_to) + + id_offerer: int = id_offerer + id_bidder: int = id_bidder + id_leader: int = id_bidder if reverse_bid else id_offerer + id_follower: int = id_offerer if reverse_bid else id_bidder logging.info(f'Offerer, bidder, leader, follower: {id_offerer}, {id_bidder}, {id_leader}, {id_follower}') - js_w0_before = read_json_api(1800, 'wallets') - js_w1_before = read_json_api(1801, 'wallets') + js_w0_before = read_json_api(1800 + id_offerer, 'wallets') + js_w1_before = read_json_api(1800 + id_bidder, 'wallets') amt_swap = ci_from.make_int(random.uniform(0.1, 2.0), r=1) rate_swap = ci_to.make_int(random.uniform(0.2, 20.0), r=1) offer_id = swap_clients[id_offerer].postOffer( coin_from, coin_to, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP, - lock_type=TxLockTypes.SEQUENCE_LOCK_BLOCKS, lock_value=32) + lock_type=TxLockTypes.SEQUENCE_LOCK_BLOCKS, lock_value=lock_value) wait_for_offer(test_delay_event, swap_clients[id_bidder], offer_id) offer = swap_clients[id_bidder].getOffer(offer_id) @@ -363,11 +384,11 @@ class TestFunctions(BaseTest): swap_clients[id_offerer].acceptBid(bid_id) leader_sent_bid: bool = True if reverse_bid else False - wait_for_bid(test_delay_event, swap_clients[id_leader], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, wait_for=200, sent=leader_sent_bid) - wait_for_bid(test_delay_event, swap_clients[id_follower], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, sent=(not leader_sent_bid)) + wait_for_bid(test_delay_event, swap_clients[id_leader], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, wait_for=(self.extra_wait_time + 200), sent=leader_sent_bid) + wait_for_bid(test_delay_event, swap_clients[id_follower], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, sent=(not leader_sent_bid), wait_for=(self.extra_wait_time + 30)) - js_w0_after = read_json_api(1800, 'wallets') - js_w1_after = read_json_api(1801, 'wallets') + js_w0_after = read_json_api(1800 + id_offerer, 'wallets') + js_w1_after = read_json_api(1800 + id_bidder, 'wallets') node0_from_before = self.getBalance(js_w0_before, coin_from) node0_from_after = self.getBalance(js_w0_after, coin_from) @@ -392,22 +413,26 @@ class TestFunctions(BaseTest): def do_test_05_self_bid(self, coin_from, coin_to): logging.info('---------- Test {} to {} same client'.format(coin_from.name, coin_to.name)) + id_offerer: int = self.node_a_id + id_bidder: int = self.node_b_id + swap_clients = self.swap_clients - ci_from = swap_clients[0].ci(coin_from) - ci_to = swap_clients[0].ci(coin_to) + ci_from = swap_clients[id_offerer].ci(coin_from) + ci_to = swap_clients[id_offerer].ci(coin_to) amt_swap = ci_from.make_int(random.uniform(0.1, 2.0), r=1) rate_swap = ci_to.make_int(random.uniform(0.2, 20.0), r=1) - offer_id = swap_clients[1].postOffer(coin_from, coin_to, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP, auto_accept_bids=True) - bid_id = swap_clients[1].postXmrBid(offer_id, amt_swap) + offer_id = swap_clients[id_bidder].postOffer(coin_from, coin_to, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP, auto_accept_bids=True) + bid_id = swap_clients[id_bidder].postXmrBid(offer_id, amt_swap) - wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, wait_for=180) + wait_for_bid(test_delay_event, swap_clients[id_bidder], bid_id, BidStates.SWAP_COMPLETED, wait_for=(self.extra_wait_time + 180)) class BasicSwapTest(TestFunctions): def test_001_nested_segwit(self): + # p2sh-p2wpkh logging.info('---------- Test {} p2sh nested segwit'.format(self.test_coin_from.name)) addr_p2sh_segwit = self.callnoderpc('getnewaddress', ['segwit test', 'p2sh-segwit']) @@ -446,6 +471,7 @@ class BasicSwapTest(TestFunctions): assert txid_with_scriptsig == tx_signed_decoded['txid'] def test_002_native_segwit(self): + # p2wpkh logging.info('---------- Test {} p2sh native segwit'.format(self.test_coin_from.name)) addr_segwit = self.callnoderpc('getnewaddress', ['segwit test', 'bech32']) @@ -542,6 +568,10 @@ class BasicSwapTest(TestFunctions): pkh = ci.decodeSegwitAddress(addr_out) script_out = ci.getScriptForPubkeyHash(pkh) + # Double check output type + prev_tx = self.callnoderpc('decoderawtransaction', [tx_signed, ]) + assert (prev_tx['vout'][utxo_pos]['scriptPubKey']['type'] == 'witness_v0_scripthash') + tx_spend = CTransaction() tx_spend.nVersion = ci.txVersion() tx_spend.vin.append(CTxIn(COutPoint(int(txid, 16), utxo_pos), @@ -567,6 +597,10 @@ class BasicSwapTest(TestFunctions): sum_addr += entry['amount'] assert (sum_addr == 1.0999) + # Ensure tx was mined + tx_wallet = self.callnoderpc('gettransaction', [txid, ]) + assert (len(tx_wallet['blockhash']) == 64) + def test_005_watchonly(self): logging.info('---------- Test {} watchonly'.format(self.test_coin_from.name)) @@ -709,6 +743,102 @@ class BasicSwapTest(TestFunctions): assert (expect_vsize >= lock_tx_b_spend_decoded['vsize']) assert (expect_vsize - lock_tx_b_spend_decoded['vsize'] < 10) + def test_011_p2sh(self): + # Not used in bsx for native-segwit coins + logging.info('---------- Test {} p2sh'.format(self.test_coin_from.name)) + + swap_clients = self.swap_clients + ci = self.swap_clients[0].ci(self.test_coin_from) + + script = CScript([2, 2, OP_EQUAL, ]) + + script_dest = ci.get_p2sh_script_pubkey(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) + + # Double check output type + prev_tx = self.callnoderpc('decoderawtransaction', [tx_signed, ]) + assert (prev_tx['vout'][utxo_pos]['scriptPubKey']['type'] == 'scripthash') + + tx_spend = CTransaction() + tx_spend.nVersion = ci.txVersion() + tx_spend.vin.append(CTxIn(COutPoint(int(txid, 16), utxo_pos), + scriptSig=CScript([script,]))) + tx_spend.vout.append(ci.txoType()(ci.make_int(1.0999), script_out)) + tx_spend_hex = ToHex(tx_spend) + + txid = self.callnoderpc('sendrawtransaction', [tx_spend_hex, ]) + self.mineBlock(1) + 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) + + # Ensure tx was mined + tx_wallet = self.callnoderpc('gettransaction', [txid, ]) + assert (len(tx_wallet['blockhash']) == 64) + + def test_012_p2sh_p2wsh(self): + # Not used in bsx for native-segwit coins + logging.info('---------- Test {} p2sh-p2wsh'.format(self.test_coin_from.name)) + + swap_clients = self.swap_clients + ci = self.swap_clients[0].ci(self.test_coin_from) + + script = CScript([2, 2, OP_EQUAL, ]) + + script_dest = ci.getP2SHP2WSHDest(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) + + # Double check output type + prev_tx = self.callnoderpc('decoderawtransaction', [tx_signed, ]) + assert (prev_tx['vout'][utxo_pos]['scriptPubKey']['type'] == 'scripthash') + + tx_spend = CTransaction() + tx_spend.nVersion = ci.txVersion() + tx_spend.vin.append(CTxIn(COutPoint(int(txid, 16), utxo_pos), + scriptSig=ci.getP2SHP2WSHScriptSig(script))) + 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) + + txid = self.callnoderpc('sendrawtransaction', [tx_spend_hex, ]) + self.mineBlock(1) + 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) + + # Ensure tx was mined + tx_wallet = self.callnoderpc('gettransaction', [txid, ]) + assert (len(tx_wallet['blockhash']) == 64) + def test_01_a_full_swap(self): if not self.has_segwit: return @@ -953,8 +1083,12 @@ class TestBTC_PARTB(TestFunctions): self.do_test_01_full_swap(self.test_coin_from, self.test_coin_to) def test_01_b_full_swap_reverse(self): - self.prepare_balance(self.test_coin_to, 100.0, 1800, 1800) - self.do_test_01_full_swap(self.test_coin_to, self.test_coin_from) + self.extra_wait_time = 60 + try: + self.prepare_balance(self.test_coin_to, 100.0, 1800, 1800) + self.do_test_01_full_swap(self.test_coin_to, self.test_coin_from) + finally: + self.extra_wait_time = 0 def test_02_a_leader_recover_a_lock_tx(self): self.prepare_balance(self.test_coin_to, 100.0, 1801, 1800) diff --git a/tests/basicswap/test_run.py b/tests/basicswap/test_run.py index ddf3d4c..dd3763d 100644 --- a/tests/basicswap/test_run.py +++ b/tests/basicswap/test_run.py @@ -92,7 +92,12 @@ class Test(BaseTest): def getBalance(self, js_wallets, coin_type): ci = self.swap_clients[0].ci(coin_type) ticker = chainparams[coin_type]['ticker'] - return ci.make_int(float(js_wallets[ticker]['balance']) + float(js_wallets[ticker]['unconfirmed'])) + coin_wallet = js_wallets[ticker] + rv = float(coin_wallet['balance']) + rv += float(coin_wallet['unconfirmed']) + if 'immature' in coin_wallet: + rv += float(coin_wallet['immature']) + return ci.make_int(rv) def test_001_js_coins(self): js_coins = read_json_api(1800, 'coins')