1// RelayRouter.swift2// SophaxChatCore3//4// Deduplication and TTL management for the multihop relay system.5//6// When a node receives a RelayEnvelope it:7// 1. Checks if the envelope ID is in the seen-set (dedup)8// 2. If not seen AND TTL > 0: adds to seen-set, forwards decremented envelope9// 3. If already seen OR TTL == 0: drops silently10//11// The seen-set is an in-memory LRU cache bounded to `maxSeen` entries.12// This prevents a single malicious peer from exhausting memory by replaying13// old envelopes with large IDs.1415import Foundation1617// Threading: RelayRouter is internally thread-safe — all mutable state is protected18// by `lock` (NSLock). It is @unchecked Sendable only because NSLock is not Sendable19// in the Swift 6 type system; the implementation is safe for concurrent use.20public final class RelayRouter:@unchecked Sendable {2122 private let maxSeen: Int23 private var seenSet: Set<String>=[]24 private var seenQueue:[String]=[]// FIFO for LRU eviction25 private let lock = NSLock()2627// MARK: - Rate limiting (per relay sender + global)2829/// Sliding window: max `maxRelaysPerWindow` relay forwards from one peer per `windowSeconds`.30 private let maxRelaysPerWindow: Int31 private let windowSeconds: TimeInterval32 private struct SenderWindow { var count: Int; var windowStart: Date }33 private var senderWindows:[String: SenderWindow]=[:]3435/// Global relay counter — prevents a coordinated amplification attack where many peers each36/// stay within the per-sender limit but collectively flood the relay path.37 private var globalRelayCount: Int = 038 private var globalWindowStart: Date = Date()39 private let maxGlobalRelaysPerWindow: Int4041 public init(maxSeen: Int = 2000, maxRelaysPerWindow: Int = 10, windowSeconds: TimeInterval = 10,42 maxGlobalRelaysPerWindow: Int = 50){43 self.maxSeen = maxSeen44 self.maxRelaysPerWindow = maxRelaysPerWindow45 self.windowSeconds = windowSeconds46 self.maxGlobalRelaysPerWindow = maxGlobalRelaysPerWindow47}4849// MARK: - Public API5051/// Call this when a RelayEnvelope arrives.52/// Returns `true` if the envelope should be processed/forwarded,53/// `false` if it was already seen or TTL is exhausted.54 public func shouldProcess(_ envelope: RelayEnvelope)-> Bool {55// Drop if TTL exhausted OR if a malicious peer crafted an inflated TTL56// that would cause unbounded flooding across the mesh.57 guard envelope.ttl > 0, envelope.ttl <= RelayEnvelope.maxTTL else { return false }5859 lock.lock()60 defer { lock.unlock()}6162 guard !seenSet.contains(envelope.id) else { return false }6364 seenSet.insert(envelope.id)65 seenQueue.append(envelope.id)6667// Evict oldest entry if we've exceeded the cap68 if seenSet.count > maxSeen {69 let evicted = seenQueue.removeFirst()70 seenSet.remove(evicted)71}72 return true73}7475/// Returns `true` if `senderID` has exceeded the relay rate limit (per-sender or global).76 public func isRateLimited(senderID: String)-> Bool {77 lock.lock()78 defer { lock.unlock()}7980 let now = Date()8182// Global window reset83 if now.timeIntervalSince(globalWindowStart)>= windowSeconds {84 globalRelayCount = 085 globalWindowStart = now86}87// Global cap — prevents coordinated amplification by many peers each within per-sender limit88 if globalRelayCount >= maxGlobalRelaysPerWindow { return true }89 globalRelayCount += 19091// Evict expired sender windows to prevent unbounded memory growth92 senderWindows = senderWindows.filter { now.timeIntervalSince($0.value.windowStart)< windowSeconds * 2 }9394// Per-sender window95 if var window = senderWindows[senderID]{96 if now.timeIntervalSince(window.windowStart)>= windowSeconds {97 window = SenderWindow(count: 1, windowStart: now)98 senderWindows[senderID]= window99 return false100}101 if window.count >= maxRelaysPerWindow { return true }102 window.count += 1103 senderWindows[senderID]= window104 return false105} else {106 senderWindows[senderID]= SenderWindow(count: 1, windowStart: now)107 return false108}109}110111/// Remove all seen entries. Useful for testing.112 public func reset(){113 lock.lock()114 defer { lock.unlock()}115 seenSet.removeAll()116 seenQueue.removeAll()117}118119/// Number of unique relay IDs currently tracked.120 public var seenCount: Int {121 lock.lock()122 defer { lock.unlock()}123 return seenSet.count124}125}