mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 01:25:20 +00:00
wip: SBR only for v2
This commit is contained in:
@@ -161,7 +161,9 @@ struct BinaryProtocol {
|
||||
}
|
||||
|
||||
let lengthFieldBytes = lengthFieldSize(for: version)
|
||||
let originalRoute = packet.route ?? []
|
||||
|
||||
// Route is only supported for v2+ packets (per SOURCE_ROUTING.md spec)
|
||||
let originalRoute = (version >= 2) ? (packet.route ?? []) : []
|
||||
if originalRoute.contains(where: { $0.isEmpty }) { return nil }
|
||||
let sanitizedRoute: [Data] = originalRoute.map { hop in
|
||||
if hop.count == senderIDSize { return hop }
|
||||
@@ -175,14 +177,15 @@ struct BinaryProtocol {
|
||||
let hasRoute = !sanitizedRoute.isEmpty
|
||||
let routeLength = hasRoute ? 1 + sanitizedRoute.count * senderIDSize : 0
|
||||
let originalSizeFieldBytes = isCompressed ? lengthFieldBytes : 0
|
||||
let payloadDataSize = routeLength + payload.count + originalSizeFieldBytes
|
||||
// payloadLength in header is payload-only (does NOT include route bytes)
|
||||
let payloadDataSize = payload.count + originalSizeFieldBytes
|
||||
|
||||
if version == 1 && payloadDataSize > Int(UInt16.max) { return nil }
|
||||
if version == 2 && payloadDataSize > Int(UInt32.max) { return nil }
|
||||
|
||||
guard let headerSize = headerSize(for: version) else { return nil }
|
||||
let estimatedHeader = headerSize + senderIDSize + (packet.recipientID == nil ? 0 : recipientIDSize)
|
||||
let estimatedPayload = payloadDataSize
|
||||
let estimatedPayload = payloadDataSize + routeLength
|
||||
let estimatedSignature = (packet.signature == nil ? 0 : signatureSize)
|
||||
var data = Data()
|
||||
data.reserveCapacity(estimatedHeader + estimatedPayload + estimatedSignature + 255)
|
||||
@@ -199,7 +202,8 @@ struct BinaryProtocol {
|
||||
if packet.recipientID != nil { flags |= Flags.hasRecipient }
|
||||
if packet.signature != nil { flags |= Flags.hasSignature }
|
||||
if isCompressed { flags |= Flags.isCompressed }
|
||||
if hasRoute { flags |= Flags.hasRoute }
|
||||
// HAS_ROUTE is only valid for v2+ packets
|
||||
if hasRoute && version >= 2 { flags |= Flags.hasRoute }
|
||||
data.append(flags)
|
||||
|
||||
if version == 2 {
|
||||
@@ -323,6 +327,8 @@ struct BinaryProtocol {
|
||||
let hasRecipient = (flags & Flags.hasRecipient) != 0
|
||||
let hasSignature = (flags & Flags.hasSignature) != 0
|
||||
let isCompressed = (flags & Flags.isCompressed) != 0
|
||||
// HAS_ROUTE is only valid for v2+ packets; ignore the flag for v1
|
||||
let hasRoute = (version >= 2) && (flags & Flags.hasRoute) != 0
|
||||
|
||||
let payloadLength: Int
|
||||
if version == 2 {
|
||||
@@ -343,27 +349,24 @@ struct BinaryProtocol {
|
||||
if recipientID == nil { return nil }
|
||||
}
|
||||
|
||||
// Route (optional, v2+ only): route bytes are NOT included in payloadLength
|
||||
var route: [Data]? = nil
|
||||
var remainingPayloadBytes = payloadLength
|
||||
|
||||
if (flags & Flags.hasRoute) != 0 {
|
||||
guard remainingPayloadBytes >= 1, let routeCount = read8() else { return nil }
|
||||
remainingPayloadBytes -= 1
|
||||
if hasRoute {
|
||||
guard let routeCount = read8() else { return nil }
|
||||
if routeCount > 0 {
|
||||
var hops: [Data] = []
|
||||
for _ in 0..<Int(routeCount) {
|
||||
guard remainingPayloadBytes >= senderIDSize,
|
||||
let hop = readData(senderIDSize) else { return nil }
|
||||
remainingPayloadBytes -= senderIDSize
|
||||
guard let hop = readData(senderIDSize) else { return nil }
|
||||
hops.append(hop)
|
||||
}
|
||||
route = hops
|
||||
}
|
||||
}
|
||||
|
||||
// Payload: payloadLength is exactly the payload size (+ compression preamble if compressed)
|
||||
let payload: Data
|
||||
if isCompressed {
|
||||
guard remainingPayloadBytes >= lengthFieldBytes else { return nil }
|
||||
guard payloadLength >= lengthFieldBytes else { return nil }
|
||||
let originalSize: Int
|
||||
if version == 2 {
|
||||
guard let rawSize = read32() else { return nil }
|
||||
@@ -372,11 +375,9 @@ struct BinaryProtocol {
|
||||
guard let rawSize = read16() else { return nil }
|
||||
originalSize = Int(rawSize)
|
||||
}
|
||||
remainingPayloadBytes -= lengthFieldBytes
|
||||
guard originalSize >= 0 && originalSize <= FileTransferLimits.maxFramedFileBytes else { return nil }
|
||||
let compressedSize = remainingPayloadBytes
|
||||
let compressedSize = payloadLength - lengthFieldBytes
|
||||
guard compressedSize > 0, let compressed = readData(compressedSize) else { return nil }
|
||||
remainingPayloadBytes = 0
|
||||
|
||||
let compressionRatio = Double(originalSize) / Double(compressedSize)
|
||||
guard compressionRatio <= 50_000.0 else {
|
||||
@@ -388,9 +389,7 @@ struct BinaryProtocol {
|
||||
decompressed.count == originalSize else { return nil }
|
||||
payload = decompressed
|
||||
} else {
|
||||
guard remainingPayloadBytes >= 0,
|
||||
let rawPayload = readData(remainingPayloadBytes) else { return nil }
|
||||
remainingPayloadBytes = 0
|
||||
guard let rawPayload = readData(payloadLength) else { return nil }
|
||||
payload = rawPayload
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,8 @@ struct BinaryProtocolTests {
|
||||
#expect(decodedPacket.signature == TestConstants.testSignature)
|
||||
}
|
||||
|
||||
// MARK: - Source-Based Routing Tests (v2 only)
|
||||
|
||||
@Test func packetWithRouteRoundTrip() throws {
|
||||
let route: [Data] = [
|
||||
try #require(Data(hexString: "0102030405060708")),
|
||||
@@ -62,6 +64,7 @@ struct BinaryProtocolTests {
|
||||
try #require(Data(hexString: "2122232425262728"))
|
||||
]
|
||||
|
||||
// Route is only supported for v2+ packets
|
||||
var packet = BitchatPacket(
|
||||
type: 0x01,
|
||||
senderID: route[0],
|
||||
@@ -69,7 +72,8 @@ struct BinaryProtocolTests {
|
||||
timestamp: 1_720_000_000_000,
|
||||
payload: Data("route-test".utf8),
|
||||
signature: nil,
|
||||
ttl: 6
|
||||
ttl: 6,
|
||||
version: 2
|
||||
)
|
||||
packet.route = route
|
||||
|
||||
@@ -78,6 +82,7 @@ struct BinaryProtocolTests {
|
||||
#expect((flagsByte & BinaryProtocol.Flags.hasRoute) != 0)
|
||||
|
||||
let decoded = try #require(BinaryProtocol.decode(encoded), "Failed to decode packet with route")
|
||||
#expect(decoded.version == 2)
|
||||
let decodedRoute = try #require(decoded.route)
|
||||
#expect(decodedRoute.count == route.count)
|
||||
for (expected, actual) in zip(route, decodedRoute) {
|
||||
@@ -90,6 +95,7 @@ struct BinaryProtocolTests {
|
||||
let destination = try #require(Data(hexString: "8899aabbccddeeff"))
|
||||
let shortHop = Data([0xAA, 0xBB, 0xCC])
|
||||
|
||||
// Route is only supported for v2+ packets
|
||||
var packet = BitchatPacket(
|
||||
type: 0x02,
|
||||
senderID: sender,
|
||||
@@ -97,7 +103,8 @@ struct BinaryProtocolTests {
|
||||
timestamp: 1_730_000_000_000,
|
||||
payload: Data("pad-test".utf8),
|
||||
signature: nil,
|
||||
ttl: 5
|
||||
ttl: 5,
|
||||
version: 2
|
||||
)
|
||||
packet.route = [shortHop, destination]
|
||||
|
||||
@@ -117,6 +124,7 @@ struct BinaryProtocolTests {
|
||||
try #require(Data(hexString: "0202020202020202"))
|
||||
]
|
||||
let repeatedString = String(repeating: "compress-me", count: 150)
|
||||
// Route is only supported for v2+ packets
|
||||
var packet = BitchatPacket(
|
||||
type: 0x03,
|
||||
senderID: route[0],
|
||||
@@ -124,7 +132,8 @@ struct BinaryProtocolTests {
|
||||
timestamp: 1_740_000_000_000,
|
||||
payload: Data(repeatedString.utf8),
|
||||
signature: nil,
|
||||
ttl: 7
|
||||
ttl: 7,
|
||||
version: 2
|
||||
)
|
||||
packet.route = route
|
||||
|
||||
@@ -135,6 +144,146 @@ struct BinaryProtocolTests {
|
||||
#expect(decodedRoute == route)
|
||||
}
|
||||
|
||||
@Test func v1PacketIgnoresRouteOnEncode() throws {
|
||||
// v1 packets should NOT include route even if route is set on the packet object
|
||||
let route: [Data] = [
|
||||
try #require(Data(hexString: "0102030405060708")),
|
||||
try #require(Data(hexString: "1112131415161718"))
|
||||
]
|
||||
|
||||
var packet = BitchatPacket(
|
||||
type: 0x01,
|
||||
senderID: route[0],
|
||||
recipientID: route.last,
|
||||
timestamp: 1_720_000_000_000,
|
||||
payload: Data("v1-no-route".utf8),
|
||||
signature: nil,
|
||||
ttl: 6
|
||||
// version defaults to 1 (v1 packet)
|
||||
)
|
||||
packet.route = route // route is set but should be ignored for v1
|
||||
|
||||
let encoded = try #require(BinaryProtocol.encode(packet), "Failed to encode v1 packet")
|
||||
|
||||
// HAS_ROUTE flag should NOT be set for v1 packets
|
||||
let flagsByte = encoded[BinaryProtocol.Offsets.flags]
|
||||
#expect((flagsByte & BinaryProtocol.Flags.hasRoute) == 0, "v1 packet should not have HAS_ROUTE flag set")
|
||||
|
||||
// Decoded packet should have no route
|
||||
let decoded = try #require(BinaryProtocol.decode(encoded), "Failed to decode v1 packet")
|
||||
#expect(decoded.version == 1)
|
||||
#expect(decoded.route == nil, "v1 packet should decode with nil route")
|
||||
#expect(decoded.payload == Data("v1-no-route".utf8))
|
||||
}
|
||||
|
||||
@Test func v2PacketIncludesRouteOnEncode() throws {
|
||||
// v2 packets SHOULD include route when route is set
|
||||
let route: [Data] = [
|
||||
try #require(Data(hexString: "0102030405060708")),
|
||||
try #require(Data(hexString: "1112131415161718"))
|
||||
]
|
||||
|
||||
var packet = BitchatPacket(
|
||||
type: 0x01,
|
||||
senderID: route[0],
|
||||
recipientID: route.last,
|
||||
timestamp: 1_720_000_000_000,
|
||||
payload: Data("v2-with-route".utf8),
|
||||
signature: nil,
|
||||
ttl: 6,
|
||||
version: 2
|
||||
)
|
||||
packet.route = route
|
||||
|
||||
let encoded = try #require(BinaryProtocol.encode(packet), "Failed to encode v2 packet")
|
||||
|
||||
// HAS_ROUTE flag SHOULD be set for v2 packets with route
|
||||
let flagsByte = encoded[BinaryProtocol.Offsets.flags]
|
||||
#expect((flagsByte & BinaryProtocol.Flags.hasRoute) != 0, "v2 packet should have HAS_ROUTE flag set")
|
||||
|
||||
// Decoded packet should have route
|
||||
let decoded = try #require(BinaryProtocol.decode(encoded), "Failed to decode v2 packet")
|
||||
#expect(decoded.version == 2)
|
||||
let decodedRoute = try #require(decoded.route, "v2 packet should decode with route")
|
||||
#expect(decodedRoute.count == route.count)
|
||||
#expect(decoded.payload == Data("v2-with-route".utf8))
|
||||
}
|
||||
|
||||
@Test func v2PacketWithoutRouteDecodesCorrectly() throws {
|
||||
// v2 packet without route should still work
|
||||
let sender = try #require(Data(hexString: "0011223344556677"))
|
||||
let recipient = try #require(Data(hexString: "8899aabbccddeeff"))
|
||||
|
||||
let packet = BitchatPacket(
|
||||
type: 0x02,
|
||||
senderID: sender,
|
||||
recipientID: recipient,
|
||||
timestamp: 1_750_000_000_000,
|
||||
payload: Data("v2-no-route".utf8),
|
||||
signature: nil,
|
||||
ttl: 5,
|
||||
version: 2
|
||||
)
|
||||
// route is nil by default
|
||||
|
||||
let encoded = try #require(BinaryProtocol.encode(packet), "Failed to encode v2 packet without route")
|
||||
|
||||
// HAS_ROUTE flag should NOT be set when no route
|
||||
let flagsByte = encoded[BinaryProtocol.Offsets.flags]
|
||||
#expect((flagsByte & BinaryProtocol.Flags.hasRoute) == 0, "v2 packet without route should not have HAS_ROUTE flag")
|
||||
|
||||
let decoded = try #require(BinaryProtocol.decode(encoded), "Failed to decode v2 packet without route")
|
||||
#expect(decoded.version == 2)
|
||||
#expect(decoded.route == nil)
|
||||
#expect(decoded.payload == Data("v2-no-route".utf8))
|
||||
}
|
||||
|
||||
@Test func v1AndV2PayloadLengthDifference() throws {
|
||||
// Verify that payloadLength does NOT include route bytes
|
||||
// by comparing encoded sizes
|
||||
let route: [Data] = [
|
||||
try #require(Data(hexString: "0102030405060708"))
|
||||
]
|
||||
let payloadData = Data("test-payload".utf8)
|
||||
|
||||
// v1 packet (route ignored)
|
||||
var v1Packet = BitchatPacket(
|
||||
type: 0x01,
|
||||
senderID: route[0],
|
||||
recipientID: nil,
|
||||
timestamp: 1_720_000_000_000,
|
||||
payload: payloadData,
|
||||
signature: nil,
|
||||
ttl: 6
|
||||
// version defaults to 1
|
||||
)
|
||||
v1Packet.route = route // will be ignored for v1
|
||||
|
||||
// v2 packet with same payload but route included
|
||||
var v2Packet = BitchatPacket(
|
||||
type: 0x01,
|
||||
senderID: route[0],
|
||||
recipientID: nil,
|
||||
timestamp: 1_720_000_000_000,
|
||||
payload: payloadData,
|
||||
signature: nil,
|
||||
ttl: 6,
|
||||
version: 2
|
||||
)
|
||||
v2Packet.route = route
|
||||
|
||||
let v1Encoded = try #require(BinaryProtocol.encode(v1Packet, padding: false))
|
||||
let v2Encoded = try #require(BinaryProtocol.encode(v2Packet, padding: false))
|
||||
|
||||
// v2 should be larger by: 2 bytes (header length field difference) + 1 byte (route count) + 8 bytes (one hop)
|
||||
// Header: v1=14, v2=16 -> +2 bytes
|
||||
// Route: 1 + 8 = 9 bytes
|
||||
// Total expected difference: 11 bytes
|
||||
let expectedDiff = 2 + 1 + 8 // header diff + route count + one hop
|
||||
#expect(v2Encoded.count - v1Encoded.count == expectedDiff,
|
||||
"v2 packet should be \(expectedDiff) bytes larger than v1 (actual diff: \(v2Encoded.count - v1Encoded.count))")
|
||||
}
|
||||
|
||||
// MARK: - Compression Tests
|
||||
|
||||
@Test("Create a large, compressible payload above current threshold (2048B)")
|
||||
|
||||
Reference in New Issue
Block a user