Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 28 additions & 46 deletions bitcoinutils/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
116 changes: 107 additions & 9 deletions bitcoinutils/script.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,15 @@
import copy
import hashlib
import struct
from typing import Any, Union
from typing import Any, Optional, Union

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
Expand Down Expand Up @@ -231,6 +237,9 @@
b"\xb2": "OP_CHECKSEQUENCEVERIFY",
}

# Define Tapscript-only opcodes
TAPSCRIPT_ONLY_OPCODES = ["OP_CHECKSIGADD"]


class Script:
"""Represents any script in Bitcoin
Expand All @@ -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
-------
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -358,16 +399,18 @@ def to_hex(self) -> str:
"""Converts the script to hexadecimal"""
return b_to_h(self.to_bytes())

@staticmethod
def from_raw(scriptrawhex: Union[str, bytes], has_segwit: bool = False):
@classmethod
def from_raw(cls, scriptrawhex: Union[str, bytes], has_segwit: bool = False, script_type: str = SCRIPT_TYPE_LEGACY):
"""
Imports a Script commands list from raw hexadecimal data
Attributes
----------
txinputraw : string (hex)
The hexadecimal raw string representing the Script commands
scriptrawhex : Union[str, bytes]
The hexadecimal raw string or bytes 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)
"""
if isinstance(scriptrawhex, str):
scriptraw = h_to_b(scriptrawhex)
Expand Down Expand Up @@ -419,7 +462,7 @@ def from_raw(scriptrawhex: Union[str, bytes], 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"""
Expand Down Expand Up @@ -453,3 +496,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
54 changes: 50 additions & 4 deletions tests/test_checksigadd.py
Original file line number Diff line number Diff line change
@@ -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:
# <pubkey1> OP_CHECKSIG <pubkey2> OP_CHECKSIGADD <pubkey3> 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()