Decred: Secret hash swap tests.

master^2
tecnovert 7 months ago
parent d527ec4974
commit 76879a2ff5
  1. 450
      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. 202
      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.
import os
import re
import sys
import zmq
import copy
@ -16,7 +15,6 @@ import random
import shutil
import string
import struct
import hashlib
import secrets
import datetime as dt
import threading
@ -53,11 +51,13 @@ from .util.script import (
)
from .util.address import (
toWIF,
getKeyID,
decodeWif,
decodeAddress,
pubkeyToAddress,
)
from .util.crypto import (
sha256,
)
from basicswap.util.network import is_private_ip_address
from .chainparams import (
Coins,
@ -147,7 +147,7 @@ from basicswap.db_util import (
remove_expired_data,
)
PROTOCOL_VERSION_SECRET_HASH = 4
PROTOCOL_VERSION_SECRET_HASH = 5
MINPROTO_VERSION_SECRET_HASH = 4
PROTOCOL_VERSION_ADAPTOR_SIG = 4
@ -209,6 +209,16 @@ class WatchedOutput(): # Watch for spends
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():
# TODO
# 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
except Exception:
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.remove()
@ -472,7 +486,9 @@ class BasicSwap(BaseApp):
'blocks_confirmed': chain_client_settings.get('blocks_confirmed', 6),
'conf_target': chain_client_settings.get('conf_target', 2),
'watched_outputs': [],
'watched_scripts': [],
'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_csv': chain_client_settings.get('use_csv', default_csv),
'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:
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 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 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
@ -1172,6 +1193,9 @@ class BasicSwap(BaseApp):
self.removeWatchedOutput(Coins(offer.coin_from), 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):
# Return unused addrs to pool
itx_state = bid.getITxState()
@ -1859,7 +1883,7 @@ class BasicSwap(BaseApp):
path += '/' + str(date.year) + '/' + str(date.month) + '/' + str(date.day)
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):
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)
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:
raise ValueError('TODO')
@ -2370,6 +2399,9 @@ class BasicSwap(BaseApp):
)
bid.setState(BidStates.BID_SENT)
if len(msg_buf.pkhash_buyer_to) > 0:
bid.pkhash_buyer_to = msg_buf.pkhash_buyer_to
try:
session = scoped_session(self.session_factory)
self.saveBidInSession(bid_id, bid, session)
@ -2554,13 +2586,19 @@ class BasicSwap(BaseApp):
coin_from = Coins(offer.coin_from)
ci_from = self.ci(coin_from)
ci_to = self.ci(offer.coin_to)
bid_date = dt.datetime.fromtimestamp(bid.created_at).date()
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)
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:
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:
if offer.lock_type < TxLockTypes.ABS_LOCK_BLOCKS:
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:
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:
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)
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 = pkhash_refund
bid.pkhash_seller = ci_to.pkh(pubkey_refund)
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
refund_txn = self.createRefundTxn(coin_from, txn, offer, bid, script)
@ -2595,6 +2631,7 @@ class BasicSwap(BaseApp):
bid_id=bid_id,
tx_type=TxTypes.ITX,
txid=bytes.fromhex(txid),
vout=lock_tx_vout,
tx_data=bytes.fromhex(txn),
script=script,
)
@ -2615,6 +2652,11 @@ class BasicSwap(BaseApp):
msg_buf.initiate_txid = bytes.fromhex(txid)
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()
payload_hex = str.format('{:02x}', MessageTypes.BID_ACCEPT) + bid_bytes.hex()
@ -3137,9 +3179,9 @@ class BasicSwap(BaseApp):
if save_bid:
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':
return None
return None, None
ci = self.ci(coin_type)
if ci.using_segwit():
@ -3154,7 +3196,12 @@ class BasicSwap(BaseApp):
txn_signed = pi.promoteMockTx(ci, prefunded_tx, initiate_script).hex()
else:
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:
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)
pkhash_seller = bid.pkhash_seller
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
lock_value = offer.lock_value // 2
if offer.lock_type < TxLockTypes.ABS_LOCK_BLOCKS:
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:
# Lock from the height or time of the block containing the initiate txn
coin_from = Coins(offer.coin_from)
initiate_tx_block_hash = self.callcoinrpc(coin_from, 'getblockhash', [bid.initiate_tx.chain_height, ])
initiate_tx_block_time = int(self.callcoinrpc(coin_from, 'getblock', [initiate_tx_block_hash, ])['time'])
block_header = self.ci(coin_from).getBlockHeaderFromHeight(bid.initiate_tx.chain_height)
initiate_tx_block_hash = block_header['hash']
initiate_tx_block_time = block_header['time']
if offer.lock_type == TxLockTypes.ABS_LOCK_BLOCKS:
# Walk the coin_to chain back until block time matches
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)
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)
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
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)
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])
txid = txjs['txid']
@ -3252,7 +3309,7 @@ class BasicSwap(BaseApp):
prev_p2wsh = ci.getScriptDest(txn_script)
script_pub_key = prev_p2wsh.hex()
else:
script_pub_key = getP2SHScriptForHash(getKeyID(txn_script)).hex()
script_pub_key = getP2SHScriptForHash(ci.pkh(txn_script)).hex()
prevout = {
'txid': prev_txnid,
@ -3262,18 +3319,17 @@ class BasicSwap(BaseApp):
'amount': ci.format_amount(prev_amount)}
bid_date = dt.datetime.fromtimestamp(bid.created_at).date()
if coin_type in (Coins.NAV, ):
wif_prefix = chainparams[coin_type][self.chain]['key_prefix']
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))
privkey = self.getContractPrivkey(bid_date, bid.contract_count)
pubkey = ci.getPubkey(privkey)
secret = bid.recovered_secret
if secret is None:
secret = self.getContractSecret(bid_date, bid.contract_count)
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':
return None
@ -3294,40 +3350,40 @@ class BasicSwap(BaseApp):
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)
else:
redeem_txn = ci.createRedeemTxn(prevout, addr_redeem_out, amount_out)
options = {}
if ci.using_segwit():
options['force_segwit'] = True
if coin_type in (Coins.NAV, ):
redeem_sig = ci.getTxSignature(redeem_txn, prevout, privkey)
if coin_type in (Coins.NAV, Coins.DCR):
privkey_wif = self.ci(coin_type).encodeKey(privkey)
redeem_sig = ci.getTxSignature(redeem_txn, prevout, privkey_wif)
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():
witness_stack = [
bytes.fromhex(redeem_sig),
pubkey,
secret,
bytes((1,)),
bytes((1,)), # Converted to OP_1 in Decred push_script_data
txn_script]
redeem_txn = ci.setTxSignature(bytes.fromhex(redeem_txn), witness_stack).hex()
else:
script = format(len(redeem_sig) // 2, '02x') + redeem_sig
script += format(33, '02x') + pubkey.hex()
script += format(32, '02x') + secret.hex()
script += format(OpCodes.OP_1, '02x')
script += format(OpCodes.OP_PUSHDATA1, '02x') + format(len(txn_script), '02x') + txn_script.hex()
redeem_txn = ci.setTxScriptSig(bytes.fromhex(redeem_txn), 0, bytes.fromhex(script)).hex()
script = (len(redeem_sig) // 2).to_bytes(1) + bytes.fromhex(redeem_sig)
script += (33).to_bytes(1) + pubkey
script += (32).to_bytes(1) + secret
script += (OpCodes.OP_1).to_bytes(1)
script += (OpCodes.OP_PUSHDATA1).to_bytes(1) + (len(txn_script)).to_bytes(1) + txn_script
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
ro = ci.verifyRawTransaction(redeem_txn, [prevout])
else:
ro = self.callcoinrpc(Coins.PART, 'verifyrawtransaction', [redeem_txn, [prevout]])
ensure(ro['inputs_valid'] is True, 'inputs_valid is false')
# outputs_valid will be false if not a Particl txn
# ensure(ro['complete'] is True, 'complete is false')
@ -3337,7 +3393,11 @@ class BasicSwap(BaseApp):
# Check fee
if ci.get_connection_type() == 'rpc':
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'])
ensure(tx_vsize >= redeem_txjs['vsize'], 'underpaid fee')
else:
@ -3355,11 +3415,9 @@ class BasicSwap(BaseApp):
ci = self.ci(coin_type)
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)
else:
# TODO: Sign in bsx for all coins
wif_prefix = chainparams[Coins.PART][self.chain]['key_prefix']
txjs = self.callcoinrpc(Coins.PART, 'decoderawtransaction', [txn])
if ci.using_segwit():
p2wsh = ci.getScriptDest(txn_script)
@ -3377,8 +3435,9 @@ class BasicSwap(BaseApp):
}
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)
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:
locktime = lock_value
if ci.use_p2shp2wsh():
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 = {}
if self.coin_clients[coin_type]['use_segwit']:
options['force_segwit'] = True
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:
refund_sig = self.callcoinrpc(Coins.PART, 'createsignaturewithkey', [refund_txn, prevout, privkey, 'ALL', options])
if coin_type == Coins.PART or self.coin_clients[coin_type]['use_segwit']:
privkey_wif = self.ci(Coins.PART).encodeKey(privkey)
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 = [
bytes.fromhex(refund_sig),
pubkey,
@ -3425,11 +3490,11 @@ class BasicSwap(BaseApp):
txn_script]
refund_txn = ci.setTxSignature(bytes.fromhex(refund_txn), witness_stack).hex()
else:
script = format(len(refund_sig) // 2, '02x') + refund_sig
script += format(33, '02x') + pubkey.hex()
script += format(OpCodes.OP_0, '02x')
script += format(OpCodes.OP_PUSHDATA1, '02x') + format(len(txn_script), '02x') + txn_script.hex()
refund_txn = ci.setTxScriptSig(bytes.fromhex(refund_txn), 0, bytes.fromhex(script)).hex()
script = (len(refund_sig) // 2).to_bytes(1) + bytes.fromhex(refund_sig)
script += (33).to_bytes(1) + pubkey
script += (OpCodes.OP_0).to_bytes(1)
script += (OpCodes.OP_PUSHDATA1).to_bytes(1) + (len(txn_script)).to_bytes(1) + txn_script
refund_txn = ci.setTxScriptSig(bytes.fromhex(refund_txn), 0, script)
if coin_type in (Coins.NAV, Coins.DCR):
# Only checks signature
@ -3445,8 +3510,12 @@ class BasicSwap(BaseApp):
if self.debug:
# Check fee
if ci.get_connection_type() == 'rpc':
refund_txjs = self.callcoinrpc(coin_type, 'decoderawtransaction', [refund_txn])
if ci.using_segwit() or coin_type in (Coins.PART, ):
refund_txjs = self.callcoinrpc(coin_type, 'decoderawtransaction', [refund_txn,])
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'])
ensure(tx_vsize >= refund_txjs['vsize'], 'underpaid fee')
else:
@ -3490,20 +3559,31 @@ class BasicSwap(BaseApp):
tx_type=TxTypes.PTX,
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
def setLastHeightChecked(self, coin_type, tx_height: int) -> int:
coin_name = self.ci(coin_type).coin_name()
def setLastHeightCheckedStart(self, coin_type, tx_height: int) -> int:
ci = self.ci(coin_type)
coin_name = ci.coin_name()
if tx_height < 1:
tx_height = self.lookupChainHeight(coin_type)
if len(self.coin_clients[coin_type]['watched_outputs']) == 0:
self.coin_clients[coin_type]['last_height_checked'] = tx_height
block_header = ci.getBlockHeaderFromHeight(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)
if self.coin_clients[coin_type]['last_height_checked'] > tx_height:
self.coin_clients[coin_type]['last_height_checked'] = tx_height
elif cc['last_height_checked'] > tx_height:
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('Rewind checking of %s chain to height %d', coin_name, 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:
# 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:
bid.participate_tx = SwapTx(
@ -3529,6 +3609,11 @@ class BasicSwap(BaseApp):
def participateTxnConfirmed(self, bid_id: bytes, bid, offer) -> None:
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.setPTxState(TxStates.TX_CONFIRMED)
@ -3774,9 +3859,9 @@ class BasicSwap(BaseApp):
return rv
# 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)
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:
return rv
@ -3863,7 +3948,7 @@ class BasicSwap(BaseApp):
refund_tx = bid.txns[TxTypes.XMR_SWAP_A_LOCK_REFUND]
if refund_tx.block_time is None:
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:
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
if state == BidStates.BID_ACCEPTED:
# 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
tx_height = None
initiate_txnid_hex = bid.initiate_tx.txid.hex()
last_initiate_txn_conf = bid.initiate_tx.conf
ci_from = self.ci(coin_from)
if coin_from == Coins.PART: # Has txindex
try:
p2sh = ci_from.encode_p2sh(bid.initiate_tx.script)
initiate_txn = self.callcoinrpc(coin_from, 'getrawtransaction', [initiate_txnid_hex, True])
# Verify amount
vout = getVoutByAddress(initiate_txn, p2sh)
@ -3932,24 +4017,29 @@ class BasicSwap(BaseApp):
dest_script = ci_from.getScriptDest(bid.initiate_tx.script)
addr = ci_from.encodeScriptDest(dest_script)
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:
bid.initiate_tx.conf = found['depth']
if 'index' in found:
index = found['index']
tx_height = found['height']
if bid.initiate_tx.conf != last_initiate_txn_conf:
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:
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:
bid.initiate_tx.vout = index
if (last_initiate_txn_conf is None or last_initiate_txn_conf < 1) and tx_height > 0:
# 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.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)
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:
index = found.get('index', participate_txvout)
if bid.participate_tx.conf != found['depth']:
save_bid = True
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'])
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)
save_bid = True
bid.participate_tx.conf = found['depth']
if found['height'] > 0 and bid.participate_tx.block_height is None:
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)
# State will update when spend is detected
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))
return False # Bid is still active
def extractSecret(self, coin_type, bid, spend_in):
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')
return bytes.fromhex(spend_in['txinwitness'][2])
else:
@ -4064,7 +4163,7 @@ class BasicSwap(BaseApp):
return 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']
@ -4085,7 +4184,29 @@ class BasicSwap(BaseApp):
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)
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)
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.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)
# TODO: More SwapTypes
@ -4268,7 +4389,7 @@ class BasicSwap(BaseApp):
session.remove()
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.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'])
@ -4283,13 +4404,65 @@ class BasicSwap(BaseApp):
else:
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):
# assert (self.mxDB.locked())
self.log.debug('checkForSpends %s', Coins(coin_type).name)
# TODO: Check for spends on watchonly txns where possible
if 'have_spent_index' in self.coin_clients[coin_type] and self.coin_clients[coin_type]['have_spent_index']:
if self.coin_clients[coin_type].get('have_spent_index', False):
# TODO: batch getspentinfo
for o in c['watched_outputs']:
found_spend = None
@ -4304,13 +4477,16 @@ class BasicSwap(BaseApp):
spend_n = found_spend['index']
spend_txn = self.callcoinrpc(Coins.PART, 'getrawtransaction', [spend_txid, True])
self.processSpentOutput(coin_type, o, spend_txid, spend_n, spend_txn)
else:
return
ci = self.ci(coin_type)
chain_blocks = ci.getChainHeight()
last_height_checked = c['last_height_checked']
self.log.debug('chain_blocks, last_height_checked %s %s', chain_blocks, last_height_checked)
last_height_checked: int = c['last_height_checked']
block_check_min_time: int = c['block_check_min_time']
self.log.debug('chain_blocks, last_height_checked %d %d', chain_blocks, last_height_checked)
while last_height_checked < chain_blocks:
block_hash = self.callcoinrpc(coin_type, 'getblockhash', [last_height_checked + 1])
block_hash = ci.rpc('getblockhash', [last_height_checked + 1])
try:
block = ci.getBlockWithTxns(block_hash)
except Exception as e:
@ -4324,19 +4500,33 @@ class BasicSwap(BaseApp):
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 i, inp in enumerate(tx['vin']):
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 o in c['watched_outputs']:
for i, inp in enumerate(tx['vin']):
inp_txid = inp.get('txid', None)
if inp_txid is None: # Coinbase
continue
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.processSpentOutput(coin_type, o, tx['txid'], i, tx)
last_height_checked += 1
if c['last_height_checked'] != last_height_checked:
c['last_height_checked'] = last_height_checked
self.setIntKV('last_height_checked_' + ci.coin_name().lower(), last_height_checked)
self.updateCheckedBlock(ci, c, block)
def expireMessages(self) -> None:
if self._is_locked is True:
@ -4572,7 +4762,7 @@ class BasicSwap(BaseApp):
return
offer_data.ParseFromString(offer_bytes)
# Validate data
# Validate offer data
now: int = self.getTime()
coin_from = Coins(offer_data.coin_from)
ci_from = self.ci(coin_from)
@ -4839,7 +5029,7 @@ class BasicSwap(BaseApp):
bid_data = BidMessage()
bid_data.ParseFromString(bid_bytes)
# Validate data
# Validate bid data
ensure(bid_data.protocol_version >= MINPROTO_VERSION_SECRET_HASH, 'Invalid protocol version')
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_b_height_start=ci_to.getChainHeight(),
)
if len(bid_data.pkhash_buyer_to) > 0:
bid.pkhash_buyer_to = bid_data.pkhash_buyer_to
else:
ensure(bid.state == BidStates.BID_SENT, 'Wrong bid state: {}'.format(BidStates(bid.state).name))
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
# TODO: Verify script without decoding?
decoded_script = self.callcoinrpc(Coins.PART, 'decodescript', [bid_accept_data.contract_script.hex()])
lock_check_op = 'OP_CHECKSEQUENCEVERIFY' if use_csv else 'OP_CHECKLOCKTIMEVERIFY'
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))
rr = prog.match(decoded_script['asm'])
if not rr:
if coin_from in (Coins.DCR, ):
op_hash = OpCodes.OP_SHA256_DECRED
else:
op_hash = OpCodes.OP_SHA256
op_lock = OpCodes.OP_CHECKSEQUENCEVERIFY if use_csv else OpCodes.OP_CHECKLOCKTIMEVERIFY
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')
scriptvalues = rr.groups()
ensure(len(scriptvalues[0]) == 64, 'Bad secret_hash length')
ensure(bytes.fromhex(scriptvalues[1]) == bid.pkhash_buyer, 'pkhash_buyer mismatch')
ensure(script_pkhash1 == bid.pkhash_buyer, 'pkhash_buyer mismatch')
script_lock_value = int(scriptvalues[2])
if use_csv:
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:
if offer.lock_type == TxLockTypes.ABS_LOCK_BLOCKS:
block_header_from = ci_from.getBlockHeaderAt(now)
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_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 high')
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:
ensure(script_lock_value <= 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(len(scriptvalues[3]) == 40, 'pkhash_refund bad length')
ensure(script_lock_val <= 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.ABS_LOCK_TIME_LEEWAY, 'script lock time too low')
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,
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.setITxState(TxStates.TX_NONE)
@ -5322,7 +5516,7 @@ class BasicSwap(BaseApp):
reverse_bid: bool = self.is_reverse_ads_bid(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)
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:
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
if len(c['watched_outputs']) > 0:
if len(c['watched_outputs']) > 0 or len(c['watched_scripts']):
self.checkForSpends(k, c)
self._last_checked_watched = now

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

@ -11,7 +11,7 @@ from enum import IntEnum, auto
from sqlalchemy.ext.declarative import declarative_base
CURRENT_DB_VERSION = 23
CURRENT_DB_VERSION = 24
CURRENT_DB_DATA_VERSION = 4
Base = declarative_base()
@ -127,6 +127,7 @@ class Bid(Base):
amount_to = sa.Column(sa.BigInteger) # amount * offer.rate
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)
rate = sa.Column(sa.BigInteger)
@ -522,3 +523,14 @@ class MessageLink(Base):
msg_type = sa.Column(sa.Integer)
msg_sequence = sa.Column(sa.Integer)
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:
db_version += 1
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:
self.db_version = db_version
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:
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:
self.closeSession(session)

@ -19,6 +19,9 @@ from basicswap.util import (
format_amount,
TemporaryError,
)
from basicswap.util.crypto import (
hash160,
)
from basicswap.util.ecc import (
ep,
getSecretInt,
@ -37,6 +40,10 @@ class Curves(IntEnum):
class CoinInterface:
@staticmethod
def watch_blocks_for_scripts() -> bool:
return False
def __init__(self, network):
self.setDefaults()
self._network = network
@ -101,6 +108,10 @@ class CoinInterface:
def has_segwit(self) -> bool:
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:
if isinstance(ex, TemporaryError):
return True
@ -128,6 +139,16 @@ class CoinInterface:
def walletRestoreHeight(self) -> int:
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):
@staticmethod
@ -137,9 +158,12 @@ class Secp256k1Interface(CoinInterface):
def getNewSecretKey(self) -> bytes:
return i2b(getSecretInt())
def getPubkey(self, privkey):
def getPubkey(self, privkey: bytes) -> bytes:
return PublicKey.from_secret(privkey).format()
def pkh(self, pubkey: bytes) -> bytes:
return hash160(pubkey)
def verifyKey(self, k: bytes) -> bool:
i = b2i(k)
return (i < ep.o and i > 0)

@ -66,8 +66,8 @@ from basicswap.contrib.test_framework.messages import (
CTxIn,
CTxInWitness,
CTxOut,
uint256_from_str)
uint256_from_str,
)
from basicswap.contrib.test_framework.script import (
CScript, CScriptOp,
OP_IF, OP_ELSE, OP_ENDIF,
@ -231,17 +231,6 @@ class BTCInterface(Secp256k1Interface):
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):
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)
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
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 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.nVersion = self.txVersion()
prev_txid = uint256_from_str(bytes.fromhex(prevout['txid'])[::-1])
@ -1520,7 +1509,7 @@ class BTCInterface(Secp256k1Interface):
tx.rehash()
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.nVersion = self.txVersion()
tx.nLockTime = locktime

@ -15,6 +15,9 @@ from basicswap.basicswap_util import (
TxLockTypes
)
from basicswap.chainparams import Coins
from basicswap.contrib.test_framework.messages import (
uint256_from_str,
)
from basicswap.interface.btc import Secp256k1Interface
from basicswap.util import (
ensure,
@ -34,8 +37,22 @@ from basicswap.util.script import (
from basicswap.util.extkey import ExtKeyPair
from basicswap.util.integer import encode_varint
from basicswap.interface.dcr.rpc import make_rpc_func
from .messages import CTransaction, CTxOut, SigHashType, TxSerializeType
from .script import push_script_data, OP_HASH160, OP_EQUAL, OP_DUP, OP_EQUALVERIFY, OP_CHECKSIG
from .messages import (
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 (
PrivateKey,
@ -124,6 +141,20 @@ def DCRSignatureHash(sign_script: bytes, hash_type: SigHashType, tx: CTransactio
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):
@staticmethod
@ -168,6 +199,10 @@ class DCRInterface(Secp256k1Interface):
return secondsLocked | SEQUENCE_LOCKTIME_TYPE_FLAG
raise ValueError('Unknown lock type')
@staticmethod
def watch_blocks_for_scripts() -> bool:
return True
def __init__(self, coin_settings, network, swap_client=None):
super().__init__(network)
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.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:
return ripemd160(blake256(pubkey))
@ -235,9 +274,6 @@ class DCRInterface(Secp256k1Interface):
def getBlockchainInfo(self):
return self.rpc('getblockchaininfo')
def using_segwit(self) -> bool:
return self._use_segwit
def getWalletInfo(self):
rv = {}
rv = self.rpc_wallet('getinfo')
@ -276,6 +312,13 @@ class DCRInterface(Secp256k1Interface):
raise ValueError('Checksum mismatch')
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:
tx = CTransaction()
tx.deserialize(tx_bytes)
@ -288,14 +331,21 @@ class DCRInterface(Secp256k1Interface):
eck = PrivateKey(key_bytes)
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.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()
for data in stack:
push_script_data(script_data, data)
tx.vin[txi].signature_script = script_data
test_ser = tx.serialize()
test_tx = self.loadTx(test_ser)
return tx.serialize()
@ -310,6 +360,20 @@ class DCRInterface(Secp256k1Interface):
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:
# P2SH
script_hash = self.pkh(script)
@ -323,7 +387,6 @@ class DCRInterface(Secp256k1Interface):
def getPubkeyHashDest(self, pkh: bytes) -> bytes:
# P2PKH
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)
@ -424,7 +487,7 @@ class DCRInterface(Secp256k1Interface):
return self.rpc_wallet('sendtoaddress', params)
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)
def encodeProofUtxos(self, proof_utxos):
@ -504,18 +567,133 @@ class DCRInterface(Secp256k1Interface):
txn_funded = self.createRawFundedTransaction(addr_to, amount)
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):
if txid is 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):
txjs = self.rpc('decoderawtransaction', [txn_hex])
n = getVoutByScriptPubKey(txjs, self.getScriptDest(txn_script).hex())
txo = txjs['vout'][n]
return {
'txid': txjs['txid'],
'vout': n,
'scriptPubKey': txjs['vout'][n]['scriptPubKey']['hex'],
'scriptPubKey': txo['scriptPubKey']['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
from enum import IntEnum
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):
@ -86,12 +86,12 @@ class CTransaction:
def deserialize(self, data: bytes) -> None:
version = int.from_bytes(data[:4], 'little')
self.version = self.version & 0xffff
self.version = version & 0xffff
ser_type: int = version >> 16
o = 4
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
for i in range(num_txin):
@ -107,7 +107,7 @@ class CTransaction:
o += 4
self.vin.append(txi)
num_txout, nb = decode_varint(data, o)
num_txout, nb = decode_compactsize(data, o)
o += nb
for i in range(num_txout):
@ -116,7 +116,7 @@ class CTransaction:
o += 8
txo.version = int.from_bytes(data[o:o + 2], 'little')
o += 2
script_bytes, nb = decode_varint(data, o)
script_bytes, nb = decode_compactsize(data, o)
o += nb
txo.script_pubkey = data[o:o + script_bytes]
o += script_bytes
@ -130,7 +130,7 @@ class CTransaction:
if ser_type == TxSerializeType.NoWitness:
return
num_wit_scripts, nb = decode_varint(data, o)
num_wit_scripts, nb = decode_compactsize(data, o)
o += nb
if ser_type == TxSerializeType.OnlyWitness:
@ -147,7 +147,7 @@ class CTransaction:
o += 4
txi.block_index = int.from_bytes(data[o:o + 4], 'little')
o += 4
script_bytes, nb = decode_varint(data, o)
script_bytes, nb = decode_compactsize(data, o)
o += nb
txi.signature_script = data[o:o + script_bytes]
o += script_bytes
@ -158,31 +158,31 @@ class CTransaction:
data += version.to_bytes(4, 'little')
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:
data += txi.prevout.hash.to_bytes(32, 'little')
data += txi.prevout.n.to_bytes(4, 'little')
data += txi.prevout.tree.to_bytes(1)
data += txi.sequence.to_bytes(4, 'little')
data += encode_varint(len(self.vout))
data += encode_compactsize(len(self.vout))
for txo in self.vout:
data += txo.value.to_bytes(8, '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 += self.locktime.to_bytes(4, 'little')
data += self.expiry.to_bytes(4, 'little')
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:
tc_value_in = txi.value_in & 0xffffffffffffffff # Convert negative values
data += tc_value_in.to_bytes(8, 'little')
data += txi.block_height.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
return data

@ -87,7 +87,7 @@ class FIROInterface(BTCInterface):
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
if not self.isAddressMine(dest_address, or_watch_only=True):
@ -337,7 +337,7 @@ class FIROInterface(BTCInterface):
return
current_height -= 1
def getBlockWithTxns(self, block_hash):
def getBlockWithTxns(self, block_hash: str):
# TODO: Bypass decoderawtransaction and getblockheader
block = self.rpc('getblock', [block_hash, False])
block_header = self.rpc('getblockheader', [block_hash])
@ -355,9 +355,11 @@ class FIROInterface(BTCInterface):
block_rv = {
'hash': block_hash,
'previousblockhash': block_header['previousblockhash'],
'tx': tx_rv,
'confirmations': block_header['confirmations'],
'height': block_header['height'],
'time': block_header['time'],
'version': block_header['version'],
'merkleroot': block_header['merkleroot'],
}

@ -415,7 +415,7 @@ class NAVInterface(BTCInterface):
return
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
if not self.isAddressMine(dest_address, or_watch_only=True):
@ -479,9 +479,11 @@ class NAVInterface(BTCInterface):
block_rv = {
'hash': block_hash,
'previousblockhash': block_header['previousblockhash'],
'tx': tx_rv,
'confirmations': block_header['confirmations'],
'height': block_header['height'],
'time': block_header['time'],
'version': block_header['version'],
'merkleroot': block_header['merkleroot'],
}

@ -14,7 +14,7 @@ class NMCInterface(BTCInterface):
def coin_type():
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
ro = self.rpc('scantxoutset', ['start', ['addr({})'.format(dest_address)]]) # TODO: Use combo(address) where possible
self._log.debug('[rm] scantxoutset end')

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

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

@ -49,6 +49,9 @@ message BidMessage {
string proof_signature = 8;
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 */
@ -65,6 +68,7 @@ message BidAcceptMessage {
bytes bid_msg_id = 1;
bytes initiate_txid = 2;
bytes contract_script = 3;
bytes pkhash_seller = 4;
}
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()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
@ -26,29 +26,29 @@ if _descriptor._USE_C_DESCRIPTORS == False:
_globals['_OFFERMESSAGE_LOCKTYPE']._serialized_start=493
_globals['_OFFERMESSAGE_LOCKTYPE']._serialized_end=606
_globals['_BIDMESSAGE']._serialized_start=609
_globals['_BIDMESSAGE']._serialized_end=815
_globals['_BIDMESSAGE_TEST']._serialized_start=817
_globals['_BIDMESSAGE_TEST']._serialized_end=932
_globals['_BIDACCEPTMESSAGE']._serialized_start=934
_globals['_BIDACCEPTMESSAGE']._serialized_end=1020
_globals['_OFFERREVOKEMESSAGE']._serialized_start=1022
_globals['_OFFERREVOKEMESSAGE']._serialized_end=1083
_globals['_BIDREJECTMESSAGE']._serialized_start=1085
_globals['_BIDREJECTMESSAGE']._serialized_end=1144
_globals['_XMRBIDMESSAGE']._serialized_start=1147
_globals['_XMRBIDMESSAGE']._serialized_end=1330
_globals['_XMRSPLITMESSAGE']._serialized_start=1332
_globals['_XMRSPLITMESSAGE']._serialized_end=1416
_globals['_XMRBIDACCEPTMESSAGE']._serialized_start=1419
_globals['_XMRBIDACCEPTMESSAGE']._serialized_end=1675
_globals['_XMRBIDLOCKTXSIGSMESSAGE']._serialized_start=1677
_globals['_XMRBIDLOCKTXSIGSMESSAGE']._serialized_end=1791
_globals['_XMRBIDLOCKSPENDTXMESSAGE']._serialized_start=1793
_globals['_XMRBIDLOCKSPENDTXMESSAGE']._serialized_end=1881
_globals['_XMRBIDLOCKRELEASEMESSAGE']._serialized_start=1883
_globals['_XMRBIDLOCKRELEASEMESSAGE']._serialized_end=1960
_globals['_ADSBIDINTENTMESSAGE']._serialized_start=1963
_globals['_ADSBIDINTENTMESSAGE']._serialized_end=2092
_globals['_ADSBIDINTENTACCEPTMESSAGE']._serialized_start=2094
_globals['_ADSBIDINTENTACCEPTMESSAGE']._serialized_end=2206
_globals['_BIDMESSAGE']._serialized_end=840
_globals['_BIDMESSAGE_TEST']._serialized_start=842
_globals['_BIDMESSAGE_TEST']._serialized_end=957
_globals['_BIDACCEPTMESSAGE']._serialized_start=959
_globals['_BIDACCEPTMESSAGE']._serialized_end=1068
_globals['_OFFERREVOKEMESSAGE']._serialized_start=1070
_globals['_OFFERREVOKEMESSAGE']._serialized_end=1131
_globals['_BIDREJECTMESSAGE']._serialized_start=1133
_globals['_BIDREJECTMESSAGE']._serialized_end=1192
_globals['_XMRBIDMESSAGE']._serialized_start=1195
_globals['_XMRBIDMESSAGE']._serialized_end=1378
_globals['_XMRSPLITMESSAGE']._serialized_start=1380
_globals['_XMRSPLITMESSAGE']._serialized_end=1464
_globals['_XMRBIDACCEPTMESSAGE']._serialized_start=1467
_globals['_XMRBIDACCEPTMESSAGE']._serialized_end=1723
_globals['_XMRBIDLOCKTXSIGSMESSAGE']._serialized_start=1725
_globals['_XMRBIDLOCKTXSIGSMESSAGE']._serialized_end=1839
_globals['_XMRBIDLOCKSPENDTXMESSAGE']._serialized_start=1841
_globals['_XMRBIDLOCKSPENDTXMESSAGE']._serialized_end=1929
_globals['_XMRBIDLOCKRELEASEMESSAGE']._serialized_start=1931
_globals['_XMRBIDLOCKRELEASEMESSAGE']._serialized_end=2008
_globals['_ADSBIDINTENTMESSAGE']._serialized_start=2011
_globals['_ADSBIDINTENTMESSAGE']._serialized_end=2140
_globals['_ADSBIDINTENTACCEPTMESSAGE']._serialized_start=2142
_globals['_ADSBIDINTENTACCEPTMESSAGE']._serialized_end=2254
# @@protoc_insertion_point(module_scope)

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020-2023 tecnovert
# Copyright (c) 2020-2024 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@ -10,12 +10,15 @@ from basicswap.db import (
from basicswap.util import (
SerialiseNum,
)
from basicswap.util.script import (
decodeScriptNum,
)
from basicswap.script import (
OpCodes,
)
from basicswap.basicswap_util import (
SwapTypes,
EventLogTypes,
SwapTypes,
)
from . import ProtocolInterface
@ -23,13 +26,13 @@ INITIATE_TX_TIMEOUT = 40 * 60 # TODO: make variable per coin
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([
OpCodes.OP_IF,
OpCodes.OP_SIZE,
0x01, 0x20, # 32
OpCodes.OP_EQUALVERIFY,
OpCodes.OP_SHA256,
op_hash,
0x20]) \
+ secret_hash \
+ bytearray([
@ -54,6 +57,46 @@ def buildContractScript(lock_val: int, secret_hash: bytes, pkh_redeem: bytes, pk
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):
return script[7:39]

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

@ -5,6 +5,29 @@
# 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):
i: int = 0
num_bytes: int = 0

@ -389,7 +389,7 @@ def extract_states_from_xu_file(file_path, prefix):
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):
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)))
raise e
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
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
# TODO
# - Occasionally DCR simnet chain stalls.
import copy
import logging
import os
import random
import select
import subprocess
import unittest
@ -14,7 +19,14 @@ import unittest
import basicswap.config as cfg
from basicswap.basicswap import (
BidStates,
Coins,
DebugTypes,
SwapTypes,
TxStates,
)
from basicswap.basicswap_util import (
TxLockTypes,
)
from basicswap.util.crypto import (
hash160
@ -27,9 +39,14 @@ from basicswap.interface.dcr.messages import (
TxSerializeType,
)
from tests.basicswap.common import (
compare_bid_states,
compare_bid_states_unordered,
stopDaemons,
waitForRPC,
wait_for_balance,
wait_for_bid,
wait_for_bid_tx_state,
wait_for_offer,
waitForRPC,
)
from tests.basicswap.util import (
read_json_api,
@ -63,6 +80,176 @@ def make_rpc_func(node_id, base_rpc_port):
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):
node_dir = os.path.join(datadir, dir_prefix + str(node_id))
if not os.path.exists(node_dir):
@ -326,7 +513,7 @@ class Test(BaseTest):
swap_clients = self.swap_clients
ci0 = swap_clients[0].ci(self.test_coin)
utxos = ci0.getNewAddress()
utxos = ci0.rpc_wallet('listunspent')
addr_out = ci0.rpc_wallet('getnewaddress')
rtx = ci0.rpc_wallet('createrawtransaction', [[], {addr_out: 2.0}])
@ -530,7 +717,8 @@ class Test(BaseTest):
assert (len(unspents) == 1)
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
rv = ci0.rpc_wallet('lockunspent', [False, [utxo, ]])
@ -538,7 +726,7 @@ class Test(BaseTest):
def wait_for_depth():
for i in range(20):
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:
return txout
test_delay_event.wait(1)
@ -555,11 +743,11 @@ class Test(BaseTest):
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)
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'])
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:
logging.info('txout spent, height before spent {}, height spent {}'.format(chain_height_before_send, ci0.getChainHeight()))
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'))
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__':
unittest.main()

@ -319,7 +319,6 @@ class Test(unittest.TestCase):
ci_btc = BTCInterface(coin_settings, 'regtest')
for i in range(10000):
test_pairs = random.randint(0, 3)
if test_pairs == 0:
ci_from = ci_btc
@ -425,6 +424,9 @@ class Test(unittest.TestCase):
msg_buf_v2.ParseFromString(serialised_msg)
assert (msg_buf_v2.protocol_version == 2)
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
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 logging
import unittest
from basicswap.basicswap import (
BidStates,
Coins,
DebugTypes,
SwapTypes,
BidStates,
TxStates,
DebugTypes,
)
from basicswap.basicswap_util import (
TxLockTypes,
@ -40,24 +39,23 @@ from tests.basicswap.util import (
read_json_api,
)
from tests.basicswap.common import (
wait_for_offer,
wait_for_bid,
BTC_BASE_RPC_PORT,
compare_bid_states,
LTC_BASE_RPC_PORT,
TEST_HTTP_PORT,
wait_for_balance,
wait_for_unspent,
wait_for_bid,
wait_for_bid_tx_state,
wait_for_in_progress,
TEST_HTTP_PORT,
LTC_BASE_RPC_PORT,
BTC_BASE_RPC_PORT,
compare_bid_states,
extract_states_from_xu_file,
wait_for_offer,
wait_for_unspent,
)
from basicswap.contrib.test_framework.messages import (
ToHex,
CTxIn,
COutPoint,
CTransaction,
CTxIn,
CTxInWitness,
ToHex,
)
from basicswap.contrib.test_framework.script import (
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/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
cls.waitForParticlHeight(3)
@ -329,7 +323,8 @@ class Test(BaseTest):
logging.info('---------- Test PART to LTC')
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)
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_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=60)
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[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=80)
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)
# Verify lock tx spends are found in the expected wallets
bid, offer = swap_clients[0].getBidAndOffer(bid_id)
max_fee: int = 1000
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()
path = f'bids/{bid_id_hex}/states'
offerer_states = read_json_api(1800, path)
bidder_states = read_json_api(1801, path)
assert (compare_bid_states(offerer_states, self.states_offerer[0]) is True)
assert (compare_bid_states(bidder_states, self.states_bidder[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_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):
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_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[1], bid_id, BidStates.SWAP_COMPLETED, 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=80)
js_0 = read_json_api(1800)
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_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=60)
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[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=80)
js_0 = read_json_api(1800)
js_1 = read_json_api(1801)
@ -419,8 +427,8 @@ class Test(BaseTest):
read_json_api(1801, 'bids/{}'.format(bid_id.hex()), {'abandon': True})
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[1], bid_id, BidStates.BID_ABANDONED, sent=True, 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=80)
js_0_bid = read_json_api(1800, '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)
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')
def test_06_self_bid(self):
@ -455,8 +463,8 @@ class Test(BaseTest):
wait_for_bid(test_delay_event, swap_clients[0], 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(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, 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=80)
js_0 = read_json_api(1800)
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)
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[1], bid_id, BidStates.SWAP_COMPLETED, sent=True, 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=80)
def test_10_bad_ptx(self):
# 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)
bidder_states = read_json_api(1801, path)
assert (compare_bid_states(offerer_states, self.states_offerer[1]) is True)
assert (compare_bid_states(bidder_states, self.states_bidder[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_sh[1]) is True)
'''
def test_11_refund(self):
@ -654,8 +662,8 @@ class Test(BaseTest):
offerer_states = read_json_api(1800, path)
bidder_states = read_json_api(1801, path)
assert (compare_bid_states(offerer_states, self.states_offerer[2]) is True)
assert (compare_bid_states(bidder_states, self.states_bidder[2], exact_match=False) is True)
assert (compare_bid_states(offerer_states, self.states_offerer_sh[2]) is True)
assert (compare_bid_states(bidder_states, self.states_bidder_sh[2], exact_match=False) is True)
def test_14_sweep_balance(self):
logging.info('---------- Test sweep balance offer')
@ -718,8 +726,8 @@ class Test(BaseTest):
wait_for_bid(test_delay_event, swap_clients[2], 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[1], bid_id, BidStates.SWAP_COMPLETED, sent=True, 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=80)
# Verify expected inputs were used
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_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[1], bid_id, BidStates.SWAP_COMPLETED, 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=80)
def pass_99_delay(self):
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_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 RESET_TEST:
logging.info('Removing ' + TEST_DIR)

Loading…
Cancel
Save