Files
bitchat/bitchat/Services/BLE/BLESourceRouteOriginationPolicy.swift
T
70229f0be1 Originate v2 source routes and wire fragmentIdFilter targeted resync (#1378)
* Originate v2 source routes and wire fragmentIdFilter targeted resync

Part A — source-route origination policy:
- Gate route application (BLESourceRouteOriginationPolicy): only packets we
  author, directed at a single peer, with TTL headroom, whose recipient is
  not directly connected. Relays no longer attach routes to (and re-sign)
  packets they merely forward.
- Version-gate paths: MeshTopologyTracker records the highest protocol
  version observed per peer; BFS routes require every intermediate hop and
  the recipient to be v2-observed, capped at 4 intermediate hops.
- Degrade on failure: BLESourceRouteFailureCache marks a routed send that
  sees no inbound traffic from the recipient within 10s as failed and floods
  for 60s before retrying routes.

Part B — REQUEST_SYNC fragmentIdFilter (TLV 0x06):
- Requester: BLEFragmentAssemblyBuffer reports stalled broadcast
  reassemblies (no new fragment for 5s, retried at most every 10s); the
  maintenance pass sends a types=fragment REQUEST_SYNC naming the stalled
  8-byte fragment stream IDs to each connected peer.
- Responder: GossipSyncManager restricts the fragment diff to exactly the
  named streams, bypassing the since-cursor while the GCS filter still
  excludes pieces the requester holds; RSR/TTL-0/rate-limit semantics
  unchanged and REQUEST_SYNC stays link-local.
- Bounds: at most 60 IDs per request (60*17-1 = 1019 bytes <= the 1024-byte
  decoder cap); oversized 0x06 values are ignored, not fatal.

Docs: SOURCE_ROUTING.md gains the iOS origination policy (§8);
REQUEST_SYNC_MANAGER.md documents 0x05/0x06 as implemented.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Fix stall-clock refresh on duplicates and overflow suppression in fragment resync

Two fixes to stalledBroadcastFragmentIDs bookkeeping in
BLEFragmentAssemblyBuffer:

- Duplicate fragments no longer reset the stall clock. Fragment packets
  bypass the packet deduplicator, so relayed duplicates of an
  already-held index arriving every few seconds kept lastFragmentAt
  fresh and suppressed the targeted REQUEST_SYNC indefinitely. Now
  lastFragmentAt only updates when the index is new (actual progress).

- Only the streams that will actually be encoded on the wire are
  rate-limited. Previously every stalled candidate got
  lastResyncRequestAt set, but encodeFragmentIdFilter serializes at most
  RequestSyncPacket.maxFragmentIdFilterCount (60) IDs, so overflow
  streams were suppressed for retryAfter without ever being requested.
  Selection now caps at that shared constant, oldest stall first, so
  overflow stays eligible and rotates fairly on the next pass.

Tests: duplicates arriving periodically still trigger the stall report;
70 stalled streams yield the 60 oldest on the first pass and the
remaining 10 on the next.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: jack <jackjackbits@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 14:42:53 +02:00

41 lines
1.8 KiB
Swift

import BitFoundation
import Foundation
/// Decides whether an outbound directed packet should carry a v2 source
/// route. Pure gating logic so BLEService's hot send path stays a thin wire.
enum BLESourceRouteOriginationPolicy {
/// Returns the intermediate-hop route to attach, or nil to keep the
/// current flood/direct-write behavior unchanged.
///
/// Routes are only originated when every gate passes:
/// - we authored the packet (relays must not rewrite and re-sign someone
/// else's packet; route-following for in-flight routed packets lives in
/// `BLERouteForwardingPolicy`),
/// - the packet is directed at a single peer (not broadcast),
/// - the packet has TTL headroom to traverse hops (link-local TTL-0
/// packets like REQUEST_SYNC never route),
/// - the recipient is not directly connected (a direct write already
/// delivers in one hop),
/// - routing to the recipient is not suppressed by a recent unconfirmed
/// routed send, and
/// - the topology yields a complete path.
static func route(
for packet: BitchatPacket,
to recipient: PeerID,
localPeerIDData: Data,
isRecipientConnected: (PeerID) -> Bool,
shouldAttemptRoute: (PeerID) -> Bool,
computeRoute: (PeerID) -> [Data]?
) -> [Data]? {
guard packet.senderID == localPeerIDData else { return nil }
guard let recipientData = packet.recipientID,
recipientData.count == 8,
!recipientData.allSatisfy({ $0 == 0xFF }) else { return nil }
guard packet.ttl > 1 else { return nil }
guard !isRecipientConnected(recipient) else { return nil }
guard shouldAttemptRoute(recipient) else { return nil }
guard let route = computeRoute(recipient), !route.isEmpty else { return nil }
return route
}
}