Add more event log types.

Auto accept only bids of the exact offer amount.
Retry sending lock B refund tx.
2024-05-20_merge
tecnovert 4 years ago
parent a27cfcba0f
commit c66160fb09
No known key found for this signature in database
GPG Key ID: 8ED6D8750C4E3F93
  1. 2
      basicswap/__init__.py
  2. 90
      basicswap/basicswap.py
  3. 4
      basicswap/interface_btc.py
  4. 2
      basicswap/interface_xmr.py
  5. 29
      tests/basicswap/extended/test_xmr_persistent.py
  6. 1
      tests/basicswap/test_xmr.py

@ -1,3 +1,3 @@
name = "basicswap"
__version__ = "0.0.15"
__version__ = "0.0.16"

@ -200,6 +200,12 @@ class EventLogTypes(IntEnum):
LOCK_TX_B_SEEN = auto()
LOCK_TX_B_CONFIRMED = auto()
DEBUG_TWEAK_APPLIED = auto()
FAILED_TX_B_REFUND = auto()
LOCK_TX_B_INVALID = auto()
LOCK_TX_A_REFUND_TX_PUBLISHED = auto()
LOCK_TX_A_REFUND_SPEND_TX_PUBLISHED = auto()
LOCK_TX_A_REFUND_SWIPE_TX_PUBLISHED = auto()
LOCK_TX_B_REFUND_TX_PUBLISHED = auto()
class XmrSplitMsgTypes(IntEnum):
@ -327,25 +333,37 @@ def getLockName(lock_type):
def describeEventEntry(event_type, event_msg):
if event_type == EventLogTypes.FAILED_TX_B_LOCK_PUBLISH:
return 'Failed to publish lock tx b'
return 'Failed to publish lock tx B'
if event_type == EventLogTypes.FAILED_TX_B_LOCK_PUBLISH:
return 'Failed to publish lock tx b'
return 'Failed to publish lock tx B'
if event_type == EventLogTypes.LOCK_TX_A_PUBLISHED:
return 'Lock tx a published'
return 'Lock tx A published'
if event_type == EventLogTypes.LOCK_TX_B_PUBLISHED:
return 'Lock tx b published'
return 'Lock tx B published'
if event_type == EventLogTypes.FAILED_TX_B_SPEND:
return 'Failed to publish lock tx b spend'
return 'Failed to publish lock tx B spend'
if event_type == EventLogTypes.LOCK_TX_A_SEEN:
return 'Lock tx a seen in chain'
return 'Lock tx A seen in chain'
if event_type == EventLogTypes.LOCK_TX_A_CONFIRMED:
return 'Lock tx a confirmed in chain'
return 'Lock tx A confirmed in chain'
if event_type == EventLogTypes.LOCK_TX_B_SEEN:
return 'Lock tx b seen in chain'
return 'Lock tx B seen in chain'
if event_type == EventLogTypes.LOCK_TX_B_CONFIRMED:
return 'Lock tx b confirmed in chain'
return 'Lock tx B confirmed in chain'
if event_type == EventLogTypes.DEBUG_TWEAK_APPLIED:
return 'Debug tweak applied ' + event_msg
if event_type == EventLogTypes.FAILED_TX_B_REFUND:
return 'Failed to publish lock tx B refund'
if event_type == EventLogTypes.LOCK_TX_B_INVALID:
return 'Detected invalid lock Tx B'
if event_type == EventLogTypes.LOCK_TX_A_REFUND_TX_PUBLISHED:
return 'Lock tx A refund tx published'
if event_type == EventLogTypes.LOCK_TX_A_REFUND_SPEND_TX_PUBLISHED:
return 'Lock tx A refund spend tx published'
if event_type == EventLogTypes.LOCK_TX_A_REFUND_SWIPE_TX_PUBLISHED:
return 'Lock tx A refund swipe tx published'
if event_type == EventLogTypes.LOCK_TX_B_REFUND_TX_PUBLISHED:
return 'Lock tx B refund tx published'
def getExpectedSequence(lockType, lockVal, coin_type):
@ -2693,6 +2711,7 @@ class BasicSwap(BaseApp):
if TxTypes.XMR_SWAP_A_LOCK_REFUND_SPEND not in bid.txns:
try:
txid = ci_from.publishTx(xmr_swap.a_lock_refund_spend_tx)
self.logBidEvent(bid, EventLogTypes.LOCK_TX_A_REFUND_SPEND_TX_PUBLISHED, '', session)
self.log.info('Submitted coin a lock refund spend tx for bid {}'.format(bid_id.hex()))
bid.txns[TxTypes.XMR_SWAP_A_LOCK_REFUND_SPEND] = SwapTx(
@ -2714,6 +2733,7 @@ class BasicSwap(BaseApp):
if TxTypes.XMR_SWAP_A_LOCK_REFUND_SWIPE not in bid.txns:
try:
txid = ci_from.publishTx(xmr_swap.a_lock_refund_swipe_tx)
self.logBidEvent(bid, EventLogTypes.LOCK_TX_A_REFUND_SWIPE_TX_PUBLISHED, '', session)
self.log.info('Submitted coin a lock refund swipe tx for bid {}'.format(bid_id.hex()))
bid.txns[TxTypes.XMR_SWAP_A_LOCK_REFUND_SWIPE] = SwapTx(
bid_id=bid_id,
@ -2742,6 +2762,7 @@ class BasicSwap(BaseApp):
txid = ci_from.publishTx(xmr_swap.a_lock_refund_tx)
self.log.info('Submitted coin a lock refund tx for bid {}'.format(bid_id.hex()))
self.logBidEvent(bid, EventLogTypes.LOCK_TX_A_REFUND_TX_PUBLISHED, '', session)
bid.txns[TxTypes.XMR_SWAP_A_LOCK_REFUND] = SwapTx(
bid_id=bid_id,
tx_type=TxTypes.XMR_SWAP_A_LOCK_REFUND,
@ -2789,6 +2810,13 @@ class BasicSwap(BaseApp):
utxo = utxos[0]
if not bid.xmr_a_lock_tx.chain_height and utxo['height'] != 0:
self.logBidEvent(bid, EventLogTypes.LOCK_TX_A_SEEN, '', session)
block_header = ci_from.getBlockHeaderFromHeight(utxo['height'])
bid.xmr_a_lock_tx.block_hash = bytes.fromhex(block_header['hash'])
bid.xmr_a_lock_tx.block_height = block_header['height']
bid.xmr_a_lock_tx.block_time = block_header['time'] # Or median_time?
bid_changed = True
if bid.xmr_a_lock_tx.chain_height != utxo['height'] and utxo['height'] != 0:
bid.xmr_a_lock_tx.chain_height = utxo['height']
@ -2817,7 +2845,12 @@ class BasicSwap(BaseApp):
bid_changed = False
# Have to use findTxB instead of relying on the first seen height to detect chain reorgs
found_tx = ci_to.findTxB(xmr_swap.vkbv, xmr_swap.pkbs, bid.amount_to, ci_to.blocks_confirmed, xmr_swap.b_restore_height)
if found_tx is not None:
if isinstance(found_tx, int) and found_tx == -1:
if self.countBidEvents(bid, EventLogTypes.LOCK_TX_B_INVALID, session) < 1:
self.logBidEvent(bid, EventLogTypes.LOCK_TX_B_INVALID, 'Detected invalid lock tx B', session)
bid_changed = True
elif found_tx is not None:
if bid.xmr_b_lock_tx is None or not bid.xmr_b_lock_tx.chain_height:
self.logBidEvent(bid, EventLogTypes.LOCK_TX_B_SEEN, '', session)
if bid.xmr_b_lock_tx is None:
@ -3581,6 +3614,7 @@ class BasicSwap(BaseApp):
assert(msg['to'] == offer.addr_from), 'Received on incorrect address'
assert(now <= offer.expire_at), 'Offer expired'
assert(bid_data.amount >= offer.min_bid_amount), 'Bid amount below minimum'
assert(bid_data.amount <= offer.amount_from), 'Bid amount above offer amount'
assert(now <= msg['sent'] + bid_data.time_valid), 'Bid expired'
# TODO: Allow higher bids
@ -3646,6 +3680,8 @@ class BasicSwap(BaseApp):
if offer.auto_accept_bids:
if self.countAcceptedBids(offer_id) > 0:
self.log.info('Not auto accepting bid %s, already have', bid_id.hex())
elif bid_data.amount != offer.amount_from:
self.log.info('Not auto accepting bid %s, want exact amount match', bid_id.hex())
else:
delay = random.randrange(self.min_delay_event, self.max_delay_event)
self.log.info('Auto accepting bid %s in %d seconds', bid_id.hex(), delay)
@ -3766,6 +3802,8 @@ class BasicSwap(BaseApp):
if offer.auto_accept_bids:
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())
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)
@ -3842,7 +3880,15 @@ class BasicSwap(BaseApp):
ci_from = self.ci(Coins(offer.coin_from))
ci_to = self.ci(Coins(offer.coin_to))
assert(offer.state == OfferStates.OFFER_RECEIVED), 'Bad offer state'
assert(msg['to'] == offer.addr_from), 'Received on incorrect address'
assert(now <= offer.expire_at), 'Offer expired'
assert(bid_data.amount >= offer.min_bid_amount), 'Bid amount below minimum'
assert(bid_data.amount <= offer.amount_from), 'Bid amount above offer amount'
assert(now <= msg['sent'] + bid_data.time_valid), 'Bid expired'
self.log.debug('TODO: xmr bid validation')
assert(ci_to.verifyKey(bid_data.kbvf))
assert(ci_from.verifyPubkey(bid_data.pkaf))
@ -4310,7 +4356,29 @@ class BasicSwap(BaseApp):
address_to = ci_to.getMainWalletAddress()
txid = ci_to.spendBLockTx(address_to, xmr_swap.vkbv, vkbs, bid.amount_to, xmr_offer.b_fee_rate, xmr_swap.b_restore_height)
try:
txid = ci_to.spendBLockTx(address_to, xmr_swap.vkbv, vkbs, bid.amount_to, xmr_offer.b_fee_rate, xmr_swap.b_restore_height)
self.log.debug('Submitted lock B refund txn %s to %s chain for bid %s', txid.hex(), ci_to.coin_name(), bid_id.hex())
self.logBidEvent(bid, EventLogTypes.LOCK_TX_B_REFUND_TX_PUBLISHED, '', session)
except Exception as ex:
# TODO: Make min-conf 10?
error_msg = 'spendBLockTx refund failed for bid {} with error {}'.format(bid_id.hex(), str(ex))
num_retries = self.countBidEvents(bid, EventLogTypes.FAILED_TX_B_REFUND, session)
if num_retries > 0:
error_msg += ', retry no. {}'.format(num_retries)
self.log.error(error_msg)
str_error = str(ex)
if num_retries < 100 and 'Invalid unlocked_balance' in str_error:
delay = random.randrange(self.min_delay_retry, self.max_delay_retry)
self.log.info('Retrying sending xmr swap chain B refund tx for bid %s in %d seconds', bid_id.hex(), delay)
self.createEventInSession(delay, EventTypes.RECOVER_XMR_SWAP_LOCK_TX_B, bid_id, session)
else:
self.setBidError(bid_id, bid, 'spendBLockTx for refund failed: ' + str(ex), save_bid=False)
self.saveBidInSession(bid_id, bid, session, xmr_swap, save_in_progress=offer)
self.logBidEvent(bid, EventLogTypes.FAILED_TX_B_REFUND, str_error, session)
return
bid.xmr_b_lock_tx.spend_txid = txid

@ -150,6 +150,10 @@ class BTCInterface(CoinInterface):
def getMempoolTx(self, txid):
return self.rpc_callback('getrawtransaction', [txid.hex()])
def getBlockHeaderFromHeight(self, height):
block_hash = self.rpc_callback('getblockhash', [height])
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)

@ -268,7 +268,7 @@ class XMRInterface(CoinInterface):
return {'txid': transfer['tx_hash'], 'amount': transfer['amount'], 'height': 0 if 'block_height' not in transfer else transfer['block_height']}
else:
self._log.warning('Incorrect amount detected for coin b lock txn: {}'.format(transfer['tx_hash']))
return -1
return None
def waitForLockTxB(self, kbv, Kbs, cb_swap_value, cb_block_confirmed, restore_height):

@ -101,11 +101,19 @@ def updateThread(cls):
try:
if cls.btc_addr is not None:
callbtcrpc(0, 'generatetoaddress', [1, cls.btc_addr])
except Exception as e:
print('updateThread error', str(e))
cls.delay_event.wait(random.randrange(cls.update_min, cls.update_max))
def updateThreadXmr(cls):
while not cls.delay_event.is_set():
try:
if cls.xmr_addr is not None:
callrpc_xmr_na(XMR_BASE_RPC_PORT + 1, 'generateblocks', {'wallet_address': cls.xmr_addr, 'amount_of_blocks': 1})
except Exception as e:
print('updateThread error', str(e))
cls.delay_event.wait(random.randrange(1, 4))
print('updateThreadXmr error', str(e))
cls.delay_event.wait(random.randrange(cls.xmr_update_min, cls.xmr_update_max))
class Test(unittest.TestCase):
@ -113,8 +121,15 @@ class Test(unittest.TestCase):
def setUpClass(cls):
super(Test, cls).setUpClass()
cls.update_min = int(os.getenv('UPDATE_THREAD_MIN_WAIT', '1'))
cls.update_max = cls.update_min * 4
cls.xmr_update_min = int(os.getenv('XMR_UPDATE_THREAD_MIN_WAIT', '1'))
cls.xmr_update_max = cls.xmr_update_min * 4
cls.delay_event = threading.Event()
cls.update_thread = None
cls.update_thread_xmr = None
cls.processes = []
cls.btc_addr = None
cls.xmr_addr = None
@ -204,8 +219,8 @@ class Test(unittest.TestCase):
settings['min_delay_event'] = 1
settings['max_delay_event'] = 4
settings['min_delay_retry'] = 10
settings['max_delay_retry'] = 20
settings['min_delay_retry'] = 15
settings['max_delay_retry'] = 30
settings['min_sequence_lock_seconds'] = 60
settings['check_progress_seconds'] = 5
@ -270,6 +285,9 @@ class Test(unittest.TestCase):
self.update_thread = threading.Thread(target=updateThread, args=(self,))
self.update_thread.start()
self.update_thread_xmr = threading.Thread(target=updateThreadXmr, args=(self,))
self.update_thread_xmr.start()
# Wait for height, or sequencelock is thrown off by genesis blocktime
num_blocks = 3
logging.info('Waiting for Particl chain height %d', num_blocks)
@ -291,11 +309,14 @@ class Test(unittest.TestCase):
cls.delay_event.set()
if cls.update_thread:
cls.update_thread.join()
if cls.update_thread_xmr:
cls.update_thread_xmr.join()
for p in cls.processes:
p.terminate()
for p in cls.processes:
p.join()
cls.update_thread = None
cls.update_thread_xmr = None
cls.processes = []
def test_persistent(self):

@ -632,7 +632,6 @@ class Test(unittest.TestCase):
assert(make_int(js_w1_after['2']['balance'], scale=8, r=1) - (make_int(js_w1_before['2']['balance'], scale=8, r=1) + amt_1) < 1000)
def test_07_revoke_offer(self):
logging.info('---------- Test offer revocaction')
swap_clients = self.swap_clients

Loading…
Cancel
Save