From 29d6a3c01110a68dd623ec6b52e9585fed547a13 Mon Sep 17 00:00:00 2001 From: JagadishSunilPednekar Date: Sun, 30 Mar 2025 15:47:43 +0530 Subject: [PATCH] Tapscript Differentiation --- bitcoinutils/constants.py | 74 ++++++++++--------------- bitcoinutils/script.py | 112 +++++++++++++++++++++++++++++++++++--- tests/test_checksigadd.py | 54 ++++++++++++++++-- 3 files changed, 183 insertions(+), 57 deletions(-) diff --git a/bitcoinutils/constants.py b/bitcoinutils/constants.py index 6c19b331..0d30a71a 100644 --- a/bitcoinutils/constants.py +++ b/bitcoinutils/constants.py @@ -8,92 +8,74 @@ # No part of python-bitcoin-utils, including this file, may be copied, modified, # propagated, or distributed except according to the terms contained in the # LICENSE file. - NETWORK_DEFAULT_PORTS = { - "mainnet": 8332, - "signet": 38332, - "testnet": 18332, - "regtest": 18443, +"mainnet": 8332, +"signet": 38332, +"testnet": 18332, +"regtest": 18443, } - NETWORK_WIF_PREFIXES = { - "mainnet": b"\x80", - "signet": b"\xef", - "testnet": b"\xef", - "regtest": b"\xef", +"mainnet": b"\x80", +"signet": b"\xef", +"testnet": b"\xef", +"regtest": b"\xef", } - NETWORK_P2PKH_PREFIXES = { - "mainnet": b"\x00", - "signet": b"\x6f", - "testnet": b"\x6f", - "regtest": b"\x6f", +"mainnet": b"\x00", +"signet": b"\x6f", +"testnet": b"\x6f", +"regtest": b"\x6f", } - NETWORK_P2SH_PREFIXES = { - "mainnet": b"\x05", - "signet": b"\xc4", - "testnet": b"\xc4", - "regtest": b"\xc4", +"mainnet": b"\x05", +"signet": b"\xc4", +"testnet": b"\xc4", +"regtest": b"\xc4", } - NETWORK_SEGWIT_PREFIXES = { - "mainnet": "bc", - "signet": "tb", - "testnet": "tb", - "regtest": "bcrt", +"mainnet": "bc", +"signet": "tb", +"testnet": "tb", +"regtest": "bcrt", } - - # Constants for address types P2PKH_ADDRESS = "p2pkh" P2SH_ADDRESS = "p2sh" P2WPKH_ADDRESS_V0 = "p2wpkhv0" P2WSH_ADDRESS_V0 = "p2wshv0" P2TR_ADDRESS_V1 = "p2trv1" - - # Constants related to transaction signature types TAPROOT_SIGHASH_ALL = 0x00 SIGHASH_ALL = 0x01 SIGHASH_NONE = 0x02 SIGHASH_SINGLE = 0x03 SIGHASH_ANYONECANPAY = 0x80 - - # Constants for time lock and RB TYPE_ABSOLUTE_TIMELOCK = 0x101 TYPE_RELATIVE_TIMELOCK = 0x201 TYPE_REPLACE_BY_FEE = 0x301 - DEFAULT_TX_LOCKTIME = b"\x00\x00\x00\x00" - EMPTY_TX_SEQUENCE = b"\x00\x00\x00\x00" DEFAULT_TX_SEQUENCE = b"\xff\xff\xff\xff" ABSOLUTE_TIMELOCK_SEQUENCE = b"\xfe\xff\xff\xff" - REPLACE_BY_FEE_SEQUENCE = b"\x01\x00\x00\x00" - - # Constants related to transaction versions and scripts LEAF_VERSION_TAPSCRIPT = 0xC0 - - +# Script type constants +SCRIPT_TYPE_LEGACY = "legacy" +SCRIPT_TYPE_SEGWIT_V0 = "segwit_v0" +SCRIPT_TYPE_TAPSCRIPT = "tapscript" # TX version 2 was introduced in BIP-68 with relative locktime -- tx v1 # does not support relative locktime DEFAULT_TX_VERSION = b"\x02\x00\x00\x00" - - # Monetary constants SATOSHIS_PER_BITCOIN = 100000000 NEGATIVE_SATOSHI = -1 - # Block HEADER_SIZE = 80 - BLOCK_MAGIC_NUMBER = { - "f9beb4d9" : "mainnet", - "0b110907" : "testnet", - "fabfb5da" : "regtest", - "0a03cf40" : "signet" +"f9beb4d9" : "mainnet", +"0b110907" : "testnet", +"fabfb5da" : "regtest", +"0a03cf40" : "signet" } \ No newline at end of file diff --git a/bitcoinutils/script.py b/bitcoinutils/script.py index 90672a10..cec28417 100644 --- a/bitcoinutils/script.py +++ b/bitcoinutils/script.py @@ -12,9 +12,15 @@ import copy import hashlib import struct -from typing import Any +from typing import Any, Optional from bitcoinutils.ripemd160 import ripemd160 +from bitcoinutils.constants import ( + SCRIPT_TYPE_LEGACY, + SCRIPT_TYPE_SEGWIT_V0, + SCRIPT_TYPE_TAPSCRIPT, + LEAF_VERSION_TAPSCRIPT +) from bitcoinutils.utils import b_to_h, h_to_b, vi_to_int # import bitcoinutils.keys @@ -231,6 +237,9 @@ b"\xb2": "OP_CHECKSEQUENCEVERIFY", } +# Define Tapscript-only opcodes +TAPSCRIPT_ONLY_OPCODES = ["OP_CHECKSIGADD"] + class Script: """Represents any script in Bitcoin @@ -242,6 +251,8 @@ class Script: ---------- script : list the list with all the script OP_CODES and data + script_type : str + the type of script (legacy, segwit_v0, or tapscript) Methods ------- @@ -265,22 +276,43 @@ class Script: to_p2wsh_script_pub_key() converts script to p2wsh scriptPubKey (locking script) + validate() + validates the script against its script_type + Raises ------ ValueError If string data is too large or integer is negative """ - def __init__(self, script: list[Any]): + def __init__(self, script: list[Any], script_type: str = SCRIPT_TYPE_LEGACY): """See Script description""" self.script: list[Any] = script + self.script_type: str = script_type + self.validate() + + def validate(self): + """Validates the script against its script_type + + Ensures that opcodes specific to certain script types are only used + in the correct context. + + Raises + ------ + ValueError + If an opcode is used in an incorrect script type + """ + if self.script_type != SCRIPT_TYPE_TAPSCRIPT: + for op in self.script: + if op in TAPSCRIPT_ONLY_OPCODES: + raise ValueError(f"{op} can only be used in Tapscript (BIP342)") @classmethod def copy(cls, script: "Script") -> "Script": """Deep copy of Script""" scripts = copy.deepcopy(script.script) - return cls(scripts) + return cls(scripts, script.script_type) def _op_push_data(self, data: str) -> bytes: """Converts data to appropriate OP_PUSHDATA OP code including length @@ -294,7 +326,16 @@ def _op_push_data(self, data: str) -> bytes: possible PUSHDATA operator must be used! """ - data_bytes = h_to_b(data) # Assuming string is hexadecimal + # Check if data is already in bytes format + if isinstance(data, bytes): + data_bytes = data + else: + # Try to convert from hex, but if it fails, treat as regular string + try: + data_bytes = h_to_b(data) # Assuming string is hexadecimal + except ValueError: + # If not valid hex, treat as a regular string and encode to bytes + data_bytes = data.encode('utf-8') if len(data_bytes) < 0x4C: return bytes([len(data_bytes)]) + data_bytes @@ -358,8 +399,8 @@ def to_hex(self) -> str: """Converts the script to hexadecimal""" return b_to_h(self.to_bytes()) - @staticmethod - def from_raw(scriptrawhex: str, has_segwit: bool = False): + @classmethod + def from_raw(cls, scriptrawhex: str, has_segwit: bool = False, script_type: str = SCRIPT_TYPE_LEGACY): """ Imports a Script commands list from raw hexadecimal data Attributes @@ -368,6 +409,8 @@ def from_raw(scriptrawhex: str, has_segwit: bool = False): The hexadecimal raw string representing the Script commands has_segwit : boolean Is the Tx Input segwit or not + script_type : str + The type of script (legacy, segwit_v0, or tapscript) """ scriptraw = h_to_b(scriptrawhex) commands = [] @@ -413,7 +456,7 @@ def from_raw(scriptrawhex: str, has_segwit: bool = False): ) index = index + data_size + size - return Script(script=commands) + return cls(script=commands, script_type=script_type) def get_script(self) -> list[Any]: """Returns script as array of strings""" @@ -447,3 +490,58 @@ def __eq__(self, _other: object) -> bool: if not isinstance(_other, Script): return False return self.script == _other.script + + +class TapscriptFactory: + """Helper class to create valid Tapscripts + + This class provides methods to create properly tagged Tapscript instances, + ensuring they're valid for use in Taproot outputs. + + Methods + ------- + create_script(script_cmds) + Creates a Tapscript instance with the given commands + + is_valid_tapscript(script) + Validates if a script is a valid Tapscript + """ + + @staticmethod + def create_script(script_cmds: list[Any]) -> Script: + """Creates a Tapscript with the proper script type + + Parameters + ---------- + script_cmds : list + List of script commands to include in the Tapscript + + Returns + ------- + Script + A Script instance with SCRIPT_TYPE_TAPSCRIPT type + """ + return Script(script_cmds, script_type=SCRIPT_TYPE_TAPSCRIPT) + + @staticmethod + def is_valid_tapscript(script: Script) -> bool: + """Validates if a script is a valid Tapscript + + Parameters + ---------- + script : Script + The script to validate + + Returns + ------- + bool + True if the script is a valid Tapscript, False otherwise + """ + # If not a Tapscript type, it's not valid + if script.script_type != SCRIPT_TYPE_TAPSCRIPT: + return False + + # Check for any additional Tapscript validation rules here + # Currently this just verifies the script_type, but can be extended + + return True \ No newline at end of file diff --git a/tests/test_checksigadd.py b/tests/test_checksigadd.py index 1437abfa..320ae213 100644 --- a/tests/test_checksigadd.py +++ b/tests/test_checksigadd.py @@ -1,12 +1,58 @@ import unittest -from bitcoinutils.script import Script +from bitcoinutils.script import Script, TapscriptFactory +from bitcoinutils.constants import SCRIPT_TYPE_LEGACY, SCRIPT_TYPE_TAPSCRIPT + class TestCheckSigAdd(unittest.TestCase): def test_checksigadd_opcode(self): - # Create a script with the new opcode - script = Script(["OP_CHECKSIGADD"]) + # Create a tapscript with the OP_CHECKSIGADD opcode + script = TapscriptFactory.create_script(["OP_CHECKSIGADD"]) + # Check if it serializes correctly to hex self.assertEqual(script.to_hex(), "ba") + + # Ensure the script type is set to SCRIPT_TYPE_TAPSCRIPT + self.assertEqual(script.script_type, SCRIPT_TYPE_TAPSCRIPT) + + def test_checksigadd_in_legacy_script(self): + # Try to create a legacy script with OP_CHECKSIGADD + # This should raise a ValueError + with self.assertRaises(ValueError): + Script(["OP_CHECKSIGADD"], script_type=SCRIPT_TYPE_LEGACY) + + def test_complex_tapscript_with_checksigadd(self): + # Create a more complex tapscript that uses OP_CHECKSIGADD + # This represents a 2-of-3 multisig using OP_CHECKSIGADD + public_key1 = "03a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7" + public_key2 = "02774e7e7682296b496278b23dc3e844c8c5c8ff0cb9306fd09a8fea389ce5ba68" + public_key3 = "03d01115d548e7561b15c38f004d734633687cf4419620095bc5b0f47070afe85a" + + script = TapscriptFactory.create_script([ + public_key1, "OP_CHECKSIG", + public_key2, "OP_CHECKSIGADD", + public_key3, "OP_CHECKSIGADD", + "OP_2", "OP_EQUAL" + ]) + + # Check that the script serializes correctly + self.assertTrue(TapscriptFactory.is_valid_tapscript(script)) + + # The script should be of the form: + # OP_CHECKSIG OP_CHECKSIGADD OP_CHECKSIGADD OP_2 OP_EQUAL + # This implements "2 of 3" multisig using the new OP_CHECKSIGADD opcode instead of OP_CHECKMULTISIG + + def test_tapscript_factory(self): + # Test that TapscriptFactory correctly creates tapscripts + script = TapscriptFactory.create_script(["OP_DUP", "OP_HASH160", "OP_EQUALVERIFY", "OP_CHECKSIG"]) + self.assertEqual(script.script_type, SCRIPT_TYPE_TAPSCRIPT) + + # Test that TapscriptFactory validation works + self.assertTrue(TapscriptFactory.is_valid_tapscript(script)) + + # Test with a non-tapscript + legacy_script = Script(["OP_DUP", "OP_HASH160", "OP_EQUALVERIFY", "OP_CHECKSIG"]) + self.assertFalse(TapscriptFactory.is_valid_tapscript(legacy_script)) + if __name__ == "__main__": - unittest.main() + unittest.main() \ No newline at end of file