Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions pyrelay/relay/handlers/send_event_handler.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -31,6 +33,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 not is_creation_time_valid(event.created_at, time.time(), nips_config.nip_22):
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the validation should happen before the delete

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)
Expand All @@ -45,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:
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you add a docstring?

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)
8 changes: 6 additions & 2 deletions pyrelay/relay/nip_config.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
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 - inclusive
UPPER_TIMESTAMP_OFFSET_LIMIT = 60 * 15 # allow up to 15 minutes into the future - exclusive


class NIPConfig(BaseModel):
# NIP-9: Event deletion
Expand All @@ -20,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: bool = False
# 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
Expand Down
70 changes: 69 additions & 1 deletion tests/relay/test_relay_dispatcher.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import time
import uuid
from collections import defaultdict

Expand All @@ -10,6 +11,8 @@
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
from pyrelay.relay.handlers.send_event_handler import is_creation_time_valid


@pytest.fixture(scope="module")
Expand All @@ -33,6 +36,18 @@ def get_dispatcher():
return dispatcher


async def _attempt_timestamp(event_builder, timestamps: list[int], should_save: bool):
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't really understand what the function is doing with the attempt_timestamp. can we rename it?

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 client_session.calls["send_event"]:
assert msg.saved == should_save


class TestRelayDispatcher:
@pytest.mark.asyncio
async def test_report_and_save_events(self, event_builder):
Expand Down Expand Up @@ -123,7 +138,7 @@ async def test_subscribe_and_broadcast(self):

]
assert calls == expected

@pytest.mark.asyncio
async def test_wrong_msg(self):
dispatcher = get_dispatcher()
Expand All @@ -144,3 +159,56 @@ 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
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this test work based on configuration?

if not nip_22_config:
return
current_time = time.time()
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the fun part about not working with a method that uses the actual time it to test it specifically without having the whole dispatcher

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 * 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)

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
Comment on lines +190 to +214
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

first - you probably want to use parameterize here :)
second - its a bit hard to read like that, anyway to improve readability?