Source code for cashscript_py.helpers.cashaddress

"""CashAddr encoding/decoding and locking bytecode helpers."""

from enum import StrEnum

from bitcash.cashaddress import Address

from cashscript_py.helpers import bech32 as bech32
from cashscript_py.helpers.bch_opcodes import OpcodesBCH
from cashscript_py.network.network_provider import Network

PUBKEY_TYPE = 0
SCRIPT_TYPE = 1
TOKEN_PUBKEY_TYPE = 2
TOKEN_SCRIPT_TYPE = 3


[docs] def mask_cash_address_prefix(prefix: str) -> list[int]: """Convert the address prefix (HRP) to uint5 values for checksum calculation.""" result = [ord(char) & 31 for char in prefix] return result
[docs] def cash_address_polynomial_modulo(v: list[int]) -> int: """Compute the polymod checksum value for the given uint5 sequence.""" bech32_generator_most_significant_byte = [0x98, 0x79, 0xF3, 0xAE, 0x1E] bech32_generator_remaining_bytes = [0xF2BC8E61, 0xB76D99E2, 0x3E5FB3C4, 0x2EABE2A8, 0x4F43E470] most_significant_byte = 0 lower_bytes = 1 c = 0 for j in range(len(v)): c = most_significant_byte >> 3 most_significant_byte &= 0x07 most_significant_byte <<= 5 most_significant_byte |= lower_bytes >> 27 lower_bytes &= 0x07FFFFFF lower_bytes <<= 5 lower_bytes ^= v[j] for i in range(len(bech32_generator_most_significant_byte)): if c & (1 << i): most_significant_byte ^= bech32_generator_most_significant_byte[i] lower_bytes ^= bech32_generator_remaining_bytes[i] lower_bytes ^= 1 return most_significant_byte * (1 << 30) * 4 + lower_bytes
[docs] def decode_cash_address_format(address: str) -> tuple[bytearray, str, int]: """Decode a CashAddress string into its components. Args: address: Full CashAddress with prefix (e.g., "bitcoincash:..."). Returns: A tuple of (payload bytes, prefix string, version byte). """ parts = address.lower().split(":") if len(parts) != 2 or not parts[0] or not parts[1]: return bytearray(), "", 0 # Invalid format prefix, payload = parts if not bech32.is_bech32_character_set(payload): return bytearray(), "", 0 # Invalid characters decoded_payload = bech32.decode_bech32(payload) polynomial = mask_cash_address_prefix(prefix) + [0] + decoded_payload if cash_address_polynomial_modulo(polynomial) != 0: return bytearray(), "", 0 # Invalid checksum checksum40_bit_placeholder_length = 8 try: payload_contents = bech32.regroup_bits( allow_padding=False, bin=decoded_payload[:-checksum40_bit_placeholder_length], result_word_length=8, # Base256 word length source_word_length=5, # Base32 word length ) except bech32.BitRegroupingError: return bytearray(), "", 0 # Improper padding version_byte = payload_contents[0] contents = payload_contents[1:] result = bytearray(contents) return result, prefix, version_byte
[docs] def split_version_byte(version_byte: int) -> tuple[int, int]: """Split a cashaddr version byte into (type_index, payload_bit_length). type_index: 0 => pubkey (non-token) 1 => script (non-token) 2 => pubkey (token-aware) 3 => script (token-aware) payload_bit_length is one of {160, 192, 224, 256, 320, 384, 448, 512}. """ type_index = version_byte >> 3 size_index = version_byte & 0x07 size_to_bits = { 0: 160, 1: 192, 2: 224, 3: 256, 4: 320, 5: 384, 6: 448, 7: 512, } return type_index, size_to_bits.get(size_index, -1)
[docs] def encode_locking_bytecode_p2pkh(public_key_hash: bytes) -> bytes: """Build a P2PKH locking bytecode from a 20-byte public key hash.""" return bytes( [ 118, # OP_DUP 169, # OP_HASH160 20, # Length of the public key hash (OP_PUSHBYTES_20) *public_key_hash, # Unpack the public key hash bytes 136, # OP_EQUALVERIFY 172, # OP_CHECKSIG ] )
[docs] def encode_locking_bytecode_p2sh20(p2sh20_hash: bytes) -> bytes: """Build a P2SH20 locking bytecode from a 20-byte script hash.""" return bytes( [ 169, # OP_HASH160 20, # Length of the P2SH hash (OP_PUSHBYTES_20) *p2sh20_hash, # Unpack the P2SH hash bytes 135, # OP_EQUAL ] )
[docs] def encode_locking_bytecode_p2sh32(p2sh32_hash: bytes) -> bytes: """Build a P2SH32 locking bytecode from a 32-byte script hash.""" return bytes( [ 170, # OP_HASH256 32, # Length of the P2SH32 hash (OP_PUSHBYTES_32) *p2sh32_hash, # Unpack the P2SH32 hash bytes 135, # OP_EQUAL ] )
[docs] def encode_locking_bytecode_p2pk(public_key: bytes) -> bytes: """Build a P2PK locking bytecode from a compressed or uncompressed public key.""" if len(public_key) == 33: # Compressed public key return bytes([33, *public_key, 172]) # OP_PUSHBYTES_33 # Unpack the public key bytes # OP_CHECKSIG else: # Uncompressed public key return bytes([65, *public_key, 172]) # OP_PUSHBYTES_65 # Unpack the public key bytes # OP_CHECKSIG
[docs] def cash_address_checksum_to_uint5_array(checksum: int) -> list[int]: """Expand a 40-bit checksum integer into 8 uint5 values (for Bech32 payload).""" base256WordLength = 8 result = [] for _ in range(base256WordLength): result.append(checksum & 31) # & 31 to get the last 5 bits checksum //= 32 # Use floor division for correct behavior result.reverse() return result
[docs] class LockingBytecodeType(StrEnum): """Supported locking bytecode templates (address types).""" P2PKH = "p2pkh" P2SH20 = "p2sh20" P2SH32 = "p2sh32" P2PK = "p2pk"
[docs] def address_contents_to_locking_bytecode(payload: bytes, type: LockingBytecodeType) -> bytes: """Encode a payload into a standard locking bytecode for the given type.""" if type == LockingBytecodeType.P2PKH: return encode_locking_bytecode_p2pkh(payload) elif type == LockingBytecodeType.P2SH20: return encode_locking_bytecode_p2sh20(payload) elif type == LockingBytecodeType.P2SH32: return encode_locking_bytecode_p2sh32(payload) else: raise ValueError(f"Unrecognized locking bytecode type: {type}")
[docs] def address_string_to_locking_bytecode(address: str) -> bytes: """Decode a CashAddr string to its raw locking bytecode bytes.""" return bytes(Address.from_string(address).scriptcode)
[docs] def encode_cash_address_format(prefix: str, version: int, payload: bytes) -> str: """Encode a CashAddr from prefix, version byte, and payload bytes.""" # Define constants inside the function base32_word_length = 5 payload_separator = 0 checksum_40_bit_placeholder = [0] * 8 # 40-bit checksum placeholder of 8 bytes of zeros. payload_contents: list[int] = bech32.regroup_bits( bin=[version] + list(payload), source_word_length=8, result_word_length=base32_word_length, allow_padding=True ) checksum_contents = ( mask_cash_address_prefix(prefix) + [payload_separator] + payload_contents + checksum_40_bit_placeholder ) checksum = cash_address_polynomial_modulo(checksum_contents) int_array_payload: list[int] = payload_contents + cash_address_checksum_to_uint5_array(checksum) # Construct the cash address by encoding the payload with Bech32 and prefixing it with the given prefix return f"{prefix}:{bech32.encode_bech32(int_array_payload)}"
[docs] def encode_cash_address_version_byte(type: int, bit_length: int) -> int: """Encode a CashAddr version byte from type index and payload bit-length.""" cashAddressTypeBitShift = 3 cashAddressSizeToBit = { 160: 0, 192: 1, 224: 2, 256: 3, 320: 4, 384: 5, 448: 6, 512: 7, } if bit_length not in cashAddressSizeToBit: raise ValueError("Unsupported bit length") return (type << cashAddressTypeBitShift) | cashAddressSizeToBit[bit_length]
[docs] def is_valid_bit_length(bit_length: int) -> bool: """Return True if the payload bit length is valid for CashAddr.""" cash_address_size_to_bit = { 160: 0, 192: 1, 224: 2, 256: 3, 320: 4, 384: 5, 448: 6, 512: 7, } return bit_length in cash_address_size_to_bit
[docs] def encode_cash_address(prefix: str, type: int, payload: bytes) -> str: """Encode a full CashAddr string from prefix, type index, and payload bytes.""" bit_length = len(payload) * 8 if not is_valid_bit_length(bit_length): raise ValueError("Unsupported hash length for cash address encoding") version_byte = encode_cash_address_version_byte(type, bit_length) return encode_cash_address_format(prefix, version_byte, payload)
[docs] def locking_bytecode_to_cash_address(bytecode: bytes, prefix: str, token_aware: bool) -> str: """Convert a locking bytecode to a CashAddr string (token-aware optional).""" payload, address_type = locking_bytecode_to_address_contents(bytecode) return payload_to_cash_address(prefix, payload, address_type, token_aware)
[docs] def payload_to_cash_address(prefix: str, payload: bytes, address_type: LockingBytecodeType, token_aware: bool) -> str: """Encode payload and address type as CashAddr (token-aware or regular).""" address_version = -1 if not token_aware: if address_type == LockingBytecodeType.P2PKH: address_version = 0 if address_type in {LockingBytecodeType.P2SH20, LockingBytecodeType.P2SH32}: address_version = 1 else: if address_type == LockingBytecodeType.P2PKH: address_version = 2 if address_type in {LockingBytecodeType.P2SH20, LockingBytecodeType.P2SH32}: address_version = 3 if address_version == -1: raise ValueError(f"Unsupported address type {address_type}") return encode_cash_address(prefix, address_version, payload)
[docs] def locking_bytecode_to_address_contents(bytecode: bytes) -> tuple[bytes, LockingBytecodeType]: """Parse a known locking bytecode template into (payload, address type).""" # Define lengths and types p2pkh_length = 25 p2sh20_length = 23 p2sh32_length = 35 p2pk_uncompressed_length = 67 p2pk_compressed_length = 35 # P2PKH format if ( len(bytecode) == p2pkh_length and bytecode[0] == OpcodesBCH["OP_DUP"] and bytecode[1] == OpcodesBCH["OP_HASH160"] and bytecode[2] == OpcodesBCH["OP_PUSHBYTES_20"] and bytecode[23] == OpcodesBCH["OP_EQUALVERIFY"] and bytecode[24] == OpcodesBCH["OP_CHECKSIG"] ): start, end = 3, 23 return bytecode[start:end], LockingBytecodeType.P2PKH # P2SH format (p2sh20) if ( len(bytecode) == p2sh20_length and bytecode[0] == OpcodesBCH["OP_HASH160"] and bytecode[1] == OpcodesBCH["OP_PUSHBYTES_20"] and bytecode[22] == OpcodesBCH["OP_EQUAL"] ): start, end = 2, 22 return bytecode[start:end], LockingBytecodeType.P2SH20 # P2SH format (p2sh32) if ( len(bytecode) == p2sh32_length and bytecode[0] == OpcodesBCH["OP_HASH256"] and bytecode[1] == OpcodesBCH["OP_PUSHBYTES_32"] and bytecode[34] == OpcodesBCH["OP_EQUAL"] ): start, end = 2, 34 return bytecode[start:end], LockingBytecodeType.P2SH32 # P2PK uncompressed format if ( len(bytecode) == p2pk_uncompressed_length and bytecode[0] == OpcodesBCH["OP_PUSHBYTES_65"] and bytecode[66] == OpcodesBCH["OP_CHECKSIG"] ): start, end = 1, 66 return bytecode[start:end], LockingBytecodeType.P2PK # P2PK compressed format if ( len(bytecode) == p2pk_compressed_length and bytecode[0] == OpcodesBCH["OP_PUSHBYTES_33"] and bytecode[34] == OpcodesBCH["OP_CHECKSIG"] ): start, end = 1, 34 return bytecode[start:end], LockingBytecodeType.P2PK # If none of the above... raise ValueError("bad number of bytes in the locking byte code.")
[docs] def get_network_prefix(network: Network) -> str: """Return the CashAddr HRP prefix for a given network.""" if network == Network.MAINNET: return "bitcoincash" if network in {Network.TESTNET4, Network.TESTNET3, Network.CHIPNET, Network.MOCKNET}: return "bchtest" if network == Network.REGTEST: return "bchreg" raise ValueError(f"Unknown network: {network}")