What Is the LRU Seen-Set in BitChat Flood Control?

The LRU seen-set is a duplicate-detection mechanism that prevents BitChat's Bluetooth mesh network from processing the same packet multiple times during message flooding.

BitChat spreads messages across a decentralized Bluetooth mesh using a flooding protocol where every relay rebroadcasts packets to its neighbors. Because packets can arrive via multiple paths or route-failure fallbacks, the protocol must recognize and discard duplicates instantly. According to the permissionlesstech/bitchat source code, the LRU seen-set provides this critical deduplication layer through three specialized cache implementations that ensure each flooding packet is processed once per node.

Core Components of the LRU Seen-Set

The flood-control system relies on two distinct deduplication layers, both implementing LRU (Least-Recently-Used) eviction strategies to balance memory constraints against duplicate detection accuracy.

MessageDeduplicationService

MessageDeduplicationService (located in bitchat/Services/MessageDeduplicationService.swift) provides the high-level deduplication logic for application-layer data. It maintains three separate LRU caches:

  • Content keys – for near-duplicate message content
  • Nostr event IDs – for Nostr protocol events
  • Nostr ACK keys – for acknowledgment packets

Each cache is an instance of LRUDeduplicationCache, a generic LRU implementation. When a new ID or key arrives, the service checks contains(_:). If the ID is present, the packet is ignored; otherwise, it is recorded using record(_:value:). The service uses @MainActor to guarantee thread-safe access from the mesh-relay code.

MessageDeduplicator

MessageDeduplicator (found in bitchat/Utils/MessageDeduplicator.swift) serves as the lower-level, thread-safe deduplicator used directly by the network layer (e.g., the BLE service). It maintains an ordered array of Entry(id:timestamp) structures plus a lookup dictionary for O(1) membership checks.

Old entries are evicted based on dual criteria:

  • Time-based eviction – entries older than maxAge are removed
  • Capacity-based eviction – the cache purges least-recently-used items when exceeding maxCount

This component uses NSLock to ensure safe concurrent access during high-throughput flooding scenarios.

TransportConfig

TransportConfig (defined in bitchat/Services/TransportConfig.swift) supplies the capacity and expiry limits that bound the LRU sets. Key parameters include:

  • contentLRUCap = 2000 – maximum entries for content deduplication
  • messageDedupMaxAgeSeconds – default 5-minute expiration for deduplication entries

These caps ensure the seen-set stays bounded, preventing unbounded memory growth while still covering the typical flood lifetime of a few seconds to minutes.

Why an LRU Cache for Mesh Flooding?

Bluetooth mesh flooding creates specific technical constraints that make LRU caches the optimal deduplication strategy:

  • Floods are short-lived: Packets travel only a few hops and are typically discarded after a few seconds. Evicting the oldest entries keeps the cache sized to recent traffic, which is exactly the window where duplicates can appear.

  • Bounded memory guarantees: The cache never exceeds the configured capacity (e.g., 2,000 entries) and purges entries automatically when they age out, protecting embedded devices from memory exhaustion.

  • Thread-safe concurrency: Both MessageDeduplicationService (via @MainActor) and MessageDeduplicator (via NSLock) guarantee safe concurrent accesses from multiple mesh-relay threads.

Using the LRU Seen-Set in Practice

The following Swift examples demonstrate how to interact with the deduplication layer directly:

// 1️⃣ Create a deduplication service (uses the LRU caches)
let dedupService = MessageDeduplicationService()

// 2️⃣ When a new public message arrives, check for duplicate content:
if let _ = dedupService.contentTimestamp(for: incomingMessage.content) {
    // Duplicate – ignore this packet
} else {
    // First-time seen – record its timestamp for future dedup checks
    dedupService.recordContent(incomingMessage.content, timestamp: Date())
    // …handle the message normally
}

// 3️⃣ For a Nostr event ID:
if dedupService.hasProcessedNostrEvent(event.id) {
    // Duplicate event – drop it
} else {
    dedupService.recordNostrEvent(event.id)
    // Process the event
}

// 4️⃣ Direct use of the low-level deduplicator (e.g., in BLE service):
let dedup = MessageDeduplicator()
if dedup.isDuplicate(packet.id) {
    // Already seen – skip forwarding
} else {
    // Forward the packet to neighbors
}

Summary

  • The LRU seen-set prevents duplicate processing during BitChat's message flooding by tracking recently seen packet IDs and content hashes.
  • MessageDeduplicationService handles application-layer deduplication for content and Nostr events using three specialized LRU caches.
  • MessageDeduplicator provides the underlying thread-safe LRU implementation used by the BLE networking layer, with O(1) lookup and automatic eviction.
  • Configuration parameters in TransportConfig (such as contentLRUCap and messageDedupMaxAgeSeconds) bound memory usage to safe limits.
  • Together, these components eliminate duplicate deliveries, prevent replay loops, and maintain efficient mesh propagation.

Frequently Asked Questions

How does the LRU seen-set prevent replay attacks in BitChat?

The seen-set prevents replay attacks by maintaining a historical record of processed Nostr event IDs and message content hashes. When a packet arrives, the system checks against this set using contains(_:) or isDuplicate(). If the identifier exists, the packet is discarded immediately, ensuring old messages cannot be rebroadcast maliciously through the mesh.

What happens when the LRU cache reaches its capacity limit?

When the cache reaches its configured capacity (e.g., contentLRUCap = 2000), the LRU eviction policy removes the least-recently-used entries to make room for new ones. In MessageDeduplicator, this works alongside time-based eviction (maxAge), ensuring the cache always contains the most relevant recent traffic while maintaining strict memory bounds suitable for mobile devices.

How does MessageDeduplicator differ from MessageDeduplicationService?

MessageDeduplicator is a low-level utility class used directly by the BLE networking layer for raw packet deduplication, while MessageDeduplicationService is a higher-level service that manages three separate caches for application-specific data (content, Nostr events, and ACKs). The service acts as a facade with specialized methods like hasProcessedNostrEvent(), whereas the deduplicator provides generic isDuplicate() functionality with explicit timestamp tracking.

What is the default expiration time for entries in the seen-set?

According to TransportConfig, the default expiration for message deduplication entries is controlled by messageDedupMaxAgeSeconds, typically set to 5 minutes. This duration covers the maximum expected lifetime of a flooding packet across the mesh while ensuring stale entries do not accumulate in memory. Content-specific caches may have separate capacity limits but follow similar aging strategies.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →