mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 18:05:19 +00:00
source routing v2
This commit is contained in:
@@ -17,7 +17,7 @@ final class BLEService: NSObject {
|
||||
// MARK: - Constants
|
||||
|
||||
#if DEBUG
|
||||
static let serviceUUID = CBUUID(string: "F47B5E2D-4A9E-4C5A-9B3F-8E1D2C3A4B5C") // testnet
|
||||
static let serviceUUID = CBUUID(string: "F47B5E2D-4A9E-4C5A-9B3F-8E1D2C3A4B5A") // testnet
|
||||
#else
|
||||
static let serviceUUID = CBUUID(string: "F47B5E2D-4A9E-4C5A-9B3F-8E1D2C3A4B5C") // mainnet
|
||||
#endif
|
||||
@@ -1758,8 +1758,9 @@ func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeriph
|
||||
peers[peerID] = info
|
||||
}
|
||||
}
|
||||
clearDirectLink(with: peerID)
|
||||
refreshLocalTopology()
|
||||
}
|
||||
|
||||
|
||||
// Restart scanning with allow duplicates for faster rediscovery
|
||||
if centralManager?.state == .poweredOn {
|
||||
@@ -2046,7 +2047,7 @@ extension BLEService: CBPeripheralDelegate {
|
||||
peripherals[peripheralUUID] = state
|
||||
}
|
||||
peerToPeripheralUUID[senderID] = peripheralUUID
|
||||
registerDirectLink(with: senderID)
|
||||
refreshLocalTopology()
|
||||
}
|
||||
|
||||
let msgID = makeMessageID(for: packet)
|
||||
@@ -2213,7 +2214,7 @@ extension BLEService: CBPeripheralManagerDelegate {
|
||||
|
||||
// Clean up mappings
|
||||
centralToPeerID.removeValue(forKey: centralUUID)
|
||||
clearDirectLink(with: peerID)
|
||||
refreshLocalTopology()
|
||||
|
||||
// Update UI immediately
|
||||
notifyUI { [weak self] in
|
||||
@@ -2332,7 +2333,7 @@ extension BLEService: CBPeripheralManagerDelegate {
|
||||
if packet.type == MessageType.announce.rawValue {
|
||||
if packet.ttl == messageTTL {
|
||||
centralToPeerID[centralUUID] = senderID
|
||||
registerDirectLink(with: senderID)
|
||||
refreshLocalTopology()
|
||||
}
|
||||
// Record ingress link for last-hop suppression then process
|
||||
let msgID = makeMessageID(for: packet)
|
||||
@@ -2447,17 +2448,11 @@ extension BLEService {
|
||||
peerID.toShort().routingData
|
||||
}
|
||||
|
||||
private func registerDirectLink(with peerID: PeerID) {
|
||||
meshTopology.recordDirectLink(between: myPeerIDData, and: routingData(for: peerID))
|
||||
}
|
||||
|
||||
private func clearDirectLink(with peerID: PeerID) {
|
||||
meshTopology.removeDirectLink(between: myPeerIDData, and: routingData(for: peerID))
|
||||
}
|
||||
|
||||
private func registerRoute(_ route: [Data]?) {
|
||||
guard let hops = route, !hops.isEmpty else { return }
|
||||
meshTopology.recordRoute(hops)
|
||||
private func refreshLocalTopology() {
|
||||
let neighbors: [Data] = collectionsQueue.sync {
|
||||
peers.values.filter { $0.isConnected }.compactMap { $0.peerID.routingData }
|
||||
}
|
||||
meshTopology.updateNeighbors(for: myPeerIDData, neighbors: neighbors)
|
||||
}
|
||||
|
||||
private func computeRoute(to peerID: PeerID) -> [Data]? {
|
||||
@@ -2468,7 +2463,6 @@ extension BLEService {
|
||||
guard let route = computeRoute(to: recipient), route.count >= 1 else {
|
||||
return packet
|
||||
}
|
||||
meshTopology.recordRoute(route)
|
||||
// Create new packet with route applied and version upgraded to 2
|
||||
let routedPacket = BitchatPacket(
|
||||
type: packet.type,
|
||||
@@ -2508,7 +2502,6 @@ extension BLEService {
|
||||
isPeerConnected(nextPeer) else {
|
||||
return false
|
||||
}
|
||||
registerDirectLink(with: nextPeer)
|
||||
var relayPacket = packet
|
||||
relayPacket.ttl = packet.ttl - 1
|
||||
sendPacketDirected(relayPacket, to: nextPeer)
|
||||
@@ -2523,7 +2516,6 @@ extension BLEService {
|
||||
isPeerConnected(destinationPeer) else {
|
||||
return false
|
||||
}
|
||||
registerDirectLink(with: destinationPeer)
|
||||
var relayPacket = packet
|
||||
relayPacket.ttl = packet.ttl - 1
|
||||
sendPacketDirected(relayPacket, to: destinationPeer)
|
||||
@@ -2538,7 +2530,6 @@ extension BLEService {
|
||||
return false
|
||||
}
|
||||
|
||||
registerDirectLink(with: nextPeer)
|
||||
var relayPacket = packet
|
||||
relayPacket.ttl = packet.ttl - 1
|
||||
sendPacketDirected(relayPacket, to: nextPeer)
|
||||
@@ -3306,10 +3297,6 @@ extension BLEService {
|
||||
return
|
||||
}
|
||||
|
||||
registerRoute(packet.route)
|
||||
if peerID != myPeerID && packet.ttl == messageTTL {
|
||||
registerDirectLink(with: peerID)
|
||||
}
|
||||
|
||||
// Deduplication (thread-safe)
|
||||
let senderID = PeerID(hexData: packet.senderID)
|
||||
@@ -3439,6 +3426,11 @@ extension BLEService {
|
||||
return
|
||||
}
|
||||
|
||||
// Update topology with their claimed neighbors
|
||||
if let neighbors = announcement.directNeighbors {
|
||||
meshTopology.updateNeighbors(for: peerID.routingData, neighbors: neighbors)
|
||||
}
|
||||
|
||||
// Verify that the sender's derived ID from the announced noise public key matches the packet senderID
|
||||
// This helps detect relayed or spoofed announces. Only warn in release; assert in debug.
|
||||
let derivedFromKey = PeerID(publicKey: announcement.noisePublicKey)
|
||||
@@ -4037,6 +4029,11 @@ extension BLEService {
|
||||
self.delegate?.didUpdatePeerList(currentPeerIDs)
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh local topology to keep our own entry fresh and sync any changes
|
||||
refreshLocalTopology()
|
||||
// Prune stale topology nodes (using safe retention window)
|
||||
meshTopology.prune(olderThan: 60.0)
|
||||
}
|
||||
|
||||
private func performCleanup() {
|
||||
|
||||
@@ -6,70 +6,46 @@ final class MeshTopologyTracker {
|
||||
|
||||
private let queue = DispatchQueue(label: "mesh.topology", attributes: .concurrent)
|
||||
private let hopSize = 8
|
||||
private var adjacency: [RoutingID: Set<RoutingID>] = [:]
|
||||
// Directed claims: Key claims to see Value (neighbors)
|
||||
private var claims: [RoutingID: Set<RoutingID>] = [:]
|
||||
// Last time we received an update from a node
|
||||
private var lastSeen: [RoutingID: Date] = [:]
|
||||
|
||||
func reset() {
|
||||
queue.sync(flags: .barrier) {
|
||||
self.adjacency.removeAll()
|
||||
self.claims.removeAll()
|
||||
self.lastSeen.removeAll()
|
||||
}
|
||||
}
|
||||
|
||||
func recordDirectLink(between a: Data?, and b: Data?) {
|
||||
guard let left = sanitize(a), let right = sanitize(b), left != right else { return }
|
||||
/// Update the topology with a node's self-reported neighbor list
|
||||
func updateNeighbors(for sourceData: Data?, neighbors: [Data]) {
|
||||
guard let source = sanitize(sourceData) else { return }
|
||||
// Sanitize neighbors and exclude self-loops
|
||||
let validNeighbors = Set(neighbors.compactMap { sanitize($0) }).subtracting([source])
|
||||
|
||||
queue.sync(flags: .barrier) {
|
||||
var setA = self.adjacency[left] ?? []
|
||||
setA.insert(right)
|
||||
self.adjacency[left] = setA
|
||||
|
||||
var setB = self.adjacency[right] ?? []
|
||||
setB.insert(left)
|
||||
self.adjacency[right] = setB
|
||||
}
|
||||
}
|
||||
|
||||
func removeDirectLink(between a: Data?, and b: Data?) {
|
||||
guard let left = sanitize(a), let right = sanitize(b), left != right else { return }
|
||||
queue.sync(flags: .barrier) {
|
||||
if var setA = self.adjacency[left] {
|
||||
setA.remove(right)
|
||||
self.adjacency[left] = setA.isEmpty ? nil : setA
|
||||
}
|
||||
if var setB = self.adjacency[right] {
|
||||
setB.remove(left)
|
||||
self.adjacency[right] = setB.isEmpty ? nil : setB
|
||||
}
|
||||
self.claims[source] = validNeighbors
|
||||
self.lastSeen[source] = Date()
|
||||
}
|
||||
}
|
||||
|
||||
func removePeer(_ data: Data?) {
|
||||
guard let peer = sanitize(data) else { return }
|
||||
queue.sync(flags: .barrier) {
|
||||
guard let neighbors = self.adjacency.removeValue(forKey: peer) else { return }
|
||||
for neighbor in neighbors {
|
||||
if var set = self.adjacency[neighbor] {
|
||||
set.remove(peer)
|
||||
self.adjacency[neighbor] = set.isEmpty ? nil : set
|
||||
}
|
||||
}
|
||||
self.claims.removeValue(forKey: peer)
|
||||
self.lastSeen.removeValue(forKey: peer)
|
||||
}
|
||||
}
|
||||
|
||||
func recordRoute(_ hops: [Data]) {
|
||||
let sanitized = hops.compactMap { sanitize($0) }
|
||||
guard sanitized.count >= 2 else { return }
|
||||
|
||||
/// Prune nodes that haven't updated their topology in `age` seconds
|
||||
func prune(olderThan age: TimeInterval) {
|
||||
let deadline = Date().addingTimeInterval(-age)
|
||||
queue.sync(flags: .barrier) {
|
||||
for idx in 0..<(sanitized.count - 1) {
|
||||
let left = sanitized[idx]
|
||||
let right = sanitized[idx + 1]
|
||||
guard left != right else { continue }
|
||||
|
||||
var setA = self.adjacency[left] ?? []
|
||||
setA.insert(right)
|
||||
self.adjacency[left] = setA
|
||||
|
||||
var setB = self.adjacency[right] ?? []
|
||||
setB.insert(left)
|
||||
self.adjacency[right] = setB
|
||||
let stale = self.lastSeen.filter { $0.value < deadline }
|
||||
for (peer, _) in stale {
|
||||
self.claims.removeValue(forKey: peer)
|
||||
self.lastSeen.removeValue(forKey: peer)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,36 +54,49 @@ final class MeshTopologyTracker {
|
||||
guard let source = sanitize(start), let target = sanitize(goal) else { return nil }
|
||||
if source == target { return [] } // Direct connection, no intermediate hops
|
||||
|
||||
let graph = queue.sync { adjacency }
|
||||
guard graph[source] != nil, graph[target] != nil else { return nil }
|
||||
return queue.sync {
|
||||
// BFS
|
||||
var visited: Set<RoutingID> = [source]
|
||||
// Queue stores paths: [Start, Hop1, Hop2, ..., Current]
|
||||
var queuePaths: [[RoutingID]] = [[source]]
|
||||
|
||||
while !queuePaths.isEmpty {
|
||||
let path = queuePaths.removeFirst()
|
||||
// Limit path length (path contains source + maxHops + target) -> maxHops intermediate
|
||||
// If maxHops = 10, max edges = 11, max nodes = 12.
|
||||
if path.count > maxHops + 1 { continue }
|
||||
|
||||
guard let last = path.last else { continue }
|
||||
|
||||
// Get neighbors that 'last' claims to see
|
||||
guard let neighbors = claims[last] else { continue }
|
||||
|
||||
var visited: Set<RoutingID> = [source]
|
||||
var queuePaths: [[RoutingID]] = [[source]]
|
||||
var index = 0
|
||||
|
||||
while index < queuePaths.count {
|
||||
let path = queuePaths[index]
|
||||
index += 1
|
||||
guard path.count <= maxHops else { continue }
|
||||
guard let last = path.last, let neighbors = graph[last] else { continue }
|
||||
|
||||
for neighbor in neighbors {
|
||||
if visited.contains(neighbor) { continue }
|
||||
var nextPath = path
|
||||
nextPath.append(neighbor)
|
||||
if neighbor == target {
|
||||
// Remove start and end, return only intermediate hops
|
||||
guard nextPath.count >= 2 else { return [] }
|
||||
return Array(nextPath.dropFirst().dropLast())
|
||||
}
|
||||
if nextPath.count <= maxHops {
|
||||
for neighbor in neighbors {
|
||||
if visited.contains(neighbor) { continue }
|
||||
|
||||
// CONFIRMED EDGE CHECK:
|
||||
// 'last' claims 'neighbor' (checked above)
|
||||
// Does 'neighbor' claim 'last'?
|
||||
guard let neighborClaims = claims[neighbor],
|
||||
neighborClaims.contains(last) else {
|
||||
continue
|
||||
}
|
||||
|
||||
var nextPath = path
|
||||
nextPath.append(neighbor)
|
||||
|
||||
if neighbor == target {
|
||||
// Return only intermediate hops
|
||||
// Path: [Source, I1, I2, Target] -> [I1, I2]
|
||||
return Array(nextPath.dropFirst().dropLast())
|
||||
}
|
||||
|
||||
visited.insert(neighbor)
|
||||
queuePaths.append(nextPath)
|
||||
}
|
||||
visited.insert(neighbor)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
Reference in New Issue
Block a user