Files
bitchat/bitchat/Services/TransportConfig.swift
T
jackandGitHub b59ad97dd6 perf: verify Nostr inbound signatures once, off the main actor (#1352)
Moves inbound Nostr Schnorr verification and NIP-17 gift-wrap decryption off the main actor into per-relay bounded AsyncStream pipelines (parallel across relays, ordered per relay), dedups before crypto, and removes the redundant second re-verify in NostrInboundPipeline/GeoPresenceTracker. Adds a panic-wipe generation counter so in-flight decrypts are discarded after a wipe.

Rebased onto main; #1451's 256 KiB pre-parse byte cap and tag limits are preserved and still enforced before any crypto/allocation.
2026-07-26 11:27:57 +02:00

398 lines
23 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Foundation
/// Centralized knobs for transport- and UI-related limits.
/// Keep values aligned with existing behavior when replacing magic numbers.
enum TransportConfig {
// BLE / Protocol
static let bleDefaultFragmentSize: Int = 469 // ~512 MTU minus protocol overhead
static let messageTTLDefault: UInt8 = 7 // Default TTL for mesh flooding
static let bleMaxInFlightAssemblies: Int = 128 // Cap concurrent fragment assemblies
static let bleHighDegreeThreshold: Int = 6 // For adaptive TTL/probabilistic relays
static let bleMaxConcurrentTransfers: Int = 2 // Limit simultaneous large media sends
static let bleFragmentRelayMinDelayMs: Int = 8 // Faster forwarding for media fragments
static let bleFragmentRelayMaxDelayMs: Int = 25 // Upper jitter bound for fragment relays
// Fragment relay TTL in sparse graphs; matches messageTTLDefault so media
// reaches as far as text. Dense graphs clamp harder in RelayController.
static let bleFragmentRelayTtlCap: UInt8 = 7
static let bleFragmentRelayTtlCapDense: UInt8 = 5 // Contain fragment floods in dense graphs
// Live voice (push-to-talk)
// Burst-content budget per voice packet. Sized so the Noise ciphertext
// (content + 1 type byte + 16 tag bytes) stays within MessagePadding's
// 256-byte bucket and the whole directed packet (16 header + 8 sender +
// 8 recipient + 256 payload = 288 bytes) rides one BLE frame — live audio
// must never enter the fragment scheduler, which caps concurrent
// transfers at 2 and would let voice starve file sends.
static let pttMaxBurstContentBytes: Int = 210
static let pttJitterBufferSeconds: TimeInterval = 0.35 // buffered audio before live playback starts
static let pttJitterDeadlineSeconds: TimeInterval = 0.5 // start anyway after this wall-clock wait
static let pttBurstEndTimeoutSeconds: TimeInterval = 3.0 // no frames -> burst considered ended
static let pttMaxConcurrentAssemblies: Int = 8 // concurrent inbound bursts cap
static let pttMaxBurstBytes: Int = 384 * 1024 // 120s at ~2KB/s + generous slack
static let pttFinishedBurstRegistrySeconds: TimeInterval = 600 // window to absorb the finalized note
// Inbound flood guard: a real burst arrives at ~2KB/s; allow 3x plus a
// small settling allowance before dropping a sender's frames.
static let pttInboundMaxBytesPerSecond: Int = 6_000
// Public bursts are live-only traffic: frames older than this are relay
// stragglers or replays, not audio anyone should start hearing.
static let pttPublicFrameMaxAgeSeconds: TimeInterval = 30
// Mesh diagnostics (/ping)
static let meshPingTimeoutSeconds: TimeInterval = 10 // Give up on a probe after this window
static let meshPingInboundMaxPerLink: Int = 5 // Inbound ping budget per ingress link (claimed sender is spoofable)...
static let meshPingInboundWindowSeconds: TimeInterval = 10 // ...per sliding window (anti-amplification)
// UI / Storage Caps
static let privateChatCap: Int = 1337
static let meshTimelineCap: Int = 1337
static let geoTimelineCap: Int = 1337
static let geoNicknameParticipantsCap: Int = 1337
static let contentLRUCap: Int = 2000
static let geoSamplingEventLRUCap: Int = 2000
// Timers
static let networkResetGraceSeconds: TimeInterval = 600 // 10 minutes
static let networkNotificationCooldownSeconds: TimeInterval = 300 // 5 minutes
static let basePublicFlushInterval: TimeInterval = 0.08 // ~12.5 fps batching
// BLE duty/announce/connect
static let bleConnectRateLimitInterval: TimeInterval = 0.5
static let bleMaxCentralLinks: Int = 6
static let bleDutyOnDuration: TimeInterval = 5.0
static let bleDutyOffDuration: TimeInterval = 10.0
static let bleAnnounceMinInterval: TimeInterval = 1.0
// BLE discovery/quality thresholds
static let bleDynamicRSSIThresholdDefault: Int = -90
static let bleConnectionCandidatesMax: Int = 100
static let blePendingWriteBufferCapBytes: Int = 1_000_000
static let bleNotificationAssemblerHardCapBytes: Int = 8 * 1024 * 1024
static let bleAssemblerStallResetMs: Int = 250
static let blePendingNotificationsCapCount: Int = 128
static let bleNotificationRetryDelayMs: Int = 25
static let bleNotificationRetryMaxAttempts: Int = 80
// Sample interval for notification backpressure logs (fire per fragment
// during media transfers).
static let bleBackpressureLogInterval: Int = 25
// Nostr
static let nostrReadAckInterval: TimeInterval = 0.35 // ~3 per second
static let nostrInboundEventDedupCap: Int = 4096
static let nostrInboundEventDedupTrimTarget: Int = 3072
static let nostrDuplicateEventLogInterval: Int = 50
// Sample interval for per-event debug logs on the inbound hot path.
static let nostrInboundEventLogInterval: Int = 100
// Reject oversized/untrusted relay frames before JSON parse / store.
static let nostrMaxInboundMessageBytes: Int = 256 * 1024
static let nostrMaxEventTags: Int = 64
static let nostrMaxEventTagValues: Int = 16
static let nostrMaxEventTagValueBytes: Int = 1024
// Bounded per-relay inbound frame buffer. Each relay connection owns its
// own serial verify pipeline; if a relay floods faster than its Schnorr
// verification drains, the oldest buffered frames for THAT relay are
// dropped (bufferingNewest) so one relay cannot stall other relays.
// Nostr inbound is already best-effort (relays are redundant and events
// replay), so dropping a flooding relay's backlog is safe. Together with
// nostrInboundMaxFrameBytes this caps buffered inbound bytes at
// cap × maxFrameBytes (128 MiB) per hostile relay — bounded, not zero.
static let nostrInboundPerRelayBufferCap: Int = 256
// Hard per-frame byte bound, applied as URLSessionWebSocketTask
// .maximumMessageSize (oversized frames fail the receive instead of
// buffering). BitChat's legitimate Nostr traffic is small: geohash chat /
// presence events (kind 20000/20001), kind-1 notes, and NIP-17
// gift-wrapped DMs carrying text payloads or receipts are all a few KiB,
// and most public relays reject events beyond ~64256 KiB anyway. 512 KiB
// leaves an order-of-magnitude margin over anything we produce or expect
// while halving the URLSession default (1 MiB), so a hostile relay's
// worst-case buffered pile-up per connection is
// nostrInboundPerRelayBufferCap × 512 KiB = 128 MiB instead of 256 MiB.
static let nostrInboundMaxFrameBytes: Int = 512 * 1024
// Conversation store diagnostics (field observability)
// Sample interval for the periodic store-audit "OK" heartbeat line
// (first + every Nth audit); violations always log at error level.
static let conversationStoreAuditLogInterval: Int = 10
// Sample interval for the mirrored-republish debug line in the ID-only
// delivery fan-out (first + every Nth republish).
static let conversationStoreMirroredRepublishLogInterval: Int = 25
// UI thresholds
static let uiProcessedNostrEventsCap: Int = 2000
// UI rate limiters (token buckets)
static let uiSenderRateBucketCapacity: Double = 5
static let uiSenderRateBucketRefillPerSec: Double = 1.0
static let uiContentRateBucketCapacity: Double = 3
static let uiContentRateBucketRefillPerSec: Double = 0.5
// Bound attacker-keyed bucket maps (sender IDs / content digests).
static let uiSenderRateBucketMaxEntries: Int = 2000
static let uiContentRateBucketMaxEntries: Int = 2000
static let uiRateBucketIdleTTL: TimeInterval = 10 * 60
// Cap teleported-participant markers so remote events cannot grow the set.
static let geoTeleportedParticipantsCap: Int = 1337
// UI sleeps/delays
static let uiStartupInitialDelaySeconds: TimeInterval = 1.0
static let uiStartupPhaseDurationSeconds: TimeInterval = 2.0
static let uiReadReceiptRetryShortSeconds: TimeInterval = 0.1
static let uiReadReceiptRetryLongSeconds: TimeInterval = 0.5
static let uiBatchDispatchStaggerSeconds: TimeInterval = 0.15
static let uiScrollThrottleSeconds: TimeInterval = 0.5
static let uiAnimationMediumSeconds: TimeInterval = 0.2
static let uiRecentCutoffFiveMinutesSeconds: TimeInterval = 5 * 60
static let uiMeshEmptyConfirmationSeconds: TimeInterval = 30.0
// BLE maintenance & thresholds
static let bleMaintenanceInterval: TimeInterval = 5.0
static let bleMaintenanceLeewaySeconds: Int = 1
static let bleIsolationRelaxThresholdSeconds: TimeInterval = 30
// Isolated nodes accept the weakest usable links — a fringe connection
// beats no connection. Relaxed floor sits at CoreBluetooth's practical
// reporting limit so prolonged isolation gates on nothing but decode.
static let bleRSSIIsolatedBase: Int = -95
static let bleRSSIIsolatedRelaxed: Int = -100
static let bleRSSIConnectedThreshold: Int = -85
// How long without seeing traffic before we sanity-check the direct link
// Lowered to make connected→reachable icon changes react faster when walking out of range
static let blePeerInactivityTimeoutSeconds: TimeInterval = 8.0
// How long to retain a peer as "reachable" (not directly connected) since lastSeen.
// Must comfortably exceed the worst-case dense announce interval (38s) plus a
// missed cycle, so duty-cycled nodes don't forget peers between announces.
static let bleReachabilityRetentionVerifiedSeconds: TimeInterval = 60.0 // verified/favorites
static let bleReachabilityRetentionUnverifiedSeconds: TimeInterval = 45.0 // unknown/unverified
static let bleFragmentLifetimeSeconds: TimeInterval = 30.0
static let bleIngressRecordLifetimeSeconds: TimeInterval = 3.0
// At most one rotation rebind per link per window: TTL is not signed, so
// a replayed announce can forge "direct", and without a cooldown two
// identities could fight over a link in a rebind flip-flop.
static let bleLinkRebindCooldownSeconds: TimeInterval = 60.0
static let bleConnectTimeoutBackoffWindowSeconds: TimeInterval = 120.0
static let bleRecentPacketWindowSeconds: TimeInterval = 30.0
static let bleRecentPacketWindowMaxCount: Int = 100
// Keep scanning fully ON when we saw traffic very recently
static let bleRecentTrafficForceScanSeconds: TimeInterval = 10.0
static let bleThreadSleepWriteShortDelaySeconds: TimeInterval = 0.05
static let bleExpectedWritePerFragmentMs: Int = 20
static let bleExpectedWriteMaxMs: Int = 5000
// Fragment pacing: Conservative spacing to prevent BLE buffer overflow
// Aggressive pacing causes packet loss; needs 25-30ms between fragments for reliable delivery
static let bleFragmentSpacingMs: Int = 30
static let bleFragmentSpacingDirectedMs: Int = 25
static let bleAnnounceIntervalSeconds: TimeInterval = 4.0
static let bleDutyOnDurationDense: TimeInterval = 3.0
static let bleDutyOffDurationDense: TimeInterval = 15.0
static let bleConnectedAnnounceBaseSecondsDense: TimeInterval = 30.0
static let bleConnectedAnnounceBaseSecondsSparse: TimeInterval = 15.0
static let bleConnectedAnnounceJitterDense: TimeInterval = 8.0
static let bleConnectedAnnounceJitterSparse: TimeInterval = 4.0
// Location
static let locationDistanceFilterMeters: Double = 1000
// Live (channel sheet open) distance threshold for meaningful updates
static let locationDistanceFilterLiveMeters: Double = 10.0
static let locationLiveRefreshInterval: TimeInterval = 5.0
// Notifications (geohash)
static let uiGeoNotifyCooldownSeconds: TimeInterval = 60.0
static let uiGeoNotifySnippetMaxLen: Int = 80
// Nostr geohash
static let nostrGeohashInitialLookbackSeconds: TimeInterval = 3600
static let nostrGeohashInitialLimit: Int = 200
static let nostrGeoRelayCount: Int = 5
static let nostrGeohashSampleLookbackSeconds: TimeInterval = 300
static let nostrGeohashSampleLimit: Int = 100
static let nostrDMSubscribeLookbackSeconds: TimeInterval = 86400
// A sampled chat message this recent means "a conversation is happening
// there" for the empty-timeline nearby-activity hint.
static let uiGeohashChatActivityWindowSeconds: TimeInterval = 900
// Startup delay before reading the gossip archive for "heard here
// earlier" echoes; covers the archive's async disk restore.
static let uiArchivedEchoLoadDelaySeconds: TimeInterval = 1.5
// Dead drops: location notes left via /drop expire after this long.
static let locationDropExpirySeconds: TimeInterval = 24 * 60 * 60
// Poll cadence while geo notes wait for a relay connection (Tor warming
// up); re-subscribes as soon as one comes up.
static let uiGeoNotesConnectivityRetrySeconds: TimeInterval = 3.0
// Message deduplication
static let messageDedupMaxAgeSeconds: TimeInterval = 300
static let messageDedupMaxCount: Int = 1000
// Verification QR
static let verificationQRMaxAgeSeconds: TimeInterval = 5 * 60
// Nostr relay backoff
static let nostrRelayInitialBackoffSeconds: TimeInterval = 1.0
static let nostrRelayMaxBackoffSeconds: TimeInterval = 300.0
static let nostrRelayBackoffMultiplier: Double = 2.0
static let nostrRelayMaxReconnectAttempts: Int = 10
// Reconnect delays get ±20% random jitter so relays that dropped together
// (e.g. a network blip) don't thundering-herd the same reconnect instant.
static let nostrRelayBackoffJitterRatio: Double = 0.2
static let nostrRelayDefaultFetchLimit: Int = 100
// How many consecutive Tor-readiness waits (each bounded by TorManager's
// bootstrap deadline) to attempt before unblocking pending EOSE callers.
static let nostrTorReadyMaxWaitAttempts: Int = 3
static let nostrPendingSendQueueCap: Int = 200
// Sample interval for the send-queue overflow warning (first + every Nth
// dropped event). Drops are ephemeral presence/geo traffic — log-only.
static let nostrPendingSendDropLogInterval: Int = 10
// Pending (not-yet-flushed) REQs are bounded per relay: oldest-by-insertion
// eviction at the cap, plus an age sweep on connect attempts. Durable
// subscription intent survives in subscriptionRequestState either way.
static let nostrPendingSubscriptionsPerRelayCap: Int = 64
static let nostrPendingSubscriptionTTLSeconds: TimeInterval = 600.0
// Fallback deadline for treating a subscription's initial fetch as complete
// when a relay never sends EOSE (generous to cover Tor circuit setup).
static let nostrSubscriptionEOSEFallbackSeconds: TimeInterval = 10.0
// A bridge drop is durable only after NIP-20 OK. Relays that omit OK must
// not pin the router's in-flight state indefinitely.
static let nostrConfirmedSendAckTimeoutSeconds: TimeInterval = 10.0
// After this long, a relay marked permanently failed gets another chance.
static let nostrRelayFailureCooldownSeconds: TimeInterval = 600.0
// Geo relay directory
static let geoRelayFetchIntervalSeconds: TimeInterval = 60 * 60 * 24
static let geoRelayRefreshCheckIntervalSeconds: TimeInterval = 60 * 60
static let geoRelayRetryInitialSeconds: TimeInterval = 60
static let geoRelayRetryMaxSeconds: TimeInterval = 60 * 60
// BLE operational delays
static let bleInitialAnnounceDelaySeconds: TimeInterval = 0.6
static let bleConnectTimeoutSeconds: TimeInterval = 8.0
static let bleRestartScanDelaySeconds: TimeInterval = 0.1
static let blePostSubscribeAnnounceDelaySeconds: TimeInterval = 0.05
static let blePostAnnounceDelaySeconds: TimeInterval = 0.4
static let bleForceAnnounceMinIntervalSeconds: TimeInterval = 0.15
// BCH-01-004: Rate-limiting for subscription-triggered announces
// Prevents rapid enumeration attacks by rate-limiting announce responses
static let bleSubscriptionRateLimitMinSeconds: TimeInterval = 2.0 // Minimum interval between announces per central
static let bleSubscriptionRateLimitBackoffFactor: Double = 2.0 // Exponential backoff multiplier
static let bleSubscriptionRateLimitMaxBackoffSeconds: TimeInterval = 30.0 // Maximum backoff period
static let bleSubscriptionRateLimitWindowSeconds: TimeInterval = 60.0 // Window for tracking subscription attempts
static let bleSubscriptionRateLimitMaxAttempts: Int = 5 // Max attempts before extended cooldown
// Source routing (v2 directed packets)
// Longest path we will originate, in intermediate hops between us and the
// recipient. Keep small: every hop must be a fresh, confirmed, v2-capable
// node, and long stale paths fail more often than floods.
static let bleSourceRouteMaxIntermediateHops: Int = 4
// A routed send with no inbound traffic from the recipient within this
// window counts as a route failure.
static let bleSourceRouteConfirmationWindowSeconds: TimeInterval = 10.0
// After a route failure, directed sends to that recipient flood instead
// of routing until this lapses.
static let bleSourceRouteSuppressionSeconds: TimeInterval = 60.0
// Targeted fragment resync (REQUEST_SYNC fragmentIdFilter)
// A broadcast reassembly with no new fragment for this long is stalled
// and triggers a targeted REQUEST_SYNC naming its fragment stream.
static let bleFragmentResyncStallSeconds: TimeInterval = 5.0
// Minimum spacing between targeted resync requests for the same stream.
static let bleFragmentResyncRetrySeconds: TimeInterval = 10.0
// Store-and-forward for directed packets at relays. Spooled packets retry
// on each maintenance flush until the window lapses; a longer window lets
// brief link gaps (walking between rooms, reconnect churn) heal themselves.
static let bleDirectedSpoolWindowSeconds: TimeInterval = 60.0
// Log/UI debounce windows
// Shorter debounce so UI reacts faster while still suppressing duplicate callbacks
static let bleDisconnectNotifyDebounceSeconds: TimeInterval = 0.9
static let bleReconnectLogDebounceSeconds: TimeInterval = 2.0
// Background wake-on-proximity (iOS). Pending connects issued on
// backgrounding never expire at the OS level: the Bluetooth controller
// completes them whenever the peer reappears in range and relaunches the
// app via state restoration. Entries older than the BLE address-rotation
// window no longer map to a reachable address, so the cache prunes them.
static let bleRecentPeripheralCacheCap: Int = 16
static let bleRecentPeripheralMaxAgeSeconds: TimeInterval = 15 * 60
// Central slots kept free for connects driven by live background discovery
static let bleBackgroundPendingConnectSlotReserve: Int = 2
// Weak-link cooldown after connection timeouts
static let bleWeakLinkCooldownSeconds: TimeInterval = 30.0
static let bleWeakLinkRSSICutoff: Int = -90
// Rediscovery ignore windows after a failed link, by failure kind:
// a connect attempt that timed out means the peer likely isn't reachable,
// so back off; a dropped established connection (walked out of range)
// usually returns, so only pause long enough for CoreBluetooth to settle.
static let bleTimeoutDiscoveryIgnoreSeconds: TimeInterval = 15.0
static let bleDisconnectDiscoveryIgnoreSeconds: TimeInterval = 3.0
// Content hashing / formatting
static let contentKeyPrefixLength: Int = 256
static let uiLongMessageLengthThreshold: Int = 2000
static let uiVeryLongTokenThreshold: Int = 512
static let uiLongMessageLineLimit: Int = 30
static let uiFingerprintSampleCount: Int = 3
// UI color tuning
static let uiColorHueAvoidanceDelta: Double = 0.05
static let uiColorHueOffset: Double = 0.12
// Peer list palette
static let uiPeerPaletteSlots: Int = 36
static let uiPeerPaletteRingBrightnessDeltaLight: Double = 0.07
static let uiPeerPaletteRingBrightnessDeltaDark: Double = -0.07
// UI windowing (infinite scroll)
static let uiWindowInitialCountPublic: Int = 300
static let uiWindowInitialCountPrivate: Int = 300
static let uiWindowStepCount: Int = 200
// Share extension
static let uiShareExtensionDismissDelaySeconds: TimeInterval = 2.0
static let uiShareAcceptWindowSeconds: TimeInterval = 30.0
static let uiMigrationCutoffSeconds: TimeInterval = 24 * 60 * 60
// Gossip Sync Configuration
static let syncSeenCapacity: Int = 1000
static let syncGCSMaxBytes: Int = 400
static let syncGCSTargetFpr: Double = 0.01
// Fragments and file transfers keep the short window; whole public
// messages get hours so a phone walking between partitions carries the
// room's recent history with it (see syncPublicMessageMaxAgeSeconds).
static let syncMaxMessageAgeSeconds: TimeInterval = 900
// How far back public broadcast messages stay sync-able. Must not exceed
// the receive-side acceptance window (BLEPublicMessagePolicy uses this
// same constant) or served packets would be dropped as stale.
static let syncPublicMessageMaxAgeSeconds: TimeInterval = 6 * 60 * 60
static let syncMaintenanceIntervalSeconds: TimeInterval = 30.0
static let syncStalePeerCleanupIntervalSeconds: TimeInterval = 60.0
static let syncStalePeerTimeoutSeconds: TimeInterval = 60.0
static let syncFragmentCapacity: Int = 600
static let syncFileTransferCapacity: Int = 200
static let syncFragmentIntervalSeconds: TimeInterval = 30.0
static let syncFileTransferIntervalSeconds: TimeInterval = 60.0
static let syncMessageIntervalSeconds: TimeInterval = 15.0
static let syncResponseRateLimitMaxResponses: Int = 8
static let syncResponseRateLimitWindowSeconds: TimeInterval = 30.0
// Courier store-and-forward
// Initial spray-and-wait budget per deposited envelope: each courier may
// hand half its remaining copies to another courier on encounter, so a
// message diffuses through a moving crowd instead of riding one person.
static let courierInitialCopies: UInt8 = 4
// Cooldown between speculative multi-hop handovers of the same envelope
// toward a recipient heard only via relayed announces.
static let courierRemoteHandoverCooldownSeconds: TimeInterval = 10 * 60
// Recently opened courier inner message IDs kept for receiver-side dedup
// (redundant copies ride distinct seals, so only the inner ID matches).
static let courierOpenedMessageIDCap: Int = 512
// One-time prekey bundles (forward-secret courier sealing)
// Own gossip-sync round for bundles: modest cadence, bounded peer count,
// and a long freshness window so bundles persist mesh-wide while their
// owners are away.
static let syncPrekeyBundleCapacity: Int = 200
static let syncPrekeyBundleIntervalSeconds: TimeInterval = 60.0
static let syncPrekeyBundleMaxAgeSeconds: TimeInterval = 24 * 60 * 60
// Unforced re-broadcasts of our own (unchanged) bundle, piggybacked on
// announces, keep it alive in peers' gossip stores; changed bundles are
// sent immediately.
static let prekeyBundleRebroadcastSeconds: TimeInterval = 60 * 60
}