mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 22:25:20 +00:00
Merge remote-tracking branch 'origin/feat/prekeys' into feat/integration-all
# Conflicts: # bitchat/Sync/GossipSyncManager.swift
This commit is contained in:
@@ -139,6 +139,7 @@ struct GossipSyncManagerTests {
|
||||
config.messageSyncIntervalSeconds = 1
|
||||
config.fragmentSyncIntervalSeconds = 1
|
||||
config.fileTransferSyncIntervalSeconds = 1
|
||||
config.prekeyBundleSyncIntervalSeconds = 1
|
||||
config.maintenanceIntervalSeconds = 0
|
||||
|
||||
let requestSyncManager = RequestSyncManager()
|
||||
@@ -195,19 +196,22 @@ struct GossipSyncManagerTests {
|
||||
manager._performMaintenanceSynchronously(now: Date())
|
||||
|
||||
// One request per due schedule so each type group gets the full
|
||||
// filter capacity: publicMessages, fragment, and fileTransfer.
|
||||
// filter capacity: publicMessages, fragment, fileTransfer, and
|
||||
// prekeyBundle.
|
||||
let sentPackets = delegate.packets
|
||||
#expect(sentPackets.count == 3)
|
||||
#expect(sentPackets.count == 4)
|
||||
let decoded = sentPackets.compactMap { RequestSyncPacket.decode(from: $0.payload) }
|
||||
#expect(decoded.count == 3)
|
||||
#expect(decoded.count == 4)
|
||||
let allTypes = decoded.compactMap(\.types).reduce(SyncTypeFlags(rawValue: 0)) { $0.union($1) }
|
||||
#expect(allTypes.contains(.announce))
|
||||
#expect(allTypes.contains(.message))
|
||||
#expect(allTypes.contains(.fragment))
|
||||
#expect(allTypes.contains(.fileTransfer))
|
||||
#expect(allTypes.contains(.prekeyBundle))
|
||||
#expect(decoded.contains { $0.types == .publicMessages })
|
||||
#expect(decoded.contains { $0.types == .fragment })
|
||||
#expect(decoded.contains { $0.types == .fileTransfer })
|
||||
#expect(decoded.contains { $0.types == .prekeyBundle })
|
||||
}
|
||||
|
||||
@Test func truncatedFilterCarriesSinceCursor() throws {
|
||||
@@ -292,6 +296,7 @@ struct GossipSyncManagerTests {
|
||||
config.messageSyncIntervalSeconds = 0
|
||||
config.fragmentSyncIntervalSeconds = 0
|
||||
config.fileTransferSyncIntervalSeconds = 0
|
||||
config.prekeyBundleSyncIntervalSeconds = 0
|
||||
|
||||
let requestSyncManager = RequestSyncManager()
|
||||
let manager = GossipSyncManager(myPeerID: myPeerID, config: config, requestSyncManager: requestSyncManager)
|
||||
@@ -401,6 +406,7 @@ struct GossipSyncManagerTests {
|
||||
config.messageSyncIntervalSeconds = 0
|
||||
config.fragmentSyncIntervalSeconds = 0
|
||||
config.fileTransferSyncIntervalSeconds = 0
|
||||
config.prekeyBundleSyncIntervalSeconds = 0
|
||||
config.responseRateLimitMaxResponses = 1
|
||||
config.responseRateLimitWindowSeconds = 60
|
||||
|
||||
@@ -455,6 +461,7 @@ struct GossipSyncManagerTests {
|
||||
#expect(types.contains(.message))
|
||||
#expect(types.contains(.fragment))
|
||||
#expect(types.contains(.fileTransfer))
|
||||
#expect(types.contains(.prekeyBundle))
|
||||
}
|
||||
|
||||
@Test func handleRequestSyncHonorsTypeFilter() async throws {
|
||||
@@ -507,6 +514,93 @@ struct GossipSyncManagerTests {
|
||||
#expect(sentPackets[0].type == MessageType.fragment.rawValue)
|
||||
}
|
||||
|
||||
@Test func prekeyBundlesServeSyncAndSurviveStalePeerCleanup() async throws {
|
||||
var config = GossipSyncManager.Config()
|
||||
config.messageSyncIntervalSeconds = 0
|
||||
config.fragmentSyncIntervalSeconds = 0
|
||||
config.fileTransferSyncIntervalSeconds = 0
|
||||
config.prekeyBundleSyncIntervalSeconds = 0
|
||||
config.stalePeerCleanupIntervalSeconds = 0
|
||||
config.stalePeerTimeoutSeconds = 5
|
||||
|
||||
let manager = GossipSyncManager(myPeerID: myPeerID, config: config, requestSyncManager: RequestSyncManager())
|
||||
let delegate = RecordingDelegate()
|
||||
manager.delegate = delegate
|
||||
|
||||
// Bundles are keyed by their authenticated identity (the noise static
|
||||
// key), not the packet senderID, so the payload must be a real bundle.
|
||||
let noiseKey = Data(repeating: 0xAB, count: 32)
|
||||
let senderPeer = PeerID(publicKey: noiseKey)
|
||||
let sender = try #require(Data(hexString: senderPeer.id))
|
||||
let bundle = PrekeyBundle(
|
||||
noiseStaticPublicKey: noiseKey,
|
||||
prekeys: [PrekeyBundle.Prekey(id: 0, publicKey: Data(repeating: 0x11, count: 32))],
|
||||
generatedAt: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||
signature: Data(count: PrekeyBundle.signatureLength)
|
||||
)
|
||||
let bundlePacket = BitchatPacket(
|
||||
type: MessageType.prekeyBundle.rawValue,
|
||||
senderID: sender,
|
||||
recipientID: nil,
|
||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||
payload: try #require(bundle.encode()),
|
||||
signature: nil,
|
||||
ttl: 1
|
||||
)
|
||||
manager.onPublicPacketSeen(bundlePacket)
|
||||
manager._performMaintenanceSynchronously(now: Date())
|
||||
#expect(manager._hasPrekeyBundle(for: senderPeer))
|
||||
|
||||
// Bundles outlive the owner's announce: a leave plus stale cleanup
|
||||
// must not drop them (they exist to reach offline owners).
|
||||
manager.removeAnnouncementForPeer(senderPeer)
|
||||
manager._performMaintenanceSynchronously(now: Date().addingTimeInterval(config.stalePeerTimeoutSeconds + 1))
|
||||
#expect(manager._hasPrekeyBundle(for: senderPeer))
|
||||
|
||||
// And a .prekeyBundle sync request is answered with the stored packet.
|
||||
let request = RequestSyncPacket(p: 7, m: 1, data: Data(), types: .prekeyBundle)
|
||||
manager.handleRequestSync(from: PeerID(str: "FFFFFFFFFFFFFFFF"), request: request)
|
||||
try await TestHelpers.waitFor({ delegate.packets.count == 1 }, timeout: TestConstants.shortTimeout)
|
||||
let served = try #require(delegate.packets.first)
|
||||
#expect(served.type == MessageType.prekeyBundle.rawValue)
|
||||
#expect(served.isRSR)
|
||||
}
|
||||
|
||||
@Test func prekeyBundleGossipIsKeyedByOwnerNotSenderID() {
|
||||
// One valid bundle re-broadcast under many fabricated sender IDs must
|
||||
// collapse to a single entry keyed by the bundle's own identity — the
|
||||
// spray-to-exhaust-the-cap DoS produces one entry, not N.
|
||||
let manager = GossipSyncManager(myPeerID: myPeerID, requestSyncManager: RequestSyncManager())
|
||||
let noiseKey = Data(repeating: 0xCD, count: 32)
|
||||
let ownerPeer = PeerID(publicKey: noiseKey)
|
||||
let bundle = PrekeyBundle(
|
||||
noiseStaticPublicKey: noiseKey,
|
||||
prekeys: [PrekeyBundle.Prekey(id: 0, publicKey: Data(repeating: 0x22, count: 32))],
|
||||
generatedAt: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||
signature: Data(count: PrekeyBundle.signatureLength)
|
||||
)
|
||||
guard let payload = bundle.encode() else { return }
|
||||
|
||||
for i in 0..<5 {
|
||||
let fakeSender = Data((0..<8).map { j in UInt8(truncatingIfNeeded: i * 31 + j) })
|
||||
let packet = BitchatPacket(
|
||||
type: MessageType.prekeyBundle.rawValue,
|
||||
senderID: fakeSender,
|
||||
recipientID: nil,
|
||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000) + UInt64(i),
|
||||
payload: payload,
|
||||
signature: nil,
|
||||
ttl: 1
|
||||
)
|
||||
manager.onPublicPacketSeen(packet)
|
||||
manager._performMaintenanceSynchronously(now: Date())
|
||||
// No fabricated sender ID ever creates its own entry.
|
||||
#expect(!manager._hasPrekeyBundle(for: PeerID(hexData: fakeSender)))
|
||||
}
|
||||
// Exactly the owner-keyed entry exists.
|
||||
#expect(manager._hasPrekeyBundle(for: ownerPeer))
|
||||
}
|
||||
|
||||
// MARK: - Archive persistence
|
||||
|
||||
@Test func publicMessagesRestoreFromArchiveAcrossRestart() async throws {
|
||||
|
||||
Reference in New Issue
Block a user