Merge ec9d4abcaa7030b07edfad9ac8086aeddd7b5350 into 094657efa0aabbb6f71c9050149d1d01aee96400

This commit is contained in:
a1denvalu3 2026-08-04 03:46:40 -04:00 committed by GitHub
commit 389dd75c43
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 177 additions and 28 deletions

View File

@ -12,7 +12,9 @@ import com.bitchat.android.sync.SyncDefaults
data class RequestSyncPacket(
val p: Int,
val m: Long,
val data: ByteArray
val data: ByteArray,
val wantedTypes: List<UByte>? = null,
val minTimestamp: ULong? = null
) {
fun encode(): ByteArray {
val out = ArrayList<Byte>()
@ -38,6 +40,28 @@ data class RequestSyncPacket(
)
// data
putTLV(0x03, data)
// wantedTypes
wantedTypes?.let { types ->
if (types.isNotEmpty()) {
val typesBytes = types.map { it.toByte() }.toByteArray()
putTLV(0x04, typesBytes)
}
}
// minTimestamp
minTimestamp?.let { ts ->
val tsLong = ts.toLong()
val tsBytes = byteArrayOf(
((tsLong ushr 56) and 0xFF).toByte(),
((tsLong ushr 48) and 0xFF).toByte(),
((tsLong ushr 40) and 0xFF).toByte(),
((tsLong ushr 32) and 0xFF).toByte(),
((tsLong ushr 24) and 0xFF).toByte(),
((tsLong ushr 16) and 0xFF).toByte(),
((tsLong ushr 8) and 0xFF).toByte(),
(tsLong and 0xFF).toByte()
)
putTLV(0x05, tsBytes)
}
return out.toByteArray()
}
@ -50,6 +74,8 @@ data class RequestSyncPacket(
var p: Int? = null
var m: Long? = null
var payload: ByteArray? = null
var wantedTypes: List<UByte>? = null
var minTimestamp: ULong? = null
while (off + 3 <= data.size) {
val t = (data[off].toInt() and 0xFF); off += 1
@ -69,6 +95,22 @@ data class RequestSyncPacket(
if (v.size > MAX_ACCEPT_FILTER_BYTES) return null
payload = v
}
0x04 -> {
val typesList = mutableListOf<UByte>()
for (b in v) {
typesList.add(b.toUByte())
}
wantedTypes = typesList
}
0x05 -> {
if (len == 8) {
var tsVal = 0L
for (i in 0 until 8) {
tsVal = (tsVal shl 8) or (v[i].toLong() and 0xFF)
}
minTimestamp = tsVal.toULong()
}
}
}
}
@ -76,7 +118,7 @@ data class RequestSyncPacket(
val mm = m ?: return null
val dd = payload ?: return null
if (pp < 1 || mm <= 0L) return null
return RequestSyncPacket(pp, mm, dd)
return RequestSyncPacket(pp, mm, dd, wantedTypes, minTimestamp)
}
}
}

View File

@ -142,8 +142,8 @@ class GossipSyncManager(
}
}
private fun sendRequestSync() {
val payload = buildGcsPayload()
fun sendRequestSync(wantedTypes: List<UByte>? = null, minTimestamp: ULong? = null) {
val payload = buildGcsPayload(wantedTypes, minTimestamp)
val packet = BitchatPacket(
type = MessageType.REQUEST_SYNC.value,
@ -157,8 +157,8 @@ class GossipSyncManager(
delegate?.sendPacket(signed)
}
private fun sendRequestSyncToPeer(peerID: String) {
val payload = buildGcsPayload()
fun sendRequestSyncToPeer(peerID: String, wantedTypes: List<UByte>? = null, minTimestamp: ULong? = null) {
val payload = buildGcsPayload(wantedTypes, minTimestamp)
val packet = BitchatPacket(
type = MessageType.REQUEST_SYNC.value,
@ -183,26 +183,40 @@ class GossipSyncManager(
return GCSFilter.contains(sorted, nonZeroV)
}
// Determine types to include (default: ANNOUNCE and MESSAGE if wantedTypes is null or empty)
val targetTypes = if (request.wantedTypes == null || request.wantedTypes.isEmpty()) {
listOf(MessageType.ANNOUNCE.value, MessageType.MESSAGE.value)
} else {
request.wantedTypes
}
val minTs = request.minTimestamp ?: 0uL
// 1) Announcements: send latest per peerID if remote doesn't have them
for ((_, pair) in latestAnnouncementByPeer.entries) {
val (id, pkt) = pair
val idBytes = hexToBytes(id)
if (!mightContain(idBytes)) {
// Send original packet unchanged to requester only (keep local TTL)
val toSend = pkt.copy(ttl = com.bitchat.android.util.AppConstants.SYNC_TTL_HOPS)
delegate?.sendPacketToPeer(fromPeerID, toSend)
Log.d(TAG, "Sent sync announce: Type ${toSend.type} from ${toSend.senderID.toHexString()} to $fromPeerID packet id ${idBytes.toHexString()}")
if (targetTypes.contains(MessageType.ANNOUNCE.value)) {
for ((_, pair) in latestAnnouncementByPeer.entries) {
val (id, pkt) = pair
if (pkt.timestamp < minTs) continue
val idBytes = hexToBytes(id)
if (!mightContain(idBytes)) {
// Send original packet unchanged to requester only (keep local TTL)
val toSend = pkt.copy(ttl = com.bitchat.android.util.AppConstants.SYNC_TTL_HOPS)
delegate?.sendPacketToPeer(fromPeerID, toSend)
Log.d(TAG, "Sent sync announce: Type ${toSend.type} from ${toSend.senderID.toHexString()} to $fromPeerID packet id ${idBytes.toHexString()}")
}
}
}
// 2) Broadcast messages: send all they lack
val toSendMsgs = synchronized(messages) { messages.values.toList() }
for (pkt in toSendMsgs) {
val idBytes = PacketIdUtil.computeIdBytes(pkt)
if (!mightContain(idBytes)) {
val toSend = pkt.copy(ttl = com.bitchat.android.util.AppConstants.SYNC_TTL_HOPS)
delegate?.sendPacketToPeer(fromPeerID, toSend)
Log.d(TAG, "Sent sync message: Type ${toSend.type} to $fromPeerID packet id ${idBytes.toHexString()}")
if (targetTypes.contains(MessageType.MESSAGE.value)) {
val toSendMsgs = synchronized(messages) { messages.values.toList() }
for (pkt in toSendMsgs) {
if (pkt.timestamp < minTs) continue
val idBytes = PacketIdUtil.computeIdBytes(pkt)
if (!mightContain(idBytes)) {
val toSend = pkt.copy(ttl = com.bitchat.android.util.AppConstants.SYNC_TTL_HOPS)
delegate?.sendPacketToPeer(fromPeerID, toSend)
Log.d(TAG, "Sent sync message: Type ${toSend.type} to $fromPeerID packet id ${idBytes.toHexString()}")
}
}
}
}
@ -232,16 +246,35 @@ class GossipSyncManager(
return out
}
private fun buildGcsPayload(): ByteArray {
private fun buildGcsPayload(wantedTypes: List<UByte>? = null, minTimestamp: ULong? = null): ByteArray {
// Collect candidates: latest announcement per peer + recent broadcast messages
val list = ArrayList<BitchatPacket>()
// Determine types to include (default: ANNOUNCE and MESSAGE if wantedTypes is null or empty)
val targetTypes = if (wantedTypes == null || wantedTypes.isEmpty()) {
listOf(MessageType.ANNOUNCE.value, MessageType.MESSAGE.value)
} else {
wantedTypes
}
val minTs = minTimestamp ?: 0uL
// announcements
for ((_, pair) in latestAnnouncementByPeer) {
list.add(pair.second)
if (targetTypes.contains(MessageType.ANNOUNCE.value)) {
for ((_, pair) in latestAnnouncementByPeer) {
val pkt = pair.second
if (pkt.timestamp >= minTs) {
list.add(pkt)
}
}
}
// messages
synchronized(messages) {
list.addAll(messages.values)
if (targetTypes.contains(MessageType.MESSAGE.value)) {
synchronized(messages) {
for (pkt in messages.values) {
if (pkt.timestamp >= minTs) {
list.add(pkt)
}
}
}
}
// sort by timestamp desc, then take up to min(seenCapacity, fit capacity)
list.sortByDescending { it.timestamp.toLong() }
@ -254,12 +287,12 @@ class GossipSyncManager(
val takeN = minOf(nMax, cap, list.size)
if (takeN <= 0) {
val p0 = GCSFilter.deriveP(fpr)
return RequestSyncPacket(p = p0, m = 1, data = ByteArray(0)).encode()
return RequestSyncPacket(p = p0, m = 1, data = ByteArray(0), wantedTypes = wantedTypes, minTimestamp = minTimestamp).encode()
}
val ids = list.take(takeN).map { pkt -> PacketIdUtil.computeIdBytes(pkt) }
val params = GCSFilter.buildFilter(ids, maxBytes, fpr)
val mVal = if (params.m <= 0L) 1 else params.m
return RequestSyncPacket(p = params.p, m = mVal, data = params.data).encode()
return RequestSyncPacket(p = params.p, m = mVal, data = params.data, wantedTypes = wantedTypes, minTimestamp = minTimestamp).encode()
}
// Periodically remove stale announcements and all their messages

View File

@ -0,0 +1,74 @@
package com.bitchat.android.protocol
import com.bitchat.android.model.RequestSyncPacket
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
class RequestSyncPacketTest {
@Test
fun testBaseFieldsRoundTrip() {
val original = RequestSyncPacket(
p = 7,
m = 12800L,
data = byteArrayOf(1, 2, 3, 4, 5)
)
val encoded = original.encode()
val decoded = RequestSyncPacket.decode(encoded)
assertNotNull(decoded)
assertEquals(7, decoded!!.p)
assertEquals(12800L, decoded.m)
assertTrue(original.data.contentEquals(decoded.data))
assertNull(decoded.wantedTypes)
assertNull(decoded.minTimestamp)
}
@Test
fun testUpgradedFieldsRoundTrip() {
val original = RequestSyncPacket(
p = 8,
m = 25600L,
data = byteArrayOf(10, 20, 30),
wantedTypes = listOf(0x01u, 0x02u),
minTimestamp = 1700000000000uL
)
val encoded = original.encode()
val decoded = RequestSyncPacket.decode(encoded)
assertNotNull(decoded)
assertEquals(8, decoded!!.p)
assertEquals(25600L, decoded.m)
assertTrue(original.data.contentEquals(decoded.data))
assertNotNull(decoded.wantedTypes)
assertEquals(2, decoded.wantedTypes!!.size)
assertEquals(0x01u.toUByte(), decoded.wantedTypes!![0])
assertEquals(0x02u.toUByte(), decoded.wantedTypes!![1])
assertEquals(1700000000000uL, decoded.minTimestamp)
}
@Test
fun testLegacyCompatibleDecode() {
// Construct a raw legacy payload manually without fields 0x04 or 0x05
// Payload consists of:
// Type 0x01: length 1, value P (7)
// Type 0x02: length 4, value M (12800 -> 0x00003200)
// Type 0x03: length 3, value data (1, 2, 3)
val payload = byteArrayOf(
0x01, 0x00, 0x01, 0x07, // P
0x02, 0x00, 0x04, 0x00, 0x00, 0x32, 0x00, // M
0x03, 0x00, 0x03, 0x01, 0x02, 0x03 // data
)
val decoded = RequestSyncPacket.decode(payload)
assertNotNull(decoded)
assertEquals(7, decoded!!.p)
assertEquals(12800L, decoded.m)
assertTrue(byteArrayOf(1, 2, 3).contentEquals(decoded.data))
assertNull(decoded.wantedTypes)
assertNull(decoded.minTimestamp)
}
}