178 lines
6.1 KiB
Python
178 lines
6.1 KiB
Python
"""Unit tests for infrastructure.nostr.event."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
import time
|
|
|
|
import pytest
|
|
|
|
from infrastructure.nostr.event import (
|
|
_event_hash,
|
|
build_event,
|
|
schnorr_sign,
|
|
schnorr_verify,
|
|
)
|
|
from infrastructure.nostr.keypair import generate_keypair
|
|
|
|
|
|
class TestSchorrSign:
|
|
def test_returns_64_bytes(self):
|
|
kp = generate_keypair()
|
|
msg = b"\x00" * 32
|
|
sig = schnorr_sign(msg, kp.privkey_bytes)
|
|
assert len(sig) == 64
|
|
|
|
def test_different_msg_different_sig(self):
|
|
kp = generate_keypair()
|
|
sig1 = schnorr_sign(b"\x01" * 32, kp.privkey_bytes)
|
|
sig2 = schnorr_sign(b"\x02" * 32, kp.privkey_bytes)
|
|
assert sig1 != sig2
|
|
|
|
def test_raises_on_wrong_msg_length(self):
|
|
kp = generate_keypair()
|
|
with pytest.raises(ValueError, match="32 bytes"):
|
|
schnorr_sign(b"too short", kp.privkey_bytes)
|
|
|
|
def test_raises_on_wrong_key_length(self):
|
|
msg = b"\x00" * 32
|
|
with pytest.raises(ValueError, match="32 bytes"):
|
|
schnorr_sign(msg, b"too short")
|
|
|
|
def test_nondeterministic_due_to_randomness(self):
|
|
# BIP-340 uses auxiliary randomness; repeated calls produce different sigs
|
|
kp = generate_keypair()
|
|
msg = b"\x42" * 32
|
|
sig1 = schnorr_sign(msg, kp.privkey_bytes)
|
|
sig2 = schnorr_sign(msg, kp.privkey_bytes)
|
|
# With different random nonces these should differ (astronomically unlikely to collide)
|
|
# We just verify both are valid
|
|
assert schnorr_verify(msg, kp.pubkey_bytes, sig1)
|
|
assert schnorr_verify(msg, kp.pubkey_bytes, sig2)
|
|
|
|
|
|
class TestSchnorrVerify:
|
|
def test_valid_signature_verifies(self):
|
|
kp = generate_keypair()
|
|
msg = hashlib.sha256(b"hello nostr").digest()
|
|
sig = schnorr_sign(msg, kp.privkey_bytes)
|
|
assert schnorr_verify(msg, kp.pubkey_bytes, sig) is True
|
|
|
|
def test_wrong_pubkey_fails(self):
|
|
kp1 = generate_keypair()
|
|
kp2 = generate_keypair()
|
|
msg = b"\x00" * 32
|
|
sig = schnorr_sign(msg, kp1.privkey_bytes)
|
|
assert schnorr_verify(msg, kp2.pubkey_bytes, sig) is False
|
|
|
|
def test_tampered_sig_fails(self):
|
|
kp = generate_keypair()
|
|
msg = b"\x00" * 32
|
|
sig = bytearray(schnorr_sign(msg, kp.privkey_bytes))
|
|
sig[0] ^= 0xFF
|
|
assert schnorr_verify(msg, kp.pubkey_bytes, bytes(sig)) is False
|
|
|
|
def test_tampered_msg_fails(self):
|
|
kp = generate_keypair()
|
|
msg = b"\x00" * 32
|
|
sig = schnorr_sign(msg, kp.privkey_bytes)
|
|
bad_msg = b"\xff" * 32
|
|
assert schnorr_verify(bad_msg, kp.pubkey_bytes, sig) is False
|
|
|
|
def test_wrong_lengths_return_false(self):
|
|
kp = generate_keypair()
|
|
msg = b"\x00" * 32
|
|
sig = schnorr_sign(msg, kp.privkey_bytes)
|
|
assert schnorr_verify(msg[:16], kp.pubkey_bytes, sig) is False
|
|
assert schnorr_verify(msg, kp.pubkey_bytes[:16], sig) is False
|
|
assert schnorr_verify(msg, kp.pubkey_bytes, sig[:32]) is False
|
|
|
|
def test_never_raises(self):
|
|
# Should return False for any garbage input, not raise
|
|
assert schnorr_verify(b"x", b"y", b"z") is False
|
|
|
|
|
|
class TestEventHash:
|
|
def test_returns_32_bytes(self):
|
|
h = _event_hash("aabbcc", 0, 1, [], "")
|
|
assert len(h) == 32
|
|
|
|
def test_deterministic(self):
|
|
h1 = _event_hash("aa", 1, 1, [], "hello")
|
|
h2 = _event_hash("aa", 1, 1, [], "hello")
|
|
assert h1 == h2
|
|
|
|
def test_different_content_different_hash(self):
|
|
h1 = _event_hash("aa", 1, 1, [], "hello")
|
|
h2 = _event_hash("aa", 1, 1, [], "world")
|
|
assert h1 != h2
|
|
|
|
|
|
class TestBuildEvent:
|
|
def test_returns_required_fields(self):
|
|
kp = generate_keypair()
|
|
ev = build_event(kind=1, content="hello", keypair=kp)
|
|
assert set(ev) >= {"id", "pubkey", "created_at", "kind", "tags", "content", "sig"}
|
|
|
|
def test_kind_matches(self):
|
|
kp = generate_keypair()
|
|
ev = build_event(kind=0, content="{}", keypair=kp)
|
|
assert ev["kind"] == 0
|
|
|
|
def test_pubkey_matches_keypair(self):
|
|
kp = generate_keypair()
|
|
ev = build_event(kind=1, content="x", keypair=kp)
|
|
assert ev["pubkey"] == kp.pubkey_hex
|
|
|
|
def test_id_is_64_char_hex(self):
|
|
kp = generate_keypair()
|
|
ev = build_event(kind=1, content="x", keypair=kp)
|
|
assert len(ev["id"]) == 64
|
|
assert all(c in "0123456789abcdef" for c in ev["id"])
|
|
|
|
def test_sig_is_128_char_hex(self):
|
|
kp = generate_keypair()
|
|
ev = build_event(kind=1, content="x", keypair=kp)
|
|
assert len(ev["sig"]) == 128
|
|
assert all(c in "0123456789abcdef" for c in ev["sig"])
|
|
|
|
def test_signature_verifies(self):
|
|
kp = generate_keypair()
|
|
ev = build_event(kind=1, content="test", keypair=kp)
|
|
sig_bytes = bytes.fromhex(ev["sig"])
|
|
id_bytes = bytes.fromhex(ev["id"])
|
|
assert schnorr_verify(id_bytes, kp.pubkey_bytes, sig_bytes)
|
|
|
|
def test_id_matches_canonical_hash(self):
|
|
kp = generate_keypair()
|
|
ts = int(time.time())
|
|
ev = build_event(kind=1, content="hi", keypair=kp, created_at=ts)
|
|
expected_hash = _event_hash(kp.pubkey_hex, ts, 1, [], "hi").hex()
|
|
assert ev["id"] == expected_hash
|
|
|
|
def test_custom_tags(self):
|
|
kp = generate_keypair()
|
|
tags = [["t", "gaming"], ["r", "wss://relay.example.com"]]
|
|
ev = build_event(kind=1, content="x", keypair=kp, tags=tags)
|
|
assert ev["tags"] == tags
|
|
|
|
def test_default_tags_empty(self):
|
|
kp = generate_keypair()
|
|
ev = build_event(kind=1, content="x", keypair=kp)
|
|
assert ev["tags"] == []
|
|
|
|
def test_custom_created_at(self):
|
|
kp = generate_keypair()
|
|
ts = 1700000000
|
|
ev = build_event(kind=1, content="x", keypair=kp, created_at=ts)
|
|
assert ev["created_at"] == ts
|
|
|
|
def test_kind0_profile_content_is_json(self):
|
|
kp = generate_keypair()
|
|
profile = {"name": "Timmy", "about": "test"}
|
|
ev = build_event(kind=0, content=json.dumps(profile), keypair=kp)
|
|
assert ev["kind"] == 0
|
|
parsed = json.loads(ev["content"])
|
|
assert parsed["name"] == "Timmy"
|