From 651e9320edf829a761ea9a4899e1d238fba5384a Mon Sep 17 00:00:00 2001 From: "jeremy.whelchel" Date: Sun, 5 Feb 2023 19:08:53 -0500 Subject: [PATCH 01/14] Fix all pytype errors and Event() signature mismatch. --- nostr/delegation.py | 3 ++- nostr/event.py | 19 ++++++++++--------- nostr/filter.py | 25 +++++++++++++------------ nostr/key.py | 12 +++++++----- nostr/message_pool.py | 3 ++- nostr/relay.py | 13 ++++++++++--- nostr/relay_manager.py | 3 ++- nostr/subscription.py | 3 ++- 8 files changed, 48 insertions(+), 33 deletions(-) diff --git a/nostr/delegation.py b/nostr/delegation.py index 94801f5..47e35f5 100644 --- a/nostr/delegation.py +++ b/nostr/delegation.py @@ -1,4 +1,5 @@ import time +from typing import Optional from dataclasses import dataclass @@ -8,7 +9,7 @@ class Delegation: delegatee_pubkey: str event_kind: int duration_secs: int = 30*24*60 # default to 30 days - signature: str = None # set in PrivateKey.sign_delegation + signature: Optional[str] = None # set in PrivateKey.sign_delegation @property def expires(self) -> int: diff --git a/nostr/event.py b/nostr/event.py index 11f56c6..a8b57fe 100644 --- a/nostr/event.py +++ b/nostr/event.py @@ -1,9 +1,10 @@ import time +from typing import Optional import json from dataclasses import dataclass, field from enum import IntEnum from typing import List -from secp256k1 import PrivateKey, PublicKey +from secp256k1 import PrivateKey, PublicKey # type: ignore from hashlib import sha256 from nostr.message_type import ClientMessageType @@ -22,12 +23,12 @@ class EventKind(IntEnum): @dataclass class Event: - content: str = None - public_key: str = None - created_at: int = None - kind: int = EventKind.TEXT_NOTE + content: Optional[str] = None + public_key: Optional[str] = None + created_at: Optional[int] = None + kind: Optional[int] = EventKind.TEXT_NOTE tags: List[List[str]] = field(default_factory=list) # Dataclasses require special handling when the default value is a mutable type - signature: str = None + signature: Optional[str] = None def __post_init__(self): @@ -92,9 +93,9 @@ def to_message(self) -> str: @dataclass class EncryptedDirectMessage(Event): - recipient_pubkey: str = None - cleartext_content: str = None - reference_event_id: str = None + recipient_pubkey: Optional[str] = None + cleartext_content: Optional[str] = None + reference_event_id: Optional[str] = None def __post_init__(self): diff --git a/nostr/filter.py b/nostr/filter.py index f4cb0a5..a531dcb 100644 --- a/nostr/filter.py +++ b/nostr/filter.py @@ -1,5 +1,5 @@ from collections import UserList -from typing import List +from typing import List, Optional from .event import Event, EventKind @@ -16,20 +16,21 @@ class Filter: added. For example: # arbitrary tag filter.add_arbitrary_tag('t', [hashtags]) - + # promoted to explicit support Filter(hashtag_refs=[hashtags]) """ def __init__( - self, - event_ids: List[str] = None, - kinds: List[EventKind] = None, - authors: List[str] = None, - since: int = None, - until: int = None, - event_refs: List[str] = None, # the "#e" attr; list of event ids referenced in an "e" tag - pubkey_refs: List[str] = None, # The "#p" attr; list of pubkeys referenced in a "p" tag - limit: int = None) -> None: + self, + event_ids: Optional[List[str]] = None, + kinds: Optional[List[EventKind]] = None, + authors: Optional[List[str]] = None, + since: Optional[int] = None, + until: Optional[int] = None, + event_refs: Optional[List[str]] = None, # the "#e" attr; list of event ids referenced in an "e" tag + pubkey_refs: Optional[List[str]] = None, # The "#p" attr; list of pubkeys referenced in a "p" tag + limit: Optional[int] = None, + ) -> None: self.event_ids = event_ids self.kinds = kinds self.authors = authors @@ -128,4 +129,4 @@ def match(self, event: Event): return False def to_json_array(self) -> list: - return [filter.to_json_object() for filter in self.data] \ No newline at end of file + return [filter.to_json_object() for filter in self.data] diff --git a/nostr/key.py b/nostr/key.py index 350c72d..28ec279 100644 --- a/nostr/key.py +++ b/nostr/key.py @@ -1,10 +1,11 @@ import secrets import base64 -import secp256k1 +import secp256k1 # type: ignore from cffi import FFI from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.primitives import padding from hashlib import sha256 +from typing import cast, Optional from nostr.delegation import Delegation from nostr.event import EncryptedDirectMessage, Event, EventKind @@ -35,7 +36,7 @@ def from_npub(cls, npub: str): class PrivateKey: - def __init__(self, raw_secret: bytes=None) -> None: + def __init__(self, raw_secret: Optional[bytes]=None) -> None: if not raw_secret is None: self.raw_secret = raw_secret else: @@ -77,7 +78,7 @@ def encrypt_message(self, message: str, public_key_hex: str) -> str: encrypted_message = encryptor.update(padded_data) + encryptor.finalize() return f"{base64.b64encode(encrypted_message).decode()}?iv={base64.b64encode(iv).decode()}" - + def encrypt_dm(self, dm: EncryptedDirectMessage) -> None: dm.content = self.encrypt_message(message=dm.cleartext_content, public_key_hex=dm.recipient_pubkey) @@ -104,7 +105,8 @@ def sign_message_hash(self, hash: bytes) -> str: def sign_event(self, event: Event) -> None: if event.kind == EventKind.ENCRYPTED_DIRECT_MESSAGE and event.content is None: - self.encrypt_dm(event) + edm = cast(EncryptedDirectMessage, event) + self.encrypt_dm(edm) if event.public_key is None: event.public_key = self.public_key.hex() event.signature = self.sign_message_hash(bytes.fromhex(event.id)) @@ -116,7 +118,7 @@ def __eq__(self, other): return self.raw_secret == other.raw_secret -def mine_vanity_key(prefix: str = None, suffix: str = None) -> PrivateKey: +def mine_vanity_key(prefix: Optional[str] = None, suffix: Optional[str] = None) -> PrivateKey: if prefix is None and suffix is None: raise ValueError("Expected at least one of 'prefix' or 'suffix' arguments") diff --git a/nostr/message_pool.py b/nostr/message_pool.py index ac46b24..ceb3f41 100644 --- a/nostr/message_pool.py +++ b/nostr/message_pool.py @@ -55,7 +55,8 @@ def _process_message(self, message: str, url: str): if message_type == RelayMessageType.EVENT: subscription_id = message_json[1] e = message_json[2] - event = Event(e['pubkey'], e['content'], e['created_at'], e['kind'], e['tags'], e['id'], e['sig']) + # XXX should have Event.from_json() + event = Event(e['pubkey'], e['content'], e['created_at'], e['kind'], e['tags'], e['sig']) with self.lock: if not event.id in self._unique_events: self.events.put(EventMessage(event, subscription_id, url)) diff --git a/nostr/relay.py b/nostr/relay.py index 373a259..edbd9ee 100644 --- a/nostr/relay.py +++ b/nostr/relay.py @@ -1,5 +1,6 @@ import json from threading import Lock +from typing import Optional from websocket import WebSocketApp from .event import Event from .filter import Filters @@ -37,7 +38,7 @@ def __init__( on_error=self._on_error, on_close=self._on_close) - def connect(self, ssl_options: dict=None, proxy: dict=None): + def connect(self, ssl_options: Optional[dict]=None, proxy: Optional[dict]=None): self.ws.run_forever( sslopt=ssl_options, http_proxy_host=None if proxy is None else proxy.get('host'), @@ -96,14 +97,20 @@ def _is_valid_message(self, message: str) -> bool: if message_type == RelayMessageType.EVENT: if not len(message_json) == 3: return False - + subscription_id = message_json[1] with self.lock: if subscription_id not in self.subscriptions: return False e = message_json[2] - event = Event(e['pubkey'], e['content'], e['created_at'], e['kind'], e['tags'], e['id'], e['sig']) + event = Event( + content=e['content'], + public_key=e['pubkey'], + created_at=e['created_at'], + kind=e['kind'], + tags=e['tags'], + signature=e['sig']) if not event.verify(): return False diff --git a/nostr/relay_manager.py b/nostr/relay_manager.py index 191f5bd..a6e2531 100644 --- a/nostr/relay_manager.py +++ b/nostr/relay_manager.py @@ -1,5 +1,6 @@ import json import threading +from typing import Optional from .event import Event from .filter import Filters @@ -35,7 +36,7 @@ def close_subscription(self, id: str): for relay in self.relays.values(): relay.close_subscription(id) - def open_connections(self, ssl_options: dict=None, proxy: dict=None): + def open_connections(self, ssl_options: Optional[dict]=None, proxy: Optional[dict]=None): for relay in self.relays.values(): threading.Thread( target=relay.connect, diff --git a/nostr/subscription.py b/nostr/subscription.py index 7afba20..87fbc05 100644 --- a/nostr/subscription.py +++ b/nostr/subscription.py @@ -1,7 +1,8 @@ +from typing import Optional from .filter import Filters class Subscription: - def __init__(self, id: str, filters: Filters=None) -> None: + def __init__(self, id: str, filters: Optional[Filters]=None) -> None: self.id = id self.filters = filters From 0aac3715103b161301208ac7f5885dc514196c5b Mon Sep 17 00:00:00 2001 From: "jeremy.whelchel" Date: Sun, 5 Feb 2023 19:14:11 -0500 Subject: [PATCH 02/14] add todo --- nostr/message_pool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nostr/message_pool.py b/nostr/message_pool.py index ceb3f41..ddb5e97 100644 --- a/nostr/message_pool.py +++ b/nostr/message_pool.py @@ -55,7 +55,7 @@ def _process_message(self, message: str, url: str): if message_type == RelayMessageType.EVENT: subscription_id = message_json[1] e = message_json[2] - # XXX should have Event.from_json() + # TODO: Create Event.from_json(). Also used in relay.py event = Event(e['pubkey'], e['content'], e['created_at'], e['kind'], e['tags'], e['sig']) with self.lock: if not event.id in self._unique_events: From a7678af7be8981d36d2f96c472837caa25744c62 Mon Sep 17 00:00:00 2001 From: "jeremy.whelchel" Date: Sun, 5 Feb 2023 19:35:45 -0500 Subject: [PATCH 03/14] Factor out Event.{to,from}_dict() and add test. --- nostr/event.py | 38 ++++++++++++++++++++++++-------------- nostr/message_pool.py | 4 +--- nostr/relay.py | 9 +-------- test/test_event.py | 24 ++++++++++++++++++------ 4 files changed, 44 insertions(+), 31 deletions(-) diff --git a/nostr/event.py b/nostr/event.py index a8b57fe..1e3bbf0 100644 --- a/nostr/event.py +++ b/nostr/event.py @@ -73,22 +73,32 @@ def verify(self) -> bool: return pub_key.schnorr_verify(bytes.fromhex(self.id), bytes.fromhex(self.signature), None, raw=True) - def to_message(self) -> str: - return json.dumps( - [ - ClientMessageType.EVENT, - { - "id": self.id, - "pubkey": self.public_key, - "created_at": self.created_at, - "kind": self.kind, - "tags": self.tags, - "content": self.content, - "sig": self.signature - } - ] + @classmethod + def from_dict(cls, msg: dict) -> 'Event': + # "id" is ignore, as it will be computed from the contents + return Event( + content=msg['content'], + public_key=msg['pubkey'], + created_at=msg['created_at'], + kind=msg['kind'], + tags=msg['tags'], + signature=msg['sig'], ) + def to_dict(self) -> dict: + return { + "id": self.id, + "pubkey": self.public_key, + "created_at": self.created_at, + "kind": self.kind, + "tags": self.tags, + "content": self.content, + "sig": self.signature + } + + def to_message(self) -> str: + return json.dumps([ClientMessageType.EVENT, self.to_dict()]) + @dataclass diff --git a/nostr/message_pool.py b/nostr/message_pool.py index ddb5e97..418da8b 100644 --- a/nostr/message_pool.py +++ b/nostr/message_pool.py @@ -54,9 +54,7 @@ def _process_message(self, message: str, url: str): message_type = message_json[0] if message_type == RelayMessageType.EVENT: subscription_id = message_json[1] - e = message_json[2] - # TODO: Create Event.from_json(). Also used in relay.py - event = Event(e['pubkey'], e['content'], e['created_at'], e['kind'], e['tags'], e['sig']) + event = Event.from_dict(message_json[2]) with self.lock: if not event.id in self._unique_events: self.events.put(EventMessage(event, subscription_id, url)) diff --git a/nostr/relay.py b/nostr/relay.py index edbd9ee..70396f2 100644 --- a/nostr/relay.py +++ b/nostr/relay.py @@ -103,14 +103,7 @@ def _is_valid_message(self, message: str) -> bool: if subscription_id not in self.subscriptions: return False - e = message_json[2] - event = Event( - content=e['content'], - public_key=e['pubkey'], - created_at=e['created_at'], - kind=e['kind'], - tags=e['tags'], - signature=e['sig']) + event = Event.from_dict(message_json[2]) if not event.verify(): return False diff --git a/test/test_event.py b/test/test_event.py index b968a98..e478d38 100644 --- a/test/test_event.py +++ b/test/test_event.py @@ -3,9 +3,11 @@ from nostr.event import Event, EncryptedDirectMessage from nostr.key import PrivateKey +import unittest -class TestEvent: + +class TestEvent(unittest.TestCase): def test_event_default_time(self): """ ensure created_at default value reflects the time at Event object instantiation @@ -15,7 +17,7 @@ def test_event_default_time(self): time.sleep(1.5) event2 = Event(content='test event') assert event1.created_at < event2.created_at - + def test_content_only_instantiation(self): """ should be able to create an Event by only specifying content without kwarg """ @@ -34,7 +36,7 @@ def test_event_id_recomputes(self): # Recomputed id should now be different assert event.id != event_id - + def test_add_event_ref(self): """ should add an 'e' tag for each event_ref added """ @@ -51,6 +53,16 @@ def test_add_pubkey_ref(self): event.add_pubkey_ref(some_pubkey) assert ['p', some_pubkey] in event.tags + def test_dict_roundtrip(self): + """ conversion to dict and back result in same object """ + event = Event(content='test event', + created_at=12345678, + kind=1, + ) + event.add_pubkey_ref("some_pubkey") + + got = Event.from_dict(event.to_dict()) + self.assertEqual(got, event) class TestEncryptedDirectMessage: @@ -66,19 +78,19 @@ def test_content_field_moved_to_cleartext_content(self): dm = EncryptedDirectMessage(content="My message!", recipient_pubkey=self.recipient_pubkey) assert dm.content is None assert dm.cleartext_content is not None - + def test_nokwarg_content_allowed(self): """ Should allow creating a new DM w/no `content` nor `cleartext_content` kwarg """ dm = EncryptedDirectMessage("My message!", recipient_pubkey=self.recipient_pubkey) assert dm.cleartext_content is not None - + def test_recipient_p_tag(self): """ Should generate recipient 'p' tag """ dm = EncryptedDirectMessage(cleartext_content="Secret message!", recipient_pubkey=self.recipient_pubkey) assert ['p', self.recipient_pubkey] in dm.tags - + def test_unencrypted_dm_has_undefined_id(self): """ Should raise Exception if `id` is requested before DM is encrypted """ From af713df649516e2ba33d4f2ea3405d9f91a5e349 Mon Sep 17 00:00:00 2001 From: "jeremy.whelchel" Date: Mon, 13 Feb 2023 09:58:19 -0500 Subject: [PATCH 04/14] fix proxy issue --- nostr/relay.py | 11 +++++++---- nostr/relay_manager.py | 16 ++++++++-------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/nostr/relay.py b/nostr/relay.py index 18b6553..66ed886 100644 --- a/nostr/relay.py +++ b/nostr/relay.py @@ -35,7 +35,7 @@ class Relay: url: str message_pool: MessagePool policy: RelayPolicy = RelayPolicy() - proxy_config: RelayProxyConnectionConfig = RelayProxyConnectionConfig() + proxy_config: Optional[RelayProxyConnectionConfig] = None ssl_options: Optional[dict] = None def __post_init__(self): @@ -52,9 +52,12 @@ def __post_init__(self): def connect(self): self.ws.run_forever( sslopt=self.ssl_options, - http_proxy_host=self.proxy_config.host, - http_proxy_port=self.proxy_config.port, - proxy_type=self.proxy_config.type + http_proxy_host=self.proxy_config.host if self.proxy_config is not + None else None, + http_proxy_port=self.proxy_config.port if self.proxy_config is not + None else None, + proxy_type=self.proxy_config.type if self.proxy_config is not None + else None, ) def close(self): diff --git a/nostr/relay_manager.py b/nostr/relay_manager.py index 6aee395..9134b7a 100644 --- a/nostr/relay_manager.py +++ b/nostr/relay_manager.py @@ -27,13 +27,13 @@ def __post_init__(self): self.lock: Lock = Lock() def add_relay( - self, - url: str, + self, + url: str, policy: RelayPolicy = RelayPolicy(), - ssl_options: dict = None, - proxy_config: RelayProxyConnectionConfig = None): + ssl_options: Optional[dict] = None, + proxy_config: Optional[RelayProxyConnectionConfig] = None): - relay = Relay(url, self.message_pool, policy, ssl_options, proxy_config) + relay = Relay(url, self.message_pool, policy, proxy_config, ssl_options) with self.lock: self.relays[url] = relay @@ -56,12 +56,12 @@ def add_subscription_on_relay(self, url: str, id: str, filters: Filters): if url in self.relays: relay = self.relays[url] if not relay.policy.should_read: - raise RelayException(f"Could not send request: {relay_url} is not configured to read from") + raise RelayException(f"Could not send request: {url} is not configured to read from") relay.add_subscription(id, filters) request = Request(id, filters) relay.publish(request.to_message()) else: - raise RelayException(f"Invalid relay url: no connection to {relay_url}") + raise RelayException(f"Invalid relay url: no connection to {url}") def add_subscription_on_all_relays(self, id: str, filters: Filters): with self.lock: @@ -78,7 +78,7 @@ def close_subscription_on_relay(self, url: str, id: str): relay.close_subscription(id) relay.publish(json.dumps(["CLOSE", id])) else: - raise RelayException(f"Invalid relay url: no connection to {relay_url}") + raise RelayException(f"Invalid relay url: no connection to {url}") def close_subscription_on_all_relays(self, id: str): with self.lock: From f1356eb59c41d67642e5dd80d580554fdabd1ac3 Mon Sep 17 00:00:00 2001 From: "jeremy.whelchel" Date: Tue, 14 Feb 2023 13:44:00 -0500 Subject: [PATCH 05/14] add relay debug messages --- nostr/relay.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/nostr/relay.py b/nostr/relay.py index 66ed886..cb6f469 100644 --- a/nostr/relay.py +++ b/nostr/relay.py @@ -1,4 +1,6 @@ import json +import logging + from dataclasses import dataclass from threading import Lock from typing import Optional @@ -87,15 +89,19 @@ def to_json_object(self) -> dict: } def _on_open(self, class_obj): + logging.debug("Relay._on_open: url=%s", self.url) pass def _on_close(self, class_obj, status_code, message): + logging.debug("Relay._on_close: url=%s, code=%s, message=%s", self.url, + status_code, message) pass def _on_message(self, class_obj, message: str): self.message_pool.add_message(message, self.url) - + def _on_error(self, class_obj, error): + logging.debug("Relay._on_error: url=%s, error=%s", self.url, error) pass def _is_valid_message(self, message: str) -> bool: From e77c2c526c67bf5456f59823ea52b613b551dd91 Mon Sep 17 00:00:00 2001 From: "jeremy.whelchel" Date: Tue, 14 Feb 2023 14:07:59 -0500 Subject: [PATCH 06/14] wrap ws.send in WebSocketConnectionClosedException --- nostr/relay.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/nostr/relay.py b/nostr/relay.py index cb6f469..346d75c 100644 --- a/nostr/relay.py +++ b/nostr/relay.py @@ -4,7 +4,7 @@ from dataclasses import dataclass from threading import Lock from typing import Optional -from websocket import WebSocketApp +from websocket import WebSocketApp, WebSocketConnectionClosedException from .event import Event from .filter import Filters from .message_pool import MessagePool @@ -66,7 +66,11 @@ def close(self): self.ws.close() def publish(self, message: str): - self.ws.send(message) + try: + self.ws.send(message) + except WebSocketConnectionClosedException as e: + logging.error("Connection closed on publish attempt. url=%s", + self.url) def add_subscription(self, id, filters: Filters): with self.lock: From 5cb5933c5a7f75baa2b10cb668c1801b485f572a Mon Sep 17 00:00:00 2001 From: "jeremy.whelchel" Date: Tue, 14 Feb 2023 14:21:59 -0500 Subject: [PATCH 07/14] custom logger --- nostr/relay.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/nostr/relay.py b/nostr/relay.py index 346d75c..f56c17d 100644 --- a/nostr/relay.py +++ b/nostr/relay.py @@ -11,6 +11,10 @@ from .message_type import RelayMessageType from .subscription import Subscription + +logger = logging.getLogger('nostr') + + @dataclass class RelayPolicy: should_read: bool = True @@ -69,8 +73,8 @@ def publish(self, message: str): try: self.ws.send(message) except WebSocketConnectionClosedException as e: - logging.error("Connection closed on publish attempt. url=%s", - self.url) + logger.error("Connection closed on publish attempt. url=%s", + self.url) def add_subscription(self, id, filters: Filters): with self.lock: @@ -93,11 +97,11 @@ def to_json_object(self) -> dict: } def _on_open(self, class_obj): - logging.debug("Relay._on_open: url=%s", self.url) + logger.debug("Relay._on_open: url=%s", self.url) pass def _on_close(self, class_obj, status_code, message): - logging.debug("Relay._on_close: url=%s, code=%s, message=%s", self.url, + logger.debug("Relay._on_close: url=%s, code=%s, message=%s", self.url, status_code, message) pass @@ -105,7 +109,7 @@ def _on_message(self, class_obj, message: str): self.message_pool.add_message(message, self.url) def _on_error(self, class_obj, error): - logging.debug("Relay._on_error: url=%s, error=%s", self.url, error) + logger.debug("Relay._on_error: url=%s, error=%s", self.url, error) pass def _is_valid_message(self, message: str) -> bool: From 47551343f048bf121ab707040086f3a6e924da06 Mon Sep 17 00:00:00 2001 From: "jeremy.whelchel" Date: Tue, 14 Feb 2023 16:48:38 -0500 Subject: [PATCH 08/14] use daemon threads in relay manager --- nostr/relay_manager.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nostr/relay_manager.py b/nostr/relay_manager.py index 9134b7a..e69ed96 100644 --- a/nostr/relay_manager.py +++ b/nostr/relay_manager.py @@ -40,7 +40,8 @@ def add_relay( threading.Thread( target=relay.connect, - name=f"{relay.url}-thread" + name=f"{relay.url}-thread", + daemon=True, ).start() time.sleep(1) From 03629179b08e80d78ff174f49ff2107f841a19d2 Mon Sep 17 00:00:00 2001 From: erik aronesty Date: Mon, 27 Feb 2023 00:32:53 -0500 Subject: [PATCH 09/14] windows compatible nostr --- nostr/event.py | 2 +- nostr/key.py | 3 ++- nostr/secp.py | 51 ++++++++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 2 +- 4 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 nostr/secp.py diff --git a/nostr/event.py b/nostr/event.py index b6b8ccf..213158b 100644 --- a/nostr/event.py +++ b/nostr/event.py @@ -3,11 +3,11 @@ from dataclasses import dataclass, field from enum import IntEnum from typing import List -from secp256k1 import PublicKey from hashlib import sha256 from . import bech32 from .message_type import ClientMessageType +from .secp import PublicKey diff --git a/nostr/key.py b/nostr/key.py index e3d5e54..b2afdcb 100644 --- a/nostr/key.py +++ b/nostr/key.py @@ -1,11 +1,12 @@ import secrets import base64 -import secp256k1 from cffi import FFI from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.primitives import padding from hashlib import sha256 +import nostr.secp as secp256k1 + from .delegation import Delegation from .event import EncryptedDirectMessage, Event, EventKind from . import bech32 diff --git a/nostr/secp.py b/nostr/secp.py new file mode 100644 index 0000000..0d49920 --- /dev/null +++ b/nostr/secp.py @@ -0,0 +1,51 @@ +import coincurve +from coincurve._libsecp256k1 import ffi, lib + +### compat with secp256k1 python lib + +class PublicKey: + def __init__(self, pubkey=None, raw=False): + self.cc = coincurve.PublicKey(pubkey) if pubkey else None + self.__compressed = None + + def serialize(self, compressed=True): + if compressed: + if not self.__compressed: + self.__compressed = self.cc.format(compressed=True) + return self.__compressed + else: + return self.cc.format(compressed=False) + + def ecdh(self, scalar, hashfn=ffi.NULL, hasharg=ffi.NULL): + priv = coincurve.PrivateKey(scalar) + result = ffi.new('char [32]') + res = lib.secp256k1_ecdh( + priv.context.ctx, result, self.cc.public_key, priv.secret, hashfn, hasharg + ) + if not res: + raise Exception(f'invalid scalar ({res})') + return bytes(ffi.buffer(result, 32)) + + def schnorr_verify(self, msg, schnorr_sig, bip340tag, raw=False): + assert bip340tag is None + assert raw + pk = coincurve.PublicKeyXOnly(self.serialize()[1:]) + try: + return pk.verify(schnorr_sig, msg) + except ValueError: + return False + +class PrivateKey: + def __init__(self, privkey=None, raw=True): + if not raw: + self.cc = coincurve.PrivateKey.from_der(privkey) + else: + self.cc = coincurve.PrivateKey(privkey) + + self.pubkey = PublicKey() + self.pubkey.cc = coincurve.PublicKey.from_valid_secret(self.cc.secret) + + def schnorr_sign(self, hash, bip340tag, raw=True): + assert bip340tag is None + assert raw + return self.cc.sign_schnorr(hash) diff --git a/pyproject.toml b/pyproject.toml index ef607a0..0db0ffa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ dependencies = [ "cryptography>=37.0.4", "pycparser>=2.21", "python-socks>=2.1.1", - "secp256k1>=0.14.0", + "coincurve>=18.0.0", "websocket-client>=1.3.3", ] license = {file = "LICENSE"} From 511154f0f32c60350cc49bd4be9ee7cc508bef6e Mon Sep 17 00:00:00 2001 From: erik aronesty Date: Mon, 27 Feb 2023 00:43:10 -0500 Subject: [PATCH 10/14] get rid of untested function --- nostr/key.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/nostr/key.py b/nostr/key.py index b2afdcb..b4f469e 100644 --- a/nostr/key.py +++ b/nostr/key.py @@ -59,10 +59,6 @@ def bech32(self) -> str: def hex(self) -> str: return self.raw_secret.hex() - def tweak_add(self, scalar: bytes) -> bytes: - sk = secp256k1.PrivateKey(self.raw_secret) - return sk.tweak_add(scalar) - def compute_shared_secret(self, public_key_hex: str) -> bytes: pk = secp256k1.PublicKey(bytes.fromhex("02" + public_key_hex), True) return pk.ecdh(self.raw_secret, hashfn=copy_x) From 6aa64fab8b94c969db078b80fcd25e9898e64fd8 Mon Sep 17 00:00:00 2001 From: erik aronesty Date: Tue, 28 Feb 2023 17:46:26 -0500 Subject: [PATCH 11/14] dameon --- nostr/relay_manager.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nostr/relay_manager.py b/nostr/relay_manager.py index ffe3868..152807d 100644 --- a/nostr/relay_manager.py +++ b/nostr/relay_manager.py @@ -39,7 +39,8 @@ def add_relay( threading.Thread( target=relay.connect, - name=f"{relay.url}-thread" + name=f"{relay.url}-thread", + daemon=True ).start() threading.Thread( From b8e7871d825d2722ac8db1b4b8d199d37b6802b3 Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Mon, 10 Jul 2023 21:07:36 +0200 Subject: [PATCH 12/14] add Event.to_json() --- nostr/event.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/nostr/event.py b/nostr/event.py index b6b8ccf..da86c9a 100644 --- a/nostr/event.py +++ b/nostr/event.py @@ -78,10 +78,8 @@ def verify(self) -> bool: pub_key = PublicKey(bytes.fromhex("02" + self.public_key), True) # add 02 for schnorr (bip340) return pub_key.schnorr_verify(bytes.fromhex(self.id), bytes.fromhex(self.signature), None, raw=True) - - def to_message(self) -> str: - return json.dumps( - [ + def to_json(self) -> list: + return [ ClientMessageType.EVENT, { "id": self.id, @@ -93,7 +91,9 @@ def to_message(self) -> str: "sig": self.signature } ] - ) + + def to_message(self) -> str: + return json.dumps(self.to_json()) From b67d5dbe86602c00b33cd0bd314568c901c216b3 Mon Sep 17 00:00:00 2001 From: erik aronesty Date: Sun, 30 Jul 2023 01:22:54 -0400 Subject: [PATCH 13/14] fix stuff --- nostr/key.py | 5 +++++ nostr/relay.py | 4 ++-- nostr/relay_manager.py | 3 ++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/nostr/key.py b/nostr/key.py index b4f469e..3cbbe20 100644 --- a/nostr/key.py +++ b/nostr/key.py @@ -52,6 +52,11 @@ def from_nsec(cls, nsec: str): raw_secret = bech32.convertbits(data, 5, 8)[:-1] return cls(bytes(raw_secret)) + @classmethod + def from_hex(cls, hex: str): + """ Load a PrivateKey from its bech32/nsec form """ + return cls(bytes.fromhex(hex)) + def bech32(self) -> str: converted_bits = bech32.convertbits(self.raw_secret, 8, 5) return bech32.bech32_encode("nsec", converted_bits, bech32.Encoding.BECH32) diff --git a/nostr/relay.py b/nostr/relay.py index 8ab88f8..bef6b78 100644 --- a/nostr/relay.py +++ b/nostr/relay.py @@ -1,6 +1,6 @@ import json import time -from dataclasses import dataclass +from dataclasses import dataclass, field from queue import Queue from threading import Lock from typing import Optional @@ -34,7 +34,7 @@ class RelayProxyConnectionConfig: class Relay: url: str message_pool: MessagePool - policy: RelayPolicy = RelayPolicy() + policy: RelayPolicy = field(default_factory=RelayPolicy) ssl_options: Optional[dict] = None proxy_config: RelayProxyConnectionConfig = None diff --git a/nostr/relay_manager.py b/nostr/relay_manager.py index ffe3868..152807d 100644 --- a/nostr/relay_manager.py +++ b/nostr/relay_manager.py @@ -39,7 +39,8 @@ def add_relay( threading.Thread( target=relay.connect, - name=f"{relay.url}-thread" + name=f"{relay.url}-thread", + daemon=True ).start() threading.Thread( From 1162357a007529a6a5761eedbc2dcae74bce095b Mon Sep 17 00:00:00 2001 From: erik aronesty Date: Sun, 30 Jul 2023 09:15:43 -0400 Subject: [PATCH 14/14] nont --- nostr/relay.py | 1 - 1 file changed, 1 deletion(-) diff --git a/nostr/relay.py b/nostr/relay.py index 6e0a0ca..4c765e6 100644 --- a/nostr/relay.py +++ b/nostr/relay.py @@ -67,7 +67,6 @@ def connect(self): http_proxy_host=self.proxy_config.host if self.proxy_config is not None else None, http_proxy_port=self.proxy_config.port if self.proxy_config is not None else None, proxy_type=self.proxy_config.type if self.proxy_config is not None else None, - else None, ) def close(self):