Decred xmr swap tests.

master^2
tecnovert 7 months ago
parent 76879a2ff5
commit 2a8c04b285
  1. 178
      basicswap/basicswap.py
  2. 1
      basicswap/basicswap_util.py
  3. 5
      basicswap/db.py
  4. 65
      basicswap/interface/base.py
  5. 247
      basicswap/interface/btc.py
  6. 2
      basicswap/interface/dash.py
  7. 683
      basicswap/interface/dcr/dcr.py
  8. 9
      basicswap/interface/dcr/messages.py
  9. 9
      basicswap/interface/dcr/rpc.py
  10. 9
      basicswap/interface/dcr/script.py
  11. 4
      basicswap/interface/firo.py
  12. 20
      basicswap/interface/nav.py
  13. 17
      basicswap/interface/part.py
  14. 2
      basicswap/interface/pivx.py
  15. 2
      basicswap/interface/xmr.py
  16. 13
      basicswap/protocols/xmr_swap_1.py
  17. 5
      tests/basicswap/common.py
  18. 267
      tests/basicswap/extended/test_dcr.py
  19. 2
      tests/basicswap/test_btc_xmr.py
  20. 20
      tests/basicswap/test_run.py
  21. 2
      tests/basicswap/test_xmr.py

@ -1153,14 +1153,26 @@ class BasicSwap(BaseApp):
raise ValueError('Offer not found')
self.loadBidTxns(bid, session)
coin_from = Coins(offer.coin_from)
coin_to = Coins(offer.coin_to)
if offer.swap_type == SwapTypes.XMR_SWAP:
xmr_swap = session.query(XmrSwap).filter_by(bid_id=bid.bid_id).first()
self.watchXmrSwap(bid, offer, xmr_swap)
if coin_to.watch_blocks_for_scripts() and bid.xmr_a_lock_tx and bid.xmr_a_lock_tx.chain_height:
if not bid.xmr_b_lock_tx or not bid.xmr_b_lock_tx.txid:
ci_from = self.ci(coin_from)
ci_to = self.ci(coin_to)
chain_a_block_header = ci_from.getBlockHeaderFromHeight(bid.xmr_a_lock_tx.chain_height)
block_time = chain_a_block_header['time']
chain_b_block_header = ci_to.getBlockHeaderAt(block_time)
dest_script = ci_to.getPkDest(xmr_swap.pkbs)
self.addWatchedScript(ci_to.coin_type(), bid.bid_id, dest_script, TxTypes.XMR_SWAP_B_LOCK)
self.setLastHeightCheckedStart(ci_to.coin_type(), chain_b_block_header['height'])
else:
self.swaps_in_progress[bid.bid_id] = (bid, offer)
coin_from = Coins(offer.coin_from)
coin_to = Coins(offer.coin_to)
if bid.initiate_tx and bid.initiate_tx.txid:
self.addWatchedOutput(coin_from, bid.bid_id, bid.initiate_tx.txid.hex(), bid.initiate_tx.vout, BidStates.SWAP_INITIATED)
if bid.participate_tx and bid.participate_tx.txid:
@ -1632,16 +1644,12 @@ class BasicSwap(BaseApp):
if swap_type == SwapTypes.XMR_SWAP:
xmr_offer = XmrOffer()
if reverse_bid:
# Delay before the chain a lock refund tx can be mined
xmr_offer.lock_time_1 = ci_to.getExpectedSequence(lock_type, lock_value)
# Delay before the follower can spend from the chain a lock refund tx
xmr_offer.lock_time_2 = ci_to.getExpectedSequence(lock_type, lock_value)
else:
# Delay before the chain a lock refund tx can be mined
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 = ci_from.getExpectedSequence(lock_type, lock_value)
chain_a_ci = ci_to if reverse_bid else ci_from
lock_value_2 = lock_value + 1000 if (None, DebugTypes.OFFER_LOCK_2_VALUE_INC) in self._debug_cases else lock_value
# Delay before the chain a lock refund tx can be mined
xmr_offer.lock_time_1 = chain_a_ci.getExpectedSequence(lock_type, lock_value)
# Delay before the follower can spend from the chain a lock refund tx
xmr_offer.lock_time_2 = chain_a_ci.getExpectedSequence(lock_type, lock_value_2)
xmr_offer.a_fee_rate = msg_buf.fee_rate_from
xmr_offer.b_fee_rate = msg_buf.fee_rate_to # Unused: TODO - Set priority?
@ -2643,8 +2651,9 @@ class BasicSwap(BaseApp):
txid = ci_from.publishTx(bid.initiate_txn_refund)
self.log.error('Submit refund_txn unexpectedly worked: ' + txid)
except Exception as ex:
if 'non-BIP68-final' not in str(ex) and 'non-final' not in str(ex):
if ci_from.isTxNonFinalError(str(ex)) is False:
self.log.error('Submit refund_txn unexpected error' + str(ex))
raise ex
if txid is not None:
msg_buf = BidAcceptMessage()
@ -2654,7 +2663,6 @@ class BasicSwap(BaseApp):
# pkh sent in script is hashed with sha256, Decred expects blake256
if bid.pkhash_seller != pkhash_refund:
assert (ci_to.coin_type() == Coins.DCR or ci_from.coin_type() == Coins.DCR) # [rm]
msg_buf.pkhash_seller = bid.pkhash_seller
bid_bytes = msg_buf.SerializeToString()
@ -2785,7 +2793,7 @@ class BasicSwap(BaseApp):
msg_buf.amount_to = amount_to
address_out = self.getReceiveAddressFromPool(coin_from, offer_id, TxTypes.XMR_SWAP_A_LOCK)
if coin_from == Coins.PART_BLIND:
if coin_from in (Coins.PART_BLIND, ):
addrinfo = ci_from.rpc('getaddressinfo', [address_out])
msg_buf.dest_af = bytes.fromhex(addrinfo['pubkey'])
else:
@ -3025,7 +3033,7 @@ class BasicSwap(BaseApp):
msg_buf.a_lock_tx = xmr_swap.a_lock_tx
msg_buf.a_lock_tx_script = xmr_swap.a_lock_tx_script
msg_buf.a_lock_refund_tx = xmr_swap.a_lock_refund_tx
msg_buf.a_lock_refund_tx_script = xmr_swap.a_lock_refund_tx_script
msg_buf.a_lock_refund_tx_script = bytes(xmr_swap.a_lock_refund_tx_script)
msg_buf.a_lock_refund_spend_tx = xmr_swap.a_lock_refund_spend_tx
msg_buf.al_lock_refund_tx_sig = xmr_swap.al_lock_refund_tx_sig
@ -3327,9 +3335,6 @@ class BasicSwap(BaseApp):
secret = self.getContractSecret(bid_date, bid.contract_count)
ensure(len(secret) == 32, 'Bad secret length')
self.log.debug('secret {}'.format(secret.hex()))
self.log.debug('sha256(secret) {}'.format(sha256(secret).hex()))
if self.coin_clients[coin_type]['connection_type'] != 'rpc':
return None
@ -3475,14 +3480,7 @@ class BasicSwap(BaseApp):
else:
privkey_wif = self.ci(Coins.PART).encodeKey(privkey)
refund_sig = self.callcoinrpc(Coins.PART, 'createsignaturewithkey', [refund_txn, prevout, privkey_wif, 'ALL', options])
if coin_type in (Coins.DCR, ):
witness_stack = [
bytes.fromhex(refund_sig),
pubkey,
(OpCodes.OP_0).to_bytes(1),
txn_script]
refund_txn = ci.setTxSignature(bytes.fromhex(refund_txn), witness_stack).hex()
elif coin_type in (Coins.PART, ) or self.coin_clients[coin_type]['use_segwit']:
if coin_type in (Coins.PART, Coins.DCR) or self.coin_clients[coin_type]['use_segwit']:
witness_stack = [
bytes.fromhex(refund_sig),
pubkey,
@ -3706,8 +3704,18 @@ class BasicSwap(BaseApp):
def findTxB(self, ci_to, xmr_swap, bid, session, bid_sender: bool) -> bool:
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, bid.chain_b_height_start, bid_sender)
found_tx = None
if ci_to.coin_type() in (Coins.DCR, ):
if bid.xmr_b_lock_tx is None or bid.xmr_b_lock_tx.txid is None:
# Watching chain for dest_address with WatchedScript
pass
else:
dest_address = ci_to.pkh_to_address(ci_to.pkh(xmr_swap.pkbs))
found_tx = ci_to.getLockTxHeight(bid.xmr_b_lock_tx.txid, dest_address, bid.amount_to, bid.chain_b_height_start, vout=bid.xmr_b_lock_tx.vout)
else:
# 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, bid.chain_b_height_start, bid_sender)
if isinstance(found_tx, int) and found_tx == -1:
if self.countBidEvents(bid, EventLogTypes.LOCK_TX_B_INVALID, session) < 1:
@ -3716,20 +3724,18 @@ class BasicSwap(BaseApp):
elif found_tx is not None:
if found_tx['height'] != 0 and (bid.xmr_b_lock_tx is None or not bid.xmr_b_lock_tx.chain_height):
self.logBidEvent(bid.bid_id, EventLogTypes.LOCK_TX_B_SEEN, '', session)
if bid.xmr_b_lock_tx is None:
if bid.xmr_b_lock_tx is None or bid.xmr_b_lock_tx.chain_height is None:
self.log.debug('Found {} lock tx in chain'.format(ci_to.coin_name()))
xmr_swap.b_lock_tx_id = bytes.fromhex(found_tx['txid'])
if bid.xmr_b_lock_tx is None:
bid.xmr_b_lock_tx = SwapTx(
bid_id=bid.bid_id,
tx_type=TxTypes.XMR_SWAP_B_LOCK,
txid=xmr_swap.b_lock_tx_id,
chain_height=found_tx['height'],
)
bid_changed = True
bid.xmr_b_lock_tx.setState(TxStates.TX_IN_CHAIN)
else:
bid.xmr_b_lock_tx.chain_height = found_tx['height']
bid_changed = True
bid.xmr_b_lock_tx.chain_height = found_tx['height']
bid_changed = True
return bid_changed
def checkXmrBidState(self, bid_id: bytes, bid, offer):
@ -3785,7 +3791,7 @@ class BasicSwap(BaseApp):
self.saveBidInSession(bid_id, bid, session, xmr_swap)
session.commit()
if TxTypes.XMR_SWAP_A_LOCK_REFUND_SWIPE not in bid.txns and BidStates(bid.state) != BidStates.BID_STALLED_FOR_TEST:
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.bid_id, EventLogTypes.LOCK_TX_A_REFUND_SWIPE_TX_PUBLISHED, '', session)
@ -3827,7 +3833,7 @@ class BasicSwap(BaseApp):
session.commit()
return rv
except Exception as ex:
if 'Transaction already in block chain' in str(ex):
if ci_from.isTxExistsError(str(ex)):
self.log.info('Found coin a lock refund tx for bid {}'.format(bid_id.hex()))
txid = ci_from.getTxid(xmr_swap.a_lock_refund_tx)
if TxTypes.XMR_SWAP_A_LOCK_REFUND not in bid.txns:
@ -3882,6 +3888,7 @@ class BasicSwap(BaseApp):
if lock_tx_chain_info['depth'] >= ci_from.blocks_confirmed:
self.logBidEvent(bid.bid_id, EventLogTypes.LOCK_TX_A_CONFIRMED, '', session)
bid.xmr_a_lock_tx.setState(TxStates.TX_CONFIRMED)
bid.setState(BidStates.XMR_SWAP_SCRIPT_COIN_LOCKED)
bid_changed = True
@ -3890,6 +3897,13 @@ class BasicSwap(BaseApp):
self.log.info('Sending adaptor-sig swap chain B lock tx for bid %s in %d seconds', bid_id.hex(), delay)
self.createActionInSession(delay, ActionTypes.SEND_XMR_SWAP_LOCK_TX_B, bid_id, session)
# bid.setState(BidStates.SWAP_DELAYING)
elif ci_to.watch_blocks_for_scripts():
chain_a_block_header = ci_from.getBlockHeaderFromHeight(bid.xmr_a_lock_tx.chain_height)
block_time = chain_a_block_header['time']
chain_b_block_header = ci_to.getBlockHeaderAt(block_time)
dest_script = ci_to.getPkDest(xmr_swap.pkbs)
self.addWatchedScript(ci_to.coin_type(), bid.bid_id, dest_script, TxTypes.XMR_SWAP_B_LOCK)
self.setLastHeightCheckedStart(ci_to.coin_type(), chain_b_block_header['height'])
if bid_changed:
self.saveBidInSession(bid_id, bid, session, xmr_swap)
@ -4131,7 +4145,7 @@ class BasicSwap(BaseApp):
self.logEvent(Concepts.BID, bid.bid_id, EventLogTypes.ITX_REFUND_PUBLISHED, '', None)
# State will update when spend is detected
except Exception as ex:
if 'non-BIP68-final' not in str(ex) and 'non-final' not in str(ex):
if ci_from.isTxNonFinalError(str(ex)) is False:
self.log.warning('Error trying to submit initiate refund txn: %s', str(ex))
if bid.getPTxState() in (TxStates.TX_SENT, TxStates.TX_CONFIRMED) \
@ -4142,7 +4156,7 @@ class BasicSwap(BaseApp):
self.logEvent(Concepts.BID, bid.bid_id, EventLogTypes.PTX_REFUND_PUBLISHED, '', None)
# State will update when spend is detected
except Exception as ex:
if 'non-BIP68-final' not in str(ex) and 'non-final' not in str(ex) and 'locks on inputs not met' not in str(ex):
if ci_to.isTxNonFinalError(str(ex)):
self.log.warning('Error trying to submit participate refund txn: %s', str(ex))
return False # Bid is still active
@ -4298,6 +4312,7 @@ class BasicSwap(BaseApp):
state = BidStates(bid.state)
spending_txid = bytes.fromhex(spend_txid_hex)
bid.xmr_a_lock_tx.spend_txid = spending_txid
if spending_txid == xmr_swap.a_lock_spend_tx_id:
if state == BidStates.XMR_SWAP_LOCK_RELEASED:
xmr_swap.a_lock_spend_tx = bytes.fromhex(spend_txn_hex)
@ -4416,6 +4431,16 @@ class BasicSwap(BaseApp):
self.saveBid(watched_script.bid_id, bid)
else:
self.log.warning('Could not find active bid for found watched script: {}'.format(watched_script.bid_id.hex()))
elif watched_script.tx_type == TxTypes.XMR_SWAP_B_LOCK:
bid = self.swaps_in_progress[watched_script.bid_id][0]
bid.xmr_b_lock_tx = SwapTx(
bid_id=watched_script.bid_id,
tx_type=TxTypes.XMR_SWAP_B_LOCK,
txid=txid,
vout=vout,
)
bid.xmr_b_lock_tx.setState(TxStates.TX_IN_CHAIN)
self.saveBid(watched_script.bid_id, bid)
else:
self.log.warning('Unknown found watched script tx type for bid {}'.format(watched_script.bid_id.hex()))
@ -4852,12 +4877,12 @@ class BasicSwap(BaseApp):
xmr_offer.offer_id = offer_id
if reverse_bid:
xmr_offer.lock_time_1 = ci_to.getExpectedSequence(offer_data.lock_type, offer_data.lock_value)
xmr_offer.lock_time_2 = ci_to.getExpectedSequence(offer_data.lock_type, offer_data.lock_value)
else:
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)
chain_a_ci = ci_to if reverse_bid else ci_from
lock_value_2 = offer_data.lock_value
if (None, DebugTypes.OFFER_LOCK_2_VALUE_INC) in self._debug_cases:
lock_value_2 += 1000
xmr_offer.lock_time_1 = chain_a_ci.getExpectedSequence(offer_data.lock_type, offer_data.lock_value)
xmr_offer.lock_time_2 = chain_a_ci.getExpectedSequence(offer_data.lock_type, lock_value_2)
xmr_offer.a_fee_rate = offer_data.fee_rate_from
xmr_offer.b_fee_rate = offer_data.fee_rate_to
@ -5618,6 +5643,8 @@ class BasicSwap(BaseApp):
a_fee_rate, xmr_swap.vkbv)
xmr_swap.a_lock_spend_tx_id = ci_from.getTxid(xmr_swap.a_lock_spend_tx)
if bid.xmr_a_lock_tx:
bid.xmr_a_lock_tx.spend_txid = xmr_swap.a_lock_spend_tx_id
prevout_amount = ci_from.getLockTxSwapOutputValue(bid, xmr_swap)
xmr_swap.al_lock_spend_tx_esig = ci_from.signTxOtVES(kal, xmr_swap.pkasf, xmr_swap.a_lock_spend_tx, 0, xmr_swap.a_lock_tx_script, prevout_amount)
'''
@ -5628,33 +5655,37 @@ class BasicSwap(BaseApp):
xmr_swap.a_lock_tx, xmr_swap.a_lock_tx_script,
xmr_swap.dest_af, a_fee_rate, xmr_swap.vkbv)
'''
delay = self.get_short_delay_event_seconds()
self.log.info('Sending lock spend tx message for bid %s in %d seconds', bid_id.hex(), delay)
self.createActionInSession(delay, ActionTypes.SEND_XMR_SWAP_LOCK_SPEND_MSG, bid_id, session)
lock_tx_sent: bool = False
# publishalocktx
if bid.xmr_a_lock_tx and bid.xmr_a_lock_tx.state:
if bid.xmr_a_lock_tx.state >= TxStates.TX_SENT:
raise ValueError('Lock tx has already been sent {}'.format(bid.xmr_a_lock_tx.txid.hex()))
self.log.warning('Lock tx has already been sent {}'.format(bid.xmr_a_lock_tx.txid.hex()))
lock_tx_sent = True
lock_tx_signed = ci_from.signTxWithWallet(xmr_swap.a_lock_tx)
txid_hex = ci_from.publishTx(lock_tx_signed)
if lock_tx_sent is False:
lock_tx_signed = ci_from.signTxWithWallet(xmr_swap.a_lock_tx)
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())
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:
bid.xmr_a_lock_tx = SwapTx(
bid_id=bid_id,
tx_type=TxTypes.XMR_SWAP_A_LOCK,
txid=bytes.fromhex(txid_hex),
vout=vout_pos,
)
bid.xmr_a_lock_tx.setState(TxStates.TX_SENT)
if bid.xmr_a_lock_tx is None:
bid.xmr_a_lock_tx = SwapTx(
bid_id=bid_id,
tx_type=TxTypes.XMR_SWAP_A_LOCK,
txid=bytes.fromhex(txid_hex),
vout=vout_pos,
)
bid.xmr_a_lock_tx.setState(TxStates.TX_SENT)
self.logBidEvent(bid.bid_id, EventLogTypes.LOCK_TX_A_PUBLISHED, '', session)
bid.setState(BidStates.XMR_SWAP_HAVE_SCRIPT_COIN_SPEND_TX)
self.watchXmrSwap(bid, offer, xmr_swap)
self.logBidEvent(bid.bid_id, EventLogTypes.LOCK_TX_A_PUBLISHED, '', session)
delay = self.get_short_delay_event_seconds()
self.log.info('Sending lock spend tx message for bid %s in %d seconds', bid_id.hex(), delay)
self.createActionInSession(delay, ActionTypes.SEND_XMR_SWAP_LOCK_SPEND_MSG, bid_id, session)
self.saveBidInSession(bid_id, bid, session, xmr_swap)
@ -5802,8 +5833,10 @@ class BasicSwap(BaseApp):
v = ci_from.verifyTxSig(xmr_swap.a_lock_spend_tx, af_lock_spend_sig, xmr_swap.pkaf, 0, xmr_swap.a_lock_tx_script, prevout_amount)
ensure(v, 'Invalid coin A lock tx spend tx follower sig')
witness_stack = [
b'',
witness_stack = []
if coin_from not in (Coins.DCR,):
witness_stack += [b'',]
witness_stack += [
al_lock_spend_sig,
af_lock_spend_sig,
xmr_swap.a_lock_tx_script,
@ -5868,7 +5901,8 @@ class BasicSwap(BaseApp):
else:
address_to = self.getReceiveAddressFromPool(coin_to, bid_id, TxTypes.XMR_SWAP_B_LOCK_SPEND)
txid = ci_to.spendBLockTx(xmr_swap.b_lock_tx_id, address_to, xmr_swap.vkbv, vkbs, bid.amount_to, b_fee_rate, bid.chain_b_height_start)
lock_tx_vout = bid.getLockTXBVout()
txid = ci_to.spendBLockTx(xmr_swap.b_lock_tx_id, address_to, xmr_swap.vkbv, vkbs, bid.amount_to, b_fee_rate, bid.chain_b_height_start, lock_tx_vout=lock_tx_vout)
self.log.debug('Submitted lock B spend txn %s to %s chain for bid %s', txid.hex(), ci_to.coin_name(), bid_id.hex())
self.logBidEvent(bid.bid_id, EventLogTypes.LOCK_TX_B_SPEND_TX_PUBLISHED, '', session)
except Exception as ex:
@ -5933,7 +5967,9 @@ class BasicSwap(BaseApp):
address_to = self.getCachedStealthAddressForCoin(coin_to)
else:
address_to = self.getReceiveAddressFromPool(coin_to, bid_id, TxTypes.XMR_SWAP_B_LOCK_REFUND)
txid = ci_to.spendBLockTx(xmr_swap.b_lock_tx_id, address_to, xmr_swap.vkbv, vkbs, bid.amount_to, b_fee_rate, bid.chain_b_height_start)
lock_tx_vout = bid.getLockTXBVout()
txid = ci_to.spendBLockTx(xmr_swap.b_lock_tx_id, address_to, xmr_swap.vkbv, vkbs, bid.amount_to, b_fee_rate, bid.chain_b_height_start, lock_tx_vout=lock_tx_vout)
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.bid_id, EventLogTypes.LOCK_TX_B_REFUND_TX_PUBLISHED, '', session)
except Exception as ex:
@ -6041,8 +6077,10 @@ class BasicSwap(BaseApp):
al_lock_refund_spend_tx_sig = ci_from.signTx(kal, xmr_swap.a_lock_refund_spend_tx, 0, xmr_swap.a_lock_refund_tx_script, prevout_amount)
self.log.debug('Setting lock refund spend tx sigs')
witness_stack = [
b'',
witness_stack = []
if coin_from not in (Coins.DCR, ):
witness_stack += [b'',]
witness_stack += [
al_lock_refund_spend_tx_sig,
xmr_swap.af_lock_refund_spend_tx_sig,
bytes((1,)),
@ -6099,6 +6137,8 @@ class BasicSwap(BaseApp):
try:
xmr_swap.a_lock_spend_tx = msg_data.a_lock_spend_tx
xmr_swap.a_lock_spend_tx_id = ci_from.getTxid(xmr_swap.a_lock_spend_tx)
if bid.xmr_a_lock_tx:
bid.xmr_a_lock_tx.spend_txid = xmr_swap.a_lock_spend_tx_id
xmr_swap.kal_sig = msg_data.kal_sig
ci_from.verifySCLockSpendTx(

@ -203,6 +203,7 @@ class DebugTypes(IntEnum):
B_LOCK_TX_MISSED_SEND = auto()
DUPLICATE_ACTIONS = auto()
DONT_CONFIRM_PTX = auto()
OFFER_LOCK_2_VALUE_INC = auto()
class NotificationTypes(IntEnum):

@ -192,6 +192,11 @@ class Bid(Base):
else:
self.states += pack_state(new_state, now)
def getLockTXBVout(self):
if self.xmr_b_lock_tx:
return self.xmr_b_lock_tx.vout
return None
class SwapTx(Base):
__tablename__ = 'transactions'

@ -44,6 +44,10 @@ class CoinInterface:
def watch_blocks_for_scripts() -> bool:
return False
@staticmethod
def compareFeeRates(a, b) -> bool:
return abs(a - b) < 20
def __init__(self, network):
self.setDefaults()
self._network = network
@ -149,8 +153,44 @@ class CoinInterface:
def use_tx_vsize(self) -> bool:
return self._use_segwit
def getLockTxSwapOutputValue(self, bid, xmr_swap):
return bid.amount
def getLockRefundTxSwapOutputValue(self, bid, xmr_swap):
return xmr_swap.a_swap_refund_value
def getLockRefundTxSwapOutput(self, xmr_swap):
# Only one prevout exists
return 0
class AdaptorSigInterface():
def getScriptLockTxDummyWitness(self, script: bytes):
return [
b'',
bytes(72),
bytes(72),
bytes(len(script))
]
def getScriptLockRefundSpendTxDummyWitness(self, script: bytes):
return [
b'',
bytes(72),
bytes(72),
bytes((1,)),
bytes(len(script))
]
def getScriptLockRefundSwipeTxDummyWitness(self, script: bytes):
return [
bytes(72),
b'',
bytes(len(script))
]
class Secp256k1Interface(CoinInterface):
class Secp256k1Interface(CoinInterface, AdaptorSigInterface):
@staticmethod
def curve_type():
return Curves.secp256k1
@ -170,3 +210,26 @@ class Secp256k1Interface(CoinInterface):
def verifyPubkey(self, pubkey_bytes: bytes) -> bool:
return verify_secp256k1_point(pubkey_bytes)
def isValidAddressHash(self, address_hash: bytes) -> bool:
hash_len = len(address_hash)
if hash_len == 20:
return True
def isValidPubkey(self, pubkey: bytes) -> bool:
try:
self.verifyPubkey(pubkey)
return True
except Exception:
return False
def verifySig(self, pubkey: bytes, signed_hash: bytes, sig: bytes) -> bool:
pubkey = PublicKey(pubkey)
return pubkey.verify(sig, signed_hash, hasher=None)
def sumKeys(self, ka: bytes, kb: bytes) -> bytes:
# TODO: Add to coincurve
return i2b((b2i(ka) + b2i(kb)) % ep.o)
def sumPubkeys(self, Ka: bytes, Kb: bytes) -> bytes:
return PublicKey.combine_keys([PublicKey(Ka), PublicKey(Kb)]).format()

@ -28,7 +28,6 @@ from basicswap.util import (
b2h, i2b, b2i, i2h,
)
from basicswap.util.ecc import (
ep,
pointToCPK, CPKToPoint,
)
from basicswap.util.script import (
@ -66,7 +65,6 @@ from basicswap.contrib.test_framework.messages import (
CTxIn,
CTxInWitness,
CTxOut,
uint256_from_str,
)
from basicswap.contrib.test_framework.script import (
CScript, CScriptOp,
@ -120,6 +118,57 @@ def find_vout_for_address_from_txobj(tx_obj, addr: str) -> int:
raise RuntimeError("Vout not found for address: txid={}, addr={}".format(tx_obj['txid'], addr))
def extractScriptLockScriptValues(script_bytes: bytes) -> (bytes, bytes):
script_len = len(script_bytes)
ensure(script_len == 71, 'Bad script length')
o = 0
ensure_op(script_bytes[o] == OP_2)
ensure_op(script_bytes[o + 1] == 33)
o += 2
pk1 = script_bytes[o: o + 33]
o += 33
ensure_op(script_bytes[o] == 33)
o += 1
pk2 = script_bytes[o: o + 33]
o += 33
ensure_op(script_bytes[o] == OP_2)
ensure_op(script_bytes[o + 1] == OP_CHECKMULTISIG)
return pk1, pk2
def extractScriptLockRefundScriptValues(script_bytes: bytes):
script_len = len(script_bytes)
ensure(script_len > 73, 'Bad script length')
ensure_op(script_bytes[0] == OP_IF)
ensure_op(script_bytes[1] == OP_2)
ensure_op(script_bytes[2] == 33)
pk1 = script_bytes[3: 3 + 33]
ensure_op(script_bytes[36] == 33)
pk2 = script_bytes[37: 37 + 33]
ensure_op(script_bytes[70] == OP_2)
ensure_op(script_bytes[71] == OP_CHECKMULTISIG)
ensure_op(script_bytes[72] == OP_ELSE)
o = 73
csv_val, nb = decodeScriptNum(script_bytes, o)
o += nb
ensure(script_len == o + 5 + 33, 'Bad script length') # Fails if script too long
ensure_op(script_bytes[o] == OP_CHECKSEQUENCEVERIFY)
o += 1
ensure_op(script_bytes[o] == OP_DROP)
o += 1
ensure_op(script_bytes[o] == 33)
o += 1
pk3 = script_bytes[o: o + 33]
o += 33
ensure_op(script_bytes[o] == OP_CHECKSIG)
o += 1
ensure_op(script_bytes[o] == OP_ENDIF)
return pk1, pk2, csv_val, pk3
class BTCInterface(Secp256k1Interface):
@staticmethod
@ -157,10 +206,6 @@ class BTCInterface(Secp256k1Interface):
rv += output.nValue
return rv
@staticmethod
def compareFeeRates(a, b) -> bool:
return abs(a - b) < 20
@staticmethod
def xmr_swap_a_lock_spend_tx_vsize() -> int:
return 147
@ -214,23 +259,6 @@ class BTCInterface(Secp256k1Interface):
self._log = self._sc.log if self._sc and self._sc.log else logging
self._expect_seedid_hex = None
def checkWallets(self) -> int:
wallets = self.rpc('listwallets')
# Wallet name is "" for some LTC and PART installs on older cores
if self._rpc_wallet not in wallets and len(wallets) > 0:
self._log.debug('Changing {} wallet name.'.format(self.ticker()))
for wallet_name in wallets:
# Skip over other expected wallets
if wallet_name in ('mweb', ):
continue
self._rpc_wallet = wallet_name
self._log.info('Switched {} wallet name to {}.'.format(self.ticker(), self._rpc_wallet))
self.rpc_wallet = make_rpc_func(self._rpcport, self._rpcauth, host=self._rpc_host, wallet=self._rpc_wallet)
break
return len(wallets)
def open_rpc(self, wallet=None):
return openrpc(self._rpcport, self._rpcauth, wallet=wallet, host=self._rpc_host)
@ -241,15 +269,30 @@ class BTCInterface(Secp256k1Interface):
except Exception as ex:
traceback.print_exc()
raise ValueError('RPC Server Error ' + str(ex))
if 'error' in r and r['error'] is not None:
raise ValueError('RPC error ' + str(r['error']))
return r['result']
def close_rpc(self, rpc_conn):
rpc_conn.close()
def checkWallets(self) -> int:
wallets = self.rpc('listwallets')
# Wallet name is "" for some LTC and PART installs on older cores
if self._rpc_wallet not in wallets and len(wallets) > 0:
self._log.debug('Changing {} wallet name.'.format(self.ticker()))
for wallet_name in wallets:
# Skip over other expected wallets
if wallet_name in ('mweb', ):
continue
self._rpc_wallet = wallet_name
self._log.info('Switched {} wallet name to {}.'.format(self.ticker(), self._rpc_wallet))
self.rpc_wallet = make_rpc_func(self._rpcport, self._rpcauth, host=self._rpc_host, wallet=self._rpc_wallet)
break
return len(wallets)
def testDaemonRPC(self, with_wallet=True) -> None:
self.rpc_wallet('getwalletinfo' if with_wallet else 'getblockchaininfo')
@ -278,7 +321,7 @@ class BTCInterface(Secp256k1Interface):
max_tries = 5000
for i in range(max_tries):
prev_block_header = self.rpc('getblock', [last_block_header['previousblockhash']])
prev_block_header = self.rpc('getblockheader', [last_block_header['previousblockhash']])
if prev_block_header['time'] <= time:
return last_block_header if block_after else prev_block_header
@ -343,18 +386,6 @@ class BTCInterface(Secp256k1Interface):
self._log.debug('validateaddress failed: {}'.format(address))
return False
def isValidAddressHash(self, address_hash: bytes) -> bool:
hash_len = len(address_hash)
if hash_len == 20:
return True
def isValidPubkey(self, pubkey: bytes) -> bool:
try:
self.verifyPubkey(pubkey)
return True
except Exception:
return False
def isAddressMine(self, address: str, or_watch_only: bool = False) -> bool:
addr_info = self.rpc_wallet('getaddressinfo', [address])
if not or_watch_only:
@ -468,13 +499,6 @@ class BTCInterface(Secp256k1Interface):
def decodeKey(self, k: str) -> bytes:
return decodeWif(k)
def sumKeys(self, ka, kb):
# TODO: Add to coincurve
return i2b((b2i(ka) + b2i(kb)) % ep.o)
def sumPubkeys(self, Ka, Kb):
return PublicKey.combine_keys([PublicKey(Ka), PublicKey(Kb)]).format()
def getScriptForPubkeyHash(self, pkh: bytes) -> CScript:
# p2wpkh
return CScript([OP_0, pkh])
@ -485,24 +509,6 @@ class BTCInterface(Secp256k1Interface):
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')
o = 0
ensure_op(script_bytes[o] == OP_2)
ensure_op(script_bytes[o + 1] == 33)
o += 2
pk1 = script_bytes[o: o + 33]
o += 33
ensure_op(script_bytes[o] == 33)
o += 1
pk2 = script_bytes[o: o + 33]
o += 33
ensure_op(script_bytes[o] == OP_2)
ensure_op(script_bytes[o + 1] == OP_CHECKMULTISIG)
return pk1, pk2
def createSCLockTx(self, value: int, script: bytearray, vkbv: bytes = None) -> bytes:
tx = CTransaction()
tx.nVersion = self.txVersion()
@ -512,37 +518,6 @@ class BTCInterface(Secp256k1Interface):
def fundSCLockTx(self, tx_bytes, feerate, vkbv=None):
return self.fundTx(tx_bytes, feerate)
def extractScriptLockRefundScriptValues(self, script_bytes: bytes):
script_len = len(script_bytes)
ensure(script_len > 73, 'Bad script length')
ensure_op(script_bytes[0] == OP_IF)
ensure_op(script_bytes[1] == OP_2)
ensure_op(script_bytes[2] == 33)
pk1 = script_bytes[3: 3 + 33]
ensure_op(script_bytes[36] == 33)
pk2 = script_bytes[37: 37 + 33]
ensure_op(script_bytes[70] == OP_2)
ensure_op(script_bytes[71] == OP_CHECKMULTISIG)
ensure_op(script_bytes[72] == OP_ELSE)
o = 73
csv_val, nb = decodeScriptNum(script_bytes, o)
o += nb
ensure(script_len == o + 5 + 33, 'Bad script length') # Fails if script too long
ensure_op(script_bytes[o] == OP_CHECKSEQUENCEVERIFY)
o += 1
ensure_op(script_bytes[o] == OP_DROP)
o += 1
ensure_op(script_bytes[o] == 33)
o += 1
pk3 = script_bytes[o: o + 33]
o += 33
ensure_op(script_bytes[o] == OP_CHECKSIG)
o += 1
ensure_op(script_bytes[o] == OP_ENDIF)
return pk1, pk2, csv_val, pk3
def genScriptLockRefundTxScript(self, Kal, Kaf, csv_val) -> CScript:
Kal_enc = Kal if len(Kal) == 33 else self.encodePubkey(Kal)
@ -634,7 +609,7 @@ class BTCInterface(Secp256k1Interface):
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)
A, B, lock2_value, C = extractScriptLockRefundScriptValues(script_lock_refund)
tx_lock_refund.rehash()
tx_lock_refund_hash_int = tx_lock_refund.sha256
@ -721,7 +696,7 @@ class BTCInterface(Secp256k1Interface):
ensure(locked_coin == swap_value, 'Bad locked value')
# Check script
A, B = self.extractScriptLockScriptValues(script_out)
A, B = extractScriptLockScriptValues(script_out)
ensure(A == Kal, 'Bad script pubkey')
ensure(B == Kaf, 'Bad script pubkey')
@ -789,7 +764,7 @@ class BTCInterface(Secp256k1Interface):
locked_coin = tx.vout[locked_n].nValue
# Check script and values
A, B, csv_val, C = self.extractScriptLockRefundScriptValues(script_out)
A, B, csv_val, C = extractScriptLockRefundScriptValues(script_out)
ensure(A == Kal, 'Bad script pubkey')
ensure(B == Kaf, 'Bad script pubkey')
ensure(csv_val == csv_val_expect, 'Bad script csv value')
@ -922,6 +897,9 @@ class BTCInterface(Secp256k1Interface):
def decryptOtVES(self, k: bytes, esig: bytes) -> bytes:
return ecdsaotves_dec_sig(k, esig) + bytes((SIGHASH_ALL,))
def recoverEncKey(self, esig, sig, K):
return ecdsaotves_rec_enc_key(K, esig, sig[:-1]) # Strip sighash type
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)
@ -929,11 +907,7 @@ class BTCInterface(Secp256k1Interface):
pubkey = PublicKey(K)
return pubkey.verify(sig[: -1], sig_hash, hasher=None) # Pop the hashtype byte
def verifySig(self, pubkey, signed_hash, sig):
pubkey = PublicKey(pubkey)
return pubkey.verify(sig, signed_hash, hasher=None)
def fundTx(self, tx, feerate):
def fundTx(self, tx: bytes, feerate) -> bytes:
feerate_str = self.format_amount(feerate)
# TODO: unlock unspents if bid cancelled
options = {
@ -943,7 +917,7 @@ class BTCInterface(Secp256k1Interface):
rv = self.rpc_wallet('fundrawtransaction', [tx.hex(), options])
return bytes.fromhex(rv['hex'])
def listInputs(self, tx_bytes):
def listInputs(self, tx_bytes: bytes):
tx = self.loadTx(tx_bytes)
all_locked = self.rpc_wallet('listlockunspent')
@ -1049,11 +1023,11 @@ class BTCInterface(Secp256k1Interface):
tx.wit.vtxinwit.clear()
return tx.serialize()
def extractLeaderSig(self, tx_bytes) -> bytes:
def extractLeaderSig(self, tx_bytes: bytes) -> bytes:
tx = self.loadTx(tx_bytes)
return tx.wit.vtxinwit[0].scriptWitness.stack[1]
def extractFollowerSig(self, tx_bytes) -> bytes:
def extractFollowerSig(self, tx_bytes: bytes) -> bytes:
tx = self.loadTx(tx_bytes)
return tx.wit.vtxinwit[0].scriptWitness.stack[2]
@ -1076,9 +1050,6 @@ class BTCInterface(Secp256k1Interface):
return bytes.fromhex(self.publishTx(b_lock_tx))
def recoverEncKey(self, esig, sig, K):
return ecdsaotves_rec_enc_key(K, esig, sig[:-1]) # Strip sighash type
def getTxVSize(self, tx, add_bytes: int = 0, add_witness_bytes: int = 0) -> int:
wsf = self.witnessScaleFactor()
len_full = len(tx.serialize_with_witness()) + add_bytes + add_witness_bytes
@ -1108,10 +1079,10 @@ class BTCInterface(Secp256k1Interface):
witness_bytes = 109
vsize = self.getTxVSize(tx, add_witness_bytes=witness_bytes)
pay_fee = round(fee_rate * vsize / 1000)
self._log.info(f'BLockSpendTx fee_rate, vsize, fee: {fee_rate}, {vsize}, {pay_fee}.')
self._log.info(f'BLockSpendTx fee_rate, vsize, fee: {fee_rate}, {vsize}, {pay_fee}.')
return pay_fee
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:
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, lock_tx_vout=None) -> bytes:
self._log.info('spendBLockTx %s:\n', chain_b_lock_txid.hex())
wtx = self.rpc_wallet('gettransaction', [chain_b_lock_txid.hex(), ])
lock_tx = self.loadTx(bytes.fromhex(wtx['hex']))
@ -1126,7 +1097,7 @@ class BTCInterface(Secp256k1Interface):
tx.nVersion = self.txVersion()
script_lock = self.getScriptForPubkeyHash(Kbs)
chain_b_lock_txid_int = uint256_from_str(chain_b_lock_txid[::-1])
chain_b_lock_txid_int = b2i(chain_b_lock_txid)
tx.vin.append(CTxIn(COutPoint(chain_b_lock_txid_int, locked_n),
nSequence=0,
@ -1148,7 +1119,7 @@ class BTCInterface(Secp256k1Interface):
addr_info = self.rpc_wallet('getaddressinfo', [address])
return addr_info['iswatchonly']
def getSCLockScriptAddress(self, lock_script):
def getSCLockScriptAddress(self, lock_script: bytes) -> str:
lock_tx_dest = self.getScriptDest(lock_script)
return self.encodeScriptDest(lock_tx_dest)
@ -1225,25 +1196,25 @@ class BTCInterface(Secp256k1Interface):
params = [addr_to, value, '', '', subfee, True, self._conf_target]
return self.rpc_wallet('sendtoaddress', params)
def signCompact(self, k, message):
def signCompact(self, k, message: str) -> bytes:
message_hash = sha256(bytes(message, 'utf-8'))
privkey = PrivateKey(k)
return privkey.sign_recoverable(message_hash, hasher=None)[:64]
def signRecoverable(self, k, message):
def signRecoverable(self, k, message: str) -> bytes:
message_hash = sha256(bytes(message, 'utf-8'))
privkey = PrivateKey(k)
return privkey.sign_recoverable(message_hash, hasher=None)
def verifyCompactSig(self, K, message, sig):
def verifyCompactSig(self, K, message: str, sig) -> None:
message_hash = sha256(bytes(message, 'utf-8'))
pubkey = PublicKey(K)
rv = pubkey.verify_compact(sig, message_hash, hasher=None)
assert (rv is True)
def verifySigAndRecover(self, sig, message):
def verifySigAndRecover(self, sig, message: str) -> bytes:
message_hash = sha256(bytes(message, 'utf-8'))
pubkey = PublicKey.from_signature_and_message(sig, message_hash, hasher=None)
return pubkey.format()
@ -1271,40 +1242,6 @@ class BTCInterface(Secp256k1Interface):
def showLockTransfers(self, kbv, Kbs, restore_height):
raise ValueError('Unimplemented')
def getLockTxSwapOutputValue(self, bid, xmr_swap):
return bid.amount
def getLockRefundTxSwapOutputValue(self, bid, xmr_swap):
return xmr_swap.a_swap_refund_value
def getLockRefundTxSwapOutput(self, xmr_swap):
# Only one prevout exists
return 0
def getScriptLockTxDummyWitness(self, script: bytes):
return [
b'',
bytes(72),
bytes(72),
bytes(len(script))
]
def getScriptLockRefundSpendTxDummyWitness(self, script: bytes):
return [
b'',
bytes(72),
bytes(72),
bytes((1,)),
bytes(len(script))
]
def getScriptLockRefundSwipeTxDummyWitness(self, script: bytes):
return [
bytes(72),
b'',
bytes(len(script))
]
def getWitnessStackSerialisedLength(self, witness_stack):
length = getCompactSizeLen(len(witness_stack))
for e in witness_stack:
@ -1501,7 +1438,7 @@ class BTCInterface(Secp256k1Interface):
def createRedeemTxn(self, prevout, output_addr: str, output_value: int, txn_script: bytes = None) -> str:
tx = CTransaction()
tx.nVersion = self.txVersion()
prev_txid = uint256_from_str(bytes.fromhex(prevout['txid'])[::-1])
prev_txid = b2i(bytes.fromhex(prevout['txid']))
tx.vin.append(CTxIn(COutPoint(prev_txid, prevout['vout'])))
pkh = self.decodeAddress(output_addr)
script = self.getScriptForPubkeyHash(pkh)
@ -1513,7 +1450,7 @@ class BTCInterface(Secp256k1Interface):
tx = CTransaction()
tx.nVersion = self.txVersion()
tx.nLockTime = locktime
prev_txid = uint256_from_str(bytes.fromhex(prevout['txid'])[::-1])
prev_txid = b2i(bytes.fromhex(prevout['txid']))
tx.vin.append(CTxIn(COutPoint(prev_txid, prevout['vout']), nSequence=sequence,))
pkh = self.decodeAddress(output_addr)
script = self.getScriptForPubkeyHash(pkh)
@ -1551,6 +1488,12 @@ class BTCInterface(Secp256k1Interface):
'amount': txjs['vout'][n]['value']
}
def isTxExistsError(self, err_str: str) -> bool:
return 'Transaction already in block chain' in err_str
def isTxNonFinalError(self, err_str: str) -> bool:
return 'non-BIP68-final' in err_str or 'non-final' in err_str
def testBTCInterface():
print('TODO: testBTCInterface')

@ -66,7 +66,7 @@ class DASHInterface(BTCInterface):
add_bytes = 107
size = len(tx.serialize_with_witness()) + add_bytes
pay_fee = round(fee_rate * size / 1000)
self._log.info(f'BLockSpendTx fee_rate, size, fee: {fee_rate}, {size}, {pay_fee}.')
self._log.info(f'BLockSpendTx fee_rate, size, fee: {fee_rate}, {size}, {pay_fee}.')
return pay_fee
def findTxnByHash(self, txid_hex: str):

@ -7,20 +7,29 @@
import base64
import hashlib
import json
import logging
import random
import traceback
from basicswap.basicswap_util import (
getVoutByScriptPubKey,
TxLockTypes
)
from basicswap.chainparams import Coins
from basicswap.contrib.test_framework.messages import (
uint256_from_str,
from basicswap.contrib.test_framework.script import (
CScriptNum,
)
from basicswap.interface.base import (
Secp256k1Interface,
)
from basicswap.interface.btc import (
extractScriptLockScriptValues,
extractScriptLockRefundScriptValues,
)
from basicswap.interface.btc import Secp256k1Interface
from basicswap.util import (
ensure,
b2h, b2i, i2b, i2h,
)
from basicswap.util.address import (
b58decode,
@ -36,28 +45,40 @@ from basicswap.util.script import (
)
from basicswap.util.extkey import ExtKeyPair
from basicswap.util.integer import encode_varint
from basicswap.interface.dcr.rpc import make_rpc_func
from basicswap.interface.dcr.rpc import make_rpc_func, openrpc
from .messages import (
COutPoint,
CTransaction,
CTxIn,
CTxOut,
COutPoint,
findOutput,
SigHashType,
TxSerializeType,
)
from .script import (
push_script_data,
OP_HASH160,
OP_EQUAL,
OP_CHECKMULTISIG,
OP_CHECKSEQUENCEVERIFY,
OP_CHECKSIG,
OP_DROP,
OP_DUP,
OP_ELSE,
OP_ENDIF,
OP_EQUAL,
OP_EQUALVERIFY,
OP_CHECKSIG,
OP_HASH160,
OP_IF,
push_script_data,
)
from coincurve.keys import (
PrivateKey,
PublicKey,
)
from coincurve.ecdsaotves import (
ecdsaotves_enc_sign,
ecdsaotves_enc_verify,
ecdsaotves_dec_sig,
ecdsaotves_rec_enc_key
)
SEQUENCE_LOCKTIME_GRANULARITY = 9 # 512 seconds
@ -203,6 +224,10 @@ class DCRInterface(Secp256k1Interface):
def watch_blocks_for_scripts() -> bool:
return True
@staticmethod
def depth_spendable() -> int:
return 0
def __init__(self, coin_settings, network, swap_client=None):
super().__init__(network)
self._rpc_host = coin_settings.get('rpchost', '127.0.0.1')
@ -212,8 +237,10 @@ class DCRInterface(Secp256k1Interface):
self._log = self._sc.log if self._sc and self._sc.log else logging
self.rpc = make_rpc_func(self._rpcport, self._rpcauth, host=self._rpc_host)
if 'walletrpcport' in coin_settings:
self.rpc_wallet = make_rpc_func(coin_settings['walletrpcport'], self._rpcauth, host=self._rpc_host)
self._walletrpcport = coin_settings['walletrpcport']
self.rpc_wallet = make_rpc_func(self._walletrpcport, self._rpcauth, host=self._rpc_host)
else:
self._walletrpcport = None
self.rpc_wallet = None
self.blocks_confirmed = coin_settings['blocks_confirmed']
self.setConfTarget(coin_settings['conf_target'])
@ -221,6 +248,23 @@ class DCRInterface(Secp256k1Interface):
self._use_segwit = True # Decred is natively segwit
self._connection_type = coin_settings['connection_type']
def open_rpc(self):
return openrpc(self._rpcport, self._rpcauth, host=self._rpc_host)
def json_request(self, rpc_conn, method, params):
try:
v = rpc_conn.json_request(method, params)
r = json.loads(v.decode('utf-8'))
except Exception as ex:
traceback.print_exc()
raise ValueError('RPC Server Error ' + str(ex))
if 'error' in r and r['error'] is not None:
raise ValueError('RPC error ' + str(r['error']))
return r['result']
def close_rpc(self, rpc_conn):
rpc_conn.close()
def use_tx_vsize(self) -> bool:
return False
@ -242,6 +286,7 @@ class DCRInterface(Secp256k1Interface):
return b58encode(data + checksum[0:4])
def decode_address(self, address: str) -> bytes:
# Different from decodeAddress returns more prefix bytes
addr_data = b58decode(address)
if addr_data is None:
return None
@ -251,6 +296,9 @@ class DCRInterface(Secp256k1Interface):
raise ValueError('Checksum mismatch')
return prefixed_data
def decodeAddress(self, address: str) -> bytes:
return self.decode_address(address)[2:]
def testDaemonRPC(self, with_wallet=True) -> None:
if with_wallet:
self.rpc_wallet('getinfo')
@ -289,6 +337,11 @@ class DCRInterface(Secp256k1Interface):
return rv
def getSpendableBalance(self) -> int:
balances = self.rpc_wallet('getbalance')
default_account_bal = balances['balances'][0] # 0 always default?
return self.make_int(default_account_bal['spendable'])
def getSeedHash(self, seed: bytes) -> bytes:
# m / purpose' / coin_type' / account' / change / address_index
# m/44'/coin_type'/0'/0/0
@ -339,6 +392,7 @@ class DCRInterface(Secp256k1Interface):
def setTxSignature(self, tx_bytes: bytes, stack, txi: int = 0) -> bytes:
tx = self.loadTx(tx_bytes)
script_data = bytearray()
for data in stack:
push_script_data(script_data, data)
@ -390,6 +444,13 @@ class DCRInterface(Secp256k1Interface):
assert len(pkh) == 20
return OP_DUP.to_bytes(1) + OP_HASH160.to_bytes(1) + len(pkh).to_bytes(1) + pkh + OP_EQUALVERIFY.to_bytes(1) + OP_CHECKSIG.to_bytes(1)
def getPkDest(self, K: bytes) -> bytearray:
return self.getPubkeyHashDest(self.pkh(K))
def getSCLockScriptAddress(self, lock_script: bytes) -> str:
lock_tx_dest = self.getScriptDest(lock_script)
return self.encodeScriptDest(lock_tx_dest)
def get_fee_rate(self, conf_target: int = 2) -> (float, str):
chain_client_settings = self._sc.getChainClientSettings(self.coin_type()) # basicswap.json
override_feerate = chain_client_settings.get('override_feerate', None)
@ -525,6 +586,29 @@ class DCRInterface(Secp256k1Interface):
return sum_value
def signCompact(self, k, message):
message_hash = blake256(bytes(message, 'utf-8'))
privkey = PrivateKey(k)
return privkey.sign_recoverable(message_hash, hasher=None)[:64]
def signRecoverable(self, k, message: str) -> bytes:
message_hash = blake256(bytes(message, 'utf-8'))
privkey = PrivateKey(k)
return privkey.sign_recoverable(message_hash, hasher=None)
def verifyCompactSig(self, K, message: str, sig) -> None:
message_hash = blake256(bytes(message, 'utf-8'))
pubkey = PublicKey(K)
rv = pubkey.verify_compact(sig, message_hash, hasher=None)
assert (rv is True)
def verifySigAndRecover(self, sig, message: str) -> bytes:
message_hash = blake256(bytes(message, 'utf-8'))
pubkey = PublicKey.from_signature_and_message(sig, message_hash, hasher=None)
return pubkey.format()
def verifyMessage(self, address: str, message: str, signature: str, message_magic: str = None) -> bool:
if message_magic is None:
message_magic = self.chainparams()['message_magic']
@ -546,7 +630,12 @@ class DCRInterface(Secp256k1Interface):
return True if address_hash == pubkey_hash else False
def signTxWithWallet(self, tx) -> bytes:
return bytes.fromhex(self.rpc('signrawtransaction', [tx.hex()])['hex'])
return bytes.fromhex(self.rpc_wallet('signrawtransaction', [tx.hex()])['hex'])
def signTxWithKey(self, tx: bytes, key: bytes) -> bytes:
key_wif = self.encodeKey(key)
rv = self.rpc_wallet('signrawtransaction', [tx.hex(), [], [key_wif, ]])
return bytes.fromhex(rv['hex'])
def createRawFundedTransaction(self, addr_to: str, amount: int, sub_fee: bool = False, lock_unspents: bool = True) -> str:
@ -593,6 +682,9 @@ class DCRInterface(Secp256k1Interface):
# self._log.warning('gettxout {}'.format(e))
return None
if found_vout is None:
return None
block_height: int = 0
confirmations: int = 0 if 'confirmations' not in txout else txout['confirmations']
@ -601,6 +693,7 @@ class DCRInterface(Secp256k1Interface):
block_height = self.getChainHeight() - confirmations
rv = {
'txid': txid.hex(),
'depth': confirmations,
'index': found_vout,
'height': block_height}
@ -628,7 +721,7 @@ class DCRInterface(Secp256k1Interface):
def createRedeemTxn(self, prevout, output_addr: str, output_value: int, txn_script: bytes = None) -> str:
tx = CTransaction()
tx.version = self.txVersion()
prev_txid = uint256_from_str(bytes.fromhex(prevout['txid'])[::-1])
prev_txid = b2i(bytes.fromhex(prevout['txid']))
tx.vin.append(CTxIn(COutPoint(prev_txid, prevout['vout'], 0)))
pkh = self.decode_address(output_addr)[2:]
script = self.getPubkeyHashDest(pkh)
@ -639,7 +732,7 @@ class DCRInterface(Secp256k1Interface):
tx = CTransaction()
tx.version = self.txVersion()
tx.locktime = locktime
prev_txid = uint256_from_str(bytes.fromhex(prevout['txid'])[::-1])
prev_txid = b2i(bytes.fromhex(prevout['txid']))
tx.vin.append(CTxIn(COutPoint(prev_txid, prevout['vout'], 0), sequence=sequence,))
pkh = self.decode_address(output_addr)[2:]
script = self.getPubkeyHashDest(pkh)
@ -678,6 +771,22 @@ class DCRInterface(Secp256k1Interface):
block_hash = self.rpc('getblockhash', [height])
return self.rpc('getblockheader', [block_hash])
def getBlockHeaderAt(self, time: int, block_after=False):
blockchaininfo = self.rpc('getblockchaininfo')
last_block_header = self.rpc('getblockheader', [blockchaininfo['bestblockhash']])
max_tries = 5000
for i in range(max_tries):
prev_block_header = self.rpc('getblockheader', [last_block_header['previousblockhash']])
if prev_block_header['time'] <= time:
return last_block_header if block_after else prev_block_header
last_block_header = prev_block_header
raise ValueError(f'Block header not found at time: {time}')
def getMempoolTx(self, txid):
raise ValueError('TODO')
def getBlockWithTxns(self, block_hash: str):
block = self.rpc('getblock', [block_hash, True, True])
@ -697,3 +806,549 @@ class DCRInterface(Secp256k1Interface):
def describeTx(self, tx_hex: str):
return self.rpc('decoderawtransaction', [tx_hex])
def fundTx(self, tx: bytes, feerate) -> bytes:
feerate_str = float(self.format_amount(feerate))
# TODO: unlock unspents if bid cancelled
options = {
'feeRate': feerate_str,
}
rv = self.rpc_wallet('fundrawtransaction', [tx.hex(), 'default', options])
tx_bytes = bytes.fromhex(rv['hex'])
tx_obj = self.loadTx(tx_bytes)
for txi in tx_obj.vin:
utxos = [{'amount': float(self.format_amount(txi.value_in)),
'txid': i2h(txi.prevout.hash),
'vout': txi.prevout.n,
'tree': txi.prevout.tree}]
rv = self.rpc_wallet('lockunspent', [False, utxos])
return tx_bytes
def createSCLockTx(self, value: int, script: bytearray, vkbv: bytes = None) -> bytes:
tx = CTransaction()
tx.version = self.txVersion()
tx.vout.append(self.txoType()(value, self.getScriptDest(script)))
return tx.serialize()
def fundSCLockTx(self, tx_bytes, feerate, vkbv=None):
return self.fundTx(tx_bytes, feerate)
def genScriptLockRefundTxScript(self, Kal, Kaf, csv_val) -> bytes:
Kal_enc = Kal if len(Kal) == 33 else self.encodePubkey(Kal)
Kaf_enc = Kaf if len(Kaf) == 33 else self.encodePubkey(Kaf)
script = bytearray()
script += OP_IF.to_bytes(1)
push_script_data(script, bytes((2,)))
push_script_data(script, Kal_enc)
push_script_data(script, Kaf_enc)
push_script_data(script, bytes((2,)))
script += OP_CHECKMULTISIG.to_bytes(1)
script += OP_ELSE.to_bytes(1)
script += CScriptNum.encode(CScriptNum(csv_val))
script += OP_CHECKSEQUENCEVERIFY.to_bytes(1)
script += OP_DROP.to_bytes(1)
push_script_data(script, Kaf_enc)
script += OP_CHECKSIG.to_bytes(1)
script += OP_ENDIF.to_bytes(1)
return script
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].value
tx_lock_id_int = b2i(tx_lock.TxHash())
tx = CTransaction()
tx.version = self.txVersion()
tx.vin.append(CTxIn(COutPoint(tx_lock_id_int, locked_n, 0)))
tx.vout.append(self.txoType()(locked_coin, self.getPubkeyHashDest(pkh_dest)))
dummy_witness_stack = self.getScriptLockTxDummyWitness(script_lock)
size = len(self.setTxSignature(tx.serialize(), dummy_witness_stack))
pay_fee = round(tx_fee_rate * size / 1000)
tx.vout[0].value = locked_coin - pay_fee
fee_info['fee_paid'] = pay_fee
fee_info['rate_used'] = tx_fee_rate
fee_info['size'] = size
self._log.info('createSCLockSpendTx %s:\n fee_rate, size, fee: %ld, %ld, %ld.',
tx.TxHash().hex(), tx_fee_rate, size, pay_fee)
return tx.serialize(TxSerializeType.NoWitness)
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].value
tx_lock_id_int = b2i(tx_lock.TxHash())
refund_script = self.genScriptLockRefundTxScript(Kal, Kaf, csv_val)
tx = CTransaction()
tx.version = self.txVersion()
tx.vin.append(CTxIn(COutPoint(tx_lock_id_int, locked_n, 0),
sequence=lock1_value))
tx.vout.append(self.txoType()(locked_coin, self.getScriptDest(refund_script)))
dummy_witness_stack = self.getScriptLockTxDummyWitness(script_lock)
size = len(self.setTxSignature(tx.serialize(), dummy_witness_stack))
pay_fee = round(tx_fee_rate * size / 1000)
tx.vout[0].value = locked_coin - pay_fee
self._log.info('createSCLockRefundTx %s:\n fee_rate, size, fee: %ld, %ld, %ld.',
tx.TxHash().hex(), tx_fee_rate, size, pay_fee)
return tx.serialize(TxSerializeType.NoWitness), refund_script, tx.vout[0].value
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].value
tx_lock_refund_hash_int = b2i(tx_lock_refund.TxHash())
tx = CTransaction()
tx.version = self.txVersion()
tx.vin.append(CTxIn(COutPoint(tx_lock_refund_hash_int, locked_n, 0),
sequence=0))
tx.vout.append(self.txoType()(locked_coin, self.getPubkeyHashDest(pkh_refund_to)))
dummy_witness_stack = self.getScriptLockRefundSpendTxDummyWitness(script_lock_refund)
size = len(self.setTxSignature(tx.serialize(), dummy_witness_stack))
pay_fee = round(tx_fee_rate * size / 1000)
tx.vout[0].value = locked_coin - pay_fee
self._log.info('createSCLockRefundSpendTx %s:\n fee_rate, size, fee: %ld, %ld, %ld.',
tx.TxHash().hex(), tx_fee_rate, size, pay_fee)
return tx.serialize(TxSerializeType.NoWitness)
def verifySCLockTx(self, tx_bytes, script_out,
swap_value,
Kal, Kaf,
feerate,
check_lock_tx_inputs, vkbv=None):
# Verify:
#
# Not necessary to check the lock txn is mineable, as protocol will wait for it to confirm
# However by checking early we can avoid wasting time processing unmineable txns
# Check fee is reasonable
tx = self.loadTx(tx_bytes)
txid = self.getTxid(tx)
self._log.info('Verifying lock tx: {}.'.format(b2h(txid)))
ensure(tx.version == self.txVersion(), 'Bad version')
ensure(tx.locktime == 0, 'Bad locktime')
ensure(tx.expiry == 0, 'Bad expiry')
script_pk = self.getScriptDest(script_out)
locked_n = findOutput(tx, script_pk)
ensure(locked_n is not None, 'Lock output not found in tx')
locked_coin = tx.vout[locked_n].value
# Check value
ensure(locked_coin == swap_value, 'Bad locked value')
# Check script
A, B = extractScriptLockScriptValues(script_out)
ensure(A == Kal, 'Bad script pubkey')
ensure(B == Kaf, 'Bad script pubkey')
if check_lock_tx_inputs:
# TODO: Check that inputs are unspent
# Verify fee rate
inputs_value = 0
add_bytes = 0
add_witness_bytes = 0
for pi in tx.vin:
ptx = self.rpc('getrawtransaction', [i2h(pi.prevout.hash), True])
prevout = ptx['vout'][pi.prevout.n]
inputs_value += self.make_int(prevout['value'])
self._log.info('prevout: {}.'.format(prevout))
prevout_type = prevout['scriptPubKey']['type']
'''
if prevout_type == 'witness_v0_keyhash':
#add_witness_bytes += 107 # sig 72, pk 33 and 2 size bytes
#add_witness_bytes += getCompactSizeLen(107)
else:
# Assume P2PKH, TODO more types
add_bytes += 107 # OP_PUSH72 <ecdsa_signature> OP_PUSH33 <public_key>
'''
outputs_value = 0
for txo in tx.vout:
outputs_value += txo.nValue
fee_paid = inputs_value - outputs_value
assert (fee_paid > 0)
size = len(tx.serialize()) + add_witness_bytes
fee_rate_paid = fee_paid * 1000 // size
self._log.info('tx amount, size, feerate: %ld, %ld, %ld', locked_coin, size, fee_rate_paid)
if not self.compareFeeRates(fee_rate_paid, feerate):
self._log.warning('feerate paid doesn\'t match expected: %ld, %ld', fee_rate_paid, feerate)
# TODO: Display warning to user
return txid, locked_n
def verifySCLockSpendTx(self, tx_bytes,
lock_tx_bytes, lock_tx_script,
a_pkhash_f, feerate, vkbv=None):
# Verify:
# Must have only one input with correct prevout (n is always 0) and sequence
# Must have only one output with destination and amount
tx = self.loadTx(tx_bytes)
txid = self.getTxid(tx)
self._log.info('Verifying lock spend tx: {}.'.format(b2h(txid)))
ensure(tx.version == self.txVersion(), 'Bad version')
ensure(tx.locktime == 0, 'Bad locktime')
ensure(tx.expiry == 0, 'Bad expiry')
ensure(len(tx.vin) == 1, 'tx doesn\'t have one input')
lock_tx = self.loadTx(lock_tx_bytes)
lock_tx_id = self.getTxid(lock_tx)
output_script = self.getScriptDest(lock_tx_script)
locked_n = findOutput(lock_tx, output_script)
ensure(locked_n is not None, 'Output not found in tx')
locked_coin = lock_tx.vout[locked_n].value
ensure(tx.vin[0].sequence == 0, 'Bad input nSequence')
ensure(len(tx.vin[0].signature_script) == 0, 'Input sig not empty')
ensure(i2b(tx.vin[0].prevout.hash) == lock_tx_id and tx.vin[0].prevout.n == locked_n, 'Input prevout mismatch')
ensure(len(tx.vout) == 1, 'tx doesn\'t have one output')
p2wpkh = self.getPubkeyHashDest(a_pkhash_f)
ensure(tx.vout[0].script_pubkey == p2wpkh, 'Bad output destination')
# The value of the lock tx output should already be verified, if the fee is as expected the difference will be the correct amount
fee_paid = locked_coin - tx.vout[0].value
assert (fee_paid > 0)
dummy_witness_stack = self.getScriptLockTxDummyWitness(lock_tx_script)
size = len(self.setTxSignature(tx.serialize(), dummy_witness_stack))
fee_rate_paid = fee_paid * 1000 // size
self._log.info('tx amount, size, feerate: %ld, %ld, %ld', tx.vout[0].value, size, fee_rate_paid)
if not self.compareFeeRates(fee_rate_paid, feerate):
raise ValueError('Bad fee rate, expected: {}'.format(feerate))
return True
def verifySCLockRefundTx(self, tx_bytes, lock_tx_bytes, script_out,
prevout_id, prevout_n, prevout_seq, prevout_script,
Kal, Kaf, csv_val_expect, swap_value, feerate, vkbv=None):
# Verify:
# Must have only one input with correct prevout and sequence
# Must have only one output to the p2wsh of the lock refund script
# Output value must be locked_coin - lock tx fee
tx = self.loadTx(tx_bytes)
txid = self.getTxid(tx)
self._log.info('Verifying lock refund tx: {}.'.format(b2h(txid)))
ensure(tx.version == self.txVersion(), 'Bad version')
ensure(tx.locktime == 0, 'locktime not 0')
ensure(tx.expiry == 0, 'Bad expiry')
ensure(len(tx.vin) == 1, 'tx doesn\'t have one input')
ensure(tx.vin[0].sequence == prevout_seq, 'Bad input sequence')
ensure(i2b(tx.vin[0].prevout.hash) == prevout_id and tx.vin[0].prevout.n == prevout_n and tx.vin[0].prevout.tree == 0, 'Input prevout mismatch')
ensure(len(tx.vin[0].signature_script) == 0, 'Input sig not empty')
ensure(len(tx.vout) == 1, 'tx doesn\'t have one output')
script_pk = self.getScriptDest(script_out)
locked_n = findOutput(tx, script_pk)
ensure(locked_n is not None, 'Output not found in tx')
locked_coin = tx.vout[locked_n].value
# Check script and values
A, B, csv_val, C = extractScriptLockRefundScriptValues(script_out)
ensure(A == Kal, 'Bad script pubkey')
ensure(B == Kaf, 'Bad script pubkey')
ensure(csv_val == csv_val_expect, 'Bad script csv value')
ensure(C == Kaf, 'Bad script pubkey')
fee_paid = swap_value - locked_coin
assert (fee_paid > 0)
dummy_witness_stack = self.getScriptLockTxDummyWitness(prevout_script)
size = len(self.setTxSignature(tx.serialize(), dummy_witness_stack))
fee_rate_paid = fee_paid * 1000 // size
self._log.info('tx amount, size, feerate: %ld, %ld, %ld', locked_coin, size, fee_rate_paid)
if not self.compareFeeRates(fee_rate_paid, feerate):
raise ValueError('Bad fee rate, expected: {}'.format(feerate))
return txid, locked_coin, locked_n
def verifySCLockRefundSpendTx(self, tx_bytes, lock_refund_tx_bytes,
lock_refund_tx_id, prevout_script,
Kal,
prevout_n, prevout_value, feerate, vkbv=None):
# Verify:
# Must have only one input with correct prevout (n is always 0) and sequence
# Must have only one output sending lock refund tx value - fee to leader's address, TODO: follower shouldn't need to verify destination addr
tx = self.loadTx(tx_bytes)
txid = self.getTxid(tx)
self._log.info('Verifying lock refund spend tx: {}.'.format(b2h(txid)))
ensure(tx.version == self.txVersion(), 'Bad version')
ensure(tx.locktime == 0, 'locktime not 0')
ensure(tx.expiry == 0, 'Bad expiry')
ensure(len(tx.vin) == 1, 'tx doesn\'t have one input')
ensure(tx.vin[0].sequence == 0, 'Bad input sequence')
ensure(len(tx.vin[0].signature_script) == 0, 'Input sig not empty')
ensure(i2b(tx.vin[0].prevout.hash) == lock_refund_tx_id and tx.vin[0].prevout.n == 0 and tx.vin[0].prevout.tree == 0, 'Input prevout mismatch')
ensure(len(tx.vout) == 1, 'tx doesn\'t have one output')
# Destination doesn't matter to the follower
'''
p2wpkh = CScript([OP_0, hash160(Kal)])
locked_n = findOutput(tx, p2wpkh)
ensure(locked_n is not None, 'Output not found in lock refund spend tx')
'''
tx_value = tx.vout[0].value
fee_paid = prevout_value - tx_value
assert (fee_paid > 0)
dummy_witness_stack = self.getScriptLockRefundSpendTxDummyWitness(prevout_script)
size = len(self.setTxSignature(tx.serialize(), dummy_witness_stack))
fee_rate_paid = fee_paid * 1000 // size
self._log.info('tx amount, size, feerate: %ld, %ld, %ld', tx_value, size, fee_rate_paid)
if not self.compareFeeRates(fee_rate_paid, feerate):
raise ValueError('Bad fee rate, expected: {}'.format(feerate))
return True
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_amount = tx_lock_refund.vout[locked_n].value
A, B, lock2_value, C = extractScriptLockRefundScriptValues(script_lock_refund)
tx_lock_refund_hash_int = b2i(tx_lock_refund.TxHash())
tx = CTransaction()
tx.version = self.txVersion()
tx.vin.append(CTxIn(COutPoint(tx_lock_refund_hash_int, locked_n, 0),
sequence=lock2_value,))
tx.vout.append(self.txoType()(locked_amount, self.getPubkeyHashDest(pkh_dest)))
dummy_witness_stack = self.getScriptLockRefundSwipeTxDummyWitness(script_lock_refund)
size = len(self.setTxSignature(tx.serialize(), dummy_witness_stack))
pay_fee = round(tx_fee_rate * size / 1000)
tx.vout[0].value = locked_amount - pay_fee
self._log.info('createSCLockRefundSpendToFTx %s:\n fee_rate, size, fee: %ld, %ld, %ld.',
tx.TxHash().hex(), tx_fee_rate, size, pay_fee)
return tx.serialize(TxSerializeType.NoWitness)
def signTxOtVES(self, key_sign: bytes, pubkey_encrypt: bytes, tx_bytes: bytes, input_n: int, prevout_script: bytes, prevout_value: int) -> bytes:
tx = self.loadTx(tx_bytes)
sig_hash = DCRSignatureHash(prevout_script, SigHashType.SigHashAll, tx, input_n)
return ecdsaotves_enc_sign(key_sign, pubkey_encrypt, sig_hash)
def verifyTxOtVES(self, tx_bytes: bytes, ct: bytes, Ks: bytes, Ke: bytes, input_n: int, prevout_script: bytes, prevout_value):
tx = self.loadTx(tx_bytes)
sig_hash = DCRSignatureHash(prevout_script, SigHashType.SigHashAll, tx, input_n)
return ecdsaotves_enc_verify(Ks, Ke, sig_hash, ct)
def decryptOtVES(self, k: bytes, esig: bytes) -> bytes:
return ecdsaotves_dec_sig(k, esig) + bytes((SigHashType.SigHashAll,))
def recoverEncKey(self, esig, sig, K):
return ecdsaotves_rec_enc_key(K, esig, sig[:-1]) # Strip sighash type
def getTxOutputPos(self, tx, script):
if isinstance(tx, bytes):
tx = self.loadTx(tx)
script_pk = self.getScriptDest(script)
return findOutput(tx, script_pk)
def getScriptLockTxDummyWitness(self, script: bytes):
return [
bytes(72),
bytes(72),
bytes(len(script))
]
def getScriptLockRefundSpendTxDummyWitness(self, script: bytes):
return [
bytes(72),
bytes(72),
bytes((1,)),
bytes(len(script))
]
def extractLeaderSig(self, tx_bytes: bytes) -> bytes:
tx = self.loadTx(tx_bytes)
sig_len = tx.vin[0].signature_script[0]
return tx.vin[0].signature_script[1: 1 + sig_len]
def extractFollowerSig(self, tx_bytes: bytes) -> bytes:
tx = self.loadTx(tx_bytes)
sig_len = tx.vin[0].signature_script[0]
ofs = 1 + sig_len
sig_len = tx.vin[0].signature_script[ofs]
ofs += 1
return tx.vin[0].signature_script[ofs: ofs + sig_len]
def unlockInputs(self, tx_bytes):
tx = self.loadTx(tx_bytes)
inputs = []
for txi in tx.vin:
inputs.append({'amount': float(self.format_amount(txi.value_in)), 'txid': i2h(txi.prevout.hash), 'vout': txi.prevout.n, 'tree': txi.prevout.tree})
self.rpc_wallet('lockunspent', [True, inputs])
def getWalletRestoreHeight(self) -> int:
start_time = self.rpc_wallet('getinfo')['keypoololdest']
blockchaininfo = self.getBlockchainInfo()
best_block = blockchaininfo['bestblockhash']
chain_synced = round(blockchaininfo['verificationprogress'], 3)
if chain_synced < 1.0:
raise ValueError('{} chain isn\'t synced.'.format(self.coin_name()))
self._log.debug('Finding block at time: {}'.format(start_time))
rpc_conn = self.open_rpc()
try:
block_hash = best_block
while True:
block_header = self.json_request(rpc_conn, 'getblockheader', [block_hash])
if block_header['time'] < start_time:
return block_header['height']
# genesis block
if block_header['previousblockhash'] == '0000000000000000000000000000000000000000000000000000000000000000':
return block_header['height']
block_hash = block_header['previousblockhash']
finally:
self.close_rpc(rpc_conn)
raise ValueError('{} wallet restore height not found.'.format(self.coin_name()))
def createBLockTx(self, Kbs, output_amount, vkbv=None) -> bytes:
tx = CTransaction()
tx.version = self.txVersion()
script_pk = self.getPkDest(Kbs)
tx.vout.append(self.txoType()(output_amount, script_pk))
return tx.serialize()
def publishBLockTx(self, kbv, Kbs, output_amount, feerate, unlock_time: int = 0) -> bytes:
b_lock_tx = self.createBLockTx(Kbs, output_amount)
b_lock_tx = self.fundTx(b_lock_tx, feerate)
b_lock_tx_id = self.getTxid(b_lock_tx)
b_lock_tx = self.signTxWithWallet(b_lock_tx)
return bytes.fromhex(self.publishTx(b_lock_tx))
def getBLockSpendTxFee(self, tx, fee_rate: int) -> int:
witness_bytes = 120 # TODO
size = len(tx.serialize()) + witness_bytes
pay_fee = round(fee_rate * size / 1000)
self._log.info(f'BLockSpendTx fee_rate, vsize, fee: {fee_rate}, {size}, {pay_fee}.')
return pay_fee
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, lock_tx_vout=None) -> bytes:
self._log.info('spendBLockTx %s:\n', chain_b_lock_txid.hex())
locked_n = lock_tx_vout
Kbs = self.getPubkey(kbs)
script_pk = self.getPkDest(Kbs)
if locked_n is None:
self._log.debug(f'Unknown lock vout, searching tx: {chain_b_lock_txid.hex()}')
# When refunding a lock tx, it should be in the wallet as a sent tx
wtx = self.rpc_wallet('gettransaction', [chain_b_lock_txid.hex(), ])
lock_tx = self.loadTx(bytes.fromhex(wtx['hex']))
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.version = self.txVersion()
chain_b_lock_txid_int = b2i(chain_b_lock_txid)
tx.vin.append(CTxIn(COutPoint(chain_b_lock_txid_int, locked_n, 0),
sequence=0))
tx.vout.append(self.txoType()(cb_swap_value, self.getPubkeyHashDest(pkh_to)))
pay_fee = self.getBLockSpendTxFee(tx, b_fee)
tx.vout[0].value = cb_swap_value - pay_fee
b_lock_spend_tx = tx.serialize()
b_lock_spend_tx = self.signTxWithKey(b_lock_spend_tx, kbs)
return bytes.fromhex(self.publishTx(b_lock_spend_tx))
def findTxnByHash(self, txid_hex: str):
try:
txout = self.rpc('gettxout', [txid_hex, 0, 0, True])
except Exception as e:
# self._log.warning('gettxout {}'.format(e))
return None
confirmations: int = 0 if 'confirmations' not in txout else txout['confirmations']
if confirmations >= self.blocks_confirmed:
block_height = self.getChainHeight() - confirmations # TODO: Better way?
return {'txid': txid_hex, 'amount': 0, 'height': block_height}
return None
def isTxExistsError(self, err_str: str) -> bool:
return 'transaction already exists' in err_str or 'already have transaction' in err_str
def isTxNonFinalError(self, err_str: str) -> bool:
return 'locks on inputs not met' in err_str

@ -153,7 +153,7 @@ class CTransaction:
o += script_bytes
def serialize(self, ser_type=TxSerializeType.Full) -> bytes:
data = bytearray()
data = bytes()
version = (self.version & 0xffff) | (ser_type << 16)
data += version.to_bytes(4, 'little')
@ -195,3 +195,10 @@ class CTransaction:
def TxHashFull(self) -> bytes:
raise ValueError('todo')
def findOutput(tx, script_pk: bytes):
for i in range(len(tx.vout)):
if tx.vout[i].script_pubkey == script_pk:
return i
return None

@ -27,6 +27,15 @@ def callrpc(rpc_port, auth, method, params=[], host='127.0.0.1'):
return r['result']
def openrpc(rpc_port, auth, host='127.0.0.1'):
try:
url = 'http://{}@{}:{}/'.format(auth, host, rpc_port)
return Jsonrpc(url)
except Exception as ex:
traceback.print_exc()
raise ValueError('RPC error ' + str(ex))
def make_rpc_func(port, auth, host='127.0.0.1'):
port = port
auth = auth

@ -9,14 +9,19 @@ OP_0 = 0x00
OP_DATA_1 = 0x01
OP_1NEGATE = 0x4f
OP_1 = 0x51
OP_IF = 0x63
OP_ELSE = 0x67
OP_ENDIF = 0x68
OP_DROP = 0x75
OP_DUP = 0x76
OP_EQUAL = 0x87
OP_EQUALVERIFY = 0x88
OP_PUSHDATA1 = 0x4c
OP_PUSHDATA2 = 0x4d
OP_PUSHDATA4 = 0x4e
OP_DUP = 0x76
OP_EQUALVERIFY = 0x88
OP_HASH160 = 0xa9
OP_CHECKSIG = 0xac
OP_CHECKMULTISIG = 0xae
OP_CHECKSEQUENCEVERIFY = 0xb2

@ -76,7 +76,7 @@ class FIROInterface(BTCInterface):
return addr_info['ismine']
return addr_info['ismine'] or addr_info['iswatchonly']
def getSCLockScriptAddress(self, lock_script):
def getSCLockScriptAddress(self, lock_script: bytes) -> str:
lock_tx_dest = self.getScriptDest(lock_script)
address = self.encodeScriptDest(lock_tx_dest)
@ -201,7 +201,7 @@ class FIROInterface(BTCInterface):
add_bytes = 107
size = len(tx.serialize_with_witness()) + add_bytes
pay_fee = round(fee_rate * size / 1000)
self._log.info(f'BLockSpendTx fee_rate, size, fee: {fee_rate}, {size}, {pay_fee}.')
self._log.info(f'BLockSpendTx fee_rate, size, fee: {fee_rate}, {size}, {pay_fee}.')
return pay_fee
def signTxWithKey(self, tx: bytes, key: bytes) -> bytes:

@ -13,7 +13,12 @@ from coincurve.keys import (
PublicKey,
PrivateKey,
)
from .btc import BTCInterface, find_vout_for_address_from_txobj, findOutput
from basicswap.interface.btc import (
BTCInterface,
extractScriptLockRefundScriptValues,
findOutput,
find_vout_for_address_from_txobj,
)
from basicswap.rpc import make_rpc_func
from basicswap.chainparams import Coins
from basicswap.interface.contrib.nav_test_framework.mininode import (
@ -24,7 +29,6 @@ from basicswap.interface.contrib.nav_test_framework.mininode import (
CTransaction,
CTxInWitness,
FromHex,
uint256_from_str,
)
from basicswap.util.crypto import hash160
from basicswap.util.address import (
@ -33,7 +37,7 @@ from basicswap.util.address import (
encodeAddress,
)
from basicswap.util import (
i2b, i2h,
b2i, i2b, i2h,
ensure,
)
from basicswap.basicswap_util import (
@ -305,7 +309,7 @@ class NAVInterface(BTCInterface):
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])
prev_txid = b2i(bytes.fromhex(prevout['txid']))
tx.vin.append(CTxIn(COutPoint(prev_txid, prevout['vout']),
scriptSig=self.getScriptScriptSig(txn_script)))
@ -319,7 +323,7 @@ class NAVInterface(BTCInterface):
tx = CTransaction()
tx.nVersion = self.txVersion()
tx.nLockTime = locktime
prev_txid = uint256_from_str(bytes.fromhex(prevout['txid'])[::-1])
prev_txid = b2i(bytes.fromhex(prevout['txid']))
tx.vin.append(CTxIn(COutPoint(prev_txid, prevout['vout']),
nSequence=sequence,
scriptSig=self.getScriptScriptSig(txn_script)))
@ -512,7 +516,7 @@ class NAVInterface(BTCInterface):
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:
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, lock_tx_vout=None) -> bytes:
self._log.info('spendBLockTx %s:\n', chain_b_lock_txid.hex())
wtx = self.rpc('gettransaction', [chain_b_lock_txid.hex(), ])
lock_tx = self.loadTx(bytes.fromhex(wtx['hex']))
@ -526,7 +530,7 @@ class NAVInterface(BTCInterface):
tx = CTransaction()
tx.nVersion = self.txVersion()
chain_b_lock_txid_int = uint256_from_str(chain_b_lock_txid[::-1])
chain_b_lock_txid_int = b2i(chain_b_lock_txid)
script_sig = self.getInputScriptForPubkeyHash(self.getPubkeyHash(Kbs))
@ -678,7 +682,7 @@ class NAVInterface(BTCInterface):
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)
A, B, lock2_value, C = extractScriptLockRefundScriptValues(script_lock_refund)
tx_lock_refund.rehash()
tx_lock_refund_hash_int = tx_lock_refund.sha256

@ -28,8 +28,13 @@ from basicswap.util.script import (
from basicswap.util.address import (
encodeStealthAddress,
)
from basicswap.interface.btc import (
BTCInterface,
extractScriptLockScriptValues,
extractScriptLockRefundScriptValues,
)
from basicswap.chainparams import Coins, chainparams
from .btc import BTCInterface
class BalanceTypes(IntEnum):
@ -354,7 +359,7 @@ class PARTInterfaceBlind(PARTInterface):
lock_txo_scriptpk = bytes.fromhex(lock_tx_obj['vout'][lock_output_n]['scriptPubKey']['hex'])
script_pk = CScript([OP_0, hashlib.sha256(script_out).digest()])
ensure(lock_txo_scriptpk == script_pk, 'Bad output script')
A, B = self.extractScriptLockScriptValues(script_out)
A, B = extractScriptLockScriptValues(script_out)
ensure(A == Kal, 'Bad script leader pubkey')
ensure(B == Kaf, 'Bad script follower pubkey')
@ -402,7 +407,7 @@ class PARTInterfaceBlind(PARTInterface):
lock_refund_txo_scriptpk = bytes.fromhex(lock_refund_tx_obj['vout'][lock_refund_output_n]['scriptPubKey']['hex'])
script_pk = CScript([OP_0, hashlib.sha256(script_out).digest()])
ensure(lock_refund_txo_scriptpk == script_pk, 'Bad output script')
A, B, csv_val, C = self.extractScriptLockRefundScriptValues(script_out)
A, B, csv_val, C = extractScriptLockRefundScriptValues(script_out)
ensure(A == Kal, 'Bad script pubkey')
ensure(B == Kaf, 'Bad script pubkey')
ensure(csv_val == csv_val_expect, 'Bad script csv value')
@ -632,7 +637,7 @@ class PARTInterfaceBlind(PARTInterface):
addr_info = self.rpc_wallet('getaddressinfo', [addr_out])
output_pubkey_hex = addr_info['pubkey']
A, B, lock2_value, C = self.extractScriptLockRefundScriptValues(script_lock_refund)
A, B, lock2_value, C = extractScriptLockRefundScriptValues(script_lock_refund)
# Follower won't be able to decode output to check amount, shouldn't matter as fee is public and output is to leader, sum has to balance
@ -715,7 +720,7 @@ class PARTInterfaceBlind(PARTInterface):
return -1
return None
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, spend_actual_balance: bool = False) -> bytes:
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, spend_actual_balance: bool = False, lock_tx_vout=None) -> bytes:
Kbv = self.getPubkey(kbv)
Kbs = self.getPubkey(kbs)
sx_addr = self.formatStealthAddress(Kbv, Kbs)
@ -851,7 +856,7 @@ class PARTInterfaceAnon(PARTInterface):
return -1
return None
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, spend_actual_balance: bool = False) -> bytes:
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, spend_actual_balance: bool = False, lock_tx_vout=None) -> bytes:
Kbv = self.getPubkey(kbv)
Kbs = self.getPubkey(kbs)
sx_addr = self.formatStealthAddress(Kbv, Kbs)

@ -107,7 +107,7 @@ class PIVXInterface(BTCInterface):
add_bytes = 107
size = len(tx.serialize_with_witness()) + add_bytes
pay_fee = round(fee_rate * size / 1000)
self._log.info(f'BLockSpendTx fee_rate, size, fee: {fee_rate}, {size}, {pay_fee}.')
self._log.info(f'BLockSpendTx fee_rate, size, fee: {fee_rate}, {size}, {pay_fee}.')
return pay_fee
def signTxWithKey(self, tx: bytes, key: bytes) -> bytes:

@ -409,7 +409,7 @@ class XMRInterface(CoinInterface):
return None
def spendBLockTx(self, chain_b_lock_txid: bytes, address_to: str, kbv: bytes, kbs: bytes, cb_swap_value: int, b_fee_rate: int, restore_height: int, spend_actual_balance: bool = False) -> bytes:
def spendBLockTx(self, chain_b_lock_txid: bytes, address_to: str, kbv: bytes, kbs: bytes, cb_swap_value: int, b_fee_rate: int, restore_height: int, spend_actual_balance: bool = False, lock_tx_vout=None) -> bytes:
'''
Notes:
"Error: No unlocked balance in the specified subaddress(es)" can mean not enough funds after tx fee.

@ -21,13 +21,17 @@ from basicswap.basicswap_util import (
from . import ProtocolInterface
from basicswap.contrib.test_framework.script import (
CScript, CScriptOp,
OP_CHECKMULTISIG)
OP_CHECKMULTISIG
)
def addLockRefundSigs(self, xmr_swap, ci):
self.log.debug('Setting lock refund tx sigs')
witness_stack = [
b'',
witness_stack = []
if ci.coin_type() not in (Coins.DCR, ):
witness_stack += [b'', ]
witness_stack += [
xmr_swap.al_lock_refund_tx_sig,
xmr_swap.af_lock_refund_tx_sig,
xmr_swap.a_lock_tx_script,
@ -74,7 +78,8 @@ def recoverNoScriptTxnWithKey(self, bid_id: bytes, encoded_key):
address_to = self.getCachedStealthAddressForCoin(offer.coin_to)
amount = bid.amount_to
txid = ci_to.spendBLockTx(xmr_swap.b_lock_tx_id, address_to, xmr_swap.vkbv, vkbs, bid.amount_to, xmr_offer.b_fee_rate, bid.chain_b_height_start, spend_actual_balance=True)
lock_tx_vout = bid.getLockTXBVout()
txid = ci_to.spendBLockTx(xmr_swap.b_lock_tx_id, address_to, xmr_swap.vkbv, vkbs, amount, xmr_offer.b_fee_rate, bid.chain_b_height_start, spend_actual_balance=True, lock_tx_vout=lock_tx_vout)
self.log.debug('Submitted lock B spend txn %s to %s chain for bid %s', txid.hex(), ci_to.coin_name(), bid_id.hex())
self.logBidEvent(bid.bid_id, EventLogTypes.LOCK_TX_B_SPEND_TX_PUBLISHED, txid.hex(), session)
session.commit()

@ -419,9 +419,10 @@ def compare_bid_states(states, expect_states, exact_match: bool = True) -> bool:
return True
def compare_bid_states_unordered(states, expect_states) -> bool:
def compare_bid_states_unordered(states, expect_states, ignore_states=[]) -> bool:
ignore_states.append('Bid Delaying')
for i in range(len(states) - 1, -1, -1):
if states[i][1] == 'Bid Delaying':
if states[i][1] in ignore_states:
del states[i]
try:

@ -5,9 +5,6 @@
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
# TODO
# - Occasionally DCR simnet chain stalls.
import copy
import logging
import os
@ -27,6 +24,7 @@ from basicswap.basicswap import (
)
from basicswap.basicswap_util import (
TxLockTypes,
TxTypes
)
from basicswap.util.crypto import (
hash160
@ -80,7 +78,25 @@ def make_rpc_func(node_id, base_rpc_port):
return rpc_func
def test_success_path(self, coin_from: Coins, coin_to: Coins):
def wait_for_dcr_height(http_port, num_blocks=3):
logging.info('Waiting for DCR chain height %d', num_blocks)
for i in range(60):
if test_delay_event.is_set():
raise ValueError('Test stopped.')
try:
wallet = read_json_api(http_port, 'wallets/dcr')
decred_blocks = wallet['blocks']
print('decred_blocks', decred_blocks)
if decred_blocks >= num_blocks:
return
except Exception as e:
print('Error reading wallets', str(e))
test_delay_event.wait(1)
raise ValueError(f'wait_for_decred_blocks failed http_port: {http_port}')
def run_test_success_path(self, coin_from: Coins, coin_to: Coins):
logging.info(f'---------- Test {coin_from.name} to {coin_to.name}')
node_from = 0
@ -106,7 +122,7 @@ def test_success_path(self, coin_from: Coins, coin_to: Coins):
swap_clients[node_from].acceptBid(bid_id)
wait_for_bid(test_delay_event, swap_clients[node_from], bid_id, BidStates.SWAP_COMPLETED, wait_for=120)
wait_for_bid(test_delay_event, swap_clients[node_to], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=120)
wait_for_bid(test_delay_event, swap_clients[node_to], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=30)
# Verify lock tx spends are found in the expected wallets
bid, offer = swap_clients[node_from].getBidAndOffer(bid_id)
@ -139,7 +155,7 @@ def test_success_path(self, coin_from: Coins, coin_to: Coins):
assert (compare_bid_states(bidder_states, self.states_bidder_sh[0]) is True)
def test_bad_ptx(self, coin_from: Coins, coin_to: Coins):
def run_test_bad_ptx(self, coin_from: Coins, coin_to: Coins):
# Invalid PTX sent, swap should stall and ITx and PTx should be reclaimed by senders
logging.info(f'---------- Test bad ptx {coin_from.name} to {coin_to.name}')
@ -164,7 +180,7 @@ def test_bad_ptx(self, coin_from: Coins, coin_to: Coins):
swap_clients[node_to].setBidDebugInd(bid_id, DebugTypes.MAKE_INVALID_PTX)
wait_for_bid(test_delay_event, swap_clients[node_from], bid_id, BidStates.SWAP_COMPLETED, wait_for=120)
wait_for_bid(test_delay_event, swap_clients[node_to], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=120)
wait_for_bid(test_delay_event, swap_clients[node_to], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=30)
js_0_bid = read_json_api(1800 + node_from, 'bids/{}'.format(bid_id.hex()))
js_1_bid = read_json_api(1800 + node_to, 'bids/{}'.format(bid_id.hex()))
@ -190,6 +206,8 @@ def test_bad_ptx(self, coin_from: Coins, coin_to: Coins):
offerer_states = read_json_api(1800 + node_from, path)
bidder_states = read_json_api(1800 + node_to, path)
if coin_to not in (Coins.XMR,):
return
# Hard to get the timing right
assert (compare_bid_states_unordered(offerer_states, self.states_offerer_sh[1]) is True)
assert (compare_bid_states_unordered(bidder_states, self.states_bidder_sh[1]) is True)
@ -200,7 +218,7 @@ def test_bad_ptx(self, coin_from: Coins, coin_to: Coins):
assert (js_1['num_swapping'] == 0 and js_1['num_watched_outputs'] == 0)
def test_itx_refund(self, coin_from: Coins, coin_to: Coins):
def run_test_itx_refund(self, coin_from: Coins, coin_to: Coins):
# Offerer claims PTX and refunds ITX after lock expires
# Bidder loses PTX value without gaining ITX value
logging.info(f'---------- Test itx refund {coin_from.name} to {coin_to.name}')
@ -250,6 +268,189 @@ def test_itx_refund(self, coin_from: Coins, coin_to: Coins):
assert (bid.amount_to - node_from_ci_to.make_int(wtx['details'][0]['amount']) < max_fee)
def run_test_ads_success_path(self, coin_from: Coins, coin_to: Coins):
logging.info(f'---------- Test ADS swap {coin_from.name} to {coin_to.name}')
# Offerer sends the offer
# Bidder sends the bid
id_offerer: int = 0
id_bidder: int = 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_bidder].ci(coin_to)
self.prepare_balance(coin_to, 100.0, 1801, 1800)
self.prepare_balance(coin_from, 100.0, 1800, 1801)
# Leader sends the initial (chain a) lock tx.
# Follower sends the participate (chain b) lock tx.
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}')
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)
wait_for_offer(test_delay_event, swap_clients[id_bidder], offer_id)
bid_id = swap_clients[id_bidder].postXmrBid(offer_id, amt_swap)
wait_for_bid(test_delay_event, swap_clients[id_offerer], bid_id, BidStates.BID_RECEIVED)
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=(30))
if reverse_bid:
return # TODO
# Verify lock tx spends are found in the expected wallets
bid, xmr_swap = swap_clients[id_offerer].getXmrBid(bid_id)
node_from_ci_to = swap_clients[0].ci(coin_to)
max_fee: int = 10000
if node_from_ci_to.coin_type() in (Coins.XMR, ):
pass
else:
wtx = node_from_ci_to.rpc_wallet('gettransaction', [bid.xmr_b_lock_tx.spend_txid.hex(),])
assert (bid.amount_to - node_from_ci_to.make_int(wtx['details'][0]['amount']) < max_fee)
node_to_ci_from = swap_clients[1].ci(coin_from)
if node_to_ci_from.coin_type() in (Coins.XMR, ):
pass
else:
wtx = node_to_ci_from.rpc_wallet('gettransaction', [xmr_swap.a_lock_spend_tx_id.hex(),])
assert (bid.amount - node_to_ci_from.make_int(wtx['details'][0]['amount']) < max_fee)
bid_id_hex = bid_id.hex()
path = f'bids/{bid_id_hex}/states'
offerer_states = read_json_api(1800 + id_offerer, path)
bidder_states = read_json_api(1800 + id_bidder, path)
assert (compare_bid_states(offerer_states, self.states_offerer[0]) is True)
assert (compare_bid_states(bidder_states, self.states_bidder[0]) is True)
def run_test_ads_both_refund(self, coin_from: Coins, coin_to: Coins, lock_value: int = 32) -> None:
logging.info('---------- Test {} to {} both lock txns refunded'.format(coin_from.name, coin_to.name))
id_offerer: int = 0
id_bidder: int = 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)
if reverse_bid:
self.prepare_balance(coin_to, 100.0, 1801, 1800)
self.prepare_balance(coin_from, 100.0, 1800, 1801)
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}')
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)
debug_case = (None, DebugTypes.OFFER_LOCK_2_VALUE_INC)
try:
swap_clients[0]._debug_cases.append(debug_case)
swap_clients[1]._debug_cases.append(debug_case)
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[id_bidder], offer_id)
offer = swap_clients[id_bidder].getOffer(offer_id)
finally:
swap_clients[0]._debug_cases.remove(debug_case)
swap_clients[1]._debug_cases.remove(debug_case)
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)
swap_clients[id_follower].setBidDebugInd(bid_id, DebugTypes.CREATE_INVALID_COIN_B_LOCK)
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=(self.extra_wait_time + 180))
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 + 40))
if reverse_bid:
return # TODO
# Verify lock tx spends are found in the expected wallets
bid, xmr_swap = swap_clients[id_offerer].getXmrBid(bid_id)
lock_refund_spend_txid = bid.txns[TxTypes.XMR_SWAP_A_LOCK_REFUND_SPEND].txid
bid, xmr_swap = swap_clients[id_bidder].getXmrBid(bid_id)
node_from_ci_from = swap_clients[0].ci(coin_from)
max_fee: int = 10000
if node_from_ci_from.coin_type() in (Coins.XMR, ):
pass
else:
wtx = node_from_ci_from.rpc_wallet('gettransaction', [lock_refund_spend_txid.hex(),])
assert (bid.amount - node_from_ci_from.make_int(wtx['details'][0]['amount']) < max_fee)
node_to_ci_to = swap_clients[1].ci(coin_to)
if node_to_ci_to.coin_type() in (Coins.XMR, ):
pass
else:
wtx = node_to_ci_to.rpc_wallet('gettransaction', [bid.xmr_b_lock_tx.spend_txid.hex(),])
assert (bid.amount_to - node_to_ci_to.make_int(wtx['details'][0]['amount']) < max_fee)
bid_id_hex = bid_id.hex()
path = f'bids/{bid_id_hex}/states'
offerer_states = read_json_api(1800 + id_offerer, path)
bidder_states = read_json_api(1800 + id_bidder, path)
assert (compare_bid_states(offerer_states, self.states_offerer[1]) is True)
assert (compare_bid_states(bidder_states, self.states_bidder[1]) is True)
def run_test_ads_swipe_refund(self, coin_from: Coins, coin_to: Coins, lock_value: int = 32) -> None:
logging.info('---------- Test {} to {} coin a lock refund tx swiped'.format(coin_from.name, coin_to.name))
id_offerer: int = 0
id_bidder: int = 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}')
if reverse_bid:
self.prepare_balance(coin_to, 100.0, 1801, 1800)
self.prepare_balance(coin_from, 100.0, 1800, 1801)
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[id_bidder], offer_id)
offer = swap_clients[id_bidder].getOffer(offer_id)
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)
swap_clients[id_follower].setBidDebugInd(bid_id, DebugTypes.BID_STOP_AFTER_COIN_A_LOCK)
swap_clients[id_leader].setBidDebugInd(bid_id, DebugTypes.BID_DONT_SPEND_COIN_A_LOCK_REFUND)
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=(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))
def prepareDCDDataDir(datadir, node_id, conf_file, dir_prefix, num_nodes=3):
node_dir = os.path.join(datadir, dir_prefix + str(node_id))
if not os.path.exists(node_dir):
@ -298,8 +499,9 @@ class Test(BaseTest):
test_coin = Coins.DCR
dcr_daemons = []
start_ltc_nodes = False
start_xmr_nodes = False
start_xmr_nodes = True
dcr_mining_addr = 'SsYbXyjkKAEXXcGdFgr4u4bo4L8RkCxwQpH'
extra_wait_time = 0
hex_seeds = [
'e8574b2a94404ee62d8acc0258cab4c0defcfab8a5dfc2f4954c1f9d7e09d72a',
@ -331,15 +533,19 @@ class Test(BaseTest):
ci0 = cls.swap_clients[0].ci(cls.test_coin)
num_passed: int = 0
for i in range(5):
for i in range(30):
try:
ci0.rpc_wallet('purchaseticket', [cls.dcr_ticket_account, 0.1, 0])
num_passed += 1
if num_passed >= 5:
break
test_delay_event.wait(0.1)
except Exception as e:
if 'double spend' in str(e):
pass
else:
logging.warning('coins_loop purchaseticket {}'.format(e))
test_delay_event.wait(0.5)
try:
if num_passed >= 5:
@ -436,6 +642,8 @@ class Test(BaseTest):
'address': js_w[coin_ticker][address_type],
'subfee': False,
}
if coin in (Coins.XMR, ):
post_json['sweepall'] = False
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_amount: float = amount
@ -763,22 +971,49 @@ class Test(BaseTest):
assert (amount_proved >= require_amount)
def test_02_part_coin(self):
test_success_path(self, Coins.PART, self.test_coin)
run_test_success_path(self, Coins.PART, self.test_coin)
def test_03_coin_part(self):
test_success_path(self, self.test_coin, Coins.PART)
run_test_success_path(self, self.test_coin, Coins.PART)
def test_04_part_coin_bad_ptx(self):
test_bad_ptx(self, Coins.PART, self.test_coin)
run_test_bad_ptx(self, Coins.PART, self.test_coin)
def test_05_coin_part_bad_ptx(self):
test_bad_ptx(self, self.test_coin, Coins.PART)
run_test_bad_ptx(self, self.test_coin, Coins.PART)
def test_06_part_coin_itx_refund(self):
test_itx_refund(self, Coins.PART, self.test_coin)
run_test_itx_refund(self, Coins.PART, self.test_coin)
def test_07_coin_part_itx_refund(self):
test_itx_refund(self, self.test_coin, Coins.PART)
run_test_itx_refund(self, self.test_coin, Coins.PART)
def test_08_ads_coin_xmr(self):
run_test_ads_success_path(self, self.test_coin, Coins.XMR)
def test_09_ads_xmr_coin(self):
# Reverse bid
run_test_ads_success_path(self, Coins.XMR, self.test_coin)
def test_10_ads_part_coin(self):
run_test_ads_success_path(self, Coins.PART, self.test_coin)
def test_11_ads_coin_xmr_both_refund(self):
run_test_ads_both_refund(self, self.test_coin, Coins.XMR, lock_value=20)
def test_12_ads_xmr_coin_both_refund(self):
# Reverse bid
run_test_ads_both_refund(self, Coins.XMR, self.test_coin, lock_value=20)
def test_13_ads_part_coin_both_refund(self):
run_test_ads_both_refund(self, Coins.PART, self.test_coin, lock_value=20)
def test_14_ads_coin_xmr_swipe_refund(self):
run_test_ads_swipe_refund(self, self.test_coin, Coins.XMR, lock_value=20)
def test_15_ads_xmr_coin_swipe_refund(self):
# Reverse bid
run_test_ads_swipe_refund(self, Coins.XMR, self.test_coin, lock_value=20)
if __name__ == '__main__':

@ -26,7 +26,7 @@ from basicswap.util import (
make_int,
format_amount,
)
from basicswap.interface import Curves
from basicswap.interface.base import Curves
from tests.basicswap.util import (
read_json_api,
)

@ -337,7 +337,7 @@ class Test(BaseTest):
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=80)
wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=80)
wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=30)
# Verify lock tx spends are found in the expected wallets
bid, offer = swap_clients[0].getBidAndOffer(bid_id)
@ -381,7 +381,7 @@ class Test(BaseTest):
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=80)
wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, wait_for=80)
wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, wait_for=30)
js_0 = read_json_api(1800)
js_1 = read_json_api(1801)
@ -404,7 +404,7 @@ class Test(BaseTest):
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=80)
wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=80)
wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=30)
js_0 = read_json_api(1800)
js_1 = read_json_api(1801)
@ -428,7 +428,7 @@ class Test(BaseTest):
swap_clients[0].acceptBid(bid_id)
wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=80)
wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.BID_ABANDONED, sent=True, wait_for=80)
wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.BID_ABANDONED, sent=True, wait_for=30)
js_0_bid = read_json_api(1800, 'bids/{}'.format(bid_id.hex()))
js_1_bid = read_json_api(1801, 'bids/{}'.format(bid_id.hex()))
@ -464,7 +464,7 @@ class Test(BaseTest):
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=80)
wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=80)
wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=30)
js_0 = read_json_api(1800)
assert (js_0['num_swapping'] == 0 and js_0['num_watched_outputs'] == 0)
@ -513,7 +513,7 @@ class Test(BaseTest):
bid_id = swap_clients[1].postBid(offer_id, offer.amount_from)
wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=80)
wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=80)
wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=30)
def test_10_bad_ptx(self):
# Invalid PTX sent, swap should stall and ITx and PTx should be reclaimed by senders
@ -534,7 +534,7 @@ class Test(BaseTest):
swap_clients[0].acceptBid(bid_id)
wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=120)
wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=120)
wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=30)
js_0_bid = read_json_api(1800, 'bids/{}'.format(bid_id.hex()))
js_1_bid = read_json_api(1801, 'bids/{}'.format(bid_id.hex()))
@ -574,7 +574,7 @@ class Test(BaseTest):
swap_clients[0].acceptBid(bid_id)
wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=120)
wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.BID_ABANDONED, sent=True, wait_for=120)
wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.BID_ABANDONED, sent=True, wait_for=30)
js_0_bid = read_json_api(1800, 'bids/{}'.format(bid_id.hex()))
js_1_bid = read_json_api(1801, 'bids/{}'.format(bid_id.hex()))
@ -727,7 +727,7 @@ class Test(BaseTest):
swap_clients[2].acceptBid(bid_id)
wait_for_bid(test_delay_event, swap_clients[2], bid_id, BidStates.SWAP_COMPLETED, wait_for=80)
wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=80)
wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=30)
# Verify expected inputs were used
bid, offer = swap_clients[2].getBidAndOffer(bid_id)
@ -762,7 +762,7 @@ class Test(BaseTest):
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=80)
wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, wait_for=80)
wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, wait_for=30)
def pass_99_delay(self):
logging.info('Delay')

@ -1528,7 +1528,7 @@ class Test(BaseTest):
swap_clients[0].acceptXmrBid(bid_id)
wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, wait_for=1800)
wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, wait_for=1800, sent=True)
wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, wait_for=30, sent=True)
def test_16_new_subaddress(self):
logging.info('---------- Test that new subaddresses are created')

Loading…
Cancel
Save