mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-26 12:45:19 +00:00
Stabilize synchronous Noise restart tests
This commit is contained in:
@@ -645,14 +645,25 @@ struct BLEServiceCoreTests {
|
|||||||
let mallory = NoiseEncryptionService(keychain: MockKeychain())
|
let mallory = NoiseEncryptionService(keychain: MockKeychain())
|
||||||
let alicePeerID = PeerID(publicKey: alice.getStaticPublicKeyData())
|
let alicePeerID = PeerID(publicKey: alice.getStaticPublicKeyData())
|
||||||
|
|
||||||
let message1 = try ble._test_noiseInitiateHandshake(with: alicePeerID)
|
// Establish BLE as responder so the following inbound reconnect is
|
||||||
|
// not intentionally coalesced by the initiator-completion grace path.
|
||||||
|
let message1 = try alice.initiateHandshake(with: ble.myPeerID)
|
||||||
let message2 = try #require(
|
let message2 = try #require(
|
||||||
try alice.processHandshakeMessage(from: ble.myPeerID, message: message1)
|
try ble._test_noiseProcessHandshakeMessage(
|
||||||
|
from: alicePeerID,
|
||||||
|
message: message1
|
||||||
|
)
|
||||||
)
|
)
|
||||||
let message3 = try #require(
|
let message3 = try #require(
|
||||||
try ble._test_noiseProcessHandshakeMessage(from: alicePeerID, message: message2)
|
try alice.processHandshakeMessage(
|
||||||
|
from: ble.myPeerID,
|
||||||
|
message: message2
|
||||||
|
)
|
||||||
|
)
|
||||||
|
_ = try ble._test_noiseProcessHandshakeMessage(
|
||||||
|
from: alicePeerID,
|
||||||
|
message: message3
|
||||||
)
|
)
|
||||||
_ = try alice.processHandshakeMessage(from: ble.myPeerID, message: message3)
|
|
||||||
await ble._test_drainNoiseMessagePipeline()
|
await ble._test_drainNoiseMessagePipeline()
|
||||||
#expect(ble.canDeliverSecurely(to: alicePeerID))
|
#expect(ble.canDeliverSecurely(to: alicePeerID))
|
||||||
|
|
||||||
@@ -674,6 +685,9 @@ struct BLEServiceCoreTests {
|
|||||||
{
|
{
|
||||||
outbound.snapshot().contains {
|
outbound.snapshot().contains {
|
||||||
$0.type == MessageType.noiseHandshake.rawValue
|
$0.type == MessageType.noiseHandshake.rawValue
|
||||||
|
&& PeerID(hexData: $0.senderID) == ble.myPeerID
|
||||||
|
&& $0.payload.count
|
||||||
|
!= NoiseSecurityConstants.xxInitialMessageSize
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
timeout: TestConstants.longTimeout
|
timeout: TestConstants.longTimeout
|
||||||
@@ -682,6 +696,9 @@ struct BLEServiceCoreTests {
|
|||||||
let forgedMessage2 = try #require(
|
let forgedMessage2 = try #require(
|
||||||
outbound.snapshot().first {
|
outbound.snapshot().first {
|
||||||
$0.type == MessageType.noiseHandshake.rawValue
|
$0.type == MessageType.noiseHandshake.rawValue
|
||||||
|
&& PeerID(hexData: $0.senderID) == ble.myPeerID
|
||||||
|
&& $0.payload.count
|
||||||
|
!= NoiseSecurityConstants.xxInitialMessageSize
|
||||||
}?.payload
|
}?.payload
|
||||||
)
|
)
|
||||||
#expect(!ble.canDeliverSecurely(to: alicePeerID))
|
#expect(!ble.canDeliverSecurely(to: alicePeerID))
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import Testing
|
|||||||
@testable import BitFoundation // to avoid unnecessary public's
|
@testable import BitFoundation // to avoid unnecessary public's
|
||||||
@testable import bitchat
|
@testable import bitchat
|
||||||
|
|
||||||
|
@Suite("Integration Tests", .serialized)
|
||||||
struct IntegrationTests {
|
struct IntegrationTests {
|
||||||
|
|
||||||
private var helper = TestNetworkHelper()
|
private var helper = TestNetworkHelper()
|
||||||
@@ -272,8 +273,18 @@ struct IntegrationTests {
|
|||||||
// Re-establish Noise handshake explicitly via managers
|
// Re-establish Noise handshake explicitly via managers
|
||||||
do {
|
do {
|
||||||
let m1 = try helper.noiseManagers["Bob"]!.initiateHandshake(with: helper.nodes["Alice"]!.peerID)
|
let m1 = try helper.noiseManagers["Bob"]!.initiateHandshake(with: helper.nodes["Alice"]!.peerID)
|
||||||
let m2 = try helper.noiseManagers["Alice"]!.handleIncomingHandshake(from: helper.nodes["Bob"]!.peerID, message: m1)!
|
let m2 = try #require(
|
||||||
let m3 = try helper.noiseManagers["Bob"]!.handleIncomingHandshake(from: helper.nodes["Alice"]!.peerID, message: m2)!
|
try helper.noiseManagers["Alice"]!.handleIncomingHandshake(
|
||||||
|
from: helper.nodes["Bob"]!.peerID,
|
||||||
|
message: m1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
let m3 = try #require(
|
||||||
|
try helper.noiseManagers["Bob"]!.handleIncomingHandshake(
|
||||||
|
from: helper.nodes["Alice"]!.peerID,
|
||||||
|
message: m2
|
||||||
|
)
|
||||||
|
)
|
||||||
_ = try helper.noiseManagers["Alice"]!.handleIncomingHandshake(from: helper.nodes["Bob"]!.peerID, message: m3)
|
_ = try helper.noiseManagers["Alice"]!.handleIncomingHandshake(from: helper.nodes["Bob"]!.peerID, message: m3)
|
||||||
} catch {
|
} catch {
|
||||||
Issue.record("Failed to re-establish Noise session after restart: \(error)")
|
Issue.record("Failed to re-establish Noise session after restart: \(error)")
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import CryptoKit
|
import CryptoKit
|
||||||
|
import Testing
|
||||||
@testable import BitFoundation // to avoid unnecessary public's
|
@testable import BitFoundation // to avoid unnecessary public's
|
||||||
@testable import bitchat
|
@testable import bitchat
|
||||||
|
|
||||||
@@ -27,9 +28,14 @@ final class TestNetworkHelper {
|
|||||||
node.mockNickname = name
|
node.mockNickname = name
|
||||||
nodes[name] = node
|
nodes[name] = node
|
||||||
|
|
||||||
// Create/replace Noise manager for this node
|
// This synchronous helper directly drives all three XX messages and
|
||||||
|
// has no transport callback loop for delayed collision recovery.
|
||||||
let key = Curve25519.KeyAgreement.PrivateKey()
|
let key = Curve25519.KeyAgreement.PrivateKey()
|
||||||
noiseManagers[name] = NoiseSessionManager(localStaticKey: key, keychain: mockKeychain)
|
noiseManagers[name] = NoiseSessionManager(
|
||||||
|
localStaticKey: key,
|
||||||
|
keychain: mockKeychain,
|
||||||
|
recentInitiatorCompletionGracePeriod: 0
|
||||||
|
)
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,8 +114,18 @@ final class TestNetworkHelper {
|
|||||||
let peer2ID = nodes[node2]?.peerID else { return }
|
let peer2ID = nodes[node2]?.peerID else { return }
|
||||||
|
|
||||||
let msg1 = try manager1.initiateHandshake(with: peer2ID)
|
let msg1 = try manager1.initiateHandshake(with: peer2ID)
|
||||||
let msg2 = try manager2.handleIncomingHandshake(from: peer1ID, message: msg1)!
|
let msg2 = try #require(
|
||||||
let msg3 = try manager1.handleIncomingHandshake(from: peer2ID, message: msg2)!
|
try manager2.handleIncomingHandshake(
|
||||||
|
from: peer1ID,
|
||||||
|
message: msg1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
let msg3 = try #require(
|
||||||
|
try manager1.handleIncomingHandshake(
|
||||||
|
from: peer2ID,
|
||||||
|
message: msg2
|
||||||
|
)
|
||||||
|
)
|
||||||
_ = try manager2.handleIncomingHandshake(from: peer1ID, message: msg3)
|
_ = try manager2.handleIncomingHandshake(from: peer1ID, message: msg3)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -357,8 +357,18 @@ struct NoiseProtocolTests {
|
|||||||
|
|
||||||
@Test func peerRestartDetection() throws {
|
@Test func peerRestartDetection() throws {
|
||||||
// Establish initial sessions
|
// Establish initial sessions
|
||||||
let aliceManager = NoiseSessionManager(localStaticKey: aliceKey, keychain: mockKeychain)
|
// This test explicitly drives the three synchronous XX messages and
|
||||||
let bobManager = NoiseSessionManager(localStaticKey: bobKey, keychain: mockKeychain)
|
// does not exercise the transport's delayed collision recovery.
|
||||||
|
let aliceManager = NoiseSessionManager(
|
||||||
|
localStaticKey: aliceKey,
|
||||||
|
keychain: mockKeychain,
|
||||||
|
recentInitiatorCompletionGracePeriod: 0
|
||||||
|
)
|
||||||
|
let bobManager = NoiseSessionManager(
|
||||||
|
localStaticKey: bobKey,
|
||||||
|
keychain: mockKeychain,
|
||||||
|
recentInitiatorCompletionGracePeriod: 0
|
||||||
|
)
|
||||||
|
|
||||||
try establishManagerSessions(aliceManager: aliceManager, bobManager: bobManager)
|
try establishManagerSessions(aliceManager: aliceManager, bobManager: bobManager)
|
||||||
|
|
||||||
@@ -377,15 +387,24 @@ struct NoiseProtocolTests {
|
|||||||
let newHandshake1 = try bobManagerRestarted.initiateHandshake(with: bobPeerID)
|
let newHandshake1 = try bobManagerRestarted.initiateHandshake(with: bobPeerID)
|
||||||
|
|
||||||
// Alice should accept the new handshake (clearing old session)
|
// Alice should accept the new handshake (clearing old session)
|
||||||
let newHandshake2 = try aliceManager.handleIncomingHandshake(
|
let newHandshake2 = try #require(
|
||||||
from: alicePeerID, message: newHandshake1)
|
try aliceManager.handleIncomingHandshake(
|
||||||
#expect(newHandshake2 != nil)
|
from: alicePeerID,
|
||||||
|
message: newHandshake1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
// Complete the new handshake
|
// Complete the new handshake
|
||||||
let newHandshake3 = try bobManagerRestarted.handleIncomingHandshake(
|
let newHandshake3 = try #require(
|
||||||
from: bobPeerID, message: newHandshake2!)
|
try bobManagerRestarted.handleIncomingHandshake(
|
||||||
#expect(newHandshake3 != nil)
|
from: bobPeerID,
|
||||||
_ = try aliceManager.handleIncomingHandshake(from: alicePeerID, message: newHandshake3!)
|
message: newHandshake2
|
||||||
|
)
|
||||||
|
)
|
||||||
|
_ = try aliceManager.handleIncomingHandshake(
|
||||||
|
from: alicePeerID,
|
||||||
|
message: newHandshake3
|
||||||
|
)
|
||||||
|
|
||||||
// Should be able to exchange messages with new sessions
|
// Should be able to exchange messages with new sessions
|
||||||
let testMessage = Data("After restart".utf8)
|
let testMessage = Data("After restart".utf8)
|
||||||
@@ -543,8 +562,18 @@ struct NoiseProtocolTests {
|
|||||||
|
|
||||||
@Test func nonceDesynchronizationCausesRehandshake() throws {
|
@Test func nonceDesynchronizationCausesRehandshake() throws {
|
||||||
// Test that nonce desynchronization leads to proper re-handshake
|
// Test that nonce desynchronization leads to proper re-handshake
|
||||||
let aliceManager = NoiseSessionManager(localStaticKey: aliceKey, keychain: mockKeychain)
|
// This test explicitly drives the three synchronous XX messages and
|
||||||
let bobManager = NoiseSessionManager(localStaticKey: bobKey, keychain: mockKeychain)
|
// does not exercise the transport's delayed collision recovery.
|
||||||
|
let aliceManager = NoiseSessionManager(
|
||||||
|
localStaticKey: aliceKey,
|
||||||
|
keychain: mockKeychain,
|
||||||
|
recentInitiatorCompletionGracePeriod: 0
|
||||||
|
)
|
||||||
|
let bobManager = NoiseSessionManager(
|
||||||
|
localStaticKey: bobKey,
|
||||||
|
keychain: mockKeychain,
|
||||||
|
recentInitiatorCompletionGracePeriod: 0
|
||||||
|
)
|
||||||
|
|
||||||
// Establish sessions
|
// Establish sessions
|
||||||
try establishManagerSessions(aliceManager: aliceManager, bobManager: bobManager)
|
try establishManagerSessions(aliceManager: aliceManager, bobManager: bobManager)
|
||||||
@@ -572,15 +601,25 @@ struct NoiseProtocolTests {
|
|||||||
let rehandshake1 = try bobManager.initiateHandshake(with: bobPeerID)
|
let rehandshake1 = try bobManager.initiateHandshake(with: bobPeerID)
|
||||||
|
|
||||||
// Alice should accept despite having a "valid" (but desynced) session
|
// Alice should accept despite having a "valid" (but desynced) session
|
||||||
let rehandshake2 = try aliceManager.handleIncomingHandshake(
|
let rehandshake2 = try #require(
|
||||||
from: alicePeerID, message: rehandshake1)
|
try aliceManager.handleIncomingHandshake(
|
||||||
#expect(rehandshake2 != nil, "Alice should accept handshake to fix desync")
|
from: alicePeerID,
|
||||||
|
message: rehandshake1
|
||||||
|
),
|
||||||
|
"Alice should accept handshake to fix desync"
|
||||||
|
)
|
||||||
|
|
||||||
// Complete handshake
|
// Complete handshake
|
||||||
let rehandshake3 = try bobManager.handleIncomingHandshake(
|
let rehandshake3 = try #require(
|
||||||
from: bobPeerID, message: rehandshake2!)
|
try bobManager.handleIncomingHandshake(
|
||||||
#expect(rehandshake3 != nil)
|
from: bobPeerID,
|
||||||
_ = try aliceManager.handleIncomingHandshake(from: alicePeerID, message: rehandshake3!)
|
message: rehandshake2
|
||||||
|
)
|
||||||
|
)
|
||||||
|
_ = try aliceManager.handleIncomingHandshake(
|
||||||
|
from: alicePeerID,
|
||||||
|
message: rehandshake3
|
||||||
|
)
|
||||||
|
|
||||||
// Verify communication works again
|
// Verify communication works again
|
||||||
let testResynced = Data("Resynced".utf8)
|
let testResynced = Data("Resynced".utf8)
|
||||||
|
|||||||
@@ -666,15 +666,17 @@ struct NoiseEncryptionServiceTests {
|
|||||||
|
|
||||||
@Test("Claim gives an attempt a full on-wire timeout window")
|
@Test("Claim gives an attempt a full on-wire timeout window")
|
||||||
func handshakeClaimRearmsDeadline() async throws {
|
func handshakeClaimRearmsDeadline() async throws {
|
||||||
|
let timeoutInterval: TimeInterval = 1
|
||||||
let service = NoiseEncryptionService(
|
let service = NoiseEncryptionService(
|
||||||
keychain: MockKeychain(),
|
keychain: MockKeychain(),
|
||||||
ordinaryHandshakeTimeout: 0.08
|
ordinaryHandshakeTimeout: timeoutInterval
|
||||||
)
|
)
|
||||||
let peerID = PeerID(str: "1021324354657687")
|
let peerID = PeerID(str: "1021324354657687")
|
||||||
let recorder = HandshakeStartRecorder()
|
let recorder = HandshakeStartRecorder()
|
||||||
service.onHandshakeRecoveryRequired = { [weak service] request in
|
service.onHandshakeRecoveryRequired = { [weak service] request in
|
||||||
|
let firedAt = DispatchTime.now().uptimeNanoseconds
|
||||||
service?.cancelHandshakeRecovery(request)
|
service?.cancelHandshakeRecovery(request)
|
||||||
recorder.recordTimeout()
|
recorder.recordTimeout(at: firedAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
let attempt = try #require(
|
let attempt = try #require(
|
||||||
@@ -683,19 +685,24 @@ struct NoiseEncryptionServiceTests {
|
|||||||
retryOnTimeout: true
|
retryOnTimeout: true
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
try? await Task.sleep(nanoseconds: 50_000_000)
|
try? await Task.sleep(nanoseconds: 500_000_000)
|
||||||
#expect(
|
let claimed = service.claimHandshakeInitiation(attempt, for: peerID)
|
||||||
service.claimHandshakeInitiation(attempt, for: peerID)
|
let claimedAt = DispatchTime.now().uptimeNanoseconds
|
||||||
== attempt.payload
|
#expect(claimed == attempt.payload)
|
||||||
)
|
|
||||||
try? await Task.sleep(nanoseconds: 45_000_000)
|
|
||||||
#expect(service.hasSession(with: peerID))
|
|
||||||
#expect(recorder.timeoutCount == 0)
|
|
||||||
let expired = await TestHelpers.waitUntil(
|
let expired = await TestHelpers.waitUntil(
|
||||||
{ recorder.timeoutCount == 1 },
|
{ recorder.timeoutCount == 1 },
|
||||||
timeout: 1
|
timeout: 5
|
||||||
)
|
)
|
||||||
#expect(expired)
|
#expect(expired)
|
||||||
|
let firedAt = try #require(recorder.firstTimeoutUptimeNanoseconds)
|
||||||
|
try #require(firedAt >= claimedAt)
|
||||||
|
let elapsed = TimeInterval(firedAt - claimedAt) / 1_000_000_000
|
||||||
|
// A non-rearmed deadline would fire roughly 0.5 seconds after the
|
||||||
|
// claim. Measure on the timeout queue instead of relying on a task to
|
||||||
|
// resume inside a narrow pre-deadline window under parallel CI load.
|
||||||
|
#expect(elapsed >= timeoutInterval * 0.75)
|
||||||
|
#expect(!service.hasSession(with: peerID))
|
||||||
|
#expect(recorder.timeoutCount == 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test("Duplicate spoofed message one cannot extend rollback or repause during cooldown")
|
@Test("Duplicate spoofed message one cannot extend rollback or repause during cooldown")
|
||||||
@@ -723,7 +730,8 @@ struct NoiseEncryptionServiceTests {
|
|||||||
message: spoofedMessage1
|
message: spoofedMessage1
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
try? await Task.sleep(nanoseconds: 35_000_000)
|
// Exercise replacement before yielding: the test runner may resume a
|
||||||
|
// short sleep after the fixed responder deadline under parallel load.
|
||||||
_ = try #require(
|
_ = try #require(
|
||||||
try bob.processHandshakeMessage(
|
try bob.processHandshakeMessage(
|
||||||
from: alicePeerID,
|
from: alicePeerID,
|
||||||
@@ -1505,6 +1513,7 @@ private final class HandshakeStartRecorder: @unchecked Sendable {
|
|||||||
private var storedMessages: [Data] = []
|
private var storedMessages: [Data] = []
|
||||||
private var storedErrorCount = 0
|
private var storedErrorCount = 0
|
||||||
private var storedTimeoutCount = 0
|
private var storedTimeoutCount = 0
|
||||||
|
private var storedTimeoutUptimes: [UInt64] = []
|
||||||
|
|
||||||
var messages: [Data] {
|
var messages: [Data] {
|
||||||
lock.lock()
|
lock.lock()
|
||||||
@@ -1524,6 +1533,12 @@ private final class HandshakeStartRecorder: @unchecked Sendable {
|
|||||||
return storedTimeoutCount
|
return storedTimeoutCount
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var firstTimeoutUptimeNanoseconds: UInt64? {
|
||||||
|
lock.lock()
|
||||||
|
defer { lock.unlock() }
|
||||||
|
return storedTimeoutUptimes.first
|
||||||
|
}
|
||||||
|
|
||||||
func record(message: Data?) {
|
func record(message: Data?) {
|
||||||
guard let message else { return }
|
guard let message else { return }
|
||||||
lock.lock()
|
lock.lock()
|
||||||
@@ -1537,9 +1552,12 @@ private final class HandshakeStartRecorder: @unchecked Sendable {
|
|||||||
lock.unlock()
|
lock.unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func recordTimeout() {
|
func recordTimeout(
|
||||||
|
at uptimeNanoseconds: UInt64 = DispatchTime.now().uptimeNanoseconds
|
||||||
|
) {
|
||||||
lock.lock()
|
lock.lock()
|
||||||
storedTimeoutCount += 1
|
storedTimeoutCount += 1
|
||||||
|
storedTimeoutUptimes.append(uptimeNanoseconds)
|
||||||
lock.unlock()
|
lock.unlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user