particl: Can swap PARTct -> XMR

This commit is contained in:
tecnovert 2021-11-01 15:52:40 +02:00
parent 1ef71ea79b
commit 6e82961da9
No known key found for this signature in database
GPG key ID: 8ED6D8750C4E3F93
19 changed files with 1807 additions and 296 deletions

View file

@ -1,3 +1,3 @@
name = "basicswap"
__version__ = "0.0.24"
__version__ = "0.0.25"

View file

@ -16,10 +16,8 @@ from .rpc import (
callrpc,
)
from .util import (
pubkeyToAddress,
)
from .basicswap_util import (
TemporaryError,
pubkeyToAddress,
)
from .chainparams import (
Coins,

View file

@ -26,7 +26,7 @@ import concurrent.futures
from sqlalchemy.orm import sessionmaker, scoped_session
from sqlalchemy.orm.session import close_all_sessions
from .interface_part import PARTInterface, PARTInterfaceAnon
from .interface_part import PARTInterface, PARTInterfaceAnon, PARTInterfaceBlind
from .interface_btc import BTCInterface
from .interface_ltc import LTCInterface
from .interface_nmc import NMCInterface
@ -371,6 +371,7 @@ class BasicSwap(BaseApp):
if coin == Coins.PART:
self.coin_clients[Coins.PART_ANON] = self.coin_clients[coin]
self.coin_clients[Coins.PART_BLIND] = self.coin_clients[coin]
if self.coin_clients[coin]['connection_type'] == 'rpc':
if coin == Coins.XMR:
@ -384,6 +385,8 @@ class BasicSwap(BaseApp):
def ci(self, coin): # Coin interface
if coin == Coins.PART_ANON:
return self.coin_clients[Coins.PART]['interface_anon']
if coin == Coins.PART_BLIND:
return self.coin_clients[Coins.PART]['interface_blind']
return self.coin_clients[coin]['interface']
def createInterface(self, coin):
@ -447,6 +450,7 @@ class BasicSwap(BaseApp):
self.coin_clients[coin]['interface'] = self.createInterface(coin)
if coin == Coins.PART:
self.coin_clients[coin]['interface_anon'] = PARTInterfaceAnon(self.coin_clients[coin], self.chain, self)
self.coin_clients[coin]['interface_blind'] = PARTInterfaceBlind(self.coin_clients[coin], self.chain, self)
elif self.coin_clients[coin]['connection_type'] == 'passthrough':
self.coin_clients[coin]['interface'] = self.createPassthroughInterface(coin)
@ -1843,7 +1847,11 @@ class BasicSwap(BaseApp):
msg_buf.amount = int(amount) # Amount of coin_from
address_out = self.getReceiveAddressFromPool(coin_from, offer_id, TxTypes.XMR_SWAP_A_LOCK)
msg_buf.dest_af = ci_from.decodeAddress(address_out)
if coin_from == Coins.PART_BLIND:
addrinfo = ci_from.rpc_callback('getaddressinfo', [address_out])
msg_buf.dest_af = bytes.fromhex(addrinfo['pubkey'])
else:
msg_buf.dest_af = ci_from.decodeAddress(address_out)
bid_created_at = int(time.time())
if offer.swap_type != SwapTypes.XMR_SWAP:
@ -1994,6 +2002,7 @@ class BasicSwap(BaseApp):
xmr_swap.pkbsl = ci_to.getPubkey(kbsl)
xmr_swap.vkbv = ci_to.sumKeys(kbvl, xmr_swap.vkbvf)
ensure(ci_to.verifyKey(xmr_swap.vkbv), 'Invalid key, vkbv')
xmr_swap.pkbv = ci_to.sumPubkeys(xmr_swap.pkbvl, xmr_swap.pkbvf)
xmr_swap.pkbs = ci_to.sumPubkeys(xmr_swap.pkbsl, xmr_swap.pkbsf)
@ -2007,32 +2016,68 @@ class BasicSwap(BaseApp):
# MSG2F
xmr_swap.a_lock_tx, xmr_swap.a_lock_tx_script = ci_from.createScriptLockTx(
bid.amount,
xmr_swap.pkal, xmr_swap.pkaf,
xmr_swap.pkal, xmr_swap.pkaf, xmr_swap.vkbv
)
xmr_swap.a_lock_tx = ci_from.fundTx(xmr_swap.a_lock_tx, xmr_offer.a_fee_rate)
xmr_swap.a_lock_tx = ci_from.fundScriptLockTx(xmr_swap.a_lock_tx, xmr_offer.a_fee_rate, xmr_swap.vkbv)
xmr_swap.a_lock_tx_id = ci_from.getTxHash(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)
xmr_swap.a_lock_refund_tx, xmr_swap.a_lock_refund_tx_script, xmr_swap.a_swap_refund_value = ci_from.createScriptLockRefundTx(
xmr_swap.a_lock_tx, xmr_swap.a_lock_tx_script,
xmr_swap.pkal, xmr_swap.pkaf,
xmr_offer.lock_time_1, xmr_offer.lock_time_2,
xmr_offer.a_fee_rate
xmr_offer.a_fee_rate, xmr_swap.vkbv
)
xmr_swap.a_lock_refund_tx_id = ci_from.getTxHash(xmr_swap.a_lock_refund_tx)
xmr_swap.a_lock_refund_tx_id = ci_from.getTxid(xmr_swap.a_lock_refund_tx)
xmr_swap.al_lock_refund_tx_sig = ci_from.signTx(kal, xmr_swap.a_lock_refund_tx, 0, xmr_swap.a_lock_tx_script, bid.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, bid.amount)
ensure(v, 'Invalid coin A lock tx refund tx leader sig')
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)
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')
pkh_refund_to = ci_from.decodeAddress(self.getReceiveAddressForCoin(coin_from))
xmr_swap.a_lock_refund_spend_tx = ci_from.createScriptLockRefundSpendTx(
xmr_swap.a_lock_refund_tx, xmr_swap.a_lock_refund_tx_script,
pkh_refund_to,
xmr_offer.a_fee_rate
xmr_offer.a_fee_rate, xmr_swap.vkbv
)
xmr_swap.a_lock_refund_spend_tx_id = ci_from.getTxHash(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)
# Double check txns before sending
self.log.debug('Bid: {} - Double checking chain A lock txns are valid before sending bid accept.'.format(bid_id.hex()))
check_lock_tx_inputs = False # TODO: check_lock_tx_inputs without txindex
_, xmr_swap.a_lock_tx_vout = ci_from.verifyLockTx(
xmr_swap.a_lock_tx,
xmr_swap.a_lock_tx_script,
bid.amount,
xmr_swap.pkal,
xmr_swap.pkaf,
xmr_offer.a_fee_rate,
check_lock_tx_inputs,
xmr_swap.vkbv)
_, _, lock_refund_vout = ci_from.verifyLockRefundTx(
xmr_swap.a_lock_refund_tx,
xmr_swap.a_lock_tx,
xmr_swap.a_lock_refund_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_offer.lock_time_2,
bid.amount,
xmr_offer.a_fee_rate,
xmr_swap.vkbv)
ci_from.verifyLockRefundSpendTx(
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.pkal,
lock_refund_vout, xmr_swap.a_swap_refund_value, xmr_offer.a_fee_rate,
xmr_swap.vkbv)
msg_buf = XmrBidAcceptMessage()
msg_buf.bid_msg_id = bid_id
@ -2113,12 +2158,12 @@ class BasicSwap(BaseApp):
session.remove()
self.mxDB.release()
def setBidError(self, bid_id, bid, error_str, save_bid=True):
def setBidError(self, bid_id, bid, error_str, save_bid=True, xmr_swap=None):
self.log.error('Bid %s - Error: %s', bid_id.hex(), error_str)
bid.setState(BidStates.BID_ERROR)
bid.state_note = 'error msg: ' + error_str
if save_bid:
self.saveBid(bid_id, bid)
self.saveBid(bid_id, bid, xmr_swap=xmr_swap)
def createInitiateTxn(self, coin_type, bid_id, bid, initiate_script):
if self.coin_clients[coin_type]['connection_type'] != 'rpc':
@ -2340,7 +2385,8 @@ class BasicSwap(BaseApp):
ro = self.callcoinrpc(Coins.PART, 'verifyrawtransaction', [redeem_txn, [prevout]])
ensure(ro['inputs_valid'] is True, 'inputs_valid is false')
ensure(ro['complete'] is True, 'complete is false')
# outputs_valid will be false if not a Particl txn
# ensure(ro['complete'] is True, 'complete is false')
ensure(ro['validscripts'] == 1, 'validscripts != 1')
if self.debug:
@ -2439,7 +2485,8 @@ class BasicSwap(BaseApp):
ro = self.callcoinrpc(Coins.PART, 'verifyrawtransaction', [refund_txn, [prevout]])
ensure(ro['inputs_valid'] is True, 'inputs_valid is false')
ensure(ro['complete'] is True, 'complete is false')
# outputs_valid will be false if not a Particl txn
# ensure(ro['complete'] is True, 'complete is false')
ensure(ro['validscripts'] == 1, 'validscripts != 1')
if self.debug:
@ -2496,17 +2543,17 @@ class BasicSwap(BaseApp):
# Bid saved in checkBidState
def setLastHeightChecked(self, coin_type, tx_height):
chain_name = chainparams[coin_type]['name']
coin_name = self.ci(coin_type).coin_name()
if tx_height < 1:
tx_height = self.lookupChainHeight(coin_type)
if len(self.coin_clients[coin_type]['watched_outputs']) == 0:
self.coin_clients[coin_type]['last_height_checked'] = tx_height
self.log.debug('Start checking %s chain at height %d', chain_name, tx_height)
self.log.debug('Start checking %s chain at height %d', coin_name, tx_height)
if self.coin_clients[coin_type]['last_height_checked'] > tx_height:
self.coin_clients[coin_type]['last_height_checked'] = tx_height
self.log.debug('Rewind checking of %s chain to height %d', chain_name, tx_height)
self.log.debug('Rewind checking of %s chain to height %d', coin_name, tx_height)
return tx_height
@ -2652,7 +2699,7 @@ class BasicSwap(BaseApp):
txid = ci_from.publishTx(xmr_swap.a_lock_refund_spend_tx)
self.logBidEvent(bid.bid_id, EventLogTypes.LOCK_TX_A_REFUND_SPEND_TX_PUBLISHED, '', session)
self.log.info('Submitted coin a lock refund spend tx for bid {}'.format(bid_id.hex()))
self.log.info('Submitted coin a lock refund spend tx for bid {}, txid {}'.format(bid_id.hex(), txid.hex()))
bid.txns[TxTypes.XMR_SWAP_A_LOCK_REFUND_SPEND] = SwapTx(
bid_id=bid_id,
tx_type=TxTypes.XMR_SWAP_A_LOCK_REFUND_SPEND,
@ -2713,7 +2760,7 @@ class BasicSwap(BaseApp):
except Exception as ex:
if 'Transaction already in block chain' in str(ex):
self.log.info('Found coin a lock refund tx for bid {}'.format(bid_id.hex()))
txid = ci_from.getTxHash(xmr_swap.a_lock_refund_tx)
txid = ci_from.getTxid(xmr_swap.a_lock_refund_tx)
bid.txns[TxTypes.XMR_SWAP_A_LOCK_REFUND] = SwapTx(
bid_id=bid_id,
tx_type=TxTypes.XMR_SWAP_A_LOCK_REFUND,
@ -2737,30 +2784,28 @@ class BasicSwap(BaseApp):
# TODO: Timeout waiting for transactions
bid_changed = False
a_lock_tx_dest = ci_from.getScriptDest(xmr_swap.a_lock_tx_script)
utxos, chain_height = ci_from.getOutput(bid.xmr_a_lock_tx.txid, a_lock_tx_dest, bid.amount)
# Changed from ci_from.getOutput(bid.xmr_a_lock_tx.txid, a_lock_tx_dest, bid.amount, xmr_swap)
if len(utxos) < 1:
lock_tx_chain_info = ci_from.getLockTxHeight(bid.xmr_a_lock_tx.txid, a_lock_tx_dest, bid.amount, xmr_swap)
if lock_tx_chain_info is None:
return rv
if len(utxos) > 1:
raise ValueError('Too many outputs for chain A lock tx')
utxo = utxos[0]
if not bid.xmr_a_lock_tx.chain_height and utxo['height'] != 0:
if not bid.xmr_a_lock_tx.chain_height and lock_tx_chain_info['height'] != 0:
self.logBidEvent(bid.bid_id, EventLogTypes.LOCK_TX_A_SEEN, '', session)
block_header = ci_from.getBlockHeaderFromHeight(utxo['height'])
block_header = ci_from.getBlockHeaderFromHeight(lock_tx_chain_info['height'])
bid.xmr_a_lock_tx.block_hash = bytes.fromhex(block_header['hash'])
bid.xmr_a_lock_tx.block_height = block_header['height']
bid.xmr_a_lock_tx.block_time = block_header['time'] # Or median_time?
bid_changed = True
if bid.xmr_a_lock_tx.chain_height != utxo['height'] and utxo['height'] != 0:
bid.xmr_a_lock_tx.chain_height = utxo['height']
if bid.xmr_a_lock_tx.chain_height != lock_tx_chain_info['height'] and lock_tx_chain_info['height'] != 0:
bid.xmr_a_lock_tx.chain_height = lock_tx_chain_info['height']
bid_changed = True
if utxo['depth'] >= ci_from.blocks_confirmed:
if lock_tx_chain_info['depth'] >= ci_from.blocks_confirmed:
self.logBidEvent(bid.bid_id, EventLogTypes.LOCK_TX_A_CONFIRMED, '', session)
bid.xmr_a_lock_tx.setState(TxStates.TX_CONFIRMED)
bid.setState(BidStates.XMR_SWAP_SCRIPT_COIN_LOCKED)
@ -3233,7 +3278,7 @@ class BasicSwap(BaseApp):
# assert(self.mxDB.locked())
self.log.debug('checkForSpends %s', coin_type)
if coin_type == Coins.PART and self.coin_clients[coin_type]['have_spent_index']:
if 'have_spent_index' in self.coin_clients[coin_type] and self.coin_clients[coin_type]['have_spent_index']:
# TODO: batch getspentinfo
for o in c['watched_outputs']:
found_spend = None
@ -3414,7 +3459,6 @@ class BasicSwap(BaseApp):
ci_from = self.ci(coin_from)
coin_to = Coins(offer_data.coin_to)
ci_to = self.ci(coin_to)
chain_from = chainparams[coin_from][self.chain]
ensure(offer_data.coin_from != offer_data.coin_to, 'coin_from == coin_to')
self.validateSwapType(coin_from, coin_to, offer_data.swap_type)
@ -3742,11 +3786,8 @@ class BasicSwap(BaseApp):
raise ValueError('Invalid coin a pubkey.')
xmr_swap.pkbsf = xmr_swap.pkasf
if not ci_to.verifyKey(xmr_swap.vkbvf):
raise ValueError('Invalid key.')
if not ci_from.verifyPubkey(xmr_swap.pkaf):
raise ValueError('Invalid pubkey.')
ensure(ci_to.verifyKey(xmr_swap.vkbvf), 'Invalid key, vkbvf')
ensure(ci_from.verifyPubkey(xmr_swap.pkaf), 'Invalid pubkey, pkaf')
self.log.info('Received valid bid %s for xmr offer %s', bid.bid_id.hex(), bid.offer_id.hex())
@ -3800,10 +3841,7 @@ class BasicSwap(BaseApp):
raise ValueError('Invalid coin a pubkey.')
xmr_swap.pkbsl = xmr_swap.pkasl
if not ci_to.verifyKey(xmr_swap.vkbvl):
raise ValueError('Invalid key.')
xmr_swap.vkbv = ci_to.sumKeys(xmr_swap.vkbvl, xmr_swap.vkbvf)
# vkbv and vkbvl are verified in processXmrBidAccept
xmr_swap.pkbv = ci_to.sumPubkeys(xmr_swap.pkbvl, xmr_swap.pkbvf)
xmr_swap.pkbs = ci_to.sumPubkeys(xmr_swap.pkbsl, xmr_swap.pkbsf)
@ -3930,6 +3968,10 @@ class BasicSwap(BaseApp):
try:
xmr_swap.pkal = msg_data.pkal
xmr_swap.vkbvl = msg_data.kbvl
ensure(ci_to.verifyKey(xmr_swap.vkbvl), 'Invalid key, vkbvl')
xmr_swap.vkbv = ci_to.sumKeys(xmr_swap.vkbvl, xmr_swap.vkbvf)
ensure(ci_to.verifyKey(xmr_swap.vkbv), 'Invalid key, vkbv')
xmr_swap.pkbvl = ci_to.getPubkey(msg_data.kbvl)
xmr_swap.kbsl_dleag = msg_data.kbsl_dleag
@ -3938,37 +3980,36 @@ class BasicSwap(BaseApp):
xmr_swap.a_lock_refund_tx = msg_data.a_lock_refund_tx
xmr_swap.a_lock_refund_tx_script = msg_data.a_lock_refund_tx_script
xmr_swap.a_lock_refund_spend_tx = msg_data.a_lock_refund_spend_tx
xmr_swap.a_lock_refund_spend_tx_id = ci_from.getTxHash(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
# TODO: check_a_lock_tx_inputs without txindex
# TODO: check_lock_tx_inputs without txindex
check_a_lock_tx_inputs = False
xmr_swap.a_lock_tx_id, xmr_swap.a_lock_tx_vout = ci_from.verifyLockTx(
xmr_swap.a_lock_tx, xmr_swap.a_lock_tx_script,
bid.amount,
xmr_swap.pkal, xmr_swap.pkaf,
xmr_offer.a_fee_rate,
check_a_lock_tx_inputs
)
check_a_lock_tx_inputs, xmr_swap.vkbv)
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 = ci_from.verifyLockRefundTx(
xmr_swap.a_lock_refund_tx, xmr_swap.a_lock_refund_tx_script,
xmr_swap.a_lock_refund_tx_id, xmr_swap.a_swap_refund_value, lock_refund_vout = ci_from.verifyLockRefundTx(
xmr_swap.a_lock_refund_tx, xmr_swap.a_lock_tx, xmr_swap.a_lock_refund_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_offer.lock_time_2,
bid.amount, xmr_offer.a_fee_rate
)
bid.amount, xmr_offer.a_fee_rate, xmr_swap.vkbv)
ci_from.verifyLockRefundSpendTx(
xmr_swap.a_lock_refund_spend_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.pkal,
xmr_swap.a_swap_refund_value, xmr_offer.a_fee_rate
)
lock_refund_vout, xmr_swap.a_swap_refund_value, xmr_offer.a_fee_rate, xmr_swap.vkbv)
self.log.info('Checking leader\'s lock refund tx signature')
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, bid.amount)
prevout_amount = ci_from.getLockTxSwapOutputValue(bid, xmr_swap)
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')
bid.setState(BidStates.BID_RECEIVING_ACC)
self.saveBid(bid.bid_id, bid, xmr_swap=xmr_swap)
@ -3985,7 +4026,7 @@ class BasicSwap(BaseApp):
except Exception as ex:
if self.debug:
traceback.print_exc()
self.setBidError(bid.bid_id, bid, str(ex))
self.setBidError(bid.bid_id, bid, str(ex), xmr_swap=xmr_swap)
def watchXmrSwap(self, bid, offer, xmr_swap):
self.log.debug('XMR swap in progress, bid %s', bid.bid_id.hex())
@ -3994,7 +4035,9 @@ class BasicSwap(BaseApp):
coin_from = Coins(offer.coin_from)
self.setLastHeightChecked(coin_from, xmr_swap.start_chain_a_height)
self.addWatchedOutput(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)
self.addWatchedOutput(coin_from, bid.bid_id, xmr_swap.a_lock_refund_tx_id.hex(), 0, TxTypes.XMR_SWAP_A_LOCK_REFUND, SwapTypes.XMR_SWAP)
lock_refund_vout = self.ci(coin_from).getLockRefundTxSwapOutput(xmr_swap)
self.addWatchedOutput(coin_from, bid.bid_id, xmr_swap.a_lock_refund_tx_id.hex(), lock_refund_vout, TxTypes.XMR_SWAP_A_LOCK_REFUND, SwapTypes.XMR_SWAP)
bid.in_progress = 1
def sendXmrBidTxnSigsFtoL(self, bid_id, session):
@ -4016,8 +4059,10 @@ class BasicSwap(BaseApp):
try:
kaf = self.getPathKey(coin_from, coin_to, bid.created_at, xmr_swap.contract_count, 3)
xmr_swap.af_lock_refund_spend_tx_esig = ci_from.signTxOtVES(kaf, xmr_swap.pkasl, xmr_swap.a_lock_refund_spend_tx, 0, xmr_swap.a_lock_refund_tx_script, xmr_swap.a_swap_refund_value)
xmr_swap.af_lock_refund_tx_sig = ci_from.signTx(kaf, xmr_swap.a_lock_refund_tx, 0, xmr_swap.a_lock_tx_script, bid.amount)
prevout_amount = ci_from.getLockRefundTxSwapOutputValue(bid, xmr_swap)
xmr_swap.af_lock_refund_spend_tx_esig = ci_from.signTxOtVES(kaf, xmr_swap.pkasl, xmr_swap.a_lock_refund_spend_tx, 0, xmr_swap.a_lock_refund_tx_script, prevout_amount)
prevout_amount = ci_from.getLockTxSwapOutputValue(bid, xmr_swap)
xmr_swap.af_lock_refund_tx_sig = ci_from.signTx(kaf, xmr_swap.a_lock_refund_tx, 0, xmr_swap.a_lock_tx_script, prevout_amount)
self.addLockRefundSigs(xmr_swap, ci_from)
@ -4038,7 +4083,7 @@ class BasicSwap(BaseApp):
self.log.info('Sent XMR_BID_TXN_SIGS_FL %s', xmr_swap.coin_a_lock_tx_sigs_l_msg_id.hex())
a_lock_tx_id = ci_from.getTxHash(xmr_swap.a_lock_tx)
a_lock_tx_id = ci_from.getTxid(xmr_swap.a_lock_tx)
a_lock_tx_vout = ci_from.getTxOutputPos(xmr_swap.a_lock_tx, xmr_swap.a_lock_tx_script)
self.log.debug('Waiting for lock txn %s to %s chain for bid %s', a_lock_tx_id.hex(), ci_from.coin_name(), bid_id.hex())
bid.xmr_a_lock_tx = SwapTx(
@ -4077,12 +4122,13 @@ class BasicSwap(BaseApp):
xmr_swap.a_lock_spend_tx = ci_from.createScriptLockSpendTx(
xmr_swap.a_lock_tx, xmr_swap.a_lock_tx_script,
xmr_swap.dest_af,
xmr_offer.a_fee_rate)
xmr_offer.a_fee_rate, xmr_swap.vkbv)
xmr_swap.a_lock_spend_tx_id = ci_from.getTxHash(xmr_swap.a_lock_spend_tx)
xmr_swap.al_lock_spend_tx_esig = ci_from.signTxOtVES(kal, xmr_swap.pkasf, xmr_swap.a_lock_spend_tx, 0, xmr_swap.a_lock_tx_script, bid.amount) # self.a_swap_value
xmr_swap.a_lock_spend_tx_id = ci_from.getTxid(xmr_swap.a_lock_spend_tx)
prevout_amount = ci_from.getLockTxSwapOutputValue(bid, xmr_swap)
xmr_swap.al_lock_spend_tx_esig = ci_from.signTxOtVES(kal, xmr_swap.pkasf, xmr_swap.a_lock_spend_tx, 0, xmr_swap.a_lock_tx_script, prevout_amount)
# Proof leader can sign for kal
# Prove leader can sign for kal
xmr_swap.kal_sig = ci_from.signCompact(kal, 'proof key owned for swap')
msg_buf = XmrBidLockSpendTxMessage(
@ -4159,8 +4205,7 @@ class BasicSwap(BaseApp):
error_msg += ', retry no. {}'.format(num_retries)
self.log.error(error_msg)
str_error = str(ex)
if num_retries < 5 and ('not enough unlocked money' in str_error or 'transaction was rejected by daemon' in str_error or self.is_transient_error(ex)):
if num_retries < 5 and (ci_to.is_transient_error(ex) or self.is_transient_error(ex)):
delay = random.randrange(self.min_delay_retry, self.max_delay_retry)
self.log.info('Retrying sending xmr swap chain B lock tx for bid %s in %d seconds', bid_id.hex(), delay)
self.createEventInSession(delay, EventTypes.SEND_XMR_SWAP_LOCK_TX_B, bid_id, session)
@ -4168,7 +4213,7 @@ class BasicSwap(BaseApp):
self.setBidError(bid_id, bid, 'publishBLockTx failed: ' + str(ex), save_bid=False)
self.saveBidInSession(bid_id, bid, session, xmr_swap, save_in_progress=offer)
self.logBidEvent(bid.bid_id, EventLogTypes.FAILED_TX_B_LOCK_PUBLISH, str_error, session)
self.logBidEvent(bid.bid_id, EventLogTypes.FAILED_TX_B_LOCK_PUBLISH, str(ex), session)
return
self.log.debug('Submitted lock txn %s to %s chain for bid %s', b_lock_tx_id.hex(), ci_to.coin_name(), bid_id.hex())
@ -4233,11 +4278,12 @@ class BasicSwap(BaseApp):
kaf = self.getPathKey(coin_from, coin_to, bid.created_at, xmr_swap.contract_count, 3)
al_lock_spend_sig = ci_from.decryptOtVES(kbsf, xmr_swap.al_lock_spend_tx_esig)
v = ci_from.verifyTxSig(xmr_swap.a_lock_spend_tx, al_lock_spend_sig, xmr_swap.pkal, 0, xmr_swap.a_lock_tx_script, bid.amount)
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)
ensure(v, 'Invalid coin A lock tx spend tx leader sig')
af_lock_spend_sig = ci_from.signTx(kaf, xmr_swap.a_lock_spend_tx, 0, xmr_swap.a_lock_tx_script, bid.amount)
v = ci_from.verifyTxSig(xmr_swap.a_lock_spend_tx, af_lock_spend_sig, xmr_swap.pkaf, 0, xmr_swap.a_lock_tx_script, bid.amount)
af_lock_spend_sig = ci_from.signTx(kaf, xmr_swap.a_lock_spend_tx, 0, xmr_swap.a_lock_tx_script, prevout_amount)
v = ci_from.verifyTxSig(xmr_swap.a_lock_spend_tx, af_lock_spend_sig, xmr_swap.pkaf, 0, xmr_swap.a_lock_tx_script, prevout_amount)
ensure(v, 'Invalid coin A lock tx spend tx follower sig')
witness_stack = [
@ -4303,8 +4349,7 @@ class BasicSwap(BaseApp):
error_msg += ', retry no. {}'.format(num_retries)
self.log.error(error_msg)
str_error = str(ex)
if num_retries < 100 and ('Invalid unlocked_balance' in str_error or self.is_transient_error(ex)):
if num_retries < 100 and (ci_to.is_transient_error(ex) or self.is_transient_error(ex)):
delay = random.randrange(self.min_delay_retry, self.max_delay_retry)
self.log.info('Retrying sending xmr swap chain B spend tx for bid %s in %d seconds', bid_id.hex(), delay)
self.createEventInSession(delay, EventTypes.REDEEM_XMR_SWAP_LOCK_TX_B, bid_id, session)
@ -4312,7 +4357,7 @@ class BasicSwap(BaseApp):
self.setBidError(bid_id, bid, 'spendBLockTx failed: ' + str(ex), save_bid=False)
self.saveBidInSession(bid_id, bid, session, xmr_swap, save_in_progress=offer)
self.logBidEvent(bid.bid_id, EventLogTypes.FAILED_TX_B_SPEND, str_error, session)
self.logBidEvent(bid.bid_id, EventLogTypes.FAILED_TX_B_SPEND, str(ex), session)
return
bid.xmr_b_lock_tx.spend_txid = txid
@ -4360,7 +4405,7 @@ class BasicSwap(BaseApp):
self.log.error(error_msg)
str_error = str(ex)
if num_retries < 100 and ('Invalid unlocked_balance' in str_error or self.is_transient_error(ex)):
if num_retries < 100 and (ci_to.is_transient_error(ex) or self.is_transient_error(ex)):
delay = random.randrange(self.min_delay_retry, self.max_delay_retry)
self.log.info('Retrying sending xmr swap chain B refund tx for bid %s in %d seconds', bid_id.hex(), delay)
self.createEventInSession(delay, EventTypes.RECOVER_XMR_SWAP_LOCK_TX_B, bid_id, session)
@ -4408,7 +4453,8 @@ class BasicSwap(BaseApp):
kal = self.getPathKey(coin_from, coin_to, bid.created_at, xmr_swap.contract_count, 3)
xmr_swap.af_lock_refund_spend_tx_sig = ci_from.decryptOtVES(kbsl, xmr_swap.af_lock_refund_spend_tx_esig)
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, xmr_swap.a_swap_refund_value)
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)
self.log.debug('Setting lock refund spend tx sigs')
witness_stack = [
@ -4422,7 +4468,7 @@ class BasicSwap(BaseApp):
ensure(signed_tx, 'setTxSignature failed')
xmr_swap.a_lock_refund_spend_tx = signed_tx
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, xmr_swap.a_swap_refund_value)
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')
self.addLockRefundSigs(xmr_swap, ci_from)
@ -4460,13 +4506,13 @@ class BasicSwap(BaseApp):
try:
xmr_swap.a_lock_spend_tx = msg_data.a_lock_spend_tx
xmr_swap.a_lock_spend_tx_id = ci_from.getTxHash(xmr_swap.a_lock_spend_tx)
xmr_swap.a_lock_spend_tx_id = ci_from.getTxid(xmr_swap.a_lock_spend_tx)
xmr_swap.kal_sig = msg_data.kal_sig
ci_from.verifyLockSpendTx(
xmr_swap.a_lock_spend_tx,
xmr_swap.a_lock_tx, xmr_swap.a_lock_tx_script,
xmr_swap.dest_af, xmr_offer.a_fee_rate)
xmr_swap.dest_af, xmr_offer.a_fee_rate, xmr_swap.vkbv)
ci_from.verifyCompact(xmr_swap.pkal, 'proof key owned for swap', xmr_swap.kal_sig)
@ -4532,12 +4578,12 @@ class BasicSwap(BaseApp):
ci_from = self.ci(Coins(offer.coin_from))
xmr_swap.al_lock_spend_tx_esig = msg_data.al_lock_spend_tx_esig
try:
prevout_amount = ci_from.getLockTxSwapOutputValue(bid, xmr_swap)
v = ci_from.verifyTxOtVES(
xmr_swap.a_lock_spend_tx, xmr_swap.al_lock_spend_tx_esig,
xmr_swap.pkal, xmr_swap.pkasf, 0, xmr_swap.a_lock_tx_script, bid.amount)
ensure(v, 'verifyTxOtVES failed')
xmr_swap.pkal, xmr_swap.pkasf, 0, xmr_swap.a_lock_tx_script, prevout_amount)
ensure(v, 'verifyTxOtVES failed for chain a lock tx leader esig')
except Exception as ex:
if self.debug:
traceback.print_exc()
@ -4648,7 +4694,7 @@ class BasicSwap(BaseApp):
if now - self._last_checked_watched >= self.check_watched_seconds:
for k, c in self.coin_clients.items():
if k == Coins.PART_ANON:
if k == Coins.PART_ANON or k == Coins.PART_BLIND:
continue
if len(c['watched_outputs']) > 0:
self.checkForSpends(k, c)
@ -5203,11 +5249,11 @@ class BasicSwap(BaseApp):
spend_tx = ci.createScriptLockRefundSpendToFTx(
xmr_swap.a_lock_refund_tx, xmr_swap.a_lock_refund_tx_script,
pkh_dest,
xmr_offer.a_fee_rate
)
xmr_offer.a_fee_rate, xmr_swap.vkbv)
vkaf = self.getPathKey(offer.coin_from, offer.coin_to, bid.created_at, xmr_swap.contract_count, 3)
sig = ci.signTx(vkaf, spend_tx, 0, xmr_swap.a_lock_refund_tx_script, xmr_swap.a_swap_refund_value)
prevout_amount = ci.getLockRefundTxSwapOutputValue(bid, xmr_swap)
sig = ci.signTx(vkaf, spend_tx, 0, xmr_swap.a_lock_refund_tx_script, prevout_amount)
witness_stack = [
sig,

View file

@ -371,7 +371,3 @@ def isActiveBidState(state):
if state == BidStates.XMR_SWAP_SCRIPT_TX_REDEEMED:
return True
return False
class TemporaryError(ValueError):
pass

View file

@ -11,6 +11,7 @@ from .util import (
COIN,
make_int,
format_amount,
TemporaryError,
)
XMR_COIN = 10 ** 12
@ -248,3 +249,21 @@ class CoinInterface:
def knownWalletSeed(self):
return not self._unknown_wallet_seed
def chainparams(self):
return chainparams[self.coin_type()]
def chainparams_network(self):
return chainparams[self.coin_type()][self._network]
def is_transient_error(self, ex):
if isinstance(ex, TemporaryError):
return True
str_error = str(ex).lower()
if 'not enough unlocked money' in str_error:
return True
if 'transaction was rejected by daemon' in str_error:
return True
if 'invalid unlocked_balance' in str_error:
return True
return False

View file

@ -340,7 +340,7 @@ class CTxIn:
self.nSequence)
class CTxOutPart:
__slots__ = ("nVersion", "nValue", "scriptPubKey")
__slots__ = ("nVersion", "nValue", "scriptPubKey", "commitment", "data", "rangeproof")
def __init__(self, nValue=0, scriptPubKey=b""):
self.nVersion = OUTPUT_TYPE_STANDARD
@ -348,13 +348,37 @@ class CTxOutPart:
self.scriptPubKey = scriptPubKey
def deserialize(self, f):
self.nValue = struct.unpack("<q", f.read(8))[0]
self.scriptPubKey = deser_string(f)
if self.nVersion == OUTPUT_TYPE_STANDARD:
self.nValue = struct.unpack("<q", f.read(8))[0]
self.scriptPubKey = deser_string(f)
elif self.nVersion == OUTPUT_TYPE_DATA:
self.data = deser_string(f)
elif self.nVersion == OUTPUT_TYPE_CT:
self.commitment = f.read(33)
self.data = deser_string(f)
self.scriptPubKey = deser_string(f)
self.rangeproof = deser_string(f)
else:
raise ValueError(f'Unknown output type {self.nVersion}')
def serialize(self):
def serialize(self, with_witness=True):
r = b""
r += struct.pack("<q", self.nValue)
r += ser_string(self.scriptPubKey)
if self.nVersion == OUTPUT_TYPE_STANDARD:
r += struct.pack("<q", self.nValue)
r += ser_string(self.scriptPubKey)
elif self.nVersion == OUTPUT_TYPE_DATA:
r += ser_string(self.data)
elif self.nVersion == OUTPUT_TYPE_CT:
assert(len(self.commitment) == 33)
r += self.commitment
r += ser_string(self.data)
r += ser_string(self.scriptPubKey)
if with_witness:
r += ser_string(self.rangeproof)
else:
r += ser_compact_size(0) # rangeproof stub
else:
raise ValueError(f'Unknown output type {self.nVersion}')
return r
def __repr__(self):
@ -521,7 +545,7 @@ class CTransaction:
self.sha256 = None
self.hash = None
def serialize_without_witness(self):
def serialize_without_witness(self, include_rangeproof=False):
if self.nVersion == PARTICL_TX_VERSION:
r = struct.pack("<H", self.nVersion)
r += struct.pack("<I", self.nLockTime)
@ -529,7 +553,7 @@ class CTransaction:
r += ser_compact_size(len(self.vout))
for txo in self.vout:
r += bytes((txo.nVersion,))
r += txo.serialize()
r += txo.serialize(with_witness=include_rangeproof)
return r
r = b""
r += struct.pack("<i", self.nVersion)
@ -541,7 +565,7 @@ class CTransaction:
# Only serialize with witness when explicitly called for
def serialize_with_witness(self):
if self.nVersion == PARTICL_TX_VERSION:
r = self.serialize_without_witness()
r = self.serialize_without_witness(include_rangeproof=True)
while len(self.wit.vtxinwit) < len(self.vin):
self.wit.vtxinwit.append(CTxInWitness())
r += self.wit.serialize()

View file

@ -671,7 +671,6 @@ def LegacySignatureHash(script, txTo, inIdx, hashtype):
# Note that this corresponds to sigversion == 1 in EvalScript, which is used
# for version 0 witnesses.
def SegwitV0SignatureHash(script, txTo, inIdx, hashtype, amount):
hashPrevouts = 0
hashSequence = 0
hashOutputs = 0
@ -703,7 +702,12 @@ def SegwitV0SignatureHash(script, txTo, inIdx, hashtype, amount):
ss += ser_uint256(hashSequence)
ss += txTo.vin[inIdx].prevout.serialize()
ss += ser_string(script)
ss += struct.pack("<q", amount)
if isinstance(amount, int):
ss += struct.pack("<q", amount)
elif isinstance(amount, bytes):
ss += amount
else:
raise ValueError('Unknown amount type')
ss += struct.pack("<I", txTo.vin[inIdx].nSequence)
ss += ser_uint256(hashOutputs)
ss += struct.pack("<i", txTo.nLockTime)

View file

@ -301,9 +301,9 @@ class XmrSwap(Base):
kbsl_dleag = sa.Column(sa.LargeBinary)
kbsf_dleag = sa.Column(sa.LargeBinary)
vkbv = sa.Column(sa.LargeBinary)
pkbv = sa.Column(sa.LargeBinary)
pkbs = sa.Column(sa.LargeBinary)
vkbv = sa.Column(sa.LargeBinary) # chain b view private key
pkbv = sa.Column(sa.LargeBinary) # chain b view public key
pkbs = sa.Column(sa.LargeBinary) # chain b view spend key
a_lock_tx = sa.Column(sa.LargeBinary)
a_lock_tx_script = sa.Column(sa.LargeBinary)

View file

@ -66,7 +66,7 @@ def value_or_none(v):
def getCoinName(c):
if c == Coins.PART_ANON:
if c == Coins.PART_ANON or c == Coins.PART_BLIND:
return chainparams[Coins.PART]['name'].capitalize() + 'Anon'
return chainparams[c]['name'].capitalize()

View file

@ -13,16 +13,16 @@ from io import BytesIO
from basicswap.contrib.test_framework import segwit_addr
from .util import (
dumpj,
toWIF,
ensure,
make_int,
b58encode,
decodeAddress,
decodeScriptNum,
getCompactSizeLen,
SerialiseNumCompact,
dumpj,
format_amount,
make_int,
toWIF,
assert_cond,
decodeAddress)
getWitnessElementLen)
from coincurve.keys import (
PrivateKey,
PublicKey)
@ -50,11 +50,9 @@ from .contrib.test_framework.messages import (
FromHex)
from .contrib.test_framework.script import (
CScript,
CScriptOp,
CScript, CScriptOp,
OP_IF, OP_ELSE, OP_ENDIF,
OP_0,
OP_2,
OP_0, OP_2,
OP_CHECKSIG,
OP_CHECKMULTISIG,
OP_CHECKSEQUENCEVERIFY,
@ -67,7 +65,7 @@ from .basicswap_util import (
SEQUENCE_LOCK_BLOCKS,
SEQUENCE_LOCK_TIME)
from .chainparams import CoinInterface, Coins, chainparams
from .chainparams import CoinInterface, Coins
from .rpc import make_rpc_func
@ -76,6 +74,10 @@ SEQUENCE_LOCKTIME_TYPE_FLAG = (1 << 22)
SEQUENCE_LOCKTIME_MASK = 0x0000ffff
def ensure_op(v, err_string='Bad opcode'):
ensure(v, err_string)
def findOutput(tx, script_pk):
for i in range(len(tx.vout)):
if tx.vout[i].scriptPubKey == script_pk:
@ -188,7 +190,7 @@ class BTCInterface(CoinInterface):
return self.rpc_callback('getblockheader', [block_hash])
def initialiseWallet(self, key_bytes):
wif_prefix = chainparams[self.coin_type()][self._network]['key_prefix']
wif_prefix = self.chainparams_network()['key_prefix']
key_wif = toWIF(wif_prefix, key_bytes)
try:
@ -211,7 +213,7 @@ class BTCInterface(CoinInterface):
chain_synced = round(blockchaininfo['verificationprogress'], 3)
if chain_synced < 1.0:
raise ValueError('{} chain isn\'t synced.'.format(self.chain_name()))
raise ValueError('{} chain isn\'t synced.'.format(self.coin_name()))
block_hash = best_block
while True:
@ -241,24 +243,35 @@ class BTCInterface(CoinInterface):
return self.rpc_callback('getnetworkinfo')['relayfee'], 'relayfee'
def decodeAddress(self, address):
bech32_prefix = chainparams[self.coin_type()][self._network]['hrp']
bech32_prefix = self.chainparams_network()['hrp']
if address.startswith(bech32_prefix):
return bytes(segwit_addr.decode(bech32_prefix, address)[1])
return decodeAddress(address)[1:]
def pubkey_to_segwit_address(self, pk):
bech32_prefix = chainparams[self.coin_type()][self._network]['hrp']
bech32_prefix = self.chainparams_network()['hrp']
version = 0
pkh = hash160(pk)
return segwit_addr.encode(bech32_prefix, version, pkh)
def pubkey_to_address(self, pk):
assert(len(pk) == 33)
prefix = chainparams[self.coin_type()][self._network]['pubkey_address']
data = bytes((prefix,)) + hash160(pk)
def pkh_to_address(self, pkh):
# pkh is hash160(pk)
assert(len(pkh) == 20)
prefix = self.chainparams_network()['pubkey_address']
data = bytes((prefix,)) + pkh
checksum = hashlib.sha256(hashlib.sha256(data).digest()).digest()
return b58encode(data + checksum[0:4])
def encode_p2wsh(self, script):
bech32_prefix = self.chainparams_network()['hrp']
version = 0
program = script[2:] # strip version and length
return segwit_addr.encode(bech32_prefix, version, program)
def pubkey_to_address(self, pk):
assert(len(pk) == 33)
return self.pkh_to_address(hash160(pk))
def getNewSecretKey(self):
return getSecretInt()
@ -299,19 +312,19 @@ class BTCInterface(CoinInterface):
def extractScriptLockScriptValues(self, script_bytes):
script_len = len(script_bytes)
assert_cond(script_len == 71, 'Bad script length')
ensure(script_len == 71, 'Bad script length')
o = 0
assert_cond(script_bytes[o] == OP_2)
assert_cond(script_bytes[o + 1] == 33)
ensure_op(script_bytes[o] == OP_2)
ensure_op(script_bytes[o + 1] == 33)
o += 2
pk1 = script_bytes[o: o + 33]
o += 33
assert_cond(script_bytes[o] == 33)
ensure_op(script_bytes[o] == 33)
o += 1
pk2 = script_bytes[o: o + 33]
o += 33
assert_cond(script_bytes[o] == OP_2)
assert_cond(script_bytes[o + 1] == OP_CHECKMULTISIG)
ensure_op(script_bytes[o] == OP_2)
ensure_op(script_bytes[o + 1] == OP_CHECKMULTISIG)
return pk1, pk2
@ -321,7 +334,7 @@ class BTCInterface(CoinInterface):
return CScript([2, Kal_enc, Kaf_enc, 2, CScriptOp(OP_CHECKMULTISIG)])
def createScriptLockTx(self, value, Kal, Kaf):
def createScriptLockTx(self, value, Kal, Kaf, vkbv=None):
script = self.genScriptLockTxScript(Kal, Kaf)
tx = CTransaction()
tx.nVersion = self.txVersion()
@ -329,34 +342,37 @@ class BTCInterface(CoinInterface):
return tx.serialize(), script
def fundScriptLockTx(self, tx_bytes, feerate, vkbv=None):
return self.fundTx(tx_bytes, feerate)
def extractScriptLockRefundScriptValues(self, script_bytes):
script_len = len(script_bytes)
assert_cond(script_len > 73, 'Bad script length')
assert_cond(script_bytes[0] == OP_IF)
assert_cond(script_bytes[1] == OP_2)
assert_cond(script_bytes[2] == 33)
ensure(script_len > 73, 'Bad script length')
ensure_op(script_bytes[0] == OP_IF)
ensure_op(script_bytes[1] == OP_2)
ensure_op(script_bytes[2] == 33)
pk1 = script_bytes[3: 3 + 33]
assert_cond(script_bytes[36] == 33)
ensure_op(script_bytes[36] == 33)
pk2 = script_bytes[37: 37 + 33]
assert_cond(script_bytes[70] == OP_2)
assert_cond(script_bytes[71] == OP_CHECKMULTISIG)
assert_cond(script_bytes[72] == OP_ELSE)
ensure_op(script_bytes[70] == OP_2)
ensure_op(script_bytes[71] == OP_CHECKMULTISIG)
ensure_op(script_bytes[72] == OP_ELSE)
o = 73
csv_val, nb = decodeScriptNum(script_bytes, o)
o += nb
assert_cond(script_len == o + 5 + 33, 'Bad script length') # Fails if script too long
assert_cond(script_bytes[o] == OP_CHECKSEQUENCEVERIFY)
ensure(script_len == o + 5 + 33, 'Bad script length') # Fails if script too long
ensure_op(script_bytes[o] == OP_CHECKSEQUENCEVERIFY)
o += 1
assert_cond(script_bytes[o] == OP_DROP)
ensure_op(script_bytes[o] == OP_DROP)
o += 1
assert_cond(script_bytes[o] == 33)
ensure_op(script_bytes[o] == 33)
o += 1
pk3 = script_bytes[o: o + 33]
o += 33
assert_cond(script_bytes[o] == OP_CHECKSIG)
ensure_op(script_bytes[o] == OP_CHECKSIG)
o += 1
assert_cond(script_bytes[o] == OP_ENDIF)
ensure_op(script_bytes[o] == OP_ENDIF)
return pk1, pk2, csv_val, pk3
@ -373,30 +389,28 @@ class BTCInterface(CoinInterface):
Kaf_enc, CScriptOp(OP_CHECKSIG),
CScriptOp(OP_ENDIF)])
def createScriptLockRefundTx(self, tx_lock_bytes, script_lock, Kal, Kaf, lock1_value, csv_val, tx_fee_rate):
def createScriptLockRefundTx(self, tx_lock_bytes, script_lock, Kal, Kaf, lock1_value, csv_val, tx_fee_rate, vkbv=None):
tx_lock = CTransaction()
tx_lock = FromHex(tx_lock, tx_lock_bytes.hex())
output_script = CScript([OP_0, hashlib.sha256(script_lock).digest()])
locked_n = findOutput(tx_lock, output_script)
assert_cond(locked_n is not None, 'Output not found in tx')
ensure(locked_n is not None, 'Output not found in tx')
locked_coin = tx_lock.vout[locked_n].nValue
tx_lock.rehash()
tx_lock_hash_int = tx_lock.sha256
tx_lock_id_int = tx_lock.sha256
refund_script = self.genScriptLockRefundTxScript(Kal, Kaf, csv_val)
tx = CTransaction()
tx.nVersion = self.txVersion()
tx.vin.append(CTxIn(COutPoint(tx_lock_hash_int, locked_n), nSequence=lock1_value))
tx.vin.append(CTxIn(COutPoint(tx_lock_id_int, locked_n), nSequence=lock1_value))
tx.vout.append(self.txoType()(locked_coin, CScript([OP_0, hashlib.sha256(refund_script).digest()])))
witness_bytes = len(script_lock)
witness_bytes += 74 * 2 # 2 signatures (72 + 1 byte sighashtype + 1 byte size) - Use maximum txn size for estimate
witness_bytes += 2 # 2 empty witness stack values
witness_bytes += getCompactSizeLen(witness_bytes)
dummy_witness_stack = self.getScriptLockTxDummyWitness(script_lock)
witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack)
vsize = self.getTxVSize(tx, add_witness_bytes=witness_bytes)
pay_fee = int(tx_fee_rate * vsize / 1000)
pay_fee = int(tx_fee_rate * vsize // 1000)
tx.vout[0].nValue = locked_coin - pay_fee
tx.rehash()
@ -405,7 +419,7 @@ class BTCInterface(CoinInterface):
return tx.serialize(), refund_script, tx.vout[0].nValue
def createScriptLockRefundSpendTx(self, tx_lock_refund_bytes, script_lock_refund, pkh_refund_to, tx_fee_rate):
def createScriptLockRefundSpendTx(self, tx_lock_refund_bytes, script_lock_refund, pkh_refund_to, tx_fee_rate, vkbv=None):
# Returns the coinA locked coin to the leader
# The follower will sign the multisig path with a signature encumbered by the leader's coinB spend pubkey
# If the leader publishes the decrypted signature the leader's coinB spend privatekey will be revealed to the follower
@ -414,7 +428,7 @@ class BTCInterface(CoinInterface):
output_script = CScript([OP_0, hashlib.sha256(script_lock_refund).digest()])
locked_n = findOutput(tx_lock_refund, output_script)
assert_cond(locked_n is not None, 'Output not found in tx')
ensure(locked_n is not None, 'Output not found in tx')
locked_coin = tx_lock_refund.vout[locked_n].nValue
tx_lock_refund.rehash()
@ -426,12 +440,10 @@ class BTCInterface(CoinInterface):
tx.vout.append(self.txoType()(locked_coin, self.getScriptForPubkeyHash(pkh_refund_to)))
witness_bytes = len(script_lock_refund)
witness_bytes += 74 * 2 # 2 signatures (72 + 1 byte sighashtype + 1 byte size) - Use maximum txn size for estimate
witness_bytes += 4 # 1 empty, 1 true witness stack values
witness_bytes += getCompactSizeLen(witness_bytes)
dummy_witness_stack = self.getScriptLockRefundSpendTxDummyWitness(script_lock_refund)
witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack)
vsize = self.getTxVSize(tx, add_witness_bytes=witness_bytes)
pay_fee = int(tx_fee_rate * vsize / 1000)
pay_fee = int(tx_fee_rate * vsize // 1000)
tx.vout[0].nValue = locked_coin - pay_fee
tx.rehash()
@ -440,14 +452,15 @@ class BTCInterface(CoinInterface):
return tx.serialize()
def createScriptLockRefundSpendToFTx(self, tx_lock_refund_bytes, script_lock_refund, pkh_dest, tx_fee_rate):
def createScriptLockRefundSpendToFTx(self, tx_lock_refund_bytes, script_lock_refund, pkh_dest, tx_fee_rate, vkbv=None):
# lock refund swipe tx
# Sends the coinA locked coin to the follower
tx_lock_refund = self.loadTx(tx_lock_refund_bytes)
output_script = CScript([OP_0, hashlib.sha256(script_lock_refund).digest()])
locked_n = findOutput(tx_lock_refund, output_script)
assert_cond(locked_n is not None, 'Output not found in tx')
ensure(locked_n is not None, 'Output not found in tx')
locked_coin = tx_lock_refund.vout[locked_n].nValue
A, B, lock2_value, C = self.extractScriptLockRefundScriptValues(script_lock_refund)
@ -461,12 +474,10 @@ class BTCInterface(CoinInterface):
tx.vout.append(self.txoType()(locked_coin, self.getScriptForPubkeyHash(pkh_dest)))
witness_bytes = len(script_lock_refund)
witness_bytes += 74 # 2 signatures (72 + 1 byte sighashtype + 1 byte size) - Use maximum txn size for estimate
witness_bytes += 1 # 1 empty stack value
witness_bytes += getCompactSizeLen(witness_bytes)
dummy_witness_stack = self.getScriptLockRefundSwipeTxDummyWitness(script_lock_refund)
witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack)
vsize = self.getTxVSize(tx, add_witness_bytes=witness_bytes)
pay_fee = int(tx_fee_rate * vsize / 1000)
pay_fee = int(tx_fee_rate * vsize // 1000)
tx.vout[0].nValue = locked_coin - pay_fee
tx.rehash()
@ -475,29 +486,26 @@ class BTCInterface(CoinInterface):
return tx.serialize()
def createScriptLockSpendTx(self, tx_lock_bytes, script_lock, pkh_dest, tx_fee_rate):
def createScriptLockSpendTx(self, tx_lock_bytes, script_lock, pkh_dest, tx_fee_rate, vkbv=None):
tx_lock = self.loadTx(tx_lock_bytes)
output_script = CScript([OP_0, hashlib.sha256(script_lock).digest()])
locked_n = findOutput(tx_lock, output_script)
assert_cond(locked_n is not None, 'Output not found in tx')
ensure(locked_n is not None, 'Output not found in tx')
locked_coin = tx_lock.vout[locked_n].nValue
tx_lock.rehash()
tx_lock_hash_int = tx_lock.sha256
tx_lock_id_int = tx_lock.sha256
tx = CTransaction()
tx.nVersion = self.txVersion()
tx.vin.append(CTxIn(COutPoint(tx_lock_hash_int, locked_n)))
tx.vin.append(CTxIn(COutPoint(tx_lock_id_int, locked_n)))
tx.vout.append(self.txoType()(locked_coin, self.getScriptForPubkeyHash(pkh_dest)))
witness_bytes = len(script_lock)
witness_bytes += 33 # sv, size
witness_bytes += 74 * 2 # 2 signatures (72 + 1 byte sighashtype + 1 byte size) - Use maximum txn size for estimate
witness_bytes += 4 # 1 empty, 1 true witness stack values
witness_bytes += getCompactSizeLen(witness_bytes)
dummy_witness_stack = self.getScriptLockTxDummyWitness(script_lock)
witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack)
vsize = self.getTxVSize(tx, add_witness_bytes=witness_bytes)
pay_fee = int(tx_fee_rate * vsize / 1000)
pay_fee = int(tx_fee_rate * vsize // 1000)
tx.vout[0].nValue = locked_coin - pay_fee
tx.rehash()
@ -510,7 +518,7 @@ class BTCInterface(CoinInterface):
swap_value,
Kal, Kaf,
feerate,
check_lock_tx_inputs):
check_lock_tx_inputs, vkbv=None):
# Verify:
#
@ -519,26 +527,28 @@ class BTCInterface(CoinInterface):
# Check fee is reasonable
tx = self.loadTx(tx_bytes)
tx_hash = self.getTxHash(tx)
self._log.info('Verifying lock tx: {}.'.format(b2h(tx_hash)))
txid = self.getTxid(tx)
self._log.info('Verifying lock tx: {}.'.format(b2h(txid)))
assert_cond(tx.nVersion == self.txVersion(), 'Bad version')
assert_cond(tx.nLockTime == 0, 'Bad nLockTime')
ensure(tx.nVersion == self.txVersion(), 'Bad version')
ensure(tx.nLockTime == 0, 'Bad nLockTime') # TODO match txns created by cores
script_pk = CScript([OP_0, hashlib.sha256(script_out).digest()])
locked_n = findOutput(tx, script_pk)
assert_cond(locked_n is not None, 'Output not found in tx')
ensure(locked_n is not None, 'Output not found in tx')
locked_coin = tx.vout[locked_n].nValue
assert_cond(locked_coin == swap_value, 'Bad locked value')
# Check value
ensure(locked_coin == swap_value, 'Bad locked value')
# Check script and values
# Check script
A, B = self.extractScriptLockScriptValues(script_out)
assert_cond(A == Kal, 'Bad script pubkey')
assert_cond(B == Kaf, 'Bad script pubkey')
ensure(A == Kal, 'Bad script pubkey')
ensure(B == Kaf, 'Bad script pubkey')
if check_lock_tx_inputs:
# Check that inputs are unspent and verify fee rate
# TODO: Check that inputs are unspent
# Verify fee rate
inputs_value = 0
add_bytes = 0
add_witness_bytes = getCompactSizeLen(len(tx.vin))
@ -562,7 +572,7 @@ class BTCInterface(CoinInterface):
assert(fee_paid > 0)
vsize = self.getTxVSize(tx, add_bytes, add_witness_bytes)
fee_rate_paid = fee_paid * 1000 / vsize
fee_rate_paid = fee_paid * 1000 // vsize
self._log.info('tx amount, vsize, feerate: %ld, %ld, %ld', locked_coin, vsize, fee_rate_paid)
@ -570,97 +580,93 @@ class BTCInterface(CoinInterface):
self._log.warning('feerate paid doesn\'t match expected: %ld, %ld', fee_rate_paid, feerate)
# TODO: Display warning to user
return tx_hash, locked_n
return txid, locked_n
def verifyLockRefundTx(self, tx_bytes, script_out,
def verifyLockRefundTx(self, tx_bytes, lock_tx_bytes, script_out,
prevout_id, prevout_n, prevout_seq, prevout_script,
Kal, Kaf, csv_val_expect, swap_value, feerate):
Kal, Kaf, csv_val_expect, swap_value, feerate, vkbv=None):
# Verify:
# Must have only one input with correct prevout and sequence
# Must have only one output to the p2wsh of the lock refund script
# Output value must be locked_coin - lock tx fee
tx = self.loadTx(tx_bytes)
tx_hash = self.getTxHash(tx)
self._log.info('Verifying lock refund tx: {}.'.format(b2h(tx_hash)))
txid = self.getTxid(tx)
self._log.info('Verifying lock refund tx: {}.'.format(b2h(txid)))
assert_cond(tx.nVersion == self.txVersion(), 'Bad version')
assert_cond(tx.nLockTime == 0, 'nLockTime not 0')
assert_cond(len(tx.vin) == 1, 'tx doesn\'t have one input')
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')
assert_cond(tx.vin[0].nSequence == prevout_seq, 'Bad input nSequence')
assert_cond(len(tx.vin[0].scriptSig) == 0, 'Input scriptsig not empty')
assert_cond(tx.vin[0].prevout.hash == b2i(prevout_id) and tx.vin[0].prevout.n == prevout_n, 'Input prevout mismatch')
ensure(tx.vin[0].nSequence == prevout_seq, 'Bad input nSequence')
ensure(len(tx.vin[0].scriptSig) == 0, 'Input scriptsig not empty')
ensure(tx.vin[0].prevout.hash == b2i(prevout_id) and tx.vin[0].prevout.n == prevout_n, 'Input prevout mismatch')
assert_cond(len(tx.vout) == 1, 'tx doesn\'t have one output')
ensure(len(tx.vout) == 1, 'tx doesn\'t have one output')
script_pk = CScript([OP_0, hashlib.sha256(script_out).digest()])
locked_n = findOutput(tx, script_pk)
assert_cond(locked_n is not None, 'Output not found in tx')
ensure(locked_n is not None, 'Output not found in tx')
locked_coin = tx.vout[locked_n].nValue
# Check script and values
A, B, csv_val, C = self.extractScriptLockRefundScriptValues(script_out)
assert_cond(A == Kal, 'Bad script pubkey')
assert_cond(B == Kaf, 'Bad script pubkey')
assert_cond(csv_val == csv_val_expect, 'Bad script csv value')
assert_cond(C == Kaf, 'Bad script pubkey')
ensure(A == Kal, 'Bad script pubkey')
ensure(B == Kaf, 'Bad script pubkey')
ensure(csv_val == csv_val_expect, 'Bad script csv value')
ensure(C == Kaf, 'Bad script pubkey')
fee_paid = swap_value - locked_coin
assert(fee_paid > 0)
witness_bytes = len(prevout_script)
witness_bytes += 74 * 2 # 2 signatures (72 + 1 byte sighashtype + 1 byte size) - Use maximum txn size for estimate
witness_bytes += 2 # 2 empty witness stack values
witness_bytes += getCompactSizeLen(witness_bytes)
dummy_witness_stack = self.getScriptLockTxDummyWitness(prevout_script)
witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack)
vsize = self.getTxVSize(tx, add_witness_bytes=witness_bytes)
fee_rate_paid = fee_paid * 1000 / vsize
fee_rate_paid = fee_paid * 1000 // vsize
self._log.info('tx amount, vsize, feerate: %ld, %ld, %ld', locked_coin, vsize, fee_rate_paid)
if not self.compareFeeRates(fee_rate_paid, feerate):
raise ValueError('Bad fee rate, expected: {}'.format(feerate))
return tx_hash, locked_coin
return txid, locked_coin, locked_n
def verifyLockRefundSpendTx(self, tx_bytes,
def verifyLockRefundSpendTx(self, tx_bytes, lock_refund_tx_bytes,
lock_refund_tx_id, prevout_script,
Kal,
prevout_value, feerate):
prevout_n, prevout_value, feerate, vkbv=None):
# Verify:
# Must have only one input with correct prevout (n is always 0) and sequence
# Must have only one output sending lock refund tx value - fee to leader's address, TODO: follower shouldn't need to verify destination addr
tx = self.loadTx(tx_bytes)
tx_hash = self.getTxHash(tx)
self._log.info('Verifying lock refund spend tx: {}.'.format(b2h(tx_hash)))
txid = self.getTxid(tx)
self._log.info('Verifying lock refund spend tx: {}.'.format(b2h(txid)))
assert_cond(tx.nVersion == self.txVersion(), 'Bad version')
assert_cond(tx.nLockTime == 0, 'nLockTime not 0')
assert_cond(len(tx.vin) == 1, 'tx doesn\'t have one input')
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')
assert_cond(tx.vin[0].nSequence == 0, 'Bad input nSequence')
assert_cond(len(tx.vin[0].scriptSig) == 0, 'Input scriptsig not empty')
assert_cond(tx.vin[0].prevout.hash == b2i(lock_refund_tx_id) and tx.vin[0].prevout.n == 0, 'Input prevout mismatch')
ensure(tx.vin[0].nSequence == 0, 'Bad input nSequence')
ensure(len(tx.vin[0].scriptSig) == 0, 'Input scriptsig not empty')
ensure(tx.vin[0].prevout.hash == b2i(lock_refund_tx_id) and tx.vin[0].prevout.n == 0, 'Input prevout mismatch')
assert_cond(len(tx.vout) == 1, 'tx doesn\'t have one output')
ensure(len(tx.vout) == 1, 'tx doesn\'t have one output')
# Destination doesn't matter to the follower
'''
p2wpkh = CScript([OP_0, hash160(Kal)])
locked_n = findOutput(tx, p2wpkh)
assert_cond(locked_n is not None, 'Output not found in lock refund spend tx')
ensure(locked_n is not None, 'Output not found in lock refund spend tx')
'''
tx_value = tx.vout[0].nValue
fee_paid = prevout_value - tx_value
assert(fee_paid > 0)
witness_bytes = len(prevout_script)
witness_bytes += 74 * 2 # 2 signatures (72 + 1 byte sighashtype + 1 byte size) - Use maximum txn size for estimate
witness_bytes += 4 # 1 empty, 1 true witness stack values
witness_bytes += getCompactSizeLen(witness_bytes)
dummy_witness_stack = self.getScriptLockRefundSpendTxDummyWitness(prevout_script)
witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack)
vsize = self.getTxVSize(tx, add_witness_bytes=witness_bytes)
fee_rate_paid = fee_paid * 1000 / vsize
fee_rate_paid = fee_paid * 1000 // vsize
self._log.info('tx amount, vsize, feerate: %ld, %ld, %ld', tx_value, vsize, fee_rate_paid)
@ -671,45 +677,43 @@ class BTCInterface(CoinInterface):
def verifyLockSpendTx(self, tx_bytes,
lock_tx_bytes, lock_tx_script,
a_pkhash_f, feerate):
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)
tx_hash = self.getTxHash(tx)
self._log.info('Verifying lock spend tx: {}.'.format(b2h(tx_hash)))
txid = self.getTxid(tx)
self._log.info('Verifying lock spend tx: {}.'.format(b2h(txid)))
assert_cond(tx.nVersion == self.txVersion(), 'Bad version')
assert_cond(tx.nLockTime == 0, 'nLockTime not 0')
assert_cond(len(tx.vin) == 1, 'tx doesn\'t have one input')
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.getTxHash(lock_tx)
lock_tx_id = self.getTxid(lock_tx)
output_script = CScript([OP_0, hashlib.sha256(lock_tx_script).digest()])
locked_n = findOutput(lock_tx, output_script)
assert_cond(locked_n is not None, 'Output not found in tx')
ensure(locked_n is not None, 'Output not found in tx')
locked_coin = lock_tx.vout[locked_n].nValue
assert_cond(tx.vin[0].nSequence == 0, 'Bad input nSequence')
assert_cond(len(tx.vin[0].scriptSig) == 0, 'Input scriptsig not empty')
assert_cond(tx.vin[0].prevout.hash == b2i(lock_tx_id) and tx.vin[0].prevout.n == locked_n, 'Input prevout mismatch')
ensure(tx.vin[0].nSequence == 0, 'Bad input nSequence')
ensure(len(tx.vin[0].scriptSig) == 0, 'Input scriptsig not empty')
ensure(tx.vin[0].prevout.hash == b2i(lock_tx_id) and tx.vin[0].prevout.n == locked_n, 'Input prevout mismatch')
assert_cond(len(tx.vout) == 1, 'tx doesn\'t have one output')
ensure(len(tx.vout) == 1, 'tx doesn\'t have one output')
p2wpkh = self.getScriptForPubkeyHash(a_pkhash_f)
assert_cond(tx.vout[0].scriptPubKey == p2wpkh, 'Bad output destination')
ensure(tx.vout[0].scriptPubKey == p2wpkh, 'Bad output destination')
# The value of the lock tx output should already be verified, if the fee is as expected the difference will be the correct amount
fee_paid = locked_coin - tx.vout[0].nValue
assert(fee_paid > 0)
witness_bytes = len(lock_tx_script)
witness_bytes += 33 # sv, size
witness_bytes += 74 * 2 # 2 signatures (72 + 1 byte sighashtype + 1 byte size) - Use maximum txn size for estimate
witness_bytes += 4 # 1 empty, 1 true witness stack values
witness_bytes += getCompactSizeLen(witness_bytes)
dummy_witness_stack = self.getScriptLockTxDummyWitness(lock_tx_script)
witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack)
vsize = self.getTxVSize(tx, add_witness_bytes=witness_bytes)
fee_rate_paid = fee_paid * 1000 / vsize
fee_rate_paid = fee_paid * 1000 // vsize
self._log.info('tx amount, vsize, feerate: %ld, %ld, %ld', tx.vout[0].nValue, vsize, fee_rate_paid)
@ -718,30 +722,30 @@ class BTCInterface(CoinInterface):
return True
def signTx(self, key_bytes, tx_bytes, prevout_n, prevout_script, prevout_value):
def signTx(self, key_bytes, tx_bytes, input_n, prevout_script, prevout_value):
tx = self.loadTx(tx_bytes)
sig_hash = SegwitV0SignatureHash(prevout_script, tx, prevout_n, SIGHASH_ALL, prevout_value)
sig_hash = SegwitV0SignatureHash(prevout_script, tx, input_n, SIGHASH_ALL, prevout_value)
eck = PrivateKey(key_bytes)
return eck.sign(sig_hash, hasher=None) + bytes((SIGHASH_ALL,))
def signTxOtVES(self, key_sign, pubkey_encrypt, tx_bytes, prevout_n, prevout_script, prevout_value):
def signTxOtVES(self, key_sign, pubkey_encrypt, tx_bytes, input_n, prevout_script, prevout_value):
tx = self.loadTx(tx_bytes)
sig_hash = SegwitV0SignatureHash(prevout_script, tx, prevout_n, SIGHASH_ALL, prevout_value)
sig_hash = SegwitV0SignatureHash(prevout_script, tx, input_n, SIGHASH_ALL, prevout_value)
return ecdsaotves_enc_sign(key_sign, pubkey_encrypt, sig_hash)
def verifyTxOtVES(self, tx_bytes, ct, Ks, Ke, prevout_n, prevout_script, prevout_value):
def verifyTxOtVES(self, tx_bytes, ct, Ks, Ke, input_n, prevout_script, prevout_value):
tx = self.loadTx(tx_bytes)
sig_hash = SegwitV0SignatureHash(prevout_script, tx, prevout_n, SIGHASH_ALL, prevout_value)
sig_hash = SegwitV0SignatureHash(prevout_script, tx, input_n, SIGHASH_ALL, prevout_value)
return ecdsaotves_enc_verify(Ks, Ke, sig_hash, ct)
def decryptOtVES(self, k, esig):
return ecdsaotves_dec_sig(k, esig) + bytes((SIGHASH_ALL,))
def verifyTxSig(self, tx_bytes, sig, K, prevout_n, prevout_script, prevout_value):
def verifyTxSig(self, tx_bytes, sig, K, input_n, prevout_script, prevout_value):
tx = self.loadTx(tx_bytes)
sig_hash = SegwitV0SignatureHash(prevout_script, tx, prevout_n, SIGHASH_ALL, prevout_value)
sig_hash = SegwitV0SignatureHash(prevout_script, tx, input_n, SIGHASH_ALL, prevout_value)
pubkey = PublicKey(K)
return pubkey.verify(sig[: -1], sig_hash, hasher=None) # Pop the hashtype byte
@ -751,7 +755,7 @@ class BTCInterface(CoinInterface):
return pubkey.verify(sig, signed_hash, hasher=None)
def fundTx(self, tx, feerate):
feerate_str = format_amount(feerate, self.exp())
feerate_str = self.format_amount(feerate)
# TODO: unlock unspents if bid cancelled
options = {
'lockUnspents': True,
@ -784,7 +788,9 @@ class BTCInterface(CoinInterface):
tx.deserialize(BytesIO(tx_bytes))
return tx
def getTxHash(self, tx):
def getTxid(self, tx):
if isinstance(tx, str):
tx = bytes.fromhex(tx)
if isinstance(tx, bytes):
tx = self.loadTx(tx)
tx.rehash()
@ -829,6 +835,11 @@ class BTCInterface(CoinInterface):
tx.wit.vtxinwit[0].scriptWitness.stack = stack
return tx.serialize()
def stripTxSignature(self, tx_bytes):
tx = self.loadTx(tx_bytes)
tx.wit.vtxinwit.clear()
return tx.serialize()
def extractLeaderSig(self, tx_bytes):
tx = self.loadTx(tx_bytes)
return tx.wit.vtxinwit[0].scriptWitness.stack[1]
@ -851,7 +862,7 @@ class BTCInterface(CoinInterface):
b_lock_tx = self.createBLockTx(Kbs, output_amount)
b_lock_tx = self.fundTx(b_lock_tx, feerate)
b_lock_tx_id = self.getTxHash(b_lock_tx)
b_lock_tx_id = self.getTxid(b_lock_tx)
b_lock_tx = self.signTxWithWallet(b_lock_tx)
return self.publishTx(b_lock_tx)
@ -881,7 +892,6 @@ class BTCInterface(CoinInterface):
return None
def waitForLockTxB(self, kbv, Kbs, cb_swap_value, cb_block_confirmed):
raw_dest = self.getPkDest(Kbs)
for i in range(20):
@ -899,9 +909,36 @@ class BTCInterface(CoinInterface):
return False
def spendBLockTx(self, chain_b_lock_txid, address_to, kbv, kbs, cb_swap_value, b_fee, restore_height):
print('TODO: spendBLockTx')
raise ValueError('TODO')
def getOutput(self, txid, dest_script, expect_value):
def getLockTxHeight(self, txid, dest_script, bid_amount, xmr_swap):
rv = None
p2wsh_addr = self.encode_p2wsh(dest_script)
addr_info = self.rpc_callback('getaddressinfo', [p2wsh_addr])
if not addr_info['iswatchonly']:
ro = self.rpc_callback('importaddress', [p2wsh_addr, 'bid', False])
self._log.info('Imported watch-only addr: {}'.format(p2wsh_addr))
self._log.info('Rescanning chain from height: {}'.format(xmr_swap.start_chain_a_height))
self.rpc_callback('rescanblockchain', [xmr_swap.start_chain_a_height])
try:
tx = self.rpc_callback('gettransaction', [txid.hex()])
block_height = 0
if 'blockhash' in tx:
block_header = self.rpc_callback('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:
pass
return rv
def getOutput(self, txid, dest_script, expect_value, xmr_swap=None):
# TODO: Use getrawtransaction if txindex is active
utxos = self.rpc_callback('scantxoutset', ['start', ['raw({})'.format(dest_script.hex())]])
if 'height' in utxos: # chain_height not returned by v18 codebase
@ -942,7 +979,7 @@ class BTCInterface(CoinInterface):
def verifyMessage(self, address, message, signature, message_magic=None) -> bool:
if message_magic is None:
message_magic = chainparams[self.coin_type()]['message_magic']
message_magic = self.chainparams_network()['message_magic']
message_bytes = SerialiseNumCompact(len(message_magic)) + bytes(message_magic, 'utf-8') + SerialiseNumCompact(len(message)) + bytes(message, 'utf-8')
message_hash = hashlib.sha256(hashlib.sha256(message_bytes).digest()).digest()
@ -961,7 +998,51 @@ class BTCInterface(CoinInterface):
return True if address_hash == pubkey_hash else False
def showLockTransfers(self, Kbv, Kbs):
return 'Unimplemented'
raise ValueError('Unimplemented')
def getLockTxSwapOutputValue(self, bid, xmr_swap):
return bid.amount
def getLockRefundTxSwapOutputValue(self, bid, xmr_swap):
return xmr_swap.a_swap_refund_value
def getLockRefundTxSwapOutput(self, xmr_swap):
# Only one prevout exists
return 0
def getScriptLockTxDummyWitness(self, script):
return [
b''.hex(),
bytes(72).hex(),
bytes(72).hex(),
bytes(len(script)).hex()
]
def getScriptLockRefundSpendTxDummyWitness(self, script):
return [
b''.hex(),
bytes(72).hex(),
bytes(72).hex(),
bytes((1,)).hex(),
bytes(len(script)).hex()
]
def getScriptLockRefundSwipeTxDummyWitness(self, script):
return [
bytes(72).hex(),
b''.hex(),
bytes(len(script)).hex()
]
def getWitnessStackSerialisedLength(self, witness_stack):
length = getCompactSizeLen(len(witness_stack))
for e in witness_stack:
length += getWitnessElementLen(len(e) // 2) # hex -> bytes
# See core SerializeTransaction
length += 32 + 4 + 1 + 4 # vinDummy
length += 1 # flags
return length
def testBTCInterface():

View file

@ -5,6 +5,8 @@
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import hashlib
import basicswap.contrib.segwit_addr as segwit_addr
from enum import IntEnum
from .contrib.test_framework.messages import (
@ -12,16 +14,20 @@ from .contrib.test_framework.messages import (
)
from .contrib.test_framework.script import (
CScript,
OP_0,
OP_DUP, OP_HASH160, OP_EQUALVERIFY, OP_CHECKSIG
)
from .ecc_util import i2b
from .util import (
encodeStealthAddress,
toWIF,
ensure,
make_int)
from .basicswap_util import (
TemporaryError)
make_int,
getP2WSH,
TemporaryError,
getCompactSizeLen,
encodeStealthAddress,
getWitnessElementLen)
from .chainparams import Coins, chainparams
from .interface_btc import BTCInterface
@ -98,12 +104,504 @@ class PARTInterface(BTCInterface):
return encodeStealthAddress(prefix_byte, scan_pubkey, spend_pubkey)
def getWitnessStackSerialisedLength(self, witness_stack):
length = getCompactSizeLen(len(witness_stack))
for e in witness_stack:
length += getWitnessElementLen(len(e) // 2) # hex -> bytes
return length
class PARTInterfaceBlind(PARTInterface):
@staticmethod
def balance_type():
return BalanceTypes.BLIND
def encodeSegwitP2WSH(self, p2wsh):
return segwit_addr.encode(self.chainparams_network()['hrp'], 0, p2wsh[2:])
def getScriptLockTxNonce(self, data):
return hashlib.sha256(data + bytes('locktx', 'utf-8')).digest()
def getScriptLockRefundTxNonce(self, data):
return hashlib.sha256(data + bytes('lockrefundtx', 'utf-8')).digest()
def findOutputByNonce(self, tx_obj, nonce):
blinded_info = None
output_n = None
for txo in tx_obj['vout']:
if txo['type'] != 'blind':
continue
try:
blinded_info = self.rpc_callback('rewindrangeproof', [txo['rangeproof'], txo['valueCommitment'], nonce.hex()])
output_n = txo['n']
self.rpc_callback('rewindrangeproof', [txo['rangeproof'], txo['valueCommitment'], nonce.hex()])
break
except Exception as e:
self._log.debug('Searching for locked output: {}'.format(str(e)))
continue
# Should not be possible for commitment not to match
v = self.rpc_callback('verifycommitment', [txo['valueCommitment'], blinded_info['blind'], blinded_info['amount']])
ensure(v['result'] is True, 'verifycommitment failed')
return output_n, blinded_info
def createScriptLockTx(self, value, Kal, Kaf, vkbv):
script = self.genScriptLockTxScript(Kal, Kaf)
# Nonce is derived from vkbv, ephemeral_key isn't used
ephemeral_key = i2b(self.getNewSecretKey())
ephemeral_pubkey = self.getPubkey(ephemeral_key)
assert(len(ephemeral_pubkey) == 33)
nonce = self.getScriptLockTxNonce(vkbv)
p2wsh_addr = self.encodeSegwitP2WSH(getP2WSH(script))
inputs = []
outputs = [{'type': 'blind', 'amount': self.format_amount(value), 'address': p2wsh_addr, 'nonce': nonce.hex(), 'data': ephemeral_pubkey.hex()}]
params = [inputs, outputs]
rv = self.rpc_callback('createrawparttransaction', params)
tx_bytes = bytes.fromhex(rv['hex'])
return tx_bytes, script
def fundScriptLockTx(self, tx_bytes, feerate, vkbv):
feerate_str = self.format_amount(feerate)
# TODO: unlock unspents if bid cancelled
tx_hex = tx_bytes.hex()
nonce = self.getScriptLockTxNonce(vkbv)
tx_obj = self.rpc_callback('decoderawtransaction', [tx_hex])
assert(len(tx_obj['vout']) == 1)
txo = tx_obj['vout'][0]
blinded_info = self.rpc_callback('rewindrangeproof', [txo['rangeproof'], txo['valueCommitment'], nonce.hex()])
outputs_info = {0: {'value': blinded_info['amount'], 'blind': blinded_info['blind'], 'nonce': nonce.hex()}}
options = {
'lockUnspents': True,
'feeRate': feerate_str,
}
rv = self.rpc_callback('fundrawtransactionfrom', ['blind', tx_hex, {}, outputs_info, options])
return bytes.fromhex(rv['hex'])
def createScriptLockRefundTx(self, tx_lock_bytes, script_lock, Kal, Kaf, lock1_value, csv_val, tx_fee_rate, vkbv):
lock_tx_obj = self.rpc_callback('decoderawtransaction', [tx_lock_bytes.hex()])
assert(self.getTxid(tx_lock_bytes).hex() == lock_tx_obj['txid'])
# Nonce is derived from vkbv, ephemeral_key isn't used
ephemeral_key = i2b(self.getNewSecretKey())
ephemeral_pubkey = self.getPubkey(ephemeral_key)
assert(len(ephemeral_pubkey) == 33)
nonce = self.getScriptLockTxNonce(vkbv)
output_nonce = self.getScriptLockRefundTxNonce(vkbv)
# Find the output of the lock tx to spend
spend_n, input_blinded_info = self.findOutputByNonce(lock_tx_obj, nonce)
ensure(spend_n is not None, 'Output not found in tx')
locked_coin = input_blinded_info['amount']
tx_lock_id = lock_tx_obj['txid']
refund_script = self.genScriptLockRefundTxScript(Kal, Kaf, csv_val)
p2wsh_addr = self.encodeSegwitP2WSH(getP2WSH(refund_script))
inputs = [{'txid': tx_lock_id, 'vout': spend_n, 'sequence': lock1_value, 'blindingfactor': input_blinded_info['blind']}]
outputs = [{'type': 'blind', 'amount': locked_coin, 'address': p2wsh_addr, 'nonce': output_nonce.hex(), 'data': ephemeral_pubkey.hex()}]
params = [inputs, outputs]
rv = self.rpc_callback('createrawparttransaction', params)
lock_refund_tx_hex = rv['hex']
# Set dummy witness data for fee estimation
dummy_witness_stack = self.getScriptLockTxDummyWitness(script_lock)
# Use a junk change pubkey to avoid adding unused keys to the wallet
zero_change_key = i2b(self.getNewSecretKey())
zero_change_pubkey = self.getPubkey(zero_change_key)
inputs_info = {'0': {'value': input_blinded_info['amount'], 'blind': input_blinded_info['blind'], 'witnessstack': dummy_witness_stack}}
outputs_info = rv['amounts']
options = {
'changepubkey': zero_change_pubkey.hex(),
'feeRate': self.format_amount(tx_fee_rate),
'subtractFeeFromOutputs': [0, ]
}
rv = self.rpc_callback('fundrawtransactionfrom', ['blind', lock_refund_tx_hex, inputs_info, outputs_info, options])
lock_refund_tx_hex = rv['hex']
for vout, txo in rv['output_amounts'].items():
if txo['value'] > 0:
refunded_value = txo['value']
return bytes.fromhex(lock_refund_tx_hex), refund_script, refunded_value
def createScriptLockRefundSpendTx(self, tx_lock_refund_bytes, script_lock_refund, pkh_refund_to, tx_fee_rate, vkbv):
# 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
lock_refund_tx_obj = self.rpc_callback('decoderawtransaction', [tx_lock_refund_bytes.hex()])
# Nonce is derived from vkbv
nonce = self.getScriptLockRefundTxNonce(vkbv)
# Find the output of the lock refund tx to spend
spend_n, input_blinded_info = self.findOutputByNonce(lock_refund_tx_obj, nonce)
ensure(spend_n is not None, 'Output not found in tx')
tx_lock_refund_id = lock_refund_tx_obj['txid']
addr_out = self.pkh_to_address(pkh_refund_to)
addr_info = self.rpc_callback('getaddressinfo', [addr_out])
output_pubkey_hex = addr_info['pubkey']
# Follower won't be able to decode output to check amount, shouldn't matter as fee is public and output is to leader, sum has to balance
inputs = [{'txid': tx_lock_refund_id, 'vout': spend_n, 'sequence': 0, 'blindingfactor': input_blinded_info['blind']}]
outputs = [{'type': 'blind', 'amount': input_blinded_info['amount'], 'address': addr_out, 'pubkey': output_pubkey_hex}]
params = [inputs, outputs]
rv = self.rpc_callback('createrawparttransaction', params)
lock_refund_spend_tx_hex = rv['hex']
# Set dummy witness data for fee estimation
dummy_witness_stack = self.getScriptLockRefundSpendTxDummyWitness(script_lock_refund)
# Use a junk change pubkey to avoid adding unused keys to the wallet
zero_change_key = i2b(self.getNewSecretKey())
zero_change_pubkey = self.getPubkey(zero_change_key)
inputs_info = {'0': {'value': input_blinded_info['amount'], 'blind': input_blinded_info['blind'], 'witnessstack': dummy_witness_stack}}
outputs_info = rv['amounts']
options = {
'changepubkey': zero_change_pubkey.hex(),
'feeRate': self.format_amount(tx_fee_rate),
'subtractFeeFromOutputs': [0, ]
}
rv = self.rpc_callback('fundrawtransactionfrom', ['blind', lock_refund_spend_tx_hex, inputs_info, outputs_info, options])
lock_refund_spend_tx_hex = rv['hex']
return bytes.fromhex(lock_refund_spend_tx_hex)
def verifyLockTx(self, tx_bytes, script_out,
swap_value,
Kal, Kaf,
feerate,
check_lock_tx_inputs, vkbv):
lock_tx_obj = self.rpc_callback('decoderawtransaction', [tx_bytes.hex()])
lock_txid_hex = lock_tx_obj['txid']
self._log.info('Verifying lock tx: {}.'.format(lock_txid_hex))
ensure(lock_tx_obj['version'] == self.txVersion(), 'Bad version')
ensure(lock_tx_obj['locktime'] == 0, 'Bad nLockTime')
# Find the output of the lock tx to verify
nonce = self.getScriptLockTxNonce(vkbv)
lock_output_n, blinded_info = self.findOutputByNonce(lock_tx_obj, nonce)
ensure(lock_output_n is not None, 'Output not found in tx')
# Check value
locked_txo_value = make_int(blinded_info['amount'])
ensure(locked_txo_value == swap_value, 'Bad locked value')
# Check script
lock_txo_scriptpk = bytes.fromhex(lock_tx_obj['vout'][lock_output_n]['scriptPubKey']['hex'])
script_pk = CScript([OP_0, hashlib.sha256(script_out).digest()])
ensure(lock_txo_scriptpk == script_pk, 'Bad output script')
A, B = self.extractScriptLockScriptValues(script_out)
ensure(A == Kal, 'Bad script leader pubkey')
ensure(B == Kaf, 'Bad script follower pubkey')
# TODO: Check that inputs are unspent, rangeproofs and commitments sum
# Verify fee rate
vsize = lock_tx_obj['vsize']
fee_paid = make_int(lock_tx_obj['vout'][0]['ct_fee'])
fee_rate_paid = fee_paid * 1000 // vsize
self._log.info('tx amount, vsize, feerate: %ld, %ld, %ld', locked_txo_value, vsize, fee_rate_paid)
if not self.compareFeeRates(fee_rate_paid, feerate):
self._log.warning('feerate paid doesn\'t match expected: %ld, %ld', fee_rate_paid, feerate)
# TODO: Display warning to user
return bytes.fromhex(lock_txid_hex), lock_output_n
def verifyLockRefundTx(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):
lock_refund_tx_obj = self.rpc_callback('decoderawtransaction', [tx_bytes.hex()])
lock_refund_txid_hex = lock_refund_tx_obj['txid']
self._log.info('Verifying lock refund tx: {}.'.format(lock_refund_txid_hex))
ensure(lock_refund_tx_obj['version'] == self.txVersion(), 'Bad version')
ensure(lock_refund_tx_obj['locktime'] == 0, 'Bad nLockTime')
ensure(len(lock_refund_tx_obj['vin']) == 1, 'tx doesn\'t have one input')
txin = lock_refund_tx_obj['vin'][0]
ensure(txin['sequence'] == prevout_seq, 'Bad input nSequence')
ensure(txin['scriptSig']['hex'] == '', 'Input scriptsig not empty')
ensure(txin['txid'] == prevout_id.hex() and txin['vout'] == prevout_n, 'Input prevout mismatch')
ensure(len(lock_refund_tx_obj['vout']) == 3, 'tx doesn\'t have three outputs')
# Find the output of the lock refund tx to verify
nonce = self.getScriptLockRefundTxNonce(vkbv)
lock_refund_output_n, blinded_info = self.findOutputByNonce(lock_refund_tx_obj, nonce)
ensure(lock_refund_output_n is not None, 'Output not found in tx')
lock_refund_txo_value = make_int(blinded_info['amount'])
# Check script
lock_refund_txo_scriptpk = bytes.fromhex(lock_refund_tx_obj['vout'][lock_refund_output_n]['scriptPubKey']['hex'])
script_pk = CScript([OP_0, hashlib.sha256(script_out).digest()])
ensure(lock_refund_txo_scriptpk == script_pk, 'Bad output script')
A, B, csv_val, C = self.extractScriptLockRefundScriptValues(script_out)
ensure(A == Kal, 'Bad script pubkey')
ensure(B == Kaf, 'Bad script pubkey')
ensure(csv_val == csv_val_expect, 'Bad script csv value')
ensure(C == Kaf, 'Bad script pubkey')
# Check rangeproofs and commitments sum
lock_tx_obj = self.rpc_callback('decoderawtransaction', [lock_tx_bytes.hex()])
prevout = lock_tx_obj['vout'][prevout_n]
prevtxns = [{'txid': prevout_id.hex(), 'vout': prevout_n, 'scriptPubKey': prevout['scriptPubKey']['hex'], 'amount_commitment': prevout['valueCommitment']}]
rv = self.rpc_callback('verifyrawtransaction', [tx_bytes.hex(), prevtxns])
ensure(rv['outputs_valid'] is True, 'Invalid outputs')
ensure(rv['inputs_valid'] is True, 'Invalid inputs')
# Check value
fee_paid = make_int(lock_refund_tx_obj['vout'][0]['ct_fee'])
ensure(swap_value - lock_refund_txo_value == fee_paid, 'Bad output value')
# Check fee rate
dummy_witness_stack = self.getScriptLockTxDummyWitness(prevout_script)
witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack)
vsize = self.getTxVSize(self.loadTx(tx_bytes), add_witness_bytes=witness_bytes)
fee_rate_paid = fee_paid * 1000 // vsize
self._log.info('vsize, feerate: %ld, %ld', vsize, fee_rate_paid)
ensure(self.compareFeeRates(fee_rate_paid, feerate), 'Bad fee rate, expected: {}'.format(feerate))
return bytes.fromhex(lock_refund_txid_hex), lock_refund_txo_value, lock_refund_output_n
def verifyLockRefundSpendTx(self, tx_bytes, lock_refund_tx_bytes,
lock_refund_tx_id, prevout_script,
Kal,
prevout_n, prevout_value, feerate, vkbv):
lock_refund_spend_tx_obj = self.rpc_callback('decoderawtransaction', [tx_bytes.hex()])
lock_refund_spend_txid_hex = lock_refund_spend_tx_obj['txid']
self._log.info('Verifying lock refund spend tx: {}.'.format(lock_refund_spend_txid_hex))
ensure(lock_refund_spend_tx_obj['version'] == self.txVersion(), 'Bad version')
ensure(lock_refund_spend_tx_obj['locktime'] == 0, 'Bad nLockTime')
ensure(len(lock_refund_spend_tx_obj['vin']) == 1, 'tx doesn\'t have one input')
txin = lock_refund_spend_tx_obj['vin'][0]
ensure(txin['sequence'] == 0, 'Bad input nSequence')
ensure(txin['scriptSig']['hex'] == '', 'Input scriptsig not empty')
ensure(txin['txid'] == lock_refund_tx_id.hex() and txin['vout'] == prevout_n, 'Input prevout mismatch')
ensure(len(lock_refund_spend_tx_obj['vout']) == 3, 'tx doesn\'t have three outputs')
# Leader picks output destinations
# Follower is not concerned with them as they pay to leader
# Check rangeproofs and commitments sum
lock_refund_tx_obj = self.rpc_callback('decoderawtransaction', [lock_refund_tx_bytes.hex()])
prevout = lock_refund_tx_obj['vout'][prevout_n]
prevtxns = [{'txid': lock_refund_tx_id.hex(), 'vout': prevout_n, 'scriptPubKey': prevout['scriptPubKey']['hex'], 'amount_commitment': prevout['valueCommitment']}]
rv = self.rpc_callback('verifyrawtransaction', [tx_bytes.hex(), prevtxns])
ensure(rv['outputs_valid'] is True, 'Invalid outputs')
ensure(rv['inputs_valid'] is True, 'Invalid inputs')
# Check fee rate
dummy_witness_stack = self.getScriptLockRefundSpendTxDummyWitness(prevout_script)
witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack)
vsize = self.getTxVSize(self.loadTx(tx_bytes), add_witness_bytes=witness_bytes)
fee_paid = make_int(lock_refund_spend_tx_obj['vout'][0]['ct_fee'])
fee_rate_paid = fee_paid * 1000 // vsize
ensure(self.compareFeeRates(fee_rate_paid, feerate), 'Bad fee rate, expected: {}'.format(feerate))
return True
def getLockTxSwapOutputValue(self, bid, xmr_swap):
lock_tx_obj = self.rpc_callback('decoderawtransaction', [xmr_swap.a_lock_tx.hex()])
nonce = self.getScriptLockTxNonce(xmr_swap.vkbv)
output_n, _ = self.findOutputByNonce(lock_tx_obj, nonce)
ensure(output_n is not None, 'Output not found in tx')
return bytes.fromhex(lock_tx_obj['vout'][output_n]['valueCommitment'])
def getLockRefundTxSwapOutputValue(self, bid, xmr_swap):
lock_refund_tx_obj = self.rpc_callback('decoderawtransaction', [xmr_swap.a_lock_refund_tx.hex()])
nonce = self.getScriptLockRefundTxNonce(xmr_swap.vkbv)
output_n, _ = self.findOutputByNonce(lock_refund_tx_obj, nonce)
ensure(output_n is not None, 'Output not found in tx')
return bytes.fromhex(lock_refund_tx_obj['vout'][output_n]['valueCommitment'])
def getLockRefundTxSwapOutput(self, xmr_swap):
lock_refund_tx_obj = self.rpc_callback('decoderawtransaction', [xmr_swap.a_lock_refund_tx.hex()])
nonce = self.getScriptLockRefundTxNonce(xmr_swap.vkbv)
output_n, _ = self.findOutputByNonce(lock_refund_tx_obj, nonce)
ensure(output_n is not None, 'Output not found in tx')
return output_n
def createScriptLockSpendTx(self, tx_lock_bytes, script_lock, pk_dest, tx_fee_rate, vkbv):
lock_tx_obj = self.rpc_callback('decoderawtransaction', [tx_lock_bytes.hex()])
lock_txid_hex = lock_tx_obj['txid']
ensure(lock_tx_obj['version'] == self.txVersion(), 'Bad version')
ensure(lock_tx_obj['locktime'] == 0, 'Bad nLockTime')
# Find the output of the lock tx to verify
nonce = self.getScriptLockTxNonce(vkbv)
spend_n, blinded_info = self.findOutputByNonce(lock_tx_obj, nonce)
ensure(spend_n is not None, 'Output not found in tx')
addr_out = self.pubkey_to_address(pk_dest)
inputs = [{'txid': lock_txid_hex, 'vout': spend_n, 'sequence': 0, 'blindingfactor': blinded_info['blind']}]
outputs = [{'type': 'blind', 'amount': blinded_info['amount'], 'address': addr_out, 'pubkey': pk_dest.hex()}]
params = [inputs, outputs]
rv = self.rpc_callback('createrawparttransaction', params)
lock_spend_tx_hex = rv['hex']
# Set dummy witness data for fee estimation
dummy_witness_stack = self.getScriptLockTxDummyWitness(script_lock)
# Use a junk change pubkey to avoid adding unused keys to the wallet
zero_change_key = i2b(self.getNewSecretKey())
zero_change_pubkey = self.getPubkey(zero_change_key)
inputs_info = {'0': {'value': blinded_info['amount'], 'blind': blinded_info['blind'], 'witnessstack': dummy_witness_stack}}
outputs_info = rv['amounts']
options = {
'changepubkey': zero_change_pubkey.hex(),
'feeRate': self.format_amount(tx_fee_rate),
'subtractFeeFromOutputs': [0, ]
}
rv = self.rpc_callback('fundrawtransactionfrom', ['blind', lock_spend_tx_hex, inputs_info, outputs_info, options])
lock_spend_tx_hex = rv['hex']
lock_spend_tx_obj = self.rpc_callback('decoderawtransaction', [lock_spend_tx_hex])
vsize = lock_spend_tx_obj['vsize']
pay_fee = make_int(lock_spend_tx_obj['vout'][0]['ct_fee'])
actual_tx_fee_rate = pay_fee * 1000 // vsize
self._log.info('createScriptLockSpendTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.',
lock_spend_tx_obj['txid'], actual_tx_fee_rate, vsize, pay_fee)
return bytes.fromhex(lock_spend_tx_hex)
def verifyLockSpendTx(self, tx_bytes,
lock_tx_bytes, lock_tx_script,
a_pk_f, feerate, vkbv):
lock_spend_tx_obj = self.rpc_callback('decoderawtransaction', [tx_bytes.hex()])
lock_spend_txid_hex = lock_spend_tx_obj['txid']
self._log.info('Verifying lock spend tx: {}.'.format(lock_spend_txid_hex))
ensure(lock_spend_tx_obj['version'] == self.txVersion(), 'Bad version')
ensure(lock_spend_tx_obj['locktime'] == 0, 'Bad nLockTime')
ensure(len(lock_spend_tx_obj['vin']) == 1, 'tx doesn\'t have one input')
lock_tx_obj = self.rpc_callback('decoderawtransaction', [lock_tx_bytes.hex()])
lock_txid_hex = lock_tx_obj['txid']
# Find the output of the lock tx to verify
nonce = self.getScriptLockTxNonce(vkbv)
spend_n, input_blinded_info = self.findOutputByNonce(lock_tx_obj, nonce)
ensure(spend_n is not None, 'Output not found in tx')
txin = lock_spend_tx_obj['vin'][0]
ensure(txin['sequence'] == 0, 'Bad input nSequence')
ensure(txin['scriptSig']['hex'] == '', 'Input scriptsig not empty')
ensure(txin['txid'] == lock_txid_hex and txin['vout'] == spend_n, 'Input prevout mismatch')
ensure(len(lock_spend_tx_obj['vout']) == 3, 'tx doesn\'t have three outputs')
addr_out = self.pubkey_to_address(a_pk_f)
privkey = self.rpc_callback('dumpprivkey', [addr_out])
# Find output:
output_blinded_info = None
output_n = None
for txo in lock_spend_tx_obj['vout']:
if txo['type'] != 'blind':
continue
try:
output_blinded_info = self.rpc_callback('rewindrangeproof', [txo['rangeproof'], txo['valueCommitment'], privkey, txo['data_hex']])
output_n = txo['n']
break
except Exception as e:
self._log.debug('Searching for locked output: {}'.format(str(e)))
pass
ensure(output_n is not None, 'Output not found in tx')
# Commitment
v = self.rpc_callback('verifycommitment', [lock_spend_tx_obj['vout'][output_n]['valueCommitment'], output_blinded_info['blind'], output_blinded_info['amount']])
ensure(v['result'] is True, 'verifycommitment failed')
# Check rangeproofs and commitments sum
prevout = lock_tx_obj['vout'][spend_n]
prevtxns = [{'txid': lock_txid_hex, 'vout': spend_n, 'scriptPubKey': prevout['scriptPubKey']['hex'], 'amount_commitment': prevout['valueCommitment']}]
rv = self.rpc_callback('verifyrawtransaction', [tx_bytes.hex(), prevtxns])
ensure(rv['outputs_valid'] is True, 'Invalid outputs')
ensure(rv['inputs_valid'] is True, 'Invalid inputs')
# Check amount
fee_paid = make_int(lock_spend_tx_obj['vout'][0]['ct_fee'])
amount_difference = make_int(input_blinded_info['amount']) - make_int(output_blinded_info['amount'])
ensure(fee_paid == amount_difference, 'Invalid output amount')
# Check fee
dummy_witness_stack = self.getScriptLockTxDummyWitness(lock_tx_script)
witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack)
vsize = self.getTxVSize(self.loadTx(tx_bytes), add_witness_bytes=witness_bytes)
fee_rate_paid = fee_paid * 1000 // vsize
self._log.info('vsize, feerate: %ld, %ld', vsize, fee_rate_paid)
if not self.compareFeeRates(fee_rate_paid, feerate):
raise ValueError('Bad fee rate, expected: {}'.format(feerate))
return True
def createScriptLockRefundSpendToFTx(self, tx_lock_refund_bytes, script_lock_refund, pkh_dest, tx_fee_rate, vkbv):
# lock refund swipe tx
# Sends the coinA locked coin to the follower
lock_refund_tx_obj = self.rpc_callback('decoderawtransaction', [tx_lock_refund_bytes.hex()])
nonce = self.getScriptLockRefundTxNonce(vkbv)
# Find the output of the lock refund tx to spend
spend_n, input_blinded_info = self.findOutputByNonce(lock_refund_tx_obj, nonce)
ensure(spend_n is not None, 'Output not found in tx')
tx_lock_refund_id = lock_refund_tx_obj['txid']
addr_out = self.pkh_to_address(pkh_dest)
addr_info = self.rpc_callback('getaddressinfo', [addr_out])
output_pubkey_hex = addr_info['pubkey']
A, B, lock2_value, C = self.extractScriptLockRefundScriptValues(script_lock_refund)
# Follower won't be able to decode output to check amount, shouldn't matter as fee is public and output is to leader, sum has to balance
inputs = [{'txid': tx_lock_refund_id, 'vout': spend_n, 'sequence': lock2_value, 'blindingfactor': input_blinded_info['blind']}]
outputs = [{'type': 'blind', 'amount': input_blinded_info['amount'], 'address': addr_out, 'pubkey': output_pubkey_hex}]
params = [inputs, outputs]
rv = self.rpc_callback('createrawparttransaction', params)
lock_refund_swipe_tx_hex = rv['hex']
# Set dummy witness data for fee estimation
dummy_witness_stack = self.getScriptLockRefundSwipeTxDummyWitness(script_lock_refund)
# Use a junk change pubkey to avoid adding unused keys to the wallet
zero_change_key = i2b(self.getNewSecretKey())
zero_change_pubkey = self.getPubkey(zero_change_key)
inputs_info = {'0': {'value': input_blinded_info['amount'], 'blind': input_blinded_info['blind'], 'witnessstack': dummy_witness_stack}}
outputs_info = rv['amounts']
options = {
'changepubkey': zero_change_pubkey.hex(),
'feeRate': self.format_amount(tx_fee_rate),
'subtractFeeFromOutputs': [0, ]
}
rv = self.rpc_callback('fundrawtransactionfrom', ['blind', lock_refund_swipe_tx_hex, inputs_info, outputs_info, options])
lock_refund_swipe_tx_hex = rv['hex']
return bytes.fromhex(lock_refund_swipe_tx_hex)
class PARTInterfaceAnon(PARTInterface):
@staticmethod
@ -134,7 +632,7 @@ class PARTInterfaceAnon(PARTInterface):
else:
addr_info = self.rpc_callback('getaddressinfo', [sx_addr])
if not addr_info['iswatchonly']:
wif_prefix = chainparams[self.coin_type()][self._network]['key_prefix']
wif_prefix = self.chainparams_network()['key_prefix']
wif_scan_key = toWIF(wif_prefix, kbv)
self.rpc_callback('importstealthaddress', [wif_scan_key, Kbs.hex()])
self._log.info('Imported watch-only sx_addr: {}'.format(sx_addr))
@ -166,7 +664,7 @@ class PARTInterfaceAnon(PARTInterface):
sx_addr = self.formatStealthAddress(Kbv, Kbs)
addr_info = self.rpc_callback('getaddressinfo', [sx_addr])
if not addr_info['ismine']:
wif_prefix = chainparams[self.coin_type()][self._network]['key_prefix']
wif_prefix = self.chainparams_network()['key_prefix']
wif_scan_key = toWIF(wif_prefix, kbv)
wif_spend_key = toWIF(wif_prefix, kbs)
self.rpc_callback('importstealthaddress', [wif_scan_key, wif_spend_key])

View file

@ -26,8 +26,7 @@ from .util import (
ensure,
dumpj,
make_int,
format_amount)
from .basicswap_util import (
format_amount,
TemporaryError)
from .rpc_xmr import (
make_xmr_rpc_func,

View file

@ -22,9 +22,13 @@ decimal_ctx = decimal.Context()
decimal_ctx.prec = 20
def assert_cond(v, err='Bad opcode'):
class TemporaryError(ValueError):
pass
def ensure(v, err_string):
if not v:
raise ValueError(err)
raise ValueError(err_string)
def toBool(s) -> bool:
@ -222,6 +226,10 @@ def getCompactSizeLen(v):
raise ValueError('Value too large')
def getWitnessElementLen(v):
return getCompactSizeLen(v) + v
def SerialiseNumCompact(v):
if v < 253:
return bytes((v,))
@ -339,8 +347,3 @@ def encodeStealthAddress(prefix_byte, scan_pubkey, spend_pubkey):
b = bytes((prefix_byte,)) + data
b += hashlib.sha256(hashlib.sha256(b).digest()).digest()[:4]
return b58encode(b)
def ensure(passed, err_string):
if not passed:
raise ValueError(err_string)

View file

@ -1,5 +1,36 @@
0.0.25
==============
- Fix extra 33 bytes in lock spend fee calculation.
- XMR swaps use watchonly addresses to save the lock tx to the wallet
- Instead of scantxoutset
- Add missing check of leader's lock refund tx signature result
- Blind part -> XMR swaps are possible:
- The sha256 hash of the chain b view private key is used as the nonce for transactions requiring cooperation to sign.
- Follower sends a public key in xmr_swap.dest_af.
- Verify the rangeproofs and commitments of blinded pre-shared txns.
- Add explicit tests for all paths of:
- PARTct -> XMR
- BTC -> XMR
- LTC -> XMR
0.0.24
==============
- Can swap Particl Anon outputs in place of XMR
0.0.23
==============
- Enables private offers
0.0.22
==============
- Improved wallets page
- Consistent wallet order
- Separated RPC calls into threads.
@ -7,6 +38,7 @@
0.0.21
==============
- Raised Particl and Monero daemon versions.
- Display shared address on bid page if show more info is enabled.
- Added View Lock Wallet Transfers button to bid page.
@ -14,5 +46,6 @@
0.0.6
==============
- Experimental support for XMR swaps.
- Single direction only, scriptless -> XMR

View file

@ -29,6 +29,10 @@ BTC_BASE_PORT = 31792
BTC_BASE_RPC_PORT = 32792
BTC_BASE_ZMQ_PORT = 33792
LTC_BASE_PORT = 34792
LTC_BASE_RPC_PORT = 35792
LTC_BASE_ZMQ_PORT = 36792
PREFIX_SECRET_KEY_REGTEST = 0x2e

View file

@ -0,0 +1,229 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (c) 2021 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import json
import random
import logging
import unittest
from urllib.request import urlopen
from basicswap.basicswap import (
Coins,
SwapTypes,
BidStates,
DebugTypes,
)
from basicswap.basicswap_util import (
SEQUENCE_LOCK_BLOCKS,
)
from basicswap.util import (
make_int,
format_amount,
)
from tests.basicswap.common import (
wait_for_bid,
wait_for_offer,
wait_for_none_active,
)
from .test_xmr import BaseTest, test_delay_event
logger = logging.getLogger()
class Test(BaseTest):
__test__ = True
@classmethod
def setUpClass(cls):
if not hasattr(cls, 'test_coin_from'):
cls.test_coin_from = Coins.BTC
if not hasattr(cls, 'start_ltc_nodes'):
cls.start_ltc_nodes = False
super(Test, cls).setUpClass()
@classmethod
def tearDownClass(cls):
logging.info('Finalising BTC Test')
super(Test, cls).tearDownClass()
def getBalance(self, js_wallets):
return float(js_wallets[str(int(self.test_coin_from))]['balance']) + float(js_wallets[str(int(self.test_coin_from))]['unconfirmed'])
def getXmrBalance(self, js_wallets):
return float(js_wallets[str(int(Coins.XMR))]['unconfirmed']) + float(js_wallets[str(int(Coins.XMR))]['balance'])
def test_01_full_swap(self):
logging.info('---------- Test {} to XMR'.format(str(self.test_coin_from)))
swap_clients = self.swap_clients
js_0 = json.loads(urlopen('http://127.0.0.1:1800/json/wallets').read())
node0_from_before = self.getBalance(js_0)
js_1 = json.loads(urlopen('http://127.0.0.1:1801/json/wallets').read())
node1_from_before = self.getBalance(js_1)
js_0_xmr = json.loads(urlopen('http://127.0.0.1:1800/json/wallets/xmr').read())
js_1_xmr = json.loads(urlopen('http://127.0.0.1:1801/json/wallets/xmr').read())
amt_swap = make_int(random.uniform(0.1, 2.0), scale=8, r=1)
rate_swap = make_int(random.uniform(0.2, 20.0), scale=12, r=1)
offer_id = swap_clients[0].postOffer(self.test_coin_from, Coins.XMR, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP)
wait_for_offer(test_delay_event, swap_clients[1], offer_id)
offers = swap_clients[0].listOffers(filters={'offer_id': offer_id})
offer = offers[0]
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)
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)
amount_from = float(format_amount(amt_swap, 8))
js_1 = json.loads(urlopen('http://127.0.0.1:1801/json/wallets').read())
node1_from_after = self.getBalance(js_1)
assert(node1_from_after > node1_from_before + (amount_from - 0.05))
js_0 = json.loads(urlopen('http://127.0.0.1:1800/json/wallets').read())
node0_from_after = self.getBalance(js_0)
# TODO: Discard block rewards
# assert(node0_from_after < node0_from_before - amount_from)
js_0_xmr_after = json.loads(urlopen('http://127.0.0.1:1800/json/wallets/xmr').read())
js_1_xmr_after = json.loads(urlopen('http://127.0.0.1:1801/json/wallets/xmr').read())
scale_from = 8
amount_to = int((amt_swap * rate_swap) // (10 ** scale_from))
amount_to_float = float(format_amount(amount_to, 12))
node1_xmr_after = float(js_1_xmr_after['unconfirmed']) + float(js_1_xmr_after['balance'])
node1_xmr_before = float(js_1_xmr['unconfirmed']) + float(js_1_xmr['balance'])
assert(node1_xmr_after > node1_xmr_before + (amount_to_float - 0.02))
def test_02_leader_recover_a_lock_tx(self):
logging.info('---------- Test {} to XMR leader recovers coin a lock tx'.format(str(self.test_coin_from)))
swap_clients = self.swap_clients
js_w0_before = json.loads(urlopen('http://127.0.0.1:1800/json/wallets').read())
node0_from_before = self.getBalance(js_w0_before)
amt_swap = make_int(random.uniform(0.1, 2.0), scale=8, r=1)
rate_swap = make_int(random.uniform(0.2, 20.0), scale=12, r=1)
offer_id = swap_clients[0].postOffer(
self.test_coin_from, Coins.XMR, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP,
lock_type=SEQUENCE_LOCK_BLOCKS, lock_value=12)
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_STOP_AFTER_COIN_A_LOCK)
swap_clients[0].acceptXmrBid(bid_id)
wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, wait_for=180)
wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, sent=True)
js_w0_after = json.loads(urlopen('http://127.0.0.1:1800/json/wallets').read())
node0_from_after = self.getBalance(js_w0_after)
# TODO: Discard block rewards
# assert(node0_from_before - node0_from_after < 0.02)
def test_03_follower_recover_a_lock_tx(self):
logging.info('---------- Test {} to XMR follower recovers coin a lock tx'.format(str(self.test_coin_from)))
swap_clients = self.swap_clients
js_w0_before = json.loads(urlopen('http://127.0.0.1:1800/json/wallets').read())
js_w1_before = json.loads(urlopen('http://127.0.0.1:1801/json/wallets').read())
amt_swap = make_int(random.uniform(0.1, 2.0), scale=8, r=1)
rate_swap = make_int(random.uniform(0.2, 20.0), scale=12, r=1)
offer_id = swap_clients[0].postOffer(
self.test_coin_from, Coins.XMR, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP,
lock_type=SEQUENCE_LOCK_BLOCKS, lock_value=12)
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_STOP_AFTER_COIN_A_LOCK)
swap_clients[0].setBidDebugInd(bid_id, DebugTypes.BID_DONT_SPEND_COIN_A_LOCK_REFUND)
swap_clients[0].acceptXmrBid(bid_id)
wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.BID_STALLED_FOR_TEST, wait_for=180)
wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.XMR_SWAP_FAILED_SWIPED, wait_for=80, sent=True)
js_w1_after = json.loads(urlopen('http://127.0.0.1:1801/json/wallets').read())
node1_from_before = self.getBalance(js_w1_before)
node1_from_after = self.getBalance(js_w1_after)
amount_from = float(format_amount(amt_swap, 8))
# TODO: Discard block rewards
# assert(node1_from_after - node1_from_before > (amount_from - 0.02))
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 {} to XMR follower recovers coin b lock tx'.format(str(self.test_coin_from)))
swap_clients = self.swap_clients
js_w0_before = json.loads(urlopen('http://127.0.0.1:1800/json/wallets').read())
js_w1_before = json.loads(urlopen('http://127.0.0.1:1801/json/wallets').read())
amt_swap = make_int(random.uniform(0.1, 2.0), scale=8, r=1)
rate_swap = make_int(random.uniform(0.2, 20.0), scale=12, r=1)
offer_id = swap_clients[0].postOffer(
self.test_coin_from, Coins.XMR, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP,
lock_type=SEQUENCE_LOCK_BLOCKS, lock_value=18)
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.CREATE_INVALID_COIN_B_LOCK)
swap_clients[0].acceptXmrBid(bid_id)
wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, wait_for=180)
wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, sent=True)
js_w0_after = json.loads(urlopen('http://127.0.0.1:1800/json/wallets').read())
js_w1_after = json.loads(urlopen('http://127.0.0.1:1801/json/wallets').read())
node0_from_before = self.getBalance(js_w0_before)
node0_from_after = self.getBalance(js_w0_after)
# TODO: Discard block rewards
# assert(node0_from_before - node0_from_after < 0.02)
node1_xmr_before = self.getXmrBalance(js_w1_before)
node1_xmr_after = self.getXmrBalance(js_w1_after)
assert(node1_xmr_before - node1_xmr_after < 0.02)
if __name__ == '__main__':
unittest.main()

View file

@ -0,0 +1,249 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (c) 2021 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import json
import random
import logging
import unittest
from urllib.request import urlopen
from basicswap.basicswap import (
Coins,
SwapTypes,
BidStates,
DebugTypes,
)
from basicswap.basicswap_util import (
SEQUENCE_LOCK_BLOCKS,
)
from basicswap.util import (
make_int,
format_amount,
)
from tests.basicswap.common import (
wait_for_bid,
wait_for_offer,
wait_for_none_active,
)
'''
# Should work?
from .test_btc_xmr import Test, test_delay_event
logger = logging.getLogger()
class TestLTC(Test):
__test__ = True
@classmethod
def setUpClass(cls):
cls.test_coin_from = Coins.LTC
cls.start_ltc_nodes = True
super(TestLTC, cls).setUpClass()
@classmethod
def tearDownClass(cls):
logging.info('Finalising LTC Test')
super(TestLTC, cls).tearDownClass()
'''
from .test_xmr import BaseTest, test_delay_event
logger = logging.getLogger()
class Test(BaseTest):
__test__ = True
@classmethod
def setUpClass(cls):
cls.test_coin_from = Coins.LTC
cls.start_ltc_nodes = True
super(Test, cls).setUpClass()
@classmethod
def tearDownClass(cls):
logging.info('Finalising LTC Test')
super(Test, cls).tearDownClass()
def getBalance(self, js_wallets):
return float(js_wallets[str(int(self.test_coin_from))]['balance']) + float(js_wallets[str(int(self.test_coin_from))]['unconfirmed'])
def getXmrBalance(self, js_wallets):
return float(js_wallets[str(int(Coins.XMR))]['unconfirmed']) + float(js_wallets[str(int(Coins.XMR))]['balance'])
def test_01_full_swap(self):
logging.info('---------- Test {} to XMR'.format(str(self.test_coin_from)))
swap_clients = self.swap_clients
js_0 = json.loads(urlopen('http://127.0.0.1:1800/json/wallets').read())
node0_from_before = self.getBalance(js_0)
js_1 = json.loads(urlopen('http://127.0.0.1:1801/json/wallets').read())
node1_from_before = self.getBalance(js_1)
js_0_xmr = json.loads(urlopen('http://127.0.0.1:1800/json/wallets/xmr').read())
js_1_xmr = json.loads(urlopen('http://127.0.0.1:1801/json/wallets/xmr').read())
amt_swap = make_int(random.uniform(0.1, 2.0), scale=8, r=1)
rate_swap = make_int(random.uniform(0.2, 20.0), scale=12, r=1)
offer_id = swap_clients[0].postOffer(self.test_coin_from, Coins.XMR, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP)
wait_for_offer(test_delay_event, swap_clients[1], offer_id)
offers = swap_clients[0].listOffers(filters={'offer_id': offer_id})
offer = offers[0]
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)
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)
amount_from = float(format_amount(amt_swap, 8))
js_1 = json.loads(urlopen('http://127.0.0.1:1801/json/wallets').read())
node1_from_after = self.getBalance(js_1)
assert(node1_from_after > node1_from_before + (amount_from - 0.05))
js_0 = json.loads(urlopen('http://127.0.0.1:1800/json/wallets').read())
node0_from_after = self.getBalance(js_0)
# TODO: Discard block rewards
# assert(node0_from_after < node0_from_before - amount_from)
js_0_xmr_after = json.loads(urlopen('http://127.0.0.1:1800/json/wallets/xmr').read())
js_1_xmr_after = json.loads(urlopen('http://127.0.0.1:1801/json/wallets/xmr').read())
scale_from = 8
amount_to = int((amt_swap * rate_swap) // (10 ** scale_from))
amount_to_float = float(format_amount(amount_to, 12))
node1_xmr_after = float(js_1_xmr_after['unconfirmed']) + float(js_1_xmr_after['balance'])
node1_xmr_before = float(js_1_xmr['unconfirmed']) + float(js_1_xmr['balance'])
assert(node1_xmr_after > node1_xmr_before + (amount_to_float - 0.02))
def test_02_leader_recover_a_lock_tx(self):
logging.info('---------- Test {} to XMR leader recovers coin a lock tx'.format(str(self.test_coin_from)))
swap_clients = self.swap_clients
js_w0_before = json.loads(urlopen('http://127.0.0.1:1800/json/wallets').read())
node0_from_before = self.getBalance(js_w0_before)
amt_swap = make_int(random.uniform(0.1, 2.0), scale=8, r=1)
rate_swap = make_int(random.uniform(0.2, 20.0), scale=12, r=1)
offer_id = swap_clients[0].postOffer(
self.test_coin_from, Coins.XMR, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP,
lock_type=SEQUENCE_LOCK_BLOCKS, lock_value=12)
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_STOP_AFTER_COIN_A_LOCK)
swap_clients[0].acceptXmrBid(bid_id)
wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, wait_for=180)
wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, sent=True)
js_w0_after = json.loads(urlopen('http://127.0.0.1:1800/json/wallets').read())
node0_from_after = self.getBalance(js_w0_after)
# TODO: Discard block rewards
# assert(node0_from_before - node0_from_after < 0.02)
def test_03_follower_recover_a_lock_tx(self):
logging.info('---------- Test {} to XMR follower recovers coin a lock tx'.format(str(self.test_coin_from)))
swap_clients = self.swap_clients
js_w0_before = json.loads(urlopen('http://127.0.0.1:1800/json/wallets').read())
js_w1_before = json.loads(urlopen('http://127.0.0.1:1801/json/wallets').read())
amt_swap = make_int(random.uniform(0.1, 2.0), scale=8, r=1)
rate_swap = make_int(random.uniform(0.2, 20.0), scale=12, r=1)
offer_id = swap_clients[0].postOffer(
self.test_coin_from, Coins.XMR, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP,
lock_type=SEQUENCE_LOCK_BLOCKS, lock_value=12)
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_STOP_AFTER_COIN_A_LOCK)
swap_clients[0].setBidDebugInd(bid_id, DebugTypes.BID_DONT_SPEND_COIN_A_LOCK_REFUND)
swap_clients[0].acceptXmrBid(bid_id)
wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.BID_STALLED_FOR_TEST, wait_for=180)
wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.XMR_SWAP_FAILED_SWIPED, wait_for=80, sent=True)
js_w1_after = json.loads(urlopen('http://127.0.0.1:1801/json/wallets').read())
node1_from_before = self.getBalance(js_w1_before)
node1_from_after = self.getBalance(js_w1_after)
amount_from = float(format_amount(amt_swap, 8))
# TODO: Discard block rewards
# assert(node1_from_after - node1_from_before > (amount_from - 0.02))
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 {} to XMR follower recovers coin b lock tx'.format(str(self.test_coin_from)))
swap_clients = self.swap_clients
js_w0_before = json.loads(urlopen('http://127.0.0.1:1800/json/wallets').read())
js_w1_before = json.loads(urlopen('http://127.0.0.1:1801/json/wallets').read())
amt_swap = make_int(random.uniform(0.1, 2.0), scale=8, r=1)
rate_swap = make_int(random.uniform(0.2, 20.0), scale=12, r=1)
offer_id = swap_clients[0].postOffer(
self.test_coin_from, Coins.XMR, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP,
lock_type=SEQUENCE_LOCK_BLOCKS, lock_value=18)
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.CREATE_INVALID_COIN_B_LOCK)
swap_clients[0].acceptXmrBid(bid_id)
wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, wait_for=180)
wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, sent=True)
js_w0_after = json.loads(urlopen('http://127.0.0.1:1800/json/wallets').read())
js_w1_after = json.loads(urlopen('http://127.0.0.1:1801/json/wallets').read())
node0_from_before = self.getBalance(js_w0_before)
node0_from_after = self.getBalance(js_w0_after)
# TODO: Discard block rewards
# assert(node0_from_before - node0_from_after < 0.02)
node1_xmr_before = self.getXmrBalance(js_w1_before)
node1_xmr_after = self.getXmrBalance(js_w1_after)
assert(node1_xmr_before - node1_xmr_after < 0.02)
if __name__ == '__main__':
unittest.main()

View file

@ -0,0 +1,235 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (c) 2021 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import json
import random
import logging
import unittest
from urllib.request import urlopen
from basicswap.basicswap import (
Coins,
SwapTypes,
BidStates,
DebugTypes,
)
from basicswap.basicswap_util import (
SEQUENCE_LOCK_BLOCKS,
)
from basicswap.util import (
make_int,
format_amount,
)
from tests.basicswap.common import (
wait_for_bid,
wait_for_offer,
wait_for_none_active,
wait_for_balance,
post_json_req,
)
from .test_xmr import BaseTest, test_delay_event
logger = logging.getLogger()
class Test(BaseTest):
__test__ = True
@classmethod
def setUpClass(cls):
super(Test, cls).setUpClass()
js_0 = json.loads(urlopen('http://127.0.0.1:1800/json/wallets/part').read())
node0_blind_before = js_0['blind_balance'] + js_0['blind_unconfirmed']
post_json = {
'value': 100,
'address': js_0['stealth_address'],
'subfee': False,
'type_to': 'blind',
}
json_rv = json.loads(post_json_req('http://127.0.0.1:1800/json/wallets/part/withdraw', post_json))
assert(len(json_rv['txid']) == 64)
logging.info('Waiting for blind balance')
wait_for_balance(test_delay_event, 'http://127.0.0.1:1800/json/wallets/part', 'blind_balance', 100.0 + node0_blind_before)
js_0 = json.loads(urlopen('http://127.0.0.1:1800/json/wallets/part').read())
node0_blind_before = js_0['blind_balance'] + js_0['blind_unconfirmed']
def getBalance(self, js_wallets):
return float(js_wallets[str(int(Coins.PART))]['blind_balance']) + float(js_wallets[str(int(Coins.PART))]['blind_unconfirmed'])
def getXmrBalance(self, js_wallets):
return float(js_wallets[str(int(Coins.XMR))]['unconfirmed']) + float(js_wallets[str(int(Coins.XMR))]['balance'])
def test_01_part_xmr(self):
logging.info('---------- Test PARTct to XMR')
swap_clients = self.swap_clients
js_0 = json.loads(urlopen('http://127.0.0.1:1800/json/wallets/part').read())
assert(float(js_0['blind_balance']) > 10.0)
node0_blind_before = js_0['blind_balance'] + js_0['blind_unconfirmed']
js_1 = json.loads(urlopen('http://127.0.0.1:1801/json/wallets/part').read())
node1_blind_before = js_1['blind_balance'] + js_1['blind_unconfirmed']
js_0_xmr = json.loads(urlopen('http://127.0.0.1:1800/json/wallets/xmr').read())
js_1_xmr = json.loads(urlopen('http://127.0.0.1:1801/json/wallets/xmr').read())
amt_swap = make_int(random.uniform(0.1, 2.0), scale=8, r=1)
rate_swap = make_int(random.uniform(0.2, 20.0), scale=12, r=1)
offer_id = swap_clients[0].postOffer(Coins.PART_BLIND, Coins.XMR, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP)
wait_for_offer(test_delay_event, swap_clients[1], offer_id)
offers = swap_clients[0].listOffers(filters={'offer_id': offer_id})
offer = offers[0]
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)
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)
amount_from = float(format_amount(amt_swap, 8))
js_1 = json.loads(urlopen('http://127.0.0.1:1801/json/wallets/part').read())
node1_blind_after = js_1['blind_balance'] + js_1['blind_unconfirmed']
assert(node1_blind_after > node1_blind_before + (amount_from - 0.05))
js_0 = json.loads(urlopen('http://127.0.0.1:1800/json/wallets/part').read())
node0_blind_after = js_0['blind_balance'] + js_0['blind_unconfirmed']
assert(node0_blind_after < node0_blind_before - amount_from)
js_0_xmr_after = json.loads(urlopen('http://127.0.0.1:1800/json/wallets/xmr').read())
js_1_xmr_after = json.loads(urlopen('http://127.0.0.1:1801/json/wallets/xmr').read())
scale_from = 8
amount_to = int((amt_swap * rate_swap) // (10 ** scale_from))
amount_to_float = float(format_amount(amount_to, 12))
node1_xmr_after = float(js_1_xmr_after['unconfirmed']) + float(js_1_xmr_after['balance'])
node1_xmr_before = float(js_1_xmr['unconfirmed']) + float(js_1_xmr['balance'])
assert(node1_xmr_after > node1_xmr_before + (amount_to_float - 0.02))
def test_02_leader_recover_a_lock_tx(self):
logging.info('---------- Test PARTct to XMR leader recovers coin a lock tx')
swap_clients = self.swap_clients
js_w0_before = json.loads(urlopen('http://127.0.0.1:1800/json/wallets').read())
node0_blind_before = self.getBalance(js_w0_before)
amt_swap = make_int(random.uniform(0.1, 2.0), scale=8, r=1)
rate_swap = make_int(random.uniform(0.2, 20.0), scale=12, r=1)
offer_id = swap_clients[0].postOffer(
Coins.PART_BLIND, Coins.XMR, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP,
lock_type=SEQUENCE_LOCK_BLOCKS, lock_value=12)
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_STOP_AFTER_COIN_A_LOCK)
swap_clients[0].acceptXmrBid(bid_id)
wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, wait_for=180)
wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, sent=True)
js_w0_after = json.loads(urlopen('http://127.0.0.1:1800/json/wallets').read())
node0_blind_after = self.getBalance(js_w0_after)
assert(node0_blind_before - node0_blind_after < 0.02)
def test_03_follower_recover_a_lock_tx(self):
logging.info('---------- Test PARTct to XMR follower recovers coin a lock tx')
swap_clients = self.swap_clients
js_w0_before = json.loads(urlopen('http://127.0.0.1:1800/json/wallets').read())
js_w1_before = json.loads(urlopen('http://127.0.0.1:1801/json/wallets').read())
amt_swap = make_int(random.uniform(0.1, 2.0), scale=8, r=1)
rate_swap = make_int(random.uniform(0.2, 20.0), scale=12, r=1)
offer_id = swap_clients[0].postOffer(
Coins.PART_BLIND, Coins.XMR, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP,
lock_type=SEQUENCE_LOCK_BLOCKS, lock_value=12)
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_STOP_AFTER_COIN_A_LOCK)
swap_clients[0].setBidDebugInd(bid_id, DebugTypes.BID_DONT_SPEND_COIN_A_LOCK_REFUND)
swap_clients[0].acceptXmrBid(bid_id)
wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.BID_STALLED_FOR_TEST, wait_for=180)
wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.XMR_SWAP_FAILED_SWIPED, wait_for=80, sent=True)
js_w1_after = json.loads(urlopen('http://127.0.0.1:1801/json/wallets').read())
node1_blind_before = self.getBalance(js_w1_before)
node1_blind_after = self.getBalance(js_w1_after)
amount_from = float(format_amount(amt_swap, 8))
assert(node1_blind_after - node1_blind_before > (amount_from - 0.02))
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 PARTct to XMR follower recovers coin b lock tx')
swap_clients = self.swap_clients
js_w0_before = json.loads(urlopen('http://127.0.0.1:1800/json/wallets').read())
js_w1_before = json.loads(urlopen('http://127.0.0.1:1801/json/wallets').read())
amt_swap = make_int(random.uniform(0.1, 2.0), scale=8, r=1)
rate_swap = make_int(random.uniform(0.2, 20.0), scale=12, r=1)
offer_id = swap_clients[0].postOffer(
Coins.PART_BLIND, Coins.XMR, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP,
lock_type=SEQUENCE_LOCK_BLOCKS, lock_value=18)
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.CREATE_INVALID_COIN_B_LOCK)
swap_clients[0].acceptXmrBid(bid_id)
wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, wait_for=180)
wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, sent=True)
js_w0_after = json.loads(urlopen('http://127.0.0.1:1800/json/wallets').read())
js_w1_after = json.loads(urlopen('http://127.0.0.1:1801/json/wallets').read())
node0_blind_before = self.getBalance(js_w0_before)
node0_blind_after = self.getBalance(js_w0_after)
assert(node0_blind_before - node0_blind_after < 0.02)
node1_xmr_before = self.getXmrBalance(js_w1_before)
node1_xmr_after = self.getXmrBalance(js_w1_after)
assert(node1_xmr_before - node1_xmr_after < 0.02)
if __name__ == '__main__':
unittest.main()

View file

@ -70,6 +70,8 @@ from tests.basicswap.common import (
BASE_ZMQ_PORT,
BTC_BASE_PORT,
BTC_BASE_RPC_PORT,
LTC_BASE_PORT,
LTC_BASE_RPC_PORT,
PREFIX_SECRET_KEY_REGTEST,
)
from bin.basicswap_run import startDaemon, startXmrDaemon
@ -80,6 +82,7 @@ logger = logging.getLogger()
NUM_NODES = 3
NUM_XMR_NODES = 3
NUM_BTC_NODES = 3
NUM_LTC_NODES = 3
TEST_DIR = cfg.TEST_DATADIRS
XMR_BASE_P2P_PORT = 17792
@ -139,7 +142,7 @@ def startXmrWalletRPC(node_dir, bin_dir, wallet_bin, node_id, opts=[]):
return subprocess.Popen(args, stdin=subprocess.PIPE, stdout=wallet_stdout, stderr=wallet_stderr, cwd=data_dir)
def prepare_swapclient_dir(datadir, node_id, network_key, network_pubkey):
def prepare_swapclient_dir(datadir, node_id, network_key, network_pubkey, with_ltc=False):
basicswap_dir = os.path.join(datadir, 'basicswap_' + str(node_id))
if not os.path.exists(basicswap_dir):
os.makedirs(basicswap_dir)
@ -198,6 +201,18 @@ def prepare_swapclient_dir(datadir, node_id, network_key, network_pubkey):
'max_delay_retry': 10
}
if with_ltc:
settings['chainclients']['litecoin'] = {
'connection_type': 'rpc',
'manage_daemon': False,
'rpcport': LTC_BASE_RPC_PORT + node_id,
'rpcuser': 'test' + str(node_id),
'rpcpassword': 'test_pass' + str(node_id),
'datadir': os.path.join(datadir, 'ltc_' + str(node_id)),
'bindir': cfg.LITECOIN_BINDIR,
'use_segwit': True,
}
with open(settings_path, 'w') as fp:
json.dump(settings, fp, indent=4)
@ -206,6 +221,10 @@ def btcRpc(cmd, node_id=0):
return callrpc_cli(cfg.BITCOIN_BINDIR, os.path.join(TEST_DIR, 'btc_' + str(node_id)), 'regtest', cmd, cfg.BITCOIN_CLI)
def ltcRpc(cmd, node_id=0):
return callrpc_cli(cfg.LITECOIN_BINDIR, os.path.join(TEST_DIR, 'ltc_' + str(node_id)), 'regtest', cmd, cfg.LITECOIN_CLI)
def signal_handler(sig, frame):
logging.info('signal {} detected.'.format(sig))
test_delay_event.set()
@ -249,6 +268,8 @@ def run_coins_loop(cls):
try:
if cls.btc_addr is not None:
btcRpc('generatetoaddress 1 {}'.format(cls.btc_addr))
if cls.ltc_addr is not None:
ltcRpc('generatetoaddress 1 {}'.format(cls.ltc_addr))
if cls.xmr_addr is not None:
callrpc_xmr_na(XMR_BASE_RPC_PORT + 1, 'generateblocks', {'wallet_address': cls.xmr_addr, 'amount_of_blocks': 1})
except Exception as e:
@ -263,12 +284,13 @@ def run_loop(cls):
test_delay_event.wait(1.0)
class Test(unittest.TestCase):
class BaseTest(unittest.TestCase):
__test__ = False
@classmethod
def setUpClass(cls):
super(Test, cls).setUpClass()
if not hasattr(cls, 'start_ltc_nodes'):
cls.start_ltc_nodes = False
random.seed(time.time())
cls.update_thread = None
@ -277,11 +299,13 @@ class Test(unittest.TestCase):
cls.swap_clients = []
cls.part_daemons = []
cls.btc_daemons = []
cls.ltc_daemons = []
cls.xmr_daemons = []
cls.xmr_wallet_auth = []
cls.xmr_addr = None
cls.btc_addr = None
cls.ltc_addr = None
logger.propagate = False
logger.handlers = []
@ -336,6 +360,17 @@ class Test(unittest.TestCase):
waitForRPC(make_rpc_func(i, base_rpc_port=BTC_BASE_RPC_PORT))
if cls.start_ltc_nodes:
for i in range(NUM_LTC_NODES):
data_dir = prepareDataDir(TEST_DIR, i, 'litecoin.conf', 'ltc_', base_p2p_port=LTC_BASE_PORT, base_rpc_port=LTC_BASE_RPC_PORT)
if os.path.exists(os.path.join(cfg.LITECOIN_BINDIR, 'litecoin-wallet')):
callrpc_cli(cfg.LITECOIN_BINDIR, data_dir, 'regtest', '-wallet=wallet.dat create', 'litecoin-wallet')
cls.ltc_daemons.append(startDaemon(os.path.join(TEST_DIR, 'ltc_' + str(i)), cfg.LITECOIN_BINDIR, cfg.LITECOIND))
logging.info('Started %s %d', cfg.LITECOIND, cls.part_daemons[-1].pid)
waitForRPC(make_rpc_func(i, base_rpc_port=LTC_BASE_RPC_PORT))
for i in range(NUM_XMR_NODES):
prepareXmrDataDir(TEST_DIR, i, 'monerod.conf')
@ -361,7 +396,7 @@ class Test(unittest.TestCase):
cls.network_pubkey = eckey.get_pubkey().get_bytes().hex()
for i in range(NUM_NODES):
prepare_swapclient_dir(TEST_DIR, i, cls.network_key, cls.network_pubkey)
prepare_swapclient_dir(TEST_DIR, i, cls.network_key, cls.network_pubkey, cls.start_ltc_nodes)
basicswap_dir = os.path.join(os.path.join(TEST_DIR, 'basicswap_' + str(i)))
settings_path = os.path.join(basicswap_dir, cfg.CONFIG_FILENAME)
with open(settings_path) as fs:
@ -370,6 +405,10 @@ class Test(unittest.TestCase):
sc = BasicSwap(fp, basicswap_dir, settings, 'regtest', log_name='BasicSwap{}'.format(i))
sc.setDaemonPID(Coins.BTC, cls.btc_daemons[i].pid)
sc.setDaemonPID(Coins.PART, cls.part_daemons[i].pid)
if cls.start_ltc_nodes:
sc.setDaemonPID(Coins.LTC, cls.ltc_daemons[i].pid)
sc.start()
# Set XMR main wallet address
xmr_ci = sc.ci(Coins.XMR)
@ -389,6 +428,12 @@ class Test(unittest.TestCase):
checkForks(callnoderpc(0, 'getblockchaininfo', base_rpc_port=BTC_BASE_RPC_PORT))
if cls.start_ltc_nodes:
cls.ltc_addr = callnoderpc(0, 'getnewaddress', ['mining_addr', 'bech32'], base_rpc_port=LTC_BASE_RPC_PORT)
logging.info('Mining %d Litecoin blocks to %s', num_blocks, cls.ltc_addr)
callnoderpc(0, 'generatetoaddress', [num_blocks, cls.ltc_addr], base_rpc_port=LTC_BASE_RPC_PORT)
checkForks(callnoderpc(0, 'getblockchaininfo', base_rpc_port=LTC_BASE_RPC_PORT))
num_blocks = 100
if callrpc_xmr_na(XMR_BASE_RPC_PORT + 1, 'get_block_count')['count'] < num_blocks:
logging.info('Mining %d Monero blocks to %s.', num_blocks, cls.xmr_addr)
@ -441,12 +486,17 @@ class Test(unittest.TestCase):
stopDaemons(cls.xmr_daemons)
stopDaemons(cls.part_daemons)
stopDaemons(cls.btc_daemons)
stopDaemons(cls.ltc_daemons)
super(Test, cls).tearDownClass()
super(BaseTest, cls).tearDownClass()
def callxmrnodewallet(self, node_id, method, params=None):
return callrpc_xmr(XMR_BASE_WALLET_RPC_PORT + node_id, self.xmr_wallet_auth[node_id], method, params)
class Test(BaseTest):
__test__ = True
def test_01_part_xmr(self):
logging.info('---------- Test PART to XMR')
swap_clients = self.swap_clients
@ -601,8 +651,6 @@ class Test(unittest.TestCase):
wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, sent=True)
js_w0_after = json.loads(urlopen('http://127.0.0.1:1800/json/wallets').read())
print('[rm] js_w0_before', json.dumps(js_w0_before))
print('[rm] js_w0_after', json.dumps(js_w0_after))
def test_03_follower_recover_a_lock_tx(self):
logging.info('---------- Test PART to XMR follower recovers coin a lock tx')
@ -740,9 +788,6 @@ class Test(unittest.TestCase):
js_w0_after = json.loads(urlopen('http://127.0.0.1:1800/json/wallets').read())
js_w1_after = json.loads(urlopen('http://127.0.0.1:1801/json/wallets').read())
logging.info('[rm] js_w0_after {}'.format(json.dumps(js_w0_after, indent=4)))
logging.info('[rm] js_w1_after {}'.format(json.dumps(js_w1_after, indent=4)))
assert(make_int(js_w1_after['2']['balance'], scale=8, r=1) - (make_int(js_w1_before['2']['balance'], scale=8, r=1) + amt_1) < 1000)
def test_07_revoke_offer(self):
@ -875,10 +920,58 @@ class Test(unittest.TestCase):
assert(js_1['anon_balance'] < node1_anon_before - amount_to)
js_0 = json.loads(urlopen('http://127.0.0.1:1800/json/wallets/part').read())
assert(js_0['anon_balance'] + js_0['anon_pending'] > node0_anon_before + (amount_to - 0.1))
assert(js_0['anon_balance'] + js_0['anon_pending'] > node0_anon_before + (amount_to - 0.05))
def test_12_particl_blind(self):
return # TODO
logging.info('---------- Test Particl blind transactions')
swap_clients = self.swap_clients
js_0 = json.loads(urlopen('http://127.0.0.1:1800/json/wallets/part').read())
node0_blind_before = js_0['blind_balance'] + js_0['blind_unconfirmed']
wait_for_balance(test_delay_event, 'http://127.0.0.1:1801/json/wallets/part', 'balance', 200.0)
js_1 = json.loads(urlopen('http://127.0.0.1:1801/json/wallets/part').read())
assert(float(js_1['balance']) > 200.0)
node1_blind_before = js_1['blind_balance'] + js_1['blind_unconfirmed']
post_json = {
'value': 100,
'address': js_0['stealth_address'],
'subfee': False,
'type_to': 'blind',
}
json_rv = json.loads(post_json_req('http://127.0.0.1:1800/json/wallets/part/withdraw', post_json))
assert(len(json_rv['txid']) == 64)
logging.info('Waiting for blind balance')
wait_for_balance(test_delay_event, 'http://127.0.0.1:1800/json/wallets/part', 'blind_balance', 100.0 + node0_blind_before)
js_0 = json.loads(urlopen('http://127.0.0.1:1800/json/wallets/part').read())
node0_blind_before = js_0['blind_balance'] + js_0['blind_unconfirmed']
amt_swap = make_int(random.uniform(0.1, 2.0), scale=8, r=1)
rate_swap = make_int(random.uniform(2.0, 20.0), scale=8, r=1)
offer_id = swap_clients[0].postOffer(Coins.PART_BLIND, Coins.XMR, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP)
wait_for_offer(test_delay_event, swap_clients[1], offer_id)
offers = swap_clients[0].listOffers(filters={'offer_id': offer_id})
offer = offers[0]
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)
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)
amount_from = float(format_amount(amt_swap, 8))
js_1 = json.loads(urlopen('http://127.0.0.1:1801/json/wallets/part').read())
node1_blind_after = js_1['blind_balance'] + js_1['blind_unconfirmed']
assert(node1_blind_after > node1_blind_before + (amount_from - 0.05))
js_0 = json.loads(urlopen('http://127.0.0.1:1800/json/wallets/part').read())
node0_blind_after = js_0['blind_balance'] + js_0['blind_unconfirmed']
assert(node0_blind_after < node0_blind_before - amount_from)
def test_98_withdraw_all(self):
logging.info('---------- Test XMR withdrawal all')