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