mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 22:25:26 +00:00
feat(gossip): implement two-way handshake for source routing edges
- Update MeshGraphService to track directed announcements - Require bidirectional announcements for a 'confirmed' edge - Update RoutePlanner to strictly use confirmed edges - Update Mesh Topology debug view to show confirmed vs unconfirmed edges (solid vs dotted)
This commit is contained in:
@@ -6,18 +6,18 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/**
|
||||
* Maintains an internal undirected graph of the mesh based on gossip.
|
||||
* Maintains an internal 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 GraphEdge(val a: String, val b: String, val isConfirmed: Boolean, val confirmedBy: String? = null)
|
||||
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>>()
|
||||
// Announcements: peerID -> set of neighbor peerIDs that *this* peer claims to see
|
||||
private val announcements = ConcurrentHashMap<String, Set<String>>()
|
||||
// Latest announcement timestamp per peer (ULong from packet)
|
||||
private val lastUpdate = ConcurrentHashMap<String, ULong>()
|
||||
|
||||
@@ -47,20 +47,10 @@ class MeshGraphService private constructor() {
|
||||
}
|
||||
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)
|
||||
}
|
||||
// Update what originPeerID announces
|
||||
// Filter out self-loops just in case
|
||||
val newSet = neighborsOrNull.distinct().take(10).filter { it != originPeerID }.toSet()
|
||||
announcements[originPeerID] = newSet
|
||||
|
||||
publishSnapshot()
|
||||
}
|
||||
@@ -73,24 +63,43 @@ class MeshGraphService private constructor() {
|
||||
}
|
||||
|
||||
private fun publishSnapshot() {
|
||||
val nodes = mutableSetOf<String>()
|
||||
adjacency.forEach { (a, neighbors) ->
|
||||
nodes.add(a)
|
||||
nodes.addAll(neighbors)
|
||||
// Collect all known nodes from nicknames and announcements
|
||||
val allNodes = mutableSetOf<String>()
|
||||
allNodes.addAll(nicknames.keys)
|
||||
announcements.forEach { (origin, neighbors) ->
|
||||
allNodes.add(origin)
|
||||
allNodes.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 nodeList = allNodes.map { GraphNode(it, nicknames[it]) }.sortedBy { it.peerID }
|
||||
|
||||
val edges = mutableListOf<GraphEdge>()
|
||||
val processedPairs = mutableSetOf<Pair<String, String>>()
|
||||
|
||||
// We only care about connections that exist in at least one direction.
|
||||
// So iterating through all entries in `announcements` covers every declared edge.
|
||||
announcements.forEach { (source, targets) ->
|
||||
targets.forEach { target ->
|
||||
val pair = if (source <= target) source to target else target to source
|
||||
if (processedPairs.add(pair)) {
|
||||
// This is a new pair we haven't evaluated yet
|
||||
val (a, b) = pair
|
||||
val aAnnouncesB = announcements[a]?.contains(b) == true
|
||||
val bAnnouncesA = announcements[b]?.contains(a) == true
|
||||
|
||||
if (aAnnouncesB && bAnnouncesA) {
|
||||
edges.add(GraphEdge(a, b, isConfirmed = true))
|
||||
} else if (aAnnouncesB) {
|
||||
edges.add(GraphEdge(a, b, isConfirmed = false, confirmedBy = a))
|
||||
} else if (bAnnouncesA) {
|
||||
edges.add(GraphEdge(a, b, isConfirmed = false, confirmedBy = b))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val edges = edgeSet.map { GraphEdge(it.first, it.second) }.sortedWith(compareBy({ it.a }, { it.b }))
|
||||
_graphState.value = GraphSnapshot(nodeList, edges)
|
||||
|
||||
val sortedEdges = edges.sortedWith(compareBy({ it.a }, { it.b }))
|
||||
_graphState.value = GraphSnapshot(nodeList, sortedEdges)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -17,7 +17,9 @@ object RoutePlanner {
|
||||
if (src == dst) return listOf(src)
|
||||
val snapshot = MeshGraphService.getInstance().graphState.value
|
||||
val neighbors = mutableMapOf<String, MutableSet<String>>()
|
||||
snapshot.edges.forEach { e ->
|
||||
|
||||
// Only consider confirmed edges for routing
|
||||
snapshot.edges.filter { it.isConfirmed }.forEach { e ->
|
||||
neighbors.getOrPut(e.a) { mutableSetOf() }.add(e.b)
|
||||
neighbors.getOrPut(e.b) { mutableSetOf() }.add(e.a)
|
||||
}
|
||||
@@ -63,4 +65,3 @@ object RoutePlanner {
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -80,7 +80,29 @@ fun MeshTopologySection() {
|
||||
val p1 = positions[e.a]
|
||||
val p2 = positions[e.b]
|
||||
if (p1 != null && p2 != null) {
|
||||
drawLine(color = Color(0xFF4A90E2), start = p1, end = p2, strokeWidth = 2f)
|
||||
if (e.isConfirmed) {
|
||||
drawLine(color = Color(0xFF4A90E2), start = p1, end = p2, strokeWidth = 2f)
|
||||
} else {
|
||||
// Unconfirmed: draw "solid" from declarer, "dashed" from other
|
||||
val start = if (e.confirmedBy == e.a) p1 else p2
|
||||
val end = if (e.confirmedBy == e.a) p2 else p1
|
||||
|
||||
val midX = (start.x + end.x) / 2
|
||||
val midY = (start.y + end.y) / 2
|
||||
val mid = androidx.compose.ui.geometry.Offset(midX, midY)
|
||||
|
||||
// Solid half
|
||||
drawLine(color = Color(0xFF4A90E2), start = start, end = mid, strokeWidth = 2f)
|
||||
|
||||
// Dotted half
|
||||
drawLine(
|
||||
color = Color(0xFF4A90E2),
|
||||
start = mid,
|
||||
end = end,
|
||||
strokeWidth = 2f,
|
||||
pathEffect = androidx.compose.ui.graphics.PathEffect.dashPathEffect(floatArrayOf(5f, 5f), 0f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user