From 9fcb5eb13363469114aca03081d6dc42fc8c8d15 Mon Sep 17 00:00:00 2001 From: Eli Al Date: Wed, 18 Jan 2023 22:43:36 +0200 Subject: [PATCH 1/6] support nip_22 no test --- pyrelay/relay/handlers/send_event_handler.py | 5 +++++ pyrelay/relay/nip_config.py | 7 ++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/pyrelay/relay/handlers/send_event_handler.py b/pyrelay/relay/handlers/send_event_handler.py index 321b061..bb5bcc7 100644 --- a/pyrelay/relay/handlers/send_event_handler.py +++ b/pyrelay/relay/handlers/send_event_handler.py @@ -31,6 +31,11 @@ async def _save_event(repo: EventsRepository, event: NostrEvent) -> NostrCommand if nips_config.nip_9 and event.kind == EventKind.EventDeletion: # type: ignore await _handle_delete_event(repo, event) + if nips_config.nip_22 and event.created_at not in range(*nips_config.nip_22): # todo add test case + return NostrCommandResults( + event_id=event.id, saved=False, message="invalid: timestamp is not in required time frame" + ) + try: # todo: add duplicate validation await repo.add(event) diff --git a/pyrelay/relay/nip_config.py b/pyrelay/relay/nip_config.py index dd300cf..8af5f18 100644 --- a/pyrelay/relay/nip_config.py +++ b/pyrelay/relay/nip_config.py @@ -1,4 +1,9 @@ from pydantic import BaseModel +import time + +time_now_sec = int(time.time()) +LOWER_TIMESTAMP_LIMIT = time_now_sec - 60 * 60 * 24 # allow up to one day into the past +UPPER_TIMESTAMP_LIMIT = time_now_sec + 60 * 15 # allow up to 15 minutes into the future class NIPConfig(BaseModel): @@ -21,7 +26,7 @@ class NIPConfig(BaseModel): nip_20: bool = True # NIP-22: End of Stored Events Notice - nip_22: bool = False + nip_22: tuple = (LOWER_TIMESTAMP_LIMIT, UPPER_TIMESTAMP_LIMIT) # NIP-33: End of Stored Events Notice nip_33: bool = False From 88cab83577b7a3036e865d34b945bd835785bca2 Mon Sep 17 00:00:00 2001 From: Eli Al Date: Thu, 19 Jan 2023 23:37:34 +0200 Subject: [PATCH 2/6] extracted to function + use real time and not server up time --- pyrelay/relay/handlers/send_event_handler.py | 11 ++++++++++- pyrelay/relay/nip_config.py | 11 +++++------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/pyrelay/relay/handlers/send_event_handler.py b/pyrelay/relay/handlers/send_event_handler.py index bb5bcc7..2c60b4b 100644 --- a/pyrelay/relay/handlers/send_event_handler.py +++ b/pyrelay/relay/handlers/send_event_handler.py @@ -1,3 +1,5 @@ +import time + from pyrelay.nostr.event import EventKind, NostrEvent from pyrelay.nostr.msgs import NostrCommandResults from pyrelay.relay.client_session import ClientSession @@ -31,7 +33,7 @@ async def _save_event(repo: EventsRepository, event: NostrEvent) -> NostrCommand if nips_config.nip_9 and event.kind == EventKind.EventDeletion: # type: ignore await _handle_delete_event(repo, event) - if nips_config.nip_22 and event.created_at not in range(*nips_config.nip_22): # todo add test case + if not _is_creation_time_valid(event.created_at, time.time(), nips_config.nip_22): # todo add test return NostrCommandResults( event_id=event.id, saved=False, message="invalid: timestamp is not in required time frame" ) @@ -50,3 +52,10 @@ async def _save_event(repo: EventsRepository, event: NostrEvent) -> NostrCommand async def _handle_delete_event(repo: EventsRepository, event: NostrEvent): keys_to_delete = event.e_tags await repo.delete(keys_to_delete) + + +def _is_creation_time_valid(event_creation_time: int, current_time: float, nip_22_config: tuple[int, int]) -> bool: + if not nip_22_config: + return True + creation_time_offset = event_creation_time - current_time + return int(creation_time_offset) in range(*nip_22_config) diff --git a/pyrelay/relay/nip_config.py b/pyrelay/relay/nip_config.py index 8af5f18..d42f2f7 100644 --- a/pyrelay/relay/nip_config.py +++ b/pyrelay/relay/nip_config.py @@ -1,9 +1,8 @@ from pydantic import BaseModel -import time -time_now_sec = int(time.time()) -LOWER_TIMESTAMP_LIMIT = time_now_sec - 60 * 60 * 24 # allow up to one day into the past -UPPER_TIMESTAMP_LIMIT = time_now_sec + 60 * 15 # allow up to 15 minutes into the future +# timestamp limits relative to current time +LOWER_TIMESTAMP_OFFSET_LIMIT = - 60 * 60 * 24 # allow up to one day into the past +UPPER_TIMESTAMP_OFFSET_LIMIT = 60 * 15 # allow up to 15 minutes into the future class NIPConfig(BaseModel): @@ -25,8 +24,8 @@ class NIPConfig(BaseModel): # NIP-20: Command Results (implemented partly) nip_20: bool = True - # NIP-22: End of Stored Events Notice - nip_22: tuple = (LOWER_TIMESTAMP_LIMIT, UPPER_TIMESTAMP_LIMIT) + # NIP-22: Event created_at Limits + nip_22: tuple = (LOWER_TIMESTAMP_OFFSET_LIMIT, UPPER_TIMESTAMP_OFFSET_LIMIT) # NIP-33: End of Stored Events Notice nip_33: bool = False From 17167111a19471281f51807348911edc7a77cf84 Mon Sep 17 00:00:00 2001 From: Eli Al Date: Sat, 21 Jan 2023 19:52:55 +0200 Subject: [PATCH 3/6] added handler test for timestamp validation --- tests/relay/test_relay_dispatcher.py | 41 +++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/tests/relay/test_relay_dispatcher.py b/tests/relay/test_relay_dispatcher.py index 2ad2a56..49a6020 100644 --- a/tests/relay/test_relay_dispatcher.py +++ b/tests/relay/test_relay_dispatcher.py @@ -1,3 +1,4 @@ +import time import uuid from collections import defaultdict @@ -10,6 +11,7 @@ from pyrelay.relay.bootstrap import get_uow_factory from pyrelay.relay.client_session import BaseClientSession from pyrelay.relay.dispatcher import RelayDispatcher +from pyrelay.relay.nip_config import nips_config @pytest.fixture(scope="module") @@ -33,6 +35,18 @@ def get_dispatcher(): return dispatcher +async def _attempt_timestamp(event_builder, timestamps: list[int], should_save: bool): + dispatcher = get_dispatcher() + client_session = MockClientSession() + for timestamp in timestamps: + event = event_builder.create_event( + "", created_at=timestamp + ) + await dispatcher.handle(client_session, event) + for msg in list(client_session.calls.values())[0]: # todo make prettier access to values + assert msg.saved == should_save + + class TestRelayDispatcher: @pytest.mark.asyncio async def test_report_and_save_events(self, event_builder): @@ -123,7 +137,7 @@ async def test_subscribe_and_broadcast(self): ] assert calls == expected - + @pytest.mark.asyncio async def test_wrong_msg(self): dispatcher = get_dispatcher() @@ -144,3 +158,28 @@ async def test_event_3(self, event_builder): ] ) await dispatcher.handle(client_session, event) + + @pytest.mark.asyncio + async def test_legal_timestamp(self, event_builder): + nip_22_config = nips_config.nip_22 + if not nip_22_config: + return + current_time = time.time() + legal_timeframe_size = nip_22_config[1] - nip_22_config[0] + lower_bound_timestamp = current_time + nip_22_config[0] + amount = 5 + # do not check lower bound as it can fail due to runtime delay + legal_timestamps = [lower_bound_timestamp + legal_timeframe_size / amount * i for i in range(1, amount + 1)] + await _attempt_timestamp(event_builder=event_builder, timestamps=legal_timestamps, should_save=True) + + @pytest.mark.asyncio + async def test_illegal_timestamp(self, event_builder): + nip_22_config = nips_config.nip_22 + if not nip_22_config: + return + current_time = time.time() + illegal_timestamps = [ + int(current_time + nip_22_config[0] - 60), # a minute before lower limit + int(current_time + nip_22_config[1] + 60) # a minute after upper limit + ] + await _attempt_timestamp(event_builder=event_builder, timestamps=illegal_timestamps, should_save=False) From ea24dddca7b51039f4dd12a937f1ae1f4713bba0 Mon Sep 17 00:00:00 2001 From: Eli Al Date: Sat, 21 Jan 2023 21:55:10 +0200 Subject: [PATCH 4/6] added unit test for timestamp validator function --- pyrelay/relay/handlers/send_event_handler.py | 4 +-- pyrelay/relay/nip_config.py | 4 +-- tests/relay/test_relay_dispatcher.py | 29 +++++++++++++++++++- 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/pyrelay/relay/handlers/send_event_handler.py b/pyrelay/relay/handlers/send_event_handler.py index 2c60b4b..c65195d 100644 --- a/pyrelay/relay/handlers/send_event_handler.py +++ b/pyrelay/relay/handlers/send_event_handler.py @@ -33,7 +33,7 @@ async def _save_event(repo: EventsRepository, event: NostrEvent) -> NostrCommand if nips_config.nip_9 and event.kind == EventKind.EventDeletion: # type: ignore await _handle_delete_event(repo, event) - if not _is_creation_time_valid(event.created_at, time.time(), nips_config.nip_22): # todo add test + if not is_creation_time_valid(event.created_at, time.time(), nips_config.nip_22): # todo add test return NostrCommandResults( event_id=event.id, saved=False, message="invalid: timestamp is not in required time frame" ) @@ -54,7 +54,7 @@ async def _handle_delete_event(repo: EventsRepository, event: NostrEvent): await repo.delete(keys_to_delete) -def _is_creation_time_valid(event_creation_time: int, current_time: float, nip_22_config: tuple[int, int]) -> bool: +def is_creation_time_valid(event_creation_time: int, current_time: float, nip_22_config: tuple[int, int]) -> bool: if not nip_22_config: return True creation_time_offset = event_creation_time - current_time diff --git a/pyrelay/relay/nip_config.py b/pyrelay/relay/nip_config.py index d42f2f7..3a1482f 100644 --- a/pyrelay/relay/nip_config.py +++ b/pyrelay/relay/nip_config.py @@ -1,8 +1,8 @@ from pydantic import BaseModel # timestamp limits relative to current time -LOWER_TIMESTAMP_OFFSET_LIMIT = - 60 * 60 * 24 # allow up to one day into the past -UPPER_TIMESTAMP_OFFSET_LIMIT = 60 * 15 # allow up to 15 minutes into the future +LOWER_TIMESTAMP_OFFSET_LIMIT = - 60 * 60 * 24 # allow up to one day into the past - inclusive +UPPER_TIMESTAMP_OFFSET_LIMIT = 60 * 15 # allow up to 15 minutes into the future - exclusive class NIPConfig(BaseModel): diff --git a/tests/relay/test_relay_dispatcher.py b/tests/relay/test_relay_dispatcher.py index 49a6020..6b8985b 100644 --- a/tests/relay/test_relay_dispatcher.py +++ b/tests/relay/test_relay_dispatcher.py @@ -12,6 +12,7 @@ from pyrelay.relay.client_session import BaseClientSession from pyrelay.relay.dispatcher import RelayDispatcher from pyrelay.relay.nip_config import nips_config +from pyrelay.relay.handlers.send_event_handler import is_creation_time_valid @pytest.fixture(scope="module") @@ -179,7 +180,33 @@ async def test_illegal_timestamp(self, event_builder): return current_time = time.time() illegal_timestamps = [ - int(current_time + nip_22_config[0] - 60), # a minute before lower limit + int(current_time + nip_22_config[0] - 60), # a minute before lower limit # todo add far fetched timestamps int(current_time + nip_22_config[1] + 60) # a minute after upper limit ] await _attempt_timestamp(event_builder=event_builder, timestamps=illegal_timestamps, should_save=False) + + def test_timestamp_validation(self): + good_timestamps_params = [ + (1500, 2000, (-600, 0)), + (1700, 2000, (-600, -200)), + (190000, 100000, (80000, 100000)), + (250000, 100000, (-600, 500000)), + (100000, 100000, (0, 1)), + (101, 100, (-600, 500000)), + (8000, 100000, None), + ] + bad_timestamps_params = [ + (1300, 2000, (-600, 0)), + (1900, 2000, (-600, -200)), + (150000, 100000, (80000, 100000)), + (9999999, 100000, (-600, 500000)), + (100001, 100000, (0, 1)), + (151, 100, (-600, 50)), + ] + for param_list, expected in [ + (good_timestamps_params, True), + (bad_timestamps_params, False) + ]: + for param_instance in param_list: + result = is_creation_time_valid(*param_instance) + assert result == expected From 8ece436f97e5ccc8fd3b6b5428a5aab77b3f9771 Mon Sep 17 00:00:00 2001 From: Eli Al Date: Sat, 21 Jan 2023 22:09:00 +0200 Subject: [PATCH 5/6] changed access to explicit key of dict isntead of iterating --- tests/relay/test_relay_dispatcher.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/relay/test_relay_dispatcher.py b/tests/relay/test_relay_dispatcher.py index 6b8985b..9b0e399 100644 --- a/tests/relay/test_relay_dispatcher.py +++ b/tests/relay/test_relay_dispatcher.py @@ -44,7 +44,7 @@ async def _attempt_timestamp(event_builder, timestamps: list[int], should_save: "", created_at=timestamp ) await dispatcher.handle(client_session, event) - for msg in list(client_session.calls.values())[0]: # todo make prettier access to values + for msg in client_session.calls["send_event"]: assert msg.saved == should_save @@ -180,8 +180,10 @@ async def test_illegal_timestamp(self, event_builder): return current_time = time.time() illegal_timestamps = [ - int(current_time + nip_22_config[0] - 60), # a minute before lower limit # todo add far fetched timestamps - int(current_time + nip_22_config[1] + 60) # a minute after upper limit + int(current_time + nip_22_config[0] - 60 * 60 * 24 * 365), # a year before lower limit + int(current_time + nip_22_config[0] - 60), # a minute before lower limit + int(current_time + nip_22_config[1] + 60), # a minute after upper limit + int(current_time + nip_22_config[1] + 60 * 60 * 24 * 365) # a year after upper limit ] await _attempt_timestamp(event_builder=event_builder, timestamps=illegal_timestamps, should_save=False) From 473c19b92f09502859cc638e6198d6e98634be40 Mon Sep 17 00:00:00 2001 From: Eli Al Date: Sat, 21 Jan 2023 22:13:04 +0200 Subject: [PATCH 6/6] removed redundant todo msg --- pyrelay/relay/handlers/send_event_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyrelay/relay/handlers/send_event_handler.py b/pyrelay/relay/handlers/send_event_handler.py index c65195d..2c47cd0 100644 --- a/pyrelay/relay/handlers/send_event_handler.py +++ b/pyrelay/relay/handlers/send_event_handler.py @@ -33,7 +33,7 @@ async def _save_event(repo: EventsRepository, event: NostrEvent) -> NostrCommand if nips_config.nip_9 and event.kind == EventKind.EventDeletion: # type: ignore await _handle_delete_event(repo, event) - if not is_creation_time_valid(event.created_at, time.time(), nips_config.nip_22): # todo add test + if not is_creation_time_valid(event.created_at, time.time(), nips_config.nip_22): return NostrCommandResults( event_id=event.id, saved=False, message="invalid: timestamp is not in required time frame" )