From bae881587cc601b8a2d49bdfc87b1b64f41445d2 Mon Sep 17 00:00:00 2001 From: Patrick Mulligan Date: Fri, 10 Apr 2026 05:49:25 -0400 Subject: [PATCH] fix: adapt nostr transport to forgejo fork's secp256k1 library The aiolabs/lnbits fork uses python-secp256k1 instead of coincurve for elliptic curve operations. Update crypto.py, relay_pool.py, and __init__.py to use the secp256k1 API (tweak_mul, schnorr_sign). Also comment out account.activated check since this field doesn't exist in the current fork (present in upstream dev). Co-Authored-By: Claude Opus 4.6 (1M context) --- .../core/services/nostr_transport/__init__.py | 6 +++--- lnbits/core/services/nostr_transport/auth.py | 5 +++-- .../core/services/nostr_transport/crypto.py | 20 +++++++++---------- .../services/nostr_transport/relay_pool.py | 6 +++--- 4 files changed, 19 insertions(+), 18 deletions(-) diff --git a/lnbits/core/services/nostr_transport/__init__.py b/lnbits/core/services/nostr_transport/__init__.py index 6d071b2e..97122460 100644 --- a/lnbits/core/services/nostr_transport/__init__.py +++ b/lnbits/core/services/nostr_transport/__init__.py @@ -51,10 +51,10 @@ async def start_nostr_transport(): else: private_key_hex = normalize_private_key(private_key_hex) # Derive public key from private key - import coincurve + import secp256k1 - privkey = coincurve.PrivateKey(bytes.fromhex(private_key_hex)) - public_key_hex = privkey.public_key_xonly.format().hex() + privkey = secp256k1.PrivateKey(bytes.fromhex(private_key_hex), True) + public_key_hex = privkey.pubkey.serialize()[1:].hex() settings.nostr_transport_public_key = public_key_hex relay_urls = settings.nostr_transport_relays diff --git a/lnbits/core/services/nostr_transport/auth.py b/lnbits/core/services/nostr_transport/auth.py index 14cea49c..c1839ee1 100644 --- a/lnbits/core/services/nostr_transport/auth.py +++ b/lnbits/core/services/nostr_transport/auth.py @@ -59,8 +59,9 @@ async def resolve_nostr_auth( extra=UserExtra(provider="nostr"), ) - if not account.activated: - raise PermissionError("Account is not activated.") + # TODO: upstream LNbits (dev) has account.activated field + # if not account.activated: + # raise PermissionError("Account is not activated.") # Account-level operation (no wallet specified) if wallet_id is None: diff --git a/lnbits/core/services/nostr_transport/crypto.py b/lnbits/core/services/nostr_transport/crypto.py index 57c7a32e..e94009be 100644 --- a/lnbits/core/services/nostr_transport/crypto.py +++ b/lnbits/core/services/nostr_transport/crypto.py @@ -1,7 +1,7 @@ """ NIP-44 v2 encryption/decryption for nostr transport. -Implements the NIP-44 v2 spec using coincurve (secp256k1 ECDH) and +Implements the NIP-44 v2 spec using secp256k1 (ECDH) and pycryptodome (ChaCha20, HMAC-SHA256, HKDF), all already in LNbits' deps. Also re-exports sign_event() and verify_event() from lnbits.utils.nostr. @@ -14,7 +14,7 @@ import os import struct from math import floor, log2 -import coincurve +import secp256k1 from Cryptodome.Cipher import ChaCha20 from lnbits.utils.nostr import sign_event, verify_event # noqa: F401 @@ -30,18 +30,18 @@ def get_conversation_key( Compute NIP-44 v2 conversation key via ECDH + HKDF-extract. The public key must be 32-byte x-only (as in nostr). We prepend 0x02 - to make it a valid compressed SEC1 point for coincurve. + to make it a valid compressed SEC1 point for secp256k1. """ - privkey = coincurve.PrivateKey(bytes.fromhex(private_key_hex)) # x-only pubkey -> compressed pubkey (assume even y) pubkey_bytes = b"\x02" + bytes.fromhex(public_key_hex) - pubkey = coincurve.PublicKey(pubkey_bytes) + pubkey = secp256k1.PublicKey(pubkey_bytes, True) - # ECDH: multiply pubkey by privkey, get raw x coordinate (32 bytes) - # coincurve's ecdh returns sha256(compressed_point) by default, - # but NIP-44 needs the unhashed x coordinate. - shared_point = pubkey.multiply(privkey.secret) - shared_x = shared_point.format(compressed=True)[1:] # strip 0x02/0x03 prefix + # ECDH: multiply pubkey by privkey scalar, get shared point + # tweak_mul multiplies the point by a scalar, returning a new PublicKey + shared_point = pubkey.tweak_mul(bytes.fromhex(private_key_hex)) + # serialize(compressed=True) gives 0x02/0x03 + 32-byte x coordinate + # strip the prefix byte to get the raw x coordinate + shared_x = shared_point.serialize()[1:] # HKDF-extract with salt="nip44-v2" return _hkdf_extract(salt=_NIP44_SALT, ikm=shared_x) diff --git a/lnbits/core/services/nostr_transport/relay_pool.py b/lnbits/core/services/nostr_transport/relay_pool.py index 871886fa..df4ca422 100644 --- a/lnbits/core/services/nostr_transport/relay_pool.py +++ b/lnbits/core/services/nostr_transport/relay_pool.py @@ -14,7 +14,7 @@ import json import time from collections.abc import Awaitable, Callable -import coincurve +import secp256k1 from loguru import logger from websockets import connect as ws_connect @@ -49,8 +49,8 @@ class NostrTransportPool: self.relay_urls = relay_urls self.event_callback = event_callback - # coincurve key objects for signing - self._privkey = coincurve.PrivateKey(bytes.fromhex(private_key_hex)) + # secp256k1 key objects for signing + self._privkey = secp256k1.PrivateKey(bytes.fromhex(private_key_hex), True) # Deduplication: event_id -> timestamp self._handled_events: dict[str, float] = {}