wip mesh graph

This commit is contained in:
callebtc
2025-09-05 16:03:24 +02:00
parent 4b81b7f97a
commit 08ffde618a
5 changed files with 286 additions and 2 deletions
@@ -0,0 +1,76 @@
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 if present.
*/
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
}
break // only one neighbors TLV expected
}
}
return result
}
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()
}
}
@@ -0,0 +1,96 @@
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?, neighbors: List<String>, timestamp: ULong) {
// Newer-only replacement per origin
val prevTs = lastUpdate[originPeerID]
if (prevTs != null && prevTs >= timestamp) return
lastUpdate[originPeerID] = timestamp
synchronized(this) {
// Update nickname
if (originNickname != null) nicknames[originPeerID] = originNickname
// 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
val newSet = neighbors.distinct().take(10).filter { it != originPeerID }.toMutableSet()
adjacency[originPeerID] = newSet
// Ensure nodes exist for neighbors
newSet.forEach { n ->
adjacency.putIfAbsent(n, mutableSetOf())
// Add symmetric edge for undirected graph visualization
adjacency[n]?.add(originPeerID)
}
// Compute snapshot
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 }
}
}
}