WIP continue implementing the BCH swap interface for XMR swap and atomic swap protocols

This commit is contained in:
mainnet-pat 2024-10-14 15:35:24 +00:00 committed by nahuhh
parent a4b411f1fd
commit 58b42c0d9a
9 changed files with 428 additions and 122 deletions

View file

@ -251,7 +251,6 @@ class BasicSwap(BaseApp):
protocolInterfaces = { protocolInterfaces = {
SwapTypes.SELLER_FIRST: atomic_swap_1.AtomicSwapInterface(), SwapTypes.SELLER_FIRST: atomic_swap_1.AtomicSwapInterface(),
SwapTypes.XMR_SWAP: xmr_swap_1.XmrSwapInterface(), SwapTypes.XMR_SWAP: xmr_swap_1.XmrSwapInterface(),
SwapTypes.XMR_BCH_SWAP: xmr_swap_1.XmrBchSwapInterface(),
} }
def __init__(self, fp, data_dir, settings, chain, log_name='BasicSwap', transient_instance=False): def __init__(self, fp, data_dir, settings, chain, log_name='BasicSwap', transient_instance=False):
@ -675,6 +674,9 @@ class BasicSwap(BaseApp):
return self.coin_clients[use_coinid][interface_ind] return self.coin_clients[use_coinid][interface_ind]
def isBchXmrSwap(self, offer: Offer):
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:
raise ValueError('Unknown protocol_ind {}'.format(int(protocol_ind))) raise ValueError('Unknown protocol_ind {}'.format(int(protocol_ind)))
@ -2976,7 +2978,32 @@ class BasicSwap(BaseApp):
# MSG2F # MSG2F
pi = self.pi(SwapTypes.XMR_SWAP) pi = self.pi(SwapTypes.XMR_SWAP)
xmr_swap.a_lock_tx_script = pi.genScriptLockTxScript(ci_from, xmr_swap.pkal, xmr_swap.pkaf)
refundExtraArgs = dict()
lockExtraArgs = dict()
if self.isBchXmrSwap(offer):
pkh_refund_to = ci_from.decodeAddress(self.getCachedAddressForCoin(coin_from))
pkh_dest = xmr_swap.dest_af
# refund script
refundExtraArgs['mining_fee'] = 1000
refundExtraArgs['out_1'] = ci_from.getScriptForPubkeyHash(pkh_refund_to)
refundExtraArgs['out_2'] = ci_from.getScriptForPubkeyHash(pkh_dest)
refundExtraArgs['public_key'] = xmr_swap.pkaf
refundExtraArgs['timelock'] = xmr_offer.lock_time_2
refund_lock_tx_script = pi.genScriptLockTxScript(ci_from, xmr_swap.pkal, xmr_swap.pkaf, 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_from.getScriptForPubkeyHash(pkh_refund_to)
lockExtraArgs['out_2'] = ci_from.scriptToP2SH32LockingBytecode(refund_lock_tx_script)
lockExtraArgs['public_key'] = xmr_swap.pkal
lockExtraArgs['timelock'] = xmr_offer.lock_time_1
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)
@ -2994,7 +3021,7 @@ class BasicSwap(BaseApp):
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 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)
@ -3003,7 +3030,7 @@ class BasicSwap(BaseApp):
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.getReceiveAddressForCoin(coin_from)) 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,
@ -3894,6 +3921,7 @@ 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
@ -4847,10 +4875,11 @@ class BasicSwap(BaseApp):
raise ValueError('TODO') raise ValueError('TODO')
elif offer_data.swap_type == SwapTypes.XMR_SWAP: elif offer_data.swap_type == SwapTypes.XMR_SWAP:
ensure(offer_data.protocol_version >= MINPROTO_VERSION_ADAPTOR_SIG, 'Invalid protocol version') ensure(offer_data.protocol_version >= MINPROTO_VERSION_ADAPTOR_SIG, 'Invalid protocol version')
if reverse_bid: if Coins.BCH not in (coin_from, coin_to):
ensure(ci_to.has_segwit(), 'Coin-to must support segwit for reverse bid offers') if reverse_bid:
else: ensure(ci_to.has_segwit(), 'Coin-to must support segwit for reverse bid offers')
ensure(ci_from.has_segwit(), 'Coin-from must support segwit') else:
ensure(ci_from.has_segwit(), 'Coin-from must support segwit')
ensure(len(offer_data.proof_address) == 0, 'Unexpected data') ensure(len(offer_data.proof_address) == 0, 'Unexpected data')
ensure(len(offer_data.proof_signature) == 0, 'Unexpected data') ensure(len(offer_data.proof_signature) == 0, 'Unexpected data')
ensure(len(offer_data.pkhash_seller) == 0, 'Unexpected data') ensure(len(offer_data.pkhash_seller) == 0, 'Unexpected data')
@ -5695,6 +5724,7 @@ 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)
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)
@ -5764,6 +5794,7 @@ 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)

View file

@ -49,8 +49,8 @@ LITECOIN_VERSION_TAG = os.getenv('LITECOIN_VERSION_TAG', '')
BITCOIN_VERSION = os.getenv('BITCOIN_VERSION', '26.0') BITCOIN_VERSION = os.getenv('BITCOIN_VERSION', '26.0')
BITCOIN_VERSION_TAG = os.getenv('BITCOIN_VERSION_TAG', '') BITCOIN_VERSION_TAG = os.getenv('BITCOIN_VERSION_TAG', '')
BITCOINCASH_VERSION = os.getenv('BITCOIN_VERSION', '27.1.0') BITCOINCASH_VERSION = os.getenv('BITCOINCASH_VERSION', '27.1.0')
BITCOINCASH_VERSION_TAG = os.getenv('BITCOIN_VERSION_TAG', '') BITCOINCASH_VERSION_TAG = os.getenv('BITCOINCASH_VERSION_TAG', '')
MONERO_VERSION = os.getenv('MONERO_VERSION', '0.18.3.4') MONERO_VERSION = os.getenv('MONERO_VERSION', '0.18.3.4')
MONERO_VERSION_TAG = os.getenv('MONERO_VERSION_TAG', '') MONERO_VERSION_TAG = os.getenv('MONERO_VERSION_TAG', '')

View file

@ -440,6 +440,9 @@ chainparams = {
'message_magic': 'Bitcoin Signed Message:\n', 'message_magic': 'Bitcoin Signed Message:\n',
'blocks_target': 60 * 2, 'blocks_target': 60 * 2,
'decimal_places': 8, 'decimal_places': 8,
'has_cltv': True,
'has_csv': True,
'has_segwit': False,
'mainnet': { 'mainnet': {
'rpcport': 8332, 'rpcport': 8332,
'pubkey_address': 0, 'pubkey_address': 0,

View file

@ -1,22 +1,62 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2020-2023 tecnovert # Copyright (c) 2022-2023 tecnovert
# Distributed under the MIT software license, see the accompanying # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
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 ensure, i2h from basicswap.util import b2h, ensure, i2h
from .btc import BTCInterface, findOutput from .btc import BTCInterface, findOutput
from basicswap.rpc import make_rpc_func from basicswap.rpc import make_rpc_func
from basicswap.chainparams import Coins, chainparams 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
from basicswap.util.crypto import hash160, sha256 from basicswap.util.crypto import hash160, sha256
from basicswap.interface.contrib.bch_test_framework.script import OP_EQUAL, OP_EQUALVERIFY, OP_HASH256, OP_DUP, OP_HASH160, OP_CHECKSIG from basicswap.interface.contrib.bch_test_framework.script import (
OP_TXINPUTCOUNT,
OP_1,
OP_NUMEQUALVERIFY,
OP_TXOUTPUTCOUNT,
OP_0,
OP_UTXOVALUE,
OP_OUTPUTVALUE,
OP_SUB,
OP_UTXOTOKENCATEGORY,
OP_OUTPUTTOKENCATEGORY,
OP_EQUALVERIFY,
OP_UTXOTOKENCOMMITMENT,
OP_OUTPUTTOKENCOMMITMENT,
OP_UTXOTOKENAMOUNT,
OP_OUTPUTTOKENAMOUNT,
OP_INPUTSEQUENCENUMBER,
OP_NOTIF,
OP_OUTPUTBYTECODE,
OP_OVER,
OP_CHECKDATASIG,
OP_ELSE,
OP_CHECKSEQUENCEVERIFY,
OP_DROP,
OP_EQUAL,
OP_ENDIF,
OP_HASH160,
OP_DUP,
OP_CHECKSIG,
OP_HASH256,
)
from basicswap.contrib.test_framework.script import ( from basicswap.contrib.test_framework.script import (
CScript, CScriptOp, CScript, CScriptOp,
) )
from coincurve.keys import (
PrivateKey,
PublicKey,
)
def findOutput(tx, script_pk: bytes):
for i in range(len(tx.vout)):
if tx.vout[i].scriptPubKey == script_pk:
return i
return None
class BCHInterface(BTCInterface): class BCHInterface(BTCInterface):
@staticmethod @staticmethod
@ -25,11 +65,184 @@ class BCHInterface(BTCInterface):
def __init__(self, coin_settings, network, swap_client=None): def __init__(self, coin_settings, network, swap_client=None):
super(BCHInterface, self).__init__(coin_settings, network, swap_client) super(BCHInterface, self).__init__(coin_settings, network, swap_client)
# No multiwallet support
self.rpc_wallet = make_rpc_func(self._rpcport, self._rpcauth, host=self._rpc_host)
def getExchangeName(self, exchange_name):
return 'bch'
def getNewAddress(self, use_segwit: bool = False, label: str = 'swap_receive') -> str:
args = [label]
return self.rpc_wallet('getnewaddress', args)
# returns pkh
def decodeAddress(self, address: str) -> bytes: def decodeAddress(self, address: str) -> bytes:
return bytes(Address.from_string(address).payload) return bytes(Address.from_string(address).payload)
def encodeSegwitAddress(self, script):
raise ValueError('TODO')
def decodeSegwitAddress(self, addr):
raise ValueError('TODO')
def getSCLockScriptAddress(self, lock_script: bytes) -> str:
lock_tx_dest = self.getScriptDest(lock_script)
address = self.encodeScriptDest(lock_tx_dest)
if not self.isAddressMine(address, or_watch_only=True):
# Expects P2WSH nested in BIP16_P2SH
ro = self.rpc('importaddress', [lock_tx_dest.hex(), 'bid lock', False, True])
addr_info = self.rpc('validateaddress', [address])
return address
def createRawFundedTransaction(self, addr_to: str, amount: int, sub_fee: bool = False, lock_unspents: bool = True) -> str:
txn = self.rpc('createrawtransaction', [[], {addr_to: self.format_amount(amount)}])
options = {
'lockUnspents': lock_unspents,
# 'conf_target': self._conf_target,
}
if sub_fee:
options['subtractFeeFromOutputs'] = [0,]
return self.rpc_wallet('fundrawtransaction', [txn, options])['hex']
def getScriptForPubkeyHash(self, pkh: bytes) -> bytearray:
# Return P2PKH
return CScript([OP_DUP, OP_HASH160, pkh, OP_EQUALVERIFY, OP_CHECKSIG])
# def getScriptDest(self, script: bytearray) -> bytearray:
# # P2SH
# script_hash = hash160(script)
# assert len(script_hash) == 20
# return CScript([OP_HASH160, script_hash, OP_EQUAL])
def encodeScriptDest(self, script_dest: bytes) -> str:
# Extract hash from script
script_hash = script_dest[2:-1]
return self.sh_to_address(script_hash)
def sh_to_address(self, sh: bytes) -> str:
assert (len(sh) == 20)
network = self._network.upper()
address = Address("P2SH20" if network == "MAINNET" else "P2SH20-"+network, sh)
return address.cash_address()
def getDestForScriptHash(self, script_hash):
assert len(script_hash) == 20
return CScript([OP_HASH160, script_hash, OP_EQUAL])
def withdrawCoin(self, value: float, addr_to: str, subfee: bool):
params = [addr_to, value, '', '', subfee, True, True]
return self.rpc_wallet('sendtoaddress', params)
def getSpendableBalance(self) -> int:
return self.make_int(self.rpc('getwalletinfo')['unconfirmed_balance'])
def getBLockSpendTxFee(self, tx, fee_rate: int) -> int:
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}.')
return pay_fee
def findTxnByHash(self, txid_hex: str):
# Only works for wallet txns
try:
rv = self.rpc('gettransaction', [txid_hex])
except Exception as ex:
self._log.debug('findTxnByHash getrawtransaction failed: {}'.format(txid_hex))
return None
if 'confirmations' in rv and rv['confirmations'] >= self.blocks_confirmed:
block_height = self.getBlockHeader(rv['blockhash'])['height']
return {'txid': txid_hex, 'amount': 0, 'height': block_height}
return None
def genScriptLockTxScript(self, ci, Kal: bytes, Kaf: bytes, **kwargs) -> CScript:
print("bch genScriptLockTxScript")
mining_fee: int = kwargs['mining_fee']
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']
return CScript([
# // v4.1.0-CashTokens-Optimized
# // Based on swaplock.cash v4.1.0-CashTokens
#
# // Alice has XMR, wants BCH and/or CashTokens.
# // Bob has BCH and/or CashTokens, wants XMR.
#
# // Verify 1-in-1-out TX form
CScriptOp(OP_TXINPUTCOUNT),
CScriptOp(OP_1), CScriptOp(OP_NUMEQUALVERIFY),
CScriptOp(OP_TXOUTPUTCOUNT),
CScriptOp(OP_1), CScriptOp(OP_NUMEQUALVERIFY),
# // int miningFee
mining_fee,
# // Verify pre-agreed mining fee and that the rest of BCH is forwarded
# // to the output.
CScriptOp(OP_0), CScriptOp(OP_UTXOVALUE),
CScriptOp(OP_0), CScriptOp(OP_OUTPUTVALUE),
CScriptOp(OP_SUB), CScriptOp(OP_NUMEQUALVERIFY),
# # // Verify that any CashTokens are forwarded to the output.
CScriptOp(OP_0), CScriptOp(OP_UTXOTOKENCATEGORY),
CScriptOp(OP_0), CScriptOp(OP_OUTPUTTOKENCATEGORY),
CScriptOp(OP_EQUALVERIFY),
CScriptOp(OP_0), CScriptOp(OP_UTXOTOKENCOMMITMENT),
CScriptOp(OP_0), CScriptOp(OP_OUTPUTTOKENCOMMITMENT),
CScriptOp(OP_EQUALVERIFY),
CScriptOp(OP_0), CScriptOp(OP_UTXOTOKENAMOUNT),
CScriptOp(OP_0), CScriptOp(OP_OUTPUTTOKENAMOUNT),
CScriptOp(OP_NUMEQUALVERIFY),
# // If sequence is not used then it is a regular swap TX.
CScriptOp(OP_0), CScriptOp(OP_INPUTSEQUENCENUMBER),
CScriptOp(OP_NOTIF),
# // bytes aliceOutput
out_1,
# // Verify that the BCH and/or CashTokens are forwarded to Alice's
# // output.
CScriptOp(OP_0), CScriptOp(OP_OUTPUTBYTECODE),
CScriptOp(OP_OVER), CScriptOp(OP_EQUALVERIFY),
# // pubkey bobPubkeyVES
public_key,
# // Require Alice to decrypt and publish Bob's VES signature.
# // The "message" signed is simply a sha256 hash of Alice's output
# // locking bytecode.
# // By decrypting Bob's VES and publishing it, Alice reveals her
# // XMR key share to Bob.
CScriptOp(OP_CHECKDATASIG),
# // If a TX using this path is mined then Alice gets her BCH.
# // Bob uses the revealed XMR key share to collect his XMR.
# // Refund will become available when timelock expires, and it would
# // expire because Alice didn't collect on time, either of her own accord
# // or because Bob bailed out and witheld the encrypted signature.
CScriptOp(OP_ELSE),
# // int timelock_0
timelock,
# // Verify refund timelock.
CScriptOp(OP_CHECKSEQUENCEVERIFY), CScriptOp(OP_DROP),
# // bytes refundLockingBytecode
out_2,
# // Verify that the BCH and/or CashTokens are forwarded to Refund
# // contract.
CScriptOp(OP_0), CScriptOp(OP_OUTPUTBYTECODE),
CScriptOp(OP_EQUAL),
# // BCH and/or CashTokens are simply forwarded to Refund contract.
CScriptOp(OP_ENDIF)
])
def pubkey_to_segwit_address(self, pk: bytes) -> str: def pubkey_to_segwit_address(self, pk: bytes) -> str:
raise NotImplementedError() raise NotImplementedError()
@ -41,10 +254,6 @@ class BCHInterface(BTCInterface):
address.prefix = prefix address.prefix = prefix
return address.cash_address() return address.cash_address()
def getNewAddress(self, use_segwit: bool = False, label: str = 'swap_receive') -> str:
args = [label]
return self.rpc_wallet('getnewaddress', args)
def addressToLockingBytecode(self, address: str) -> bytes: def addressToLockingBytecode(self, address: str) -> bytes:
return b'\x76\xa9\x14' + bytes(Address.from_string(address).payload) + b'\x88\xac' return b'\x76\xa9\x14' + bytes(Address.from_string(address).payload) + b'\x88\xac'
@ -83,10 +292,11 @@ class BCHInterface(BTCInterface):
if ves is not None: if ves is not None:
return CScript([ves, script]) return CScript([ves, script])
else: else:
return CScript([script]) return CScript([0, script])
def createSCLockSpendTx(self, tx_lock_bytes, script_lock, pkh_dest, tx_fee_rate, ves=None, fee_info={}): def createSCLockSpendTx(self, tx_lock_bytes, script_lock, pkh_dest, tx_fee_rate, vkbv=None, fee_info={}, **kwargs):
# tx_fee_rate in this context is equal to `mining_fee` contract param # tx_fee_rate in this context is equal to `mining_fee` contract param
ves = kwargs['ves'] if 'ves' in kwargs else None
tx_lock = self.loadTx(tx_lock_bytes) tx_lock = self.loadTx(tx_lock_bytes)
output_script = self.getScriptDest(script_lock) output_script = self.getScriptDest(script_lock)
locked_n = findOutput(tx_lock, output_script) locked_n = findOutput(tx_lock, output_script)
@ -111,9 +321,127 @@ class BCHInterface(BTCInterface):
fee_info['fee_paid'] = pay_fee fee_info['fee_paid'] = pay_fee
fee_info['rate_used'] = tx_fee_rate fee_info['rate_used'] = tx_fee_rate
fee_info['size'] = size fee_info['size'] = size
# vsize is the same as size for BCH
fee_info['vsize'] = size
tx.rehash() tx.rehash()
self._log.info('createSCLockSpendTx %s:\n fee_rate, size, fee: %ld, %ld, %ld.', self._log.info('createSCLockSpendTx %s:\n fee_rate, size, fee: %ld, %ld, %ld.',
i2h(tx.sha256), tx_fee_rate, size, pay_fee) i2h(tx.sha256), tx_fee_rate, size, pay_fee)
return tx.serialize_without_witness() return tx.serialize_without_witness()
def createSCLockRefundTx(self, tx_lock_bytes, script_lock, Kal, Kaf, lock1_value, csv_val, tx_fee_rate, vkbv=None, **kwargs):
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].nValue
tx_lock.rehash()
tx_lock_id_int = tx_lock.sha256
refund_script = kwargs['refund_lock_tx_script']
tx = CTransaction()
tx.nVersion = self.txVersion()
tx.vin.append(CTxIn(COutPoint(tx_lock_id_int, locked_n),
nSequence=kwargs['timelock'] if 'timelock' in kwargs else lock1_value,
scriptSig=self.getScriptScriptSig(script_lock, None)))
tx.vout.append(self.txoType()(locked_coin, self.getScriptDest(refund_script)))
pay_fee = kwargs['mining_fee'] if 'mining_fee' in kwargs else tx_fee_rate
tx.vout[0].nValue = locked_coin - pay_fee
size = self.getTxSize(tx)
vsize = size
tx.rehash()
self._log.info('createSCLockRefundTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.',
i2h(tx.sha256), tx_fee_rate, vsize, pay_fee)
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):
# 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
# 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:
# simply sign the entire tx data, as this is not a preimage signature
eck = PrivateKey(key_bytes)
return eck.sign(tx_bytes, hasher=None)
def verifyTxSig(self, tx_bytes: bytes, sig: bytes, K: bytes, input_n: int, prevout_script: bytes, prevout_value: int) -> bool:
# simple ecdsa signature verification
pubkey = PublicKey(K)
return pubkey.verify(sig, tx_bytes, hasher=None)
def setTxSignature(self, tx_bytes: bytes, stack) -> bytes:
return tx_bytes
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.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')
# TODO: better script matching, see interfaces/btc.py
return txid, locked_n

View file

@ -1148,7 +1148,8 @@ class BTCInterface(Secp256k1Interface):
return None return None
try: try:
tx = self.rpc_wallet('gettransaction', [txid.hex()]) # 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 block_height = 0
if 'blockhash' in tx: if 'blockhash' in tx:

View file

@ -189,7 +189,7 @@ class Address:
:rtype: ``str`` :rtype: ``str``
""" """
version_bit = Address.VERSIONS[self.version]["version_bit"] version_bit = Address.VERSIONS[self.version]["version_bit"]
payload = [version_bit] + self.payload payload = [version_bit] + list(self.payload)
payload = convertbits(payload, 8, 5) payload = convertbits(payload, 8, 5)
checksum = calculate_checksum(self.prefix, payload) checksum = calculate_checksum(self.prefix, payload)
return self.prefix + ":" + b32encode(payload + checksum) return self.prefix + ":" + b32encode(payload + checksum)

View file

@ -191,7 +191,11 @@ def setDLEAG(xmr_swap, ci_to, kbsf: bytes) -> None:
class XmrSwapInterface(ProtocolInterface): class XmrSwapInterface(ProtocolInterface):
swap_type = SwapTypes.XMR_SWAP swap_type = SwapTypes.XMR_SWAP
def genScriptLockTxScript(self, ci, Kal: bytes, Kaf: bytes) -> CScript: def genScriptLockTxScript(self, ci, Kal: bytes, Kaf: bytes, **kwargs) -> CScript:
# fallthrough to ci if genScriptLockTxScript is implemented there
if hasattr(ci, 'genScriptLockTxScript') and callable(ci.genScriptLockTxScript):
return ci.genScriptLockTxScript(ci, Kal, Kaf, **kwargs)
Kal_enc = Kal if len(Kal) == 33 else ci.encodePubkey(Kal) Kal_enc = Kal if len(Kal) == 33 else ci.encodePubkey(Kal)
Kaf_enc = Kaf if len(Kaf) == 33 else ci.encodePubkey(Kaf) Kaf_enc = Kaf if len(Kaf) == 33 else ci.encodePubkey(Kaf)
@ -221,82 +225,3 @@ class XmrSwapInterface(ProtocolInterface):
ctx.nLockTime = 0 ctx.nLockTime = 0
return ctx.serialize() return ctx.serialize()
class XmrBchSwapInterface(ProtocolInterface):
swap_type = SwapTypes.XMR_BCH_SWAP
def genScriptLockTxScript(self, mining_fee: int, out_1: bytes, out_2: bytes, public_key: bytes, timelock: int) -> CScript:
return CScript([
# // v4.1.0-CashTokens-Optimized
# // Based on swaplock.cash v4.1.0-CashTokens
#
# // Alice has XMR, wants BCH and/or CashTokens.
# // Bob has BCH and/or CashTokens, wants XMR.
#
# // Verify 1-in-1-out TX form
CScriptOp(OP_TXINPUTCOUNT),
CScriptOp(OP_1), CScriptOp(OP_NUMEQUALVERIFY),
CScriptOp(OP_TXOUTPUTCOUNT),
CScriptOp(OP_1), CScriptOp(OP_NUMEQUALVERIFY),
# // int miningFee
mining_fee,
# // Verify pre-agreed mining fee and that the rest of BCH is forwarded
# // to the output.
CScriptOp(OP_0), CScriptOp(OP_UTXOVALUE),
CScriptOp(OP_0), CScriptOp(OP_OUTPUTVALUE),
CScriptOp(OP_SUB), CScriptOp(OP_NUMEQUALVERIFY),
# # // Verify that any CashTokens are forwarded to the output.
CScriptOp(OP_0), CScriptOp(OP_UTXOTOKENCATEGORY),
CScriptOp(OP_0), CScriptOp(OP_OUTPUTTOKENCATEGORY),
CScriptOp(OP_EQUALVERIFY),
CScriptOp(OP_0), CScriptOp(OP_UTXOTOKENCOMMITMENT),
CScriptOp(OP_0), CScriptOp(OP_OUTPUTTOKENCOMMITMENT),
CScriptOp(OP_EQUALVERIFY),
CScriptOp(OP_0), CScriptOp(OP_UTXOTOKENAMOUNT),
CScriptOp(OP_0), CScriptOp(OP_OUTPUTTOKENAMOUNT),
CScriptOp(OP_NUMEQUALVERIFY),
# // If sequence is not used then it is a regular swap TX.
CScriptOp(OP_0), CScriptOp(OP_INPUTSEQUENCENUMBER),
CScriptOp(OP_NOTIF),
# // bytes aliceOutput
out_1,
# // Verify that the BCH and/or CashTokens are forwarded to Alice's
# // output.
CScriptOp(OP_0), CScriptOp(OP_OUTPUTBYTECODE),
CScriptOp(OP_OVER), CScriptOp(OP_EQUALVERIFY),
# // pubkey bobPubkeyVES
public_key,
# // Require Alice to decrypt and publish Bob's VES signature.
# // The "message" signed is simply a sha256 hash of Alice's output
# // locking bytecode.
# // By decrypting Bob's VES and publishing it, Alice reveals her
# // XMR key share to Bob.
CScriptOp(OP_CHECKDATASIG),
# // If a TX using this path is mined then Alice gets her BCH.
# // Bob uses the revealed XMR key share to collect his XMR.
# // Refund will become available when timelock expires, and it would
# // expire because Alice didn't collect on time, either of her own accord
# // or because Bob bailed out and witheld the encrypted signature.
CScriptOp(OP_ELSE),
# // int timelock_0
timelock,
# // Verify refund timelock.
CScriptOp(OP_CHECKSEQUENCEVERIFY), CScriptOp(OP_DROP),
# // bytes refundLockingBytecode
out_2,
# // Verify that the BCH and/or CashTokens are forwarded to Refund
# // contract.
CScriptOp(OP_0), CScriptOp(OP_OUTPUTBYTECODE),
CScriptOp(OP_EQUAL),
# // BCH and/or CashTokens are simply forwarded to Refund contract.
CScriptOp(OP_ENDIF)
])

View file

@ -9,6 +9,7 @@ import random
import logging import logging
import unittest import unittest
from basicswap.chainparams import XMR_COIN
from basicswap.db import ( from basicswap.db import (
Concepts, Concepts,
) )
@ -23,6 +24,7 @@ from basicswap.basicswap_util import (
EventLogTypes, EventLogTypes,
) )
from basicswap.util import ( from basicswap.util import (
COIN,
make_int, make_int,
format_amount, format_amount,
) )
@ -107,9 +109,7 @@ class TestFunctions(BaseTest):
mining_fee = 1000 mining_fee = 1000
timelock = 2 timelock = 2
a_receive = ci.getNewAddress() a_receive = ci.getNewAddress()
b_receive = ci.getNewAddress()
b_refund = ci.getNewAddress() b_refund = ci.getNewAddress()
print(pi)
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) 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() addr_out = ci.getNewAddress()
@ -124,11 +124,10 @@ class TestFunctions(BaseTest):
assert (len(unspents) > len(unspents_after)) assert (len(unspents) > len(unspents_after))
tx_decoded = ci.rpc('decoderawtransaction', [lock_tx.hex()]) tx_decoded = ci.rpc('decoderawtransaction', [lock_tx.hex()])
print(tx_decoded)
txid = tx_decoded['txid'] txid = tx_decoded['txid']
size = tx_decoded['size'] vsize = tx_decoded['size']
expect_fee_int = round(fee_rate * size) expect_fee_int = round(fee_rate * vsize)
expect_fee = ci.format_amount(expect_fee_int) expect_fee = ci.format_amount(expect_fee_int)
out_value: int = 0 out_value: int = 0
@ -147,7 +146,6 @@ class TestFunctions(BaseTest):
ci.rpc('sendrawtransaction', [lock_tx.hex()]) ci.rpc('sendrawtransaction', [lock_tx.hex()])
rv = ci.rpc('gettransaction', [txid]) rv = ci.rpc('gettransaction', [txid])
print(rv)
wallet_tx_fee = -ci.make_int(rv['fee']) wallet_tx_fee = -ci.make_int(rv['fee'])
assert (wallet_tx_fee == fee_value) assert (wallet_tx_fee == fee_value)
@ -165,28 +163,24 @@ class TestFunctions(BaseTest):
# alice decrypts the adaptor signature # alice decrypts the adaptor signature
bAdaptorSig_dec = ecdsaotves_dec_sig(a, bAdaptorSig) bAdaptorSig_dec = ecdsaotves_dec_sig(a, bAdaptorSig)
print("\nbAdaptorSig_dec", bAdaptorSig_dec.hex())
print(ci.addressToLockingBytecode(a_receive).hex(), msg.hex(), bAdaptorSig_dec.hex(), B.hex())
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, ves=bAdaptorSig_dec, fee_info=fee_info)
print(lock_spend_tx.hex()) vsize_estimated: int = fee_info['vsize']
size_estimated: int = fee_info['size']
tx_decoded = ci.rpc('decoderawtransaction', [lock_spend_tx.hex()]) tx_decoded = ci.rpc('decoderawtransaction', [lock_spend_tx.hex()])
print(tx_decoded) print(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()])
size_actual: int = tx_decoded['size'] vsize_actual: int = tx_decoded['size']
assert (size_actual <= size_estimated and size_estimated - size_actual < 4) assert (vsize_actual <= vsize_estimated and vsize_estimated - vsize_actual < 4)
assert (ci.rpc('sendrawtransaction', [lock_spend_tx.hex()]) == txid) assert (ci.rpc('sendrawtransaction', [lock_spend_tx.hex()]) == txid)
expect_size: int = ci.xmr_swap_a_lock_spend_tx_vsize() expect_size: int = ci.xmr_swap_a_lock_spend_tx_vsize()
assert (expect_size >= size_actual) assert (expect_size >= vsize_actual)
assert (expect_size - size_actual < 10) assert (expect_size - vsize_actual < 10)
# Test chain b (no-script) lock tx size # Test chain b (no-script) lock tx size
v = ci.getNewSecretKey() v = ci.getNewSecretKey()
@ -204,3 +198,27 @@ class TestFunctions(BaseTest):
expect_size: int = ci.xmr_swap_b_lock_spend_tx_vsize() expect_size: int = ci.xmr_swap_b_lock_spend_tx_vsize()
assert (expect_size >= lock_tx_b_spend_decoded['size']) assert (expect_size >= lock_tx_b_spend_decoded['size'])
assert (expect_size - lock_tx_b_spend_decoded['size'] < 10) assert (expect_size - lock_tx_b_spend_decoded['size'] < 10)
def test_05_bch_xmr(self):
logging.info('---------- Test BCH to XMR')
swap_clients = self.swap_clients
offer_id = swap_clients[0].postOffer(Coins.BCH, Coins.XMR, 10 * COIN, 100 * XMR_COIN, 10 * COIN, SwapTypes.XMR_SWAP)
wait_for_offer(test_delay_event, swap_clients[1], offer_id)
offers = swap_clients[1].listOffers(filters={'offer_id': offer_id})
offer = offers[0]
swap_clients[1].ci(Coins.XMR).setFeePriority(3)
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[0].acceptXmrBid(bid_id)
wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=180)
wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True)
swap_clients[1].ci(Coins.XMR).setFeePriority(0)

View file

@ -98,11 +98,11 @@ from basicswap.bin.run import startDaemon, startXmrDaemon, startXmrWalletDaemon
logger = logging.getLogger() logger = logging.getLogger()
NUM_NODES = 3 NUM_NODES = 2
NUM_XMR_NODES = 3 NUM_XMR_NODES = 2
NUM_BTC_NODES = 3 NUM_BTC_NODES = 2
NUM_BCH_NODES = 3 NUM_BCH_NODES = 2
NUM_LTC_NODES = 3 NUM_LTC_NODES = 2
TEST_DIR = cfg.TEST_DATADIRS TEST_DIR = cfg.TEST_DATADIRS
XMR_BASE_P2P_PORT = 17792 XMR_BASE_P2P_PORT = 17792