mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 18:45:20 +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
|
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.
|
* Nodes are peers (peerID), edges are direct connections.
|
||||||
*/
|
*/
|
||||||
class MeshGraphService private constructor() {
|
class MeshGraphService private constructor() {
|
||||||
data class GraphNode(val peerID: String, val nickname: String?)
|
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>)
|
data class GraphSnapshot(val nodes: List<GraphNode>, val edges: List<GraphEdge>)
|
||||||
|
|
||||||
// Map peerID -> nickname (may be null if unknown)
|
// Map peerID -> nickname (may be null if unknown)
|
||||||
private val nicknames = ConcurrentHashMap<String, String?>()
|
private val nicknames = ConcurrentHashMap<String, String?>()
|
||||||
// Adjacency (undirected): peerID -> set of neighbor peerIDs
|
// Announcements: peerID -> set of neighbor peerIDs that *this* peer claims to see
|
||||||
private val adjacency = ConcurrentHashMap<String, MutableSet<String>>()
|
private val announcements = ConcurrentHashMap<String, Set<String>>()
|
||||||
// Latest announcement timestamp per peer (ULong from packet)
|
// Latest announcement timestamp per peer (ULong from packet)
|
||||||
private val lastUpdate = ConcurrentHashMap<String, ULong>()
|
private val lastUpdate = ConcurrentHashMap<String, ULong>()
|
||||||
|
|
||||||
@@ -47,20 +47,10 @@ class MeshGraphService private constructor() {
|
|||||||
}
|
}
|
||||||
lastUpdate[originPeerID] = timestamp
|
lastUpdate[originPeerID] = timestamp
|
||||||
|
|
||||||
// Remove old symmetric edges contributed by this origin
|
// Update what originPeerID announces
|
||||||
val prevNeighbors = adjacency[originPeerID]?.toSet().orEmpty()
|
// Filter out self-loops just in case
|
||||||
prevNeighbors.forEach { n ->
|
val newSet = neighborsOrNull.distinct().take(10).filter { it != originPeerID }.toSet()
|
||||||
adjacency[n]?.remove(originPeerID)
|
announcements[originPeerID] = newSet
|
||||||
}
|
|
||||||
|
|
||||||
// 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()
|
publishSnapshot()
|
||||||
}
|
}
|
||||||
@@ -73,24 +63,43 @@ class MeshGraphService private constructor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun publishSnapshot() {
|
private fun publishSnapshot() {
|
||||||
val nodes = mutableSetOf<String>()
|
// Collect all known nodes from nicknames and announcements
|
||||||
adjacency.forEach { (a, neighbors) ->
|
val allNodes = mutableSetOf<String>()
|
||||||
nodes.add(a)
|
allNodes.addAll(nicknames.keys)
|
||||||
nodes.addAll(neighbors)
|
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 nodeList = allNodes.map { GraphNode(it, nicknames[it]) }.sortedBy { it.peerID }
|
||||||
val edgeSet = mutableSetOf<Pair<String, String>>()
|
|
||||||
adjacency.forEach { (a, ns) ->
|
val edges = mutableListOf<GraphEdge>()
|
||||||
ns.forEach { b ->
|
val processedPairs = mutableSetOf<Pair<String, String>>()
|
||||||
val (x, y) = if (a <= b) a to b else b to a
|
|
||||||
edgeSet.add(x to y)
|
// 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 {
|
companion object {
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ object RoutePlanner {
|
|||||||
if (src == dst) return listOf(src)
|
if (src == dst) return listOf(src)
|
||||||
val snapshot = MeshGraphService.getInstance().graphState.value
|
val snapshot = MeshGraphService.getInstance().graphState.value
|
||||||
val neighbors = mutableMapOf<String, MutableSet<String>>()
|
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.a) { mutableSetOf() }.add(e.b)
|
||||||
neighbors.getOrPut(e.b) { mutableSetOf() }.add(e.a)
|
neighbors.getOrPut(e.b) { mutableSetOf() }.add(e.a)
|
||||||
}
|
}
|
||||||
@@ -63,4 +65,3 @@ object RoutePlanner {
|
|||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -80,7 +80,29 @@ fun MeshTopologySection() {
|
|||||||
val p1 = positions[e.a]
|
val p1 = positions[e.a]
|
||||||
val p2 = positions[e.b]
|
val p2 = positions[e.b]
|
||||||
if (p1 != null && p2 != null) {
|
if (p1 != null && p2 != null) {
|
||||||
|
if (e.isConfirmed) {
|
||||||
drawLine(color = Color(0xFF4A90E2), start = p1, end = p2, strokeWidth = 2f)
|
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