Harden PTT audio and the 1.7.1 release (#1423)

Centralize PTT and voice audio-session ownership, harden courier/bridge/outbox delivery and recovery, correct location and delivery-state races, add privacy/release metadata, and ship reproducible universal Arti slices with Release CI coverage.

Validated by the full iOS suite, repeated audio/fragment/performance regressions, BitFoundation tests, strict lint and dead-code analysis, universal iOS Release builds, and iOS/macOS archives.
This commit is contained in:
jack
2026-07-10 14:04:09 +02:00
committed by GitHub
parent b205a55523
commit 820c933958
91 changed files with 8381 additions and 1083 deletions
@@ -25,6 +25,9 @@ struct BridgeCourierServiceTests {
var held: [CourierEnvelope] = []
var sealResult: CourierEnvelope?
var deliverResult = true
var openResult = true
/// nil leaves the simulated relay confirmation in flight.
var automaticPublishResult: Bool? = true
private(set) var publishedEvents: [NostrEvent] = []
private(set) var openedSubscriptions: [[String]] = []
@@ -33,14 +36,28 @@ struct BridgeCourierServiceTests {
private(set) var delivered: [(envelope: CourierEnvelope, peer: PeerID)] = []
private(set) var sealRequests: [(content: String, messageID: String, key: Data)] = []
private(set) var heldCooldowns: [TimeInterval] = []
private(set) var markedHeldEnvelopes: [CourierEnvelope] = []
private(set) var scheduledTimers: [(delay: TimeInterval, fire: @MainActor () -> Void)] = []
private(set) var pendingPublishCompletions: [@MainActor (Bool) -> Void] = []
let service: BridgeCourierService
init(dedupStore: BridgeDropDedupStore? = nil) {
service = BridgeCourierService(dedupStore: dedupStore)
init(now: @escaping () -> Date = Date.init, dedupStore: BridgeDropDedupStore? = nil) {
service = BridgeCourierService(now: now, dedupStore: dedupStore)
service.bridgeEnabled = { [weak self] in self?.bridgeOn ?? false }
service.relaysConnected = { [weak self] in self?.relaysConnected ?? false }
service.publishEvent = { [weak self] event in self?.publishedEvents.append(event) }
service.publishEvent = { [weak self] event, completion in
guard let self else {
completion(false)
return
}
self.publishedEvents.append(event)
if let result = self.automaticPublishResult {
completion(result)
} else {
self.pendingPublishCompletions.append(completion)
}
}
service.openSubscription = { [weak self] tags in self?.openedSubscriptions.append(tags) }
service.closeSubscription = { [weak self] in self?.closedSubscriptions += 1 }
service.myNoiseKey = { [weak self] in self?.myKey }
@@ -49,7 +66,10 @@ struct BridgeCourierServiceTests {
self?.sealRequests.append((content, messageID, key))
return self?.sealResult
}
service.openEnvelope = { [weak self] envelope in self?.openedEnvelopes.append(envelope) }
service.openEnvelope = { [weak self] envelope in
self?.openedEnvelopes.append(envelope)
return self?.openResult ?? false
}
service.deliverToPeer = { [weak self] envelope, peer in
self?.delivered.append((envelope, peer))
return self?.deliverResult ?? false
@@ -58,7 +78,17 @@ struct BridgeCourierServiceTests {
self?.heldCooldowns.append(cooldown)
return self?.held ?? []
}
service.scheduleTimer = { _, _ in } // timers driven manually
service.markHeldEnvelopePublished = { [weak self] envelope in
self?.markedHeldEnvelopes.append(envelope)
}
service.scheduleTimer = { [weak self] delay, fire in
self?.scheduledTimers.append((delay, fire))
}
}
func resolveNextPublish(_ succeeded: Bool) {
guard !pendingPublishCompletions.isEmpty else { return }
pendingPublishCompletions.removeFirst()(succeeded)
}
static func randomKey() -> Data {
@@ -121,6 +151,115 @@ struct BridgeCourierServiceTests {
#expect(fixture.sealRequests.isEmpty)
}
@Test func missingRelayPublisherDoesNotConsumeDurableDedupSlot() {
let fixture = Fixture()
let key = Fixture.randomKey()
fixture.sealResult = makeEnvelope(recipientKey: key)
fixture.service.publishEvent = nil
let messageID = UUID().uuidString
var results: [Bool] = []
fixture.service.depositDrop(content: "retry", messageID: messageID, recipientNoiseKey: key) { results.append($0) }
fixture.service.depositDrop(content: "retry", messageID: messageID, recipientNoiseKey: key) { results.append($0) }
#expect(fixture.sealRequests.count == 2)
#expect(fixture.publishedEvents.isEmpty)
#expect(results == [false, false])
}
@Test func relayRejectionDoesNotPersistDedupAndRetryCanSucceed() {
let fileURL = FileManager.default.temporaryDirectory
.appendingPathComponent("bridge-dedup-\(UUID().uuidString).json")
defer { try? FileManager.default.removeItem(at: fileURL) }
let key = Fixture.randomKey()
let messageID = UUID().uuidString
let failed = Fixture(dedupStore: BridgeDropDedupStore(fileURL: fileURL))
failed.sealResult = makeEnvelope(recipientKey: key)
failed.automaticPublishResult = false
var failedResults: [Bool] = []
failed.service.depositDrop(content: "retry", messageID: messageID, recipientNoiseKey: key) {
failedResults.append($0)
}
failed.service.flushDedupSnapshot()
#expect(failedResults == [false])
#expect(failed.publishedEvents.count == 1)
// A relaunch over the failed attempt must be allowed to send again.
let retry = Fixture(dedupStore: BridgeDropDedupStore(fileURL: fileURL))
retry.sealResult = makeEnvelope(recipientKey: key)
var retryResults: [Bool] = []
retry.service.depositDrop(content: "retry", messageID: messageID, recipientNoiseKey: key) {
retryResults.append($0)
}
retry.service.flushDedupSnapshot()
#expect(retryResults == [true])
#expect(retry.publishedEvents.count == 1)
// Only confirmed relay acceptance consumes durable dedup.
let confirmed = Fixture(dedupStore: BridgeDropDedupStore(fileURL: fileURL))
confirmed.sealResult = makeEnvelope(recipientKey: key)
confirmed.service.depositDrop(content: "retry", messageID: messageID, recipientNoiseKey: key)
#expect(confirmed.publishedEvents.isEmpty)
#expect(confirmed.sealRequests.isEmpty)
}
@Test func panicWipeInvalidatesInFlightPublishCompletion() throws {
let fileURL = FileManager.default.temporaryDirectory
.appendingPathComponent("bridge-dedup-\(UUID().uuidString).json")
defer { try? FileManager.default.removeItem(at: fileURL) }
let key = Fixture.randomKey()
let messageID = UUID().uuidString
let fixture = Fixture(dedupStore: BridgeDropDedupStore(fileURL: fileURL))
fixture.sealResult = makeEnvelope(recipientKey: key)
fixture.automaticPublishResult = nil
var results: [Bool] = []
fixture.service.depositDrop(content: "in flight", messageID: messageID, recipientNoiseKey: key) {
results.append($0)
}
let staleCompletion = try #require(fixture.pendingPublishCompletions.first)
fixture.service.wipe()
#expect(results == [false])
// The pre-wipe relay completion cannot resurrect durable dedup or
// complete the caller a second time.
staleCompletion(true)
fixture.service.flushDedupSnapshot()
#expect(results == [false])
let relaunched = Fixture(dedupStore: BridgeDropDedupStore(fileURL: fileURL))
relaunched.sealResult = makeEnvelope(recipientKey: key)
relaunched.service.depositDrop(content: "retry", messageID: messageID, recipientNoiseKey: key)
#expect(relaunched.publishedEvents.count == 1)
}
@Test func bridgeDisableCancelsPendingAndInFlightPublishes() throws {
let fixture = Fixture()
let key = Fixture.randomKey()
fixture.sealResult = makeEnvelope(recipientKey: key)
fixture.automaticPublishResult = nil
var results: [Bool] = []
fixture.service.depositDrop(content: "in flight", messageID: "in-flight", recipientNoiseKey: key) {
results.append($0)
}
let staleCompletion = try #require(fixture.pendingPublishCompletions.first)
fixture.relaysConnected = false
fixture.service.depositDrop(content: "pending", messageID: "pending", recipientNoiseKey: key) {
results.append($0)
}
#expect(fixture.service.pendingDrops.count == 1)
fixture.bridgeOn = false
fixture.service.refresh()
#expect(results == [false, false])
#expect(fixture.service.pendingDrops.isEmpty)
staleCompletion(true)
#expect(results == [false, false])
}
@Test func depositQueuesWithoutRelaysAndFlushesOnReconnect() {
let fixture = Fixture()
fixture.relaysConnected = false
@@ -148,15 +287,18 @@ struct BridgeCourierServiceTests {
fixture.sealResult = makeEnvelope(recipientKey: key)
let firstID = UUID().uuidString
#expect(fixture.service.depositDrop(content: "0", messageID: firstID, recipientNoiseKey: key))
var firstResults: [Bool] = []
fixture.service.depositDrop(content: "0", messageID: firstID, recipientNoiseKey: key) { firstResults.append($0) }
// Fill past capacity so the first drop is evicted.
for i in 1...BridgeCourierService.Limits.maxPendingDrops {
fixture.service.depositDrop(content: "\(i)", messageID: UUID().uuidString, recipientNoiseKey: key)
}
#expect(fixture.service.pendingDrops.count == BridgeCourierService.Limits.maxPendingDrops)
#expect(firstResults == [false])
// The evicted first drop is deposit-able again (slot released).
#expect(fixture.service.depositDrop(content: "0-retry", messageID: firstID, recipientNoiseKey: key))
fixture.service.depositDrop(content: "0-retry", messageID: firstID, recipientNoiseKey: key)
#expect(fixture.service.pendingDrops.last?.dedupKey == firstID)
}
@Test func oversizeDropConsumesSlotInsteadOfChurning() {
@@ -170,13 +312,45 @@ struct BridgeCourierServiceTests {
ciphertext: Data(repeating: 7, count: BridgeCourierService.Limits.maxDropEnvelopeBytes + 1)
)
let messageID = UUID().uuidString
var results: [Bool] = []
#expect(!fixture.service.depositDrop(content: "big", messageID: messageID, recipientNoiseKey: key))
fixture.service.depositDrop(content: "big", messageID: messageID, recipientNoiseKey: key) { results.append($0) }
#expect(fixture.publishedEvents.isEmpty)
// The retry sweep must not seal the same payload again.
#expect(!fixture.service.depositDrop(content: "big", messageID: messageID, recipientNoiseKey: key))
fixture.service.depositDrop(content: "big", messageID: messageID, recipientNoiseKey: key) { results.append($0) }
#expect(fixture.sealRequests.count == 1)
#expect(results == [false, false])
}
@Test func rejectedOversizeDropKeysExpireAndStayBounded() {
var date = Date(timeIntervalSince1970: 1_750_000_000)
let fixture = Fixture(now: { date })
let key = Fixture.randomKey()
fixture.sealResult = makeEnvelope(
recipientKey: key,
ciphertext: Data(repeating: 7, count: BridgeCourierService.Limits.maxDropEnvelopeBytes + 1)
)
let firstID = "oversize-0"
fixture.service.depositDrop(content: "big", messageID: firstID, recipientNoiseKey: key)
for index in 1...BridgeCourierService.Limits.maxTrackedIDs {
date = date.addingTimeInterval(1)
fixture.service.depositDrop(content: "big", messageID: "oversize-\(index)", recipientNoiseKey: key)
}
let afterCapacityFill = fixture.sealRequests.count
date = date.addingTimeInterval(1)
fixture.service.depositDrop(content: "big", messageID: firstID, recipientNoiseKey: key)
#expect(fixture.sealRequests.count == afterCapacityFill + 1)
let newestID = "oversize-\(BridgeCourierService.Limits.maxTrackedIDs)"
let beforeExpiry = fixture.sealRequests.count
fixture.service.depositDrop(content: "big", messageID: newestID, recipientNoiseKey: key)
#expect(fixture.sealRequests.count == beforeExpiry)
date = date.addingTimeInterval(CourierEnvelope.maxLifetimeSeconds + 1)
fixture.service.depositDrop(content: "big", messageID: newestID, recipientNoiseKey: key)
#expect(fixture.sealRequests.count == beforeExpiry + 1)
}
@Test func publishedDropDedupSurvivesRelaunch() throws {
@@ -192,8 +366,10 @@ struct BridgeCourierServiceTests {
let fixture = Fixture(dedupStore: BridgeDropDedupStore(fileURL: fileURL))
fixture.sealResult = makeEnvelope(recipientKey: recipientKey)
#expect(fixture.service.depositDrop(content: "hello", messageID: messageID, recipientNoiseKey: recipientKey))
var publishResults: [Bool] = []
fixture.service.depositDrop(content: "hello", messageID: messageID, recipientNoiseKey: recipientKey) { publishResults.append($0) }
#expect(fixture.publishedEvents.count == 1)
#expect(publishResults == [true])
// Persistence is coalesced; a real launch flushes within a second or
// on backgrounding tests flush explicitly.
fixture.service.flushDedupSnapshot()
@@ -202,9 +378,11 @@ struct BridgeCourierServiceTests {
// publish the same message ID again (before even re-sealing it).
let relaunched = Fixture(dedupStore: BridgeDropDedupStore(fileURL: fileURL))
relaunched.sealResult = makeEnvelope(recipientKey: recipientKey)
#expect(!relaunched.service.depositDrop(content: "hello", messageID: messageID, recipientNoiseKey: recipientKey))
var relaunchResults: [Bool] = []
relaunched.service.depositDrop(content: "hello", messageID: messageID, recipientNoiseKey: recipientKey) { relaunchResults.append($0) }
#expect(relaunched.publishedEvents.isEmpty)
#expect(relaunched.sealRequests.isEmpty)
#expect(relaunchResults == [false])
}
@Test func seenDropEventDedupSurvivesRelaunch() throws {
@@ -245,7 +423,7 @@ struct BridgeCourierServiceTests {
let fixture = Fixture(dedupStore: BridgeDropDedupStore(fileURL: fileURL))
fixture.relaysConnected = false
fixture.sealResult = makeEnvelope(recipientKey: recipientKey)
#expect(fixture.service.depositDrop(content: "later", messageID: messageID, recipientNoiseKey: recipientKey))
fixture.service.depositDrop(content: "later", messageID: messageID, recipientNoiseKey: recipientKey)
#expect(fixture.publishedEvents.isEmpty)
// Even a flush while the drop is still pending must exclude its key.
fixture.service.flushDedupSnapshot()
@@ -253,7 +431,7 @@ struct BridgeCourierServiceTests {
// "App killed before relays connected": pendingDrops were memory-only.
let relaunched = Fixture(dedupStore: BridgeDropDedupStore(fileURL: fileURL))
relaunched.sealResult = makeEnvelope(recipientKey: recipientKey)
#expect(relaunched.service.depositDrop(content: "later", messageID: messageID, recipientNoiseKey: recipientKey))
relaunched.service.depositDrop(content: "later", messageID: messageID, recipientNoiseKey: recipientKey)
#expect(relaunched.publishedEvents.count == 1)
}
@@ -269,7 +447,7 @@ struct BridgeCourierServiceTests {
let fixture = Fixture(dedupStore: BridgeDropDedupStore(fileURL: fileURL))
fixture.relaysConnected = false
fixture.sealResult = makeEnvelope(recipientKey: recipientKey)
#expect(fixture.service.depositDrop(content: "later", messageID: messageID, recipientNoiseKey: recipientKey))
fixture.service.depositDrop(content: "later", messageID: messageID, recipientNoiseKey: recipientKey)
fixture.relaysConnected = true
fixture.service.flushPendingDrops()
#expect(fixture.publishedEvents.count == 1)
@@ -277,8 +455,10 @@ struct BridgeCourierServiceTests {
let relaunched = Fixture(dedupStore: BridgeDropDedupStore(fileURL: fileURL))
relaunched.sealResult = makeEnvelope(recipientKey: recipientKey)
#expect(!relaunched.service.depositDrop(content: "later", messageID: messageID, recipientNoiseKey: recipientKey))
var relaunchResults: [Bool] = []
relaunched.service.depositDrop(content: "later", messageID: messageID, recipientNoiseKey: recipientKey) { relaunchResults.append($0) }
#expect(relaunched.publishedEvents.isEmpty)
#expect(relaunchResults == [false])
}
@Test func failedGatewayHandoffReleasesSeenSlot() throws {
@@ -307,6 +487,30 @@ struct BridgeCourierServiceTests {
#expect(fixture.delivered.count == 2)
}
@Test func staleWatchSetDeliveryIsNotConsumedBeforePeerBecomesCurrent() throws {
let fixture = Fixture()
let peerKey = Fixture.randomKey()
let peer = PeerID(str: "aabbccdd00112233")
let event = try makeDropEvent(for: makeEnvelope(recipientKey: peerKey))
// A callback from the previous relay subscription can land after its
// peer was removed from the bounded watch set. Ignore it without
// poisoning the persistent event-ID dedup record.
fixture.service.refresh()
fixture.service.handleDropEvent(event)
#expect(fixture.delivered.isEmpty)
fixture.localPeers = [(peer, peerKey)]
fixture.service.refresh()
fixture.service.handleDropEvent(event)
#expect(fixture.delivered.count == 1)
// Once the current peer's physical handoff succeeds, normal durable
// dedup applies.
fixture.service.handleDropEvent(event)
#expect(fixture.delivered.count == 1)
}
@Test func distinctDropsUseDistinctThrowawayKeys() {
let fixture = Fixture()
let keyA = Fixture.randomKey()
@@ -328,6 +532,50 @@ struct BridgeCourierServiceTests {
#expect(fixture.publishedEvents.count == 1)
#expect(fixture.heldCooldowns == [BridgeCourierService.Limits.heldEnvelopePublishCooldown])
#expect(fixture.markedHeldEnvelopes == fixture.held)
}
@Test func rejectedHeldPublishDoesNotStartCooldown() {
let fixture = Fixture()
fixture.automaticPublishResult = false
fixture.held = [makeEnvelope(recipientKey: Fixture.randomKey())]
fixture.service.publishHeldEnvelopes()
#expect(fixture.publishedEvents.count == 1)
#expect(fixture.markedHeldEnvelopes.isEmpty)
}
@Test func heldPublishIsSingleFlightAndRetryableAfterRejection() {
let fixture = Fixture()
fixture.automaticPublishResult = nil
fixture.held = [makeEnvelope(recipientKey: Fixture.randomKey())]
fixture.service.publishHeldEnvelopes()
fixture.service.publishHeldEnvelopes()
#expect(fixture.publishedEvents.count == 1)
fixture.resolveNextPublish(false)
fixture.service.publishHeldEnvelopes()
#expect(fixture.publishedEvents.count == 2)
#expect(fixture.markedHeldEnvelopes.isEmpty)
fixture.resolveNextPublish(true)
#expect(fixture.markedHeldEnvelopes == fixture.held)
}
@Test func bridgeDisableInvalidatesHeldPublishOperation() throws {
let fixture = Fixture()
fixture.automaticPublishResult = nil
fixture.held = [makeEnvelope(recipientKey: Fixture.randomKey())]
fixture.service.publishHeldEnvelopes()
let staleCompletion = try #require(fixture.pendingPublishCompletions.first)
fixture.bridgeOn = false
fixture.service.refresh()
staleCompletion(true)
#expect(fixture.markedHeldEnvelopes.isEmpty)
}
// MARK: - Subscription management
@@ -364,6 +612,32 @@ struct BridgeCourierServiceTests {
#expect(fixture.closedSubscriptions == 1)
}
@Test func announceDebounceSchedulesTrailingRefreshForPeersLearnedInsideWindow() throws {
var date = Date(timeIntervalSince1970: 1_750_000_000)
let fixture = Fixture(now: { date })
// Leading edge opens the own-tag subscription immediately.
fixture.service.refreshAfterVerifiedAnnounce()
#expect(fixture.openedSubscriptions.count == 1)
// A second peer learned inside the debounce window must not wait for
// the 30-minute periodic timer.
date = date.addingTimeInterval(10)
fixture.localPeers = [(PeerID(str: "aabbccdd00112233"), Fixture.randomKey())]
fixture.service.refreshAfterVerifiedAnnounce()
fixture.service.refreshAfterVerifiedAnnounce() // coalesces, not a second timer
let trailingTimers = fixture.scheduledTimers.filter { $0.delay < 100 }
#expect(trailingTimers.count == 1)
let trailing = try #require(trailingTimers.first)
#expect(trailing.delay == 50)
date = date.addingTimeInterval(50)
trailing.fire()
#expect(fixture.openedSubscriptions.count == 2)
#expect(fixture.openedSubscriptions.last?.count == 6)
}
// MARK: - Inbound drops
@Test func dropForUsIsOpened() throws {
@@ -378,6 +652,21 @@ struct BridgeCourierServiceTests {
#expect(fixture.delivered.isEmpty)
}
@Test func transientOwnDropOpenFailureRemainsRetryable() throws {
let fixture = Fixture()
let myKey = try #require(fixture.myKey)
fixture.service.refresh()
let event = try makeDropEvent(for: makeEnvelope(recipientKey: myKey))
fixture.openResult = false
fixture.service.handleDropEvent(event)
fixture.openResult = true
fixture.service.handleDropEvent(event)
fixture.service.handleDropEvent(event)
#expect(fixture.openedEnvelopes.count == 2)
}
@Test func duplicateDropEventOpensOnce() throws {
let fixture = Fixture()
let myKey = try #require(fixture.myKey)