"""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 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_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}")