Source code for cashscript_py.helpers.schnorr

"""BCH-compatible Schnorr (R,s) signatures.

Implements the BCH 2019-05-15 Schnorr variant (Ref: https://reference.cash/protocol/forks/2019-05-15-schnorr):
- Sign/verify over secp256k1.
- e = SHA256(r || compressed_pubkey || m) mod n
- R selection with Jacobi(R.y) == 1 (flip k -> n - k if needed)
- Signature format: 64 bytes r||s for OP_CHECKSIG/VERIFY (append hashtype externally).

Optionally supports deterministic RFC6979-style nonces with a 16-byte "additional data" tag.
"""

import hashlib
import hmac
import secrets

from coincurve import PrivateKey as ECPrivateKey
from coincurve import PublicKey as ECPublicKey

# secp256k1 domain parameters
_SECP256K1_P = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
_SECP256K1_N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141


def _bytes32(i: int) -> bytes:
    return i.to_bytes(32, "big")


def _int_from_be(b: bytes) -> int:
    return int.from_bytes(b, "big")


def _is_jacobi_one(y: int) -> bool:
    # Legendre/Jacobi symbol: y^((p-1)/2) mod p == 1 => quadratic residue
    return pow(y % _SECP256K1_P, (_SECP256K1_P - 1) // 2, _SECP256K1_P) == 1


def _pubkey_compressed(pub: ECPublicKey) -> bytes:
    key: bytes = pub.format(compressed=True)
    return key


def _pubkey_uncompressed(pub: ECPublicKey) -> bytes:
    # 0x04 || X(32) || Y(32)
    key: bytes = pub.format(compressed=False)
    return key


def _schnorr_hash_e(r_bytes: bytes, pubkey_compressed: bytes, m32: bytes) -> int:
    h = hashlib.sha256(r_bytes + pubkey_compressed + m32).digest()
    return _int_from_be(h) % _SECP256K1_N


def _rfc6979_nonce(priv32: bytes, h1: bytes, add: bytes = b"Schnorr+SHA256  ") -> int:
    """RFC6979 HMAC-DRBG with SHA256 for secp256k1, using 'additional data'.

    Returns:
        A scalar k in [1, n-1].
    """
    if len(priv32) != 32 or len(h1) != 32:
        raise ValueError("RFC6979 requires 32-byte priv and 32-byte message hash.")
    K = b"\x00" * 32
    V = b"\x01" * 32
    key_material = priv32 + h1 + (add or b"")
    K = hmac.new(K, V + b"\x00" + key_material, hashlib.sha256).digest()
    V = hmac.new(K, V, hashlib.sha256).digest()
    K = hmac.new(K, V + b"\x01" + key_material, hashlib.sha256).digest()
    V = hmac.new(K, V, hashlib.sha256).digest()
    while True:
        T = b""
        while len(T) < 32:
            V = hmac.new(K, V, hashlib.sha256).digest()
            T += V
        k = _int_from_be(T[:32])
        if 1 <= k < _SECP256K1_N:
            return k
        K = hmac.new(K, V + b"\x00", hashlib.sha256).digest()
        V = hmac.new(K, V, hashlib.sha256).digest()


[docs] def schnorr_sign(priv: bytes, m32: bytes, deterministic: bool = False) -> bytes: """Sign a 32-byte message digest using BCH Schnorr (R,s). Returns: 64-byte signature: r(32) || s(32). """ if len(m32) != 32: raise ValueError("BCH Schnorr requires a 32-byte message digest.") x = _int_from_be(priv) if not (1 <= x < _SECP256K1_N): raise ValueError("Invalid private key scalar.") # Nonce k (deterministic RFC6979 with BCH tag, or secure random) k = _rfc6979_nonce(priv, m32) if deterministic else (secrets.randbelow(_SECP256K1_N - 1) + 1) # Compute R = k*G, ensure Jacobi(R.y) == 1 (flip k -> n - k if needed) R_pub = ECPrivateKey(_bytes32(k)).public_key R_uncomp = _pubkey_uncompressed(R_pub) rx = _int_from_be(R_uncomp[1:33]) ry = _int_from_be(R_uncomp[33:65]) if not _is_jacobi_one(ry): k = (_SECP256K1_N - k) % _SECP256K1_N R_pub = ECPrivateKey(_bytes32(k)).public_key R_uncomp = _pubkey_uncompressed(R_pub) rx = _int_from_be(R_uncomp[1:33]) r_bytes = _bytes32(rx) P_comp = _pubkey_compressed(ECPrivateKey(priv).public_key) e = _schnorr_hash_e(r_bytes, P_comp, m32) s = (k + (e * x) % _SECP256K1_N) % _SECP256K1_N return r_bytes + _bytes32(s)
[docs] def schnorr_verify(sig64: bytes, m32: bytes, pubkey_comp: bytes) -> bool: """Verify a 64-byte BCH Schnorr signature r||s for a 32-byte message digest. Returns: True if valid, else False. """ if len(sig64) != 64 or len(m32) != 32: return False try: r = _int_from_be(sig64[:32]) s = _int_from_be(sig64[32:]) if not (0 < s < _SECP256K1_N): return False P = ECPublicKey(pubkey_comp) sG = ECPrivateKey(_bytes32(s)).public_key e = _schnorr_hash_e(sig64[:32], pubkey_comp, m32) neP = P.multiply(_bytes32((_SECP256K1_N - e) % _SECP256K1_N)) # -eP R_prime = ECPublicKey.combine_keys([sG, neP]) # sG + (-eP) R_uncomp = _pubkey_uncompressed(R_prime) rxp = _int_from_be(R_uncomp[1:33]) ryp = _int_from_be(R_uncomp[33:65]) return rxp == r and _is_jacobi_one(ryp) except Exception: return False