Files
bitchat/bitchatTests/SchnorrConcurrencyRepro.swift
jackandClaude Fable 5 cc76086615 Inject notification/favorites singletons through coordinator contexts
NotificationService.shared and FavoritesPersistenceService.shared
accesses across nine coordinators/components consolidate into
ChatViewModel witnesses behind intent-named context members
(notifyPrivateMessage, notifyMention, favoriteRelationship(forNoiseKey:),
allFavoriteRelationships, postLocalNotification, ...). Singleton reach
from coordinators is now zero; nine new tests cover previously
untestable notification/favorites flows.

Also root-causes the long-flaky gift-wrap dedup test: parallel tests
share LocationChannelManager.shared, so channel-switch tests trigger
clearProcessedNostrEvents() on every live ChatViewModel, wiping the
dedup record mid-test. The invariant (forged-signature copies never
poison dedup) now lives as a deterministic NostrInboundPipeline
mock-context test; two Schnorr concurrency probes added along the way
stay as regression guards for the P256K shared-context assumptions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 23:04:20 +02:00

83 lines
3.2 KiB
Swift

import Testing
import Foundation
@testable import bitchat
struct SchnorrConcurrencyRepro {
@Test func concurrentVerificationOfValidEventAlwaysSucceeds() async throws {
let identity = try NostrIdentity.generate()
let event = NostrEvent(
pubkey: identity.publicKeyHex,
createdAt: Date(),
kind: .ephemeralEvent,
tags: [["g", "sn"]],
content: "concurrency probe"
)
let signed = try event.sign(with: identity.schnorrSigningKey())
#expect(signed.isValidSignature())
let failures = await withTaskGroup(of: Int.self) { group in
for _ in 0..<8 {
group.addTask {
var localFailures = 0
for _ in 0..<250 {
if !signed.isValidSignature() { localFailures += 1 }
}
return localFailures
}
}
var total = 0
for await f in group { total += f }
return total
}
#expect(failures == 0, "Schnorr verification returned false \(failures)/2000 times under concurrency")
}
@Test func verificationSurvivesConcurrentSigningAndKeyGeneration() async throws {
let identity = try NostrIdentity.generate()
let event = NostrEvent(
pubkey: identity.publicKeyHex,
createdAt: Date(),
kind: .ephemeralEvent,
tags: [["g", "sn"]],
content: "concurrency probe"
)
let signed = try event.sign(with: identity.schnorrSigningKey())
#expect(signed.isValidSignature())
// Signing and key generation may mutate shared library state
// (secp256k1 context randomization); verification must stay correct
// while they run on other tasks.
let failures = await withTaskGroup(of: Int.self) { group in
for worker in 0..<8 {
group.addTask {
if worker < 4 {
var localFailures = 0
for _ in 0..<250 {
if !signed.isValidSignature() { localFailures += 1 }
}
return localFailures
} else {
for i in 0..<100 {
if let id = try? NostrIdentity.generate() {
let e = NostrEvent(
pubkey: id.publicKeyHex,
createdAt: Date(),
kind: .ephemeralEvent,
tags: [],
content: "signer \(worker)-\(i)"
)
_ = try? e.sign(with: id.schnorrSigningKey())
}
}
return 0
}
}
}
var total = 0
for await f in group { total += f }
return total
}
#expect(failures == 0, "Schnorr verification returned false \(failures)/1000 times while signing ran concurrently")
}
}