mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 05:05:20 +00:00
@@ -1,77 +0,0 @@
|
||||
package com.bitchat.android.services.meshgraph
|
||||
|
||||
import android.util.Log
|
||||
|
||||
/**
|
||||
* Gossip TLV helpers for embedding direct neighbor peer IDs in ANNOUNCE payloads.
|
||||
* Uses compact TLV: [type=0x04][len=1 byte][value=N*8 bytes of peerIDs]
|
||||
*/
|
||||
object GossipTLV {
|
||||
// TLV type for a compact list of direct neighbor peerIDs (each 8 bytes)
|
||||
const val DIRECT_NEIGHBORS_TYPE: UByte = 0x04u
|
||||
|
||||
/**
|
||||
* Encode up to 10 unique peerIDs (hex string up to 16 chars) as TLV value.
|
||||
*/
|
||||
fun encodeNeighbors(peerIDs: List<String>): ByteArray {
|
||||
val unique = peerIDs.distinct().take(10)
|
||||
val valueBytes = unique.flatMap { id -> hexStringPeerIdTo8Bytes(id).toList() }.toByteArray()
|
||||
if (valueBytes.size > 255) {
|
||||
// Safety check, though 10*8 = 80 bytes, so well under 255
|
||||
Log.w("GossipTLV", "Neighbors value exceeds 255, truncating")
|
||||
}
|
||||
return byteArrayOf(DIRECT_NEIGHBORS_TYPE.toByte(), valueBytes.size.toByte()) + valueBytes
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan a TLV-encoded announce payload and extract neighbor peerIDs.
|
||||
* Returns null if the TLV is not present at all; returns an empty list if present with length 0.
|
||||
*/
|
||||
fun decodeNeighborsFromAnnouncementPayload(payload: ByteArray): List<String>? {
|
||||
val result = mutableListOf<String>()
|
||||
var offset = 0
|
||||
while (offset + 2 <= payload.size) {
|
||||
val type = payload[offset].toUByte()
|
||||
val len = payload[offset + 1].toUByte().toInt()
|
||||
offset += 2
|
||||
if (offset + len > payload.size) break
|
||||
val value = payload.sliceArray(offset until offset + len)
|
||||
offset += len
|
||||
|
||||
if (type == DIRECT_NEIGHBORS_TYPE) {
|
||||
// Value is N*8 bytes of peer IDs
|
||||
var pos = 0
|
||||
while (pos + 8 <= value.size) {
|
||||
val idBytes = value.sliceArray(pos until pos + 8)
|
||||
result.add(bytesToPeerIdHex(idBytes))
|
||||
pos += 8
|
||||
}
|
||||
return result // present (possibly empty)
|
||||
}
|
||||
}
|
||||
// Not present
|
||||
return null
|
||||
}
|
||||
|
||||
private fun hexStringPeerIdTo8Bytes(hexString: String): ByteArray {
|
||||
val clean = hexString.lowercase().take(16)
|
||||
val result = ByteArray(8) { 0 }
|
||||
var idx = 0
|
||||
var out = 0
|
||||
while (idx + 1 < clean.length && out < 8) {
|
||||
val byteStr = clean.substring(idx, idx + 2)
|
||||
val b = byteStr.toIntOrNull(16)?.toByte() ?: 0
|
||||
result[out++] = b
|
||||
idx += 2
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun bytesToPeerIdHex(bytes: ByteArray): String {
|
||||
val sb = StringBuilder()
|
||||
for (b in bytes.take(8)) {
|
||||
sb.append(String.format("%02x", b))
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
package com.bitchat.android.services.meshgraph
|
||||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/**
|
||||
* Maintains an internal undirected graph of the mesh based on gossip.
|
||||
* Nodes are peers (peerID), edges are direct connections.
|
||||
*/
|
||||
class MeshGraphService private constructor() {
|
||||
data class GraphNode(val peerID: String, val nickname: String?)
|
||||
data class GraphEdge(val a: String, val b: String)
|
||||
data class GraphSnapshot(val nodes: List<GraphNode>, val edges: List<GraphEdge>)
|
||||
|
||||
// Map peerID -> nickname (may be null if unknown)
|
||||
private val nicknames = ConcurrentHashMap<String, String?>()
|
||||
// Adjacency (undirected): peerID -> set of neighbor peerIDs
|
||||
private val adjacency = ConcurrentHashMap<String, MutableSet<String>>()
|
||||
// Latest announcement timestamp per peer (ULong from packet)
|
||||
private val lastUpdate = ConcurrentHashMap<String, ULong>()
|
||||
|
||||
private val _graphState = MutableStateFlow(GraphSnapshot(emptyList(), emptyList()))
|
||||
val graphState: StateFlow<GraphSnapshot> = _graphState.asStateFlow()
|
||||
|
||||
/**
|
||||
* Update graph from a verified announcement.
|
||||
* Replaces previous neighbors for origin if this is newer (by timestamp).
|
||||
*/
|
||||
fun updateFromAnnouncement(originPeerID: String, originNickname: String?, neighborsOrNull: List<String>?, timestamp: ULong) {
|
||||
synchronized(this) {
|
||||
// Always update nickname if provided
|
||||
if (originNickname != null) nicknames[originPeerID] = originNickname
|
||||
|
||||
// If no neighbors TLV present, do not modify edges or timestamps
|
||||
if (neighborsOrNull == null) {
|
||||
publishSnapshot()
|
||||
return
|
||||
}
|
||||
|
||||
// Newer-only replacement per origin (based on TLV-bearing announcements only)
|
||||
val prevTs = lastUpdate[originPeerID]
|
||||
if (prevTs != null && prevTs >= timestamp) {
|
||||
// Older or equal TLV-bearing update: ignore
|
||||
return
|
||||
}
|
||||
lastUpdate[originPeerID] = timestamp
|
||||
|
||||
// Remove old symmetric edges contributed by this origin
|
||||
val prevNeighbors = adjacency[originPeerID]?.toSet().orEmpty()
|
||||
prevNeighbors.forEach { n ->
|
||||
adjacency[n]?.remove(originPeerID)
|
||||
}
|
||||
|
||||
// Replace origin's adjacency with new set (may be empty)
|
||||
val newSet = neighborsOrNull.distinct().take(10).filter { it != originPeerID }.toMutableSet()
|
||||
adjacency[originPeerID] = newSet
|
||||
// Ensure undirected edges
|
||||
newSet.forEach { n ->
|
||||
adjacency.putIfAbsent(n, mutableSetOf())
|
||||
adjacency[n]?.add(originPeerID)
|
||||
}
|
||||
|
||||
publishSnapshot()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateNickname(peerID: String, nickname: String?) {
|
||||
if (nickname == null) return
|
||||
nicknames[peerID] = nickname
|
||||
publishSnapshot()
|
||||
}
|
||||
|
||||
private fun publishSnapshot() {
|
||||
val nodes = mutableSetOf<String>()
|
||||
adjacency.forEach { (a, neighbors) ->
|
||||
nodes.add(a)
|
||||
nodes.addAll(neighbors)
|
||||
}
|
||||
// Merge in nicknames-only nodes
|
||||
nodes.addAll(nicknames.keys)
|
||||
|
||||
val nodeList = nodes.map { GraphNode(it, nicknames[it]) }.sortedBy { it.peerID }
|
||||
val edgeSet = mutableSetOf<Pair<String, String>>()
|
||||
adjacency.forEach { (a, ns) ->
|
||||
ns.forEach { b ->
|
||||
val (x, y) = if (a <= b) a to b else b to a
|
||||
edgeSet.add(x to y)
|
||||
}
|
||||
}
|
||||
val edges = edgeSet.map { GraphEdge(it.first, it.second) }.sortedWith(compareBy({ it.a }, { it.b }))
|
||||
_graphState.value = GraphSnapshot(nodeList, edges)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@Volatile private var INSTANCE: MeshGraphService? = null
|
||||
fun getInstance(): MeshGraphService = INSTANCE ?: synchronized(this) {
|
||||
INSTANCE ?: MeshGraphService().also { INSTANCE = it }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
package com.bitchat.android.services.meshgraph
|
||||
|
||||
import android.util.Log
|
||||
import java.util.PriorityQueue
|
||||
|
||||
/**
|
||||
* Computes shortest paths on the current mesh graph snapshot using Dijkstra.
|
||||
* Assumes unit edge weights.
|
||||
*/
|
||||
object RoutePlanner {
|
||||
private const val TAG = "RoutePlanner"
|
||||
|
||||
/**
|
||||
* Return full path [src, ..., dst] if reachable, else null.
|
||||
*/
|
||||
fun shortestPath(src: String, dst: String): List<String>? {
|
||||
if (src == dst) return listOf(src)
|
||||
val snapshot = MeshGraphService.getInstance().graphState.value
|
||||
val neighbors = mutableMapOf<String, MutableSet<String>>()
|
||||
snapshot.edges.forEach { e ->
|
||||
neighbors.getOrPut(e.a) { mutableSetOf() }.add(e.b)
|
||||
neighbors.getOrPut(e.b) { mutableSetOf() }.add(e.a)
|
||||
}
|
||||
// Ensure nodes known even if isolated
|
||||
snapshot.nodes.forEach { n -> neighbors.putIfAbsent(n.peerID, mutableSetOf()) }
|
||||
|
||||
if (!neighbors.containsKey(src) || !neighbors.containsKey(dst)) return null
|
||||
|
||||
val dist = mutableMapOf<String, Int>()
|
||||
val prev = mutableMapOf<String, String?>()
|
||||
val pq = PriorityQueue<Pair<String, Int>>(compareBy { it.second })
|
||||
|
||||
neighbors.keys.forEach { v ->
|
||||
dist[v] = if (v == src) 0 else Int.MAX_VALUE
|
||||
prev[v] = null
|
||||
}
|
||||
pq.add(src to 0)
|
||||
|
||||
while (pq.isNotEmpty()) {
|
||||
val (u, d) = pq.poll()
|
||||
if (d > (dist[u] ?: Int.MAX_VALUE)) continue
|
||||
if (u == dst) break
|
||||
neighbors[u]?.forEach { v ->
|
||||
val alt = d + 1
|
||||
if (alt < (dist[v] ?: Int.MAX_VALUE)) {
|
||||
dist[v] = alt
|
||||
prev[v] = u
|
||||
pq.add(v to alt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((dist[dst] ?: Int.MAX_VALUE) == Int.MAX_VALUE) return null
|
||||
|
||||
val path = mutableListOf<String>()
|
||||
var cur: String? = dst
|
||||
while (cur != null) {
|
||||
path.add(cur)
|
||||
cur = prev[cur]
|
||||
}
|
||||
path.reverse()
|
||||
Log.d(TAG, "Computed path $path")
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user