wip: SBR only for v2

This commit is contained in:
callebtc
2026-01-07 15:11:46 +07:00
parent aff700a15e
commit d469704c34
2 changed files with 170 additions and 22 deletions
+18 -19
View File
@@ -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
} }
+152 -3
View File
@@ -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)")