RequestSyncManager in Bitchat: How Targeted Fragment Recovery Works
The RequestSyncManager is a Swift component that tracks outgoing sync requests and validates incoming Request-Sync-Response (RSR) packets, enabling precise recovery of specific fragment streams without waiting for periodic gossip rounds.
In the permissionless Bitchat protocol, the RequestSyncManager (bitchat/Sync/RequestSyncManager.swift) solves a critical problem: how to request missing data from specific peers without flooding the network or accepting unsolicited responses. This article explains its core mechanisms and how it powers targeted fragment recovery when message streams stall.
Core Responsibilities of the RequestSyncManager
The manager maintains lightweight, thread-safe state across three primary duties.
Tracking Outgoing Sync Requests
When the sync layer sends a REQUEST_SYNC packet to a specific peer, that solicitation must be recorded. The registerRequest(to:) method stores a mapping of peerID → timestamp in an internal dictionary.
// RequestSyncManager.swift L32-L40
func registerRequest(to peerID: PeerID) {
pendingRequests[peerID] = DispatchTime.now()
}
This registration creates the attribution that later distinguishes legitimate responses from unsolicited traffic.
Validating Incoming RSR Packets
Packets carrying the IS_RSR flag bypass the normal 2-minute timestamp freshness window—but only if they match a pending request. The isValidResponse(from:isRSR:) method enforces this:
// RequestSyncManager.swift L42-L66
func isValidResponse(from peerID: PeerID, isRSR: Bool) -> Bool {
guard isRSR else { return false }
guard let requestTime = pendingRequests[peerID] else { return false }
let elapsed = DispatchTime.now().uptimeNanoseconds - requestTime.uptimeNanoseconds
return elapsed < responseWindowNanos // default 30 seconds
}
If validation fails, the packet is dropped before processing.
Cleaning Up Expired Requests
Stale entries accumulate memory and create attack surface. The cleanup() method filters out requests older than the configured response window:
// RequestSyncManager.swift L68-L81
func cleanup() {
let now = DispatchTime.now()
pendingRequests = pendingRequests.filter { _, time in
now.uptimeNanoseconds - time.uptimeNanoseconds < responseWindowNanos
}
}
How RequestSyncManager Integrates with Sync Flow
The manager sits at the intersection of three operational stages: request generation, security validation, and response marking.
Outgoing REQUEST_SYNC Registration
Before any unicast sync packet is transmitted, GossipSyncManager registers it:
// GossipSyncManager.swift L60-L63
func sendRequestSync(to peerID: PeerID, types: SyncTypeFlags, fragmentIdFilter: String? = nil) {
requestSyncManager.registerRequest(to: peerID)
// ... packet construction and transmission
}
This marks the forthcoming response as solicited rather than spontaneous gossip.
Security Layer Validation
BLEService acts as the gatekeeper for all incoming packets. Its logic branches on the RSR flag:
// Conceptual flow per REQUEST_SYNC_MANAGER.md L42-L46
func handleIncoming(_ packet: BitchatPacket, from peerID: PeerID) {
if packet.isRSR {
// RSR packets: validate against tracked requests, skip timestamp check
guard requestSyncManager.isValidResponse(from: peerID, isRSR: true) else { return }
} else {
// Normal gossip: enforce 2-minute freshness window
guard abs(now - packet.timestamp) < 120 else { return }
}
}
Response Marking
When constructing replies to REQUEST_SYNC, the sync layer explicitly marks packets:
// GossipSyncManager.swift L19-L25, L33-L36
var responsePacket = BitchatPacket(...)
responsePacket.isRSR = true // Receiver will validate against pending request
responsePacket.ttl = 0 // Prevents further gossip propagation
These markings ensure the receiver can match responses to their original requests while containing response spread.
Targeted Fragment Recovery Mechanism
Broadcast fragments are normally reassembled from promiscuously received traffic. When a fragment stream stalls—detected when no new fragment arrives for approximately 5 seconds—the reassembler triggers targeted recovery.
Building the Fragment Filter
The sync manager creates a fragmentIdFilter containing comma-separated 16-character hex stream IDs:
// Called when fragment assembly stalls
func requestMissingFragments(fragmentIDs: [Data]) {
let filter = RequestSyncPacket.encodeFragmentIdFilter(fragmentIDs)
// filter example: "a3f9b2c8d1e5f7a0,b4c1d2e3f4a5b6c7"
for peer in delegate?.getConnectedPeers() ?? [] {
sendRequestSync(to: peer, types: .fragment, fragmentIdFilter: filter)
}
}
Filter Processing on the Responder
Receiving peers decode the filter and restrict their response:
// RequestSyncPacket.decodeFragmentIdFilter extracts stream IDs
// GossipSyncManager.swift L39-L45, L48-L60
func handleRequestSync(_ request: BitchatPacket, from peerID: PeerID) {
let streamIDs = request.decodeFragmentIdFilter()
for fragment in localFragments {
let fragmentStreamID = fragment.payload.prefix(8) // First 8 bytes = stream ID
guard streamIDs.contains(fragmentStreamID) else { continue }
// Send matching fragment with isRSR=true, ignoring sinceTimestamp cursor
sendRSR(fragment, to: peerID)
}
}
Why This Works
- Attribution:
RequestSyncManagerverifies that RSR packets come from requested peers - Precision:
fragmentIdFilterlimits responses to exactly the stalled streams - Freshness exemption: RSR packets bypass
sinceTimestampcursors, filling gaps without historical constraints
The result: stalled message streams recover in seconds rather than waiting for the next periodic GCS (Galois/Counter Mode Synchronization) round.
Practical Implementation Example
Here's how the complete flow coordinates between components:
// FragmentReassembler detects stall and calls:
GossipSyncManager.shared.requestMissingFragments(fragmentIDs: stalledIDs)
// Inside GossipSyncManager.swift L41-L56
func requestMissingFragments(_ fragmentIDs: [Data]) {
let filter = RequestSyncPacket.encodeFragmentIdFilter(fragmentIDs)
for peer in connectedPeers {
// Registers request for validation
requestSyncManager.registerRequest(to: peer.id)
// Builds targeted REQUEST_SYNC
let packet = buildRequestSync(types: .fragment, fragmentIdFilter: filter)
send(packet, to: peer)
}
}
// BLEService validates the response
func receivePacket(_ packet: BitchatPacket, from peer: PeerID) {
if packet.isRSR {
// Only passes if we requested from this peer
guard requestSyncManager.isValidResponse(from: peer, isRSR: true) else {
logger.warning("Dropping unsolicited RSR from \(peer)")
return
}
processRSR(packet)
}
}
Summary
RequestSyncManagermaintains thread-safe state mapping peer IDs to request timestamps- RSR validation (
isValidResponse) prevents unsolicited responses from entering the sync pipeline - Targeted fragment recovery combines the manager's attribution with
fragmentIdFilterto request specific stalled streams - Security integration in
BLEServiceapplies different validation rules for solicited (RSR) versus spontaneous traffic - Cleanup logic bounds memory usage and prevents replay attacks beyond the 30-second response window
Frequently Asked Questions
What happens if an RSR packet arrives without a matching registered request?
The isValidResponse method returns false, causing BLEService to drop the packet. This prevents unsolicited data injection and replay attacks using old RSR packets.
How does the fragment ID filter format work?
Stream IDs are 8-byte (16 hex character) values. The encodeFragmentIdFilter method joins multiple IDs with commas into a UTF-8 string. The responder's decodeFragmentIdFilter parses this string and matches against the first 8 bytes of each fragment payload.
Can the response window be adjusted for different network conditions?
Yes. The responseWindowNanos property is configurable at initialization. The default 30 seconds balances latency tolerance against memory usage and attack surface, but slower networks could extend this value.
Does targeted fragment recovery work across multiple hops?
No. RSR packets are marked with ttl = 0, making them unicast-only. Targeted recovery requires direct peer connections, which is intentional—fragment streams are reassembled from direct broadcast traffic, and stall detection operates on the local assembly state.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →