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>
This commit is contained in:
jack
2026-07-06 20:02:12 +02:00
co-authored by Claude Fable 5
parent 7341696280
commit 18dcc747d7
17 changed files with 966 additions and 34 deletions
@@ -0,0 +1,98 @@
//
// BLESourceRouteFailureCacheTests.swift
// bitchatTests
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import Testing
import Foundation
import BitFoundation
@testable import bitchat
struct BLESourceRouteFailureCacheTests {
private let recipient = PeerID(str: "0102030405060708")
private let config = BLESourceRouteFailureCache.Config(
confirmationWindowSeconds: 10,
suppressionSeconds: 60
)
private func attempts(_ cache: inout BLESourceRouteFailureCache, at date: Date) -> Bool {
cache.shouldAttemptRoute(to: recipient, now: date)
}
@Test func allowsRoutingByDefault() {
var cache = BLESourceRouteFailureCache(config: config)
#expect(attempts(&cache, at: Date()))
}
@Test func unconfirmedRoutedSendSuppressesRouting() {
var cache = BLESourceRouteFailureCache(config: config)
let t0 = Date()
cache.noteRoutedSend(to: recipient, now: t0)
// Inside the confirmation window: keep routing.
#expect(attempts(&cache, at: t0.addingTimeInterval(5)))
// Past the window with no inbound traffic: route failed, flood.
#expect(!attempts(&cache, at: t0.addingTimeInterval(11)))
// Still suppressed for the suppression TTL.
#expect(!attempts(&cache, at: t0.addingTimeInterval(40)))
// Suppression lapses: routing may be attempted again.
#expect(attempts(&cache, at: t0.addingTimeInterval(11 + 61)))
}
@Test func inboundActivityConfirmsPendingSend() {
var cache = BLESourceRouteFailureCache(config: config)
let t0 = Date()
cache.noteRoutedSend(to: recipient, now: t0)
cache.noteInboundActivity(from: recipient)
// Confirmed: no suppression even long after the window.
#expect(attempts(&cache, at: t0.addingTimeInterval(30)))
}
@Test func inboundActivityDoesNotLiftActiveSuppression() {
var cache = BLESourceRouteFailureCache(config: config)
let t0 = Date()
cache.noteRoutedSend(to: recipient, now: t0)
// Trip the failure suppression starts at t0+15.
#expect(!attempts(&cache, at: t0.addingTimeInterval(15)))
// Inbound traffic may have arrived via flood; suppression holds.
cache.noteInboundActivity(from: recipient)
#expect(!attempts(&cache, at: t0.addingTimeInterval(20)))
#expect(attempts(&cache, at: t0.addingTimeInterval(15 + 61)))
}
@Test func backToBackSendsShareOneDeadline() {
var cache = BLESourceRouteFailureCache(config: config)
let t0 = Date()
cache.noteRoutedSend(to: recipient, now: t0)
cache.noteRoutedSend(to: recipient, now: t0.addingTimeInterval(8))
// Deadline runs from the first unconfirmed send.
#expect(!attempts(&cache, at: t0.addingTimeInterval(11)))
}
@Test func pruneDropsExpiredEntries() {
var cache = BLESourceRouteFailureCache(config: config)
let t0 = Date()
cache.noteRoutedSend(to: recipient, now: t0)
// Past confirmation + suppression: the entry can no longer matter.
cache.prune(now: t0.addingTimeInterval(75))
#expect(attempts(&cache, at: t0.addingTimeInterval(76)))
}
@Test func pruneKeepsEntriesThatStillMatter() {
var cache = BLESourceRouteFailureCache(config: config)
let t0 = Date()
cache.noteRoutedSend(to: recipient, now: t0)
cache.prune(now: t0.addingTimeInterval(30))
// The unconverted pending entry survives pruning and still converts
// into a suppression on the next routing decision.
#expect(!attempts(&cache, at: t0.addingTimeInterval(31)))
}
}