mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-26 02:25:19 +00:00
Verify Nostr inbound signatures once, off the main actor
Every inbound relay event was Schnorr-verified TWICE, both times on the main actor: once in NostrRelayManager.handleParsedMessage and again in each NostrInboundPipeline / GeoPresenceTracker handler. Each verification re-serializes the event JSON, hashes it, and runs a secp256k1 Schnorr verify, so busy geohashes paid double crypto on the UI thread. Geohash gift-wrap NIP-17 decryption (two ECDH+ChaCha layers) also ran on the main actor. Changes: - NostrRelayManager now owns a serial off-main inbound pipeline (AsyncStream + single consumer task): frames are parsed and verified in arrival order off the main actor, preserving per-subscription delivery order. This is the single verification point for the whole inbound path; downstream handlers only ever see verified events. - Dedup stays two-phase and unpoisonable: a cheap main-actor duplicate LOOKUP runs before verification (duplicate fan-in from several relays never pays for crypto — previously every duplicate was verified), and events are RECORDED as seen only after the signature verifies, so a forged-signature copy can never suppress the genuine event. - NostrInboundPipeline and GeoPresenceTracker drop their redundant re-verification; geohash gift-wrap decryption moves off the main actor following the existing account-mailbox Task.detached pattern (atomic main-actor check-and-record, then decrypt off-main, then hop back for state updates). - Tests: relay-level coverage for tampered gift wraps and for in-order delivery of back-to-back frames; pipeline-level tampered-signature tests move to the relay boundary where the invariant now lives; the mock relay connection queues frames emitted before receive re-arms. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -326,26 +326,11 @@ struct ChatViewModelPresenceHandlingTests {
|
||||
#expect(viewModel.geohashParticipantCount(for: activeGeohash) >= 1)
|
||||
}
|
||||
|
||||
@Test func subscribeNostrEvent_samplingInvalidSignatureDoesNotPoisonDedup() async throws {
|
||||
let (viewModel, _) = makeTestableViewModel()
|
||||
let sampleGeohash = "u4pru"
|
||||
let identity = try NostrIdentity.generate()
|
||||
let event = NostrEvent(
|
||||
pubkey: identity.publicKeyHex,
|
||||
createdAt: Date(),
|
||||
kind: .geohashPresence,
|
||||
tags: [["g", sampleGeohash]],
|
||||
content: ""
|
||||
)
|
||||
let signed = try event.sign(with: identity.schnorrSigningKey())
|
||||
var invalid = signed
|
||||
invalid.sig = String(repeating: "0", count: 128)
|
||||
|
||||
viewModel.subscribeNostrEvent(invalid, gh: sampleGeohash)
|
||||
viewModel.subscribeNostrEvent(signed, gh: sampleGeohash)
|
||||
|
||||
#expect(viewModel.geohashParticipantCount(for: sampleGeohash) == 1)
|
||||
}
|
||||
// NOTE: Tampered-signature rejection (and the forged-copy dedup-poisoning
|
||||
// invariant) is enforced once, off the main actor, at the relay boundary —
|
||||
// the sampling path only ever sees verified events. See
|
||||
// NostrRelayManagerTests
|
||||
// `test_receiveEvent_invalidSignatureDoesNotPoisonDuplicateCache`.
|
||||
|
||||
// MARK: - Test Helper
|
||||
|
||||
|
||||
Reference in New Issue
Block a user