Plumtree sync (#393)

* wip plumtree

* sync works

* fix logging

* ttl to 0

* fix send packet to one peer

* spec

* wip GCS instead of bloom

* remove bloom filter remainders

* clean

* prune old announcements

* remove announcements from sync after LEAVE

* sync after 1 second

* pruning

* track own announcement and prune messages without announcements

* fix pruning

* getGcsMaxFilterBytes default value 400 bytes

* parameters
This commit is contained in:
callebtc
2025-09-14 03:32:10 +02:00
committed by GitHub
parent 3967ef8922
commit 73c91b9509
18 changed files with 963 additions and 60 deletions
@@ -153,7 +153,7 @@ class GeohashViewModel(
isRelay = false
)
fun startGeohashDM(pubkeyHex: String, onStartPrivateChat: (String) -> Unit) {
val convKey = "nostr_${'$'}{pubkeyHex.take(16)}"
val convKey = "nostr_${pubkeyHex.take(16)}"
repo.putNostrKeyMapping(convKey, pubkeyHex)
// Record the conversation's geohash using the currently selected location channel (if any)
val current = state.selectedLocationChannel.value
@@ -163,7 +163,7 @@ class GeohashViewModel(
com.bitchat.android.nostr.GeohashConversationRegistry.set(convKey, gh)
}
onStartPrivateChat(convKey)
Log.d(TAG, "🗨️ Started geohash DM with ${'$'}pubkeyHex -> ${'$'}convKey (geohash=${'$'}gh)")
Log.d(TAG, "🗨️ Started geohash DM with ${pubkeyHex} -> ${convKey} (geohash=${gh})")
}
messageManager.addMessage(sysMsg)
@@ -16,6 +16,10 @@ object DebugPreferenceManager {
private const val KEY_MAX_CONN_OVERALL = "max_connections_overall"
private const val KEY_MAX_CONN_SERVER = "max_connections_server"
private const val KEY_MAX_CONN_CLIENT = "max_connections_client"
private const val KEY_SEEN_PACKET_CAP = "seen_packet_capacity"
// GCS keys (no migration/back-compat)
private const val KEY_GCS_MAX_BYTES = "gcs_max_filter_bytes"
private const val KEY_GCS_FPR = "gcs_filter_fpr_percent"
private lateinit var prefs: SharedPreferences
@@ -74,4 +78,26 @@ object DebugPreferenceManager {
fun setMaxConnectionsClient(value: Int) {
if (ready()) prefs.edit().putInt(KEY_MAX_CONN_CLIENT, value).apply()
}
// Sync/GCS settings
fun getSeenPacketCapacity(default: Int = 500): Int =
if (ready()) prefs.getInt(KEY_SEEN_PACKET_CAP, default) else default
fun setSeenPacketCapacity(value: Int) {
if (ready()) prefs.edit().putInt(KEY_SEEN_PACKET_CAP, value).apply()
}
fun getGcsMaxFilterBytes(default: Int = 400): Int =
if (ready()) prefs.getInt(KEY_GCS_MAX_BYTES, default) else default
fun setGcsMaxFilterBytes(value: Int) {
if (ready()) prefs.edit().putInt(KEY_GCS_MAX_BYTES, value).apply()
}
fun getGcsFprPercent(default: Double = 1.0): Double =
if (ready()) java.lang.Double.longBitsToDouble(prefs.getLong(KEY_GCS_FPR, java.lang.Double.doubleToRawLongBits(default))) else default
fun setGcsFprPercent(value: Double) {
if (ready()) prefs.edit().putLong(KEY_GCS_FPR, java.lang.Double.doubleToRawLongBits(value)).apply()
}
}
@@ -201,6 +201,37 @@ class DebugSettingsManager private constructor() {
fun updateRelayStats(stats: PacketRelayStats) {
_relayStats.value = stats
}
// Sync/GCS settings (UI-configurable)
private val _seenPacketCapacity = MutableStateFlow(DebugPreferenceManager.getSeenPacketCapacity(500))
val seenPacketCapacity: StateFlow<Int> = _seenPacketCapacity.asStateFlow()
private val _gcsMaxBytes = MutableStateFlow(DebugPreferenceManager.getGcsMaxFilterBytes(400))
val gcsMaxBytes: StateFlow<Int> = _gcsMaxBytes.asStateFlow()
private val _gcsFprPercent = MutableStateFlow(DebugPreferenceManager.getGcsFprPercent(1.0))
val gcsFprPercent: StateFlow<Double> = _gcsFprPercent.asStateFlow()
fun setSeenPacketCapacity(value: Int) {
val clamped = value.coerceIn(10, 1000)
DebugPreferenceManager.setSeenPacketCapacity(clamped)
_seenPacketCapacity.value = clamped
addDebugMessage(DebugMessage.SystemMessage("🧩 max packets per sync set to $clamped"))
}
fun setGcsMaxBytes(value: Int) {
val clamped = value.coerceIn(128, 1024)
DebugPreferenceManager.setGcsMaxFilterBytes(clamped)
_gcsMaxBytes.value = clamped
addDebugMessage(DebugMessage.SystemMessage("🌸 max GCS filter size set to $clamped bytes"))
}
fun setGcsFprPercent(value: Double) {
val clamped = value.coerceIn(0.1, 5.0)
DebugPreferenceManager.setGcsFprPercent(clamped)
_gcsFprPercent.value = clamped
addDebugMessage(DebugMessage.SystemMessage("🎯 GCS FPR set to ${String.format("%.2f", clamped)}%"))
}
// MARK: - Debug Message Creation Helpers
@@ -48,6 +48,9 @@ fun DebugSettingsSheet(
val scanResults by manager.scanResults.collectAsState()
val connectedDevices by manager.connectedDevices.collectAsState()
val relayStats by manager.relayStats.collectAsState()
val seenCapacity by manager.seenPacketCapacity.collectAsState()
val gcsMaxBytes by manager.gcsMaxBytes.collectAsState()
val gcsFpr by manager.gcsFprPercent.collectAsState()
// Push live connected devices from mesh service whenever sheet is visible
LaunchedEffect(isPresented) {
@@ -284,6 +287,27 @@ fun DebugSettingsSheet(
}
}
// Connected devices
item {
Surface(shape = RoundedCornerShape(12.dp), color = colorScheme.surfaceVariant.copy(alpha = 0.2f)) {
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Icon(Icons.Filled.SettingsEthernet, contentDescription = null, tint = Color(0xFF9C27B0))
Text("sync settings", fontFamily = FontFamily.Monospace, fontSize = 14.sp, fontWeight = FontWeight.Medium)
}
Text("max packets per sync: $seenCapacity", fontFamily = FontFamily.Monospace, fontSize = 11.sp, color = colorScheme.onSurface.copy(alpha = 0.7f))
Slider(value = seenCapacity.toFloat(), onValueChange = { manager.setSeenPacketCapacity(it.toInt()) }, valueRange = 10f..1000f, steps = 99)
Text("max GCS filter size: $gcsMaxBytes bytes (1281024)", fontFamily = FontFamily.Monospace, fontSize = 11.sp, color = colorScheme.onSurface.copy(alpha = 0.7f))
Slider(value = gcsMaxBytes.toFloat(), onValueChange = { manager.setGcsMaxBytes(it.toInt()) }, valueRange = 128f..1024f, steps = 0)
Text("target FPR: ${String.format("%.2f", gcsFpr)}%", fontFamily = FontFamily.Monospace, fontSize = 11.sp, color = colorScheme.onSurface.copy(alpha = 0.7f))
Slider(value = gcsFpr.toFloat(), onValueChange = { manager.setGcsFprPercent(it.toDouble()) }, valueRange = 0.1f..5.0f, steps = 49)
val p = remember(gcsFpr) { com.bitchat.android.sync.GCSFilter.deriveP(gcsFpr / 100.0) }
val nmax = remember(gcsFpr, gcsMaxBytes) { com.bitchat.android.sync.GCSFilter.estimateMaxElementsForSize(gcsMaxBytes, p) }
Text("derived P: $p • est. max elements: $nmax", fontFamily = FontFamily.Monospace, fontSize = 11.sp, color = colorScheme.onSurface.copy(alpha = 0.7f))
}
}
}
// Connected devices
item {
Surface(shape = RoundedCornerShape(12.dp), color = colorScheme.surfaceVariant.copy(alpha = 0.2f)) {