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