From a3f5bc1a5aebd1473310ef8173d3f9b916d8b789 Mon Sep 17 00:00:00 2001 From: tecnovert Date: Wed, 15 May 2024 11:39:32 +0200 Subject: [PATCH] Decred: Secret hash swap tests. --- basicswap/basicswap.py | 504 +++++++++++++++++++-------- basicswap/basicswap_util.py | 1 + basicswap/db.py | 14 +- basicswap/db_upgrades.py | 12 + basicswap/db_util.py | 2 + basicswap/interface/base.py | 26 +- basicswap/interface/btc.py | 21 +- basicswap/interface/dcr/dcr.py | 206 ++++++++++- basicswap/interface/dcr/messages.py | 24 +- basicswap/interface/firo.py | 6 +- basicswap/interface/nav.py | 4 +- basicswap/interface/nmc.py | 2 +- basicswap/interface/part.py | 25 +- basicswap/interface/pivx.py | 2 + basicswap/messages.proto | 4 + basicswap/messages_pb2.py | 52 +-- basicswap/protocols/atomic_swap_1.py | 51 ++- basicswap/script.py | 2 + basicswap/util/integer.py | 23 ++ tests/basicswap/common.py | 18 +- tests/basicswap/extended/test_dcr.py | 218 +++++++++++- tests/basicswap/test_other.py | 4 +- tests/basicswap/test_run.py | 102 +++--- tests/basicswap/test_xmr.py | 3 + 24 files changed, 1025 insertions(+), 301 deletions(-) diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index 9e4a184..52a5184 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.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 - pkhash_buyer_refund = bid.pkhash_buyer + + if bid.pkhash_buyer_to and len(bid.pkhash_buyer_to) > 0: + pkhash_buyer_refund = bid.pkhash_buyer_to + else: + pkhash_buyer_refund = bid.pkhash_buyer + + if coin_to in (Coins.DCR, ): + op_hash = OpCodes.OP_SHA256_DECRED + else: + op_hash = OpCodes.OP_SHA256 # Participate txn is locked for half the time of the initiate txn 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) + redeem_txn = ci.createRedeemTxn(prevout, addr_redeem_out, amount_out, txn_script) 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) + refund_txn = ci.createRefundTxn(prevout, addr_refund_out, amount_out, locktime, sequence, txn_script) 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'] - index = found['index'] + 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 + if bid.participate_tx.conf is None and bid.participate_tx.state != TxStates.TX_SENT: + txid = found.get('txid', None if participate_txid is None else participate_txid.hex()) + self.log.debug('Found bid %s participate txn %s in chain %s', bid_id.hex(), txid, Coins(coin_to).name) + self.addParticipateTxn(bid_id, bid, coin_to, txid, index, found['height']) + + # Only update tx state if tx hasn't already been seen + if bid.participate_tx.state is None or bid.participate_tx.state < TxStates.TX_SENT: + bid.setPTxState(TxStates.TX_SENT) + bid.participate_tx.conf = found['depth'] - index = found['index'] - if bid.participate_tx is None or bid.participate_tx.txid is None: - self.log.debug('Found bid %s participate txn %s in chain %s', bid_id.hex(), found['txid'], Coins(coin_to).name) - self.addParticipateTxn(bid_id, bid, coin_to, found['txid'], found['index'], found['height']) - bid.setPTxState(TxStates.TX_SENT) - save_bid = True if found['height'] > 0 and bid.participate_tx.block_height is None: 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,39 +4477,56 @@ 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: - 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) - while last_height_checked < chain_blocks: - block_hash = self.callcoinrpc(coin_type, 'getblockhash', [last_height_checked + 1]) - try: - block = ci.getBlockWithTxns(block_hash) - except Exception as e: - if 'Block not available (pruned data)' in str(e): - # TODO: Better solution? - bci = self.callcoinrpc(coin_type, 'getblockchaininfo') - self.log.error('Coin %s last_height_checked %d set to pruneheight %d', self.ci(coin_type).coin_name(), last_height_checked, bci['pruneheight']) - last_height_checked = bci['pruneheight'] - continue - else: - self.logException(f'getblock error {e}') - break + return - for tx in block['tx']: + ci = self.ci(coin_type) + chain_blocks = ci.getChainHeight() + 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 = ci.rpc('getblockhash', [last_height_checked + 1]) + try: + block = ci.getBlockWithTxns(block_hash) + except Exception as e: + if 'Block not available (pruned data)' in str(e): + # TODO: Better solution? + bci = self.callcoinrpc(coin_type, 'getblockchaininfo') + self.log.error('Coin %s last_height_checked %d set to pruneheight %d', self.ci(coin_type).coin_name(), last_height_checked, bci['pruneheight']) + last_height_checked = bci['pruneheight'] + continue + else: + self.logException(f'getblock error {e}') + break + + if block_check_min_time > block['time'] or last_height_checked < 1: + pass + elif not self.haveCheckedPrevBlock(ci, c, block): + last_height_checked -= 1 + self.log.debug('Have not seen previousblockhash {} for block {}'.format(block['previousblockhash'], block['hash'])) + continue + + for tx in block['tx']: + for s in c['watched_scripts']: + for i, txo in enumerate(tx['vout']): + if 'scriptPubKey' in txo and 'hex' in txo['scriptPubKey']: + # TODO: Optimise by loading rawtx in CTransaction + if bytes.fromhex(txo['scriptPubKey']['hex']) == s.script: + self.log.debug('Found script from search for bid %s: %s %d', s.bid_id.hex(), tx['txid'], i) + self.processFoundScript(coin_type, s, bytes.fromhex(tx['txid']), i) + + for o in c['watched_outputs']: for i, inp in enumerate(tx['vin']): - for o in c['watched_outputs']: - 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) + 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 + 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 diff --git a/basicswap/basicswap_util.py b/basicswap/basicswap_util.py index bbe5a03..8f53362 100644 --- a/basicswap/basicswap_util.py +++ b/basicswap/basicswap_util.py @@ -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): diff --git a/basicswap/db.py b/basicswap/db.py index b8dce4f..466283a 100644 --- a/basicswap/db.py +++ b/basicswap/db.py @@ -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) diff --git a/basicswap/db_upgrades.py b/basicswap/db_upgrades.py index ae3f3cd..605ff09 100644 --- a/basicswap/db_upgrades.py +++ b/basicswap/db_upgrades.py @@ -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) diff --git a/basicswap/db_util.py b/basicswap/db_util.py index 478f891..286766c 100644 --- a/basicswap/db_util.py +++ b/basicswap/db_util.py @@ -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) diff --git a/basicswap/interface/base.py b/basicswap/interface/base.py index da4dea1..ff69e1c 100644 --- a/basicswap/interface/base.py +++ b/basicswap/interface/base.py @@ -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) diff --git a/basicswap/interface/btc.py b/basicswap/interface/btc.py index 02dec4f..4f62030 100644 --- a/basicswap/interface/btc.py +++ b/basicswap/interface/btc.py @@ -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 diff --git a/basicswap/interface/dcr/dcr.py b/basicswap/interface/dcr/dcr.py index 8456cd9..c6195bc 100644 --- a/basicswap/interface/dcr/dcr.py +++ b/basicswap/interface/dcr/dcr.py @@ -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): - self._log.debug('TODO: getLockTxHeight') - return None + 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]) diff --git a/basicswap/interface/dcr/messages.py b/basicswap/interface/dcr/messages.py index 723e0c2..8138c41 100644 --- a/basicswap/interface/dcr/messages.py +++ b/basicswap/interface/dcr/messages.py @@ -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 diff --git a/basicswap/interface/firo.py b/basicswap/interface/firo.py index a99b046..0be2c3f 100644 --- a/basicswap/interface/firo.py +++ b/basicswap/interface/firo.py @@ -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'], } diff --git a/basicswap/interface/nav.py b/basicswap/interface/nav.py index 53c7717..250d257 100644 --- a/basicswap/interface/nav.py +++ b/basicswap/interface/nav.py @@ -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'], } diff --git a/basicswap/interface/nmc.py b/basicswap/interface/nmc.py index 32b558d..b71e85b 100644 --- a/basicswap/interface/nmc.py +++ b/basicswap/interface/nmc.py @@ -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') diff --git a/basicswap/interface/part.py b/basicswap/interface/part.py index aa124ee..db9af17 100644 --- a/basicswap/interface/part.py +++ b/basicswap/interface/part.py @@ -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)) diff --git a/basicswap/interface/pivx.py b/basicswap/interface/pivx.py index 5d29853..de420c5 100644 --- a/basicswap/interface/pivx.py +++ b/basicswap/interface/pivx.py @@ -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'], } diff --git a/basicswap/messages.proto b/basicswap/messages.proto index fb5815d..8bd5b27 100644 --- a/basicswap/messages.proto +++ b/basicswap/messages.proto @@ -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 { diff --git a/basicswap/messages_pb2.py b/basicswap/messages_pb2.py index 74f08a2..d56accc 100644 --- a/basicswap/messages_pb2.py +++ b/basicswap/messages_pb2.py @@ -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) diff --git a/basicswap/protocols/atomic_swap_1.py b/basicswap/protocols/atomic_swap_1.py index d0a8765..e65a97e 100644 --- a/basicswap/protocols/atomic_swap_1.py +++ b/basicswap/protocols/atomic_swap_1.py @@ -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] diff --git a/basicswap/script.py b/basicswap/script.py index 3d6bae2..e601236 100644 --- a/basicswap/script.py +++ b/basicswap/script.py @@ -26,3 +26,5 @@ class OpCodes(IntEnum): OP_CHECKSIG = 0xac, OP_CHECKLOCKTIMEVERIFY = 0xb1, OP_CHECKSEQUENCEVERIFY = 0xb2, + + OP_SHA256_DECRED = 0xc0, diff --git a/basicswap/util/integer.py b/basicswap/util/integer.py index 78940b3..75430dc 100644 --- a/basicswap/util/integer.py +++ b/basicswap/util/integer.py @@ -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 diff --git a/tests/basicswap/common.py b/tests/basicswap/common.py index dbf5f5b..604acc0 100644 --- a/tests/basicswap/common.py +++ b/tests/basicswap/common.py @@ -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 diff --git a/tests/basicswap/extended/test_dcr.py b/tests/basicswap/extended/test_dcr.py index 034e850..de861a1 100644 --- a/tests/basicswap/extended/test_dcr.py +++ b/tests/basicswap/extended/test_dcr.py @@ -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() diff --git a/tests/basicswap/test_other.py b/tests/basicswap/test_other.py index ccac3f8..84c9bad 100644 --- a/tests/basicswap/test_other.py +++ b/tests/basicswap/test_other.py @@ -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]) diff --git a/tests/basicswap/test_run.py b/tests/basicswap/test_run.py index fc10488..629aec3 100644 --- a/tests/basicswap/test_run.py +++ b/tests/basicswap/test_run.py @@ -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 ( - Coins, - SwapTypes, BidStates, - TxStates, + Coins, DebugTypes, + SwapTypes, + TxStates, ) 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, - wait_for_balance, - wait_for_unspent, - 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, + LTC_BASE_RPC_PORT, + TEST_HTTP_PORT, + wait_for_balance, + wait_for_bid, + wait_for_bid_tx_state, + wait_for_in_progress, + 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') diff --git a/tests/basicswap/test_xmr.py b/tests/basicswap/test_xmr.py index f10a688..b8d67c6 100644 --- a/tests/basicswap/test_xmr.py +++ b/tests/basicswap/test_xmr.py @@ -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)