backports

This commit is contained in:
tecnovert
2025-09-29 09:55:50 +02:00
parent 911ca189bc
commit 9d6e566c3b
13 changed files with 296 additions and 23 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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")

View File

@@ -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("<i", f.read(4))[0]
self.nVersion = ver32bit & 0xffff
self.nType = (ver32bit >> 16) & 0xffff

View File

@@ -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("<i", f.read(4))[0]
self.nTime = struct.unpack("<i", f.read(4))[0]
self.vin = deser_vector(f, CTxIn)
flags = 0
if len(self.vin) == 0:
if len(self.vin) == 0 and allow_witness:
flags = struct.unpack("<B", f.read(1))[0]
# Not sure why flags can't be zero, but this
# matches the implementation in bitcoind

View File

@@ -505,7 +505,7 @@ class CTransaction:
self.sha256 = tx.sha256
self.hash = tx.hash
def deserialize(self, f):
def deserialize(self, f, allow_witness: bool = True):
self.nVersion = struct.unpack("<h", f.read(2))[0]
self.nType = struct.unpack("<h", f.read(2))[0]
self.vin = deser_vector(f, CTxIn)

View File

@@ -13,6 +13,8 @@ import logging
import random
import traceback
from typing import List
from basicswap.basicswap_util import getVoutByScriptPubKey, TxLockTypes
from basicswap.chainparams import Coins
from basicswap.contrib.test_framework.script import (
@@ -1609,11 +1611,11 @@ class DCRInterface(Secp256k1Interface):
script_pk = self.getScriptDest(script)
return findOutput(tx, script_pk)
def getScriptLockTxDummyWitness(self, script: bytes):
def getScriptLockTxDummyWitness(self, script: bytes) -> 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)

View File

@@ -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

View File

@@ -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"]

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -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 = {