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:
jack
2026-06-11 21:29:08 +02:00
co-authored by Claude Fable 5
parent 1480b51c76
commit 0b007eee1a
5 changed files with 178 additions and 7 deletions
+24 -6
View File
@@ -74,6 +74,8 @@ final class BLEService: NSObject {
private let identityManager: SecureIdentityStateManagerProtocol
private let keychain: KeychainManagerProtocol
private let idBridge: NostrIdentityBridge
/// Binary form of `myPeerID`; same contract mutated only inside a
/// `messageQueue` barrier via `refreshPeerIdentity()`.
private var myPeerIDData: Data = Data()
// MARK: - Advertising Privacy
@@ -407,8 +409,10 @@ final class BLEService: NSObject {
// MARK: Identity
/// Derived from the Noise identity fingerprint; rotated only via
/// `refreshPeerIdentity()` (e.g. panic reset). Externally read-only
/// no out-of-band mutation may bypass that derivation.
/// `refreshPeerIdentity()` (e.g. panic reset), which performs the swap
/// inside a `messageQueue` barrier so concurrent queue work never sees a
/// half-updated identity. Externally read-only no out-of-band mutation
/// may bypass that derivation.
private(set) var myPeerID = PeerID(str: "")
/// Externally read-only; mutate via `setNickname(_:)`, which also
/// broadcasts the change to peers.
@@ -2370,11 +2374,25 @@ extension BLEService {
}
}
/// Swaps `myPeerID`/`myPeerIDData` to match the current Noise identity.
/// The swap runs as a `messageQueue` barrier so in-flight work items that
/// read the identity (e.g. `sendMessage` building packets) complete
/// against the old value and everything after sees the new one atomically.
/// Callers (init, panic reset on the main thread) are never on
/// `messageQueue`; the re-entrancy check keeps any future on-queue caller
/// from deadlocking.
private func refreshPeerIdentity() {
let fingerprint = noiseService.getIdentityFingerprint()
myPeerID = PeerID(str: fingerprint.prefix(16))
myPeerIDData = Data(hexString: myPeerID.id) ?? Data()
meshTopology.reset()
let swap = {
let fingerprint = self.noiseService.getIdentityFingerprint()
self.myPeerID = PeerID(str: fingerprint.prefix(16))
self.myPeerIDData = Data(hexString: self.myPeerID.id) ?? Data()
self.meshTopology.reset()
}
if DispatchQueue.getSpecific(key: messageQueueKey) != nil {
swap()
} else {
messageQueue.sync(flags: .barrier, execute: swap)
}
}