mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 13:05:18 +00:00
Close the final re-grade findings: parked EOSE fallback, panic atomicity, overflow visibility
EOSE callbacks parked while Tor is bootstrapping now get a fallback unblock at the standard 10s EOSE timeout (via the injected scheduler, generation-guarded, single-fire) instead of waiting up to ~225s for Tor-readiness retry exhaustion. The identity swap in refreshPeerIdentity runs inside a messageQueue barrier with re-entrancy guard, so a panic reset can no longer race in-flight packet builds (deadlock analysis documented; both call paths verified off-queue). Relay send-queue overflow drops now log a sampled warning. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -164,6 +164,23 @@ struct BLEServiceCoreTests {
|
||||
#expect(!ble._test_recordIngressIfNew(packet: packet, linkID: "central-b"))
|
||||
}
|
||||
|
||||
@Test
|
||||
func panicReset_rotatesPeerIDDerivedFromNewNoiseFingerprint() async throws {
|
||||
let ble = makeService()
|
||||
let originalPeerID = ble.myPeerID
|
||||
let originalFingerprint = ble.noiseIdentityFingerprint()
|
||||
#expect(originalPeerID == PeerID(str: originalFingerprint.prefix(16)))
|
||||
|
||||
ble.resetIdentityForPanic(currentNickname: "anon")
|
||||
|
||||
// The Noise identity is regenerated and the peer ID swaps with it
|
||||
// (atomically, behind a messageQueue barrier).
|
||||
let newFingerprint = ble.noiseIdentityFingerprint()
|
||||
#expect(newFingerprint != originalFingerprint)
|
||||
#expect(ble.myPeerID != originalPeerID)
|
||||
#expect(ble.myPeerID == PeerID(str: newFingerprint.prefix(16)))
|
||||
}
|
||||
|
||||
@Test
|
||||
func modifiedServices_rediscoverWhenBitChatServiceIsInvalidated() async throws {
|
||||
let otherService = CBUUID(string: "0000180F-0000-1000-8000-00805F9B34FB")
|
||||
|
||||
@@ -133,6 +133,99 @@ final class NostrRelayManagerTests: XCTestCase {
|
||||
XCTAssertTrue(context.sessionFactory.requestedURLs.isEmpty)
|
||||
}
|
||||
|
||||
func test_subscribe_parkedEOSEFiresAfterFallbackTimeoutWhenTorNeverBecomesReady() async {
|
||||
let relayURL = "wss://tor-eose-parked-fallback.example"
|
||||
let context = makeContext(permission: .denied, userTorEnabled: true, torEnforced: true, torIsReady: false)
|
||||
var eoseCount = 0
|
||||
|
||||
context.manager.subscribe(
|
||||
filter: makeFilter(),
|
||||
id: "tor-parked-fallback",
|
||||
relayUrls: [relayURL],
|
||||
handler: { _ in },
|
||||
onEOSE: { eoseCount += 1 }
|
||||
)
|
||||
|
||||
// Parking the callback schedules the normal EOSE fallback, not just
|
||||
// the Tor retry-exhaustion unblock (~minutes later).
|
||||
XCTAssertEqual(context.scheduler.scheduled.first?.delay, TransportConfig.nostrSubscriptionEOSEFallbackSeconds)
|
||||
XCTAssertEqual(eoseCount, 0)
|
||||
|
||||
context.scheduler.runNext()
|
||||
let unblocked = await waitUntil { eoseCount == 1 }
|
||||
XCTAssertTrue(unblocked)
|
||||
XCTAssertTrue(context.sessionFactory.requestedURLs.isEmpty)
|
||||
|
||||
// A later retry-exhaustion unblock must not fire the callback again.
|
||||
for _ in 0..<TransportConfig.nostrTorReadyMaxWaitAttempts {
|
||||
context.torWaiter.resolve(false)
|
||||
}
|
||||
try? await Task.sleep(nanoseconds: 20_000_000)
|
||||
XCTAssertEqual(eoseCount, 1)
|
||||
}
|
||||
|
||||
func test_subscribe_parkedEOSEFallbackIsNoOpWhenTorRecoversFirst() async throws {
|
||||
let relayURL = "wss://tor-eose-parked-recover.example"
|
||||
let context = makeContext(permission: .denied, userTorEnabled: true, torEnforced: true, torIsReady: false)
|
||||
var eoseCount = 0
|
||||
|
||||
context.manager.subscribe(
|
||||
filter: makeFilter(),
|
||||
id: "tor-parked-recover",
|
||||
relayUrls: [relayURL],
|
||||
handler: { _ in },
|
||||
onEOSE: { eoseCount += 1 }
|
||||
)
|
||||
|
||||
// Tor recovers before the fallback fires; the callback is promoted to
|
||||
// a real EOSE tracker when the subscription flushes.
|
||||
context.torWaiter.resolve(true)
|
||||
let subscribed = await waitUntil {
|
||||
context.sessionFactory.latestConnection(for: relayURL)?.sentStrings.count == 1
|
||||
}
|
||||
XCTAssertTrue(subscribed)
|
||||
|
||||
// The parked fallback (scheduled first) is now a no-op.
|
||||
context.scheduler.runNext()
|
||||
try? await Task.sleep(nanoseconds: 20_000_000)
|
||||
XCTAssertEqual(eoseCount, 0)
|
||||
|
||||
// The real EOSE still completes the initial load exactly once...
|
||||
try context.sessionFactory.latestConnection(for: relayURL)?.emitEOSE(subscriptionID: "tor-parked-recover")
|
||||
let eoseCompleted = await waitUntil { eoseCount == 1 }
|
||||
XCTAssertTrue(eoseCompleted)
|
||||
|
||||
// ...and the promoted tracker's own fallback does not double-fire.
|
||||
context.scheduler.runNext()
|
||||
try? await Task.sleep(nanoseconds: 20_000_000)
|
||||
XCTAssertEqual(eoseCount, 1)
|
||||
}
|
||||
|
||||
func test_subscribe_parkedEOSEFallbackIsNoOpAfterRetryExhaustionUnblock() async {
|
||||
let relayURL = "wss://tor-eose-parked-exhausted.example"
|
||||
let context = makeContext(permission: .denied, userTorEnabled: true, torEnforced: true, torIsReady: false)
|
||||
var eoseCount = 0
|
||||
|
||||
context.manager.subscribe(
|
||||
filter: makeFilter(),
|
||||
id: "tor-parked-exhausted",
|
||||
relayUrls: [relayURL],
|
||||
handler: { _ in },
|
||||
onEOSE: { eoseCount += 1 }
|
||||
)
|
||||
|
||||
// Retry exhaustion unblocks the parked callback first.
|
||||
for _ in 0..<TransportConfig.nostrTorReadyMaxWaitAttempts {
|
||||
context.torWaiter.resolve(false)
|
||||
}
|
||||
XCTAssertEqual(eoseCount, 1)
|
||||
|
||||
// The parked fallback finds nothing to do.
|
||||
context.scheduler.runNext()
|
||||
try? await Task.sleep(nanoseconds: 20_000_000)
|
||||
XCTAssertEqual(eoseCount, 1)
|
||||
}
|
||||
|
||||
func test_sendEvent_survivesFailedTorWaitAndSendsWhenTorRecovers() async throws {
|
||||
let relayURL = "wss://tor-send-retry.example"
|
||||
let context = makeContext(permission: .denied, userTorEnabled: true, torEnforced: true, torIsReady: false)
|
||||
|
||||
Reference in New Issue
Block a user