From deb71856e8b9593b98a09710ebe56da0fa0586d2 Mon Sep 17 00:00:00 2001 From: tecnovert Date: Wed, 3 Feb 2021 16:01:27 +0200 Subject: [PATCH] Poll chainstates. Litecoin download link changed. Fix fee comparison tx weight difference. Remove format8. New stalled for test bid state. Moved sequence code to coin interfaces. Display estimated time lock refund tx will be valid. --- basicswap/basicswap.py | 136 ++++++++++++++++++------------- basicswap/config.py | 11 +-- basicswap/interface_btc.py | 53 +++++++++--- basicswap/interface_xmr.py | 2 +- basicswap/js_server.py | 4 +- basicswap/templates/bid_xmr.html | 5 ++ basicswap/types.py | 10 +++ basicswap/ui.py | 20 ++++- basicswap/util.py | 10 --- bin/basicswap_prepare.py | 2 +- tests/basicswap/test_other.py | 20 ++--- tests/basicswap/test_run.py | 8 +- tests/basicswap/test_xmr.py | 2 +- 13 files changed, 180 insertions(+), 103 deletions(-) create mode 100644 basicswap/types.py diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index 996acbd..7b56a6d 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -16,6 +16,7 @@ import struct import hashlib import secrets import datetime as dt +import threading import traceback import sqlalchemy as sa import collections @@ -33,7 +34,6 @@ from .interface_bitcore_btc import BitcoreBTCInterface from . import __version__ from .util import ( pubkeyToAddress, - format8, format_amount, format_timestamp, encodeAddress, @@ -89,6 +89,11 @@ from .explorers import ( ExplorerBitAps, ExplorerChainz, ) +from .types import ( + SEQUENCE_LOCK_BLOCKS, + SEQUENCE_LOCK_TIME, + ABS_LOCK_BLOCKS, + ABS_LOCK_TIME) import basicswap.config as cfg import basicswap.network as bsn import basicswap.protocols.atomic_swap_1 as atomic_swap_1 @@ -152,6 +157,7 @@ class BidStates(IntEnum): SWAP_TIMEDOUT = auto() BID_ABANDONED = auto() # Bid will no longer be processed BID_ERROR = auto() # An error occurred + BID_STALLED_FOR_TEST = auto() class TxStates(IntEnum): @@ -221,14 +227,6 @@ class DebugTypes(IntEnum): MAKE_INVALID_PTX = auto() -SEQUENCE_LOCK_BLOCKS = 1 -SEQUENCE_LOCK_TIME = 2 -ABS_LOCK_BLOCKS = 3 -ABS_LOCK_TIME = 4 - -SEQUENCE_LOCKTIME_GRANULARITY = 9 # 512 seconds -SEQUENCE_LOCKTIME_TYPE_FLAG = (1 << 22) -SEQUENCE_LOCKTIME_MASK = 0x0000ffff INITIATE_TX_TIMEOUT = 40 * 60 # TODO: make variable per coin @@ -263,6 +261,8 @@ def strBidState(state): return 'Timed-out' if state == BidStates.BID_ABANDONED: return 'Abandoned' + if state == BidStates.BID_STALLED_FOR_TEST: + return 'Stalled (debug)' if state == BidStates.BID_ERROR: return 'Error' if state == BidStates.XMR_SWAP_SCRIPT_COIN_LOCKED: @@ -366,27 +366,6 @@ def describeEventEntry(event_type, event_msg): return 'Lock tx B refund tx published' -def getExpectedSequence(lockType, lockVal, coin_type): - assert(lockVal >= 1), 'Bad lockVal' - if lockType == SEQUENCE_LOCK_BLOCKS: - return lockVal - if lockType == SEQUENCE_LOCK_TIME: - secondsLocked = lockVal - # Ensure the locked time is never less than lockVal - if secondsLocked % (1 << SEQUENCE_LOCKTIME_GRANULARITY) != 0: - secondsLocked += (1 << SEQUENCE_LOCKTIME_GRANULARITY) - secondsLocked >>= SEQUENCE_LOCKTIME_GRANULARITY - return secondsLocked | SEQUENCE_LOCKTIME_TYPE_FLAG - raise ValueError('Unknown lock type') - - -def decodeSequence(lock_value): - # Return the raw value - if lock_value & SEQUENCE_LOCKTIME_TYPE_FLAG: - return (lock_value & SEQUENCE_LOCKTIME_MASK) << SEQUENCE_LOCKTIME_GRANULARITY - return lock_value & SEQUENCE_LOCKTIME_MASK - - def getVoutByAddress(txjs, p2sh): for o in txjs['vout']: try: @@ -420,6 +399,29 @@ def getOfferProofOfFundsHash(offer_msg, offer_addr): return h.digest() +def threadPollChainState(swap_client, coin_type): + while not swap_client.delay_event.is_set(): + try: + ci = swap_client.ci(coin_type) + if coin_type == Coins.XMR: + new_height = ci.getChainHeight() + if new_height != swap_client.coin_clients[coin_type]['chain_height']: + swap_client.log.debug('New {} block at height: {}'.format(str(coin_type), new_height)) + with swap_client.mxDB: + swap_client.coin_clients[coin_type]['chain_height'] = new_height + else: + chain_state = ci.getBlockchainInfo() + if chain_state['bestblockhash'] != swap_client.coin_clients[coin_type]['chain_best_block']: + swap_client.log.debug('New {} block at height: {}'.format(str(coin_type), chain_state['blocks'])) + with swap_client.mxDB: + swap_client.coin_clients[coin_type]['chain_height'] = chain_state['blocks'] + swap_client.coin_clients[coin_type]['chain_best_block'] = chain_state['bestblockhash'] + swap_client.coin_clients[coin_type]['chain_median_time'] = chain_state['mediantime'] + except Exception as e: + swap_client.log.warning('threadPollChainState error: {}'.format(str(e))) + swap_client.delay_event.wait(random.randrange(20, 30)) # random to stagger updates + + class WatchedOutput(): # Watch for spends __slots__ = ('bid_id', 'txid_hex', 'vout', 'tx_type', 'swap_type') @@ -476,6 +478,9 @@ class BasicSwap(BaseApp): self.SMSG_SECONDS_IN_HOUR = 60 * 2 if self.chain == 'regtest' else 60 * 60 + self.delay_event = threading.Event() + self.threads = [] + # Encode key to match network wif_prefix = chainparams[Coins.PART][self.chain]['key_prefix'] self.network_key = toWIF(wif_prefix, decodeWif(self.settings['network_key'])) @@ -551,11 +556,15 @@ class BasicSwap(BaseApp): with self.mxDB: self.is_running = False + self.delay_event.set() if self._network: self._network.stopNetwork() self._network = None + for t in self.threads: + t.join() + def setCoinConnectParams(self, coin): # Set anything that does not require the daemon to be running chain_client_settings = self.getChainClientSettings(coin) @@ -594,7 +603,6 @@ class BasicSwap(BaseApp): 'conf_target': chain_client_settings.get('conf_target', 2), 'watched_outputs': [], 'last_height_checked': last_height_checked, - 'last_height': None, 'use_segwit': chain_client_settings.get('use_segwit', False), 'use_csv': chain_client_settings.get('use_csv', True), 'core_version_group': chain_client_settings.get('core_version_group', 0), @@ -604,6 +612,11 @@ class BasicSwap(BaseApp): 'chain_lookups': chain_client_settings.get('chain_lookups', 'local'), 'restore_height': chain_client_settings.get('restore_height', 0), 'fee_priority': chain_client_settings.get('fee_priority', 0), + + # Chain state + 'chain_height': None, + 'chain_best_block': None, + 'chain_median_time': None, } if self.coin_clients[coin]['connection_type'] == 'rpc': @@ -697,6 +710,10 @@ class BasicSwap(BaseApp): self.log.info('%s Core version %d', ci.coin_name(), core_version) self.coin_clients[c]['core_version'] = core_version + t = threading.Thread(target=threadPollChainState, args=(self, c)) + self.threads.append(t) + t.start() + if c == Coins.PART: self.coin_clients[c]['have_spent_index'] = ci.haveSpentIndex() @@ -1065,10 +1082,12 @@ class BasicSwap(BaseApp): assert(coin_from != coin_to), 'coin_from == coin_to' try: coin_from_t = Coins(coin_from) + ci_from = self.ci(coin_from_t) except Exception: raise ValueError('Unknown coin from type') try: coin_to_t = Coins(coin_to) + ci_to = self.ci(coin_to_t) except Exception: raise ValueError('Unknown coin to type') @@ -1124,10 +1143,10 @@ class BasicSwap(BaseApp): xmr_offer = XmrOffer() # Delay before the chain a lock refund tx can be mined - xmr_offer.lock_time_1 = getExpectedSequence(lock_type, lock_value, coin_from) + xmr_offer.lock_time_1 = ci_from.getExpectedSequence(lock_type, lock_value) # Delay before the follower can spend from the chain a lock refund tx - xmr_offer.lock_time_2 = getExpectedSequence(lock_type, lock_value, coin_from) + xmr_offer.lock_time_2 = ci_from.getExpectedSequence(lock_type, lock_value) xmr_offer.a_fee_rate = msg_buf.fee_rate_from xmr_offer.b_fee_rate = msg_buf.fee_rate_to # Unused: TODO - Set priority? @@ -1359,7 +1378,8 @@ class BasicSwap(BaseApp): return self.callcoinrpc(coin_type, 'getnetworkinfo')['relayfee'] def getFeeRateForCoin(self, coin_type, conf_target=2): - override_feerate = self.coin_clients[coin_type].get('override_feerate', None) + 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) return override_feerate, 'override_feerate' @@ -1592,7 +1612,7 @@ class BasicSwap(BaseApp): def postBid(self, offer_id, amount, addr_send_from=None, extra_options={}): # Bid to send bid.amount * offer.rate of coin_to in exchange for bid.amount of coin_from - self.log.debug('postBid %s %s', offer_id.hex(), format8(amount)) + self.log.debug('postBid %s', offer_id.hex()) offer = self.getOffer(offer_id) assert(offer), 'Offer not found: {}.'.format(offer_id.hex()) @@ -1828,6 +1848,7 @@ class BasicSwap(BaseApp): bid.contract_count = self.getNewContractId() coin_from = Coins(offer.coin_from) + ci_from = self.ci(coin_from) bid_date = dt.datetime.fromtimestamp(bid.created_at).date() secret = self.getContractSecret(bid_date, bid.contract_count) @@ -1842,7 +1863,7 @@ class BasicSwap(BaseApp): script = bid.initiate_tx.script else: if offer.lock_type < ABS_LOCK_BLOCKS: - sequence = getExpectedSequence(offer.lock_type, offer.lock_value, coin_from) + sequence = ci_from.getExpectedSequence(offer.lock_type, offer.lock_value) script = atomic_swap_1.buildContractScript(sequence, secret_hash, bid.pkhash_buyer, pkhash_refund) else: if offer.lock_type == ABS_LOCK_BLOCKS: @@ -1907,7 +1928,7 @@ class BasicSwap(BaseApp): def postXmrBid(self, offer_id, amount, addr_send_from=None): # Bid to send bid.amount * offer.rate of coin_to in exchange for bid.amount of coin_from # Send MSG1L F -> L - self.log.debug('postXmrBid %s %s', offer_id.hex(), format8(amount)) + self.log.debug('postXmrBid %s', offer_id.hex()) self.mxDB.acquire() try: @@ -2187,13 +2208,14 @@ class BasicSwap(BaseApp): def createInitiateTxn(self, coin_type, bid_id, bid, initiate_script): if self.coin_clients[coin_type]['connection_type'] != 'rpc': return None + ci = self.ci(coin_type) if self.coin_clients[coin_type]['use_segwit']: addr_to = self.encodeSegwitP2WSH(coin_type, getP2WSH(initiate_script)) else: addr_to = self.getScriptAddress(coin_type, initiate_script) self.log.debug('Create initiate txn for coin %s to %s for bid %s', str(coin_type), addr_to, bid_id.hex()) - txn = self.callcoinrpc(coin_type, 'createrawtransaction', [[], {addr_to: format8(bid.amount)}]) + txn = self.callcoinrpc(coin_type, 'createrawtransaction', [[], {addr_to: ci.format_amount(bid.amount)}]) options = { 'lockUnspents': True, @@ -2207,6 +2229,7 @@ class BasicSwap(BaseApp): self.log.debug('deriveParticipateScript for bid %s', bid_id.hex()) coin_to = Coins(offer.coin_to) + ci_to = self.ci(coin_to) bid_date = dt.datetime.fromtimestamp(bid.created_at).date() @@ -2217,7 +2240,7 @@ class BasicSwap(BaseApp): # Participate txn is locked for half the time of the initiate txn lock_value = offer.lock_value // 2 if offer.lock_type < ABS_LOCK_BLOCKS: - sequence = getExpectedSequence(offer.lock_type, lock_value, coin_to) + sequence = ci_to.getExpectedSequence(offer.lock_type, lock_value) participate_script = atomic_swap_1.buildContractScript(sequence, secret_hash, pkhash_seller, pkhash_buyer_refund) else: # Lock from the height or time of the block containing the initiate txn @@ -2259,6 +2282,7 @@ class BasicSwap(BaseApp): if self.coin_clients[coin_to]['connection_type'] != 'rpc': return None + ci = self.ci(coin_to) amount_to = bid.amount_to # Check required? @@ -2275,7 +2299,7 @@ class BasicSwap(BaseApp): else: addr_to = self.getScriptAddress(coin_to, participate_script) - txn = self.callcoinrpc(coin_to, 'createrawtransaction', [[], {addr_to: format8(amount_to)}]) + txn = self.callcoinrpc(coin_to, 'createrawtransaction', [[], {addr_to: ci.format_amount(amount_to)}]) options = { 'lockUnspents': True, 'conf_target': self.coin_clients[coin_to]['conf_target'], @@ -2311,6 +2335,7 @@ class BasicSwap(BaseApp): 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)) + ci = self.ci(coin_type) if for_txn_type == 'participate': prev_txnid = bid.participate_tx.txid.hex() @@ -2334,7 +2359,7 @@ class BasicSwap(BaseApp): 'vout': prev_n, 'scriptPubKey': script_pub_key, 'redeemScript': txn_script.hex(), - 'amount': format8(prev_amount)} + 'amount': ci.format_amount(prev_amount)} bid_date = dt.datetime.fromtimestamp(bid.created_at).date() wif_prefix = chainparams[Coins.PART][self.chain]['key_prefix'] @@ -2357,7 +2382,6 @@ class BasicSwap(BaseApp): tx_vsize = self.getContractSpendTxVSize(coin_type) tx_fee = (fee_rate * tx_vsize) / 1000 - ci = self.ci(coin_type) self.log.debug('Redeem tx fee %s, rate %s', ci.format_amount(tx_fee, conv_int=True, r=1), str(fee_rate)) amount_out = prev_amount - ci.make_int(tx_fee, r=1) @@ -2373,7 +2397,7 @@ class BasicSwap(BaseApp): else: addr_redeem_out = replaceAddrPrefix(addr_redeem_out, Coins.PART, self.chain) self.log.debug('addr_redeem_out %s', addr_redeem_out) - output_to = ' outaddr={}:{}'.format(format8(amount_out), addr_redeem_out) + output_to = ' outaddr={}:{}'.format(ci.format_amount(amount_out), addr_redeem_out) if coin_type == Coins.PART: redeem_txn = self.calltx('-create' + prevout_s + output_to) else: @@ -2471,7 +2495,7 @@ class BasicSwap(BaseApp): addr_refund_out = replaceAddrPrefix(addr_refund_out, Coins.PART, self.chain) self.log.debug('addr_refund_out %s', addr_refund_out) - output_to = ' outaddr={}:{}'.format(format8(amount_out), addr_refund_out) + output_to = ' outaddr={}:{}'.format(ci.format_amount(amount_out), addr_refund_out) if coin_type == Coins.PART: refund_txn = self.calltx('-create' + prevout_s + output_to) else: @@ -2700,8 +2724,8 @@ class BasicSwap(BaseApp): refund_tx = bid.txns[TxTypes.XMR_SWAP_A_LOCK_REFUND] if bid.was_received: if bid.debug_ind == DebugTypes.BID_DONT_SPEND_COIN_A_LOCK_REFUND: - self.log.debug('XMR bid %s: Abandoning bid for testing: %d.', bid_id.hex(), bid.debug_ind) - bid.setState(BidStates.BID_ABANDONED) + self.log.debug('XMR bid %s: Stalling bid for testing: %d.', bid_id.hex(), bid.debug_ind) + bid.setState(BidStates.BID_STALLED_FOR_TEST) rv = True self.saveBidInSession(bid_id, bid, session, xmr_swap) self.logBidEvent(bid, EventLogTypes.DEBUG_TWEAK_APPLIED, 'ind {}'.format(bid.debug_ind), session) @@ -2799,7 +2823,6 @@ class BasicSwap(BaseApp): bid_changed = False a_lock_tx_dest = ci_from.getScriptDest(xmr_swap.a_lock_tx_script) utxos, chain_height = ci_from.getOutput(bid.xmr_a_lock_tx.txid, a_lock_tx_dest, bid.amount) - self.coin_clients[ci_from.coin_type()]['last_height'] = chain_height if len(utxos) < 1: return rv @@ -2869,7 +2892,6 @@ class BasicSwap(BaseApp): if bid.xmr_b_lock_tx and bid.xmr_b_lock_tx.chain_height is not None and bid.xmr_b_lock_tx.chain_height > 0: chain_height = ci_to.getChainHeight() - self.coin_clients[ci_to.coin_type()]['last_height'] = chain_height if chain_height - bid.xmr_b_lock_tx.chain_height >= ci_to.blocks_confirmed: self.logBidEvent(bid, EventLogTypes.LOCK_TX_B_CONFIRMED, '', session) @@ -3476,7 +3498,9 @@ class BasicSwap(BaseApp): # Validate data now = int(time.time()) coin_from = Coins(offer_data.coin_from) + ci_from = self.ci(coin_from) coin_to = Coins(offer_data.coin_to) + ci_to = self.ci(coin_to) chain_from = chainparams[coin_from][self.chain] assert(offer_data.coin_from != offer_data.coin_to), 'coin_from == coin_to' @@ -3536,8 +3560,8 @@ class BasicSwap(BaseApp): xmr_offer = XmrOffer() xmr_offer.offer_id = offer_id - xmr_offer.lock_time_1 = getExpectedSequence(offer_data.lock_type, offer_data.lock_value, coin_from) - xmr_offer.lock_time_2 = getExpectedSequence(offer_data.lock_type, offer_data.lock_value, coin_from) + xmr_offer.lock_time_1 = ci_from.getExpectedSequence(offer_data.lock_type, offer_data.lock_value) + xmr_offer.lock_time_2 = ci_from.getExpectedSequence(offer_data.lock_type, offer_data.lock_value) xmr_offer.a_fee_rate = offer_data.fee_rate_from xmr_offer.b_fee_rate = offer_data.fee_rate_to @@ -3705,6 +3729,7 @@ class BasicSwap(BaseApp): assert(bid is not None and bid.was_sent is True), 'Unknown bidid' assert(offer), 'Offer not found ' + bid.offer_id.hex() coin_from = Coins(offer.coin_from) + ci_from = self.ci(coin_from) assert(bid.expire_at > now + self._bid_expired_leeway), 'Bid expired' @@ -3730,7 +3755,7 @@ class BasicSwap(BaseApp): script_lock_value = int(scriptvalues[2]) if use_csv: - expect_sequence = getExpectedSequence(offer.lock_type, offer.lock_value, coin_from) + expect_sequence = ci_from.getExpectedSequence(offer.lock_type, offer.lock_value) assert(script_lock_value == expect_sequence), 'sequence mismatch' else: if offer.lock_type == ABS_LOCK_BLOCKS: @@ -3803,7 +3828,7 @@ class BasicSwap(BaseApp): if self.countAcceptedBids(bid.offer_id) > 0: self.log.info('Not auto accepting bid %s, already have', bid.bid_id.hex()) elif bid.amount != offer.amount_from: - self.log.info('Not auto accepting bid %s, want exact amount match', bid_id.hex()) + self.log.info('Not auto accepting bid %s, want exact amount match', bid.bid_id.hex()) else: delay = random.randrange(self.min_delay_event, self.max_delay_event) self.log.info('Auto accepting xmr bid %s in %d seconds', bid.bid_id.hex(), delay) @@ -4155,8 +4180,8 @@ class BasicSwap(BaseApp): ci_to = self.ci(coin_to) if bid.debug_ind == DebugTypes.BID_STOP_AFTER_COIN_A_LOCK: - self.log.debug('XMR bid %s: Abandoning bid for testing: %d.', bid_id.hex(), bid.debug_ind) - bid.setState(BidStates.BID_ABANDONED) + self.log.debug('XMR bid %s: Stalling bid for testing: %d.', bid_id.hex(), bid.debug_ind) + bid.setState(BidStates.BID_STALLED_FOR_TEST) self.saveBidInSession(bid_id, bid, session, xmr_swap, save_in_progress=offer) self.logBidEvent(bid, EventLogTypes.DEBUG_TWEAK_APPLIED, 'ind {}'.format(bid.debug_ind), session) return @@ -4890,7 +4915,8 @@ class BasicSwap(BaseApp): now = int(time.time()) session = scoped_session(self.session_factory) - query_str = 'SELECT bids.created_at, bids.bid_id, bids.offer_id, bids.amount, bids.state, bids.was_received, tx1.state, tx2.state FROM bids ' + \ + query_str = 'SELECT bids.created_at, bids.bid_id, bids.offer_id, bids.amount, bids.state, bids.was_received, tx1.state, tx2.state, offers.coin_from FROM bids ' + \ + 'LEFT JOIN offers ON offers.offer_id = bids.offer_id ' + \ 'LEFT JOIN transactions AS tx1 ON tx1.bid_id = bids.bid_id AND tx1.tx_type = {} '.format(TxTypes.ITX) + \ 'LEFT JOIN transactions AS tx2 ON tx2.bid_id = bids.bid_id AND tx2.tx_type = {} '.format(TxTypes.PTX) diff --git a/basicswap/config.py b/basicswap/config.py index 4be7f33..59fad0f 100644 --- a/basicswap/config.py +++ b/basicswap/config.py @@ -10,28 +10,29 @@ CONFIG_FILENAME = 'basicswap.json' DEFAULT_DATADIR = '~/.basicswap' DEFAULT_ALLOW_CORS = False TEST_DATADIRS = os.path.expanduser(os.getenv('DATADIRS', '/tmp/basicswap')) +DEFAULT_TEST_BINDIR = os.path.expanduser(os.getenv('DEFAULT_TEST_BINDIR', '~/tmp/bin')) bin_suffix = ('.exe' if os.name == 'nt' else '') -PARTICL_BINDIR = os.path.expanduser(os.getenv('PARTICL_BINDIR', '')) +PARTICL_BINDIR = os.path.expanduser(os.getenv('PARTICL_BINDIR', os.path.join(DEFAULT_TEST_BINDIR, 'particl'))) PARTICLD = os.getenv('PARTICLD', 'particld' + bin_suffix) PARTICL_CLI = os.getenv('PARTICL_CLI', 'particl-cli' + bin_suffix) PARTICL_TX = os.getenv('PARTICL_TX', 'particl-tx' + bin_suffix) -BITCOIN_BINDIR = os.path.expanduser(os.getenv('BITCOIN_BINDIR', '')) +BITCOIN_BINDIR = os.path.expanduser(os.getenv('BITCOIN_BINDIR', os.path.join(DEFAULT_TEST_BINDIR, 'bitcoin'))) BITCOIND = os.getenv('BITCOIND', 'bitcoind' + bin_suffix) BITCOIN_CLI = os.getenv('BITCOIN_CLI', 'bitcoin-cli' + bin_suffix) BITCOIN_TX = os.getenv('BITCOIN_TX', 'bitcoin-tx' + bin_suffix) -LITECOIN_BINDIR = os.path.expanduser(os.getenv('LITECOIN_BINDIR', '')) +LITECOIN_BINDIR = os.path.expanduser(os.getenv('LITECOIN_BINDIR', os.path.join(DEFAULT_TEST_BINDIR, 'litecoin'))) LITECOIND = os.getenv('LITECOIND', 'litecoind' + bin_suffix) LITECOIN_CLI = os.getenv('LITECOIN_CLI', 'litecoin-cli' + bin_suffix) LITECOIN_TX = os.getenv('LITECOIN_TX', 'litecoin-tx' + bin_suffix) -NAMECOIN_BINDIR = os.path.expanduser(os.getenv('NAMECOIN_BINDIR', '')) +NAMECOIN_BINDIR = os.path.expanduser(os.getenv('NAMECOIN_BINDIR', os.path.join(DEFAULT_TEST_BINDIR, 'namecoin'))) NAMECOIND = os.getenv('NAMECOIND', 'namecoind' + bin_suffix) NAMECOIN_CLI = os.getenv('NAMECOIN_CLI', 'namecoin-cli' + bin_suffix) NAMECOIN_TX = os.getenv('NAMECOIN_TX', 'namecoin-tx' + bin_suffix) -XMR_BINDIR = os.path.expanduser(os.getenv('XMR_BINDIR', '')) +XMR_BINDIR = os.path.expanduser(os.getenv('XMR_BINDIR', os.path.join(DEFAULT_TEST_BINDIR, 'monero'))) XMRD = os.getenv('XMRD', 'monerod' + bin_suffix) XMR_WALLET_RPC = os.getenv('XMR_WALLET_RPC', 'monero-wallet-rpc' + bin_suffix) diff --git a/basicswap/interface_btc.py b/basicswap/interface_btc.py index 1db63da..0f888a8 100644 --- a/basicswap/interface_btc.py +++ b/basicswap/interface_btc.py @@ -62,10 +62,19 @@ from .contrib.test_framework.script import ( SegwitV0SignatureHash, hash160) +from .types import ( + SEQUENCE_LOCK_BLOCKS, + SEQUENCE_LOCK_TIME) + from .chainparams import CoinInterface, Coins, chainparams from .rpc import make_rpc_func +SEQUENCE_LOCKTIME_GRANULARITY = 9 # 512 seconds +SEQUENCE_LOCKTIME_TYPE_FLAG = (1 << 22) +SEQUENCE_LOCKTIME_MASK = 0x0000ffff + + def findOutput(tx, script_pk): for i in range(len(tx.vout)): if tx.vout[i].scriptPubKey == script_pk: @@ -121,6 +130,27 @@ class BTCInterface(CoinInterface): def txoType(): return CTxOut + @staticmethod + def getExpectedSequence(lockType, lockVal): + assert(lockVal >= 1), 'Bad lockVal' + if lockType == SEQUENCE_LOCK_BLOCKS: + return lockVal + if lockType == SEQUENCE_LOCK_TIME: + secondsLocked = lockVal + # Ensure the locked time is never less than lockVal + if secondsLocked % (1 << SEQUENCE_LOCKTIME_GRANULARITY) != 0: + secondsLocked += (1 << SEQUENCE_LOCKTIME_GRANULARITY) + secondsLocked >>= SEQUENCE_LOCKTIME_GRANULARITY + return secondsLocked | SEQUENCE_LOCKTIME_TYPE_FLAG + raise ValueError('Unknown lock type') + + @staticmethod + def decodeSequence(lock_value): + # Return the raw value + if lock_value & SEQUENCE_LOCKTIME_TYPE_FLAG: + return (lock_value & SEQUENCE_LOCKTIME_MASK) << SEQUENCE_LOCKTIME_GRANULARITY + return lock_value & SEQUENCE_LOCKTIME_MASK + def __init__(self, coin_settings, network, swap_client=None): super().__init__() rpc_host = coin_settings.get('rpchost', '127.0.0.1') @@ -129,7 +159,7 @@ class BTCInterface(CoinInterface): self.blocks_confirmed = coin_settings['blocks_confirmed'] self.setConfTarget(coin_settings['conf_target']) self._sc = swap_client - self._log = self._sc.log if self._sc.log else logging + self._log = self._sc.log if self._sc and self._sc.log else logging def setConfTarget(self, new_conf_target): assert(new_conf_target >= 1 and new_conf_target < 33), 'Invalid conf_target value' @@ -154,6 +184,9 @@ class BTCInterface(CoinInterface): block_hash = self.rpc_callback('getblockhash', [height]) return self.rpc_callback('getblockheader', [block_hash]) + def getBlockHeader(self, block_hash): + return self.rpc_callback('getblockheader', [block_hash]) + def initialiseWallet(self, key_bytes): wif_prefix = chainparams[self.coin_type()][self._network]['key_prefix'] key_wif = toWIF(wif_prefix, key_bytes) @@ -363,7 +396,7 @@ class BTCInterface(CoinInterface): tx.vout.append(self.txoType()(locked_coin, self.getScriptForPubkeyHash(pkh_refund_to))) witness_bytes = len(script_lock_refund) - witness_bytes += 73 * 2 # 2 signatures (72 + 1 byte size) + witness_bytes += 74 * 2 # 2 signatures (72 + 1 byte sighashtype + 1 byte size) - Use maximum txn size for estimate witness_bytes += 4 # 1 empty, 1 true witness stack values witness_bytes += getCompactSizeLen(witness_bytes) vsize = self.getTxVSize(tx, add_witness_bytes=witness_bytes) @@ -398,7 +431,7 @@ class BTCInterface(CoinInterface): tx.vout.append(self.txoType()(locked_coin, self.getScriptForPubkeyHash(pkh_dest))) witness_bytes = len(script_lock_refund) - witness_bytes += 73 # signature (72 + 1 byte size) + witness_bytes += 74 # 2 signatures (72 + 1 byte sighashtype + 1 byte size) - Use maximum txn size for estimate witness_bytes += 1 # 1 empty stack value witness_bytes += getCompactSizeLen(witness_bytes) vsize = self.getTxVSize(tx, add_witness_bytes=witness_bytes) @@ -429,7 +462,7 @@ class BTCInterface(CoinInterface): witness_bytes = len(script_lock) witness_bytes += 33 # sv, size - witness_bytes += 73 * 2 # 2 signatures (72 + 1 byte size) + witness_bytes += 74 * 2 # 2 signatures (72 + 1 byte sighashtype + 1 byte size) - Use maximum txn size for estimate witness_bytes += 4 # 1 empty, 1 true witness stack values witness_bytes += getCompactSizeLen(witness_bytes) vsize = self.getTxVSize(tx, add_witness_bytes=witness_bytes) @@ -546,7 +579,7 @@ class BTCInterface(CoinInterface): assert(fee_paid > 0) witness_bytes = len(prevout_script) - witness_bytes += 73 * 2 # 2 signatures (72 + 1 byts size) + witness_bytes += 74 * 2 # 2 signatures (72 + 1 byte sighashtype + 1 byte size) - Use maximum txn size for estimate witness_bytes += 2 # 2 empty witness stack values witness_bytes += getCompactSizeLen(witness_bytes) vsize = self.getTxVSize(tx, add_witness_bytes=witness_bytes) @@ -555,7 +588,7 @@ class BTCInterface(CoinInterface): self._log.info('tx amount, vsize, feerate: %ld, %ld, %ld', locked_coin, vsize, fee_rate_paid) if not self.compareFeeRates(fee_rate_paid, feerate): - raise ValueError('Bad fee rate') + raise ValueError('Bad fee rate, expected: {}'.format(feerate)) return tx_hash, locked_coin @@ -592,7 +625,7 @@ class BTCInterface(CoinInterface): assert(fee_paid > 0) witness_bytes = len(prevout_script) - witness_bytes += 73 * 2 # 2 signatures (72 + 1 byts size) + witness_bytes += 74 * 2 # 2 signatures (72 + 1 byte sighashtype + 1 byte size) - Use maximum txn size for estimate witness_bytes += 4 # 1 empty, 1 true witness stack values witness_bytes += getCompactSizeLen(witness_bytes) vsize = self.getTxVSize(tx, add_witness_bytes=witness_bytes) @@ -601,7 +634,7 @@ class BTCInterface(CoinInterface): self._log.info('tx amount, vsize, feerate: %ld, %ld, %ld', tx_value, vsize, fee_rate_paid) if not self.compareFeeRates(fee_rate_paid, feerate): - raise ValueError('Bad fee rate') + raise ValueError('Bad fee rate, expected: {}'.format(feerate)) return True @@ -641,7 +674,7 @@ class BTCInterface(CoinInterface): witness_bytes = len(lock_tx_script) witness_bytes += 33 # sv, size - witness_bytes += 73 * 2 # 2 signatures (72 + 1 byts size) + witness_bytes += 74 * 2 # 2 signatures (72 + 1 byte sighashtype + 1 byte size) - Use maximum txn size for estimate witness_bytes += 4 # 1 empty, 1 true witness stack values witness_bytes += getCompactSizeLen(witness_bytes) vsize = self.getTxVSize(tx, add_witness_bytes=witness_bytes) @@ -650,7 +683,7 @@ class BTCInterface(CoinInterface): self._log.info('tx amount, vsize, feerate: %ld, %ld, %ld', tx.vout[0].nValue, vsize, fee_rate_paid) if not self.compareFeeRates(fee_rate_paid, feerate): - raise ValueError('Bad fee rate') + raise ValueError('Bad fee rate, expected: {}'.format(feerate)) return True diff --git a/basicswap/interface_xmr.py b/basicswap/interface_xmr.py index e84c6da..fc84fc9 100644 --- a/basicswap/interface_xmr.py +++ b/basicswap/interface_xmr.py @@ -69,7 +69,7 @@ class XMRInterface(CoinInterface): self._restore_height = coin_settings.get('restore_height', 0) self.setFeePriority(coin_settings.get('fee_priority', 0)) self._sc = swap_client - self._log = self._sc.log if self._sc.log else logging + self._log = self._sc.log if self._sc and self._sc.log else logging def setFeePriority(self, new_priority): assert(new_priority >= 0 and new_priority < 4), 'Invalid fee_priority value' diff --git a/basicswap/js_server.py b/basicswap/js_server.py index 638a6b0..77713b2 100644 --- a/basicswap/js_server.py +++ b/basicswap/js_server.py @@ -8,7 +8,6 @@ import json import urllib.parse from .util import ( - format8, format_timestamp, ) from .basicswap import ( @@ -174,7 +173,8 @@ def js_bids(self, url_split, post_string, is_json): 'bid_id': b[1].hex(), 'offer_id': b[2].hex(), 'created_at': format_timestamp(b[0]), - 'amount_from': format8(b[3]), + 'coin_from': b[8], + 'amount_from': swap_client.ci(b[8]).format_amount(b[3]), 'bid_state': strBidState(b[4]) } for b in bids]), 'UTF-8') diff --git a/basicswap/templates/bid_xmr.html b/basicswap/templates/bid_xmr.html index 609cd49..887ec59 100644 --- a/basicswap/templates/bid_xmr.html +++ b/basicswap/templates/bid_xmr.html @@ -23,6 +23,11 @@ Expired At{{ data.expired_at }} Sent{{ data.was_sent }} Received{{ data.was_received }} +{% if data.coin_a_lock_refund_tx_est_final != 'None' %} +{{ data.ticker_from }} lock refund tx valid at{{ data.coin_a_lock_refund_tx_est_final | formatts }} +{{ data.ticker_from }} chain median time{{ data.coin_a_last_median_time | formatts }} +{% endif %} +
diff --git a/basicswap/types.py b/basicswap/types.py new file mode 100644 index 0000000..645b24d --- /dev/null +++ b/basicswap/types.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2021 tecnovert +# Distributed under the MIT software license, see the accompanying +# file LICENSE or http://www.opensource.org/licenses/mit-license.php. + +SEQUENCE_LOCK_BLOCKS = 1 +SEQUENCE_LOCK_TIME = 2 +ABS_LOCK_BLOCKS = 3 +ABS_LOCK_TIME = 4 diff --git a/basicswap/ui.py b/basicswap/ui.py index d8c0a7f..c4c58c6 100644 --- a/basicswap/ui.py +++ b/basicswap/ui.py @@ -20,6 +20,9 @@ from .basicswap import ( strBidState, strTxState, ) +from .types import ( + SEQUENCE_LOCK_TIME, +) PAGE_LIMIT = 50 @@ -186,15 +189,15 @@ def describeBid(swap_client, bid, xmr_swap, offer, xmr_offer, bid_events, edit_b txns = [] if bid.xmr_a_lock_tx: confirms = None - if swap_client.coin_clients[ci_from.coin_type()]['last_height'] and bid.xmr_a_lock_tx.chain_height: - confirms = (swap_client.coin_clients[ci_from.coin_type()]['last_height'] - bid.xmr_a_lock_tx.chain_height) + 1 + if swap_client.coin_clients[ci_from.coin_type()]['chain_height'] and bid.xmr_a_lock_tx.chain_height: + confirms = (swap_client.coin_clients[ci_from.coin_type()]['chain_height'] - bid.xmr_a_lock_tx.chain_height) + 1 txns.append({'type': 'Chain A Lock', 'txid': bid.xmr_a_lock_tx.txid.hex(), 'confirms': confirms}) if bid.xmr_a_lock_spend_tx: txns.append({'type': 'Chain A Lock Spend', 'txid': bid.xmr_a_lock_spend_tx.txid.hex()}) if bid.xmr_b_lock_tx: confirms = None - if swap_client.coin_clients[ci_to.coin_type()]['last_height'] and bid.xmr_b_lock_tx.chain_height: - confirms = (swap_client.coin_clients[ci_to.coin_type()]['last_height'] - bid.xmr_b_lock_tx.chain_height) + 1 + if swap_client.coin_clients[ci_to.coin_type()]['chain_height'] and bid.xmr_b_lock_tx.chain_height: + confirms = (swap_client.coin_clients[ci_to.coin_type()]['chain_height'] - bid.xmr_b_lock_tx.chain_height) + 1 txns.append({'type': 'Chain B Lock', 'txid': bid.xmr_b_lock_tx.txid.hex(), 'confirms': confirms}) if bid.xmr_b_lock_tx and bid.xmr_b_lock_tx.spend_txid: txns.append({'type': 'Chain B Lock Spend', 'txid': bid.xmr_b_lock_tx.spend_txid.hex()}) @@ -214,6 +217,15 @@ def describeBid(swap_client, bid, xmr_swap, offer, xmr_offer, bid_events, edit_b data['participate_tx_spend'] = getTxSpendHex(bid, TxTypes.PTX) if offer.swap_type == SwapTypes.XMR_SWAP: + + data['coin_a_lock_refund_tx_est_final'] = 'None' + if bid.xmr_a_lock_tx and bid.xmr_a_lock_tx.block_time: + if offer.lock_type == SEQUENCE_LOCK_TIME: + raw_sequence = ci_from.getExpectedSequence(offer.lock_type, offer.lock_value) + seconds_locked = ci_from.decodeSequence(raw_sequence) + data['coin_a_lock_refund_tx_est_final'] = bid.xmr_a_lock_tx.block_time + seconds_locked + data['coin_a_last_median_time'] = swap_client.coin_clients[offer.coin_from]['chain_median_time'] + if view_tx_ind: data['view_tx_ind'] = view_tx_ind view_tx_id = bytes.fromhex(view_tx_ind) diff --git a/basicswap/util.py b/basicswap/util.py index 1cc653c..0793ed0 100644 --- a/basicswap/util.py +++ b/basicswap/util.py @@ -27,16 +27,6 @@ def assert_cond(v, err='Bad opcode'): raise ValueError(err) -def format8(i): - n = abs(i) - quotient = n // COIN - remainder = n % COIN - rv = "%d.%08d" % (quotient, remainder) - if i < 0: - rv = '-' + rv - return rv - - def toBool(s): return s.lower() in ["1", "true"] diff --git a/bin/basicswap_prepare.py b/bin/basicswap_prepare.py index 0771b86..90b49c1 100755 --- a/bin/basicswap_prepare.py +++ b/bin/basicswap_prepare.py @@ -205,7 +205,7 @@ def prepareCore(coin, version, settings, data_dir): assert_url = 'https://raw.githubusercontent.com/tecnovert/gitian.sigs/master/%s-%s/%s/%s' % (version, os_dir_name, signing_key_name, assert_filename) elif coin == 'litecoin': signing_key_name = 'thrasher' - release_url = 'https://download.litecoin.org/litecoin-{}/{}/{}'.format(version, os_name, release_filename) + release_url = 'https://download2.litecoin.org/litecoin-{}/{}/{}'.format(version, os_name, release_filename) assert_filename = '{}-{}-{}-build.assert'.format(coin, os_name, version.rsplit('.', 1)[0]) assert_url = 'https://raw.githubusercontent.com/litecoin-project/gitian.sigs.ltc/master/%s-%s/%s/%s' % (version, os_dir_name, signing_key_name, assert_filename) elif coin == 'bitcoin': diff --git a/tests/basicswap/test_other.py b/tests/basicswap/test_other.py index c82f379..014c704 100644 --- a/tests/basicswap/test_other.py +++ b/tests/basicswap/test_other.py @@ -30,10 +30,7 @@ from basicswap.util import ( make_int, format_amount, validate_amount) -from basicswap.basicswap import ( - Coins, - getExpectedSequence, - decodeSequence, +from basicswap.types import ( SEQUENCE_LOCK_BLOCKS, SEQUENCE_LOCK_TIME) @@ -57,21 +54,24 @@ class Test(unittest.TestCase): test_case(4194642) def test_sequence(self): + coin_settings = {'rpcport': 0, 'rpcauth': 'none', 'blocks_confirmed': 1, 'conf_target': 1} + ci = BTCInterface(coin_settings, 'regtest') + time_val = 48 * 60 * 60 - encoded = getExpectedSequence(SEQUENCE_LOCK_TIME, time_val, Coins.PART) - decoded = decodeSequence(encoded) + encoded = ci.getExpectedSequence(SEQUENCE_LOCK_TIME, time_val) + decoded = ci.decodeSequence(encoded) assert(decoded >= time_val) assert(decoded <= time_val + 512) time_val = 24 * 60 - encoded = getExpectedSequence(SEQUENCE_LOCK_TIME, time_val, Coins.PART) - decoded = decodeSequence(encoded) + encoded = ci.getExpectedSequence(SEQUENCE_LOCK_TIME, time_val) + decoded = ci.decodeSequence(encoded) assert(decoded >= time_val) assert(decoded <= time_val + 512) blocks_val = 123 - encoded = getExpectedSequence(SEQUENCE_LOCK_BLOCKS, blocks_val, Coins.PART) - decoded = decodeSequence(encoded) + encoded = ci.getExpectedSequence(SEQUENCE_LOCK_BLOCKS, blocks_val) + decoded = ci.decodeSequence(encoded) assert(decoded == blocks_val) def test_make_int(self): diff --git a/tests/basicswap/test_run.py b/tests/basicswap/test_run.py index 4403945..ed01bc3 100644 --- a/tests/basicswap/test_run.py +++ b/tests/basicswap/test_run.py @@ -554,14 +554,14 @@ class Test(unittest.TestCase): wait_for_bid(delay_event, swap_clients[0], bid_id) swap_clients[0].acceptBid(bid_id) - swap_clients[0].coin_clients[Coins.BTC]['override_feerate'] = 10.0 - swap_clients[0].coin_clients[Coins.LTC]['override_feerate'] = 10.0 + swap_clients[0].getChainClientSettings(Coins.BTC)['override_feerate'] = 10.0 + swap_clients[0].getChainClientSettings(Coins.LTC)['override_feerate'] = 10.0 wait_for_bid(delay_event, swap_clients[0], bid_id, BidStates.BID_ERROR, wait_for=60) swap_clients[0].abandonBid(bid_id) - del swap_clients[0].coin_clients[Coins.BTC]['override_feerate'] - del swap_clients[0].coin_clients[Coins.LTC]['override_feerate'] + del swap_clients[0].getChainClientSettings(Coins.BTC)['override_feerate'] + del swap_clients[0].getChainClientSettings(Coins.LTC)['override_feerate'] def test_08_part_ltc_buyer_first(self): logging.info('---------- Test PART to LTC, buyer first') diff --git a/tests/basicswap/test_xmr.py b/tests/basicswap/test_xmr.py index e11c773..ef1654b 100644 --- a/tests/basicswap/test_xmr.py +++ b/tests/basicswap/test_xmr.py @@ -515,7 +515,7 @@ class Test(unittest.TestCase): swap_clients[0].acceptXmrBid(bid_id) - wait_for_bid(delay_event, swap_clients[0], bid_id, BidStates.BID_ABANDONED, wait_for=180) + wait_for_bid(delay_event, swap_clients[0], bid_id, BidStates.BID_STALLED_FOR_TEST, wait_for=180) wait_for_bid(delay_event, swap_clients[1], bid_id, BidStates.XMR_SWAP_FAILED_SWIPED, wait_for=80, sent=True) js_w0_after = json.loads(urlopen('http://127.0.0.1:1800/json/wallets').read())