mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-24 23:25:19 +00:00
source routing v2
This commit is contained in:
@@ -17,7 +17,7 @@ final class BLEService: NSObject {
|
|||||||
// MARK: - Constants
|
// MARK: - Constants
|
||||||
|
|
||||||
#if DEBUG
|
#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
|
#else
|
||||||
static let serviceUUID = CBUUID(string: "F47B5E2D-4A9E-4C5A-9B3F-8E1D2C3A4B5C") // mainnet
|
static let serviceUUID = CBUUID(string: "F47B5E2D-4A9E-4C5A-9B3F-8E1D2C3A4B5C") // mainnet
|
||||||
#endif
|
#endif
|
||||||
@@ -1758,8 +1758,9 @@ func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeriph
|
|||||||
peers[peerID] = info
|
peers[peerID] = info
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
clearDirectLink(with: peerID)
|
refreshLocalTopology()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Restart scanning with allow duplicates for faster rediscovery
|
// Restart scanning with allow duplicates for faster rediscovery
|
||||||
if centralManager?.state == .poweredOn {
|
if centralManager?.state == .poweredOn {
|
||||||
@@ -2046,7 +2047,7 @@ extension BLEService: CBPeripheralDelegate {
|
|||||||
peripherals[peripheralUUID] = state
|
peripherals[peripheralUUID] = state
|
||||||
}
|
}
|
||||||
peerToPeripheralUUID[senderID] = peripheralUUID
|
peerToPeripheralUUID[senderID] = peripheralUUID
|
||||||
registerDirectLink(with: senderID)
|
refreshLocalTopology()
|
||||||
}
|
}
|
||||||
|
|
||||||
let msgID = makeMessageID(for: packet)
|
let msgID = makeMessageID(for: packet)
|
||||||
@@ -2213,7 +2214,7 @@ extension BLEService: CBPeripheralManagerDelegate {
|
|||||||
|
|
||||||
// Clean up mappings
|
// Clean up mappings
|
||||||
centralToPeerID.removeValue(forKey: centralUUID)
|
centralToPeerID.removeValue(forKey: centralUUID)
|
||||||
clearDirectLink(with: peerID)
|
refreshLocalTopology()
|
||||||
|
|
||||||
// Update UI immediately
|
// Update UI immediately
|
||||||
notifyUI { [weak self] in
|
notifyUI { [weak self] in
|
||||||
@@ -2332,7 +2333,7 @@ extension BLEService: CBPeripheralManagerDelegate {
|
|||||||
if packet.type == MessageType.announce.rawValue {
|
if packet.type == MessageType.announce.rawValue {
|
||||||
if packet.ttl == messageTTL {
|
if packet.ttl == messageTTL {
|
||||||
centralToPeerID[centralUUID] = senderID
|
centralToPeerID[centralUUID] = senderID
|
||||||
registerDirectLink(with: senderID)
|
refreshLocalTopology()
|
||||||
}
|
}
|
||||||
// Record ingress link for last-hop suppression then process
|
// Record ingress link for last-hop suppression then process
|
||||||
let msgID = makeMessageID(for: packet)
|
let msgID = makeMessageID(for: packet)
|
||||||
@@ -2447,17 +2448,11 @@ extension BLEService {
|
|||||||
peerID.toShort().routingData
|
peerID.toShort().routingData
|
||||||
}
|
}
|
||||||
|
|
||||||
private func registerDirectLink(with peerID: PeerID) {
|
private func refreshLocalTopology() {
|
||||||
meshTopology.recordDirectLink(between: myPeerIDData, and: routingData(for: peerID))
|
let neighbors: [Data] = collectionsQueue.sync {
|
||||||
}
|
peers.values.filter { $0.isConnected }.compactMap { $0.peerID.routingData }
|
||||||
|
}
|
||||||
private func clearDirectLink(with peerID: PeerID) {
|
meshTopology.updateNeighbors(for: myPeerIDData, neighbors: neighbors)
|
||||||
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 computeRoute(to peerID: PeerID) -> [Data]? {
|
private func computeRoute(to peerID: PeerID) -> [Data]? {
|
||||||
@@ -2468,7 +2463,6 @@ extension BLEService {
|
|||||||
guard let route = computeRoute(to: recipient), route.count >= 1 else {
|
guard let route = computeRoute(to: recipient), route.count >= 1 else {
|
||||||
return packet
|
return packet
|
||||||
}
|
}
|
||||||
meshTopology.recordRoute(route)
|
|
||||||
// Create new packet with route applied and version upgraded to 2
|
// Create new packet with route applied and version upgraded to 2
|
||||||
let routedPacket = BitchatPacket(
|
let routedPacket = BitchatPacket(
|
||||||
type: packet.type,
|
type: packet.type,
|
||||||
@@ -2508,7 +2502,6 @@ extension BLEService {
|
|||||||
isPeerConnected(nextPeer) else {
|
isPeerConnected(nextPeer) else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
registerDirectLink(with: nextPeer)
|
|
||||||
var relayPacket = packet
|
var relayPacket = packet
|
||||||
relayPacket.ttl = packet.ttl - 1
|
relayPacket.ttl = packet.ttl - 1
|
||||||
sendPacketDirected(relayPacket, to: nextPeer)
|
sendPacketDirected(relayPacket, to: nextPeer)
|
||||||
@@ -2523,7 +2516,6 @@ extension BLEService {
|
|||||||
isPeerConnected(destinationPeer) else {
|
isPeerConnected(destinationPeer) else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
registerDirectLink(with: destinationPeer)
|
|
||||||
var relayPacket = packet
|
var relayPacket = packet
|
||||||
relayPacket.ttl = packet.ttl - 1
|
relayPacket.ttl = packet.ttl - 1
|
||||||
sendPacketDirected(relayPacket, to: destinationPeer)
|
sendPacketDirected(relayPacket, to: destinationPeer)
|
||||||
@@ -2538,7 +2530,6 @@ extension BLEService {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
registerDirectLink(with: nextPeer)
|
|
||||||
var relayPacket = packet
|
var relayPacket = packet
|
||||||
relayPacket.ttl = packet.ttl - 1
|
relayPacket.ttl = packet.ttl - 1
|
||||||
sendPacketDirected(relayPacket, to: nextPeer)
|
sendPacketDirected(relayPacket, to: nextPeer)
|
||||||
@@ -3306,10 +3297,6 @@ extension BLEService {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
registerRoute(packet.route)
|
|
||||||
if peerID != myPeerID && packet.ttl == messageTTL {
|
|
||||||
registerDirectLink(with: peerID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deduplication (thread-safe)
|
// Deduplication (thread-safe)
|
||||||
let senderID = PeerID(hexData: packet.senderID)
|
let senderID = PeerID(hexData: packet.senderID)
|
||||||
@@ -3439,6 +3426,11 @@ extension BLEService {
|
|||||||
return
|
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
|
// 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.
|
// This helps detect relayed or spoofed announces. Only warn in release; assert in debug.
|
||||||
let derivedFromKey = PeerID(publicKey: announcement.noisePublicKey)
|
let derivedFromKey = PeerID(publicKey: announcement.noisePublicKey)
|
||||||
@@ -4037,6 +4029,11 @@ extension BLEService {
|
|||||||
self.delegate?.didUpdatePeerList(currentPeerIDs)
|
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() {
|
private func performCleanup() {
|
||||||
|
|||||||
@@ -6,70 +6,46 @@ final class MeshTopologyTracker {
|
|||||||
|
|
||||||
private let queue = DispatchQueue(label: "mesh.topology", attributes: .concurrent)
|
private let queue = DispatchQueue(label: "mesh.topology", attributes: .concurrent)
|
||||||
private let hopSize = 8
|
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() {
|
func reset() {
|
||||||
queue.sync(flags: .barrier) {
|
queue.sync(flags: .barrier) {
|
||||||
self.adjacency.removeAll()
|
self.claims.removeAll()
|
||||||
|
self.lastSeen.removeAll()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func recordDirectLink(between a: Data?, and b: Data?) {
|
/// Update the topology with a node's self-reported neighbor list
|
||||||
guard let left = sanitize(a), let right = sanitize(b), left != right else { return }
|
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) {
|
queue.sync(flags: .barrier) {
|
||||||
var setA = self.adjacency[left] ?? []
|
self.claims[source] = validNeighbors
|
||||||
setA.insert(right)
|
self.lastSeen[source] = Date()
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func removePeer(_ data: Data?) {
|
func removePeer(_ data: Data?) {
|
||||||
guard let peer = sanitize(data) else { return }
|
guard let peer = sanitize(data) else { return }
|
||||||
queue.sync(flags: .barrier) {
|
queue.sync(flags: .barrier) {
|
||||||
guard let neighbors = self.adjacency.removeValue(forKey: peer) else { return }
|
self.claims.removeValue(forKey: peer)
|
||||||
for neighbor in neighbors {
|
self.lastSeen.removeValue(forKey: peer)
|
||||||
if var set = self.adjacency[neighbor] {
|
|
||||||
set.remove(peer)
|
|
||||||
self.adjacency[neighbor] = set.isEmpty ? nil : set
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func recordRoute(_ hops: [Data]) {
|
/// Prune nodes that haven't updated their topology in `age` seconds
|
||||||
let sanitized = hops.compactMap { sanitize($0) }
|
func prune(olderThan age: TimeInterval) {
|
||||||
guard sanitized.count >= 2 else { return }
|
let deadline = Date().addingTimeInterval(-age)
|
||||||
queue.sync(flags: .barrier) {
|
queue.sync(flags: .barrier) {
|
||||||
for idx in 0..<(sanitized.count - 1) {
|
let stale = self.lastSeen.filter { $0.value < deadline }
|
||||||
let left = sanitized[idx]
|
for (peer, _) in stale {
|
||||||
let right = sanitized[idx + 1]
|
self.claims.removeValue(forKey: peer)
|
||||||
guard left != right else { continue }
|
self.lastSeen.removeValue(forKey: peer)
|
||||||
|
|
||||||
var setA = self.adjacency[left] ?? []
|
|
||||||
setA.insert(right)
|
|
||||||
self.adjacency[left] = setA
|
|
||||||
|
|
||||||
var setB = self.adjacency[right] ?? []
|
|
||||||
setB.insert(left)
|
|
||||||
self.adjacency[right] = setB
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -78,36 +54,49 @@ final class MeshTopologyTracker {
|
|||||||
guard let source = sanitize(start), let target = sanitize(goal) else { return nil }
|
guard let source = sanitize(start), let target = sanitize(goal) else { return nil }
|
||||||
if source == target { return [] } // Direct connection, no intermediate hops
|
if source == target { return [] } // Direct connection, no intermediate hops
|
||||||
|
|
||||||
let graph = queue.sync { adjacency }
|
return queue.sync {
|
||||||
guard graph[source] != nil, graph[target] != nil else { return nil }
|
// 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]
|
for neighbor in neighbors {
|
||||||
var queuePaths: [[RoutingID]] = [[source]]
|
if visited.contains(neighbor) { continue }
|
||||||
var index = 0
|
|
||||||
|
// CONFIRMED EDGE CHECK:
|
||||||
while index < queuePaths.count {
|
// 'last' claims 'neighbor' (checked above)
|
||||||
let path = queuePaths[index]
|
// Does 'neighbor' claim 'last'?
|
||||||
index += 1
|
guard let neighborClaims = claims[neighbor],
|
||||||
guard path.count <= maxHops else { continue }
|
neighborClaims.contains(last) else {
|
||||||
guard let last = path.last, let neighbors = graph[last] else { continue }
|
continue
|
||||||
|
}
|
||||||
for neighbor in neighbors {
|
|
||||||
if visited.contains(neighbor) { continue }
|
var nextPath = path
|
||||||
var nextPath = path
|
nextPath.append(neighbor)
|
||||||
nextPath.append(neighbor)
|
|
||||||
if neighbor == target {
|
if neighbor == target {
|
||||||
// Remove start and end, return only intermediate hops
|
// Return only intermediate hops
|
||||||
guard nextPath.count >= 2 else { return [] }
|
// Path: [Source, I1, I2, Target] -> [I1, I2]
|
||||||
return Array(nextPath.dropFirst().dropLast())
|
return Array(nextPath.dropFirst().dropLast())
|
||||||
}
|
}
|
||||||
if nextPath.count <= maxHops {
|
|
||||||
|
visited.insert(neighbor)
|
||||||
queuePaths.append(nextPath)
|
queuePaths.append(nextPath)
|
||||||
}
|
}
|
||||||
visited.insert(neighbor)
|
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Helpers
|
// MARK: - Helpers
|
||||||
|
|||||||
@@ -20,7 +20,10 @@ struct MeshTopologyTrackerTests {
|
|||||||
let a = try hex("0102030405060708")
|
let a = try hex("0102030405060708")
|
||||||
let b = try hex("1112131415161718")
|
let b = try hex("1112131415161718")
|
||||||
|
|
||||||
tracker.recordDirectLink(between: a, and: b)
|
// Bidirectional announcement
|
||||||
|
tracker.updateNeighbors(for: a, neighbors: [b])
|
||||||
|
tracker.updateNeighbors(for: b, neighbors: [a])
|
||||||
|
|
||||||
let route = try #require(tracker.computeRoute(from: a, to: b))
|
let route = try #require(tracker.computeRoute(from: a, to: b))
|
||||||
// Direct connection returns empty route (no intermediate hops)
|
// Direct connection returns empty route (no intermediate hops)
|
||||||
#expect(route == [])
|
#expect(route == [])
|
||||||
@@ -33,43 +36,38 @@ struct MeshTopologyTrackerTests {
|
|||||||
let c = try hex("2021222324252627")
|
let c = try hex("2021222324252627")
|
||||||
let d = try hex("3031323334353637")
|
let d = try hex("3031323334353637")
|
||||||
|
|
||||||
tracker.recordDirectLink(between: a, and: b)
|
// Bidirectional announcements for A-B, B-C, C-D
|
||||||
tracker.recordDirectLink(between: b, and: c)
|
tracker.updateNeighbors(for: a, neighbors: [b])
|
||||||
tracker.recordDirectLink(between: c, and: d)
|
tracker.updateNeighbors(for: b, neighbors: [a, c])
|
||||||
|
tracker.updateNeighbors(for: c, neighbors: [b, d])
|
||||||
|
tracker.updateNeighbors(for: d, neighbors: [c])
|
||||||
|
|
||||||
let route = try #require(tracker.computeRoute(from: a, to: d))
|
let route = try #require(tracker.computeRoute(from: a, to: d))
|
||||||
// Route should only contain intermediate hops (b, c), excluding start (a) and end (d)
|
// Route should only contain intermediate hops (b, c), excluding start (a) and end (d)
|
||||||
#expect(route == [b, c])
|
#expect(route == [b, c])
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func recordRouteAddsEdges() throws {
|
@Test func unconfirmedEdgeDoesNotRoute() throws {
|
||||||
let tracker = MeshTopologyTracker()
|
|
||||||
var a = Data([0xAA, 0xBB, 0xCC])
|
|
||||||
let b = try hex("4445464748494A4B")
|
|
||||||
let c = try hex("5455565758595A5B")
|
|
||||||
|
|
||||||
tracker.recordRoute([a, b, c])
|
|
||||||
|
|
||||||
a.append(Data(repeating: 0, count: BinaryProtocol.senderIDSize - a.count))
|
|
||||||
let route = try #require(tracker.computeRoute(from: a, to: c))
|
|
||||||
// Route should only contain intermediate hops (b), excluding start (a) and end (c)
|
|
||||||
#expect(route == [b])
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test func removingDirectLinkBreaksRoute() throws {
|
|
||||||
let tracker = MeshTopologyTracker()
|
let tracker = MeshTopologyTracker()
|
||||||
let a = try hex("0101010101010101")
|
let a = try hex("0101010101010101")
|
||||||
let b = try hex("0202020202020202")
|
let b = try hex("0202020202020202")
|
||||||
let c = try hex("0303030303030303")
|
let c = try hex("0303030303030303")
|
||||||
|
|
||||||
tracker.recordDirectLink(between: a, and: b)
|
// A announces B (confirmed)
|
||||||
tracker.recordDirectLink(between: b, and: c)
|
// B announces A, C (confirmed A-B, unconfirmed B-C)
|
||||||
let initialRoute = try #require(tracker.computeRoute(from: a, to: c))
|
// C does NOT announce B
|
||||||
// Route should only contain intermediate hops (b), excluding start (a) and end (c)
|
tracker.updateNeighbors(for: a, neighbors: [b])
|
||||||
#expect(initialRoute == [b])
|
tracker.updateNeighbors(for: b, neighbors: [a, c])
|
||||||
|
// C is silent or announces empty
|
||||||
|
|
||||||
tracker.removeDirectLink(between: b, and: c)
|
// Should NOT find route A->C because B->C is unconfirmed (C didn't announce B)
|
||||||
#expect(tracker.computeRoute(from: a, to: c) == nil)
|
#expect(tracker.computeRoute(from: a, to: c) == nil)
|
||||||
|
|
||||||
|
// Now C announces B
|
||||||
|
tracker.updateNeighbors(for: c, neighbors: [b])
|
||||||
|
// Should find route
|
||||||
|
let route = try #require(tracker.computeRoute(from: a, to: c))
|
||||||
|
#expect(route == [b])
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func removingPeerClearsEdges() throws {
|
@Test func removingPeerClearsEdges() throws {
|
||||||
@@ -78,9 +76,11 @@ struct MeshTopologyTrackerTests {
|
|||||||
let b = try hex("0A0B0C0D0E0F0001")
|
let b = try hex("0A0B0C0D0E0F0001")
|
||||||
let c = try hex("0011223344556677")
|
let c = try hex("0011223344556677")
|
||||||
|
|
||||||
tracker.recordRoute([a, b, c])
|
tracker.updateNeighbors(for: a, neighbors: [b])
|
||||||
|
tracker.updateNeighbors(for: b, neighbors: [a, c])
|
||||||
|
tracker.updateNeighbors(for: c, neighbors: [b])
|
||||||
|
|
||||||
let initialRoute = try #require(tracker.computeRoute(from: a, to: c))
|
let initialRoute = try #require(tracker.computeRoute(from: a, to: c))
|
||||||
// Route should only contain intermediate hops (b), excluding start (a) and end (c)
|
|
||||||
#expect(initialRoute == [b])
|
#expect(initialRoute == [b])
|
||||||
|
|
||||||
tracker.removePeer(b)
|
tracker.removePeer(b)
|
||||||
@@ -92,7 +92,9 @@ struct MeshTopologyTrackerTests {
|
|||||||
let a = try hex("0102030405060708")
|
let a = try hex("0102030405060708")
|
||||||
let b = try hex("1112131415161718")
|
let b = try hex("1112131415161718")
|
||||||
|
|
||||||
tracker.recordDirectLink(between: a, and: b)
|
tracker.updateNeighbors(for: a, neighbors: [b])
|
||||||
|
tracker.updateNeighbors(for: b, neighbors: [a])
|
||||||
|
|
||||||
// When start == end, route should be empty (no intermediate hops needed)
|
// When start == end, route should be empty (no intermediate hops needed)
|
||||||
let route = try #require(tracker.computeRoute(from: a, to: a))
|
let route = try #require(tracker.computeRoute(from: a, to: a))
|
||||||
#expect(route == [])
|
#expect(route == [])
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
# Source-Based Routing for BitChat Packets (v2)
|
||||||
|
|
||||||
|
This document specifies the Source-Based Routing extension (v2) for the BitChat protocol. This upgrade enables efficient unicast routing across the mesh by allowing senders to specify an explicit path of intermediate relays.
|
||||||
|
|
||||||
|
**Status:** Implemented in Android and iOS. Backward compatible (v1 clients ignore routing data).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Protocol Versioning & Layering
|
||||||
|
|
||||||
|
To support source routing and larger payloads, the packet format has been upgraded to **Version 2**.
|
||||||
|
|
||||||
|
* **Version 1 (Legacy):** 2-byte payload length limit. Ignores routing flags.
|
||||||
|
* **Version 2 (Current):** 4-byte payload length limit. Supports Source Routing.
|
||||||
|
|
||||||
|
**Key Rule:** The `HAS_ROUTE (0x08)` flag is **only valid** if the packet `version >= 2`. Relays receiving a v1 packet must ignore this flag even if set.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Packet Structure Comparison
|
||||||
|
|
||||||
|
The following diagram illustrates the structural differences between a standard v1 packet and a source-routed v2 packet.
|
||||||
|
|
||||||
|
### V1 Packet (Legacy)
|
||||||
|
```text
|
||||||
|
+-------------------+---------------------------------------------------------+
|
||||||
|
| Fixed Header (14) | Variable Sections |
|
||||||
|
+-------------------+----------+-------------+------------------+-------------+
|
||||||
|
| Ver: 1 (1B) | SenderID | RecipientID | Payload | Signature |
|
||||||
|
| Type, TTL, etc. | (8B) | (8B) | (Length in Head) | (64B) |
|
||||||
|
| Len: 2 Bytes | | (Optional) | | (Optional) |
|
||||||
|
+-------------------+----------+-------------+------------------+-------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
### V2 Packet (Source Routed)
|
||||||
|
```text
|
||||||
|
+-------------------+-----------------------------------------------------------------------------+
|
||||||
|
| Fixed Header (16) | Variable Sections |
|
||||||
|
+-------------------+----------+-------------+-----------------------+------------------+-------------+
|
||||||
|
| Ver: 2 (1B) | SenderID | RecipientID | SOURCE ROUTE | Payload | Signature |
|
||||||
|
| Type, TTL, etc. | (8B) | (8B) | (Variable) | (Length in Head) | (64B) |
|
||||||
|
| Len: 4 Bytes | | (Required*) | Only if HAS_ROUTE=1 | | (Optional) |
|
||||||
|
+-------------------+----------+-------------+-----------------------+------------------+-------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
**(*) Note:** A `Route` can be attached to **any** packet type that has a `RecipientID` (flag `HAS_RECIPIENT` set).
|
||||||
|
|
||||||
|
### Fixed Header Differences
|
||||||
|
|
||||||
|
| Field | Size (v1) | Size (v2) | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **Version** | 1 byte | 1 byte | `0x01` vs `0x02` |
|
||||||
|
| **Payload Length** | **2 bytes** | **4 bytes** | `UInt32` in v2 to support large files. **Excludes** route/IDs/sig. |
|
||||||
|
| **Total Size** | **14 bytes** | **16 bytes** | V2 header is 2 bytes larger. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Source Route Specification
|
||||||
|
|
||||||
|
The `Source Route` field is a variable-length list of **intermediate hops** that the packet must traverse.
|
||||||
|
|
||||||
|
* **Location:** Immediately follows `RecipientID`.
|
||||||
|
* **Structure:**
|
||||||
|
* `Count` (1 byte): Number of intermediate hops (`N`).
|
||||||
|
* `Hops` (`N * 8` bytes): Sequence of Peer IDs.
|
||||||
|
|
||||||
|
### Intermediate Hops Only
|
||||||
|
The route list MUST contain **only** the intermediate relays between the sender and the recipient.
|
||||||
|
* **DO NOT** include the `SenderID` (it is already in the packet).
|
||||||
|
* **DO NOT** include the `RecipientID` (it is already in the packet).
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
Topology: `Alice (Sender) -> Bob -> Charlie -> Dave (Recipient)`
|
||||||
|
* Packet `SenderID`: Alice
|
||||||
|
* Packet `RecipientID`: Dave
|
||||||
|
* Packet `Route`: `[Bob, Charlie]` (Count = 2)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Topology Discovery (Gossip)
|
||||||
|
|
||||||
|
To calculate routes, nodes need a view of the network topology. This is achieved via a **Neighbor List** extension to the `IdentityAnnouncement` packet.
|
||||||
|
|
||||||
|
* **Mechanism:** `IdentityAnnouncement` packets contain a TLV (Type-Length-Value) payload.
|
||||||
|
* **New TLV Type:** `0x04` (Direct Neighbors).
|
||||||
|
* **Content:** A list of Peer IDs that the announcing node is directly connected to.
|
||||||
|
|
||||||
|
**TLV Structure (Type 0x04):**
|
||||||
|
```text
|
||||||
|
[Type: 0x04] [Length: 1B] [Count: 1B] [NeighborID1 (8B)] [NeighborID2 (8B)] ...
|
||||||
|
```
|
||||||
|
Nodes receiving this TLV update their local mesh graph, linking the sender to the listed neighbors.
|
||||||
|
|
||||||
|
### Edge Verification (Two-Way Handshake)
|
||||||
|
|
||||||
|
To prevent spoofing and routing through stale connections, the Mesh Graph service implements a strict two-way handshake verification:
|
||||||
|
|
||||||
|
* **Unconfirmed Edge:** If Peer A announces Peer B, but Peer B does *not* announce Peer A, the connection is treated as **unconfirmed**. Unconfirmed edges are visualized as dotted lines in debug tools but are **excluded** from route calculations.
|
||||||
|
* **Confirmed Edge:** An edge is only valid for routing when **both** peers explicitly announce each other in their neighbor lists. This ensures that the connection is bidirectional and currently active from both perspectives.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Fragmentation & Source Routing
|
||||||
|
|
||||||
|
When a large source-routed packet (e.g., File Transfer) exceeds the MTU and requires fragmentation:
|
||||||
|
|
||||||
|
1. **Version Inheritance:** All fragments MUST be marked as **Version 2**.
|
||||||
|
2. **Route Inheritance:** All fragments MUST contain the **exact same Route field** as the parent packet.
|
||||||
|
|
||||||
|
**Why?** If fragments were sent as v1 packets or without routes, they would fall back to flooding, negating the bandwidth benefits of source routing for large data transfers.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Security & Signing
|
||||||
|
|
||||||
|
Source routing is fully secured by the existing Ed25519 signature scheme.
|
||||||
|
|
||||||
|
* **Scope:** The signature covers the **entire packet structure** (Header + Sender + Recipient + Route + Payload).
|
||||||
|
* **Verification:** The receiver verifies the signature against the `SenderID`'s public key.
|
||||||
|
* **Integrity:** Any tampering with the route list by malicious relays will invalidate the signature, causing the packet to be dropped by the destination.
|
||||||
|
|
||||||
|
**Signature Input Construction:**
|
||||||
|
Serialize the packet exactly as transmitted, but temporarily set `TTL = 0` and remove the `Signature` bytes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Relay Logic
|
||||||
|
|
||||||
|
When a node receives a packet **not** addressed to itself:
|
||||||
|
|
||||||
|
1. **Check Route:**
|
||||||
|
* Is `Version >= 2`?
|
||||||
|
* Is `HAS_ROUTE` flag set?
|
||||||
|
* Is the route list non-empty?
|
||||||
|
2. **If YES (Source Routed):**
|
||||||
|
* Find local Peer ID in the route list at index `i`.
|
||||||
|
* **Next Hop:** The peer at `i + 1`.
|
||||||
|
* **Last Hop:** If `i` is the last index, the Next Hop is the `RecipientID`.
|
||||||
|
* **Action:** Attempt to unicast (`sendToPeer`) to the Next Hop.
|
||||||
|
* **Fallback:** If the Next Hop is unreachable, **fall back to broadcast/flood** to ensure delivery.
|
||||||
|
3. **If NO (Standard):**
|
||||||
|
* Flood the packet to all connected neighbors (subject to TTL and probability rules).
|
||||||
Reference in New Issue
Block a user