Add BTC type swipe tx mercy outputs.

This commit is contained in:
tecnovert 2024-11-08 14:18:06 +02:00
parent 9d7841da46
commit c561efaba0
No known key found for this signature in database
GPG key ID: 8ED6D8750C4E3F93
7 changed files with 119 additions and 18 deletions

View file

@ -4492,7 +4492,7 @@ class BasicSwap(BaseApp):
if session is None:
self.closeSession(use_session)
def process_XMR_SWAP_A_LOCK_REFUND_tx_spend(self, bid_id: bytes, spend_txid_hex, spend_txn) -> None:
def process_XMR_SWAP_A_LOCK_REFUND_tx_spend(self, bid_id: bytes, spend_txid_hex: str, spend_txn) -> None:
self.log.debug('Detected spend of Adaptor-sig swap coin a lock refund tx for bid %s', bid_id.hex())
try:
session = self.openSession()
@ -4510,15 +4510,17 @@ class BasicSwap(BaseApp):
was_sent: bool = bid.was_received if reverse_bid else bid.was_sent
was_received: bool = bid.was_sent if reverse_bid else bid.was_received
ci_from = self.ci(coin_from)
state = BidStates(bid.state)
spending_txid = bytes.fromhex(spend_txid_hex)
spend_txn_hex = spend_txn['hex']
spend_tx = self.ci(coin_from).loadTx(h2b(spend_txn_hex))
spend_tx = ci_from.loadTx(h2b(spend_txn_hex))
is_spending_lock_refund_tx = False
if self.isBchXmrSwap(offer):
is_spending_lock_refund_tx = self.ci(coin_from).isSpendingLockRefundTx(spend_tx)
is_spending_lock_refund_tx = ci_from.isSpendingLockRefundTx(spend_tx)
if spending_txid == xmr_swap.a_lock_refund_spend_tx_id or (i2b(spend_tx.vin[0].prevout.hash) == xmr_swap.a_lock_refund_tx_id and is_spending_lock_refund_tx):
self.log.info('Found coin a lock refund spend tx, bid {}'.format(bid_id.hex()))
@ -4559,9 +4561,38 @@ class BasicSwap(BaseApp):
else:
self.log.info('Coin a lock refund spent by unknown tx, bid {}'.format(bid_id.hex()))
if not was_received or bid.xmr_b_lock_tx is None:
# Leave active if was received, to try and get lock tx b with mercy release
mercy_keyshare = None
if was_received:
if self.isBchXmrSwap(offer):
# Mercy tx is sent separately
pass
else:
# Look for a mercy output
try:
mercy_keyshare = ci_from.inspectSwipeTx(spend_txn)
if mercy_keyshare is None:
raise ValueError('Not found')
ensure(self.ci(coin_to).verifyKey(mercy_keyshare), 'Invalid keyshare')
except Exception as e:
self.log.warning('Could not extract mercy output from swipe tx: {}, {}'.format(spend_txid_hex, e))
if mercy_keyshare is None:
bid.setState(BidStates.XMR_SWAP_FAILED_SWIPED)
else:
delay = self.get_delay_event_seconds()
self.log.info('Redeeming coin b lock tx for bid %s in %d seconds', bid_id.hex(), delay)
self.createActionInSession(delay, ActionTypes.REDEEM_XMR_SWAP_LOCK_TX_B, bid_id, session)
else:
bid.setState(BidStates.XMR_SWAP_FAILED_SWIPED)
if TxTypes.XMR_SWAP_A_LOCK_REFUND_SWIPE not in bid.txns:
bid.txns[TxTypes.XMR_SWAP_A_LOCK_REFUND_SWIPE] = SwapTx(
bid_id=bid_id,
tx_type=TxTypes.XMR_SWAP_A_LOCK_REFUND_SWIPE,
txid=spending_txid,
)
if mercy_keyshare:
bid.txns[TxTypes.XMR_SWAP_A_LOCK_REFUND_SWIPE].tx_data = mercy_keyshare
self.saveBidInSession(bid_id, bid, session, xmr_swap, save_in_progress=offer)
except Exception as ex:
@ -4628,15 +4659,15 @@ class BasicSwap(BaseApp):
if bid_id not in self.swaps_in_progress:
self.log.warning('Could not find active bid for found mercy tx: {}'.format(bid_id.hex()))
else:
remote_keyshare = bytes.fromhex(tx['vout'][0]['scriptPubKey']['asm'].split(' ')[2])
ensure(ci.verifyKey(remote_keyshare), 'Invalid keyshare')
mercy_keyshare = bytes.fromhex(tx['vout'][0]['scriptPubKey']['asm'].split(' ')[2])
ensure(ci.verifyKey(mercy_keyshare), 'Invalid keyshare')
bid = self.swaps_in_progress[bid_id][0]
bid.txns[TxTypes.BCH_MERCY] = SwapTx(
bid_id=bid_id,
tx_type=TxTypes.BCH_MERCY,
txid=txid,
tx_data=remote_keyshare,
tx_data=mercy_keyshare,
)
self.saveBid(bid_id, bid)
@ -6168,15 +6199,19 @@ class BasicSwap(BaseApp):
if lock_tx_depth < ci_to.depth_spendable():
raise TemporaryError(f'Chain B lock tx depth {lock_tx_depth} < required for spending.')
# Extract the leader's decrypted signature and use it to recover the follower's privatekey
xmr_swap.al_lock_spend_tx_sig = ci_from.extractLeaderSig(xmr_swap.a_lock_spend_tx)
if TxTypes.BCH_MERCY in bid.txns:
self.log.info('Using keyshare from mercy tx.')
kbsf = bid.txns[TxTypes.BCH_MERCY].tx_data
pkbsf = ci_to.getPubkey(kbsf)
ensure(pkbsf == xmr_swap.pkbsf, 'Keyshare from mercy tx does not match expected pubkey')
elif TxTypes.XMR_SWAP_A_LOCK_REFUND_SWIPE in bid.txns:
self.log.info('Using keyshare from swipe tx.')
kbsf = bid.txns[TxTypes.XMR_SWAP_A_LOCK_REFUND_SWIPE].tx_data
pkbsf = ci_to.getPubkey(kbsf)
ensure(pkbsf == xmr_swap.pkbsf, 'Keyshare from swipe tx does not match expected pubkey')
else:
# Extract the leader's decrypted signature and use it to recover the follower's privatekey
xmr_swap.al_lock_spend_tx_sig = ci_from.extractLeaderSig(xmr_swap.a_lock_spend_tx)
kbsf = ci_from.recoverEncKey(xmr_swap.al_lock_spend_tx_esig, xmr_swap.al_lock_spend_tx_sig, xmr_swap.pkasf)
assert (kbsf is not None)
@ -6201,7 +6236,6 @@ class BasicSwap(BaseApp):
if num_retries > 0:
error_msg += ', retry no. {}'.format(num_retries)
self.log.error(error_msg)
self.log.error(traceback.format_exc()) # [rm]
if num_retries < 100 and (ci_to.is_transient_error(ex) or self.is_transient_error(ex)):
delay = self.get_delay_retry_seconds()
@ -7912,7 +7946,7 @@ class BasicSwap(BaseApp):
kbsf = None
# BCH sends a separate mercy tx
else:
for_ed25519: bool = True if ci.curve_type() == Curves.ed25519 else False
for_ed25519: bool = True if self.ci(coin_to).curve_type() == Curves.ed25519 else False
kbsf = self.getPathKey(coin_from, coin_to, bid.created_at, xmr_swap.contract_count, KeyTypes.KBSF, for_ed25519)
pkh_dest = ci.decodeAddress(self.getReceiveAddressForCoin(ci.coin_type()))

View file

@ -75,6 +75,7 @@ from basicswap.contrib.test_framework.script import (
OP_CHECKSEQUENCEVERIFY,
OP_DROP,
OP_HASH160, OP_EQUAL,
OP_RETURN,
SIGHASH_ALL,
SegwitV0SignatureHash,
)
@ -258,6 +259,7 @@ class BTCInterface(Secp256k1Interface):
self._sc = swap_client
self._log = self._sc.log if self._sc and self._sc.log else logging
self._expect_seedid_hex = None
self._altruistic = coin_settings.get('altruistic', True)
def open_rpc(self, wallet=None):
return openrpc(self._rpcport, self._rpcauth, wallet=wallet, host=self._rpc_host)
@ -601,7 +603,7 @@ class BTCInterface(Secp256k1Interface):
return tx.serialize()
def createSCLockRefundSpendToFTx(self, tx_lock_refund_bytes, script_lock_refund, pkh_dest, tx_fee_rate, vkbv=None):
def createSCLockRefundSpendToFTx(self, tx_lock_refund_bytes, script_lock_refund, pkh_dest, tx_fee_rate, vkbv=None, kbsf=None):
# lock refund swipe tx
# Sends the coinA locked coin to the follower
@ -625,6 +627,10 @@ class BTCInterface(Secp256k1Interface):
tx.vout.append(self.txoType()(locked_coin, self.getScriptForPubkeyHash(pkh_dest)))
if self._altruistic and kbsf:
# Add mercy_keyshare
tx.vout.append(self.txoType()(0, CScript([OP_RETURN, b'XBSW', kbsf])))
dummy_witness_stack = self.getScriptLockRefundSwipeTxDummyWitness(script_lock_refund)
witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack)
vsize = self.getTxVSize(tx, add_witness_bytes=witness_bytes)
@ -1492,6 +1498,18 @@ class BTCInterface(Secp256k1Interface):
'amount': txjs['vout'][n]['value']
}
def inspectSwipeTx(self, tx: dict) -> bytes | None:
mercy_keyshare = None
for vout in tx['vout']:
script_bytes = bytes.fromhex(vout['scriptPubKey']['hex'])
if len(script_bytes) < 39:
continue
if script_bytes[0] != OP_RETURN:
continue
script_bytes[0]
return script_bytes[7: 7 + 32]
return None
def isTxExistsError(self, err_str: str) -> bool:
return 'Transaction already in block chain' in err_str

View file

@ -262,6 +262,7 @@ class DCRInterface(Secp256k1Interface):
self._use_segwit = True # Decred is natively segwit
self._connection_type = coin_settings['connection_type']
self._altruistic = coin_settings.get('altruistic', True)
def open_rpc(self):
return openrpc(self._rpcport, self._rpcauth, host=self._rpc_host)
@ -1232,7 +1233,7 @@ class DCRInterface(Secp256k1Interface):
return True
def createSCLockRefundSpendToFTx(self, tx_lock_refund_bytes, script_lock_refund, pkh_dest, tx_fee_rate, vkbv=None):
def createSCLockRefundSpendToFTx(self, tx_lock_refund_bytes, script_lock_refund, pkh_dest, tx_fee_rate, vkbv=None, kbsf=None):
# lock refund swipe tx
# Sends the coinA locked coin to the follower

View file

@ -668,7 +668,7 @@ class NAVInterface(BTCInterface):
return tx.serialize()
def createSCLockRefundSpendToFTx(self, tx_lock_refund_bytes, script_lock_refund, pkh_dest, tx_fee_rate, vkbv=None):
def createSCLockRefundSpendToFTx(self, tx_lock_refund_bytes, script_lock_refund, pkh_dest, tx_fee_rate, vkbv=None, kbsf=None):
# lock refund swipe tx
# Sends the coinA locked coin to the follower

View file

@ -622,7 +622,7 @@ class PARTInterfaceBlind(PARTInterface):
return True
def createSCLockRefundSpendToFTx(self, tx_lock_refund_bytes, script_lock_refund, pkh_dest, tx_fee_rate, vkbv):
def createSCLockRefundSpendToFTx(self, tx_lock_refund_bytes, script_lock_refund, pkh_dest, tx_fee_rate, vkbv, kbsf=None):
# lock refund swipe tx
# Sends the coinA locked coin to the follower
lock_refund_tx_obj = self.rpc('decoderawtransaction', [tx_lock_refund_bytes.hex()])

View file

@ -268,7 +268,7 @@ class TestFunctions(BaseTest):
logging.info('---------- Test {} to {} follower recovers coin a lock tx{}'.format(coin_from.name, coin_to.name, ' (with mercy tx)' if with_mercy else ''))
# Leader is too slow to recover the coin a lock tx and follower swipes it
# coin b lock tx remains unspent
# Coin B lock tx remains unspent unless a mercy output revealing the follower's keyshare is sent
id_offerer: int = self.node_a_id
id_bidder: int = self.node_b_id
@ -282,6 +282,8 @@ class TestFunctions(BaseTest):
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}')
swap_clients[id_follower].ci(coin_from if reverse_bid else coin_to)._altruistic = with_mercy
js_w0_before = read_json_api(1800 + id_offerer, 'wallets')
js_w1_before = read_json_api(1800 + id_bidder, 'wallets')
@ -941,6 +943,17 @@ class BasicSwapTest(TestFunctions):
def test_03_d_follower_recover_a_lock_tx_from_part(self):
self.do_test_03_follower_recover_a_lock_tx(Coins.PART, self.test_coin_from)
def test_03_e_follower_recover_a_lock_tx_mercy_release(self):
if not self.has_segwit:
return
self.do_test_03_follower_recover_a_lock_tx(self.test_coin_from, Coins.XMR, with_mercy=True)
def test_03_f_follower_recover_a_lock_tx_mercy_release_reverse(self):
if not self.has_segwit:
return
self.prepare_balance(Coins.XMR, 100.0, 1800, 1801)
self.do_test_03_follower_recover_a_lock_tx(Coins.XMR, self.test_coin_from, with_mercy=True)
def test_04_a_follower_recover_b_lock_tx(self):
if not self.has_segwit:
return

View file

@ -1031,6 +1031,8 @@ class Test(BaseTest):
logging.info('---------- Test PART to XMR follower recovers coin a lock tx')
swap_clients = self.swap_clients
swap_clients[1].ci(Coins.PART)._altruistic = False
offer_id = swap_clients[0].postOffer(
Coins.PART, Coins.XMR, 101 * COIN, 0.13 * XMR_COIN, 101 * COIN, SwapTypes.XMR_SWAP,
lock_type=TxLockTypes.SEQUENCE_LOCK_BLOCKS, lock_value=16)
@ -1062,6 +1064,39 @@ class Test(BaseTest):
bidder_states = [s for s in bidder_states if s[1] != 'Bid Stalled (debug)']
assert (compare_bid_states(bidder_states, self.states_bidder[2]) is True)
def test_03b_follower_recover_a_lock_tx_with_mercy(self):
logging.info('---------- Test PART to XMR follower recovers coin a lock tx with mercy output')
swap_clients = self.swap_clients
swap_clients[1].ci(Coins.PART)._altruistic = True
offer_id = swap_clients[0].postOffer(
Coins.PART, Coins.XMR, 101 * COIN, 0.13 * XMR_COIN, 101 * COIN, SwapTypes.XMR_SWAP,
lock_type=TxLockTypes.SEQUENCE_LOCK_BLOCKS, lock_value=16)
wait_for_offer(test_delay_event, swap_clients[1], offer_id)
offer = swap_clients[1].getOffer(offer_id)
bid_id = swap_clients[1].postXmrBid(offer_id, offer.amount_from)
wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.BID_RECEIVED)
bid, xmr_swap = swap_clients[0].getXmrBid(bid_id)
assert (xmr_swap)
swap_clients[1].setBidDebugInd(bid_id, DebugTypes.BID_DONT_SPEND_COIN_B_LOCK)
swap_clients[0].setBidDebugInd(bid_id, DebugTypes.BID_DONT_SPEND_COIN_A_LOCK_REFUND2)
swap_clients[0].setBidDebugInd(bid_id, DebugTypes.WAIT_FOR_COIN_B_LOCK_BEFORE_REFUND, False)
swap_clients[1].setBidDebugInd(bid_id, DebugTypes.WAIT_FOR_COIN_B_LOCK_BEFORE_REFUND, False)
swap_clients[0].acceptXmrBid(bid_id)
wait_for_bid(test_delay_event, swap_clients[0], bid_id, (BidStates.XMR_SWAP_NOSCRIPT_TX_REDEEMED, BidStates.SWAP_COMPLETED), wait_for=220)
wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.XMR_SWAP_FAILED_SWIPED, wait_for=120, sent=True)
wait_for_none_active(test_delay_event, 1800)
wait_for_none_active(test_delay_event, 1801)
def test_04_follower_recover_b_lock_tx(self):
logging.info('---------- Test PART to XMR follower recovers coin b lock tx')