Gossip mesh topology + source-based routing (#445)

* wip mesh graph

* gossip fix

* gossip works

* source-based routing wip

* log

* update spec to be explicit about intermediate hops only

* drop duplicate hops

* add to spec

* test

* forgot comma

* source routing v2

* add compression bomb protection

* v2 source routing

* fragmented packets inherit route

* update spec

* Gossip routing tmp with connection limit fixed (#569)

* fix: r8 exception for LocationManager (#566)

* fragmented packets inherit route

* update spec

* fix connection limits

* fix: deserialization issue with routed packets

* log

* dynamic fragment size

* fragment size

* add tests

* 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)

* docs: update SOURCE_ROUTING.md with two-way handshake requirement

* evict stale peers from mesh graph service

* better logging

* fix announce spe

* fix: empty route

* fix spec

* fix: compile error in DebugSettingsSheet and potential NPE in RoutePlanner

* revert

* try again
This commit is contained in:
callebtc
2026-01-13 04:18:07 +07:00
committed by GitHub
parent d164b3f9bc
commit 66012e9fe9
18 changed files with 1251 additions and 133 deletions
@@ -0,0 +1,117 @@
package com.bitchat.android.mesh
import com.bitchat.android.model.RoutedPacket
import com.bitchat.android.protocol.BitchatPacket
import com.bitchat.android.protocol.MessageType
import com.bitchat.android.util.toHexString
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.mockito.kotlin.any
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
@ExperimentalCoroutinesApi
class PacketRelayManagerTest {
private lateinit var packetRelayManager: PacketRelayManager
private val delegate: PacketRelayManagerDelegate = mock()
private val myPeerID = "1111111111111111"
private val otherPeerID = "2222222222222222"
private val nextHopPeerID = "3333333333333333"
private val finalRecipientID = "4444444444444444"
@Before
fun setUp() {
packetRelayManager = PacketRelayManager(myPeerID)
packetRelayManager.delegate = delegate
whenever(delegate.getNetworkSize()).thenReturn(10)
whenever(delegate.getBroadcastRecipient()).thenReturn(byteArrayOf(0,0,0,0,0,0,0,0))
}
private fun createPacket(route: List<ByteArray>?, recipient: String? = null): BitchatPacket {
return BitchatPacket(
type = MessageType.MESSAGE.value,
senderID = hexStringToPeerBytes(otherPeerID),
recipientID = recipient?.let { hexStringToPeerBytes(it) },
timestamp = System.currentTimeMillis().toULong(),
payload = "hello".toByteArray(),
ttl = 5u,
route = route
)
}
@Test
fun `packet with duplicate hops is dropped`() = runTest {
val route = listOf(
hexStringToPeerBytes(nextHopPeerID),
hexStringToPeerBytes(nextHopPeerID)
)
val packet = createPacket(route)
val routedPacket = RoutedPacket(packet, otherPeerID)
packetRelayManager.handlePacketRelay(routedPacket)
verify(delegate, never()).sendToPeer(any(), any())
verify(delegate, never()).broadcastPacket(any())
}
@Test
fun `valid source-routed packet is relayed to next hop`() = runTest {
val route = listOf(
hexStringToPeerBytes(myPeerID),
hexStringToPeerBytes(nextHopPeerID)
)
val packet = createPacket(route, finalRecipientID)
val routedPacket = RoutedPacket(packet, otherPeerID)
whenever(delegate.sendToPeer(any(), any())).thenReturn(true)
packetRelayManager.handlePacketRelay(routedPacket)
verify(delegate).sendToPeer(org.mockito.kotlin.eq(nextHopPeerID), any())
verify(delegate, never()).broadcastPacket(any())
}
@Test
fun `last hop does not relay further`() = runTest {
val route = listOf(
hexStringToPeerBytes(myPeerID)
)
val packet = createPacket(route, finalRecipientID)
val routedPacket = RoutedPacket(packet, otherPeerID)
whenever(delegate.sendToPeer(any(), any())).thenReturn(true)
packetRelayManager.handlePacketRelay(routedPacket)
verify(delegate).sendToPeer(org.mockito.kotlin.eq(finalRecipientID), any())
verify(delegate, never()).broadcastPacket(any())
}
@Test
fun `packet with empty route is broadcast`() = runTest {
val packet = createPacket(null)
val routedPacket = RoutedPacket(packet, otherPeerID)
packetRelayManager.handlePacketRelay(routedPacket)
verify(delegate, never()).sendToPeer(any(), any())
verify(delegate).broadcastPacket(any())
}
private fun hexStringToPeerBytes(hex: String): ByteArray {
val result = ByteArray(8)
var idx = 0
var out = 0
while (idx + 1 < hex.length && out < 8) {
val b = hex.substring(idx, idx + 2).toIntOrNull(16)?.toByte() ?: 0
result[out++] = b
idx += 2
}
return result
}
}