mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-24 22:45:19 +00:00
Sync cleanups: normalize SyncTypeFlags, single announce-ID path, TODO (#1373)
Follow-ups deferred from the REQUEST_SYNC review (#1371): - SyncTypeFlags.init(rawValue:) now masks to the union of bits that map to a known message type (derived from the bit↔type table, so it tracks new types automatically). Phantom bits from a truncated/garbled flags field — or a type a newer peer added — no longer live in the set as membership no contains() matches yet toData() re-serializes. - GossipSyncManager stored each latest announce as (hex-id string, packet) and diffed announces against the stored string while every other type recomputed the ID via PacketIdUtil. Collapsed the store to just the packet and recompute the ID everywhere, removing the latent dual-path divergence. - Documented the REQUEST_SYNC TLV table and marked fragmentIdFilter (0x06) with a TODO(v2): it's parsed/re-serialized but never populated or honored (reserved for incremental fragment sync) — finish or drop, not silent dead surface. Adds SyncTypeFlags phantom-bit/round-trip tests and a GossipSyncManager test that an announce already in the requester's filter is suppressed (guards the recompute path). Full suite: 1034 tests pass. Co-authored-by: jack <jackjackbits@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
jack
Claude Opus 4.8
parent
7341696280
commit
c2a5668569
@@ -357,6 +357,44 @@ struct GossipSyncManagerTests {
|
||||
#expect(sentPackets.allSatisfy { $0.isRSR })
|
||||
}
|
||||
|
||||
@Test func handleRequestSyncSkipsAnnounceAlreadyInFilter() async throws {
|
||||
var config = GossipSyncManager.Config()
|
||||
config.messageSyncIntervalSeconds = 0
|
||||
config.fragmentSyncIntervalSeconds = 0
|
||||
config.fileTransferSyncIntervalSeconds = 0
|
||||
|
||||
let requestSyncManager = RequestSyncManager()
|
||||
let manager = GossipSyncManager(myPeerID: myPeerID, config: config, requestSyncManager: requestSyncManager)
|
||||
let delegate = RecordingDelegate()
|
||||
manager.delegate = delegate
|
||||
|
||||
let sender = try #require(Data(hexString: "aabbccddeeff0011"))
|
||||
let announcePacket = BitchatPacket(
|
||||
type: MessageType.announce.rawValue,
|
||||
senderID: sender,
|
||||
recipientID: nil,
|
||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||
payload: Data(),
|
||||
signature: nil,
|
||||
ttl: 1
|
||||
)
|
||||
manager.onPublicPacketSeen(announcePacket)
|
||||
|
||||
// A filter that already contains the announce's canonical ID must
|
||||
// suppress the response — this only holds if the responder recomputes
|
||||
// the ID the same way the filter was built (the dual-path bug would
|
||||
// diff a stored hex string instead).
|
||||
let announceID = PacketIdUtil.computeId(announcePacket)
|
||||
let params = GCSFilter.buildFilter(ids: [announceID], maxBytes: 256, targetFpr: 0.01)
|
||||
let request = RequestSyncPacket(p: params.p, m: params.m, data: params.data, types: .announce)
|
||||
|
||||
let peer = PeerID(str: "FFFFFFFFFFFFFFFF")
|
||||
manager.handleRequestSync(from: peer, request: request)
|
||||
// Barrier: the async handler is enqueued, so this sync flush runs after it.
|
||||
manager._performMaintenanceSynchronously(now: Date())
|
||||
#expect(delegate.packets.isEmpty)
|
||||
}
|
||||
|
||||
@Test func handleRequestSyncIsRateLimitedPerPeer() async throws {
|
||||
var config = GossipSyncManager.Config()
|
||||
config.seenCapacity = 5
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
import BitFoundation
|
||||
@testable import bitchat
|
||||
|
||||
struct SyncTypeFlagsTests {
|
||||
|
||||
@Test func knownTypesRoundTripThroughData() throws {
|
||||
let flags: SyncTypeFlags = [.announce, .message, .fragment, .fileTransfer]
|
||||
let data = try #require(flags.toData())
|
||||
let decoded = try #require(SyncTypeFlags.decode(data))
|
||||
#expect(decoded == flags)
|
||||
}
|
||||
|
||||
@Test func decodeDropsPhantomBits() {
|
||||
// Bits 8+ map to no message type. They must not survive decode as
|
||||
// phantom membership.
|
||||
let phantom = Data([0x00, 0xFF]) // bits 8..15 set, no known type
|
||||
let decoded = SyncTypeFlags.decode(phantom)
|
||||
#expect(decoded?.rawValue == 0)
|
||||
#expect(decoded?.toMessageTypes().isEmpty == true)
|
||||
}
|
||||
|
||||
@Test func phantomBitsAreStrippedButKnownBitsSurvive() {
|
||||
// Low byte = announce(0) + message(1); high byte = phantom.
|
||||
let mixed = Data([0b0000_0011, 0xFF])
|
||||
let decoded = SyncTypeFlags.decode(mixed)
|
||||
#expect(decoded?.contains(.announce) == true)
|
||||
#expect(decoded?.contains(.message) == true)
|
||||
// Only the two known bits remain; phantom high byte is gone.
|
||||
#expect(decoded?.rawValue == 0b0000_0011)
|
||||
}
|
||||
|
||||
@Test func rawValueInitNormalizesPhantomBits() {
|
||||
let flags = SyncTypeFlags(rawValue: 0xFFFF_FFFF_FFFF_FFFF)
|
||||
// Every known type bit is set; nothing above them survives, so the
|
||||
// field serializes to a single byte.
|
||||
#expect(flags.contains(.announce))
|
||||
#expect(flags.contains(.fileTransfer))
|
||||
let data = flags.toData()
|
||||
#expect(data?.count == 1)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user