From c467125b73ac15b5be1485586e87dc01cf244b08 Mon Sep 17 00:00:00 2001 From: a1denvalu3 <43107113+a1denvalu3@users.noreply.github.com> Date: Thu, 23 Jul 2026 22:13:45 +0200 Subject: [PATCH] Use embit for Python 3.14-compatible NIP-42 signing (#11) * Avoid native dependency for NIP-42 signing * Use embit for NIP-42 Schnorr signatures --- nostr_relay_discovery.py | 25 +++++++++++++++++-------- requirements.txt | 2 +- tests/test_nostr_relay_discovery.py | 6 +++--- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/nostr_relay_discovery.py b/nostr_relay_discovery.py index f8348db..14fe58b 100644 --- a/nostr_relay_discovery.py +++ b/nostr_relay_discovery.py @@ -22,7 +22,7 @@ from typing import Set, List, Dict, Optional, Tuple from urllib.parse import urlparse import websockets import websockets.exceptions -from coincurve import PrivateKey +from embit import ec # Configure logging @@ -35,7 +35,6 @@ logger = logging.getLogger(__name__) # Save progress every N relays processed SAVE_POINT = 10 - @dataclass class RelayDiscoveryStats: """Statistics for the relay discovery process""" @@ -127,10 +126,14 @@ class NostrRelayDiscovery: return base64.b64encode(random_bytes).decode() @staticmethod - def _load_private_key(private_key_hex: Optional[str]) -> PrivateKey: + def _load_private_key(private_key_hex: Optional[str]) -> ec.PrivateKey: """Load a configured Nostr secret or generate an ephemeral discovery key.""" if private_key_hex is None: - return PrivateKey() + while True: + try: + return ec.PrivateKey(secrets.token_bytes(32)) + except ValueError: + continue try: secret = bytes.fromhex(private_key_hex) @@ -141,13 +144,15 @@ class NostrRelayDiscovery: raise ValueError("--private-key must be 64 hexadecimal characters") try: - return PrivateKey(secret) + return ec.PrivateKey(secret) except ValueError as exc: - raise ValueError("--private-key is not a valid secp256k1 secret") from exc + raise ValueError( + "--private-key is not a valid secp256k1 secret" + ) from exc def build_auth_event(self, relay_url: str, challenge: str) -> Dict: """Build and sign a NIP-42 kind 22242 authentication event.""" - pubkey = self.private_key.public_key.format(compressed=True)[1:].hex() + pubkey = self.private_key.xonly().hex() event = { "pubkey": pubkey, "created_at": int(time.time()), @@ -172,7 +177,11 @@ class NostrRelayDiscovery: ).encode() event_id = hashlib.sha256(serialized).digest() event["id"] = event_id.hex() - event["sig"] = self.private_key.sign_schnorr(event_id).hex() + signature = self.private_key.schnorr_sign(event_id) + public_key = ec.PublicKey.from_xonly(bytes.fromhex(pubkey)) + if not public_key.schnorr_verify(signature, event_id): + raise RuntimeError("generated an invalid BIP-340 signature") + event["sig"] = signature.serialize().hex() return event async def receive_with_auth( diff --git a/requirements.txt b/requirements.txt index 175a289..5eed68a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ websockets>=12.0 -coincurve>=20.0.0 +embit>=0.8.0 matplotlib>=3.5.0 pandas>=1.4.0 numpy>=1.20.0 diff --git a/tests/test_nostr_relay_discovery.py b/tests/test_nostr_relay_discovery.py index 5525d50..d573c98 100644 --- a/tests/test_nostr_relay_discovery.py +++ b/tests/test_nostr_relay_discovery.py @@ -4,7 +4,7 @@ import json import unittest from unittest.mock import patch -from coincurve import PublicKeyXOnly +from embit import ec from nostr_relay_discovery import NostrRelayDiscovery @@ -50,8 +50,8 @@ class Nip42Tests(unittest.TestCase): self.assertEqual(event["id"], event_id.hex()) self.assertTrue( - PublicKeyXOnly(bytes.fromhex(event["pubkey"])).verify( - bytes.fromhex(event["sig"]), event_id + ec.PublicKey.from_xonly(bytes.fromhex(event["pubkey"])).schnorr_verify( + ec.SchnorrSig.parse(bytes.fromhex(event["sig"])), event_id ) )