Add protocol version negotiation for future compatibility

- Add version negotiation messages (0x20 versionHello, 0x21 versionAck)
- Implement VersionHello and VersionAck message types with platform info
- Add ProtocolVersion struct for version management and negotiation
- Update BinaryProtocol to check supported versions
- Add version negotiation to connection flow before Noise handshake
- Maintain backward compatibility with legacy peers (assume v1)
- Add comprehensive test suite with 40+ test cases
- Update documentation with version negotiation details

This ensures BitChat clients can negotiate protocol versions for smooth
upgrades while maintaining full backward compatibility with existing clients.
This commit is contained in:
jack
2025-07-15 14:33:37 +02:00
parent 59927486eb
commit 3fb39b8a30
11 changed files with 1532 additions and 9 deletions
@@ -0,0 +1,297 @@
//
// VersionNegotiationScenarioTests.swift
// bitchatTests
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import XCTest
@testable import bitchat
class VersionNegotiationScenarioTests: XCTestCase {
// MARK: - Real-World Scenarios
func testOldClientConnectsToNewServer() {
// Scenario: Old client (v1 only) connects to new server (v1, v2, v3)
let oldClientVersions: [UInt8] = [1]
let newServerVersions: [UInt8] = [1, 2, 3]
let agreed = ProtocolVersion.negotiateVersion(
clientVersions: oldClientVersions,
serverVersions: newServerVersions
)
XCTAssertEqual(agreed, 1, "Should agree on v1 for backward compatibility")
}
func testNewClientConnectsToOldServer() {
// Scenario: New client (v1, v2, v3) connects to old server (v1 only)
let newClientVersions: [UInt8] = [1, 2, 3]
let oldServerVersions: [UInt8] = [1]
let agreed = ProtocolVersion.negotiateVersion(
clientVersions: newClientVersions,
serverVersions: oldServerVersions
)
XCTAssertEqual(agreed, 1, "Should agree on v1 for backward compatibility")
}
func testMixedVersionNetwork() {
// Scenario: Network with mixed client versions
let clients = [
[1], // Old client
[1, 2], // Mid-version client
[1, 2, 3] // New client
]
// All should be able to negotiate with each other
for (i, client1) in clients.enumerated() {
for (j, client2) in clients.enumerated() {
let agreed = ProtocolVersion.negotiateVersion(
clientVersions: client1,
serverVersions: client2
)
XCTAssertNotNil(agreed, "Clients \(i) and \(j) should negotiate successfully")
XCTAssertGreaterThanOrEqual(agreed ?? 0, 1, "Should at least agree on v1")
}
}
}
func testFutureClientWithUnsupportedVersion() {
// Scenario: Future client with only unsupported versions
let futureClientVersions: [UInt8] = [10, 11, 12]
let currentServerVersions: [UInt8] = Array(ProtocolVersion.supportedVersions)
let agreed = ProtocolVersion.negotiateVersion(
clientVersions: futureClientVersions,
serverVersions: currentServerVersions
)
XCTAssertNil(agreed, "Should fail to negotiate with incompatible future client")
}
// MARK: - Race Condition Tests
func testSimultaneousVersionHello() {
// Scenario: Both peers send version hello at the same time
// This tests that the protocol handles simultaneous negotiation
let hello1 = VersionHello(
supportedVersions: [1, 2],
preferredVersion: 2,
clientVersion: "1.1.0",
platform: "iOS"
)
let hello2 = VersionHello(
supportedVersions: [1, 2, 3],
preferredVersion: 3,
clientVersion: "1.2.0",
platform: "macOS"
)
// Both should be able to encode/decode regardless of order
XCTAssertNotNil(hello1.encode())
XCTAssertNotNil(hello2.encode())
// Version negotiation should be deterministic
let agreed1 = ProtocolVersion.negotiateVersion(
clientVersions: hello1.supportedVersions,
serverVersions: hello2.supportedVersions
)
let agreed2 = ProtocolVersion.negotiateVersion(
clientVersions: hello2.supportedVersions,
serverVersions: hello1.supportedVersions
)
XCTAssertEqual(agreed1, agreed2, "Negotiation should be symmetric")
XCTAssertEqual(agreed1, 2, "Should agree on highest common version")
}
// MARK: - Error Recovery Tests
func testRecoveryFromFailedNegotiation() {
// Test that a peer can retry after failed negotiation
var state = VersionNegotiationState.failed(reason: "Network error")
// Reset state for retry
state = .none
// Should be able to start new negotiation
state = .helloSent
if case .helloSent = state {
// Success - can retry after failure
} else {
XCTFail("Should be able to retry after failed negotiation")
}
}
func testPartialMessageHandling() {
// Test handling of truncated version messages
let truncatedData = Data([123, 34]) // Partial JSON
XCTAssertNil(VersionHello.decode(from: truncatedData))
XCTAssertNil(VersionAck.decode(from: truncatedData))
// Should not crash, just return nil
}
// MARK: - Platform Compatibility Tests
func testCrossPlatformNegotiation() {
let platforms = ["iOS", "macOS", "iPadOS", "Unknown"]
for platform1 in platforms {
for platform2 in platforms {
let hello1 = VersionHello(
clientVersion: "1.0.0",
platform: platform1
)
let hello2 = VersionHello(
clientVersion: "1.0.0",
platform: platform2
)
// All platforms should be able to negotiate
XCTAssertNotNil(hello1.encode())
XCTAssertNotNil(hello2.encode())
// Platform difference should not affect version negotiation
let agreed = ProtocolVersion.negotiateVersion(
clientVersions: hello1.supportedVersions,
serverVersions: hello2.supportedVersions
)
XCTAssertNotNil(agreed, "\(platform1) and \(platform2) should negotiate")
}
}
}
// MARK: - Capability Tests
func testCapabilityNegotiation() {
// Test future capability negotiation
let clientCapabilities = ["noise", "compression", "multipath"]
let serverCapabilities = ["noise", "compression", "federation"]
let hello = VersionHello(
clientVersion: "1.0.0",
platform: "iOS",
capabilities: clientCapabilities
)
let ack = VersionAck(
agreedVersion: 1,
serverVersion: "1.0.0",
platform: "macOS",
capabilities: serverCapabilities
)
// Find common capabilities (for future use)
let commonCapabilities = Set(clientCapabilities).intersection(Set(serverCapabilities))
XCTAssertEqual(commonCapabilities, ["noise", "compression"])
}
func testEmptyCapabilityHandling() {
// Test peers with no capabilities
let hello1 = VersionHello(
clientVersion: "1.0.0",
platform: "iOS",
capabilities: nil
)
let hello2 = VersionHello(
clientVersion: "1.0.0",
platform: "macOS",
capabilities: []
)
XCTAssertNotNil(hello1.encode())
XCTAssertNotNil(hello2.encode())
// Should still negotiate successfully
let agreed = ProtocolVersion.negotiateVersion(
clientVersions: hello1.supportedVersions,
serverVersions: hello2.supportedVersions
)
XCTAssertNotNil(agreed)
}
// MARK: - Stress Tests
func testManyVersionsNegotiation() {
// Test with many supported versions
let manyVersions = Array<UInt8>(1...100)
let someVersions = Array<UInt8>([1, 50, 75, 100])
let agreed = ProtocolVersion.negotiateVersion(
clientVersions: manyVersions,
serverVersions: someVersions
)
XCTAssertEqual(agreed, 100, "Should pick highest common version")
}
func testRapidConnectionDisconnection() {
// Test rapid connect/disconnect cycles
var states: [String: VersionNegotiationState] = [:]
for i in 0..<100 {
let peerID = "peer\(i)"
// Connect
states[peerID] = .helloSent
// Negotiate
states[peerID] = .ackReceived(version: 1)
// Disconnect
states.removeValue(forKey: peerID)
}
XCTAssertTrue(states.isEmpty, "All states should be cleaned up")
}
// MARK: - Security Tests
func testLargeVersionListDoS() {
// Test protection against DoS with huge version lists
let hugeVersionList = Array<UInt8>(0...255) // All possible versions
let hello = VersionHello(
supportedVersions: hugeVersionList,
preferredVersion: 255,
clientVersion: "1.0.0",
platform: "iOS"
)
// Should handle without performance issues
let startTime = Date()
_ = hello.encode()
let encodingTime = Date().timeIntervalSince(startTime)
XCTAssertLessThan(encodingTime, 0.1, "Encoding should be fast even with large version list")
}
func testVersionDowngradeAttack() {
// Test that negotiation always picks highest common version
// to prevent downgrade attacks
let clientVersions: [UInt8] = [1, 2, 3]
let serverVersions: [UInt8] = [1, 2, 3]
let agreed = ProtocolVersion.negotiateVersion(
clientVersions: clientVersions,
serverVersions: serverVersions
)
XCTAssertEqual(agreed, 3, "Should not allow downgrade to lower version")
}
}