"""MessageValidator — reject expired, far-future, and duplicate I2NP messages. Ported from net.i2p.router.MessageValidator. Uses a DecayingBloomFilter to detect duplicate messages. Messages are identified by their (message_id XOR truncated_expiration) fingerprint. """ from __future__ import annotations import struct import time from i2p_router.bloom_filter import DecayingBloomFilter class MessageValidator: """Validate inbound I2NP messages for expiration and duplicates.""" # Java I2P uses 65 seconds of clock fudge CLOCK_FUDGE_FACTOR_MS = 65_000 def __init__(self, bloom_filter: DecayingBloomFilter | None = None) -> None: if bloom_filter is None: bloom_filter = DecayingBloomFilter( "msg-validator", duration_seconds=600, expected_entries=50000 ) self._bloom = bloom_filter def validate_message( self, message_id: int, expiration_ms: int, now_ms: int | None = None ) -> bool: """Return True if the message should be accepted. Rejects expired, far-future, and duplicate messages. """ if now_ms is None: now_ms = int(time.time() * 1000) if self._is_expired(expiration_ms, now_ms): return False if self._is_too_far_future(expiration_ms, now_ms): return False if self._is_duplicate(message_id, expiration_ms): return False return True def _is_expired(self, expiration_ms: int, now_ms: int) -> bool: """True if message has expired (with 1.5x fudge).""" return now_ms > expiration_ms + int(self.CLOCK_FUDGE_FACTOR_MS * 1.5) def _is_too_far_future(self, expiration_ms: int, now_ms: int) -> bool: """True if expiration is unreasonably far in the future.""" return expiration_ms > now_ms + self.CLOCK_FUDGE_FACTOR_MS * 4 def _is_duplicate(self, message_id: int, expiration_ms: int) -> bool: """True if we've already seen this message.""" # Fingerprint: XOR message_id with truncated expiration exp_trunc = expiration_ms & 0xFFFFFFFF fingerprint = struct.pack("!I", message_id ^ exp_trunc) return self._bloom.add(fingerprint)