mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 02: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 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 }
|
if originalRoute.contains(where: { $0.isEmpty }) { return nil }
|
||||||
let sanitizedRoute: [Data] = originalRoute.map { hop in
|
let sanitizedRoute: [Data] = originalRoute.map { hop in
|
||||||
if hop.count == senderIDSize { return hop }
|
if hop.count == senderIDSize { return hop }
|
||||||
@@ -175,14 +177,15 @@ struct BinaryProtocol {
|
|||||||
let hasRoute = !sanitizedRoute.isEmpty
|
let hasRoute = !sanitizedRoute.isEmpty
|
||||||
let routeLength = hasRoute ? 1 + sanitizedRoute.count * senderIDSize : 0
|
let routeLength = hasRoute ? 1 + sanitizedRoute.count * senderIDSize : 0
|
||||||
let originalSizeFieldBytes = isCompressed ? lengthFieldBytes : 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 == 1 && payloadDataSize > Int(UInt16.max) { return nil }
|
||||||
if version == 2 && payloadDataSize > Int(UInt32.max) { return nil }
|
if version == 2 && payloadDataSize > Int(UInt32.max) { return nil }
|
||||||
|
|
||||||
guard let headerSize = headerSize(for: version) else { return nil }
|
guard let headerSize = headerSize(for: version) else { return nil }
|
||||||
let estimatedHeader = headerSize + senderIDSize + (packet.recipientID == nil ? 0 : recipientIDSize)
|
let estimatedHeader = headerSize + senderIDSize + (packet.recipientID == nil ? 0 : recipientIDSize)
|
||||||
let estimatedPayload = payloadDataSize
|
let estimatedPayload = payloadDataSize + routeLength
|
||||||
let estimatedSignature = (packet.signature == nil ? 0 : signatureSize)
|
let estimatedSignature = (packet.signature == nil ? 0 : signatureSize)
|
||||||
var data = Data()
|
var data = Data()
|
||||||
data.reserveCapacity(estimatedHeader + estimatedPayload + estimatedSignature + 255)
|
data.reserveCapacity(estimatedHeader + estimatedPayload + estimatedSignature + 255)
|
||||||
@@ -199,7 +202,8 @@ struct BinaryProtocol {
|
|||||||
if packet.recipientID != nil { flags |= Flags.hasRecipient }
|
if packet.recipientID != nil { flags |= Flags.hasRecipient }
|
||||||
if packet.signature != nil { flags |= Flags.hasSignature }
|
if packet.signature != nil { flags |= Flags.hasSignature }
|
||||||
if isCompressed { flags |= Flags.isCompressed }
|
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)
|
data.append(flags)
|
||||||
|
|
||||||
if version == 2 {
|
if version == 2 {
|
||||||
@@ -323,6 +327,8 @@ struct BinaryProtocol {
|
|||||||
let hasRecipient = (flags & Flags.hasRecipient) != 0
|
let hasRecipient = (flags & Flags.hasRecipient) != 0
|
||||||
let hasSignature = (flags & Flags.hasSignature) != 0
|
let hasSignature = (flags & Flags.hasSignature) != 0
|
||||||
let isCompressed = (flags & Flags.isCompressed) != 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
|
let payloadLength: Int
|
||||||
if version == 2 {
|
if version == 2 {
|
||||||
@@ -343,27 +349,24 @@ struct BinaryProtocol {
|
|||||||
if recipientID == nil { return nil }
|
if recipientID == nil { return nil }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Route (optional, v2+ only): route bytes are NOT included in payloadLength
|
||||||
var route: [Data]? = nil
|
var route: [Data]? = nil
|
||||||
var remainingPayloadBytes = payloadLength
|
if hasRoute {
|
||||||
|
guard let routeCount = read8() else { return nil }
|
||||||
if (flags & Flags.hasRoute) != 0 {
|
|
||||||
guard remainingPayloadBytes >= 1, let routeCount = read8() else { return nil }
|
|
||||||
remainingPayloadBytes -= 1
|
|
||||||
if routeCount > 0 {
|
if routeCount > 0 {
|
||||||
var hops: [Data] = []
|
var hops: [Data] = []
|
||||||
for _ in 0..<Int(routeCount) {
|
for _ in 0..<Int(routeCount) {
|
||||||
guard remainingPayloadBytes >= senderIDSize,
|
guard let hop = readData(senderIDSize) else { return nil }
|
||||||
let hop = readData(senderIDSize) else { return nil }
|
|
||||||
remainingPayloadBytes -= senderIDSize
|
|
||||||
hops.append(hop)
|
hops.append(hop)
|
||||||
}
|
}
|
||||||
route = hops
|
route = hops
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Payload: payloadLength is exactly the payload size (+ compression preamble if compressed)
|
||||||
let payload: Data
|
let payload: Data
|
||||||
if isCompressed {
|
if isCompressed {
|
||||||
guard remainingPayloadBytes >= lengthFieldBytes else { return nil }
|
guard payloadLength >= lengthFieldBytes else { return nil }
|
||||||
let originalSize: Int
|
let originalSize: Int
|
||||||
if version == 2 {
|
if version == 2 {
|
||||||
guard let rawSize = read32() else { return nil }
|
guard let rawSize = read32() else { return nil }
|
||||||
@@ -372,11 +375,9 @@ struct BinaryProtocol {
|
|||||||
guard let rawSize = read16() else { return nil }
|
guard let rawSize = read16() else { return nil }
|
||||||
originalSize = Int(rawSize)
|
originalSize = Int(rawSize)
|
||||||
}
|
}
|
||||||
remainingPayloadBytes -= lengthFieldBytes
|
|
||||||
guard originalSize >= 0 && originalSize <= FileTransferLimits.maxFramedFileBytes else { return nil }
|
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 }
|
guard compressedSize > 0, let compressed = readData(compressedSize) else { return nil }
|
||||||
remainingPayloadBytes = 0
|
|
||||||
|
|
||||||
let compressionRatio = Double(originalSize) / Double(compressedSize)
|
let compressionRatio = Double(originalSize) / Double(compressedSize)
|
||||||
guard compressionRatio <= 50_000.0 else {
|
guard compressionRatio <= 50_000.0 else {
|
||||||
@@ -388,9 +389,7 @@ struct BinaryProtocol {
|
|||||||
decompressed.count == originalSize else { return nil }
|
decompressed.count == originalSize else { return nil }
|
||||||
payload = decompressed
|
payload = decompressed
|
||||||
} else {
|
} else {
|
||||||
guard remainingPayloadBytes >= 0,
|
guard let rawPayload = readData(payloadLength) else { return nil }
|
||||||
let rawPayload = readData(remainingPayloadBytes) else { return nil }
|
|
||||||
remainingPayloadBytes = 0
|
|
||||||
payload = rawPayload
|
payload = rawPayload
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ struct BinaryProtocolTests {
|
|||||||
#expect(decodedPacket.signature == TestConstants.testSignature)
|
#expect(decodedPacket.signature == TestConstants.testSignature)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Source-Based Routing Tests (v2 only)
|
||||||
|
|
||||||
@Test func packetWithRouteRoundTrip() throws {
|
@Test func packetWithRouteRoundTrip() throws {
|
||||||
let route: [Data] = [
|
let route: [Data] = [
|
||||||
try #require(Data(hexString: "0102030405060708")),
|
try #require(Data(hexString: "0102030405060708")),
|
||||||
@@ -62,6 +64,7 @@ struct BinaryProtocolTests {
|
|||||||
try #require(Data(hexString: "2122232425262728"))
|
try #require(Data(hexString: "2122232425262728"))
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// Route is only supported for v2+ packets
|
||||||
var packet = BitchatPacket(
|
var packet = BitchatPacket(
|
||||||
type: 0x01,
|
type: 0x01,
|
||||||
senderID: route[0],
|
senderID: route[0],
|
||||||
@@ -69,7 +72,8 @@ struct BinaryProtocolTests {
|
|||||||
timestamp: 1_720_000_000_000,
|
timestamp: 1_720_000_000_000,
|
||||||
payload: Data("route-test".utf8),
|
payload: Data("route-test".utf8),
|
||||||
signature: nil,
|
signature: nil,
|
||||||
ttl: 6
|
ttl: 6,
|
||||||
|
version: 2
|
||||||
)
|
)
|
||||||
packet.route = route
|
packet.route = route
|
||||||
|
|
||||||
@@ -78,6 +82,7 @@ struct BinaryProtocolTests {
|
|||||||
#expect((flagsByte & BinaryProtocol.Flags.hasRoute) != 0)
|
#expect((flagsByte & BinaryProtocol.Flags.hasRoute) != 0)
|
||||||
|
|
||||||
let decoded = try #require(BinaryProtocol.decode(encoded), "Failed to decode packet with route")
|
let decoded = try #require(BinaryProtocol.decode(encoded), "Failed to decode packet with route")
|
||||||
|
#expect(decoded.version == 2)
|
||||||
let decodedRoute = try #require(decoded.route)
|
let decodedRoute = try #require(decoded.route)
|
||||||
#expect(decodedRoute.count == route.count)
|
#expect(decodedRoute.count == route.count)
|
||||||
for (expected, actual) in zip(route, decodedRoute) {
|
for (expected, actual) in zip(route, decodedRoute) {
|
||||||
@@ -90,6 +95,7 @@ struct BinaryProtocolTests {
|
|||||||
let destination = try #require(Data(hexString: "8899aabbccddeeff"))
|
let destination = try #require(Data(hexString: "8899aabbccddeeff"))
|
||||||
let shortHop = Data([0xAA, 0xBB, 0xCC])
|
let shortHop = Data([0xAA, 0xBB, 0xCC])
|
||||||
|
|
||||||
|
// Route is only supported for v2+ packets
|
||||||
var packet = BitchatPacket(
|
var packet = BitchatPacket(
|
||||||
type: 0x02,
|
type: 0x02,
|
||||||
senderID: sender,
|
senderID: sender,
|
||||||
@@ -97,7 +103,8 @@ struct BinaryProtocolTests {
|
|||||||
timestamp: 1_730_000_000_000,
|
timestamp: 1_730_000_000_000,
|
||||||
payload: Data("pad-test".utf8),
|
payload: Data("pad-test".utf8),
|
||||||
signature: nil,
|
signature: nil,
|
||||||
ttl: 5
|
ttl: 5,
|
||||||
|
version: 2
|
||||||
)
|
)
|
||||||
packet.route = [shortHop, destination]
|
packet.route = [shortHop, destination]
|
||||||
|
|
||||||
@@ -117,6 +124,7 @@ struct BinaryProtocolTests {
|
|||||||
try #require(Data(hexString: "0202020202020202"))
|
try #require(Data(hexString: "0202020202020202"))
|
||||||
]
|
]
|
||||||
let repeatedString = String(repeating: "compress-me", count: 150)
|
let repeatedString = String(repeating: "compress-me", count: 150)
|
||||||
|
// Route is only supported for v2+ packets
|
||||||
var packet = BitchatPacket(
|
var packet = BitchatPacket(
|
||||||
type: 0x03,
|
type: 0x03,
|
||||||
senderID: route[0],
|
senderID: route[0],
|
||||||
@@ -124,7 +132,8 @@ struct BinaryProtocolTests {
|
|||||||
timestamp: 1_740_000_000_000,
|
timestamp: 1_740_000_000_000,
|
||||||
payload: Data(repeatedString.utf8),
|
payload: Data(repeatedString.utf8),
|
||||||
signature: nil,
|
signature: nil,
|
||||||
ttl: 7
|
ttl: 7,
|
||||||
|
version: 2
|
||||||
)
|
)
|
||||||
packet.route = route
|
packet.route = route
|
||||||
|
|
||||||
@@ -135,6 +144,146 @@ struct BinaryProtocolTests {
|
|||||||
#expect(decodedRoute == route)
|
#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
|
// MARK: - Compression Tests
|
||||||
|
|
||||||
@Test("Create a large, compressible payload above current threshold (2048B)")
|
@Test("Create a large, compressible payload above current threshold (2048B)")
|
||||||
|
|||||||
Reference in New Issue
Block a user