Continue building out the BCH interface

Adapt the swap flow to BCH specifics - txids change after funding and when signing inputs
BCH happy path (lock-spend) done
This commit is contained in:
mainnet-pat 2024-10-20 16:29:50 +00:00 committed by nahuhh
parent 58b42c0d9a
commit c009d555e7
6 changed files with 678 additions and 145 deletions

View file

@ -41,8 +41,12 @@ from .util import (
LockedCoinError, LockedCoinError,
TemporaryError, TemporaryError,
InactiveCoin, InactiveCoin,
b2h,
b2i,
format_timestamp, format_timestamp,
DeserialiseNum, DeserialiseNum,
h2b,
i2b,
zeroIfNone, zeroIfNone,
make_int, make_int,
ensure, ensure,
@ -675,7 +679,7 @@ class BasicSwap(BaseApp):
return self.coin_clients[use_coinid][interface_ind] return self.coin_clients[use_coinid][interface_ind]
def isBchXmrSwap(self, offer: Offer): def isBchXmrSwap(self, offer: Offer):
return (offer['coin_from'] == Coins.BCH or offer['coin_to'] == Coins.BCH) and offer.swap_type == SwapTypes.XMR_SWAP return (offer.coin_from == Coins.BCH or offer.coin_to == Coins.BCH) and offer.swap_type == SwapTypes.XMR_SWAP
def pi(self, protocol_ind): def pi(self, protocol_ind):
if protocol_ind not in self.protocolInterfaces: if protocol_ind not in self.protocolInterfaces:
@ -2103,24 +2107,25 @@ class BasicSwap(BaseApp):
else: else:
raise ValueError('Wallet seed doesn\'t match expected.') raise ValueError('Wallet seed doesn\'t match expected.')
def getCachedAddressForCoin(self, coin_type): def getCachedAddressForCoin(self, coin_type, session=None):
self.log.debug('getCachedAddressForCoin %s', Coins(coin_type).name) self.log.debug('getCachedAddressForCoin %s', Coins(coin_type).name)
# TODO: auto refresh after used # TODO: auto refresh after used
ci = self.ci(coin_type) ci = self.ci(coin_type)
key_str = 'receive_addr_' + ci.coin_name().lower() key_str = 'receive_addr_' + ci.coin_name().lower()
session = self.openSession() use_session = self.openSession(session)
try: try:
try: try:
addr = session.query(DBKVString).filter_by(key=key_str).first().value addr = use_session.query(DBKVString).filter_by(key=key_str).first().value
except Exception: except Exception:
addr = self.getReceiveAddressForCoin(coin_type) addr = self.getReceiveAddressForCoin(coin_type)
session.add(DBKVString( use_session.add(DBKVString(
key=key_str, key=key_str,
value=addr value=addr
)) ))
finally: finally:
self.closeSession(session) if session is None:
self.closeSession(use_session)
return addr return addr
def cacheNewStealthAddressForCoin(self, coin_type): def cacheNewStealthAddressForCoin(self, coin_type):
@ -2982,28 +2987,27 @@ class BasicSwap(BaseApp):
refundExtraArgs = dict() refundExtraArgs = dict()
lockExtraArgs = dict() lockExtraArgs = dict()
if self.isBchXmrSwap(offer): if self.isBchXmrSwap(offer):
pkh_refund_to = ci_from.decodeAddress(self.getCachedAddressForCoin(coin_from)) pkh_refund_to = ci_from.decodeAddress(self.getCachedAddressForCoin(offer.coin_from, use_session))
pkh_dest = xmr_swap.dest_af pkh_dest = xmr_swap.dest_af
# refund script # refund script
refundExtraArgs['mining_fee'] = 1000 refundExtraArgs['mining_fee'] = 1000
refundExtraArgs['out_1'] = ci_from.getScriptForPubkeyHash(pkh_refund_to) refundExtraArgs['out_1'] = ci_from.getScriptForPubkeyHash(pkh_refund_to)
refundExtraArgs['out_2'] = ci_from.getScriptForPubkeyHash(pkh_dest) refundExtraArgs['out_2'] = ci_from.getScriptForPubkeyHash(pkh_dest)
refundExtraArgs['public_key'] = xmr_swap.pkaf refundExtraArgs['public_key'] = xmr_swap.pkaf
refundExtraArgs['timelock'] = xmr_offer.lock_time_2 refundExtraArgs['timelock'] = xmr_offer.lock_time_2
refund_lock_tx_script = ci_from.genScriptLockTxScript(ci_from, xmr_swap.pkal, xmr_swap.pkaf, **refundExtraArgs)
refund_lock_tx_script = pi.genScriptLockTxScript(ci_from, xmr_swap.pkal, xmr_swap.pkaf, refundExtraArgs)
# will make use of this in `createSCLockRefundTx` # will make use of this in `createSCLockRefundTx`
refundExtraArgs['refund_lock_tx_script'] = refund_lock_tx_script refundExtraArgs['refund_lock_tx_script'] = refund_lock_tx_script
# lock script # lock script
lockExtraArgs['mining_fee'] = 1000 lockExtraArgs['mining_fee'] = 1000
lockExtraArgs['out_1'] = ci_from.getScriptForPubkeyHash(pkh_refund_to) lockExtraArgs['out_1'] = ci_from.getScriptForPubkeyHash(pkh_dest)
lockExtraArgs['out_2'] = ci_from.scriptToP2SH32LockingBytecode(refund_lock_tx_script) lockExtraArgs['out_2'] = ci_from.scriptToP2SH32LockingBytecode(refund_lock_tx_script)
lockExtraArgs['public_key'] = xmr_swap.pkal lockExtraArgs['public_key'] = xmr_swap.pkal
lockExtraArgs['timelock'] = xmr_offer.lock_time_1 lockExtraArgs['timelock'] = xmr_offer.lock_time_1
xmr_swap.a_lock_tx_script = pi.genScriptLockTxScript(ci_from, xmr_swap.pkal, xmr_swap.pkaf, lockExtraArgs)
xmr_swap.a_lock_tx_script = pi.genScriptLockTxScript(ci_from, xmr_swap.pkal, xmr_swap.pkaf, **lockExtraArgs)
prefunded_tx = self.getPreFundedTx(Concepts.OFFER, bid.offer_id, TxTypes.ITX_PRE_FUNDED, session=use_session) prefunded_tx = self.getPreFundedTx(Concepts.OFFER, bid.offer_id, TxTypes.ITX_PRE_FUNDED, session=use_session)
if prefunded_tx: if prefunded_tx:
xmr_swap.a_lock_tx = pi.promoteMockTx(ci_from, prefunded_tx, xmr_swap.a_lock_tx_script) xmr_swap.a_lock_tx = pi.promoteMockTx(ci_from, prefunded_tx, xmr_swap.a_lock_tx_script)
@ -3016,25 +3020,23 @@ class BasicSwap(BaseApp):
xmr_swap.a_lock_tx_id = ci_from.getTxid(xmr_swap.a_lock_tx) xmr_swap.a_lock_tx_id = ci_from.getTxid(xmr_swap.a_lock_tx)
a_lock_tx_dest = ci_from.getScriptDest(xmr_swap.a_lock_tx_script) a_lock_tx_dest = ci_from.getScriptDest(xmr_swap.a_lock_tx_script)
xmr_swap.a_lock_refund_tx, xmr_swap.a_lock_refund_tx_script, xmr_swap.a_swap_refund_value = ci_from.createSCLockRefundTx( xmr_swap.a_lock_refund_tx, xmr_swap.a_lock_refund_tx_script, xmr_swap.a_swap_refund_value = ci_from.createSCLockRefundTx(
xmr_swap.a_lock_tx, xmr_swap.a_lock_tx_script, xmr_swap.a_lock_tx, xmr_swap.a_lock_tx_script,
xmr_swap.pkal, xmr_swap.pkaf, xmr_swap.pkal, xmr_swap.pkaf,
xmr_offer.lock_time_1, xmr_offer.lock_time_2, xmr_offer.lock_time_1, xmr_offer.lock_time_2,
a_fee_rate, xmr_swap.vkbv, refundExtraArgs a_fee_rate, xmr_swap.vkbv, **refundExtraArgs
) )
xmr_swap.a_lock_refund_tx_id = ci_from.getTxid(xmr_swap.a_lock_refund_tx) xmr_swap.a_lock_refund_tx_id = ci_from.getTxid(xmr_swap.a_lock_refund_tx)
prevout_amount = ci_from.getLockTxSwapOutputValue(bid, xmr_swap) prevout_amount = ci_from.getLockTxSwapOutputValue(bid, xmr_swap)
xmr_swap.al_lock_refund_tx_sig = ci_from.signTx(kal, xmr_swap.a_lock_refund_tx, 0, xmr_swap.a_lock_tx_script, prevout_amount) xmr_swap.al_lock_refund_tx_sig = ci_from.signTx(kal, xmr_swap.a_lock_refund_tx, 0, xmr_swap.a_lock_tx_script, prevout_amount)
v = ci_from.verifyTxSig(xmr_swap.a_lock_refund_tx, xmr_swap.al_lock_refund_tx_sig, xmr_swap.pkal, 0, xmr_swap.a_lock_tx_script, prevout_amount) v = ci_from.verifyTxSig(xmr_swap.a_lock_refund_tx, xmr_swap.al_lock_refund_tx_sig, xmr_swap.pkal, 0, xmr_swap.a_lock_tx_script, prevout_amount)
ensure(v, 'Invalid coin A lock refund tx leader sig') ensure(v, 'Invalid coin A lock refund tx leader sig')
pkh_refund_to = ci_from.decodeAddress(self.getCachedAddressForCoin(coin_from, use_session))
pkh_refund_to = ci_from.decodeAddress(self.getCachedAddressForCoin(coin_from))
xmr_swap.a_lock_refund_spend_tx = ci_from.createSCLockRefundSpendTx( xmr_swap.a_lock_refund_spend_tx = ci_from.createSCLockRefundSpendTx(
xmr_swap.a_lock_refund_tx, xmr_swap.a_lock_refund_tx_script, xmr_swap.a_lock_refund_tx, xmr_swap.a_lock_refund_tx_script,
pkh_refund_to, pkh_refund_to,
a_fee_rate, xmr_swap.vkbv a_fee_rate, xmr_swap.vkbv,
**refundExtraArgs
) )
xmr_swap.a_lock_refund_spend_tx_id = ci_from.getTxid(xmr_swap.a_lock_refund_spend_tx) xmr_swap.a_lock_refund_spend_tx_id = ci_from.getTxid(xmr_swap.a_lock_refund_spend_tx)
@ -3049,7 +3051,8 @@ class BasicSwap(BaseApp):
xmr_swap.pkaf, xmr_swap.pkaf,
a_fee_rate, a_fee_rate,
check_lock_tx_inputs, check_lock_tx_inputs,
xmr_swap.vkbv) xmr_swap.vkbv,
**lockExtraArgs)
_, _, lock_refund_vout = ci_from.verifySCLockRefundTx( _, _, lock_refund_vout = ci_from.verifySCLockRefundTx(
xmr_swap.a_lock_refund_tx, xmr_swap.a_lock_refund_tx,
@ -3064,14 +3067,16 @@ class BasicSwap(BaseApp):
xmr_offer.lock_time_2, xmr_offer.lock_time_2,
bid.amount, bid.amount,
a_fee_rate, a_fee_rate,
xmr_swap.vkbv) xmr_swap.vkbv,
**refundExtraArgs)
ci_from.verifySCLockRefundSpendTx( ci_from.verifySCLockRefundSpendTx(
xmr_swap.a_lock_refund_spend_tx, xmr_swap.a_lock_refund_tx, xmr_swap.a_lock_refund_spend_tx, xmr_swap.a_lock_refund_tx,
xmr_swap.a_lock_refund_tx_id, xmr_swap.a_lock_refund_tx_script, xmr_swap.a_lock_refund_tx_id, xmr_swap.a_lock_refund_tx_script,
xmr_swap.pkal, xmr_swap.pkal,
lock_refund_vout, xmr_swap.a_swap_refund_value, a_fee_rate, lock_refund_vout, xmr_swap.a_swap_refund_value, a_fee_rate,
xmr_swap.vkbv) xmr_swap.vkbv,
**refundExtraArgs)
msg_buf = XmrBidAcceptMessage() msg_buf = XmrBidAcceptMessage()
msg_buf.bid_msg_id = bid_id msg_buf.bid_msg_id = bid_id
@ -3921,7 +3926,6 @@ class BasicSwap(BaseApp):
self.createActionInSession(delay, ActionTypes.RECOVER_XMR_SWAP_LOCK_TX_B, bid_id, session) self.createActionInSession(delay, ActionTypes.RECOVER_XMR_SWAP_LOCK_TX_B, bid_id, session)
session.commit() session.commit()
elif state == BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_SPEND_TX: elif state == BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_SPEND_TX:
print(3, bid.xmr_a_lock_tx)
if bid.xmr_a_lock_tx is None: if bid.xmr_a_lock_tx is None:
return rv return rv
@ -3933,6 +3937,29 @@ class BasicSwap(BaseApp):
if lock_tx_chain_info is None: if lock_tx_chain_info is None:
return rv return rv
if lock_tx_chain_info['txid'] != b2h(xmr_swap.a_lock_tx_id):
# if we find that txid was changed (by funding or otherwise), we need to update it to track correctly
xmr_swap.a_lock_tx_id = h2b(lock_tx_chain_info['txid'])
xmr_swap.a_lock_tx = h2b(lock_tx_chain_info['txhex'])
tx = ci_from.loadTx(xmr_swap.a_lock_refund_tx)
tx.vin[0].prevout.hash = b2i(xmr_swap.a_lock_tx_id)
xmr_swap.a_lock_refund_tx = tx.serialize_without_witness()
xmr_swap.a_lock_refund_tx_id = ci_from.getTxid(xmr_swap.a_lock_refund_tx)
tx = ci_from.loadTx(xmr_swap.a_lock_spend_tx)
tx.vin[0].prevout.hash = b2i(xmr_swap.a_lock_tx_id)
xmr_swap.a_lock_spend_tx = tx.serialize_without_witness()
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.txid = xmr_swap.a_lock_tx_id
bid.xmr_a_lock_tx.tx_data = xmr_swap.a_lock_tx
bid.xmr_a_lock_tx.spend_txid = xmr_swap.a_lock_spend_tx_id
self.addWatchedOutput(offer.coin_from, bid.bid_id, bid.xmr_a_lock_tx.txid.hex(), bid.xmr_a_lock_tx.vout, TxTypes.XMR_SWAP_A_LOCK, SwapTypes.XMR_SWAP)
bid_changed = True
if bid.xmr_a_lock_tx.state == TxStates.TX_NONE and lock_tx_chain_info['height'] == 0: if bid.xmr_a_lock_tx.state == TxStates.TX_NONE and lock_tx_chain_info['height'] == 0:
bid.xmr_a_lock_tx.setState(TxStates.TX_IN_MEMPOOL) bid.xmr_a_lock_tx.setState(TxStates.TX_IN_MEMPOOL)
@ -4374,8 +4401,15 @@ class BasicSwap(BaseApp):
state = BidStates(bid.state) state = BidStates(bid.state)
spending_txid = bytes.fromhex(spend_txid_hex) spending_txid = bytes.fromhex(spend_txid_hex)
spend_tx = self.ci(coin_from).loadTx(h2b(spend_txn_hex))
bid.xmr_a_lock_tx.spend_txid = spending_txid bid.xmr_a_lock_tx.spend_txid = spending_txid
if spending_txid == xmr_swap.a_lock_spend_tx_id: if spending_txid == xmr_swap.a_lock_spend_tx_id or i2b(spend_tx.vin[0].prevout.hash) == xmr_swap.a_lock_tx_id:
# bch txids change
if xmr_swap.a_lock_spend_tx_id != spending_txid:
xmr_swap.a_lock_spend_tx_id = spending_txid
xmr_swap.a_lock_spend_tx = bytes.fromhex(spend_txn_hex)
if state == BidStates.XMR_SWAP_LOCK_RELEASED: if state == BidStates.XMR_SWAP_LOCK_RELEASED:
xmr_swap.a_lock_spend_tx = bytes.fromhex(spend_txn_hex) xmr_swap.a_lock_spend_tx = bytes.fromhex(spend_txn_hex)
bid.setState(BidStates.XMR_SWAP_SCRIPT_TX_REDEEMED) # TODO: Wait for confirmation? bid.setState(BidStates.XMR_SWAP_SCRIPT_TX_REDEEMED) # TODO: Wait for confirmation?
@ -4389,7 +4423,12 @@ class BasicSwap(BaseApp):
# Could already be processed if spend was detected in the mempool # Could already be processed if spend was detected in the mempool
self.log.warning('Coin a lock tx spend ignored due to bid state for bid {}'.format(bid_id.hex())) self.log.warning('Coin a lock tx spend ignored due to bid state for bid {}'.format(bid_id.hex()))
elif spending_txid == xmr_swap.a_lock_refund_tx_id: elif spending_txid == xmr_swap.a_lock_refund_tx_id or i2b(spend_tx.vin[0].prevout.hash) == xmr_swap.a_lock_refund_tx_id:
# bch txids change
if xmr_swap.a_lock_refund_tx_id != spending_txid:
xmr_swap.a_lock_refund_tx_id = spending_txid
xmr_swap.a_lock_refund_tx = bytes.fromhex(spend_txn_hex)
self.log.debug('Coin a lock tx spent by lock refund tx.') self.log.debug('Coin a lock tx spent by lock refund tx.')
bid.setState(BidStates.XMR_SWAP_SCRIPT_TX_PREREFUND) bid.setState(BidStates.XMR_SWAP_SCRIPT_TX_PREREFUND)
self.logBidEvent(bid.bid_id, EventLogTypes.LOCK_TX_A_REFUND_TX_SEEN, '', use_session) self.logBidEvent(bid.bid_id, EventLogTypes.LOCK_TX_A_REFUND_TX_SEEN, '', use_session)
@ -4424,14 +4463,22 @@ class BasicSwap(BaseApp):
state = BidStates(bid.state) state = BidStates(bid.state)
spending_txid = bytes.fromhex(spend_txid_hex) spending_txid = bytes.fromhex(spend_txid_hex)
if spending_txid == xmr_swap.a_lock_refund_spend_tx_id: spend_txn_hex = spend_txn['hex']
spend_tx = self.ci(coin_from).loadTx(h2b(spend_txn_hex))
if spending_txid == xmr_swap.a_lock_refund_spend_tx_id or i2b(spend_tx.vin[0].prevout.hash) == xmr_swap.a_lock_refund_spend_tx_id:
# bch txids change
if xmr_swap.a_lock_refund_spend_tx_id != spending_txid:
xmr_swap.a_lock_refund_spend_tx_id = spending_txid
xmr_swap.a_lock_refund_spend_tx = bytes.fromhex(spend_txn_hex)
self.log.info('Found coin a lock refund spend tx, bid {}'.format(bid_id.hex())) self.log.info('Found coin a lock refund spend tx, bid {}'.format(bid_id.hex()))
self.logBidEvent(bid.bid_id, EventLogTypes.LOCK_TX_A_REFUND_SPEND_TX_SEEN, '', session) self.logBidEvent(bid.bid_id, EventLogTypes.LOCK_TX_A_REFUND_SPEND_TX_SEEN, '', session)
if bid.xmr_a_lock_tx: if bid.xmr_a_lock_tx:
bid.xmr_a_lock_tx.setState(TxStates.TX_REFUNDED) bid.xmr_a_lock_tx.setState(TxStates.TX_REFUNDED)
if was_sent: if was_sent:
xmr_swap.a_lock_refund_spend_tx = bytes.fromhex(spend_txn['hex']) # Replace with fully signed tx xmr_swap.a_lock_refund_spend_tx = bytes.fromhex(spend_txn_hex) # Replace with fully signed tx
if TxTypes.XMR_SWAP_A_LOCK_REFUND_SPEND not in bid.txns: if TxTypes.XMR_SWAP_A_LOCK_REFUND_SPEND not in bid.txns:
bid.txns[TxTypes.XMR_SWAP_A_LOCK_REFUND_SPEND] = SwapTx( bid.txns[TxTypes.XMR_SWAP_A_LOCK_REFUND_SPEND] = SwapTx(
bid_id=bid_id, bid_id=bid_id,
@ -5548,6 +5595,33 @@ class BasicSwap(BaseApp):
xmr_swap.a_lock_refund_spend_tx_id = ci_from.getTxid(xmr_swap.a_lock_refund_spend_tx) xmr_swap.a_lock_refund_spend_tx_id = ci_from.getTxid(xmr_swap.a_lock_refund_spend_tx)
xmr_swap.al_lock_refund_tx_sig = msg_data.al_lock_refund_tx_sig xmr_swap.al_lock_refund_tx_sig = msg_data.al_lock_refund_tx_sig
refundExtraArgs = dict()
lockExtraArgs = dict()
if self.isBchXmrSwap(offer):
# perform check that both lock and refund transactions have their outs pointing to correct follower address
# and prepare extra args for validation
bch_ci = self.ci(Coins.BCH)
mining_fee, out_1, out_2, public_key, timelock = bch_ci.extractScriptLockScriptValues(xmr_swap.a_lock_tx_script)
ensure(out_1 == bch_ci.getScriptForPubkeyHash(xmr_swap.dest_af), 'Invalid BCH lock tx script out_1')
ensure(out_2 == bch_ci.scriptToP2SH32LockingBytecode(xmr_swap.a_lock_refund_tx_script), 'Invalid BCH lock tx script out_2')
lockExtraArgs['mining_fee'] = mining_fee
lockExtraArgs['out_1'] = out_1
lockExtraArgs['out_2'] = out_2
lockExtraArgs['public_key'] = public_key
lockExtraArgs['timelock'] = timelock
mining_fee, out_1, out_2, public_key, timelock = bch_ci.extractScriptLockScriptValues(xmr_swap.a_lock_refund_tx_script)
ensure(out_2 == bch_ci.getScriptForPubkeyHash(xmr_swap.dest_af), 'Invalid BCH refund tx script out_2')
refundExtraArgs['mining_fee'] = mining_fee
refundExtraArgs['out_1'] = out_1
refundExtraArgs['out_2'] = out_2
refundExtraArgs['public_key'] = public_key
refundExtraArgs['timelock'] = timelock
# TODO: check_lock_tx_inputs without txindex # TODO: check_lock_tx_inputs without txindex
check_a_lock_tx_inputs = False check_a_lock_tx_inputs = False
xmr_swap.a_lock_tx_id, xmr_swap.a_lock_tx_vout = ci_from.verifySCLockTx( xmr_swap.a_lock_tx_id, xmr_swap.a_lock_tx_vout = ci_from.verifySCLockTx(
@ -5555,7 +5629,8 @@ class BasicSwap(BaseApp):
bid.amount, bid.amount,
xmr_swap.pkal, xmr_swap.pkaf, xmr_swap.pkal, xmr_swap.pkaf,
a_fee_rate, a_fee_rate,
check_a_lock_tx_inputs, xmr_swap.vkbv) check_a_lock_tx_inputs, xmr_swap.vkbv,
**lockExtraArgs)
a_lock_tx_dest = ci_from.getScriptDest(xmr_swap.a_lock_tx_script) a_lock_tx_dest = ci_from.getScriptDest(xmr_swap.a_lock_tx_script)
xmr_swap.a_lock_refund_tx_id, xmr_swap.a_swap_refund_value, lock_refund_vout = ci_from.verifySCLockRefundTx( xmr_swap.a_lock_refund_tx_id, xmr_swap.a_swap_refund_value, lock_refund_vout = ci_from.verifySCLockRefundTx(
@ -5563,13 +5638,15 @@ class BasicSwap(BaseApp):
xmr_swap.a_lock_tx_id, xmr_swap.a_lock_tx_vout, xmr_offer.lock_time_1, xmr_swap.a_lock_tx_script, xmr_swap.a_lock_tx_id, xmr_swap.a_lock_tx_vout, xmr_offer.lock_time_1, xmr_swap.a_lock_tx_script,
xmr_swap.pkal, xmr_swap.pkaf, xmr_swap.pkal, xmr_swap.pkaf,
xmr_offer.lock_time_2, xmr_offer.lock_time_2,
bid.amount, a_fee_rate, xmr_swap.vkbv) bid.amount, a_fee_rate, xmr_swap.vkbv,
**refundExtraArgs)
ci_from.verifySCLockRefundSpendTx( ci_from.verifySCLockRefundSpendTx(
xmr_swap.a_lock_refund_spend_tx, xmr_swap.a_lock_refund_tx, xmr_swap.a_lock_refund_spend_tx, xmr_swap.a_lock_refund_tx,
xmr_swap.a_lock_refund_tx_id, xmr_swap.a_lock_refund_tx_script, xmr_swap.a_lock_refund_tx_id, xmr_swap.a_lock_refund_tx_script,
xmr_swap.pkal, xmr_swap.pkal,
lock_refund_vout, xmr_swap.a_swap_refund_value, a_fee_rate, xmr_swap.vkbv) lock_refund_vout, xmr_swap.a_swap_refund_value, a_fee_rate, xmr_swap.vkbv,
**refundExtraArgs)
self.log.info('Checking leader\'s lock refund tx signature') self.log.info('Checking leader\'s lock refund tx signature')
prevout_amount = ci_from.getLockTxSwapOutputValue(bid, xmr_swap) prevout_amount = ci_from.getLockTxSwapOutputValue(bid, xmr_swap)
@ -5724,8 +5801,25 @@ class BasicSwap(BaseApp):
if lock_tx_sent is False: if lock_tx_sent is False:
lock_tx_signed = ci_from.signTxWithWallet(xmr_swap.a_lock_tx) lock_tx_signed = ci_from.signTxWithWallet(xmr_swap.a_lock_tx)
print(1, lock_tx_signed)
txid_hex = ci_from.publishTx(lock_tx_signed) txid_hex = ci_from.publishTx(lock_tx_signed)
if txid_hex != b2h(xmr_swap.a_lock_tx_id):
xmr_swap.a_lock_tx = lock_tx_signed
xmr_swap.a_lock_tx_id = bytes.fromhex(txid_hex)
tx = ci_from.loadTx(xmr_swap.a_lock_refund_tx)
tx.vin[0].prevout.hash = b2i(xmr_swap.a_lock_tx_id)
xmr_swap.a_lock_refund_tx = tx.serialize_without_witness()
xmr_swap.a_lock_refund_tx_id = ci_from.getTxid(xmr_swap.a_lock_refund_tx)
tx = ci_from.loadTx(xmr_swap.a_lock_spend_tx)
tx.vin[0].prevout.hash = b2i(xmr_swap.a_lock_tx_id)
xmr_swap.a_lock_spend_tx = tx.serialize_without_witness()
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.txid = xmr_swap.a_lock_tx_id
bid.xmr_a_lock_tx.tx_data = lock_tx_signed
bid.xmr_a_lock_tx.spend_txid = xmr_swap.a_lock_spend_tx_id
vout_pos = ci_from.getTxOutputPos(xmr_swap.a_lock_tx, xmr_swap.a_lock_tx_script) 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()) self.log.debug('Submitted lock txn %s to %s chain for bid %s', txid_hex, ci_from.coin_name(), bid_id.hex())
@ -5794,7 +5888,6 @@ class BasicSwap(BaseApp):
try: try:
b_lock_tx_id = ci_to.publishBLockTx(xmr_swap.vkbv, xmr_swap.pkbs, bid.amount_to, b_fee_rate, unlock_time=unlock_time) b_lock_tx_id = ci_to.publishBLockTx(xmr_swap.vkbv, xmr_swap.pkbs, bid.amount_to, b_fee_rate, unlock_time=unlock_time)
print(2, b_lock_tx_id)
if bid.debug_ind == DebugTypes.B_LOCK_TX_MISSED_SEND: if bid.debug_ind == DebugTypes.B_LOCK_TX_MISSED_SEND:
self.log.debug('Adaptor-sig bid %s: Debug %d - Losing xmr lock tx %s.', bid_id.hex(), bid.debug_ind, b_lock_tx_id.hex()) self.log.debug('Adaptor-sig bid %s: Debug %d - Losing xmr lock tx %s.', bid_id.hex(), bid.debug_ind, b_lock_tx_id.hex())
self.logBidEvent(bid.bid_id, EventLogTypes.DEBUG_TWEAK_APPLIED, 'ind {}'.format(bid.debug_ind), session) self.logBidEvent(bid.bid_id, EventLogTypes.DEBUG_TWEAK_APPLIED, 'ind {}'.format(bid.debug_ind), session)
@ -5888,6 +5981,8 @@ class BasicSwap(BaseApp):
kbsf = self.getPathKey(coin_from, coin_to, bid.created_at, xmr_swap.contract_count, KeyTypes.KBSF, for_ed25519) kbsf = self.getPathKey(coin_from, coin_to, bid.created_at, xmr_swap.contract_count, KeyTypes.KBSF, for_ed25519)
kaf = self.getPathKey(coin_from, coin_to, bid.created_at, xmr_swap.contract_count, KeyTypes.KAF) kaf = self.getPathKey(coin_from, coin_to, bid.created_at, xmr_swap.contract_count, KeyTypes.KAF)
if not self.isBchXmrSwap(offer):
# segwit coins sign the transaction
al_lock_spend_sig = ci_from.decryptOtVES(kbsf, xmr_swap.al_lock_spend_tx_esig) al_lock_spend_sig = ci_from.decryptOtVES(kbsf, xmr_swap.al_lock_spend_tx_esig)
prevout_amount = ci_from.getLockTxSwapOutputValue(bid, xmr_swap) prevout_amount = ci_from.getLockTxSwapOutputValue(bid, xmr_swap)
v = ci_from.verifyTxSig(xmr_swap.a_lock_spend_tx, al_lock_spend_sig, xmr_swap.pkal, 0, xmr_swap.a_lock_tx_script, prevout_amount) v = ci_from.verifyTxSig(xmr_swap.a_lock_spend_tx, al_lock_spend_sig, xmr_swap.pkal, 0, xmr_swap.a_lock_tx_script, prevout_amount)
@ -5907,6 +6002,19 @@ class BasicSwap(BaseApp):
] ]
xmr_swap.a_lock_spend_tx = ci_from.setTxSignature(xmr_swap.a_lock_spend_tx, witness_stack) xmr_swap.a_lock_spend_tx = ci_from.setTxSignature(xmr_swap.a_lock_spend_tx, witness_stack)
else:
# bch signs the output pkh
tx = ci_from.loadTx(xmr_swap.a_lock_spend_tx)
out1 = tx.vout[0].scriptPubKey
out1_sig = ci_from.decryptOtVES(kbsf, xmr_swap.al_lock_spend_tx_esig)
v = ci_from.verifyDataSig(out1, out1_sig, xmr_swap.pkal)
ensure(v, 'Invalid signature for lock spend txn')
# update prevout after tx was signed
tx.vin[0].prevout.hash = b2i(xmr_swap.a_lock_tx_id)
tx.vin[0].scriptSig = ci_from.getScriptScriptSig(xmr_swap.a_lock_tx_script, out1_sig)
xmr_swap.a_lock_spend_tx = tx.serialize_without_witness()
xmr_swap.a_lock_spend_tx_id = ci_from.getTxid(xmr_swap.a_lock_spend_tx)
txid = bytes.fromhex(ci_from.publishTx(xmr_swap.a_lock_spend_tx)) txid = bytes.fromhex(ci_from.publishTx(xmr_swap.a_lock_spend_tx))
self.log.debug('Submitted lock spend txn %s to %s chain for bid %s', txid.hex(), ci_from.coin_name(), bid_id.hex()) self.log.debug('Submitted lock spend txn %s to %s chain for bid %s', txid.hex(), ci_from.coin_name(), bid_id.hex())
@ -6136,6 +6244,8 @@ class BasicSwap(BaseApp):
kbsl = self.getPathKey(coin_from, coin_to, bid.created_at, xmr_swap.contract_count, KeyTypes.KBSL, for_ed25519) kbsl = self.getPathKey(coin_from, coin_to, bid.created_at, xmr_swap.contract_count, KeyTypes.KBSL, for_ed25519)
kal = self.getPathKey(coin_from, coin_to, bid.created_at, xmr_swap.contract_count, KeyTypes.KAL) kal = self.getPathKey(coin_from, coin_to, bid.created_at, xmr_swap.contract_count, KeyTypes.KAL)
if not self.isBchXmrSwap(offer):
# segwit coins sign the transaction
xmr_swap.af_lock_refund_spend_tx_sig = ci_from.decryptOtVES(kbsl, xmr_swap.af_lock_refund_spend_tx_esig) xmr_swap.af_lock_refund_spend_tx_sig = ci_from.decryptOtVES(kbsl, xmr_swap.af_lock_refund_spend_tx_esig)
prevout_amount = ci_from.getLockRefundTxSwapOutputValue(bid, xmr_swap) prevout_amount = ci_from.getLockRefundTxSwapOutputValue(bid, xmr_swap)
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) 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)
@ -6157,6 +6267,17 @@ class BasicSwap(BaseApp):
v = ci_from.verifyTxSig(xmr_swap.a_lock_refund_spend_tx, xmr_swap.af_lock_refund_spend_tx_sig, xmr_swap.pkaf, 0, xmr_swap.a_lock_refund_tx_script, prevout_amount) v = ci_from.verifyTxSig(xmr_swap.a_lock_refund_spend_tx, xmr_swap.af_lock_refund_spend_tx_sig, xmr_swap.pkaf, 0, xmr_swap.a_lock_refund_tx_script, prevout_amount)
ensure(v, 'Invalid signature for lock refund spend txn') ensure(v, 'Invalid signature for lock refund spend txn')
xmr_swap_1.addLockRefundSigs(self, xmr_swap, ci_from) xmr_swap_1.addLockRefundSigs(self, xmr_swap, ci_from)
else:
# bch signs the output pkh
tx = ci_from.loadTx(xmr_swap.a_lock_refund_spend_tx)
out1 = tx.vout[0].scriptPubKey
out1_sig = ci_from.decryptOtVES(kbsl, xmr_swap.af_lock_refund_spend_tx_esig)
v = ci_from.verifyDataSig(out1, out1_sig, xmr_swap.pkaf)
ensure(v, 'Invalid signature for lock refund spend txn')
tx.vin[0].scriptSig = ci_from.getScriptScriptSig(xmr_swap.a_lock_refund_tx_script, out1_sig)
xmr_swap.a_lock_refund_spend_tx = tx.serialize_without_witness()
xmr_swap.a_lock_refund_spend_tx_id = ci_from.getTxid(xmr_swap.a_lock_refund_spend_tx)
delay = self.get_delay_event_seconds() delay = self.get_delay_event_seconds()
self.log.info('Sending coin A lock tx for adaptor-sig bid %s in %d seconds', bid_id.hex(), delay) self.log.info('Sending coin A lock tx for adaptor-sig bid %s in %d seconds', bid_id.hex(), delay)

View file

@ -7,8 +7,9 @@
from typing import Union from typing import Union
from basicswap.contrib.test_framework.messages import COutPoint, CTransaction, CTxIn, CTxOut from basicswap.contrib.test_framework.messages import COutPoint, CTransaction, CTxIn, CTxOut
from basicswap.util import b2h, ensure, i2h from basicswap.util import b2h, b2i, ensure, i2h
from .btc import BTCInterface, findOutput from basicswap.util.script import decodePushData, decodeScriptNum
from .btc import BTCInterface, ensure_op, find_vout_for_address_from_txobj, findOutput
from basicswap.rpc import make_rpc_func from basicswap.rpc import make_rpc_func
from basicswap.chainparams import Coins from basicswap.chainparams import Coins
from basicswap.interface.contrib.bch_test_framework.cashaddress import Address from basicswap.interface.contrib.bch_test_framework.cashaddress import Address
@ -51,6 +52,12 @@ from coincurve.keys import (
PrivateKey, PrivateKey,
PublicKey, PublicKey,
) )
from coincurve.ecdsaotves import (
ecdsaotves_enc_sign,
ecdsaotves_enc_verify,
ecdsaotves_dec_sig,
ecdsaotves_rec_enc_key,
)
def findOutput(tx, script_pk: bytes): def findOutput(tx, script_pk: bytes):
for i in range(len(tx.vout)): for i in range(len(tx.vout)):
@ -80,10 +87,10 @@ class BCHInterface(BTCInterface):
return bytes(Address.from_string(address).payload) return bytes(Address.from_string(address).payload)
def encodeSegwitAddress(self, script): def encodeSegwitAddress(self, script):
raise ValueError('TODO') raise ValueError('Segwit not supported')
def decodeSegwitAddress(self, addr): def decodeSegwitAddress(self, addr):
raise ValueError('TODO') raise ValueError('Segwit not supported')
def getSCLockScriptAddress(self, lock_script: bytes) -> str: def getSCLockScriptAddress(self, lock_script: bytes) -> str:
lock_tx_dest = self.getScriptDest(lock_script) lock_tx_dest = self.getScriptDest(lock_script)
@ -125,14 +132,22 @@ class BCHInterface(BTCInterface):
return self.sh_to_address(script_hash) return self.sh_to_address(script_hash)
def sh_to_address(self, sh: bytes) -> str: def sh_to_address(self, sh: bytes) -> str:
assert (len(sh) == 20) assert (len(sh) == 20 or len(sh) == 32)
network = self._network.upper() network = self._network.upper()
address = None
if len(sh) == 20:
address = Address("P2SH20" if network == "MAINNET" else "P2SH20-"+network, sh) address = Address("P2SH20" if network == "MAINNET" else "P2SH20-"+network, sh)
else:
address = Address("P2SH32" if network == "MAINNET" else "P2SH32-"+network, sh)
return address.cash_address() return address.cash_address()
def getDestForScriptHash(self, script_hash): def getDestForScriptHash(self, script_hash):
assert len(script_hash) == 20 assert (len(script_hash) == 20 or len(script_hash) == 32)
if len(script_hash) == 20:
return CScript([OP_HASH160, script_hash, OP_EQUAL]) return CScript([OP_HASH160, script_hash, OP_EQUAL])
else:
return CScript([OP_HASH256, script_hash, OP_EQUAL])
def withdrawCoin(self, value: float, addr_to: str, subfee: bool): def withdrawCoin(self, value: float, addr_to: str, subfee: bool):
params = [addr_to, value, '', '', subfee, True, True] params = [addr_to, value, '', '', subfee, True, True]
@ -160,9 +175,63 @@ class BCHInterface(BTCInterface):
return {'txid': txid_hex, 'amount': 0, 'height': block_height} return {'txid': txid_hex, 'amount': 0, 'height': block_height}
return None return None
def getLockTxHeight(self, txid, dest_address, bid_amount, rescan_from, find_index: bool = False, vout: int = -1):
# Add watchonly address and rescan if required
txid = None
# first lookup by dest_address
if not self.isAddressMine(dest_address, or_watch_only=False):
self.importWatchOnlyAddress(dest_address, 'bid')
self._log.info('Imported watch-only addr: {}'.format(dest_address))
self._log.info('Rescanning {} chain from height: {}'.format(self.coin_name(), rescan_from))
self.rpc_wallet('rescanblockchain', [rescan_from])
return_txid = True
txns = self.rpc_wallet('listunspent', [0, 9999999, [dest_address, ]])
for tx in txns:
if self.make_int(tx['amount']) == bid_amount:
txid = bytes.fromhex(tx['txid'])
break
# try to look up in past transactions
if not txid:
txns = self.rpc_wallet('listtransactions', ["*", 100000, 0, True])
for tx in txns:
if self.make_int(tx['amount']) == bid_amount and tx['category'] == 'send' and tx['address'] == dest_address:
txid = bytes.fromhex(tx['txid'])
break
try:
# set `include_watchonly` explicitly to `True` to get transactions for watchonly addresses also in BCH
tx = self.rpc_wallet('gettransaction', [txid.hex(), True])
block_height = 0
if 'blockhash' in tx:
block_header = self.rpc('getblockheader', [tx['blockhash']])
block_height = block_header['height']
rv = {
'depth': 0 if 'confirmations' not in tx else tx['confirmations'],
'height': block_height}
except Exception as e:
# self._log.debug('getLockTxHeight gettransaction failed: %s, %s', txid.hex(), str(e))
return None
if find_index:
tx_obj = self.rpc('decoderawtransaction', [tx['hex']])
rv['index'] = find_vout_for_address_from_txobj(tx_obj, dest_address)
if return_txid:
rv['txid'] = txid.hex()
rv['txhex'] = tx['hex']
return rv
def genScriptLockTxScript(self, ci, Kal: bytes, Kaf: bytes, **kwargs) -> CScript: def genScriptLockTxScript(self, ci, Kal: bytes, Kaf: bytes, **kwargs) -> CScript:
print("bch genScriptLockTxScript") mining_fee: int = kwargs['mining_fee'] if 'mining_fee' in kwargs else 1000
mining_fee: int = kwargs['mining_fee']
out_1: bytes = kwargs['out_1'] out_1: bytes = kwargs['out_1']
out_2: bytes = kwargs['out_2'] out_2: bytes = kwargs['out_2']
public_key: bytes = kwargs['public_key'] if 'public_key' in kwargs else Kal public_key: bytes = kwargs['public_key'] if 'public_key' in kwargs else Kal
@ -288,7 +357,7 @@ class BCHInterface(BTCInterface):
def getTxSize(self, tx: CTransaction) -> int: def getTxSize(self, tx: CTransaction) -> int:
return len(tx.serialize_without_witness()) return len(tx.serialize_without_witness())
def getScriptScriptSig(self, script: bytes, ves: bytes) -> bytes: def getScriptScriptSig(self, script: bytes, ves: bytes = None) -> bytes:
if ves is not None: if ves is not None:
return CScript([ves, script]) return CScript([ves, script])
else: else:
@ -363,54 +432,24 @@ class BCHInterface(BTCInterface):
return tx.serialize_without_witness(), refund_script, tx.vout[0].nValue return tx.serialize_without_witness(), refund_script, tx.vout[0].nValue
def createSCLockRefundSpendTx(self, tx_lock_refund_bytes, script_lock_refund, pkh_refund_to, tx_fee_rate, vkbv=None, **kwargs): def createSCLockRefundSpendTx(self, tx_lock_refund_bytes, script_lock_refund, pkh_refund_to, tx_fee_rate, vkbv=None, **kwargs):
# Returns the coinA locked coin to the leader # it is not possible to create the refund spend tx without the prior knowledge of the VES which is part of transaction preimage
# The follower will sign the multisig path with a signature encumbered by the leader's coinB spend pubkey # but it is better and more secure to create a lock spend transaction committing to zero VES than returning static data
# If the leader publishes the decrypted signature the leader's coinB spend privatekey will be revealed to the follower kwargs['ves'] = bytes(73)
return self.createSCLockSpendTx(tx_lock_refund_bytes, script_lock_refund, pkh_refund_to, tx_fee_rate, vkbv, **kwargs)
# spending the refund contract back to leader requires their adaptor signature to be published, but at the moment of this function call it is too early to share it
# TODO: bettter handling of this case
# allow for template ves for transaction to be signed and verified between parties
ves = kwargs['ves'] if 'ves' in kwargs else bytes(70)
tx_lock_refund = self.loadTx(tx_lock_refund_bytes)
output_script = self.getScriptDest(script_lock_refund)
locked_n = findOutput(tx_lock_refund, output_script)
ensure(locked_n is not None, 'Output not found in tx')
locked_coin = tx_lock_refund.vout[locked_n].nValue
tx_lock_refund.rehash()
tx_lock_refund_hash_int = tx_lock_refund.sha256
tx = CTransaction()
tx.nVersion = self.txVersion()
tx.vin.append(CTxIn(COutPoint(tx_lock_refund_hash_int, locked_n),
nSequence=0,
scriptSig=self.getScriptScriptSig(script_lock_refund, ves)))
tx.vout.append(self.txoType()(locked_coin, self.getScriptForPubkeyHash(pkh_refund_to)))
pay_fee = tx_fee_rate
tx.vout[0].nValue = locked_coin - pay_fee
size = self.getTxSize(tx)
vsize = size
tx.rehash()
self._log.info('createSCLockRefundSpendTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.',
i2h(tx.sha256), tx_fee_rate, vsize, pay_fee)
return tx.serialize_without_witness()
def signTx(self, key_bytes: bytes, tx_bytes: bytes, input_n: int, prevout_script: bytes, prevout_value: int) -> bytes: def signTx(self, key_bytes: bytes, tx_bytes: bytes, input_n: int, prevout_script: bytes, prevout_value: int) -> bytes:
# simply sign the entire tx data, as this is not a preimage signature # simply sign the entire tx data, as this is not a preimage signature
eck = PrivateKey(key_bytes) eck = PrivateKey(key_bytes)
return eck.sign(tx_bytes, hasher=None) return eck.sign(sha256(tx_bytes), hasher=None)
def verifyTxSig(self, tx_bytes: bytes, sig: bytes, K: bytes, input_n: int, prevout_script: bytes, prevout_value: int) -> bool: def verifyTxSig(self, tx_bytes: bytes, sig: bytes, K: bytes, input_n: int, prevout_script: bytes, prevout_value: int) -> bool:
# simple ecdsa signature verification
return self.verifyDataSig(tx_bytes, sig, K)
def verifyDataSig(self, data: bytes, sig: bytes, K: bytes) -> bool:
# simple ecdsa signature verification # simple ecdsa signature verification
pubkey = PublicKey(K) pubkey = PublicKey(K)
return pubkey.verify(sig, tx_bytes, hasher=None) return pubkey.verify(sig, sha256(data), hasher=None)
def setTxSignature(self, tx_bytes: bytes, stack) -> bytes: def setTxSignature(self, tx_bytes: bytes, stack) -> bytes:
return tx_bytes return tx_bytes
@ -445,3 +484,295 @@ class BCHInterface(BTCInterface):
# TODO: better script matching, see interfaces/btc.py # TODO: better script matching, see interfaces/btc.py
return txid, locked_n return txid, locked_n
def extractScriptLockScriptValuesFromScriptSig(self, script_bytes):
signature, nb = decodePushData(script_bytes, 0)
unlock_script, _ = decodePushData(script_bytes, nb)
mining_fee, out_1, out_2, public_key, timelock = self.extractScriptLockScriptValues(unlock_script)
return signature, mining_fee, out_1, out_2, public_key, timelock
def extractScriptLockScriptValues(self, script_bytes):
# see BCHInterface.genScriptLockTxScript for reference
o = 0
script_len = len(script_bytes)
# TODO: stricter script_len checks
ensure_op(script_bytes[o] == OP_TXINPUTCOUNT); o += 1
ensure_op(script_bytes[o] == OP_1); o += 1
ensure_op(script_bytes[o] == OP_NUMEQUALVERIFY); o += 1
ensure_op(script_bytes[o] == OP_TXOUTPUTCOUNT); o += 1
ensure_op(script_bytes[o] == OP_1); o += 1
ensure_op(script_bytes[o] == OP_NUMEQUALVERIFY); o += 1
mining_fee, nb = decodeScriptNum(script_bytes, o); o += nb
ensure_op(script_bytes[o] == OP_0); o += 1
ensure_op(script_bytes[o] == OP_UTXOVALUE); o += 1
ensure_op(script_bytes[o] == OP_0); o += 1
ensure_op(script_bytes[o] == OP_OUTPUTVALUE); o += 1
ensure_op(script_bytes[o] == OP_SUB); o += 1
ensure_op(script_bytes[o] == OP_NUMEQUALVERIFY); o += 1
ensure_op(script_bytes[o] == OP_0); o += 1
ensure_op(script_bytes[o] == OP_UTXOTOKENCATEGORY); o += 1
ensure_op(script_bytes[o] == OP_0); o += 1
ensure_op(script_bytes[o] == OP_OUTPUTTOKENCATEGORY); o += 1
ensure_op(script_bytes[o] == OP_EQUALVERIFY); o += 1
ensure_op(script_bytes[o] == OP_0); o += 1
ensure_op(script_bytes[o] == OP_UTXOTOKENCOMMITMENT); o += 1
ensure_op(script_bytes[o] == OP_0); o += 1
ensure_op(script_bytes[o] == OP_OUTPUTTOKENCOMMITMENT); o += 1
ensure_op(script_bytes[o] == OP_EQUALVERIFY); o += 1
ensure_op(script_bytes[o] == OP_0); o += 1
ensure_op(script_bytes[o] == OP_UTXOTOKENAMOUNT); o += 1
ensure_op(script_bytes[o] == OP_0); o += 1
ensure_op(script_bytes[o] == OP_OUTPUTTOKENAMOUNT); o += 1
ensure_op(script_bytes[o] == OP_NUMEQUALVERIFY); o += 1
ensure_op(script_bytes[o] == OP_0); o += 1
ensure_op(script_bytes[o] == OP_INPUTSEQUENCENUMBER); o += 1
ensure_op(script_bytes[o] == OP_NOTIF); o += 1
out_1, nb = decodePushData(script_bytes, o); o += nb
ensure_op(script_bytes[o] == OP_0); o += 1
ensure_op(script_bytes[o] == OP_OUTPUTBYTECODE); o += 1
ensure_op(script_bytes[o] == OP_OVER); o += 1
ensure_op(script_bytes[o] == OP_EQUALVERIFY); o += 1
public_key, nb = decodePushData(script_bytes, o); o += nb
ensure_op(script_bytes[o] == OP_CHECKDATASIG); o += 1
ensure_op(script_bytes[o] == OP_ELSE); o += 1
timelock, nb = decodeScriptNum(script_bytes, o); o += nb
ensure_op(script_bytes[o] == OP_CHECKSEQUENCEVERIFY); o += 1
ensure_op(script_bytes[o] == OP_DROP); o += 1
out_2, nb = decodePushData(script_bytes, o); o += nb
ensure_op(script_bytes[o] == OP_0); o += 1
ensure_op(script_bytes[o] == OP_OUTPUTBYTECODE); o += 1
ensure_op(script_bytes[o] == OP_EQUAL); o += 1
ensure_op(script_bytes[o] == OP_ENDIF); o += 1
ensure(o == script_len, 'Unexpected script length')
ensure(mining_fee >= 700 and mining_fee <= 10000, 'Bad mining_fee')
ensure(len(out_1) == 25, 'Bad out_1')
ensure(len(out_2) == 25 or len(out_2) == 35, 'Bad out_2')
ensure(len(public_key) == 33, 'Bad public_key')
ensure(timelock >= 0, 'Bad timelock')
return mining_fee, out_1, out_2, public_key, timelock
def verifySCLockTx(self, tx_bytes, script_out,
swap_value,
Kal, Kaf,
feerate,
check_lock_tx_inputs, vkbv=None,
**kwargs):
# 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.nVersion == self.txVersion(), 'Bad version')
ensure(tx.nLockTime == 0, 'Bad nLockTime') # TODO match txns created by cores
script_pk = self.getScriptDest(script_out)
locked_n = findOutput(tx, script_pk)
ensure(locked_n is not None, 'Output not found in tx')
locked_coin = tx.vout[locked_n].nValue
# Check value
ensure(locked_coin == swap_value, 'Bad locked value')
# Check script
mining_fee: int = kwargs['mining_fee'] if 'mining_fee' in kwargs else 1000
out_1: bytes = kwargs['out_1']
out_2: bytes = kwargs['out_2']
public_key: bytes = kwargs['public_key'] if 'public_key' in kwargs else Kal
timelock: int = kwargs['timelock']
_mining_fee, _out_1, _out_2, _public_key, _timelock = self.extractScriptLockScriptValues(script_out)
ensure(mining_fee == _mining_fee, 'mining mismatch fee')
ensure(out_1 == _out_1, 'out_1 mismatch')
ensure(out_2 == _out_2, 'out_2 mismatch')
ensure(public_key == _public_key, 'public_key mismatch')
ensure(timelock == _timelock, 'timelock mismatch')
return txid, locked_n
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, **kwargs):
# 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.nVersion == self.txVersion(), 'Bad version')
ensure(tx.nLockTime == 0, 'nLockTime not 0')
ensure(len(tx.vin) == 1, 'tx doesn\'t have one input')
ensure(tx.vin[0].nSequence == prevout_seq, 'Bad input nSequence')
ensure(tx.vin[0].scriptSig == self.getScriptScriptSig(prevout_script, None), 'Input scriptsig mismatch')
ensure(tx.vin[0].prevout.hash == b2i(prevout_id) and tx.vin[0].prevout.n == prevout_n, 'Input prevout mismatch')
ensure(len(tx.vout) == 1, 'tx doesn\'t have one output')
script_pk = self.getScriptDest(script_out)
locked_n = findOutput(tx, script_pk)
ensure(locked_n is not None, 'Output not found in tx')
locked_coin = tx.vout[locked_n].nValue
# Check script
mining_fee: int = kwargs['mining_fee'] if 'mining_fee' in kwargs else 1000
out_1: bytes = kwargs['out_1']
out_2: bytes = kwargs['out_2']
public_key: bytes = kwargs['public_key'] if 'public_key' in kwargs else Kal
timelock: int = kwargs['timelock']
_mining_fee, _out_1, _out_2, _public_key, _timelock = self.extractScriptLockScriptValues(script_out)
ensure(mining_fee == _mining_fee, 'mining mismatch fee')
ensure(out_1 == _out_1, 'out_1 mismatch')
ensure(out_2 == _out_2, 'out_2 mismatch')
ensure(public_key == _public_key, 'public_key mismatch')
ensure(timelock == _timelock, 'timelock mismatch')
fee_paid = locked_coin - mining_fee
assert (fee_paid > 0)
size = self.getTxSize(tx)
vsize = size
self._log.info('tx amount, vsize, fee: %ld, %ld, %ld', locked_coin, vsize, fee_paid)
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, **kwargs):
# 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.nVersion == self.txVersion(), 'Bad version')
ensure(tx.nLockTime == 0, 'nLockTime not 0')
ensure(len(tx.vin) == 1, 'tx doesn\'t have one input')
ensure(tx.vin[0].nSequence == 0, 'Bad input nSequence')
ensure(tx.vin[0].scriptSig == self.getScriptScriptSig(prevout_script, bytes(73)), 'Input scriptsig mismatch')
ensure(tx.vin[0].prevout.hash == b2i(lock_refund_tx_id) and tx.vin[0].prevout.n == 0, 'Input prevout mismatch')
ensure(len(tx.vout) == 1, 'tx doesn\'t have one output')
# Check script
mining_fee: int = kwargs['mining_fee'] if 'mining_fee' in kwargs else 1000
out_1: bytes = kwargs['out_1']
out_2: bytes = kwargs['out_2']
public_key: bytes = kwargs['public_key'] if 'public_key' in kwargs else Kal
timelock: int = kwargs['timelock']
_mining_fee, _out_1, _out_2, _public_key, _timelock = self.extractScriptLockScriptValues(prevout_script)
ensure(mining_fee == _mining_fee, 'mining mismatch fee')
ensure(out_1 == _out_1, 'out_1 mismatch')
ensure(out_2 == _out_2, 'out_2 mismatch')
ensure(public_key == _public_key, 'public_key mismatch')
ensure(timelock == _timelock, 'timelock mismatch')
tx_value = tx.vout[0].nValue
fee_paid = tx_value - mining_fee
assert (fee_paid > 0)
size = self.getTxSize(tx)
vsize = size
self._log.info('tx amount, vsize, fee: %ld, %ld, %ld', tx_value, vsize, fee_paid)
return True
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.nVersion == self.txVersion(), 'Bad version')
ensure(tx.nLockTime == 0, 'nLockTime not 0')
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].nValue
ensure(tx.vin[0].nSequence == 0, 'Bad input nSequence')
ensure(tx.vin[0].scriptSig == self.getScriptScriptSig(lock_tx_script), 'Input scriptsig mismatch')
# allow for this mismatch in BCH, since the lock txid will get changed after signing
# ensure(tx.vin[0].prevout.hash == b2i(lock_tx_id) and tx.vin[0].prevout.n == locked_n, 'Input prevout mismatch')
ensure(len(tx.vout) == 1, 'tx doesn\'t have one output')
p2pkh = self.getScriptForPubkeyHash(a_pkhash_f)
ensure(tx.vout[0].scriptPubKey == p2pkh, '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].nValue
assert (fee_paid > 0)
size = self.getTxSize(tx)
vsize = size
self._log.info('tx amount, vsize, fee: %ld, %ld, %ld', tx.vout[0].nValue, vsize, fee_paid)
return True
def signTxOtVES(self, key_sign: bytes, pubkey_encrypt: bytes, tx_bytes: bytes, input_n: int, prevout_script: bytes, prevout_value: int) -> bytes:
_, out_1, _, _, _ = self.extractScriptLockScriptValues(prevout_script)
msg = sha256(out_1)
return ecdsaotves_enc_sign(key_sign, pubkey_encrypt, msg)
def decryptOtVES(self, k: bytes, esig: bytes) -> bytes:
return ecdsaotves_dec_sig(k, esig)
def recoverEncKey(self, esig, sig, K):
return ecdsaotves_rec_enc_key(K, esig, sig)
def verifyTxOtVES(self, tx_bytes: bytes, ct: bytes, Ks: bytes, Ke: bytes, input_n: int, prevout_script: bytes, prevout_value):
_, out_1, _, _, _ = self.extractScriptLockScriptValues(prevout_script)
msg = sha256(out_1)
return ecdsaotves_enc_verify(Ks, Ke, msg, ct)
def extractLeaderSig(self, tx_bytes: bytes) -> bytes:
tx = self.loadTx(tx_bytes)
signature, _, _, _, _, _ = self.extractScriptLockScriptValuesFromScriptSig(tx.vin[0].scriptSig)
return signature

View file

@ -6,6 +6,7 @@
import struct import struct
import hashlib import hashlib
from basicswap.contrib.test_framework.script import OP_PUSHDATA1, OP_PUSHDATA2, OP_PUSHDATA4, CScriptInvalidError, CScriptTruncatedPushDataError
from basicswap.script import OpCodes from basicswap.script import OpCodes
@ -31,6 +32,49 @@ def decodeScriptNum(script_bytes, o):
v += int(b) << 8 * i v += int(b) << 8 * i
return (v, 1 + num_len) return (v, 1 + num_len)
def decodePushData(script_bytes, o):
datasize = None
pushdata_type = None
i = o
opcode = script_bytes[i]
i += 1
if opcode < OP_PUSHDATA1:
pushdata_type = 'PUSHDATA(%d)' % opcode
datasize = opcode
elif opcode == OP_PUSHDATA1:
pushdata_type = 'PUSHDATA1'
if i >= len(script_bytes):
raise CScriptInvalidError('PUSHDATA1: missing data length')
datasize = script_bytes[i]
i += 1
elif opcode == OP_PUSHDATA2:
pushdata_type = 'PUSHDATA2'
if i + 1 >= len(script_bytes):
raise CScriptInvalidError('PUSHDATA2: missing data length')
datasize = script_bytes[i] + (script_bytes[i + 1] << 8)
i += 2
elif opcode == OP_PUSHDATA4:
pushdata_type = 'PUSHDATA4'
if i + 3 >= len(script_bytes):
raise CScriptInvalidError('PUSHDATA4: missing data length')
datasize = script_bytes[i] + (script_bytes[i + 1] << 8) + (script_bytes[i + 2] << 16) + (script_bytes[i + 3] << 24)
i += 4
else:
assert False # shouldn't happen
data = bytes(script_bytes[i:i + datasize])
# Check for truncation
if len(data) < datasize:
raise CScriptTruncatedPushDataError('%s: truncated data' % pushdata_type, data)
# return data and the number of bytes to skip forward
return (data, i + datasize - o)
def getP2SHScriptForHash(p2sh): def getP2SHScriptForHash(p2sh):
return bytes((OpCodes.OP_HASH160, 0x14)) \ return bytes((OpCodes.OP_HASH160, 0x14)) \

View file

@ -1,11 +1,29 @@
import unittest import unittest
from basicswap.protocols.xmr_swap_1 import XmrBchSwapInterface from basicswap.contrib.test_framework.script import CScript
from basicswap.interface.bch import BCHInterface
bch_lock_spend_tx = '0200000001bfc6bbb47851441c7827059ae337a06aa9064da7f9537eb9243e45766c3dd34c00000000d8473045022100a0161ea14d3b41ed41250c8474fc8ec6ce1cab8df7f401e69ecf77c2ab63d82102207a2a57ddf2ea400e09ea059f3b261da96f5098858b17239931f3cc2fb929bb2a4c8ec3519dc4519d02e80300c600cc949d00ce00d18800cf00d28800d000d39d00cb641976a91481ec21969399d15c26af089d5db437ead066c5ba88ac00cd788821024ffcc0481629866671d89f05f3da813a2aacec1b52e69b8c0c586b665f5d4574ba6752b27523aa20df65a90e9becc316ff5aca44d4e06dfaade56622f32bafa197aba706c5e589758700cd87680000000001251cde06000000001976a91481ec21969399d15c26af089d5db437ead066c5ba88ac00000000'
bch_lock_script = 'c3519dc4519d02e80300c600cc949d00ce00d18800cf00d28800d000d39d00cb641976a91481ec21969399d15c26af089d5db437ead066c5ba88ac00cd788821024ffcc0481629866671d89f05f3da813a2aacec1b52e69b8c0c586b665f5d4574ba6752b27523aa20df65a90e9becc316ff5aca44d4e06dfaade56622f32bafa197aba706c5e589758700cd8768'
bch_lock_spend_script = '473045022100a0161ea14d3b41ed41250c8474fc8ec6ce1cab8df7f401e69ecf77c2ab63d82102207a2a57ddf2ea400e09ea059f3b261da96f5098858b17239931f3cc2fb929bb2a4c8ec3519dc4519d02e80300c600cc949d00ce00d18800cf00d28800d000d39d00cb641976a91481ec21969399d15c26af089d5db437ead066c5ba88ac00cd788821024ffcc0481629866671d89f05f3da813a2aacec1b52e69b8c0c586b665f5d4574ba6752b27523aa20df65a90e9becc316ff5aca44d4e06dfaade56622f32bafa197aba706c5e589758700cd8768'
coin_settings = {'rpcport': 0, 'rpcauth': 'none', 'blocks_confirmed': 1, 'conf_target': 1, 'use_segwit': False, 'connection_type': 'rpc'}
class TestXmrBchSwapInterface(unittest.TestCase): class TestXmrBchSwapInterface(unittest.TestCase):
def test_generate_script(self): # def test_generate_script(self):
out_1 = bytes.fromhex('a9147171b53baf87efc9c78ffc0e37a78859cebaae4a87') # out_1 = bytes.fromhex('a9147171b53baf87efc9c78ffc0e37a78859cebaae4a87')
out_2 = bytes.fromhex('a9147171b53baf87efc9c78ffc0e37a78859cebaae4a87') # out_2 = bytes.fromhex('a9147171b53baf87efc9c78ffc0e37a78859cebaae4a87')
public_key = bytes.fromhex('03fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556') # public_key = bytes.fromhex('03fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556')
print(XmrBchSwapInterface().genScriptLockTxScript(None, 1000, out_1, out_2, public_key, 2).hex())
# ci = BCHInterface(coin_settings, "regtest")
# print(ci.genScriptLockTxScript(None, 1000, out_1, out_2, public_key, 2).hex())
def test_extractScriptLockScriptValues(self):
ci = BCHInterface(coin_settings, "regtest")
script_bytes = CScript(bytes.fromhex(bch_lock_script))
ci.extractScriptLockScriptValues(script_bytes)
script_bytes = CScript(bytes.fromhex(bch_lock_spend_script))
signature, mining_fee, out_1, out_2, public_key, timelock = ci.extractScriptLockScriptValuesFromScriptSig(script_bytes)
print(timelock)

View file

@ -90,7 +90,7 @@ class TestFunctions(BaseTest):
swap_clients = self.swap_clients swap_clients = self.swap_clients
ci = swap_clients[0].ci(Coins.BCH) ci = swap_clients[0].ci(Coins.BCH)
pi = swap_clients[0].pi(SwapTypes.XMR_BCH_SWAP) pi = swap_clients[0].pi(SwapTypes.XMR_SWAP)
amount: int = ci.make_int(random.uniform(0.1, 2.0), r=1) amount: int = ci.make_int(random.uniform(0.1, 2.0), r=1)
@ -108,17 +108,36 @@ class TestFunctions(BaseTest):
mining_fee = 1000 mining_fee = 1000
timelock = 2 timelock = 2
a_receive = ci.getNewAddress() b_receive = ci.getNewAddress()
b_refund = ci.getNewAddress() a_refund = ci.getNewAddress()
refund_lock_tx_script = pi.genScriptLockTxScript(mining_fee=mining_fee, out_1=ci.addressToLockingBytecode(b_refund), out_2=ci.addressToLockingBytecode(a_receive), public_key=A, timelock=timelock)
addr_out = ci.getNewAddress()
lock_tx_script = pi.genScriptLockTxScript(mining_fee=mining_fee, out_1=ci.addressToLockingBytecode(a_receive), out_2=ci.scriptToP2SH32LockingBytecode(refund_lock_tx_script), public_key=B, timelock=timelock) refundExtraArgs = dict()
lockExtraArgs = dict()
refundExtraArgs['mining_fee'] = 1000
refundExtraArgs['out_1'] = ci.addressToLockingBytecode(a_refund)
refundExtraArgs['out_2'] = ci.addressToLockingBytecode(b_receive)
refundExtraArgs['public_key'] = B
refundExtraArgs['timelock'] = 5
refund_lock_tx_script = pi.genScriptLockTxScript(ci, A, B, **refundExtraArgs)
# will make use of this in `createSCLockRefundTx`
refundExtraArgs['refund_lock_tx_script'] = refund_lock_tx_script
# lock script
lockExtraArgs['mining_fee'] = 1000
lockExtraArgs['out_1'] = ci.addressToLockingBytecode(b_receive)
lockExtraArgs['out_2'] = ci.scriptToP2SH32LockingBytecode(refund_lock_tx_script)
lockExtraArgs['public_key'] = A
lockExtraArgs['timelock'] = 2
lock_tx_script = pi.genScriptLockTxScript(ci, A, B, **lockExtraArgs)
lock_tx = ci.createSCLockTx(amount, lock_tx_script) lock_tx = ci.createSCLockTx(amount, lock_tx_script)
lock_tx = ci.fundSCLockTx(lock_tx, fee_rate) lock_tx = ci.fundSCLockTx(lock_tx, fee_rate)
lock_tx = ci.signTxWithWallet(lock_tx) lock_tx = ci.signTxWithWallet(lock_tx)
print(lock_tx.hex()) print(lock_tx.hex())
return
unspents_after = ci.rpc('listunspent') unspents_after = ci.rpc('listunspent')
assert (len(unspents) > len(unspents_after)) assert (len(unspents) > len(unspents_after))
@ -151,25 +170,25 @@ class TestFunctions(BaseTest):
assert (wallet_tx_fee == fee_value) assert (wallet_tx_fee == fee_value)
assert (wallet_tx_fee == expect_fee_int) assert (wallet_tx_fee == expect_fee_int)
pkh_out = ci.decodeAddress(a_receive) pkh_out = ci.decodeAddress(b_receive)
msg = sha256(ci.addressToLockingBytecode(a_receive)) msg = sha256(ci.addressToLockingBytecode(b_receive))
# bob creates an adaptor signature for alice and transmits it to her # leader creates an adaptor signature for follower and transmits it to the follower
bAdaptorSig = ecdsaotves_enc_sign(b, A, msg) aAdaptorSig = ecdsaotves_enc_sign(a, B, msg)
# alice verifies the adaptor signature # alice verifies the adaptor signature
assert (ecdsaotves_enc_verify(B, A, msg, bAdaptorSig)) assert (ecdsaotves_enc_verify(A, B, msg, aAdaptorSig))
# alice decrypts the adaptor signature # alice decrypts the adaptor signature
bAdaptorSig_dec = ecdsaotves_dec_sig(a, bAdaptorSig) aAdaptorSig_dec = ecdsaotves_dec_sig(b, aAdaptorSig)
fee_info = {} fee_info = {}
lock_spend_tx = ci.createSCLockSpendTx(lock_tx, lock_tx_script, pkh_out, mining_fee, ves=bAdaptorSig_dec, fee_info=fee_info) lock_spend_tx = ci.createSCLockSpendTx(lock_tx, lock_tx_script, pkh_out, mining_fee, fee_info=fee_info, ves=aAdaptorSig_dec)
vsize_estimated: int = fee_info['vsize'] vsize_estimated: int = fee_info['vsize']
tx_decoded = ci.rpc('decoderawtransaction', [lock_spend_tx.hex()]) tx_decoded = ci.rpc('decoderawtransaction', [lock_spend_tx.hex()])
print(tx_decoded) print('lock_spend_tx', lock_spend_tx.hex(), '\n', 'tx_decoded', tx_decoded)
txid = tx_decoded['txid'] txid = tx_decoded['txid']
tx_decoded = ci.rpc('decoderawtransaction', [lock_spend_tx.hex()]) tx_decoded = ci.rpc('decoderawtransaction', [lock_spend_tx.hex()])

View file

@ -98,11 +98,11 @@ from basicswap.bin.run import startDaemon, startXmrDaemon, startXmrWalletDaemon
logger = logging.getLogger() logger = logging.getLogger()
NUM_NODES = 2 NUM_NODES = 3
NUM_XMR_NODES = 2 NUM_XMR_NODES = 3
NUM_BTC_NODES = 2 NUM_BTC_NODES = 3
NUM_BCH_NODES = 2 NUM_BCH_NODES = 3
NUM_LTC_NODES = 2 NUM_LTC_NODES = 3
TEST_DIR = cfg.TEST_DATADIRS TEST_DIR = cfg.TEST_DATADIRS
XMR_BASE_P2P_PORT = 17792 XMR_BASE_P2P_PORT = 17792
@ -692,7 +692,7 @@ class BaseTest(unittest.TestCase):
if cls.btc_addr is not None: if cls.btc_addr is not None:
btcCli('generatetoaddress 1 {}'.format(cls.btc_addr)) btcCli('generatetoaddress 1 {}'.format(cls.btc_addr))
if cls.bch_addr is not None: if cls.bch_addr is not None:
ltcCli('generatetoaddress 1 {}'.format(cls.bch_addr)) bchCli('generatetoaddress 1 {}'.format(cls.bch_addr))
if cls.ltc_addr is not None: if cls.ltc_addr is not None:
ltcCli('generatetoaddress 1 {}'.format(cls.ltc_addr)) ltcCli('generatetoaddress 1 {}'.format(cls.ltc_addr))
if cls.xmr_addr is not None: if cls.xmr_addr is not None: