Decred: Secret hash swap tests.

master^2
tecnovert 7 months ago
parent d527ec4974
commit 76879a2ff5
  1. 506
      basicswap/basicswap.py
  2. 1
      basicswap/basicswap_util.py
  3. 14
      basicswap/db.py
  4. 12
      basicswap/db_upgrades.py
  5. 2
      basicswap/db_util.py
  6. 26
      basicswap/interface/base.py
  7. 21
      basicswap/interface/btc.py
  8. 206
      basicswap/interface/dcr/dcr.py
  9. 24
      basicswap/interface/dcr/messages.py
  10. 6
      basicswap/interface/firo.py
  11. 4
      basicswap/interface/nav.py
  12. 2
      basicswap/interface/nmc.py
  13. 25
      basicswap/interface/part.py
  14. 2
      basicswap/interface/pivx.py
  15. 4
      basicswap/messages.proto
  16. 52
      basicswap/messages_pb2.py
  17. 51
      basicswap/protocols/atomic_swap_1.py
  18. 2
      basicswap/script.py
  19. 23
      basicswap/util/integer.py
  20. 18
      tests/basicswap/common.py
  21. 218
      tests/basicswap/extended/test_dcr.py
  22. 4
      tests/basicswap/test_other.py
  23. 98
      tests/basicswap/test_run.py
  24. 3
      tests/basicswap/test_xmr.py

@ -5,7 +5,6 @@
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import os import os
import re
import sys import sys
import zmq import zmq
import copy import copy
@ -16,7 +15,6 @@ import random
import shutil import shutil
import string import string
import struct import struct
import hashlib
import secrets import secrets
import datetime as dt import datetime as dt
import threading import threading
@ -53,11 +51,13 @@ from .util.script import (
) )
from .util.address import ( from .util.address import (
toWIF, toWIF,
getKeyID,
decodeWif, decodeWif,
decodeAddress, decodeAddress,
pubkeyToAddress, pubkeyToAddress,
) )
from .util.crypto import (
sha256,
)
from basicswap.util.network import is_private_ip_address from basicswap.util.network import is_private_ip_address
from .chainparams import ( from .chainparams import (
Coins, Coins,
@ -147,7 +147,7 @@ from basicswap.db_util import (
remove_expired_data, remove_expired_data,
) )
PROTOCOL_VERSION_SECRET_HASH = 4 PROTOCOL_VERSION_SECRET_HASH = 5
MINPROTO_VERSION_SECRET_HASH = 4 MINPROTO_VERSION_SECRET_HASH = 4
PROTOCOL_VERSION_ADAPTOR_SIG = 4 PROTOCOL_VERSION_ADAPTOR_SIG = 4
@ -209,6 +209,16 @@ class WatchedOutput(): # Watch for spends
self.swap_type = swap_type self.swap_type = swap_type
class WatchedScript(): # Watch for txns containing outputs
__slots__ = ('bid_id', 'script', 'tx_type', 'swap_type')
def __init__(self, bid_id: bytes, script: bytes, tx_type, swap_type):
self.bid_id = bid_id
self.script = script
self.tx_type = tx_type
self.swap_type = swap_type
class WatchedTransaction(): class WatchedTransaction():
# TODO # TODO
# Watch for presence in mempool (getrawtransaction) # Watch for presence in mempool (getrawtransaction)
@ -454,6 +464,10 @@ class BasicSwap(BaseApp):
last_height_checked = session.query(DBKVInt).filter_by(key='last_height_checked_' + chainparams[coin]['name']).first().value last_height_checked = session.query(DBKVInt).filter_by(key='last_height_checked_' + chainparams[coin]['name']).first().value
except Exception: except Exception:
last_height_checked = 0 last_height_checked = 0
try:
block_check_min_time = session.query(DBKVInt).filter_by(key='block_check_min_time_' + chainparams[coin]['name']).first().value
except Exception:
block_check_min_time = 0xffffffffffffffff
session.close() session.close()
session.remove() session.remove()
@ -472,7 +486,9 @@ class BasicSwap(BaseApp):
'blocks_confirmed': chain_client_settings.get('blocks_confirmed', 6), 'blocks_confirmed': chain_client_settings.get('blocks_confirmed', 6),
'conf_target': chain_client_settings.get('conf_target', 2), 'conf_target': chain_client_settings.get('conf_target', 2),
'watched_outputs': [], 'watched_outputs': [],
'watched_scripts': [],
'last_height_checked': last_height_checked, 'last_height_checked': last_height_checked,
'block_check_min_time': block_check_min_time,
'use_segwit': chain_client_settings.get('use_segwit', default_segwit), 'use_segwit': chain_client_settings.get('use_segwit', default_segwit),
'use_csv': chain_client_settings.get('use_csv', default_csv), 'use_csv': chain_client_settings.get('use_csv', default_csv),
'core_version_group': chain_client_settings.get('core_version_group', 0), 'core_version_group': chain_client_settings.get('core_version_group', 0),
@ -1150,12 +1166,17 @@ class BasicSwap(BaseApp):
if bid.participate_tx and bid.participate_tx.txid: if bid.participate_tx and bid.participate_tx.txid:
self.addWatchedOutput(coin_to, bid.bid_id, bid.participate_tx.txid.hex(), bid.participate_tx.vout, BidStates.SWAP_PARTICIPATING) self.addWatchedOutput(coin_to, bid.bid_id, bid.participate_tx.txid.hex(), bid.participate_tx.vout, BidStates.SWAP_PARTICIPATING)
if bid.participate_tx and bid.participate_tx.txid is None:
self.addWatchedScript(coin_to, bid.bid_id, self.ci(coin_to).getScriptDest(bid.participate_tx.script), TxTypes.PTX)
if bid.initiate_tx and bid.initiate_tx.chain_height:
self.setLastHeightCheckedStart(coin_to, bid.initiate_tx.chain_height)
if self.coin_clients[coin_from]['last_height_checked'] < 1: if self.coin_clients[coin_from]['last_height_checked'] < 1:
if bid.initiate_tx and bid.initiate_tx.chain_height: if bid.initiate_tx and bid.initiate_tx.chain_height:
self.coin_clients[coin_from]['last_height_checked'] = bid.initiate_tx.chain_height self.setLastHeightCheckedStart(coin_from, bid.initiate_tx.chain_height)
if self.coin_clients[coin_to]['last_height_checked'] < 1: if self.coin_clients[coin_to]['last_height_checked'] < 1:
if bid.participate_tx and bid.participate_tx.chain_height: if bid.participate_tx and bid.participate_tx.chain_height:
self.coin_clients[coin_to]['last_height_checked'] = bid.participate_tx.chain_height self.setLastHeightCheckedStart(coin_to, bid.participate_tx.chain_height)
# TODO process addresspool if bid has previously been abandoned # TODO process addresspool if bid has previously been abandoned
@ -1172,6 +1193,9 @@ class BasicSwap(BaseApp):
self.removeWatchedOutput(Coins(offer.coin_from), bid.bid_id, None) self.removeWatchedOutput(Coins(offer.coin_from), bid.bid_id, None)
self.removeWatchedOutput(Coins(offer.coin_to), bid.bid_id, None) self.removeWatchedOutput(Coins(offer.coin_to), bid.bid_id, None)
self.removeWatchedScript(Coins(offer.coin_from), bid.bid_id, None)
self.removeWatchedScript(Coins(offer.coin_to), bid.bid_id, None)
if bid.state in (BidStates.BID_ABANDONED, BidStates.SWAP_COMPLETED): if bid.state in (BidStates.BID_ABANDONED, BidStates.SWAP_COMPLETED):
# Return unused addrs to pool # Return unused addrs to pool
itx_state = bid.getITxState() itx_state = bid.getITxState()
@ -1859,7 +1883,7 @@ class BasicSwap(BaseApp):
path += '/' + str(date.year) + '/' + str(date.month) + '/' + str(date.day) path += '/' + str(date.year) + '/' + str(date.month) + '/' + str(date.day)
path += '/' + str(contract_count) path += '/' + str(contract_count)
return hashlib.sha256(bytes(self.callcoinrpc(Coins.PART, 'extkey', ['info', evkey, path])['key_info']['result'], 'utf-8')).digest() return sha256(bytes(self.callcoinrpc(Coins.PART, 'extkey', ['info', evkey, path])['key_info']['result'], 'utf-8'))
def getReceiveAddressFromPool(self, coin_type, bid_id: bytes, tx_type): def getReceiveAddressFromPool(self, coin_type, bid_id: bytes, tx_type):
self.log.debug('Get address from pool bid_id {}, type {}, coin {}'.format(bid_id.hex(), tx_type, coin_type)) self.log.debug('Get address from pool bid_id {}, type {}, coin {}'.format(bid_id.hex(), tx_type, coin_type))
@ -2337,7 +2361,12 @@ class BasicSwap(BaseApp):
msg_buf.proof_utxos = ci_to.encodeProofUtxos(proof_utxos) msg_buf.proof_utxos = ci_to.encodeProofUtxos(proof_utxos)
contract_count = self.getNewContractId() contract_count = self.getNewContractId()
msg_buf.pkhash_buyer = getKeyID(self.getContractPubkey(dt.datetime.fromtimestamp(now).date(), contract_count)) contract_pubkey = self.getContractPubkey(dt.datetime.fromtimestamp(now).date(), contract_count)
msg_buf.pkhash_buyer = ci_from.pkh(contract_pubkey)
pkhash_buyer_to = ci_to.pkh(contract_pubkey)
if pkhash_buyer_to != msg_buf.pkhash_buyer:
# Different pubkey hash
msg_buf.pkhash_buyer_to = pkhash_buyer_to
else: else:
raise ValueError('TODO') raise ValueError('TODO')
@ -2370,6 +2399,9 @@ class BasicSwap(BaseApp):
) )
bid.setState(BidStates.BID_SENT) bid.setState(BidStates.BID_SENT)
if len(msg_buf.pkhash_buyer_to) > 0:
bid.pkhash_buyer_to = msg_buf.pkhash_buyer_to
try: try:
session = scoped_session(self.session_factory) session = scoped_session(self.session_factory)
self.saveBidInSession(bid_id, bid, session) self.saveBidInSession(bid_id, bid, session)
@ -2554,13 +2586,19 @@ class BasicSwap(BaseApp):
coin_from = Coins(offer.coin_from) coin_from = Coins(offer.coin_from)
ci_from = self.ci(coin_from) ci_from = self.ci(coin_from)
ci_to = self.ci(offer.coin_to)
bid_date = dt.datetime.fromtimestamp(bid.created_at).date() bid_date = dt.datetime.fromtimestamp(bid.created_at).date()
secret = self.getContractSecret(bid_date, bid.contract_count) secret = self.getContractSecret(bid_date, bid.contract_count)
secret_hash = hashlib.sha256(secret).digest() secret_hash = sha256(secret)
pubkey_refund = self.getContractPubkey(bid_date, bid.contract_count) pubkey_refund = self.getContractPubkey(bid_date, bid.contract_count)
pkhash_refund = getKeyID(pubkey_refund) pkhash_refund = ci_from.pkh(pubkey_refund)
if coin_from in (Coins.DCR, ):
op_hash = OpCodes.OP_SHA256_DECRED
else:
op_hash = OpCodes.OP_SHA256
if bid.initiate_tx is not None: if bid.initiate_tx is not None:
self.log.warning('Initiate txn %s already exists for bid %s', bid.initiate_tx.txid, bid_id.hex()) self.log.warning('Initiate txn %s already exists for bid %s', bid.initiate_tx.txid, bid_id.hex())
@ -2569,21 +2607,19 @@ class BasicSwap(BaseApp):
else: else:
if offer.lock_type < TxLockTypes.ABS_LOCK_BLOCKS: if offer.lock_type < TxLockTypes.ABS_LOCK_BLOCKS:
sequence = ci_from.getExpectedSequence(offer.lock_type, offer.lock_value) sequence = ci_from.getExpectedSequence(offer.lock_type, offer.lock_value)
script = atomic_swap_1.buildContractScript(sequence, secret_hash, bid.pkhash_buyer, pkhash_refund) script = atomic_swap_1.buildContractScript(sequence, secret_hash, bid.pkhash_buyer, pkhash_refund, op_hash=op_hash)
else: else:
if offer.lock_type == TxLockTypes.ABS_LOCK_BLOCKS: if offer.lock_type == TxLockTypes.ABS_LOCK_BLOCKS:
lock_value = self.callcoinrpc(coin_from, 'getblockcount') + offer.lock_value lock_value = ci_from.getChainHeight() + offer.lock_value
else: else:
lock_value = self.getTime() + offer.lock_value lock_value = self.getTime() + offer.lock_value
self.log.debug('Initiate %s lock_value %d %d', ci_from.coin_name(), offer.lock_value, lock_value) self.log.debug('Initiate %s lock_value %d %d', ci_from.coin_name(), offer.lock_value, lock_value)
script = atomic_swap_1.buildContractScript(lock_value, secret_hash, bid.pkhash_buyer, pkhash_refund, OpCodes.OP_CHECKLOCKTIMEVERIFY) script = atomic_swap_1.buildContractScript(lock_value, secret_hash, bid.pkhash_buyer, pkhash_refund, OpCodes.OP_CHECKLOCKTIMEVERIFY, op_hash=op_hash)
p2sh = self.callcoinrpc(Coins.PART, 'decodescript', [script.hex()])['p2sh'] bid.pkhash_seller = ci_to.pkh(pubkey_refund)
bid.pkhash_seller = pkhash_refund
prefunded_tx = self.getPreFundedTx(Concepts.OFFER, offer.offer_id, TxTypes.ITX_PRE_FUNDED) prefunded_tx = self.getPreFundedTx(Concepts.OFFER, offer.offer_id, TxTypes.ITX_PRE_FUNDED)
txn = self.createInitiateTxn(coin_from, bid_id, bid, script, prefunded_tx) txn, lock_tx_vout = self.createInitiateTxn(coin_from, bid_id, bid, script, prefunded_tx)
# Store the signed refund txn in case wallet is locked when refund is possible # Store the signed refund txn in case wallet is locked when refund is possible
refund_txn = self.createRefundTxn(coin_from, txn, offer, bid, script) refund_txn = self.createRefundTxn(coin_from, txn, offer, bid, script)
@ -2595,6 +2631,7 @@ class BasicSwap(BaseApp):
bid_id=bid_id, bid_id=bid_id,
tx_type=TxTypes.ITX, tx_type=TxTypes.ITX,
txid=bytes.fromhex(txid), txid=bytes.fromhex(txid),
vout=lock_tx_vout,
tx_data=bytes.fromhex(txn), tx_data=bytes.fromhex(txn),
script=script, script=script,
) )
@ -2615,6 +2652,11 @@ class BasicSwap(BaseApp):
msg_buf.initiate_txid = bytes.fromhex(txid) msg_buf.initiate_txid = bytes.fromhex(txid)
msg_buf.contract_script = bytes(script) msg_buf.contract_script = bytes(script)
# pkh sent in script is hashed with sha256, Decred expects blake256
if bid.pkhash_seller != pkhash_refund:
assert (ci_to.coin_type() == Coins.DCR or ci_from.coin_type() == Coins.DCR) # [rm]
msg_buf.pkhash_seller = bid.pkhash_seller
bid_bytes = msg_buf.SerializeToString() bid_bytes = msg_buf.SerializeToString()
payload_hex = str.format('{:02x}', MessageTypes.BID_ACCEPT) + bid_bytes.hex() payload_hex = str.format('{:02x}', MessageTypes.BID_ACCEPT) + bid_bytes.hex()
@ -3137,9 +3179,9 @@ class BasicSwap(BaseApp):
if save_bid: if save_bid:
self.saveBid(bid_id, bid, xmr_swap=xmr_swap) self.saveBid(bid_id, bid, xmr_swap=xmr_swap)
def createInitiateTxn(self, coin_type, bid_id: bytes, bid, initiate_script, prefunded_tx=None) -> Optional[str]: def createInitiateTxn(self, coin_type, bid_id: bytes, bid, initiate_script, prefunded_tx=None) -> (Optional[str], Optional[int]):
if self.coin_clients[coin_type]['connection_type'] != 'rpc': if self.coin_clients[coin_type]['connection_type'] != 'rpc':
return None return None, None
ci = self.ci(coin_type) ci = self.ci(coin_type)
if ci.using_segwit(): if ci.using_segwit():
@ -3154,7 +3196,12 @@ class BasicSwap(BaseApp):
txn_signed = pi.promoteMockTx(ci, prefunded_tx, initiate_script).hex() txn_signed = pi.promoteMockTx(ci, prefunded_tx, initiate_script).hex()
else: else:
txn_signed = ci.createRawSignedTransaction(addr_to, bid.amount) txn_signed = ci.createRawSignedTransaction(addr_to, bid.amount)
return txn_signed
txjs = ci.describeTx(txn_signed)
vout = getVoutByAddress(txjs, addr_to)
assert (vout is not None)
return txn_signed, vout
def deriveParticipateScript(self, bid_id: bytes, bid, offer) -> bytearray: def deriveParticipateScript(self, bid_id: bytes, bid, offer) -> bytearray:
self.log.debug('deriveParticipateScript for bid %s', bid_id.hex()) self.log.debug('deriveParticipateScript for bid %s', bid_id.hex())
@ -3164,18 +3211,28 @@ class BasicSwap(BaseApp):
secret_hash = atomic_swap_1.extractScriptSecretHash(bid.initiate_tx.script) secret_hash = atomic_swap_1.extractScriptSecretHash(bid.initiate_tx.script)
pkhash_seller = bid.pkhash_seller pkhash_seller = bid.pkhash_seller
pkhash_buyer_refund = bid.pkhash_buyer
if bid.pkhash_buyer_to and len(bid.pkhash_buyer_to) > 0:
pkhash_buyer_refund = bid.pkhash_buyer_to
else:
pkhash_buyer_refund = bid.pkhash_buyer
if coin_to in (Coins.DCR, ):
op_hash = OpCodes.OP_SHA256_DECRED
else:
op_hash = OpCodes.OP_SHA256
# Participate txn is locked for half the time of the initiate txn # Participate txn is locked for half the time of the initiate txn
lock_value = offer.lock_value // 2 lock_value = offer.lock_value // 2
if offer.lock_type < TxLockTypes.ABS_LOCK_BLOCKS: if offer.lock_type < TxLockTypes.ABS_LOCK_BLOCKS:
sequence = ci_to.getExpectedSequence(offer.lock_type, lock_value) sequence = ci_to.getExpectedSequence(offer.lock_type, lock_value)
participate_script = atomic_swap_1.buildContractScript(sequence, secret_hash, pkhash_seller, pkhash_buyer_refund) participate_script = atomic_swap_1.buildContractScript(sequence, secret_hash, pkhash_seller, pkhash_buyer_refund, op_hash=op_hash)
else: else:
# Lock from the height or time of the block containing the initiate txn # Lock from the height or time of the block containing the initiate txn
coin_from = Coins(offer.coin_from) coin_from = Coins(offer.coin_from)
initiate_tx_block_hash = self.callcoinrpc(coin_from, 'getblockhash', [bid.initiate_tx.chain_height, ]) block_header = self.ci(coin_from).getBlockHeaderFromHeight(bid.initiate_tx.chain_height)
initiate_tx_block_time = int(self.callcoinrpc(coin_from, 'getblock', [initiate_tx_block_hash, ])['time']) initiate_tx_block_hash = block_header['hash']
initiate_tx_block_time = block_header['time']
if offer.lock_type == TxLockTypes.ABS_LOCK_BLOCKS: if offer.lock_type == TxLockTypes.ABS_LOCK_BLOCKS:
# Walk the coin_to chain back until block time matches # Walk the coin_to chain back until block time matches
block_header_at = ci_to.getBlockHeaderAt(initiate_tx_block_time, block_after=True) block_header_at = ci_to.getBlockHeaderAt(initiate_tx_block_time, block_after=True)
@ -3188,7 +3245,7 @@ class BasicSwap(BaseApp):
self.log.debug('Setting lock value from time of block %s %s', Coins(coin_from).name, initiate_tx_block_hash) self.log.debug('Setting lock value from time of block %s %s', Coins(coin_from).name, initiate_tx_block_hash)
contract_lock_value = initiate_tx_block_time + lock_value contract_lock_value = initiate_tx_block_time + lock_value
self.log.debug('participate %s lock_value %d %d', Coins(coin_to).name, lock_value, contract_lock_value) self.log.debug('participate %s lock_value %d %d', Coins(coin_to).name, lock_value, contract_lock_value)
participate_script = atomic_swap_1.buildContractScript(contract_lock_value, secret_hash, pkhash_seller, pkhash_buyer_refund, OpCodes.OP_CHECKLOCKTIMEVERIFY) participate_script = atomic_swap_1.buildContractScript(contract_lock_value, secret_hash, pkhash_seller, pkhash_buyer_refund, OpCodes.OP_CHECKLOCKTIMEVERIFY, op_hash=op_hash)
return participate_script return participate_script
def createParticipateTxn(self, bid_id: bytes, bid, offer, participate_script: bytearray): def createParticipateTxn(self, bid_id: bytes, bid, offer, participate_script: bytearray):
@ -3219,7 +3276,7 @@ class BasicSwap(BaseApp):
refund_txn = self.createRefundTxn(coin_to, txn_signed, offer, bid, participate_script, tx_type=TxTypes.PTX_REFUND) refund_txn = self.createRefundTxn(coin_to, txn_signed, offer, bid, participate_script, tx_type=TxTypes.PTX_REFUND)
bid.participate_txn_refund = bytes.fromhex(refund_txn) bid.participate_txn_refund = bytes.fromhex(refund_txn)
chain_height = self.callcoinrpc(coin_to, 'getblockcount') chain_height = ci.getChainHeight()
txjs = self.callcoinrpc(coin_to, 'decoderawtransaction', [txn_signed]) txjs = self.callcoinrpc(coin_to, 'decoderawtransaction', [txn_signed])
txid = txjs['txid'] txid = txjs['txid']
@ -3252,7 +3309,7 @@ class BasicSwap(BaseApp):
prev_p2wsh = ci.getScriptDest(txn_script) prev_p2wsh = ci.getScriptDest(txn_script)
script_pub_key = prev_p2wsh.hex() script_pub_key = prev_p2wsh.hex()
else: else:
script_pub_key = getP2SHScriptForHash(getKeyID(txn_script)).hex() script_pub_key = getP2SHScriptForHash(ci.pkh(txn_script)).hex()
prevout = { prevout = {
'txid': prev_txnid, 'txid': prev_txnid,
@ -3262,18 +3319,17 @@ class BasicSwap(BaseApp):
'amount': ci.format_amount(prev_amount)} 'amount': ci.format_amount(prev_amount)}
bid_date = dt.datetime.fromtimestamp(bid.created_at).date() bid_date = dt.datetime.fromtimestamp(bid.created_at).date()
if coin_type in (Coins.NAV, ): privkey = self.getContractPrivkey(bid_date, bid.contract_count)
wif_prefix = chainparams[coin_type][self.chain]['key_prefix'] pubkey = ci.getPubkey(privkey)
else:
wif_prefix = chainparams[Coins.PART][self.chain]['key_prefix']
pubkey = self.getContractPubkey(bid_date, bid.contract_count)
privkey = toWIF(wif_prefix, self.getContractPrivkey(bid_date, bid.contract_count))
secret = bid.recovered_secret secret = bid.recovered_secret
if secret is None: if secret is None:
secret = self.getContractSecret(bid_date, bid.contract_count) secret = self.getContractSecret(bid_date, bid.contract_count)
ensure(len(secret) == 32, 'Bad secret length') ensure(len(secret) == 32, 'Bad secret length')
self.log.debug('secret {}'.format(secret.hex()))
self.log.debug('sha256(secret) {}'.format(sha256(secret).hex()))
if self.coin_clients[coin_type]['connection_type'] != 'rpc': if self.coin_clients[coin_type]['connection_type'] != 'rpc':
return None return None
@ -3294,40 +3350,40 @@ class BasicSwap(BaseApp):
self.log.debug('addr_redeem_out %s', addr_redeem_out) self.log.debug('addr_redeem_out %s', addr_redeem_out)
if ci.use_p2shp2wsh(): redeem_txn = ci.createRedeemTxn(prevout, addr_redeem_out, amount_out, txn_script)
redeem_txn = ci.createRedeemTxn(prevout, addr_redeem_out, amount_out, txn_script)
else:
redeem_txn = ci.createRedeemTxn(prevout, addr_redeem_out, amount_out)
options = {} options = {}
if ci.using_segwit(): if ci.using_segwit():
options['force_segwit'] = True options['force_segwit'] = True
if coin_type in (Coins.NAV, ): if coin_type in (Coins.NAV, Coins.DCR):
redeem_sig = ci.getTxSignature(redeem_txn, prevout, privkey) privkey_wif = self.ci(coin_type).encodeKey(privkey)
redeem_sig = ci.getTxSignature(redeem_txn, prevout, privkey_wif)
else: else:
redeem_sig = self.callcoinrpc(Coins.PART, 'createsignaturewithkey', [redeem_txn, prevout, privkey, 'ALL', options]) privkey_wif = self.ci(Coins.PART).encodeKey(privkey)
redeem_sig = self.callcoinrpc(Coins.PART, 'createsignaturewithkey', [redeem_txn, prevout, privkey_wif, 'ALL', options])
if coin_type == Coins.PART or ci.using_segwit(): if coin_type == Coins.PART or ci.using_segwit():
witness_stack = [ witness_stack = [
bytes.fromhex(redeem_sig), bytes.fromhex(redeem_sig),
pubkey, pubkey,
secret, secret,
bytes((1,)), bytes((1,)), # Converted to OP_1 in Decred push_script_data
txn_script] txn_script]
redeem_txn = ci.setTxSignature(bytes.fromhex(redeem_txn), witness_stack).hex() redeem_txn = ci.setTxSignature(bytes.fromhex(redeem_txn), witness_stack).hex()
else: else:
script = format(len(redeem_sig) // 2, '02x') + redeem_sig script = (len(redeem_sig) // 2).to_bytes(1) + bytes.fromhex(redeem_sig)
script += format(33, '02x') + pubkey.hex() script += (33).to_bytes(1) + pubkey
script += format(32, '02x') + secret.hex() script += (32).to_bytes(1) + secret
script += format(OpCodes.OP_1, '02x') script += (OpCodes.OP_1).to_bytes(1)
script += format(OpCodes.OP_PUSHDATA1, '02x') + format(len(txn_script), '02x') + txn_script.hex() script += (OpCodes.OP_PUSHDATA1).to_bytes(1) + (len(txn_script)).to_bytes(1) + txn_script
redeem_txn = ci.setTxScriptSig(bytes.fromhex(redeem_txn), 0, bytes.fromhex(script)).hex() redeem_txn = ci.setTxScriptSig(bytes.fromhex(redeem_txn), 0, script).hex()
if coin_type in (Coins.NAV, ): if coin_type in (Coins.NAV, Coins.DCR):
# Only checks signature # Only checks signature
ro = ci.verifyRawTransaction(redeem_txn, [prevout]) ro = ci.verifyRawTransaction(redeem_txn, [prevout])
else: else:
ro = self.callcoinrpc(Coins.PART, 'verifyrawtransaction', [redeem_txn, [prevout]]) ro = self.callcoinrpc(Coins.PART, 'verifyrawtransaction', [redeem_txn, [prevout]])
ensure(ro['inputs_valid'] is True, 'inputs_valid is false') ensure(ro['inputs_valid'] is True, 'inputs_valid is false')
# outputs_valid will be false if not a Particl txn # outputs_valid will be false if not a Particl txn
# ensure(ro['complete'] is True, 'complete is false') # ensure(ro['complete'] is True, 'complete is false')
@ -3337,7 +3393,11 @@ class BasicSwap(BaseApp):
# Check fee # Check fee
if ci.get_connection_type() == 'rpc': if ci.get_connection_type() == 'rpc':
redeem_txjs = self.callcoinrpc(coin_type, 'decoderawtransaction', [redeem_txn]) redeem_txjs = self.callcoinrpc(coin_type, 'decoderawtransaction', [redeem_txn])
if ci.using_segwit() or coin_type in (Coins.PART, ): if coin_type in (Coins.DCR, ):
txsize = len(redeem_txn) // 2
self.log.debug('size paid, actual size %d %d', tx_vsize, txsize)
ensure(tx_vsize >= txsize, 'underpaid fee')
elif ci.use_tx_vsize():
self.log.debug('vsize paid, actual vsize %d %d', tx_vsize, redeem_txjs['vsize']) self.log.debug('vsize paid, actual vsize %d %d', tx_vsize, redeem_txjs['vsize'])
ensure(tx_vsize >= redeem_txjs['vsize'], 'underpaid fee') ensure(tx_vsize >= redeem_txjs['vsize'], 'underpaid fee')
else: else:
@ -3355,11 +3415,9 @@ class BasicSwap(BaseApp):
ci = self.ci(coin_type) ci = self.ci(coin_type)
if coin_type in (Coins.NAV, Coins.DCR): if coin_type in (Coins.NAV, Coins.DCR):
wif_prefix = chainparams[coin_type][self.chain]['key_prefix']
prevout = ci.find_prevout_info(txn, txn_script) prevout = ci.find_prevout_info(txn, txn_script)
else: else:
# TODO: Sign in bsx for all coins # TODO: Sign in bsx for all coins
wif_prefix = chainparams[Coins.PART][self.chain]['key_prefix']
txjs = self.callcoinrpc(Coins.PART, 'decoderawtransaction', [txn]) txjs = self.callcoinrpc(Coins.PART, 'decoderawtransaction', [txn])
if ci.using_segwit(): if ci.using_segwit():
p2wsh = ci.getScriptDest(txn_script) p2wsh = ci.getScriptDest(txn_script)
@ -3377,8 +3435,9 @@ class BasicSwap(BaseApp):
} }
bid_date = dt.datetime.fromtimestamp(bid.created_at).date() bid_date = dt.datetime.fromtimestamp(bid.created_at).date()
pubkey = self.getContractPubkey(bid_date, bid.contract_count)
privkey = toWIF(wif_prefix, self.getContractPrivkey(bid_date, bid.contract_count)) privkey = self.getContractPrivkey(bid_date, bid.contract_count)
pubkey = ci.getPubkey(privkey)
lock_value = DeserialiseNum(txn_script, 64) lock_value = DeserialiseNum(txn_script, 64)
sequence: int = 1 sequence: int = 1
@ -3405,19 +3464,25 @@ class BasicSwap(BaseApp):
if offer.lock_type == TxLockTypes.ABS_LOCK_BLOCKS or offer.lock_type == TxLockTypes.ABS_LOCK_TIME: if offer.lock_type == TxLockTypes.ABS_LOCK_BLOCKS or offer.lock_type == TxLockTypes.ABS_LOCK_TIME:
locktime = lock_value locktime = lock_value
if ci.use_p2shp2wsh(): refund_txn = ci.createRefundTxn(prevout, addr_refund_out, amount_out, locktime, sequence, txn_script)
refund_txn = ci.createRefundTxn(prevout, addr_refund_out, amount_out, locktime, sequence, txn_script)
else:
refund_txn = ci.createRefundTxn(prevout, addr_refund_out, amount_out, locktime, sequence)
options = {} options = {}
if self.coin_clients[coin_type]['use_segwit']: if self.coin_clients[coin_type]['use_segwit']:
options['force_segwit'] = True options['force_segwit'] = True
if coin_type in (Coins.NAV, Coins.DCR): if coin_type in (Coins.NAV, Coins.DCR):
refund_sig = ci.getTxSignature(refund_txn, prevout, privkey) privkey_wif = ci.encodeKey(privkey)
refund_sig = ci.getTxSignature(refund_txn, prevout, privkey_wif)
else: else:
refund_sig = self.callcoinrpc(Coins.PART, 'createsignaturewithkey', [refund_txn, prevout, privkey, 'ALL', options]) privkey_wif = self.ci(Coins.PART).encodeKey(privkey)
if coin_type == Coins.PART or self.coin_clients[coin_type]['use_segwit']: refund_sig = self.callcoinrpc(Coins.PART, 'createsignaturewithkey', [refund_txn, prevout, privkey_wif, 'ALL', options])
if coin_type in (Coins.DCR, ):
witness_stack = [
bytes.fromhex(refund_sig),
pubkey,
(OpCodes.OP_0).to_bytes(1),
txn_script]
refund_txn = ci.setTxSignature(bytes.fromhex(refund_txn), witness_stack).hex()
elif coin_type in (Coins.PART, ) or self.coin_clients[coin_type]['use_segwit']:
witness_stack = [ witness_stack = [
bytes.fromhex(refund_sig), bytes.fromhex(refund_sig),
pubkey, pubkey,
@ -3425,11 +3490,11 @@ class BasicSwap(BaseApp):
txn_script] txn_script]
refund_txn = ci.setTxSignature(bytes.fromhex(refund_txn), witness_stack).hex() refund_txn = ci.setTxSignature(bytes.fromhex(refund_txn), witness_stack).hex()
else: else:
script = format(len(refund_sig) // 2, '02x') + refund_sig script = (len(refund_sig) // 2).to_bytes(1) + bytes.fromhex(refund_sig)
script += format(33, '02x') + pubkey.hex() script += (33).to_bytes(1) + pubkey
script += format(OpCodes.OP_0, '02x') script += (OpCodes.OP_0).to_bytes(1)
script += format(OpCodes.OP_PUSHDATA1, '02x') + format(len(txn_script), '02x') + txn_script.hex() script += (OpCodes.OP_PUSHDATA1).to_bytes(1) + (len(txn_script)).to_bytes(1) + txn_script
refund_txn = ci.setTxScriptSig(bytes.fromhex(refund_txn), 0, bytes.fromhex(script)).hex() refund_txn = ci.setTxScriptSig(bytes.fromhex(refund_txn), 0, script)
if coin_type in (Coins.NAV, Coins.DCR): if coin_type in (Coins.NAV, Coins.DCR):
# Only checks signature # Only checks signature
@ -3445,8 +3510,12 @@ class BasicSwap(BaseApp):
if self.debug: if self.debug:
# Check fee # Check fee
if ci.get_connection_type() == 'rpc': if ci.get_connection_type() == 'rpc':
refund_txjs = self.callcoinrpc(coin_type, 'decoderawtransaction', [refund_txn]) refund_txjs = self.callcoinrpc(coin_type, 'decoderawtransaction', [refund_txn,])
if ci.using_segwit() or coin_type in (Coins.PART, ): if coin_type in (Coins.DCR, ):
txsize = len(refund_txn) // 2
self.log.debug('size paid, actual size %d %d', tx_vsize, txsize)
ensure(tx_vsize >= txsize, 'underpaid fee')
elif ci.use_tx_vsize():
self.log.debug('vsize paid, actual vsize %d %d', tx_vsize, refund_txjs['vsize']) self.log.debug('vsize paid, actual vsize %d %d', tx_vsize, refund_txjs['vsize'])
ensure(tx_vsize >= refund_txjs['vsize'], 'underpaid fee') ensure(tx_vsize >= refund_txjs['vsize'], 'underpaid fee')
else: else:
@ -3490,20 +3559,31 @@ class BasicSwap(BaseApp):
tx_type=TxTypes.PTX, tx_type=TxTypes.PTX,
script=participate_script, script=participate_script,
) )
ci = self.ci(offer.coin_to)
if ci.watch_blocks_for_scripts() is True:
self.addWatchedScript(offer.coin_to, bid_id, ci.getScriptDest(participate_script), TxTypes.PTX)
self.setLastHeightCheckedStart(offer.coin_to, bid.initiate_tx.chain_height)
# Bid saved in checkBidState # Bid saved in checkBidState
def setLastHeightChecked(self, coin_type, tx_height: int) -> int: def setLastHeightCheckedStart(self, coin_type, tx_height: int) -> int:
coin_name = self.ci(coin_type).coin_name() ci = self.ci(coin_type)
coin_name = ci.coin_name()
if tx_height < 1: if tx_height < 1:
tx_height = self.lookupChainHeight(coin_type) tx_height = self.lookupChainHeight(coin_type)
if len(self.coin_clients[coin_type]['watched_outputs']) == 0: block_header = ci.getBlockHeaderFromHeight(tx_height)
self.coin_clients[coin_type]['last_height_checked'] = tx_height block_time = block_header['time']
cc = self.coin_clients[coin_type]
if len(cc['watched_outputs']) == 0 and len(cc['watched_scripts']) == 0:
cc['last_height_checked'] = tx_height
cc['block_check_min_time'] = block_time
self.setIntKV('block_check_min_time_' + coin_name, block_time)
self.log.debug('Start checking %s chain at height %d', coin_name, tx_height) self.log.debug('Start checking %s chain at height %d', coin_name, tx_height)
elif cc['last_height_checked'] > tx_height:
if self.coin_clients[coin_type]['last_height_checked'] > tx_height: cc['last_height_checked'] = tx_height
self.coin_clients[coin_type]['last_height_checked'] = tx_height cc['block_check_min_time'] = block_time
self.setIntKV('block_check_min_time_' + coin_name, block_time)
self.log.debug('Rewind checking of %s chain to height %d', coin_name, tx_height) self.log.debug('Rewind checking of %s chain to height %d', coin_name, tx_height)
return tx_height return tx_height
@ -3511,7 +3591,7 @@ class BasicSwap(BaseApp):
def addParticipateTxn(self, bid_id: bytes, bid, coin_type, txid_hex: str, vout, tx_height) -> None: def addParticipateTxn(self, bid_id: bytes, bid, coin_type, txid_hex: str, vout, tx_height) -> None:
# TODO: Check connection type # TODO: Check connection type
participate_txn_height = self.setLastHeightChecked(coin_type, tx_height) participate_txn_height = self.setLastHeightCheckedStart(coin_type, tx_height)
if bid.participate_tx is None: if bid.participate_tx is None:
bid.participate_tx = SwapTx( bid.participate_tx = SwapTx(
@ -3529,6 +3609,11 @@ class BasicSwap(BaseApp):
def participateTxnConfirmed(self, bid_id: bytes, bid, offer) -> None: def participateTxnConfirmed(self, bid_id: bytes, bid, offer) -> None:
self.log.debug('participateTxnConfirmed for bid %s', bid_id.hex()) self.log.debug('participateTxnConfirmed for bid %s', bid_id.hex())
if bid.debug_ind == DebugTypes.DONT_CONFIRM_PTX:
self.log.debug('Not confirming PTX for debugging', bid_id.hex())
return
bid.setState(BidStates.SWAP_PARTICIPATING) bid.setState(BidStates.SWAP_PARTICIPATING)
bid.setPTxState(TxStates.TX_CONFIRMED) bid.setPTxState(TxStates.TX_CONFIRMED)
@ -3774,9 +3859,9 @@ class BasicSwap(BaseApp):
return rv return rv
# TODO: Timeout waiting for transactions # TODO: Timeout waiting for transactions
bid_changed = False bid_changed: bool = False
a_lock_tx_addr = ci_from.getSCLockScriptAddress(xmr_swap.a_lock_tx_script) a_lock_tx_addr = ci_from.getSCLockScriptAddress(xmr_swap.a_lock_tx_script)
lock_tx_chain_info = ci_from.getLockTxHeight(bid.xmr_a_lock_tx.txid, a_lock_tx_addr, bid.amount, bid.chain_a_height_start) lock_tx_chain_info = ci_from.getLockTxHeight(bid.xmr_a_lock_tx.txid, a_lock_tx_addr, bid.amount, bid.chain_a_height_start, vout=bid.xmr_a_lock_tx.vout)
if lock_tx_chain_info is None: if lock_tx_chain_info is None:
return rv return rv
@ -3863,7 +3948,7 @@ class BasicSwap(BaseApp):
refund_tx = bid.txns[TxTypes.XMR_SWAP_A_LOCK_REFUND] refund_tx = bid.txns[TxTypes.XMR_SWAP_A_LOCK_REFUND]
if refund_tx.block_time is None: if refund_tx.block_time is None:
refund_tx_addr = ci_from.getSCLockScriptAddress(xmr_swap.a_lock_refund_tx_script) refund_tx_addr = ci_from.getSCLockScriptAddress(xmr_swap.a_lock_refund_tx_script)
lock_refund_tx_chain_info = ci_from.getLockTxHeight(refund_tx.txid, refund_tx_addr, 0, bid.chain_a_height_start) lock_refund_tx_chain_info = ci_from.getLockTxHeight(refund_tx.txid, refund_tx_addr, 0, bid.chain_a_height_start, vout=refund_tx.vout)
if lock_refund_tx_chain_info is not None and lock_refund_tx_chain_info.get('height', 0) > 0: if lock_refund_tx_chain_info is not None and lock_refund_tx_chain_info.get('height', 0) > 0:
self.setTxBlockInfoFromHeight(ci_from, refund_tx, lock_refund_tx_chain_info['height']) self.setTxBlockInfoFromHeight(ci_from, refund_tx, lock_refund_tx_chain_info['height'])
@ -3904,14 +3989,14 @@ class BasicSwap(BaseApp):
return True # Mark bid for archiving return True # Mark bid for archiving
if state == BidStates.BID_ACCEPTED: if state == BidStates.BID_ACCEPTED:
# Waiting for initiate txn to be confirmed in 'from' chain # Waiting for initiate txn to be confirmed in 'from' chain
initiate_txnid_hex = bid.initiate_tx.txid.hex()
p2sh = ci_from.encode_p2sh(bid.initiate_tx.script)
index = None index = None
tx_height = None tx_height = None
initiate_txnid_hex = bid.initiate_tx.txid.hex()
last_initiate_txn_conf = bid.initiate_tx.conf last_initiate_txn_conf = bid.initiate_tx.conf
ci_from = self.ci(coin_from) ci_from = self.ci(coin_from)
if coin_from == Coins.PART: # Has txindex if coin_from == Coins.PART: # Has txindex
try: try:
p2sh = ci_from.encode_p2sh(bid.initiate_tx.script)
initiate_txn = self.callcoinrpc(coin_from, 'getrawtransaction', [initiate_txnid_hex, True]) initiate_txn = self.callcoinrpc(coin_from, 'getrawtransaction', [initiate_txnid_hex, True])
# Verify amount # Verify amount
vout = getVoutByAddress(initiate_txn, p2sh) vout = getVoutByAddress(initiate_txn, p2sh)
@ -3932,24 +4017,29 @@ class BasicSwap(BaseApp):
dest_script = ci_from.getScriptDest(bid.initiate_tx.script) dest_script = ci_from.getScriptDest(bid.initiate_tx.script)
addr = ci_from.encodeScriptDest(dest_script) addr = ci_from.encodeScriptDest(dest_script)
else: else:
addr = p2sh addr = ci_from.encode_p2sh(bid.initiate_tx.script)
found = ci_from.getLockTxHeight(bytes.fromhex(initiate_txnid_hex), addr, bid.amount, bid.chain_a_height_start, find_index=True) found = ci_from.getLockTxHeight(bid.initiate_tx.txid, addr, bid.amount, bid.chain_a_height_start, find_index=True, vout=bid.initiate_tx.vout)
index = None
if found: if found:
bid.initiate_tx.conf = found['depth'] bid.initiate_tx.conf = found['depth']
index = found['index'] if 'index' in found:
index = found['index']
tx_height = found['height'] tx_height = found['height']
if bid.initiate_tx.conf != last_initiate_txn_conf: if bid.initiate_tx.conf != last_initiate_txn_conf:
save_bid = True save_bid = True
if bid.initiate_tx.vout is None and index is not None:
bid.initiate_tx.vout = index
save_bid = True
if bid.initiate_tx.conf is not None: if bid.initiate_tx.conf is not None:
self.log.debug('initiate_txnid %s confirms %d', initiate_txnid_hex, bid.initiate_tx.conf) self.log.debug('initiate_txnid %s confirms %d', initiate_txnid_hex, bid.initiate_tx.conf)
if bid.initiate_tx.vout is None and tx_height > 0: if (last_initiate_txn_conf is None or last_initiate_txn_conf < 1) and tx_height > 0:
bid.initiate_tx.vout = index
# Start checking for spends of initiate_txn before fully confirmed # Start checking for spends of initiate_txn before fully confirmed
bid.initiate_tx.chain_height = self.setLastHeightChecked(coin_from, tx_height) bid.initiate_tx.chain_height = self.setLastHeightCheckedStart(coin_from, tx_height)
self.setTxBlockInfoFromHeight(ci_from, bid.initiate_tx, tx_height) self.setTxBlockInfoFromHeight(ci_from, bid.initiate_tx, tx_height)
self.addWatchedOutput(coin_from, bid_id, initiate_txnid_hex, bid.initiate_tx.vout, BidStates.SWAP_INITIATED) self.addWatchedOutput(coin_from, bid_id, initiate_txnid_hex, bid.initiate_tx.vout, BidStates.SWAP_INITIATED)
@ -3978,17 +4068,22 @@ class BasicSwap(BaseApp):
ci_to = self.ci(coin_to) ci_to = self.ci(coin_to)
participate_txid = None if bid.participate_tx is None or bid.participate_tx.txid is None else bid.participate_tx.txid participate_txid = None if bid.participate_tx is None or bid.participate_tx.txid is None else bid.participate_tx.txid
found = ci_to.getLockTxHeight(participate_txid, addr, bid.amount_to, bid.chain_b_height_start, find_index=True) participate_txvout = None if bid.participate_tx is None or bid.participate_tx.vout is None else bid.participate_tx.vout
found = ci_to.getLockTxHeight(participate_txid, addr, bid.amount_to, bid.chain_b_height_start, find_index=True, vout=participate_txvout)
if found: if found:
index = found.get('index', participate_txvout)
if bid.participate_tx.conf != found['depth']: if bid.participate_tx.conf != found['depth']:
save_bid = True save_bid = True
if bid.participate_tx.conf is None and bid.participate_tx.state != TxStates.TX_SENT:
txid = found.get('txid', None if participate_txid is None else participate_txid.hex())
self.log.debug('Found bid %s participate txn %s in chain %s', bid_id.hex(), txid, Coins(coin_to).name)
self.addParticipateTxn(bid_id, bid, coin_to, txid, index, found['height'])
# Only update tx state if tx hasn't already been seen
if bid.participate_tx.state is None or bid.participate_tx.state < TxStates.TX_SENT:
bid.setPTxState(TxStates.TX_SENT)
bid.participate_tx.conf = found['depth'] bid.participate_tx.conf = found['depth']
index = found['index']
if bid.participate_tx is None or bid.participate_tx.txid is None:
self.log.debug('Found bid %s participate txn %s in chain %s', bid_id.hex(), found['txid'], Coins(coin_to).name)
self.addParticipateTxn(bid_id, bid, coin_to, found['txid'], found['index'], found['height'])
bid.setPTxState(TxStates.TX_SENT)
save_bid = True
if found['height'] > 0 and bid.participate_tx.block_height is None: if found['height'] > 0 and bid.participate_tx.block_height is None:
self.setTxBlockInfoFromHeight(ci_to, bid.participate_tx, found['height']) self.setTxBlockInfoFromHeight(ci_to, bid.participate_tx, found['height'])
@ -4047,13 +4142,17 @@ class BasicSwap(BaseApp):
self.logEvent(Concepts.BID, bid.bid_id, EventLogTypes.PTX_REFUND_PUBLISHED, '', None) self.logEvent(Concepts.BID, bid.bid_id, EventLogTypes.PTX_REFUND_PUBLISHED, '', None)
# State will update when spend is detected # State will update when spend is detected
except Exception as ex: except Exception as ex:
if 'non-BIP68-final' not in str(ex) and 'non-final' not in str(ex): if 'non-BIP68-final' not in str(ex) and 'non-final' not in str(ex) and 'locks on inputs not met' not in str(ex):
self.log.warning('Error trying to submit participate refund txn: %s', str(ex)) self.log.warning('Error trying to submit participate refund txn: %s', str(ex))
return False # Bid is still active return False # Bid is still active
def extractSecret(self, coin_type, bid, spend_in): def extractSecret(self, coin_type, bid, spend_in):
try: try:
if coin_type == Coins.PART or self.coin_clients[coin_type]['use_segwit']: if coin_type in (Coins.DCR, ):
script_sig = spend_in['scriptSig']['asm'].split(' ')
ensure(len(script_sig) == 5, 'Bad witness size')
return bytes.fromhex(script_sig[2])
elif coin_type in (Coins.PART, ) or self.coin_clients[coin_type]['use_segwit']:
ensure(len(spend_in['txinwitness']) == 5, 'Bad witness size') ensure(len(spend_in['txinwitness']) == 5, 'Bad witness size')
return bytes.fromhex(spend_in['txinwitness'][2]) return bytes.fromhex(spend_in['txinwitness'][2])
else: else:
@ -4064,7 +4163,7 @@ class BasicSwap(BaseApp):
return None return None
def addWatchedOutput(self, coin_type, bid_id, txid_hex, vout, tx_type, swap_type=None): def addWatchedOutput(self, coin_type, bid_id, txid_hex, vout, tx_type, swap_type=None):
self.log.debug('Adding watched output %s bid %s tx %s type %s', coin_type, bid_id.hex(), txid_hex, tx_type) self.log.debug('Adding watched output %s bid %s tx %s type %s', Coins(coin_type).name, bid_id.hex(), txid_hex, tx_type)
watched = self.coin_clients[coin_type]['watched_outputs'] watched = self.coin_clients[coin_type]['watched_outputs']
@ -4085,7 +4184,29 @@ class BasicSwap(BaseApp):
del self.coin_clients[coin_type]['watched_outputs'][i] del self.coin_clients[coin_type]['watched_outputs'][i]
self.log.debug('Removed watched output %s %s %s', Coins(coin_type).name, bid_id.hex(), wo.txid_hex) self.log.debug('Removed watched output %s %s %s', Coins(coin_type).name, bid_id.hex(), wo.txid_hex)
def initiateTxnSpent(self, bid_id: bytes, spend_txid: str, spend_n: int, spend_txn): def addWatchedScript(self, coin_type, bid_id, script, tx_type, swap_type=None):
self.log.debug('Adding watched script %s bid %s type %s', Coins(coin_type).name, bid_id.hex(), tx_type)
watched = self.coin_clients[coin_type]['watched_scripts']
for ws in watched:
if ws.bid_id == bid_id and ws.tx_type == tx_type and ws.script == script:
self.log.debug('Script already being watched.')
return
watched.append(WatchedScript(bid_id, script, tx_type, swap_type))
def removeWatchedScript(self, coin_type, bid_id: bytes, script: bytes) -> None:
# Remove all for bid if txid is None
self.log.debug('removeWatchedScript %s %s', Coins(coin_type).name, bid_id.hex())
old_len = len(self.coin_clients[coin_type]['watched_scripts'])
for i in range(old_len - 1, -1, -1):
ws = self.coin_clients[coin_type]['watched_scripts'][i]
if ws.bid_id == bid_id and (script is None or ws.script == script):
del self.coin_clients[coin_type]['watched_scripts'][i]
self.log.debug('Removed watched script %s %s', Coins(coin_type).name, bid_id.hex())
def initiateTxnSpent(self, bid_id: bytes, spend_txid: str, spend_n: int, spend_txn) -> None:
self.log.debug('Bid %s initiate txn spent by %s %d', bid_id.hex(), spend_txid, spend_n) self.log.debug('Bid %s initiate txn spent by %s %d', bid_id.hex(), spend_txid, spend_n)
if bid_id in self.swaps_in_progress: if bid_id in self.swaps_in_progress:
@ -4112,7 +4233,7 @@ class BasicSwap(BaseApp):
self.removeWatchedOutput(coin_from, bid_id, bid.initiate_tx.txid.hex()) self.removeWatchedOutput(coin_from, bid_id, bid.initiate_tx.txid.hex())
self.saveBid(bid_id, bid) self.saveBid(bid_id, bid)
def participateTxnSpent(self, bid_id: bytes, spend_txid: str, spend_n: int, spend_txn): def participateTxnSpent(self, bid_id: bytes, spend_txid: str, spend_n: int, spend_txn) -> None:
self.log.debug('Bid %s participate txn spent by %s %d', bid_id.hex(), spend_txid, spend_n) self.log.debug('Bid %s participate txn spent by %s %d', bid_id.hex(), spend_txid, spend_n)
# TODO: More SwapTypes # TODO: More SwapTypes
@ -4268,7 +4389,7 @@ class BasicSwap(BaseApp):
session.remove() session.remove()
self.mxDB.release() self.mxDB.release()
def processSpentOutput(self, coin_type, watched_output, spend_txid_hex, spend_n, spend_txn): def processSpentOutput(self, coin_type, watched_output, spend_txid_hex, spend_n, spend_txn) -> None:
if watched_output.swap_type == SwapTypes.XMR_SWAP: if watched_output.swap_type == SwapTypes.XMR_SWAP:
if watched_output.tx_type == TxTypes.XMR_SWAP_A_LOCK: if watched_output.tx_type == TxTypes.XMR_SWAP_A_LOCK:
self.process_XMR_SWAP_A_LOCK_tx_spend(watched_output.bid_id, spend_txid_hex, spend_txn['hex']) self.process_XMR_SWAP_A_LOCK_tx_spend(watched_output.bid_id, spend_txid_hex, spend_txn['hex'])
@ -4283,13 +4404,65 @@ class BasicSwap(BaseApp):
else: else:
self.initiateTxnSpent(watched_output.bid_id, spend_txid_hex, spend_n, spend_txn) self.initiateTxnSpent(watched_output.bid_id, spend_txid_hex, spend_n, spend_txn)
def processFoundScript(self, coin_type, watched_script, txid: bytes, vout: int) -> None:
if watched_script.tx_type == TxTypes.PTX:
if watched_script.bid_id in self.swaps_in_progress:
bid = self.swaps_in_progress[watched_script.bid_id][0]
bid.participate_tx.txid = txid
bid.participate_tx.vout = vout
bid.setPTxState(TxStates.TX_IN_CHAIN)
self.saveBid(watched_script.bid_id, bid)
else:
self.log.warning('Could not find active bid for found watched script: {}'.format(watched_script.bid_id.hex()))
else:
self.log.warning('Unknown found watched script tx type for bid {}'.format(watched_script.bid_id.hex()))
self.removeWatchedScript(coin_type, watched_script.bid_id, watched_script.script)
def checkNewBlock(self, coin_type, c):
pass
def haveCheckedPrevBlock(self, ci, c, block, session=None) -> bool:
previousblockhash = bytes.fromhex(block['previousblockhash'])
try:
use_session = self.openSession(session)
q = use_session.execute('SELECT COUNT(*) FROM checkedblocks WHERE block_hash = :block_hash', {'block_hash': previousblockhash}).first()
if q[0] > 0:
return True
finally:
if session is None:
self.closeSession(use_session, commit=False)
return False
def updateCheckedBlock(self, ci, cc, block, session=None) -> None:
now: int = self.getTime()
try:
use_session = self.openSession(session)
block_height = int(block['height'])
if cc['last_height_checked'] != block_height:
cc['last_height_checked'] = block_height
self.setIntKVInSession('last_height_checked_' + ci.coin_name().lower(), block_height, use_session)
query = '''INSERT INTO checkedblocks (created_at, coin_type, block_height, block_hash, block_time)
VALUES (:now, :coin_type, :block_height, :block_hash, :block_time)'''
use_session.execute(query, {'now': now, 'coin_type': int(ci.coin_type()), 'block_height': block_height, 'block_hash': bytes.fromhex(block['hash']), 'block_time': int(block['time'])})
finally:
if session is None:
self.closeSession(use_session)
def checkForSpends(self, coin_type, c): def checkForSpends(self, coin_type, c):
# assert (self.mxDB.locked()) # assert (self.mxDB.locked())
self.log.debug('checkForSpends %s', Coins(coin_type).name) self.log.debug('checkForSpends %s', Coins(coin_type).name)
# TODO: Check for spends on watchonly txns where possible # TODO: Check for spends on watchonly txns where possible
if self.coin_clients[coin_type].get('have_spent_index', False):
if 'have_spent_index' in self.coin_clients[coin_type] and self.coin_clients[coin_type]['have_spent_index']:
# TODO: batch getspentinfo # TODO: batch getspentinfo
for o in c['watched_outputs']: for o in c['watched_outputs']:
found_spend = None found_spend = None
@ -4304,39 +4477,56 @@ class BasicSwap(BaseApp):
spend_n = found_spend['index'] spend_n = found_spend['index']
spend_txn = self.callcoinrpc(Coins.PART, 'getrawtransaction', [spend_txid, True]) spend_txn = self.callcoinrpc(Coins.PART, 'getrawtransaction', [spend_txid, True])
self.processSpentOutput(coin_type, o, spend_txid, spend_n, spend_txn) self.processSpentOutput(coin_type, o, spend_txid, spend_n, spend_txn)
else: return
ci = self.ci(coin_type)
chain_blocks = ci.getChainHeight() ci = self.ci(coin_type)
last_height_checked = c['last_height_checked'] chain_blocks = ci.getChainHeight()
self.log.debug('chain_blocks, last_height_checked %s %s', chain_blocks, last_height_checked) last_height_checked: int = c['last_height_checked']
while last_height_checked < chain_blocks: block_check_min_time: int = c['block_check_min_time']
block_hash = self.callcoinrpc(coin_type, 'getblockhash', [last_height_checked + 1]) self.log.debug('chain_blocks, last_height_checked %d %d', chain_blocks, last_height_checked)
try:
block = ci.getBlockWithTxns(block_hash) while last_height_checked < chain_blocks:
except Exception as e: block_hash = ci.rpc('getblockhash', [last_height_checked + 1])
if 'Block not available (pruned data)' in str(e): try:
# TODO: Better solution? block = ci.getBlockWithTxns(block_hash)
bci = self.callcoinrpc(coin_type, 'getblockchaininfo') except Exception as e:
self.log.error('Coin %s last_height_checked %d set to pruneheight %d', self.ci(coin_type).coin_name(), last_height_checked, bci['pruneheight']) if 'Block not available (pruned data)' in str(e):
last_height_checked = bci['pruneheight'] # TODO: Better solution?
continue bci = self.callcoinrpc(coin_type, 'getblockchaininfo')
else: self.log.error('Coin %s last_height_checked %d set to pruneheight %d', self.ci(coin_type).coin_name(), last_height_checked, bci['pruneheight'])
self.logException(f'getblock error {e}') last_height_checked = bci['pruneheight']
break continue
else:
self.logException(f'getblock error {e}')
break
if block_check_min_time > block['time'] or last_height_checked < 1:
pass
elif not self.haveCheckedPrevBlock(ci, c, block):
last_height_checked -= 1
self.log.debug('Have not seen previousblockhash {} for block {}'.format(block['previousblockhash'], block['hash']))
continue
for tx in block['tx']:
for s in c['watched_scripts']:
for i, txo in enumerate(tx['vout']):
if 'scriptPubKey' in txo and 'hex' in txo['scriptPubKey']:
# TODO: Optimise by loading rawtx in CTransaction
if bytes.fromhex(txo['scriptPubKey']['hex']) == s.script:
self.log.debug('Found script from search for bid %s: %s %d', s.bid_id.hex(), tx['txid'], i)
self.processFoundScript(coin_type, s, bytes.fromhex(tx['txid']), i)
for tx in block['tx']: for o in c['watched_outputs']:
for i, inp in enumerate(tx['vin']): for i, inp in enumerate(tx['vin']):
for o in c['watched_outputs']: inp_txid = inp.get('txid', None)
inp_txid = inp.get('txid', None) if inp_txid is None: # Coinbase
if inp_txid is None: # Coinbase continue
continue if inp_txid == o.txid_hex and inp['vout'] == o.vout:
if inp_txid == o.txid_hex and inp['vout'] == o.vout: self.log.debug('Found spend from search %s %d in %s %d', o.txid_hex, o.vout, tx['txid'], i)
self.log.debug('Found spend from search %s %d in %s %d', o.txid_hex, o.vout, tx['txid'], i) self.processSpentOutput(coin_type, o, tx['txid'], i, tx)
self.processSpentOutput(coin_type, o, tx['txid'], i, tx)
last_height_checked += 1 last_height_checked += 1
if c['last_height_checked'] != last_height_checked: self.updateCheckedBlock(ci, c, block)
c['last_height_checked'] = last_height_checked
self.setIntKV('last_height_checked_' + ci.coin_name().lower(), last_height_checked)
def expireMessages(self) -> None: def expireMessages(self) -> None:
if self._is_locked is True: if self._is_locked is True:
@ -4572,7 +4762,7 @@ class BasicSwap(BaseApp):
return return
offer_data.ParseFromString(offer_bytes) offer_data.ParseFromString(offer_bytes)
# Validate data # Validate offer data
now: int = self.getTime() now: int = self.getTime()
coin_from = Coins(offer_data.coin_from) coin_from = Coins(offer_data.coin_from)
ci_from = self.ci(coin_from) ci_from = self.ci(coin_from)
@ -4839,7 +5029,7 @@ class BasicSwap(BaseApp):
bid_data = BidMessage() bid_data = BidMessage()
bid_data.ParseFromString(bid_bytes) bid_data.ParseFromString(bid_bytes)
# Validate data # Validate bid data
ensure(bid_data.protocol_version >= MINPROTO_VERSION_SECRET_HASH, 'Invalid protocol version') ensure(bid_data.protocol_version >= MINPROTO_VERSION_SECRET_HASH, 'Invalid protocol version')
ensure(len(bid_data.offer_msg_id) == 28, 'Bad offer_id length') ensure(len(bid_data.offer_msg_id) == 28, 'Bad offer_id length')
@ -4899,6 +5089,9 @@ class BasicSwap(BaseApp):
chain_a_height_start=ci_from.getChainHeight(), chain_a_height_start=ci_from.getChainHeight(),
chain_b_height_start=ci_to.getChainHeight(), chain_b_height_start=ci_to.getChainHeight(),
) )
if len(bid_data.pkhash_buyer_to) > 0:
bid.pkhash_buyer_to = bid_data.pkhash_buyer_to
else: else:
ensure(bid.state == BidStates.BID_SENT, 'Wrong bid state: {}'.format(BidStates(bid.state).name)) ensure(bid.state == BidStates.BID_SENT, 'Wrong bid state: {}'.format(BidStates(bid.state).name))
bid.created_at = msg['sent'] bid.created_at = msg['sent']
@ -4952,33 +5145,29 @@ class BasicSwap(BaseApp):
use_csv = True if offer.lock_type < TxLockTypes.ABS_LOCK_BLOCKS else False use_csv = True if offer.lock_type < TxLockTypes.ABS_LOCK_BLOCKS else False
# TODO: Verify script without decoding? if coin_from in (Coins.DCR, ):
decoded_script = self.callcoinrpc(Coins.PART, 'decodescript', [bid_accept_data.contract_script.hex()]) op_hash = OpCodes.OP_SHA256_DECRED
lock_check_op = 'OP_CHECKSEQUENCEVERIFY' if use_csv else 'OP_CHECKLOCKTIMEVERIFY' else:
prog = re.compile(r'OP_IF OP_SIZE 32 OP_EQUALVERIFY OP_SHA256 (\w+) OP_EQUALVERIFY OP_DUP OP_HASH160 (\w+) OP_ELSE (\d+) {} OP_DROP OP_DUP OP_HASH160 (\w+) OP_ENDIF OP_EQUALVERIFY OP_CHECKSIG'.format(lock_check_op)) op_hash = OpCodes.OP_SHA256
rr = prog.match(decoded_script['asm']) op_lock = OpCodes.OP_CHECKSEQUENCEVERIFY if use_csv else OpCodes.OP_CHECKLOCKTIMEVERIFY
if not rr: script_valid, script_hash, script_pkhash1, script_lock_val, script_pkhash2 = atomic_swap_1.verifyContractScript(bid_accept_data.contract_script, op_lock=op_lock, op_hash=op_hash)
if not script_valid:
raise ValueError('Bad script') raise ValueError('Bad script')
scriptvalues = rr.groups()
ensure(len(scriptvalues[0]) == 64, 'Bad secret_hash length') ensure(script_pkhash1 == bid.pkhash_buyer, 'pkhash_buyer mismatch')
ensure(bytes.fromhex(scriptvalues[1]) == bid.pkhash_buyer, 'pkhash_buyer mismatch')
script_lock_value = int(scriptvalues[2])
if use_csv: if use_csv:
expect_sequence = ci_from.getExpectedSequence(offer.lock_type, offer.lock_value) expect_sequence = ci_from.getExpectedSequence(offer.lock_type, offer.lock_value)
ensure(script_lock_value == expect_sequence, 'sequence mismatch') ensure(script_lock_val == expect_sequence, 'sequence mismatch')
else: else:
if offer.lock_type == TxLockTypes.ABS_LOCK_BLOCKS: if offer.lock_type == TxLockTypes.ABS_LOCK_BLOCKS:
block_header_from = ci_from.getBlockHeaderAt(now) block_header_from = ci_from.getBlockHeaderAt(now)
chain_height_at_bid_creation = block_header_from['height'] chain_height_at_bid_creation = block_header_from['height']
ensure(script_lock_value <= chain_height_at_bid_creation + offer.lock_value + atomic_swap_1.ABS_LOCK_BLOCKS_LEEWAY, 'script lock height too high') ensure(script_lock_val <= chain_height_at_bid_creation + offer.lock_value + atomic_swap_1.ABS_LOCK_BLOCKS_LEEWAY, 'script lock height too high')
ensure(script_lock_value >= chain_height_at_bid_creation + offer.lock_value - atomic_swap_1.ABS_LOCK_BLOCKS_LEEWAY, 'script lock height too low') ensure(script_lock_val >= chain_height_at_bid_creation + offer.lock_value - atomic_swap_1.ABS_LOCK_BLOCKS_LEEWAY, 'script lock height too low')
else: else:
ensure(script_lock_value <= now + offer.lock_value + atomic_swap_1.INITIATE_TX_TIMEOUT, 'script lock time too high') ensure(script_lock_val <= now + offer.lock_value + atomic_swap_1.INITIATE_TX_TIMEOUT, 'script lock time too high')
ensure(script_lock_value >= now + offer.lock_value - atomic_swap_1.ABS_LOCK_TIME_LEEWAY, 'script lock time too low') ensure(script_lock_val >= now + offer.lock_value - atomic_swap_1.ABS_LOCK_TIME_LEEWAY, 'script lock time too low')
ensure(len(scriptvalues[3]) == 40, 'pkhash_refund bad length')
ensure(self.countMessageLinks(Concepts.BID, bid_id, MessageTypes.BID_ACCEPT) == 0, 'Bid already accepted') ensure(self.countMessageLinks(Concepts.BID, bid_id, MessageTypes.BID_ACCEPT) == 0, 'Bid already accepted')
@ -4991,7 +5180,12 @@ class BasicSwap(BaseApp):
txid=bid_accept_data.initiate_txid, txid=bid_accept_data.initiate_txid,
script=bid_accept_data.contract_script, script=bid_accept_data.contract_script,
) )
bid.pkhash_seller = bytes.fromhex(scriptvalues[3])
if len(bid_accept_data.pkhash_seller) == 20:
bid.pkhash_seller = bid_accept_data.pkhash_seller
else:
bid.pkhash_seller = script_pkhash2
bid.setState(BidStates.BID_ACCEPTED) bid.setState(BidStates.BID_ACCEPTED)
bid.setITxState(TxStates.TX_NONE) bid.setITxState(TxStates.TX_NONE)
@ -5322,7 +5516,7 @@ class BasicSwap(BaseApp):
reverse_bid: bool = self.is_reverse_ads_bid(offer.coin_from) reverse_bid: bool = self.is_reverse_ads_bid(offer.coin_from)
coin_from = Coins(offer.coin_to if reverse_bid else offer.coin_from) coin_from = Coins(offer.coin_to if reverse_bid else offer.coin_from)
self.setLastHeightChecked(coin_from, bid.chain_a_height_start) self.setLastHeightCheckedStart(coin_from, bid.chain_a_height_start)
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, bid.xmr_a_lock_tx.txid.hex(), bid.xmr_a_lock_tx.vout, TxTypes.XMR_SWAP_A_LOCK, SwapTypes.XMR_SWAP)
lock_refund_vout = self.ci(coin_from).getLockRefundTxSwapOutput(xmr_swap) lock_refund_vout = self.ci(coin_from).getLockRefundTxSwapOutput(xmr_swap)
@ -6351,9 +6545,9 @@ class BasicSwap(BaseApp):
if now - self._last_checked_watched >= self.check_watched_seconds: if now - self._last_checked_watched >= self.check_watched_seconds:
for k, c in self.coin_clients.items(): for k, c in self.coin_clients.items():
if k == Coins.PART_ANON or k == Coins.PART_BLIND: if k == Coins.PART_ANON or k == Coins.PART_BLIND or k == Coins.LTC_MWEB:
continue continue
if len(c['watched_outputs']) > 0: if len(c['watched_outputs']) > 0 or len(c['watched_scripts']):
self.checkForSpends(k, c) self.checkForSpends(k, c)
self._last_checked_watched = now self._last_checked_watched = now

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

@ -11,7 +11,7 @@ from enum import IntEnum, auto
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
CURRENT_DB_VERSION = 23 CURRENT_DB_VERSION = 24
CURRENT_DB_DATA_VERSION = 4 CURRENT_DB_DATA_VERSION = 4
Base = declarative_base() Base = declarative_base()
@ -127,6 +127,7 @@ class Bid(Base):
amount_to = sa.Column(sa.BigInteger) # amount * offer.rate amount_to = sa.Column(sa.BigInteger) # amount * offer.rate
pkhash_buyer = sa.Column(sa.LargeBinary) pkhash_buyer = sa.Column(sa.LargeBinary)
pkhash_buyer_to = sa.Column(sa.LargeBinary) # Used for the ptx if coin pubkey hashes differ
amount = sa.Column(sa.BigInteger) amount = sa.Column(sa.BigInteger)
rate = sa.Column(sa.BigInteger) rate = sa.Column(sa.BigInteger)
@ -522,3 +523,14 @@ class MessageLink(Base):
msg_type = sa.Column(sa.Integer) msg_type = sa.Column(sa.Integer)
msg_sequence = sa.Column(sa.Integer) msg_sequence = sa.Column(sa.Integer)
msg_id = sa.Column(sa.LargeBinary) msg_id = sa.Column(sa.LargeBinary)
class CheckedBlock(Base):
__tablename__ = 'checkedblocks'
record_id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)
created_at = sa.Column(sa.BigInteger)
coin_type = sa.Column(sa.Integer)
block_height = sa.Column(sa.Integer)
block_hash = sa.Column(sa.LargeBinary)
block_time = sa.Column(sa.BigInteger)

@ -300,6 +300,18 @@ def upgradeDatabase(self, db_version):
elif current_version == 22: elif current_version == 22:
db_version += 1 db_version += 1
session.execute('ALTER TABLE offers ADD COLUMN amount_to INTEGER') session.execute('ALTER TABLE offers ADD COLUMN amount_to INTEGER')
elif current_version == 23:
db_version += 1
session.execute('''
CREATE TABLE checkedblocks (
record_id INTEGER NOT NULL,
created_at BIGINT,
coin_type INTEGER,
block_height INTEGER,
block_hash BLOB,
block_time INTEGER,
PRIMARY KEY (record_id))''')
session.execute('ALTER TABLE bids ADD COLUMN pkhash_buyer_to BLOB')
if current_version != db_version: if current_version != db_version:
self.db_version = db_version self.db_version = db_version
self.setIntKVInSession('db_version', db_version, session) self.setIntKVInSession('db_version', db_version, session)

@ -52,5 +52,7 @@ def remove_expired_data(self, time_offset: int = 0):
if num_offers > 0 or num_bids > 0: if num_offers > 0 or num_bids > 0:
self.log.info('Removed data for {} expired offer{} and {} bid{}.'.format(num_offers, 's' if num_offers != 1 else '', num_bids, 's' if num_bids != 1 else '')) self.log.info('Removed data for {} expired offer{} and {} bid{}.'.format(num_offers, 's' if num_offers != 1 else '', num_bids, 's' if num_bids != 1 else ''))
session.execute('DELETE FROM checkedblocks WHERE created_at <= :expired_at', {'expired_at': now - time_offset})
finally: finally:
self.closeSession(session) self.closeSession(session)

@ -19,6 +19,9 @@ from basicswap.util import (
format_amount, format_amount,
TemporaryError, TemporaryError,
) )
from basicswap.util.crypto import (
hash160,
)
from basicswap.util.ecc import ( from basicswap.util.ecc import (
ep, ep,
getSecretInt, getSecretInt,
@ -37,6 +40,10 @@ class Curves(IntEnum):
class CoinInterface: class CoinInterface:
@staticmethod
def watch_blocks_for_scripts() -> bool:
return False
def __init__(self, network): def __init__(self, network):
self.setDefaults() self.setDefaults()
self._network = network self._network = network
@ -101,6 +108,10 @@ class CoinInterface:
def has_segwit(self) -> bool: def has_segwit(self) -> bool:
return chainparams[self.coin_type()].get('has_segwit', True) return chainparams[self.coin_type()].get('has_segwit', True)
def use_p2shp2wsh(self) -> bool:
# p2sh-p2wsh
return False
def is_transient_error(self, ex) -> bool: def is_transient_error(self, ex) -> bool:
if isinstance(ex, TemporaryError): if isinstance(ex, TemporaryError):
return True return True
@ -128,6 +139,16 @@ class CoinInterface:
def walletRestoreHeight(self) -> int: def walletRestoreHeight(self) -> int:
return self._restore_height return self._restore_height
def get_connection_type(self):
return self._connection_type
def using_segwit(self) -> bool:
# Using btc native segwit
return self._use_segwit
def use_tx_vsize(self) -> bool:
return self._use_segwit
class Secp256k1Interface(CoinInterface): class Secp256k1Interface(CoinInterface):
@staticmethod @staticmethod
@ -137,9 +158,12 @@ class Secp256k1Interface(CoinInterface):
def getNewSecretKey(self) -> bytes: def getNewSecretKey(self) -> bytes:
return i2b(getSecretInt()) return i2b(getSecretInt())
def getPubkey(self, privkey): def getPubkey(self, privkey: bytes) -> bytes:
return PublicKey.from_secret(privkey).format() return PublicKey.from_secret(privkey).format()
def pkh(self, pubkey: bytes) -> bytes:
return hash160(pubkey)
def verifyKey(self, k: bytes) -> bool: def verifyKey(self, k: bytes) -> bool:
i = b2i(k) i = b2i(k)
return (i < ep.o and i > 0) return (i < ep.o and i > 0)

@ -66,8 +66,8 @@ from basicswap.contrib.test_framework.messages import (
CTxIn, CTxIn,
CTxInWitness, CTxInWitness,
CTxOut, CTxOut,
uint256_from_str) uint256_from_str,
)
from basicswap.contrib.test_framework.script import ( from basicswap.contrib.test_framework.script import (
CScript, CScriptOp, CScript, CScriptOp,
OP_IF, OP_ELSE, OP_ENDIF, OP_IF, OP_ELSE, OP_ENDIF,
@ -231,17 +231,6 @@ class BTCInterface(Secp256k1Interface):
return len(wallets) return len(wallets)
def using_segwit(self) -> bool:
# Using btc native segwit
return self._use_segwit
def use_p2shp2wsh(self) -> bool:
# p2sh-p2wsh
return False
def get_connection_type(self):
return self._connection_type
def open_rpc(self, wallet=None): def open_rpc(self, wallet=None):
return openrpc(self._rpcport, self._rpcauth, wallet=wallet, host=self._rpc_host) return openrpc(self._rpcport, self._rpcauth, wallet=wallet, host=self._rpc_host)
@ -1163,7 +1152,7 @@ class BTCInterface(Secp256k1Interface):
lock_tx_dest = self.getScriptDest(lock_script) lock_tx_dest = self.getScriptDest(lock_script)
return self.encodeScriptDest(lock_tx_dest) return self.encodeScriptDest(lock_tx_dest)
def getLockTxHeight(self, txid, dest_address, bid_amount, rescan_from, find_index: bool = False): def getLockTxHeight(self, txid, dest_address, bid_amount, rescan_from, find_index: bool = False, vout: int = -1):
# Add watchonly address and rescan if required # Add watchonly address and rescan if required
if not self.isAddressMine(dest_address, or_watch_only=True): if not self.isAddressMine(dest_address, or_watch_only=True):
@ -1509,7 +1498,7 @@ class BTCInterface(Secp256k1Interface):
return {'txid': txid_hex, 'amount': 0, 'height': rv['blockheight']} return {'txid': txid_hex, 'amount': 0, 'height': rv['blockheight']}
return None return None
def createRedeemTxn(self, prevout, output_addr: str, output_value: int) -> str: def createRedeemTxn(self, prevout, output_addr: str, output_value: int, txn_script: bytes = None) -> str:
tx = CTransaction() tx = CTransaction()
tx.nVersion = self.txVersion() tx.nVersion = self.txVersion()
prev_txid = uint256_from_str(bytes.fromhex(prevout['txid'])[::-1]) prev_txid = uint256_from_str(bytes.fromhex(prevout['txid'])[::-1])
@ -1520,7 +1509,7 @@ class BTCInterface(Secp256k1Interface):
tx.rehash() tx.rehash()
return tx.serialize().hex() return tx.serialize().hex()
def createRefundTxn(self, prevout, output_addr: str, output_value: int, locktime: int, sequence: int) -> str: def createRefundTxn(self, prevout, output_addr: str, output_value: int, locktime: int, sequence: int, txn_script: bytes = None) -> str:
tx = CTransaction() tx = CTransaction()
tx.nVersion = self.txVersion() tx.nVersion = self.txVersion()
tx.nLockTime = locktime tx.nLockTime = locktime

@ -15,6 +15,9 @@ from basicswap.basicswap_util import (
TxLockTypes TxLockTypes
) )
from basicswap.chainparams import Coins from basicswap.chainparams import Coins
from basicswap.contrib.test_framework.messages import (
uint256_from_str,
)
from basicswap.interface.btc import Secp256k1Interface from basicswap.interface.btc import Secp256k1Interface
from basicswap.util import ( from basicswap.util import (
ensure, ensure,
@ -34,8 +37,22 @@ from basicswap.util.script import (
from basicswap.util.extkey import ExtKeyPair from basicswap.util.extkey import ExtKeyPair
from basicswap.util.integer import encode_varint from basicswap.util.integer import encode_varint
from basicswap.interface.dcr.rpc import make_rpc_func from basicswap.interface.dcr.rpc import make_rpc_func
from .messages import CTransaction, CTxOut, SigHashType, TxSerializeType from .messages import (
from .script import push_script_data, OP_HASH160, OP_EQUAL, OP_DUP, OP_EQUALVERIFY, OP_CHECKSIG CTransaction,
CTxIn,
CTxOut,
COutPoint,
SigHashType,
TxSerializeType,
)
from .script import (
push_script_data,
OP_HASH160,
OP_EQUAL,
OP_DUP,
OP_EQUALVERIFY,
OP_CHECKSIG,
)
from coincurve.keys import ( from coincurve.keys import (
PrivateKey, PrivateKey,
@ -124,6 +141,20 @@ def DCRSignatureHash(sign_script: bytes, hash_type: SigHashType, tx: CTransactio
return blake256(hash_buffer) return blake256(hash_buffer)
def extract_sig_and_pk(sig_script: bytes) -> (bytes, bytes):
sig = None
pk = None
o: int = 0
num_bytes = sig_script[o]
o += 1
sig = sig_script[o: o + num_bytes]
o += num_bytes
num_bytes = sig_script[o]
o += 1
pk = sig_script[o: o + num_bytes]
return sig, pk
class DCRInterface(Secp256k1Interface): class DCRInterface(Secp256k1Interface):
@staticmethod @staticmethod
@ -168,6 +199,10 @@ class DCRInterface(Secp256k1Interface):
return secondsLocked | SEQUENCE_LOCKTIME_TYPE_FLAG return secondsLocked | SEQUENCE_LOCKTIME_TYPE_FLAG
raise ValueError('Unknown lock type') raise ValueError('Unknown lock type')
@staticmethod
def watch_blocks_for_scripts() -> bool:
return True
def __init__(self, coin_settings, network, swap_client=None): def __init__(self, coin_settings, network, swap_client=None):
super().__init__(network) super().__init__(network)
self._rpc_host = coin_settings.get('rpchost', '127.0.0.1') self._rpc_host = coin_settings.get('rpchost', '127.0.0.1')
@ -183,7 +218,11 @@ class DCRInterface(Secp256k1Interface):
self.blocks_confirmed = coin_settings['blocks_confirmed'] self.blocks_confirmed = coin_settings['blocks_confirmed']
self.setConfTarget(coin_settings['conf_target']) self.setConfTarget(coin_settings['conf_target'])
self._use_segwit = coin_settings['use_segwit'] self._use_segwit = True # Decred is natively segwit
self._connection_type = coin_settings['connection_type']
def use_tx_vsize(self) -> bool:
return False
def pkh(self, pubkey: bytes) -> bytes: def pkh(self, pubkey: bytes) -> bytes:
return ripemd160(blake256(pubkey)) return ripemd160(blake256(pubkey))
@ -235,9 +274,6 @@ class DCRInterface(Secp256k1Interface):
def getBlockchainInfo(self): def getBlockchainInfo(self):
return self.rpc('getblockchaininfo') return self.rpc('getblockchaininfo')
def using_segwit(self) -> bool:
return self._use_segwit
def getWalletInfo(self): def getWalletInfo(self):
rv = {} rv = {}
rv = self.rpc_wallet('getinfo') rv = self.rpc_wallet('getinfo')
@ -276,6 +312,13 @@ class DCRInterface(Secp256k1Interface):
raise ValueError('Checksum mismatch') raise ValueError('Checksum mismatch')
return key[2], key[3:] return key[2], key[3:]
def encodeKey(self, key_bytes: bytes) -> str:
wif_prefix = self.chainparams_network()['key_prefix']
key_type = 0 # STEcdsaSecp256k1
b = wif_prefix.to_bytes(2, 'big') + key_type.to_bytes(1) + key_bytes
b += blake256(b)[:4]
return b58encode(b)
def loadTx(self, tx_bytes: bytes) -> CTransaction: def loadTx(self, tx_bytes: bytes) -> CTransaction:
tx = CTransaction() tx = CTransaction()
tx.deserialize(tx_bytes) tx.deserialize(tx_bytes)
@ -288,14 +331,21 @@ class DCRInterface(Secp256k1Interface):
eck = PrivateKey(key_bytes) eck = PrivateKey(key_bytes)
return eck.sign(sig_hash, hasher=None) + bytes((SigHashType.SigHashAll,)) return eck.sign(sig_hash, hasher=None) + bytes((SigHashType.SigHashAll,))
def setTxSignature(self, tx_bytes: bytes, stack, txi: int = 0) -> bytes: def setTxSignatureScript(self, tx_bytes: bytes, script: bytes, txi: int = 0) -> bytes:
tx = self.loadTx(tx_bytes) tx = self.loadTx(tx_bytes)
tx.vin[txi].signature_script = script
return tx.serialize()
def setTxSignature(self, tx_bytes: bytes, stack, txi: int = 0) -> bytes:
tx = self.loadTx(tx_bytes)
script_data = bytearray() script_data = bytearray()
for data in stack: for data in stack:
push_script_data(script_data, data) push_script_data(script_data, data)
tx.vin[txi].signature_script = script_data tx.vin[txi].signature_script = script_data
test_ser = tx.serialize()
test_tx = self.loadTx(test_ser)
return tx.serialize() return tx.serialize()
@ -310,6 +360,20 @@ class DCRInterface(Secp256k1Interface):
return sig.hex() return sig.hex()
def verifyTxSig(self, tx_bytes: bytes, sig: bytes, K: bytes, input_n: int, prevout_script: bytes, prevout_value: int) -> bool:
tx = self.loadTx(tx_bytes)
sig_hash = DCRSignatureHash(prevout_script, SigHashType.SigHashAll, tx, input_n)
pubkey = PublicKey(K)
return pubkey.verify(sig[: -1], sig_hash, hasher=None) # Pop the hashtype byte
def getTxid(self, tx) -> bytes:
if isinstance(tx, str):
tx = bytes.fromhex(tx)
if isinstance(tx, bytes):
tx = self.loadTx(tx)
return tx.TxHash()
def getScriptDest(self, script: bytes) -> bytes: def getScriptDest(self, script: bytes) -> bytes:
# P2SH # P2SH
script_hash = self.pkh(script) script_hash = self.pkh(script)
@ -323,7 +387,6 @@ class DCRInterface(Secp256k1Interface):
def getPubkeyHashDest(self, pkh: bytes) -> bytes: def getPubkeyHashDest(self, pkh: bytes) -> bytes:
# P2PKH # P2PKH
assert len(pkh) == 20 assert len(pkh) == 20
return OP_DUP.to_bytes(1) + OP_HASH160.to_bytes(1) + len(pkh).to_bytes(1) + pkh + OP_EQUALVERIFY.to_bytes(1) + OP_CHECKSIG.to_bytes(1) return OP_DUP.to_bytes(1) + OP_HASH160.to_bytes(1) + len(pkh).to_bytes(1) + pkh + OP_EQUALVERIFY.to_bytes(1) + OP_CHECKSIG.to_bytes(1)
@ -424,7 +487,7 @@ class DCRInterface(Secp256k1Interface):
return self.rpc_wallet('sendtoaddress', params) return self.rpc_wallet('sendtoaddress', params)
def isAddressMine(self, address: str, or_watch_only: bool = False) -> bool: def isAddressMine(self, address: str, or_watch_only: bool = False) -> bool:
addr_info = self.rpc('validateaddress', [address]) addr_info = self.rpc_wallet('validateaddress', [address])
return addr_info.get('ismine', False) return addr_info.get('ismine', False)
def encodeProofUtxos(self, proof_utxos): def encodeProofUtxos(self, proof_utxos):
@ -504,18 +567,133 @@ class DCRInterface(Secp256k1Interface):
txn_funded = self.createRawFundedTransaction(addr_to, amount) txn_funded = self.createRawFundedTransaction(addr_to, amount)
return self.rpc_wallet('signrawtransaction', [txn_funded])['hex'] return self.rpc_wallet('signrawtransaction', [txn_funded])['hex']
def getLockTxHeight(self, txid, dest_address, bid_amount, rescan_from, find_index: bool = False): def getLockTxHeight(self, txid, dest_address, bid_amount, rescan_from, find_index: bool = False, vout: int = -1):
self._log.debug('TODO: getLockTxHeight') if txid is None:
return None self._log.debug('TODO: getLockTxHeight')
return None
found_vout = None
# Search for txo at vout 0 and 1 if vout is not known
if vout is None:
test_range = range(2)
else:
test_range = (vout, )
for try_vout in test_range:
try:
txout = self.rpc('gettxout', [txid.hex(), try_vout, 0, True])
addresses = txout['scriptPubKey']['addresses']
if len(addresses) != 1 or addresses[0] != dest_address:
continue
if self.make_int(txout['value']) != bid_amount:
self._log.warning('getLockTxHeight found txout {} with incorrect amount {}'.format(txid.hex(), txout['value']))
continue
found_vout = try_vout
break
except Exception as e:
# self._log.warning('gettxout {}'.format(e))
return None
block_height: int = 0
confirmations: int = 0 if 'confirmations' not in txout else txout['confirmations']
# TODO: Better way?
if confirmations > 0:
block_height = self.getChainHeight() - confirmations
rv = {
'depth': confirmations,
'index': found_vout,
'height': block_height}
return rv
def find_prevout_info(self, txn_hex: str, txn_script: bytes): def find_prevout_info(self, txn_hex: str, txn_script: bytes):
txjs = self.rpc('decoderawtransaction', [txn_hex]) txjs = self.rpc('decoderawtransaction', [txn_hex])
n = getVoutByScriptPubKey(txjs, self.getScriptDest(txn_script).hex()) n = getVoutByScriptPubKey(txjs, self.getScriptDest(txn_script).hex())
txo = txjs['vout'][n]
return { return {
'txid': txjs['txid'], 'txid': txjs['txid'],
'vout': n, 'vout': n,
'scriptPubKey': txjs['vout'][n]['scriptPubKey']['hex'], 'scriptPubKey': txo['scriptPubKey']['hex'],
'redeemScript': txn_script.hex(), 'redeemScript': txn_script.hex(),
'amount': txjs['vout'][n]['value'] 'amount': txo['value'],
} }
def getHTLCSpendTxVSize(self, redeem: bool = True) -> int:
tx_vsize = 5 # Add a few bytes, sequence in script takes variable amount of bytes
tx_vsize += 348 if redeem else 316
return tx_vsize
def createRedeemTxn(self, prevout, output_addr: str, output_value: int, txn_script: bytes = None) -> str:
tx = CTransaction()
tx.version = self.txVersion()
prev_txid = uint256_from_str(bytes.fromhex(prevout['txid'])[::-1])
tx.vin.append(CTxIn(COutPoint(prev_txid, prevout['vout'], 0)))
pkh = self.decode_address(output_addr)[2:]
script = self.getPubkeyHashDest(pkh)
tx.vout.append(self.txoType()(output_value, script))
return tx.serialize().hex()
def createRefundTxn(self, prevout, output_addr: str, output_value: int, locktime: int, sequence: int, txn_script: bytes = None) -> str:
tx = CTransaction()
tx.version = self.txVersion()
tx.locktime = locktime
prev_txid = uint256_from_str(bytes.fromhex(prevout['txid'])[::-1])
tx.vin.append(CTxIn(COutPoint(prev_txid, prevout['vout'], 0), sequence=sequence,))
pkh = self.decode_address(output_addr)[2:]
script = self.getPubkeyHashDest(pkh)
tx.vout.append(self.txoType()(output_value, script))
return tx.serialize().hex()
def verifyRawTransaction(self, tx_hex: str, prevouts):
inputs_valid: bool = True
validscripts: int = 0
tx_bytes = bytes.fromhex(tx_hex)
tx = self.loadTx(bytes.fromhex(tx_hex))
for i, txi in enumerate(tx.vin):
prevout_data = prevouts[i]
redeem_script = bytes.fromhex(prevout_data['redeemScript'])
prevout_value = self.make_int(prevout_data['amount'])
sig, pk = extract_sig_and_pk(txi.signature_script)
if not sig or not pk:
self._log.warning(f'verifyRawTransaction failed to extract signature for input {i}')
continue
if self.verifyTxSig(tx_bytes, sig, pk, i, redeem_script, prevout_value):
validscripts += 1
# TODO: validate inputs
inputs_valid = True
return {
'inputs_valid': inputs_valid,
'validscripts': validscripts,
}
def getBlockHeaderFromHeight(self, height):
block_hash = self.rpc('getblockhash', [height])
return self.rpc('getblockheader', [block_hash])
def getBlockWithTxns(self, block_hash: str):
block = self.rpc('getblock', [block_hash, True, True])
return {
'hash': block['hash'],
'previousblockhash': block['previousblockhash'],
'tx': block['rawtx'],
'confirmations': block['confirmations'],
'height': block['height'],
'time': block['time'],
'version': block['version'],
'merkleroot': block['merkleroot'],
}
def publishTx(self, tx: bytes):
return self.rpc('sendrawtransaction', [tx.hex()])
def describeTx(self, tx_hex: str):
return self.rpc('decoderawtransaction', [tx_hex])

@ -8,7 +8,7 @@
import copy import copy
from enum import IntEnum from enum import IntEnum
from basicswap.util.crypto import blake256 from basicswap.util.crypto import blake256
from basicswap.util.integer import decode_varint, encode_varint from basicswap.util.integer import decode_compactsize, encode_compactsize
class TxSerializeType(IntEnum): class TxSerializeType(IntEnum):
@ -86,12 +86,12 @@ class CTransaction:
def deserialize(self, data: bytes) -> None: def deserialize(self, data: bytes) -> None:
version = int.from_bytes(data[:4], 'little') version = int.from_bytes(data[:4], 'little')
self.version = self.version & 0xffff self.version = version & 0xffff
ser_type: int = version >> 16 ser_type: int = version >> 16
o = 4 o = 4
if ser_type == TxSerializeType.Full or ser_type == TxSerializeType.NoWitness: if ser_type == TxSerializeType.Full or ser_type == TxSerializeType.NoWitness:
num_txin, nb = decode_varint(data, o) num_txin, nb = decode_compactsize(data, o)
o += nb o += nb
for i in range(num_txin): for i in range(num_txin):
@ -107,7 +107,7 @@ class CTransaction:
o += 4 o += 4
self.vin.append(txi) self.vin.append(txi)
num_txout, nb = decode_varint(data, o) num_txout, nb = decode_compactsize(data, o)
o += nb o += nb
for i in range(num_txout): for i in range(num_txout):
@ -116,7 +116,7 @@ class CTransaction:
o += 8 o += 8
txo.version = int.from_bytes(data[o:o + 2], 'little') txo.version = int.from_bytes(data[o:o + 2], 'little')
o += 2 o += 2
script_bytes, nb = decode_varint(data, o) script_bytes, nb = decode_compactsize(data, o)
o += nb o += nb
txo.script_pubkey = data[o:o + script_bytes] txo.script_pubkey = data[o:o + script_bytes]
o += script_bytes o += script_bytes
@ -130,7 +130,7 @@ class CTransaction:
if ser_type == TxSerializeType.NoWitness: if ser_type == TxSerializeType.NoWitness:
return return
num_wit_scripts, nb = decode_varint(data, o) num_wit_scripts, nb = decode_compactsize(data, o)
o += nb o += nb
if ser_type == TxSerializeType.OnlyWitness: if ser_type == TxSerializeType.OnlyWitness:
@ -147,7 +147,7 @@ class CTransaction:
o += 4 o += 4
txi.block_index = int.from_bytes(data[o:o + 4], 'little') txi.block_index = int.from_bytes(data[o:o + 4], 'little')
o += 4 o += 4
script_bytes, nb = decode_varint(data, o) script_bytes, nb = decode_compactsize(data, o)
o += nb o += nb
txi.signature_script = data[o:o + script_bytes] txi.signature_script = data[o:o + script_bytes]
o += script_bytes o += script_bytes
@ -158,31 +158,31 @@ class CTransaction:
data += version.to_bytes(4, 'little') data += version.to_bytes(4, 'little')
if ser_type == TxSerializeType.Full or ser_type == TxSerializeType.NoWitness: if ser_type == TxSerializeType.Full or ser_type == TxSerializeType.NoWitness:
data += encode_varint(len(self.vin)) data += encode_compactsize(len(self.vin))
for txi in self.vin: for txi in self.vin:
data += txi.prevout.hash.to_bytes(32, 'little') data += txi.prevout.hash.to_bytes(32, 'little')
data += txi.prevout.n.to_bytes(4, 'little') data += txi.prevout.n.to_bytes(4, 'little')
data += txi.prevout.tree.to_bytes(1) data += txi.prevout.tree.to_bytes(1)
data += txi.sequence.to_bytes(4, 'little') data += txi.sequence.to_bytes(4, 'little')
data += encode_varint(len(self.vout)) data += encode_compactsize(len(self.vout))
for txo in self.vout: for txo in self.vout:
data += txo.value.to_bytes(8, 'little') data += txo.value.to_bytes(8, 'little')
data += txo.version.to_bytes(2, 'little') data += txo.version.to_bytes(2, 'little')
data += encode_varint(len(txo.script_pubkey)) data += encode_compactsize(len(txo.script_pubkey))
data += txo.script_pubkey data += txo.script_pubkey
data += self.locktime.to_bytes(4, 'little') data += self.locktime.to_bytes(4, 'little')
data += self.expiry.to_bytes(4, 'little') data += self.expiry.to_bytes(4, 'little')
if ser_type == TxSerializeType.Full or ser_type == TxSerializeType.OnlyWitness: if ser_type == TxSerializeType.Full or ser_type == TxSerializeType.OnlyWitness:
data += encode_varint(len(self.vin)) data += encode_compactsize(len(self.vin))
for txi in self.vin: for txi in self.vin:
tc_value_in = txi.value_in & 0xffffffffffffffff # Convert negative values tc_value_in = txi.value_in & 0xffffffffffffffff # Convert negative values
data += tc_value_in.to_bytes(8, 'little') data += tc_value_in.to_bytes(8, 'little')
data += txi.block_height.to_bytes(4, 'little') data += txi.block_height.to_bytes(4, 'little')
data += txi.block_index.to_bytes(4, 'little') data += txi.block_index.to_bytes(4, 'little')
data += encode_varint(len(txi.signature_script)) data += encode_compactsize(len(txi.signature_script))
data += txi.signature_script data += txi.signature_script
return data return data

@ -87,7 +87,7 @@ class FIROInterface(BTCInterface):
return address return address
def getLockTxHeight(self, txid, dest_address, bid_amount, rescan_from, find_index: bool = False): def getLockTxHeight(self, txid, dest_address, bid_amount, rescan_from, find_index: bool = False, vout: int = -1):
# Add watchonly address and rescan if required # Add watchonly address and rescan if required
if not self.isAddressMine(dest_address, or_watch_only=True): if not self.isAddressMine(dest_address, or_watch_only=True):
@ -337,7 +337,7 @@ class FIROInterface(BTCInterface):
return return
current_height -= 1 current_height -= 1
def getBlockWithTxns(self, block_hash): def getBlockWithTxns(self, block_hash: str):
# TODO: Bypass decoderawtransaction and getblockheader # TODO: Bypass decoderawtransaction and getblockheader
block = self.rpc('getblock', [block_hash, False]) block = self.rpc('getblock', [block_hash, False])
block_header = self.rpc('getblockheader', [block_hash]) block_header = self.rpc('getblockheader', [block_hash])
@ -355,9 +355,11 @@ class FIROInterface(BTCInterface):
block_rv = { block_rv = {
'hash': block_hash, 'hash': block_hash,
'previousblockhash': block_header['previousblockhash'],
'tx': tx_rv, 'tx': tx_rv,
'confirmations': block_header['confirmations'], 'confirmations': block_header['confirmations'],
'height': block_header['height'], 'height': block_header['height'],
'time': block_header['time'],
'version': block_header['version'], 'version': block_header['version'],
'merkleroot': block_header['merkleroot'], 'merkleroot': block_header['merkleroot'],
} }

@ -415,7 +415,7 @@ class NAVInterface(BTCInterface):
return return
current_height -= 1 current_height -= 1
def getLockTxHeight(self, txid, dest_address, bid_amount, rescan_from, find_index: bool = False): def getLockTxHeight(self, txid, dest_address, bid_amount, rescan_from, find_index: bool = False, vout: int = -1):
# Add watchonly address and rescan if required # Add watchonly address and rescan if required
if not self.isAddressMine(dest_address, or_watch_only=True): if not self.isAddressMine(dest_address, or_watch_only=True):
@ -479,9 +479,11 @@ class NAVInterface(BTCInterface):
block_rv = { block_rv = {
'hash': block_hash, 'hash': block_hash,
'previousblockhash': block_header['previousblockhash'],
'tx': tx_rv, 'tx': tx_rv,
'confirmations': block_header['confirmations'], 'confirmations': block_header['confirmations'],
'height': block_header['height'], 'height': block_header['height'],
'time': block_header['time'],
'version': block_header['version'], 'version': block_header['version'],
'merkleroot': block_header['merkleroot'], 'merkleroot': block_header['merkleroot'],
} }

@ -14,7 +14,7 @@ class NMCInterface(BTCInterface):
def coin_type(): def coin_type():
return Coins.NMC return Coins.NMC
def getLockTxHeight(self, txid, dest_address, bid_amount, rescan_from, find_index=False): def getLockTxHeight(self, txid, dest_address, bid_amount, rescan_from, find_index: bool = False, vout: int = -1):
self._log.debug('[rm] scantxoutset start') # scantxoutset is slow self._log.debug('[rm] scantxoutset start') # scantxoutset is slow
ro = self.rpc('scantxoutset', ['start', ['addr({})'.format(dest_address)]]) # TODO: Use combo(address) where possible ro = self.rpc('scantxoutset', ['start', ['addr({})'.format(dest_address)]]) # TODO: Use combo(address) where possible
self._log.debug('[rm] scantxoutset end') self._log.debug('[rm] scantxoutset end')

@ -14,7 +14,7 @@ from basicswap.contrib.test_framework.messages import (
from basicswap.contrib.test_framework.script import ( from basicswap.contrib.test_framework.script import (
CScript, CScript,
OP_0, OP_0,
OP_DUP, OP_HASH160, OP_EQUALVERIFY, OP_CHECKSIG OP_DUP, OP_HASH160, OP_EQUALVERIFY, OP_CHECKSIG,
) )
from basicswap.util import ( from basicswap.util import (
ensure, ensure,
@ -26,8 +26,8 @@ from basicswap.util.script import (
getWitnessElementLen, getWitnessElementLen,
) )
from basicswap.util.address import ( from basicswap.util.address import (
toWIF, encodeStealthAddress,
encodeStealthAddress) )
from basicswap.chainparams import Coins, chainparams from basicswap.chainparams import Coins, chainparams
from .btc import BTCInterface from .btc import BTCInterface
@ -73,6 +73,9 @@ class PARTInterface(BTCInterface):
super().__init__(coin_settings, network, swap_client) super().__init__(coin_settings, network, swap_client)
self.setAnonTxRingSize(int(coin_settings.get('anon_tx_ring_size', 12))) self.setAnonTxRingSize(int(coin_settings.get('anon_tx_ring_size', 12)))
def use_tx_vsize(self) -> bool:
return True
def setAnonTxRingSize(self, value): def setAnonTxRingSize(self, value):
ensure(value >= 3 and value < 33, 'Invalid anon_tx_ring_size value') ensure(value >= 3 and value < 33, 'Invalid anon_tx_ring_size value')
self._anon_tx_ring_size = value self._anon_tx_ring_size = value
@ -687,8 +690,7 @@ class PARTInterfaceBlind(PARTInterface):
else: else:
addr_info = self.rpc_wallet('getaddressinfo', [sx_addr]) addr_info = self.rpc_wallet('getaddressinfo', [sx_addr])
if not addr_info['iswatchonly']: if not addr_info['iswatchonly']:
wif_prefix = self.chainparams_network()['key_prefix'] wif_scan_key = self.encodeKey(kbv)
wif_scan_key = toWIF(wif_prefix, kbv)
self.rpc_wallet('importstealthaddress', [wif_scan_key, Kbs.hex()]) self.rpc_wallet('importstealthaddress', [wif_scan_key, Kbs.hex()])
self._log.info('Imported watch-only sx_addr: {}'.format(sx_addr)) self._log.info('Imported watch-only sx_addr: {}'.format(sx_addr))
self._log.info('Rescanning {} chain from height: {}'.format(self.coin_name(), restore_height)) self._log.info('Rescanning {} chain from height: {}'.format(self.coin_name(), restore_height))
@ -719,9 +721,8 @@ class PARTInterfaceBlind(PARTInterface):
sx_addr = self.formatStealthAddress(Kbv, Kbs) sx_addr = self.formatStealthAddress(Kbv, Kbs)
addr_info = self.rpc_wallet('getaddressinfo', [sx_addr]) addr_info = self.rpc_wallet('getaddressinfo', [sx_addr])
if not addr_info['ismine']: if not addr_info['ismine']:
wif_prefix = self.chainparams_network()['key_prefix'] wif_scan_key = self.encodeKey(kbv)
wif_scan_key = toWIF(wif_prefix, kbv) wif_spend_key = self.encodeKey(kbs)
wif_spend_key = toWIF(wif_prefix, kbs)
self.rpc_wallet('importstealthaddress', [wif_scan_key, wif_spend_key]) self.rpc_wallet('importstealthaddress', [wif_scan_key, wif_spend_key])
self._log.info('Imported spend key for sx_addr: {}'.format(sx_addr)) self._log.info('Imported spend key for sx_addr: {}'.format(sx_addr))
self._log.info('Rescanning {} chain from height: {}'.format(self.coin_name(), restore_height)) self._log.info('Rescanning {} chain from height: {}'.format(self.coin_name(), restore_height))
@ -825,8 +826,7 @@ class PARTInterfaceAnon(PARTInterface):
else: else:
addr_info = self.rpc_wallet('getaddressinfo', [sx_addr]) addr_info = self.rpc_wallet('getaddressinfo', [sx_addr])
if not addr_info['iswatchonly']: if not addr_info['iswatchonly']:
wif_prefix = self.chainparams_network()['key_prefix'] wif_scan_key = self.encodeKey(kbv)
wif_scan_key = toWIF(wif_prefix, kbv)
self.rpc_wallet('importstealthaddress', [wif_scan_key, Kbs.hex()]) self.rpc_wallet('importstealthaddress', [wif_scan_key, Kbs.hex()])
self._log.info('Imported watch-only sx_addr: {}'.format(sx_addr)) self._log.info('Imported watch-only sx_addr: {}'.format(sx_addr))
self._log.info('Rescanning {} chain from height: {}'.format(self.coin_name(), restore_height)) self._log.info('Rescanning {} chain from height: {}'.format(self.coin_name(), restore_height))
@ -857,9 +857,8 @@ class PARTInterfaceAnon(PARTInterface):
sx_addr = self.formatStealthAddress(Kbv, Kbs) sx_addr = self.formatStealthAddress(Kbv, Kbs)
addr_info = self.rpc_wallet('getaddressinfo', [sx_addr]) addr_info = self.rpc_wallet('getaddressinfo', [sx_addr])
if not addr_info['ismine']: if not addr_info['ismine']:
wif_prefix = self.chainparams_network()['key_prefix'] wif_scan_key = self.encodeKey(kbv)
wif_scan_key = toWIF(wif_prefix, kbv) wif_spend_key = self.encodeKey(kbs)
wif_spend_key = toWIF(wif_prefix, kbs)
self.rpc_wallet('importstealthaddress', [wif_scan_key, wif_spend_key]) self.rpc_wallet('importstealthaddress', [wif_scan_key, wif_spend_key])
self._log.info('Imported spend key for sx_addr: {}'.format(sx_addr)) self._log.info('Imported spend key for sx_addr: {}'.format(sx_addr))
self._log.info('Rescanning {} chain from height: {}'.format(self.coin_name(), restore_height)) self._log.info('Rescanning {} chain from height: {}'.format(self.coin_name(), restore_height))

@ -75,9 +75,11 @@ class PIVXInterface(BTCInterface):
block_rv = { block_rv = {
'hash': block_hash, 'hash': block_hash,
'previousblockhash': block_header['previousblockhash'],
'tx': tx_rv, 'tx': tx_rv,
'confirmations': block_header['confirmations'], 'confirmations': block_header['confirmations'],
'height': block_header['height'], 'height': block_header['height'],
'time': block_header['time'],
'version': block_header['version'], 'version': block_header['version'],
'merkleroot': block_header['merkleroot'], 'merkleroot': block_header['merkleroot'],
} }

@ -49,6 +49,9 @@ message BidMessage {
string proof_signature = 8; string proof_signature = 8;
bytes proof_utxos = 9; /* 32 byte txid 2 byte vout, repeated */ bytes proof_utxos = 9; /* 32 byte txid 2 byte vout, repeated */
/* optional */
bytes pkhash_buyer_to = 13; /* When pubkey hash is different on the to-chain */
} }
/* For tests */ /* For tests */
@ -65,6 +68,7 @@ message BidAcceptMessage {
bytes bid_msg_id = 1; bytes bid_msg_id = 1;
bytes initiate_txid = 2; bytes initiate_txid = 2;
bytes contract_script = 3; bytes contract_script = 3;
bytes pkhash_seller = 4;
} }
message OfferRevokeMessage { message OfferRevokeMessage {

@ -14,7 +14,7 @@ _sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0emessages.proto\x12\tbasicswap\"\xc0\x04\n\x0cOfferMessage\x12\x18\n\x10protocol_version\x18\x01 \x01(\r\x12\x11\n\tcoin_from\x18\x02 \x01(\r\x12\x0f\n\x07\x63oin_to\x18\x03 \x01(\r\x12\x13\n\x0b\x61mount_from\x18\x04 \x01(\x04\x12\x11\n\tamount_to\x18\x05 \x01(\x04\x12\x16\n\x0emin_bid_amount\x18\x06 \x01(\x04\x12\x12\n\ntime_valid\x18\x07 \x01(\x04\x12\x33\n\tlock_type\x18\x08 \x01(\x0e\x32 .basicswap.OfferMessage.LockType\x12\x12\n\nlock_value\x18\t \x01(\r\x12\x11\n\tswap_type\x18\n \x01(\r\x12\x15\n\rproof_address\x18\x0b \x01(\t\x12\x17\n\x0fproof_signature\x18\x0c \x01(\t\x12\x15\n\rpkhash_seller\x18\r \x01(\x0c\x12\x13\n\x0bsecret_hash\x18\x0e \x01(\x0c\x12\x15\n\rfee_rate_from\x18\x0f \x01(\x04\x12\x13\n\x0b\x66\x65\x65_rate_to\x18\x10 \x01(\x04\x12\x19\n\x11\x61mount_negotiable\x18\x11 \x01(\x08\x12\x17\n\x0frate_negotiable\x18\x12 \x01(\x08\x12\x13\n\x0bproof_utxos\x18\x13 \x01(\x0c\"q\n\x08LockType\x12\x0b\n\x07NOT_SET\x10\x00\x12\x18\n\x14SEQUENCE_LOCK_BLOCKS\x10\x01\x12\x16\n\x12SEQUENCE_LOCK_TIME\x10\x02\x12\x13\n\x0f\x41\x42S_LOCK_BLOCKS\x10\x03\x12\x11\n\rABS_LOCK_TIME\x10\x04\"\xce\x01\n\nBidMessage\x12\x18\n\x10protocol_version\x18\x01 \x01(\r\x12\x14\n\x0coffer_msg_id\x18\x02 \x01(\x0c\x12\x12\n\ntime_valid\x18\x03 \x01(\x04\x12\x0e\n\x06\x61mount\x18\x04 \x01(\x04\x12\x11\n\tamount_to\x18\x05 \x01(\x04\x12\x14\n\x0cpkhash_buyer\x18\x06 \x01(\x0c\x12\x15\n\rproof_address\x18\x07 \x01(\t\x12\x17\n\x0fproof_signature\x18\x08 \x01(\t\x12\x13\n\x0bproof_utxos\x18\t \x01(\x0c\"s\n\x0f\x42idMessage_test\x12\x18\n\x10protocol_version\x18\x01 \x01(\r\x12\x14\n\x0coffer_msg_id\x18\x02 \x01(\x0c\x12\x12\n\ntime_valid\x18\x03 \x01(\x04\x12\x0e\n\x06\x61mount\x18\x04 \x01(\x04\x12\x0c\n\x04rate\x18\x05 \x01(\x04\"V\n\x10\x42idAcceptMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x15\n\rinitiate_txid\x18\x02 \x01(\x0c\x12\x17\n\x0f\x63ontract_script\x18\x03 \x01(\x0c\"=\n\x12OfferRevokeMessage\x12\x14\n\x0coffer_msg_id\x18\x01 \x01(\x0c\x12\x11\n\tsignature\x18\x02 \x01(\x0c\";\n\x10\x42idRejectMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x13\n\x0breject_code\x18\x02 \x01(\r\"\xb7\x01\n\rXmrBidMessage\x12\x18\n\x10protocol_version\x18\x01 \x01(\r\x12\x14\n\x0coffer_msg_id\x18\x02 \x01(\x0c\x12\x12\n\ntime_valid\x18\x03 \x01(\x04\x12\x0e\n\x06\x61mount\x18\x04 \x01(\x04\x12\x11\n\tamount_to\x18\x05 \x01(\x04\x12\x0c\n\x04pkaf\x18\x06 \x01(\x0c\x12\x0c\n\x04kbvf\x18\x07 \x01(\x0c\x12\x12\n\nkbsf_dleag\x18\x08 \x01(\x0c\x12\x0f\n\x07\x64\x65st_af\x18\t \x01(\x0c\"T\n\x0fXmrSplitMessage\x12\x0e\n\x06msg_id\x18\x01 \x01(\x0c\x12\x10\n\x08msg_type\x18\x02 \x01(\r\x12\x10\n\x08sequence\x18\x03 \x01(\r\x12\r\n\x05\x64leag\x18\x04 \x01(\x0c\"\x80\x02\n\x13XmrBidAcceptMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x0c\n\x04pkal\x18\x02 \x01(\x0c\x12\x0c\n\x04kbvl\x18\x03 \x01(\x0c\x12\x12\n\nkbsl_dleag\x18\x04 \x01(\x0c\x12\x11\n\ta_lock_tx\x18\x05 \x01(\x0c\x12\x18\n\x10\x61_lock_tx_script\x18\x06 \x01(\x0c\x12\x18\n\x10\x61_lock_refund_tx\x18\x07 \x01(\x0c\x12\x1f\n\x17\x61_lock_refund_tx_script\x18\x08 \x01(\x0c\x12\x1e\n\x16\x61_lock_refund_spend_tx\x18\t \x01(\x0c\x12\x1d\n\x15\x61l_lock_refund_tx_sig\x18\n \x01(\x0c\"r\n\x17XmrBidLockTxSigsMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12$\n\x1c\x61\x66_lock_refund_spend_tx_esig\x18\x02 \x01(\x0c\x12\x1d\n\x15\x61\x66_lock_refund_tx_sig\x18\x03 \x01(\x0c\"X\n\x18XmrBidLockSpendTxMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x17\n\x0f\x61_lock_spend_tx\x18\x02 \x01(\x0c\x12\x0f\n\x07kal_sig\x18\x03 \x01(\x0c\"M\n\x18XmrBidLockReleaseMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x1d\n\x15\x61l_lock_spend_tx_esig\x18\x02 \x01(\x0c\"\x81\x01\n\x13\x41\x44SBidIntentMessage\x12\x18\n\x10protocol_version\x18\x01 \x01(\r\x12\x14\n\x0coffer_msg_id\x18\x02 \x01(\x0c\x12\x12\n\ntime_valid\x18\x03 \x01(\x04\x12\x13\n\x0b\x61mount_from\x18\x04 \x01(\x04\x12\x11\n\tamount_to\x18\x05 \x01(\x04\"p\n\x19\x41\x44SBidIntentAcceptMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x0c\n\x04pkaf\x18\x02 \x01(\x0c\x12\x0c\n\x04kbvf\x18\x03 \x01(\x0c\x12\x12\n\nkbsf_dleag\x18\x04 \x01(\x0c\x12\x0f\n\x07\x64\x65st_af\x18\x05 \x01(\x0c\x62\x06proto3') DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0emessages.proto\x12\tbasicswap\"\xc0\x04\n\x0cOfferMessage\x12\x18\n\x10protocol_version\x18\x01 \x01(\r\x12\x11\n\tcoin_from\x18\x02 \x01(\r\x12\x0f\n\x07\x63oin_to\x18\x03 \x01(\r\x12\x13\n\x0b\x61mount_from\x18\x04 \x01(\x04\x12\x11\n\tamount_to\x18\x05 \x01(\x04\x12\x16\n\x0emin_bid_amount\x18\x06 \x01(\x04\x12\x12\n\ntime_valid\x18\x07 \x01(\x04\x12\x33\n\tlock_type\x18\x08 \x01(\x0e\x32 .basicswap.OfferMessage.LockType\x12\x12\n\nlock_value\x18\t \x01(\r\x12\x11\n\tswap_type\x18\n \x01(\r\x12\x15\n\rproof_address\x18\x0b \x01(\t\x12\x17\n\x0fproof_signature\x18\x0c \x01(\t\x12\x15\n\rpkhash_seller\x18\r \x01(\x0c\x12\x13\n\x0bsecret_hash\x18\x0e \x01(\x0c\x12\x15\n\rfee_rate_from\x18\x0f \x01(\x04\x12\x13\n\x0b\x66\x65\x65_rate_to\x18\x10 \x01(\x04\x12\x19\n\x11\x61mount_negotiable\x18\x11 \x01(\x08\x12\x17\n\x0frate_negotiable\x18\x12 \x01(\x08\x12\x13\n\x0bproof_utxos\x18\x13 \x01(\x0c\"q\n\x08LockType\x12\x0b\n\x07NOT_SET\x10\x00\x12\x18\n\x14SEQUENCE_LOCK_BLOCKS\x10\x01\x12\x16\n\x12SEQUENCE_LOCK_TIME\x10\x02\x12\x13\n\x0f\x41\x42S_LOCK_BLOCKS\x10\x03\x12\x11\n\rABS_LOCK_TIME\x10\x04\"\xe7\x01\n\nBidMessage\x12\x18\n\x10protocol_version\x18\x01 \x01(\r\x12\x14\n\x0coffer_msg_id\x18\x02 \x01(\x0c\x12\x12\n\ntime_valid\x18\x03 \x01(\x04\x12\x0e\n\x06\x61mount\x18\x04 \x01(\x04\x12\x11\n\tamount_to\x18\x05 \x01(\x04\x12\x14\n\x0cpkhash_buyer\x18\x06 \x01(\x0c\x12\x15\n\rproof_address\x18\x07 \x01(\t\x12\x17\n\x0fproof_signature\x18\x08 \x01(\t\x12\x13\n\x0bproof_utxos\x18\t \x01(\x0c\x12\x17\n\x0fpkhash_buyer_to\x18\r \x01(\x0c\"s\n\x0f\x42idMessage_test\x12\x18\n\x10protocol_version\x18\x01 \x01(\r\x12\x14\n\x0coffer_msg_id\x18\x02 \x01(\x0c\x12\x12\n\ntime_valid\x18\x03 \x01(\x04\x12\x0e\n\x06\x61mount\x18\x04 \x01(\x04\x12\x0c\n\x04rate\x18\x05 \x01(\x04\"m\n\x10\x42idAcceptMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x15\n\rinitiate_txid\x18\x02 \x01(\x0c\x12\x17\n\x0f\x63ontract_script\x18\x03 \x01(\x0c\x12\x15\n\rpkhash_seller\x18\x04 \x01(\x0c\"=\n\x12OfferRevokeMessage\x12\x14\n\x0coffer_msg_id\x18\x01 \x01(\x0c\x12\x11\n\tsignature\x18\x02 \x01(\x0c\";\n\x10\x42idRejectMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x13\n\x0breject_code\x18\x02 \x01(\r\"\xb7\x01\n\rXmrBidMessage\x12\x18\n\x10protocol_version\x18\x01 \x01(\r\x12\x14\n\x0coffer_msg_id\x18\x02 \x01(\x0c\x12\x12\n\ntime_valid\x18\x03 \x01(\x04\x12\x0e\n\x06\x61mount\x18\x04 \x01(\x04\x12\x11\n\tamount_to\x18\x05 \x01(\x04\x12\x0c\n\x04pkaf\x18\x06 \x01(\x0c\x12\x0c\n\x04kbvf\x18\x07 \x01(\x0c\x12\x12\n\nkbsf_dleag\x18\x08 \x01(\x0c\x12\x0f\n\x07\x64\x65st_af\x18\t \x01(\x0c\"T\n\x0fXmrSplitMessage\x12\x0e\n\x06msg_id\x18\x01 \x01(\x0c\x12\x10\n\x08msg_type\x18\x02 \x01(\r\x12\x10\n\x08sequence\x18\x03 \x01(\r\x12\r\n\x05\x64leag\x18\x04 \x01(\x0c\"\x80\x02\n\x13XmrBidAcceptMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x0c\n\x04pkal\x18\x02 \x01(\x0c\x12\x0c\n\x04kbvl\x18\x03 \x01(\x0c\x12\x12\n\nkbsl_dleag\x18\x04 \x01(\x0c\x12\x11\n\ta_lock_tx\x18\x05 \x01(\x0c\x12\x18\n\x10\x61_lock_tx_script\x18\x06 \x01(\x0c\x12\x18\n\x10\x61_lock_refund_tx\x18\x07 \x01(\x0c\x12\x1f\n\x17\x61_lock_refund_tx_script\x18\x08 \x01(\x0c\x12\x1e\n\x16\x61_lock_refund_spend_tx\x18\t \x01(\x0c\x12\x1d\n\x15\x61l_lock_refund_tx_sig\x18\n \x01(\x0c\"r\n\x17XmrBidLockTxSigsMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12$\n\x1c\x61\x66_lock_refund_spend_tx_esig\x18\x02 \x01(\x0c\x12\x1d\n\x15\x61\x66_lock_refund_tx_sig\x18\x03 \x01(\x0c\"X\n\x18XmrBidLockSpendTxMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x17\n\x0f\x61_lock_spend_tx\x18\x02 \x01(\x0c\x12\x0f\n\x07kal_sig\x18\x03 \x01(\x0c\"M\n\x18XmrBidLockReleaseMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x1d\n\x15\x61l_lock_spend_tx_esig\x18\x02 \x01(\x0c\"\x81\x01\n\x13\x41\x44SBidIntentMessage\x12\x18\n\x10protocol_version\x18\x01 \x01(\r\x12\x14\n\x0coffer_msg_id\x18\x02 \x01(\x0c\x12\x12\n\ntime_valid\x18\x03 \x01(\x04\x12\x13\n\x0b\x61mount_from\x18\x04 \x01(\x04\x12\x11\n\tamount_to\x18\x05 \x01(\x04\"p\n\x19\x41\x44SBidIntentAcceptMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x0c\n\x04pkaf\x18\x02 \x01(\x0c\x12\x0c\n\x04kbvf\x18\x03 \x01(\x0c\x12\x12\n\nkbsf_dleag\x18\x04 \x01(\x0c\x12\x0f\n\x07\x64\x65st_af\x18\x05 \x01(\x0c\x62\x06proto3')
_globals = globals() _globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
@ -26,29 +26,29 @@ if _descriptor._USE_C_DESCRIPTORS == False:
_globals['_OFFERMESSAGE_LOCKTYPE']._serialized_start=493 _globals['_OFFERMESSAGE_LOCKTYPE']._serialized_start=493
_globals['_OFFERMESSAGE_LOCKTYPE']._serialized_end=606 _globals['_OFFERMESSAGE_LOCKTYPE']._serialized_end=606
_globals['_BIDMESSAGE']._serialized_start=609 _globals['_BIDMESSAGE']._serialized_start=609
_globals['_BIDMESSAGE']._serialized_end=815 _globals['_BIDMESSAGE']._serialized_end=840
_globals['_BIDMESSAGE_TEST']._serialized_start=817 _globals['_BIDMESSAGE_TEST']._serialized_start=842
_globals['_BIDMESSAGE_TEST']._serialized_end=932 _globals['_BIDMESSAGE_TEST']._serialized_end=957
_globals['_BIDACCEPTMESSAGE']._serialized_start=934 _globals['_BIDACCEPTMESSAGE']._serialized_start=959
_globals['_BIDACCEPTMESSAGE']._serialized_end=1020 _globals['_BIDACCEPTMESSAGE']._serialized_end=1068
_globals['_OFFERREVOKEMESSAGE']._serialized_start=1022 _globals['_OFFERREVOKEMESSAGE']._serialized_start=1070
_globals['_OFFERREVOKEMESSAGE']._serialized_end=1083 _globals['_OFFERREVOKEMESSAGE']._serialized_end=1131
_globals['_BIDREJECTMESSAGE']._serialized_start=1085 _globals['_BIDREJECTMESSAGE']._serialized_start=1133
_globals['_BIDREJECTMESSAGE']._serialized_end=1144 _globals['_BIDREJECTMESSAGE']._serialized_end=1192
_globals['_XMRBIDMESSAGE']._serialized_start=1147 _globals['_XMRBIDMESSAGE']._serialized_start=1195
_globals['_XMRBIDMESSAGE']._serialized_end=1330 _globals['_XMRBIDMESSAGE']._serialized_end=1378
_globals['_XMRSPLITMESSAGE']._serialized_start=1332 _globals['_XMRSPLITMESSAGE']._serialized_start=1380
_globals['_XMRSPLITMESSAGE']._serialized_end=1416 _globals['_XMRSPLITMESSAGE']._serialized_end=1464
_globals['_XMRBIDACCEPTMESSAGE']._serialized_start=1419 _globals['_XMRBIDACCEPTMESSAGE']._serialized_start=1467
_globals['_XMRBIDACCEPTMESSAGE']._serialized_end=1675 _globals['_XMRBIDACCEPTMESSAGE']._serialized_end=1723
_globals['_XMRBIDLOCKTXSIGSMESSAGE']._serialized_start=1677 _globals['_XMRBIDLOCKTXSIGSMESSAGE']._serialized_start=1725
_globals['_XMRBIDLOCKTXSIGSMESSAGE']._serialized_end=1791 _globals['_XMRBIDLOCKTXSIGSMESSAGE']._serialized_end=1839
_globals['_XMRBIDLOCKSPENDTXMESSAGE']._serialized_start=1793 _globals['_XMRBIDLOCKSPENDTXMESSAGE']._serialized_start=1841
_globals['_XMRBIDLOCKSPENDTXMESSAGE']._serialized_end=1881 _globals['_XMRBIDLOCKSPENDTXMESSAGE']._serialized_end=1929
_globals['_XMRBIDLOCKRELEASEMESSAGE']._serialized_start=1883 _globals['_XMRBIDLOCKRELEASEMESSAGE']._serialized_start=1931
_globals['_XMRBIDLOCKRELEASEMESSAGE']._serialized_end=1960 _globals['_XMRBIDLOCKRELEASEMESSAGE']._serialized_end=2008
_globals['_ADSBIDINTENTMESSAGE']._serialized_start=1963 _globals['_ADSBIDINTENTMESSAGE']._serialized_start=2011
_globals['_ADSBIDINTENTMESSAGE']._serialized_end=2092 _globals['_ADSBIDINTENTMESSAGE']._serialized_end=2140
_globals['_ADSBIDINTENTACCEPTMESSAGE']._serialized_start=2094 _globals['_ADSBIDINTENTACCEPTMESSAGE']._serialized_start=2142
_globals['_ADSBIDINTENTACCEPTMESSAGE']._serialized_end=2206 _globals['_ADSBIDINTENTACCEPTMESSAGE']._serialized_end=2254
# @@protoc_insertion_point(module_scope) # @@protoc_insertion_point(module_scope)

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2020-2023 tecnovert # Copyright (c) 2020-2024 tecnovert
# Distributed under the MIT software license, see the accompanying # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@ -10,12 +10,15 @@ from basicswap.db import (
from basicswap.util import ( from basicswap.util import (
SerialiseNum, SerialiseNum,
) )
from basicswap.util.script import (
decodeScriptNum,
)
from basicswap.script import ( from basicswap.script import (
OpCodes, OpCodes,
) )
from basicswap.basicswap_util import ( from basicswap.basicswap_util import (
SwapTypes,
EventLogTypes, EventLogTypes,
SwapTypes,
) )
from . import ProtocolInterface from . import ProtocolInterface
@ -23,13 +26,13 @@ INITIATE_TX_TIMEOUT = 40 * 60 # TODO: make variable per coin
ABS_LOCK_TIME_LEEWAY = 10 * 60 ABS_LOCK_TIME_LEEWAY = 10 * 60
def buildContractScript(lock_val: int, secret_hash: bytes, pkh_redeem: bytes, pkh_refund: bytes, op_lock=OpCodes.OP_CHECKSEQUENCEVERIFY) -> bytearray: def buildContractScript(lock_val: int, secret_hash: bytes, pkh_redeem: bytes, pkh_refund: bytes, op_lock=OpCodes.OP_CHECKSEQUENCEVERIFY, op_hash=OpCodes.OP_SHA256) -> bytearray:
script = bytearray([ script = bytearray([
OpCodes.OP_IF, OpCodes.OP_IF,
OpCodes.OP_SIZE, OpCodes.OP_SIZE,
0x01, 0x20, # 32 0x01, 0x20, # 32
OpCodes.OP_EQUALVERIFY, OpCodes.OP_EQUALVERIFY,
OpCodes.OP_SHA256, op_hash,
0x20]) \ 0x20]) \
+ secret_hash \ + secret_hash \
+ bytearray([ + bytearray([
@ -54,6 +57,46 @@ def buildContractScript(lock_val: int, secret_hash: bytes, pkh_redeem: bytes, pk
return script return script
def verifyContractScript(script, op_lock=OpCodes.OP_CHECKSEQUENCEVERIFY, op_hash=OpCodes.OP_SHA256):
if script[0] != OpCodes.OP_IF or \
script[1] != OpCodes.OP_SIZE or \
script[2] != 0x01 or script[3] != 0x20 or \
script[4] != OpCodes.OP_EQUALVERIFY or \
script[5] != op_hash or \
script[6] != 0x20:
return False, None, None, None, None
o = 7
script_hash = script[o: o + 32]
o += 32
if script[o] != OpCodes.OP_EQUALVERIFY or \
script[o + 1] != OpCodes.OP_DUP or \
script[o + 2] != OpCodes.OP_HASH160 or \
script[o + 3] != 0x14:
return False, script_hash, None, None, None
o += 4
pkh_redeem = script[o: o + 20]
o += 20
if script[o] != OpCodes.OP_ELSE:
return False, script_hash, pkh_redeem, None, None
o += 1
lock_val, nb = decodeScriptNum(script, o)
o += nb
if script[o] != op_lock or \
script[o + 1] != OpCodes.OP_DROP or \
script[o + 2] != OpCodes.OP_DUP or \
script[o + 3] != OpCodes.OP_HASH160 or \
script[o + 4] != 0x14:
return False, script_hash, pkh_redeem, lock_val, None
o += 5
pkh_refund = script[o: o + 20]
o += 20
if script[o] != OpCodes.OP_ENDIF or \
script[o + 1] != OpCodes.OP_EQUALVERIFY or \
script[o + 2] != OpCodes.OP_CHECKSIG:
return False, script_hash, pkh_redeem, lock_val, pkh_refund
return True, script_hash, pkh_redeem, lock_val, pkh_refund
def extractScriptSecretHash(script): def extractScriptSecretHash(script):
return script[7:39] return script[7:39]

@ -26,3 +26,5 @@ class OpCodes(IntEnum):
OP_CHECKSIG = 0xac, OP_CHECKSIG = 0xac,
OP_CHECKLOCKTIMEVERIFY = 0xb1, OP_CHECKLOCKTIMEVERIFY = 0xb1,
OP_CHECKSEQUENCEVERIFY = 0xb2, OP_CHECKSEQUENCEVERIFY = 0xb2,
OP_SHA256_DECRED = 0xc0,

@ -5,6 +5,29 @@
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
def decode_compactsize(b: bytes, offset: int = 0) -> (int, int):
i = b[offset]
if i < 0xfd:
return i, 1
offset += 1
if i == 0xfd:
return int.from_bytes(b[offset: offset + 2]), 3
if i == 0xfe:
return int.from_bytes(b[offset: offset + 4]), 5
# 0xff
return int.from_bytes(b[offset: offset + 8]), 9
def encode_compactsize(i: int) -> bytes:
if i < 0xfd:
return bytes((i,))
if i <= 0xffff:
return bytes((0xfd,)) + i.to_bytes(2, 'little')
if i <= 0xffffffff:
return bytes((0xfe,)) + i.to_bytes(4, 'little')
return bytes((0xff,)) + i.to_bytes(8, 'little')
def decode_varint(b: bytes, offset: int = 0) -> (int, int): def decode_varint(b: bytes, offset: int = 0) -> (int, int):
i: int = 0 i: int = 0
num_bytes: int = 0 num_bytes: int = 0

@ -389,7 +389,7 @@ def extract_states_from_xu_file(file_path, prefix):
return states return states
def compare_bid_states(states, expect_states, exact_match=True): def compare_bid_states(states, expect_states, exact_match: bool = True) -> bool:
for i in range(len(states) - 1, -1, -1): for i in range(len(states) - 1, -1, -1):
if states[i][1] == 'Bid Delaying': if states[i][1] == 'Bid Delaying':
@ -417,3 +417,19 @@ def compare_bid_states(states, expect_states, exact_match=True):
logging.info('Have states: {}'.format(json.dumps(states, indent=4))) logging.info('Have states: {}'.format(json.dumps(states, indent=4)))
raise e raise e
return True return True
def compare_bid_states_unordered(states, expect_states) -> bool:
for i in range(len(states) - 1, -1, -1):
if states[i][1] == 'Bid Delaying':
del states[i]
try:
assert len(states) == len(expect_states)
for state in expect_states:
assert (any(state in s[1] for s in states))
except Exception as e:
logging.info('Expecting states: {}'.format(json.dumps(expect_states, indent=4)))
logging.info('Have states: {}'.format(json.dumps(states, indent=4)))
raise e
return True

@ -5,8 +5,13 @@
# Distributed under the MIT software license, see the accompanying # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
# TODO
# - Occasionally DCR simnet chain stalls.
import copy
import logging import logging
import os import os
import random
import select import select
import subprocess import subprocess
import unittest import unittest
@ -14,7 +19,14 @@ import unittest
import basicswap.config as cfg import basicswap.config as cfg
from basicswap.basicswap import ( from basicswap.basicswap import (
BidStates,
Coins, Coins,
DebugTypes,
SwapTypes,
TxStates,
)
from basicswap.basicswap_util import (
TxLockTypes,
) )
from basicswap.util.crypto import ( from basicswap.util.crypto import (
hash160 hash160
@ -27,9 +39,14 @@ from basicswap.interface.dcr.messages import (
TxSerializeType, TxSerializeType,
) )
from tests.basicswap.common import ( from tests.basicswap.common import (
compare_bid_states,
compare_bid_states_unordered,
stopDaemons, stopDaemons,
waitForRPC,
wait_for_balance, wait_for_balance,
wait_for_bid,
wait_for_bid_tx_state,
wait_for_offer,
waitForRPC,
) )
from tests.basicswap.util import ( from tests.basicswap.util import (
read_json_api, read_json_api,
@ -63,6 +80,176 @@ def make_rpc_func(node_id, base_rpc_port):
return rpc_func return rpc_func
def test_success_path(self, coin_from: Coins, coin_to: Coins):
logging.info(f'---------- Test {coin_from.name} to {coin_to.name}')
node_from = 0
node_to = 1
swap_clients = self.swap_clients
ci_from = swap_clients[node_from].ci(coin_from)
ci_to = swap_clients[node_to].ci(coin_to)
self.prepare_balance(coin_to, 100.0, 1801, 1800)
self.prepare_balance(coin_from, 100.0, 1800, 1801)
amt_swap = ci_from.make_int(random.uniform(0.1, 5.0), r=1)
rate_swap = ci_to.make_int(random.uniform(0.2, 10.0), r=1)
offer_id = swap_clients[node_from].postOffer(coin_from, coin_to, amt_swap, rate_swap, amt_swap, SwapTypes.SELLER_FIRST)
wait_for_offer(test_delay_event, swap_clients[node_to], offer_id)
offer = swap_clients[node_to].getOffer(offer_id)
bid_id = swap_clients[node_to].postBid(offer_id, offer.amount_from)
wait_for_bid(test_delay_event, swap_clients[node_from], bid_id)
swap_clients[node_from].acceptBid(bid_id)
wait_for_bid(test_delay_event, swap_clients[node_from], bid_id, BidStates.SWAP_COMPLETED, wait_for=120)
wait_for_bid(test_delay_event, swap_clients[node_to], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=120)
# Verify lock tx spends are found in the expected wallets
bid, offer = swap_clients[node_from].getBidAndOffer(bid_id)
max_fee: int = 10000
itx_spend = bid.initiate_tx.spend_txid.hex()
node_to_ci_from = swap_clients[node_to].ci(coin_from)
wtx = node_to_ci_from.rpc_wallet('gettransaction', [itx_spend,])
assert (amt_swap - node_to_ci_from.make_int(wtx['details'][0]['amount']) < max_fee)
node_from_ci_to = swap_clients[node_from].ci(coin_to)
ptx_spend = bid.participate_tx.spend_txid.hex()
wtx = node_from_ci_to.rpc_wallet('gettransaction', [ptx_spend,])
assert (bid.amount_to - node_from_ci_to.make_int(wtx['details'][0]['amount']) < max_fee)
js_0 = read_json_api(1800 + node_from)
js_1 = read_json_api(1800 + node_to)
assert (js_0['num_swapping'] == 0 and js_0['num_watched_outputs'] == 0)
assert (js_1['num_swapping'] == 0 and js_1['num_watched_outputs'] == 0)
bid_id_hex = bid_id.hex()
path = f'bids/{bid_id_hex}/states'
offerer_states = read_json_api(1800 + node_from, path)
bidder_states = read_json_api(1800 + node_to, path)
expect_states = copy.deepcopy(self.states_offerer_sh[0])
# Will miss PTX Sent event as PTX is found by searching the chain.
if coin_to == Coins.DCR:
expect_states[5] = 'PTX In Chain'
assert (compare_bid_states(offerer_states, expect_states) is True)
assert (compare_bid_states(bidder_states, self.states_bidder_sh[0]) is True)
def test_bad_ptx(self, coin_from: Coins, coin_to: Coins):
# Invalid PTX sent, swap should stall and ITx and PTx should be reclaimed by senders
logging.info(f'---------- Test bad ptx {coin_from.name} to {coin_to.name}')
node_from = 0
node_to = 1
swap_clients = self.swap_clients
ci_from = swap_clients[node_from].ci(coin_from)
ci_to = swap_clients[node_to].ci(coin_to)
self.prepare_balance(coin_to, 100.0, 1801, 1800)
self.prepare_balance(coin_from, 100.0, 1800, 1801)
amt_swap = ci_from.make_int(random.uniform(1.1, 10.0), r=1)
rate_swap = ci_to.make_int(random.uniform(0.1, 2.0), r=1)
offer_id = swap_clients[node_from].postOffer(coin_from, coin_to, amt_swap, rate_swap, amt_swap, SwapTypes.SELLER_FIRST,
TxLockTypes.SEQUENCE_LOCK_BLOCKS, 10, auto_accept_bids=True)
wait_for_offer(test_delay_event, swap_clients[node_to], offer_id)
offer = swap_clients[node_to].getOffer(offer_id)
bid_id = swap_clients[node_to].postBid(offer_id, offer.amount_from)
swap_clients[node_to].setBidDebugInd(bid_id, DebugTypes.MAKE_INVALID_PTX)
wait_for_bid(test_delay_event, swap_clients[node_from], bid_id, BidStates.SWAP_COMPLETED, wait_for=120)
wait_for_bid(test_delay_event, swap_clients[node_to], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=120)
js_0_bid = read_json_api(1800 + node_from, 'bids/{}'.format(bid_id.hex()))
js_1_bid = read_json_api(1800 + node_to, 'bids/{}'.format(bid_id.hex()))
assert (js_0_bid['itx_state'] == 'Refunded')
assert (js_1_bid['ptx_state'] == 'Refunded')
# Verify lock tx spends are found in the expected wallets
bid, offer = swap_clients[node_from].getBidAndOffer(bid_id)
max_fee: int = 10000
itx_spend = bid.initiate_tx.spend_txid.hex()
node_from_ci_from = swap_clients[node_from].ci(coin_from)
wtx = node_from_ci_from.rpc_wallet('gettransaction', [itx_spend,])
assert (amt_swap - node_from_ci_from.make_int(wtx['details'][0]['amount']) < max_fee)
node_to_ci_to = swap_clients[node_to].ci(coin_to)
bid, offer = swap_clients[node_to].getBidAndOffer(bid_id)
ptx_spend = bid.participate_tx.spend_txid.hex()
wtx = node_to_ci_to.rpc_wallet('gettransaction', [ptx_spend,])
assert (bid.amount_to - node_to_ci_to.make_int(wtx['details'][0]['amount']) < max_fee)
bid_id_hex = bid_id.hex()
path = f'bids/{bid_id_hex}/states'
offerer_states = read_json_api(1800 + node_from, path)
bidder_states = read_json_api(1800 + node_to, path)
# Hard to get the timing right
assert (compare_bid_states_unordered(offerer_states, self.states_offerer_sh[1]) is True)
assert (compare_bid_states_unordered(bidder_states, self.states_bidder_sh[1]) is True)
js_0 = read_json_api(1800 + node_from)
js_1 = read_json_api(1800 + node_to)
assert (js_0['num_swapping'] == 0 and js_0['num_watched_outputs'] == 0)
assert (js_1['num_swapping'] == 0 and js_1['num_watched_outputs'] == 0)
def test_itx_refund(self, coin_from: Coins, coin_to: Coins):
# Offerer claims PTX and refunds ITX after lock expires
# Bidder loses PTX value without gaining ITX value
logging.info(f'---------- Test itx refund {coin_from.name} to {coin_to.name}')
node_from = 0
node_to = 1
swap_clients = self.swap_clients
ci_from = swap_clients[node_from].ci(coin_from)
ci_to = swap_clients[node_to].ci(coin_to)
self.prepare_balance(coin_to, 100.0, 1801, 1800)
self.prepare_balance(coin_from, 100.0, 1800, 1801)
swap_value = ci_from.make_int(random.uniform(2.0, 20.0), r=1)
rate_swap = ci_to.make_int(0.5, r=1)
offer_id = swap_clients[node_from].postOffer(coin_from, coin_to, swap_value, rate_swap, swap_value, SwapTypes.SELLER_FIRST,
TxLockTypes.SEQUENCE_LOCK_BLOCKS, 12)
wait_for_offer(test_delay_event, swap_clients[node_to], offer_id)
offer = swap_clients[node_to].getOffer(offer_id)
bid_id = swap_clients[node_to].postBid(offer_id, offer.amount_from)
swap_clients[node_to].setBidDebugInd(bid_id, DebugTypes.DONT_SPEND_ITX)
wait_for_bid(test_delay_event, swap_clients[node_from], bid_id)
# For testing: Block refunding the ITX until PTX has been redeemed, else ITX refund can become spendable before PTX confirms
swap_clients[node_from].setBidDebugInd(bid_id, DebugTypes.SKIP_LOCK_TX_REFUND)
swap_clients[node_from].acceptBid(bid_id)
wait_for_bid_tx_state(test_delay_event, swap_clients[node_from], bid_id, TxStates.TX_CONFIRMED, TxStates.TX_REDEEMED, wait_for=120)
swap_clients[node_from].setBidDebugInd(bid_id, DebugTypes.NONE)
wait_for_bid_tx_state(test_delay_event, swap_clients[node_from], bid_id, TxStates.TX_REFUNDED, TxStates.TX_REDEEMED, wait_for=90)
wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=60)
# Verify lock tx spends are found in the expected wallets
bid, offer = swap_clients[node_from].getBidAndOffer(bid_id)
max_fee: int = 10000
itx_spend = bid.initiate_tx.spend_txid.hex()
node_from_ci_from = swap_clients[node_from].ci(coin_from)
wtx = node_from_ci_from.rpc_wallet('gettransaction', [itx_spend,])
assert (swap_value - node_from_ci_from.make_int(wtx['details'][0]['amount']) < max_fee)
node_from_ci_to = swap_clients[node_from].ci(coin_to)
ptx_spend = bid.participate_tx.spend_txid.hex()
wtx = node_from_ci_to.rpc_wallet('gettransaction', [ptx_spend,])
assert (bid.amount_to - node_from_ci_to.make_int(wtx['details'][0]['amount']) < max_fee)
def prepareDCDDataDir(datadir, node_id, conf_file, dir_prefix, num_nodes=3): def prepareDCDDataDir(datadir, node_id, conf_file, dir_prefix, num_nodes=3):
node_dir = os.path.join(datadir, dir_prefix + str(node_id)) node_dir = os.path.join(datadir, dir_prefix + str(node_id))
if not os.path.exists(node_dir): if not os.path.exists(node_dir):
@ -326,7 +513,7 @@ class Test(BaseTest):
swap_clients = self.swap_clients swap_clients = self.swap_clients
ci0 = swap_clients[0].ci(self.test_coin) ci0 = swap_clients[0].ci(self.test_coin)
utxos = ci0.getNewAddress() utxos = ci0.rpc_wallet('listunspent')
addr_out = ci0.rpc_wallet('getnewaddress') addr_out = ci0.rpc_wallet('getnewaddress')
rtx = ci0.rpc_wallet('createrawtransaction', [[], {addr_out: 2.0}]) rtx = ci0.rpc_wallet('createrawtransaction', [[], {addr_out: 2.0}])
@ -530,7 +717,8 @@ class Test(BaseTest):
assert (len(unspents) == 1) assert (len(unspents) == 1)
utxo = unspents[0] utxo = unspents[0]
txout = ci0.rpc('gettxout', [utxo['txid'], utxo['vout'], utxo['tree']]) include_mempool: bool = False
txout = ci0.rpc('gettxout', [utxo['txid'], utxo['vout'], utxo['tree'], include_mempool])
# Lock utxo so it's not spent for tickets, while waiting for depth # Lock utxo so it's not spent for tickets, while waiting for depth
rv = ci0.rpc_wallet('lockunspent', [False, [utxo, ]]) rv = ci0.rpc_wallet('lockunspent', [False, [utxo, ]])
@ -538,7 +726,7 @@ class Test(BaseTest):
def wait_for_depth(): def wait_for_depth():
for i in range(20): for i in range(20):
logging.info('Waiting for txout depth, iter {}'.format(i)) logging.info('Waiting for txout depth, iter {}'.format(i))
txout = ci0.rpc('gettxout', [utxo['txid'], utxo['vout'], utxo['tree']]) txout = ci0.rpc('gettxout', [utxo['txid'], utxo['vout'], utxo['tree'], True])
if txout['confirmations'] > 0: if txout['confirmations'] > 0:
return txout return txout
test_delay_event.wait(1) test_delay_event.wait(1)
@ -555,11 +743,11 @@ class Test(BaseTest):
sent_txid = ci0.rpc_wallet('sendrawtransaction', [stx['hex'], ]) sent_txid = ci0.rpc_wallet('sendrawtransaction', [stx['hex'], ])
# NOTE: UTXO is still found when spent in the mempool (tested in loop, not delay from wallet to core) # NOTE: UTXO is still found when spent in the mempool (tested in loop, not delay from wallet to core)
txout = ci0.rpc('gettxout', [utxo['txid'], utxo['vout'], utxo['tree']]) txout = ci0.rpc('gettxout', [utxo['txid'], utxo['vout'], utxo['tree'], include_mempool])
assert (addr in txout['scriptPubKey']['addresses']) assert (addr in txout['scriptPubKey']['addresses'])
for i in range(20): for i in range(20):
txout = ci0.rpc('gettxout', [utxo['txid'], utxo['vout'], utxo['tree']]) txout = ci0.rpc('gettxout', [utxo['txid'], utxo['vout'], utxo['tree'], include_mempool])
if txout is None: if txout is None:
logging.info('txout spent, height before spent {}, height spent {}'.format(chain_height_before_send, ci0.getChainHeight())) logging.info('txout spent, height before spent {}, height spent {}'.format(chain_height_before_send, ci0.getChainHeight()))
break break
@ -574,6 +762,24 @@ class Test(BaseTest):
amount_proved = ci0.verifyProofOfFunds(funds_proof[0], funds_proof[1], funds_proof[2], 'test'.encode('utf-8')) amount_proved = ci0.verifyProofOfFunds(funds_proof[0], funds_proof[1], funds_proof[2], 'test'.encode('utf-8'))
assert (amount_proved >= require_amount) assert (amount_proved >= require_amount)
def test_02_part_coin(self):
test_success_path(self, Coins.PART, self.test_coin)
def test_03_coin_part(self):
test_success_path(self, self.test_coin, Coins.PART)
def test_04_part_coin_bad_ptx(self):
test_bad_ptx(self, Coins.PART, self.test_coin)
def test_05_coin_part_bad_ptx(self):
test_bad_ptx(self, self.test_coin, Coins.PART)
def test_06_part_coin_itx_refund(self):
test_itx_refund(self, Coins.PART, self.test_coin)
def test_07_coin_part_itx_refund(self):
test_itx_refund(self, self.test_coin, Coins.PART)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

@ -319,7 +319,6 @@ class Test(unittest.TestCase):
ci_btc = BTCInterface(coin_settings, 'regtest') ci_btc = BTCInterface(coin_settings, 'regtest')
for i in range(10000): for i in range(10000):
test_pairs = random.randint(0, 3) test_pairs = random.randint(0, 3)
if test_pairs == 0: if test_pairs == 0:
ci_from = ci_btc ci_from = ci_btc
@ -425,6 +424,9 @@ class Test(unittest.TestCase):
msg_buf_v2.ParseFromString(serialised_msg) msg_buf_v2.ParseFromString(serialised_msg)
assert (msg_buf_v2.protocol_version == 2) assert (msg_buf_v2.protocol_version == 2)
assert (msg_buf_v2.time_valid == 1024) assert (msg_buf_v2.time_valid == 1024)
assert (msg_buf_v2.amount == 0)
assert (msg_buf_v2.pkhash_buyer is not None)
assert (len(msg_buf_v2.pkhash_buyer) == 0)
# Decode only the first field # Decode only the first field
msg_buf_v2.ParseFromString(serialised_msg[:2]) msg_buf_v2.ParseFromString(serialised_msg[:2])

@ -13,17 +13,16 @@ $ pytest -v -s tests/basicswap/test_run.py::Test::test_04_ltc_btc
""" """
import os
import random import random
import logging import logging
import unittest import unittest
from basicswap.basicswap import ( from basicswap.basicswap import (
BidStates,
Coins, Coins,
DebugTypes,
SwapTypes, SwapTypes,
BidStates,
TxStates, TxStates,
DebugTypes,
) )
from basicswap.basicswap_util import ( from basicswap.basicswap_util import (
TxLockTypes, TxLockTypes,
@ -40,24 +39,23 @@ from tests.basicswap.util import (
read_json_api, read_json_api,
) )
from tests.basicswap.common import ( from tests.basicswap.common import (
wait_for_offer, BTC_BASE_RPC_PORT,
wait_for_bid, compare_bid_states,
LTC_BASE_RPC_PORT,
TEST_HTTP_PORT,
wait_for_balance, wait_for_balance,
wait_for_unspent, wait_for_bid,
wait_for_bid_tx_state, wait_for_bid_tx_state,
wait_for_in_progress, wait_for_in_progress,
TEST_HTTP_PORT, wait_for_offer,
LTC_BASE_RPC_PORT, wait_for_unspent,
BTC_BASE_RPC_PORT,
compare_bid_states,
extract_states_from_xu_file,
) )
from basicswap.contrib.test_framework.messages import ( from basicswap.contrib.test_framework.messages import (
ToHex,
CTxIn,
COutPoint, COutPoint,
CTransaction, CTransaction,
CTxIn,
CTxInWitness, CTxInWitness,
ToHex,
) )
from basicswap.contrib.test_framework.script import ( from basicswap.contrib.test_framework.script import (
CScript, CScript,
@ -88,10 +86,6 @@ class Test(BaseTest):
wait_for_balance(test_delay_event, 'http://127.0.0.1:1801/json/wallets/btc', 'balance', 1000.0) wait_for_balance(test_delay_event, 'http://127.0.0.1:1801/json/wallets/btc', 'balance', 1000.0)
wait_for_balance(test_delay_event, 'http://127.0.0.1:1801/json/wallets/ltc', 'balance', 1000.0) wait_for_balance(test_delay_event, 'http://127.0.0.1:1801/json/wallets/ltc', 'balance', 1000.0)
diagrams_dir = 'doc/protocols/sequence_diagrams'
cls.states_bidder = extract_states_from_xu_file(os.path.join(diagrams_dir, 'bidder.alt.xu'), 'B')
cls.states_offerer = extract_states_from_xu_file(os.path.join(diagrams_dir, 'offerer.alt.xu'), 'O')
# Wait for height, or sequencelock is thrown off by genesis blocktime # Wait for height, or sequencelock is thrown off by genesis blocktime
cls.waitForParticlHeight(3) cls.waitForParticlHeight(3)
@ -329,7 +323,8 @@ class Test(BaseTest):
logging.info('---------- Test PART to LTC') logging.info('---------- Test PART to LTC')
swap_clients = self.swap_clients swap_clients = self.swap_clients
offer_id = swap_clients[0].postOffer(Coins.PART, Coins.LTC, 100 * COIN, 0.1 * COIN, 100 * COIN, SwapTypes.SELLER_FIRST) swap_value = 100 * COIN
offer_id = swap_clients[0].postOffer(Coins.PART, Coins.LTC, swap_value, 0.1 * COIN, swap_value, SwapTypes.SELLER_FIRST)
wait_for_offer(test_delay_event, swap_clients[1], offer_id) wait_for_offer(test_delay_event, swap_clients[1], offer_id)
offer = swap_clients[1].getOffer(offer_id) offer = swap_clients[1].getOffer(offer_id)
@ -341,21 +336,34 @@ class Test(BaseTest):
wait_for_in_progress(test_delay_event, swap_clients[1], bid_id, sent=True) wait_for_in_progress(test_delay_event, swap_clients[1], bid_id, sent=True)
wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=60) wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=80)
wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=60) wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=80)
js_0 = read_json_api(1800) # Verify lock tx spends are found in the expected wallets
js_1 = read_json_api(1801) bid, offer = swap_clients[0].getBidAndOffer(bid_id)
assert (js_0['num_swapping'] == 0 and js_0['num_watched_outputs'] == 0) max_fee: int = 1000
assert (js_1['num_swapping'] == 0 and js_1['num_watched_outputs'] == 0) itx_spend = bid.initiate_tx.spend_txid.hex()
ci_node_1_from = swap_clients[1].ci(Coins.PART)
wtx = ci_node_1_from.rpc_wallet('gettransaction', [itx_spend,])
assert (swap_value - ci_node_1_from.make_int(wtx['details'][0]['amount']) < max_fee)
ci_node_0_to = swap_clients[0].ci(Coins.LTC)
ptx_spend = bid.participate_tx.spend_txid.hex()
wtx = ci_node_0_to.rpc_wallet('gettransaction', [ptx_spend,])
assert (bid.amount_to - ci_node_0_to.make_int(wtx['details'][0]['amount']) < max_fee)
bid_id_hex = bid_id.hex() bid_id_hex = bid_id.hex()
path = f'bids/{bid_id_hex}/states' path = f'bids/{bid_id_hex}/states'
offerer_states = read_json_api(1800, path) offerer_states = read_json_api(1800, path)
bidder_states = read_json_api(1801, path) bidder_states = read_json_api(1801, path)
assert (compare_bid_states(offerer_states, self.states_offerer[0]) is True) assert (compare_bid_states(offerer_states, self.states_offerer_sh[0]) is True)
assert (compare_bid_states(bidder_states, self.states_bidder[0]) is True) assert (compare_bid_states(bidder_states, self.states_bidder_sh[0]) is True)
js_0 = read_json_api(1800)
js_1 = read_json_api(1801)
assert (js_0['num_swapping'] == 0 and js_0['num_watched_outputs'] == 0)
assert (js_1['num_swapping'] == 0 and js_1['num_watched_outputs'] == 0)
def test_03_ltc_part(self): def test_03_ltc_part(self):
logging.info('---------- Test LTC to PART') logging.info('---------- Test LTC to PART')
@ -372,8 +380,8 @@ class Test(BaseTest):
wait_for_in_progress(test_delay_event, swap_clients[0], bid_id, sent=True) wait_for_in_progress(test_delay_event, swap_clients[0], bid_id, sent=True)
wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=60) wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=80)
wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, wait_for=60) wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, wait_for=80)
js_0 = read_json_api(1800) js_0 = read_json_api(1800)
js_1 = read_json_api(1801) js_1 = read_json_api(1801)
@ -395,8 +403,8 @@ class Test(BaseTest):
wait_for_in_progress(test_delay_event, swap_clients[1], bid_id, sent=True) wait_for_in_progress(test_delay_event, swap_clients[1], bid_id, sent=True)
wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=60) wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=80)
wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=60) wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=80)
js_0 = read_json_api(1800) js_0 = read_json_api(1800)
js_1 = read_json_api(1801) js_1 = read_json_api(1801)
@ -419,8 +427,8 @@ class Test(BaseTest):
read_json_api(1801, 'bids/{}'.format(bid_id.hex()), {'abandon': True}) read_json_api(1801, 'bids/{}'.format(bid_id.hex()), {'abandon': True})
swap_clients[0].acceptBid(bid_id) swap_clients[0].acceptBid(bid_id)
wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=60) wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=80)
wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.BID_ABANDONED, sent=True, wait_for=60) wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.BID_ABANDONED, sent=True, wait_for=80)
js_0_bid = read_json_api(1800, 'bids/{}'.format(bid_id.hex())) js_0_bid = read_json_api(1800, 'bids/{}'.format(bid_id.hex()))
js_1_bid = read_json_api(1801, 'bids/{}'.format(bid_id.hex())) js_1_bid = read_json_api(1801, 'bids/{}'.format(bid_id.hex()))
@ -437,7 +445,7 @@ class Test(BaseTest):
offerer_states = read_json_api(1800, path) offerer_states = read_json_api(1800, path)
bidder_states = read_json_api(1801, path) bidder_states = read_json_api(1801, path)
assert (compare_bid_states(offerer_states, self.states_offerer[1]) is True) assert (compare_bid_states(offerer_states, self.states_offerer_sh[1]) is True)
assert (bidder_states[-1][1] == 'Bid Abandoned') assert (bidder_states[-1][1] == 'Bid Abandoned')
def test_06_self_bid(self): def test_06_self_bid(self):
@ -455,8 +463,8 @@ class Test(BaseTest):
wait_for_bid(test_delay_event, swap_clients[0], bid_id) wait_for_bid(test_delay_event, swap_clients[0], bid_id)
swap_clients[0].acceptBid(bid_id) swap_clients[0].acceptBid(bid_id)
wait_for_bid_tx_state(test_delay_event, swap_clients[0], bid_id, TxStates.TX_REDEEMED, TxStates.TX_REDEEMED, wait_for=60) wait_for_bid_tx_state(test_delay_event, swap_clients[0], bid_id, TxStates.TX_REDEEMED, TxStates.TX_REDEEMED, wait_for=80)
wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=60) wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=80)
js_0 = read_json_api(1800) js_0 = read_json_api(1800)
assert (js_0['num_swapping'] == 0 and js_0['num_watched_outputs'] == 0) assert (js_0['num_swapping'] == 0 and js_0['num_watched_outputs'] == 0)
@ -504,8 +512,8 @@ class Test(BaseTest):
offer = swap_clients[1].getOffer(offer_id) offer = swap_clients[1].getOffer(offer_id)
bid_id = swap_clients[1].postBid(offer_id, offer.amount_from) bid_id = swap_clients[1].postBid(offer_id, offer.amount_from)
wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=60) wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=80)
wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=60) wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=80)
def test_10_bad_ptx(self): def test_10_bad_ptx(self):
# Invalid PTX sent, swap should stall and ITx and PTx should be reclaimed by senders # Invalid PTX sent, swap should stall and ITx and PTx should be reclaimed by senders
@ -543,8 +551,8 @@ class Test(BaseTest):
offerer_states = read_json_api(1800, path) offerer_states = read_json_api(1800, path)
bidder_states = read_json_api(1801, path) bidder_states = read_json_api(1801, path)
assert (compare_bid_states(offerer_states, self.states_offerer[1]) is True) assert (compare_bid_states(offerer_states, self.states_offerer_sh[1]) is True)
assert (compare_bid_states(bidder_states, self.states_bidder[1]) is True) assert (compare_bid_states(bidder_states, self.states_bidder_sh[1]) is True)
''' '''
def test_11_refund(self): def test_11_refund(self):
@ -654,8 +662,8 @@ class Test(BaseTest):
offerer_states = read_json_api(1800, path) offerer_states = read_json_api(1800, path)
bidder_states = read_json_api(1801, path) bidder_states = read_json_api(1801, path)
assert (compare_bid_states(offerer_states, self.states_offerer[2]) is True) assert (compare_bid_states(offerer_states, self.states_offerer_sh[2]) is True)
assert (compare_bid_states(bidder_states, self.states_bidder[2], exact_match=False) is True) assert (compare_bid_states(bidder_states, self.states_bidder_sh[2], exact_match=False) is True)
def test_14_sweep_balance(self): def test_14_sweep_balance(self):
logging.info('---------- Test sweep balance offer') logging.info('---------- Test sweep balance offer')
@ -718,8 +726,8 @@ class Test(BaseTest):
wait_for_bid(test_delay_event, swap_clients[2], bid_id) wait_for_bid(test_delay_event, swap_clients[2], bid_id)
swap_clients[2].acceptBid(bid_id) swap_clients[2].acceptBid(bid_id)
wait_for_bid(test_delay_event, swap_clients[2], bid_id, BidStates.SWAP_COMPLETED, wait_for=60) wait_for_bid(test_delay_event, swap_clients[2], bid_id, BidStates.SWAP_COMPLETED, wait_for=80)
wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=60) wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=80)
# Verify expected inputs were used # Verify expected inputs were used
bid, offer = swap_clients[2].getBidAndOffer(bid_id) bid, offer = swap_clients[2].getBidAndOffer(bid_id)
@ -753,8 +761,8 @@ class Test(BaseTest):
wait_for_in_progress(test_delay_event, swap_clients[0], bid_id, sent=True) wait_for_in_progress(test_delay_event, swap_clients[0], bid_id, sent=True)
wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=60) wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=80)
wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, wait_for=60) wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, wait_for=80)
def pass_99_delay(self): def pass_99_delay(self):
logging.info('Delay') logging.info('Delay')

@ -334,6 +334,9 @@ class BaseTest(unittest.TestCase):
cls.states_bidder = extract_states_from_xu_file(os.path.join(diagrams_dir, 'ads.bidder.alt.xu'), 'B') cls.states_bidder = extract_states_from_xu_file(os.path.join(diagrams_dir, 'ads.bidder.alt.xu'), 'B')
cls.states_offerer = extract_states_from_xu_file(os.path.join(diagrams_dir, 'ads.offerer.alt.xu'), 'O') cls.states_offerer = extract_states_from_xu_file(os.path.join(diagrams_dir, 'ads.offerer.alt.xu'), 'O')
cls.states_bidder_sh = extract_states_from_xu_file(os.path.join(diagrams_dir, 'bidder.alt.xu'), 'B')
cls.states_offerer_sh = extract_states_from_xu_file(os.path.join(diagrams_dir, 'offerer.alt.xu'), 'O')
if os.path.isdir(TEST_DIR): if os.path.isdir(TEST_DIR):
if RESET_TEST: if RESET_TEST:
logging.info('Removing ' + TEST_DIR) logging.info('Removing ' + TEST_DIR)

Loading…
Cancel
Save