Source code for cashscript_py.signature_template

"""SignatureTemplate utilities for Schnorr/ECDSA signatures.

Supports BCH SIGHASH flags (including SIGHASH_UTXOS), deterministic Schnorr,
and a P2PKH unlocker helper.
"""

from bitcash.format import wif_to_bytes
from coincurve import PrivateKey

from cashscript_py.helpers.address import public_key_to_p2pkh_locking_bytecode
from cashscript_py.helpers.crypto import hash256
from cashscript_py.helpers.data_encoding import hex_to_bin, is_hex
from cashscript_py.helpers.schnorr import schnorr_sign, schnorr_verify
from cashscript_py.helpers.script import serialize_script
from cashscript_py.helpers.transaction import create_preimage
from cashscript_py.interfaces import HashType, Output, SignatureAlgorithm, Transaction, Unlocker


[docs] class SignatureTemplate: """Encapsulates private-key signing for Bitcoin Cash transactions/messages. Provides methods to sign 32-byte hashes with Schnorr or ECDSA, generate signatures including the hashtype byte for CHECKSIG, and assemble a P2PKH unlocker for standard inputs. """ def __init__( self, signer: bytes | str, hashtype: HashType = HashType.SIGHASH_ALL | HashType.SIGHASH_UTXOS, signature_algorithm: SignatureAlgorithm = SignatureAlgorithm.SCHNORR, deterministic_schnorr: bool = False, ): """Create a SignatureTemplate from raw bytes, hex string or WIF. Args: signer: Private key as 32-byte secret (bytes), hex string (with or without 0x prefix), or WIF string. hashtype: SIGHASH flags to use (SIGHASH_FORKID is optionally added later). signature_algorithm: Schnorr (default) or ECDSA. deterministic_schnorr: If True, use deterministic nonces for Schnorr. Raises: ValueError: If the provided WIF cannot be decoded. """ self.hashtype = hashtype self.signature_algorithm = signature_algorithm self.deterministic = deterministic_schnorr secret: bytes if isinstance(signer, bytes): secret = signer else: signer_string = signer[2:] if signer.startswith("0x") else signer if is_hex(signer_string): secret = hex_to_bin(signer_string) else: secret, _, _ = wif_to_bytes(signer_string) self.private_key = PrivateKey(secret)
[docs] def sign_message_hash(self, payload: bytes) -> bytes: """Sign a 32-byte message hash (no hashtype byte). Args: payload: A 32-byte message digest. Returns: Raw signature bytes: - 64-byte Schnorr signature, or - DER-encoded ECDSA signature (variable length). """ signature: bytes if self.signature_algorithm == SignatureAlgorithm.SCHNORR: signature = schnorr_sign(self.private_key.secret, payload, self.deterministic) else: signature = self.private_key.sign(payload, hasher=None) return signature
[docs] def generate_signature(self, payload: bytes, bch_fork_id: bool = True) -> bytes: """Sign a 32-byte hash and append the hashtype byte for CHECKSIG. Args: payload: 32-byte sighash. bch_fork_id: If True, include SIGHASH_FORKID in the hashtype. Returns: Signature bytes suitable for CHECKSIG: raw sig + 1-byte hashtype. """ raw_sig = self.sign_message_hash(payload) hash_type = self.get_hash_type(bch_fork_id).value.to_bytes(1, "little") return raw_sig + hash_type
[docs] def verify_signature(self, signature: bytes, payload: bytes) -> bool: """Verify a raw signature against a 32-byte message hash. Args: signature: Raw signature (64-byte Schnorr or DER ECDSA). payload: 32-byte message digest. Returns: True if the signature is valid for this template's public key. """ is_valid: bool if self.signature_algorithm == SignatureAlgorithm.SCHNORR: sig64 = signature[:64] pubkey = self.private_key.public_key.format(compressed=True) is_valid = schnorr_verify(sig64, payload, pubkey) else: public_key = self.private_key.public_key is_valid = public_key.verify(signature, payload, hasher=None) return is_valid
[docs] def get_hash_type(self, bch_fork_id: bool = True) -> HashType: """Return the configured SIGHASH flags (optionally OR-ed with ForkID).""" return self.hashtype | HashType.SIGHASH_FORKID if bch_fork_id else self.hashtype
[docs] def get_signature_algorithm(self) -> SignatureAlgorithm: """Return the signature algorithm (Schnorr or ECDSA).""" return self.signature_algorithm
[docs] def get_public_key(self) -> bytes: """Return the compressed public key (33 bytes).""" pubkey: bytes = self.private_key.public_key.format(compressed=True) return pubkey
[docs] def unlock_p2pkh(self) -> Unlocker: """Create a standard P2PKH Unlocker using this template. Returns: An Unlocker that can sign and unlock P2PKH inputs for this key. """ public_key = self.get_public_key() prev_out_script = public_key_to_p2pkh_locking_bytecode(public_key) hash_type = self.get_hash_type().value def generate_locking_bytecode() -> bytes: return prev_out_script def generate_unlocking_bytecode( transaction: Transaction, source_outputs: list[Output], input_index: int ) -> bytes: preimage = create_preimage(transaction, source_outputs, input_index, prev_out_script, hash_type) sighash = hash256(preimage) signature = self.generate_signature(sighash) unlocking_bytecode = serialize_script([signature, public_key]) return unlocking_bytecode return Unlocker(generate_locking_bytecode, generate_unlocking_bytecode)