fix(nip09): honour a tags in deletion requests
A kind-5 request could only name events by `e` tag. Worse, one that carried `a` tags and no `e` tags built a filter with an empty id list, which matched every event by that author and marked them all deleted. Handle `a` tags per NIP-09: parse `kind:pubkey:d` (the `d` value may contain ':'), require the pubkey to be the request author and the kind to be replaceable or addressable, and remove only versions up to the request's `created_at` so a later re-publication survives. An empty `d` addresses a replaceable kind. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013Tbyw6FwjhEJg3gHfPHxWt (cherry picked from commit e695f9c66881d3d520d40f0cd3e9c95d9435938c)
This commit is contained in:
parent
d2350f907d
commit
fd19acff97
5 changed files with 191 additions and 52 deletions
137
tests/test_nip09.py
Normal file
137
tests/test_nip09.py
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from ..crud import create_event, get_events
|
||||
from ..relay.client_connection import NostrClientConnection
|
||||
from ..relay.event import NostrEvent
|
||||
from ..relay.filter import NostrFilter
|
||||
from ..relay.relay import RelaySpec
|
||||
|
||||
ALICE = "a" * 64
|
||||
BOB = "b" * 64
|
||||
SIG = "0" * 128
|
||||
|
||||
|
||||
def _connection(relay_id: str) -> NostrClientConnection:
|
||||
conn = NostrClientConnection(relay_id=relay_id, websocket=MagicMock())
|
||||
conn.get_client_config = lambda: RelaySpec()
|
||||
conn._send_msg = AsyncMock() # type: ignore[method-assign]
|
||||
return conn
|
||||
|
||||
|
||||
def _event(relay_id, id_, pubkey, kind, created_at, tags=None) -> NostrEvent:
|
||||
return NostrEvent(
|
||||
id=id_.rjust(64, "0"),
|
||||
relay_id=relay_id,
|
||||
publisher=pubkey,
|
||||
pubkey=pubkey,
|
||||
created_at=created_at,
|
||||
kind=kind,
|
||||
tags=tags or [],
|
||||
content="",
|
||||
sig=SIG,
|
||||
)
|
||||
|
||||
|
||||
async def _remaining(relay_id: str) -> set[str]:
|
||||
events = await get_events(relay_id, NostrFilter(kinds=[1, 3, 30023]), False)
|
||||
return {e.id.lstrip("0") or "0" for e in events}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_a_tag_deletes_only_the_addressed_event():
|
||||
relay_id = "nip09_a_tag"
|
||||
await create_event(_event(relay_id, "1", ALICE, 30023, 100, [["d", "one"]]))
|
||||
await create_event(_event(relay_id, "2", ALICE, 30023, 100, [["d", "two"]]))
|
||||
await create_event(_event(relay_id, "3", BOB, 30023, 100, [["d", "one"]]))
|
||||
|
||||
await _connection(relay_id)._handle_delete_event(
|
||||
_event(relay_id, "9", ALICE, 5, 200, [["a", f"30023:{ALICE}:one"]])
|
||||
)
|
||||
|
||||
assert await _remaining(relay_id) == {"2", "3"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_a_tag_keeps_versions_newer_than_the_request():
|
||||
relay_id = "nip09_until"
|
||||
await create_event(_event(relay_id, "1", ALICE, 30023, 100, [["d", "one"]]))
|
||||
await create_event(_event(relay_id, "2", ALICE, 30023, 300, [["d", "one"]]))
|
||||
|
||||
await _connection(relay_id)._handle_delete_event(
|
||||
_event(relay_id, "9", ALICE, 5, 200, [["a", f"30023:{ALICE}:one"]])
|
||||
)
|
||||
|
||||
assert await _remaining(relay_id) == {"2"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_a_tag_for_replaceable_kind_has_empty_d():
|
||||
relay_id = "nip09_replaceable"
|
||||
await create_event(_event(relay_id, "1", ALICE, 3, 100))
|
||||
await create_event(_event(relay_id, "2", ALICE, 1, 100))
|
||||
|
||||
await _connection(relay_id)._handle_delete_event(
|
||||
_event(relay_id, "9", ALICE, 5, 200, [["a", f"3:{ALICE}:"]])
|
||||
)
|
||||
|
||||
assert await _remaining(relay_id) == {"2"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_a_tag_for_non_addressable_kind_is_ignored():
|
||||
relay_id = "nip09_kind1"
|
||||
await create_event(_event(relay_id, "1", ALICE, 1, 100))
|
||||
|
||||
await _connection(relay_id)._handle_delete_event(
|
||||
_event(relay_id, "9", ALICE, 5, 200, [["a", f"1:{ALICE}:"]])
|
||||
)
|
||||
|
||||
assert await _remaining(relay_id) == {"1"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_a_tag_d_value_may_contain_colons():
|
||||
relay_id = "nip09_colon"
|
||||
await create_event(_event(relay_id, "1", ALICE, 30023, 100, [["d", "a:b:c"]]))
|
||||
|
||||
await _connection(relay_id)._handle_delete_event(
|
||||
_event(relay_id, "9", ALICE, 5, 200, [["a", f"30023:{ALICE}:a:b:c"]])
|
||||
)
|
||||
|
||||
assert await _remaining(relay_id) == set()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_e_tag_deletes_only_own_events():
|
||||
relay_id = "nip09_e_tag"
|
||||
await create_event(_event(relay_id, "1", ALICE, 1, 100))
|
||||
await create_event(_event(relay_id, "2", BOB, 1, 100))
|
||||
|
||||
await _connection(relay_id)._handle_delete_event(
|
||||
_event(
|
||||
relay_id,
|
||||
"9",
|
||||
ALICE,
|
||||
5,
|
||||
200,
|
||||
[["e", "1".rjust(64, "0")], ["e", "2".rjust(64, "0")]],
|
||||
)
|
||||
)
|
||||
|
||||
assert await _remaining(relay_id) == {"2"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_a_tag_with_empty_d_on_addressable_kind_is_specific():
|
||||
relay_id = "nip09_empty_d"
|
||||
await create_event(_event(relay_id, "1", ALICE, 30023, 100))
|
||||
await create_event(_event(relay_id, "2", ALICE, 30023, 100, [["d", ""]]))
|
||||
await create_event(_event(relay_id, "3", ALICE, 30023, 100, [["d", "one"]]))
|
||||
|
||||
await _connection(relay_id)._handle_delete_event(
|
||||
_event(relay_id, "9", ALICE, 5, 200, [["a", f"30023:{ALICE}:"]])
|
||||
)
|
||||
|
||||
assert await _remaining(relay_id) == {"3"}
|
||||
Loading…
Add table
Add a link
Reference in a new issue