From 9d6e566c3b1a5a95dec943787f892770731bff2e Mon Sep 17 00:00:00 2001 From: tecnovert Date: Mon, 29 Sep 2025 09:55:50 +0200 Subject: [PATCH] backports --- basicswap/contrib/test_framework/messages.py | 4 +- .../websocket_server/websocket_server.py | 40 ++++- basicswap/interface/btc.py | 51 ++++++- .../contrib/firo_test_framework/mininode.py | 2 +- .../contrib/nav_test_framework/mininode.py | 4 +- .../contrib/pivx_test_framework/messages.py | 2 +- basicswap/interface/dcr/dcr.py | 8 +- basicswap/interface/dcr/messages.py | 2 +- basicswap/interface/part.py | 6 + basicswap/util/smsg.py | 1 + tests/basicswap/test_btc_xmr.py | 15 +- tests/basicswap/test_other.py | 44 +++++- tests/basicswap/test_run.py | 140 ++++++++++++++++++ 13 files changed, 296 insertions(+), 23 deletions(-) diff --git a/basicswap/contrib/test_framework/messages.py b/basicswap/contrib/test_framework/messages.py index a8b7a95..47380f6 100755 --- a/basicswap/contrib/test_framework/messages.py +++ b/basicswap/contrib/test_framework/messages.py @@ -640,7 +640,7 @@ class CTransaction: self.hash = tx.hash self.wit = copy.deepcopy(tx.wit) - def deserialize(self, f): + def deserialize(self, f, allow_witness: bool = True): self.nVersion = int.from_bytes(f.read(1), "little") if self.nVersion == PARTICL_TX_VERSION: self.nVersion |= int.from_bytes(f.read(1), "little") << 8 @@ -668,7 +668,7 @@ class CTransaction: # self.nVersion = int.from_bytes(f.read(4), "little") self.vin = deser_vector(f, CTxIn) flags = 0 - if len(self.vin) == 0: + if len(self.vin) == 0 and allow_witness: flags = int.from_bytes(f.read(1), "little") # Not sure why flags can't be zero, but this # matches the implementation in bitcoind diff --git a/basicswap/contrib/websocket_server/websocket_server.py b/basicswap/contrib/websocket_server/websocket_server.py index 75894b1..2af80a0 100644 --- a/basicswap/contrib/websocket_server/websocket_server.py +++ b/basicswap/contrib/websocket_server/websocket_server.py @@ -166,6 +166,9 @@ class WebsocketServer(ThreadingMixIn, TCPServer, API): def _message_received_(self, handler, msg): self.message_received(self.handler_to_client(handler), self, msg) + def _binary_message_received_(self, handler, msg): + self.binary_message_received(self.handler_to_client(handler), self, msg) + def _ping_received_(self, handler, msg): handler.send_pong(msg) @@ -309,6 +312,7 @@ class WebSocketHandler(StreamRequestHandler): opcode = b1 & OPCODE masked = b2 & MASKED payload_length = b2 & PAYLOAD_LEN + is_binary: bool = False if opcode == OPCODE_CLOSE_CONN: logger.info("Client asked to close connection.") @@ -322,8 +326,8 @@ class WebSocketHandler(StreamRequestHandler): logger.warning("Continuation frames are not supported.") return elif opcode == OPCODE_BINARY: - logger.warning("Binary frames are not supported.") - return + is_binary = True + opcode_handler = self.server._binary_message_received_ elif opcode == OPCODE_TEXT: opcode_handler = self.server._message_received_ elif opcode == OPCODE_PING: @@ -345,7 +349,8 @@ class WebSocketHandler(StreamRequestHandler): for message_byte in self.read_bytes(payload_length): message_byte ^= masks[len(message_bytes) % 4] message_bytes.append(message_byte) - opcode_handler(self, message_bytes.decode('utf8')) + + opcode_handler(self, message_bytes if is_binary else message_bytes.decode('utf8')) def send_message(self, message): self.send_text(message) @@ -375,6 +380,35 @@ class WebSocketHandler(StreamRequestHandler): with self._send_lock: self.request.send(header + payload) + def send_bytes(self, message, opcode=OPCODE_BINARY): + header = bytearray() + payload = message + payload_length = len(payload) + + # Normal payload + if payload_length <= 125: + header.append(FIN | opcode) + header.append(payload_length) + + # Extended payload + elif payload_length >= 126 and payload_length <= 65535: + header.append(FIN | opcode) + header.append(PAYLOAD_LEN_EXT16) + header.extend(struct.pack(">H", payload_length)) + + # Huge extended payload + elif payload_length < 18446744073709551616: + header.append(FIN | opcode) + header.append(PAYLOAD_LEN_EXT64) + header.extend(struct.pack(">Q", payload_length)) + + else: + raise Exception("Message is too big. Consider breaking it into chunks.") + return + + with self._send_lock: + self.request.send(header + payload) + def send_text(self, message, opcode=OPCODE_TEXT): """ Important: Fragmented(=continuation) messages are not supported since diff --git a/basicswap/interface/btc.py b/basicswap/interface/btc.py index c9f4b81..8cc25cc 100644 --- a/basicswap/interface/btc.py +++ b/basicswap/interface/btc.py @@ -17,7 +17,7 @@ import sqlite3 import traceback from io import BytesIO -from typing import Dict, Optional +from typing import Dict, List, Optional from basicswap.basicswap_util import ( getVoutByAddress, @@ -77,17 +77,19 @@ from basicswap.contrib.test_framework.messages import ( from basicswap.contrib.test_framework.script import ( CScript, CScriptOp, - OP_IF, - OP_ELSE, - OP_ENDIF, OP_0, OP_2, - OP_CHECKSIG, OP_CHECKMULTISIG, OP_CHECKSEQUENCEVERIFY, + OP_CHECKSIG, OP_DROP, - OP_HASH160, + OP_DUP, + OP_ELSE, + OP_ENDIF, OP_EQUAL, + OP_EQUALVERIFY, + OP_HASH160, + OP_IF, OP_RETURN, SIGHASH_ALL, SegwitV0SignatureHash, @@ -761,10 +763,11 @@ class BTCInterface(Secp256k1Interface): # p2wpkh return CScript([OP_0, pkh]) - def loadTx(self, tx_bytes: bytes) -> CTransaction: + def loadTx(self, tx_bytes: bytes, allow_witness: bool = True) -> CTransaction: # Load tx from bytes to internal representation + # Transactions with no inputs require allow_witness set to false to decode correctly tx = CTransaction() - tx.deserialize(BytesIO(tx_bytes)) + tx.deserialize(BytesIO(tx_bytes), allow_witness) return tx def createSCLockTx( @@ -801,6 +804,35 @@ class BTCInterface(Secp256k1Interface): CScriptOp(OP_ENDIF)]) # fmt: on + def isScriptP2PKH(self, script: bytes) -> bool: + if len(script) != 25: + return False + if script[0] != OP_DUP: + return False + if script[1] != OP_HASH160: + return False + if script[2] != 20: + return False + if script[23] != OP_EQUALVERIFY: + return False + if script[24] != OP_CHECKSIG: + return False + return True + + def isScriptP2WPKH(self, script: bytes) -> bool: + if len(script) != 22: + return False + if script[0] != OP_0: + return False + if script[1] != 20: + return False + return True + + def getScriptDummyWitness(self, script: bytes) -> List[bytes]: + if self.isScriptP2WPKH(script): + return [bytes(72), bytes(33)] + raise ValueError("Unknown script type") + def createSCLockRefundTx( self, tx_lock_bytes, @@ -1959,6 +1991,9 @@ class BTCInterface(Secp256k1Interface): def getBlockWithTxns(self, block_hash: str): return self.rpc("getblock", [block_hash, 2]) + def listUtxos(self): + return self.rpc_wallet("listunspent") + def getUnspentsByAddr(self): unspent_addr = dict() unspent = self.rpc_wallet("listunspent") diff --git a/basicswap/interface/contrib/firo_test_framework/mininode.py b/basicswap/interface/contrib/firo_test_framework/mininode.py index 6313ffe..43f428b 100644 --- a/basicswap/interface/contrib/firo_test_framework/mininode.py +++ b/basicswap/interface/contrib/firo_test_framework/mininode.py @@ -521,7 +521,7 @@ class CTransaction(object): self.hash = tx.hash self.wit = copy.deepcopy(tx.wit) - def deserialize(self, f): + def deserialize(self, f, allow_witness: bool = True): ver32bit = struct.unpack("> 16) & 0xffff diff --git a/basicswap/interface/contrib/nav_test_framework/mininode.py b/basicswap/interface/contrib/nav_test_framework/mininode.py index bec35ba..f63ac1c 100755 --- a/basicswap/interface/contrib/nav_test_framework/mininode.py +++ b/basicswap/interface/contrib/nav_test_framework/mininode.py @@ -455,12 +455,12 @@ class CTransaction(object): self.wit = copy.deepcopy(tx.wit) self.strDZeel = copy.deepcopy(tx.strDZeel) - def deserialize(self, f): + def deserialize(self, f, allow_witness: bool = True): self.nVersion = struct.unpack(" List[bytes]: return [bytes(72), bytes(72), bytes(len(script))] - def getScriptLockRefundSpendTxDummyWitness(self, script: bytes): - return [bytes(72), bytes(72), bytes((1,)), bytes(len(script))] + def getScriptLockRefundSpendTxDummyWitness(self, script: bytes) -> List[bytes]: + return [bytes(72), bytes(72), bytes(len(script))] def extractLeaderSig(self, tx_bytes: bytes) -> bytes: tx = self.loadTx(tx_bytes) diff --git a/basicswap/interface/dcr/messages.py b/basicswap/interface/dcr/messages.py index c0a2ff6..0860d65 100644 --- a/basicswap/interface/dcr/messages.py +++ b/basicswap/interface/dcr/messages.py @@ -89,7 +89,7 @@ class CTransaction: self.locktime = tx.locktime self.expiry = tx.expiry - def deserialize(self, data: bytes) -> None: + def deserialize(self, data: bytes, allow_witness: bool = True) -> None: version = int.from_bytes(data[:4], "little") self.version = version & 0xFFFF diff --git a/basicswap/interface/part.py b/basicswap/interface/part.py index 48890b6..646f895 100644 --- a/basicswap/interface/part.py +++ b/basicswap/interface/part.py @@ -8,6 +8,7 @@ import hashlib from enum import IntEnum +from typing import List from basicswap.contrib.test_framework.messages import ( CTxOutPart, @@ -134,6 +135,11 @@ class PARTInterface(BTCInterface): def getScriptForPubkeyHash(self, pkh: bytes) -> CScript: return CScript([OP_DUP, OP_HASH160, pkh, OP_EQUALVERIFY, OP_CHECKSIG]) + def getScriptDummyWitness(self, script: bytes) -> List[bytes]: + if self.isScriptP2WPKH(script) or self.isScriptP2PKH(script): + return [bytes(72), bytes(33)] + raise ValueError("Unknown script type") + def formatStealthAddress(self, scan_pubkey, spend_pubkey) -> str: prefix_byte = chainparams[self.coin_type()][self._network]["stealth_key_prefix"] diff --git a/basicswap/util/smsg.py b/basicswap/util/smsg.py index ccfc1d0..3bdcd7e 100644 --- a/basicswap/util/smsg.py +++ b/basicswap/util/smsg.py @@ -249,6 +249,7 @@ def smsgDecrypt( pubkey_signer = PublicKey.from_signature_and_message( signature, payload_hash, hasher=None ).format() + pkh_from_recovered: bytes = hash160(pubkey_signer) assert pkh_from == pkh_from_recovered diff --git a/tests/basicswap/test_btc_xmr.py b/tests/basicswap/test_btc_xmr.py index 8522330..ae75a70 100644 --- a/tests/basicswap/test_btc_xmr.py +++ b/tests/basicswap/test_btc_xmr.py @@ -1011,7 +1011,7 @@ class BasicSwapTest(TestFunctions): def test_002_native_segwit(self): # p2wpkh logging.info( - "---------- Test {} p2sh native segwit".format(self.test_coin_from.name) + "---------- Test {} native segwit".format(self.test_coin_from.name) ) ci = self.swap_clients[0].ci(self.test_coin_from) @@ -1073,6 +1073,14 @@ class BasicSwapTest(TestFunctions): tx_signed, ], ) + prev_txo = tx["vout"][tx_signed_decoded["vin"][0]["vout"]] + prevscript: bytes = bytes.fromhex(prev_txo["scriptPubKey"]["hex"]) + assert ci.isScriptP2WPKH(prevscript) is True + txin_witness = tx_signed_decoded["vin"][0]["txinwitness"] + assert len(txin_witness) == 2 + txin_witness_0 = bytes.fromhex(txin_witness[0]) + assert len(txin_witness_0) > 68 and len(txin_witness_0) <= 72 + assert len(bytes.fromhex(txin_witness[1])) == 33 assert tx_funded_decoded["txid"] == tx_signed_decoded["txid"] def test_003_cltv(self): @@ -2295,6 +2303,11 @@ class BasicSwapTest(TestFunctions): def test_09_expire_accepted_rev(self): self.do_test_09_expire_accepted(Coins.XMR, self.test_coin_from) + def test_10_presigned_txns(self): + raise RuntimeError( + "TODO" + ) # Build without xmr first for quicker test iterations + class TestBTC(BasicSwapTest): __test__ = True diff --git a/tests/basicswap/test_other.py b/tests/basicswap/test_other.py index 5229b0f..c8f57cd 100644 --- a/tests/basicswap/test_other.py +++ b/tests/basicswap/test_other.py @@ -51,7 +51,18 @@ from basicswap.util import ( from basicswap.messages_npb import ( BidMessage, ) -from basicswap.contrib.test_framework.script import hash160 as hash160_btc +from basicswap.contrib.test_framework.script import ( + hash160 as hash160_btc, + SegwitV0SignatureHash, + SIGHASH_ALL, +) +from basicswap.contrib.test_framework.messages import ( + COutPoint, + CTransaction, + CTxIn, + CTxOut, + uint256_from_str, +) logger = logging.getLogger() @@ -663,6 +674,37 @@ class Test(unittest.TestCase): finally: db_test.closeDB(cursor) + def test_tx_hashes(self): + tx = CTransaction() + tx.nVersion = 2 + tx.nLockTime = 0 + tx.vout.append(CTxOut(1, bytes.fromhex("a15143aa086e05e3b5a73046"))) + tx.vout.append(CTxOut(2, bytes.fromhex("eed7d63fd86225f7159ed7e5"))) + tx.vin.append( + CTxIn( + COutPoint( + uint256_from_str( + bytes.fromhex( + "0101010101010101010101010101010101010010101010101010101010101010" + ) + ), + 1, + ), + bytes.fromhex("c2dca8ecbcf058b79f188692"), + ) + ) + assert ( + tx.rehash() + == "28d0e9afad2740504eb9d0428352bc77a7b94eaafa364ef4cc07aeeff0c631a2" + ) + sighash = SegwitV0SignatureHash( + bytes.fromhex("a15143aa086e05e3b5a73046"), tx, 0, SIGHASH_ALL, 3 + ) + assert ( + sighash.hex() + == "252cd6e85b99e0fd554c44d5fe638923f7ef563048362406a665cf3400feb1bd" + ) + if __name__ == "__main__": unittest.main() diff --git a/tests/basicswap/test_run.py b/tests/basicswap/test_run.py index 74690d7..1428cd0 100644 --- a/tests/basicswap/test_run.py +++ b/tests/basicswap/test_run.py @@ -315,6 +315,146 @@ class Test(BaseTest): break assert len(tx_wallet["blockhash"]) == 64 + def test_004_native_segwit(self): + test_coin_from = Coins.PART + # p2wpkh + logging.info("---------- Test {} segwit".format(test_coin_from.name)) + ci = self.swap_clients[0].ci(test_coin_from) + + addr_native = ci.rpc_wallet("getnewaddress", ["p2pkh segwit test"]) + addr_info = ci.rpc_wallet( + "getaddressinfo", + [ + addr_native, + ], + ) + assert addr_info["iswitness"] is False # address is p2pkh, not p2wpkh + addr_segwit = ci.rpc_wallet( + "getnewaddress", ["p2wpkh segwit test", True, False, False, "bech32"] + ) + addr_info = ci.rpc_wallet( + "getaddressinfo", + [ + addr_segwit, + ], + ) + assert addr_info["iswitness"] is True + + txid = ci.rpc_wallet( + "sendtypeto", + [ + "part", + "part", + [ + {"address": addr_native, "amount": 1.0}, + {"address": addr_segwit, "amount": 1.0}, + ], + ], + ) + assert len(txid) == 64 + tx_wallet = ci.rpc_wallet( + "gettransaction", + [ + txid, + ], + )["hex"] + tx = ci.rpc( + "decoderawtransaction", + [ + tx_wallet, + ], + ) + + # Wait for stake + for i in range(20): + test_delay_event.wait(1) + ro = ci.rpc("scantxoutset", ["start", ["addr({})".format(addr_native)]]) + if len(ro["unspents"]) > 0: + break + + ro = ci.rpc("scantxoutset", ["start", ["addr({})".format(addr_native)]]) + assert len(ro["unspents"]) == 1 + assert ro["unspents"][0]["txid"] == txid + ro = ci.rpc("scantxoutset", ["start", ["addr({})".format(addr_segwit)]]) + assert len(ro["unspents"]) == 1 + assert ro["unspents"][0]["txid"] == txid + + prevout_p2pkh_n: int = -1 + prevout_p2wpkh_n: int = -1 + for txo in tx["vout"]: + if addr_native == txo["scriptPubKey"]["address"]: + prevout_p2pkh_n = txo["n"] + if addr_segwit == txo["scriptPubKey"]["address"]: + prevout_p2wpkh_n = txo["n"] + assert prevout_p2pkh_n > -1 + assert prevout_p2wpkh_n > -1 + + tx_funded = ci.rpc( + "createrawtransaction", + [[{"txid": txid, "vout": prevout_p2pkh_n}], {addr_segwit: 0.99}], + ) + tx_signed = ci.rpc_wallet( + "signrawtransactionwithwallet", + [ + tx_funded, + ], + )["hex"] + tx_funded_decoded = ci.rpc( + "decoderawtransaction", + [ + tx_funded, + ], + ) + tx_signed_decoded = ci.rpc( + "decoderawtransaction", + [ + tx_signed, + ], + ) + prev_txo = tx["vout"][tx_signed_decoded["vin"][0]["vout"]] + prevscript: bytes = bytes.fromhex(prev_txo["scriptPubKey"]["hex"]) + assert ci.isScriptP2PKH(prevscript) is True + assert ci.isScriptP2WPKH(prevscript) is False + txin_witness = tx_signed_decoded["vin"][0]["txinwitness"] + assert len(txin_witness) == 2 + txin_witness_0 = bytes.fromhex(txin_witness[0]) + assert len(txin_witness_0) > 68 and len(txin_witness_0) <= 72 + assert len(bytes.fromhex(txin_witness[1])) == 33 + assert tx_funded_decoded["txid"] == tx_signed_decoded["txid"] + + tx_funded = ci.rpc( + "createrawtransaction", + [[{"txid": txid, "vout": prevout_p2wpkh_n}], {addr_segwit: 0.99}], + ) + tx_signed = ci.rpc_wallet( + "signrawtransactionwithwallet", + [ + tx_funded, + ], + )["hex"] + tx_funded_decoded = ci.rpc( + "decoderawtransaction", + [ + tx_funded, + ], + ) + tx_signed_decoded = ci.rpc( + "decoderawtransaction", + [ + tx_signed, + ], + ) + prev_txo = tx["vout"][tx_signed_decoded["vin"][0]["vout"]] + prevscript: bytes = bytes.fromhex(prev_txo["scriptPubKey"]["hex"]) + assert ci.isScriptP2PKH(prevscript) is False + assert ci.isScriptP2WPKH(prevscript) is True + txin_witness = tx_signed_decoded["vin"][0]["txinwitness"] + assert len(txin_witness) == 2 + txin_witness_0 = bytes.fromhex(txin_witness[0]) + assert len(txin_witness_0) > 68 and len(txin_witness_0) <= 72 + assert len(bytes.fromhex(txin_witness[1])) == 33 + assert tx_funded_decoded["txid"] == tx_signed_decoded["txid"] + def test_01_verifyrawtransaction(self): txn = "0200000001eb6e5c4ebba4efa32f40c7314cad456a64008e91ee30b2dd0235ab9bb67fbdbb01000000ee47304402200956933242dde94f6cf8f195a470f8d02aef21ec5c9b66c5d3871594bdb74c9d02201d7e1b440de8f4da672d689f9e37e98815fb63dbc1706353290887eb6e8f7235012103dc1b24feb32841bc2f4375da91fa97834e5983668c2a39a6b7eadb60e7033f9d205a803b28fe2f86c17db91fa99d7ed2598f79b5677ffe869de2e478c0d1c02cc7514c606382012088a8201fe90717abb84b481c2a59112414ae56ec8acc72273642ca26cc7a5812fdc8f68876a914225fbfa4cb725b75e511810ac4d6f74069bdded26703520140b27576a914207eb66b2fd6ed9924d6217efc7fa7b38dfabe666888acffffffff01e0167118020000001976a9140044e188928710cecba8311f1cf412135b98145c88ac00000000" prevout = {