offline_protocol_transport/constants.rs
1//! Constants used throughout the offline-protocol-transport crate.
2
3// BLE Transport Constants
4/// BLE service UUID for the Offline Protocol.
5pub const BLE_SERVICE_UUID: &str = "6E400001-B5A3-F393-E0A9-E50E24DCCA9E";
6
7/// BLE characteristic UUID for message data.
8pub const BLE_MESSAGE_CHAR_UUID: &str = "6E400002-B5A3-F393-E0A9-E50E24DCCA9E";
9
10/// BLE characteristic UUID for device ID.
11pub const BLE_DEVICE_ID_CHAR_UUID: &str = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E";
12
13/// BLE characteristic UUID for the app tag: eight bytes naming the application
14/// behind one instance of the service.
15///
16/// Every SDK app on a phone registers the same service, so a phone running two
17/// of them presents two instances behind one BLE link. A central reads this
18/// only then, to pick its own app's instance. The bridges serve and read it,
19/// and `react_native_ble_binds_one_service_instance_per_link` pins both
20/// bridges' literals to this one.
21pub const BLE_APP_TAG_CHAR_UUID: &str = "6E400005-B5A3-F393-E0A9-E50E24DCCA9E";
22
23/// BLE characteristic UUID for the hello: the identity assertion a central
24/// writes once on the link it opens, so the peripheral can map the central's
25/// address to a peer without dialling it back.
26///
27/// Optional on both sides (`docs/spec/ble-framing.md`, "Hello"). iOS,
28/// Android and the Python service serve and write it;
29/// `react_native_android_ble_central_says_hello`,
30/// `react_native_ios_central_writes_its_hello` and
31/// `react_native_ios_peripheral_serves_hello` pin the bridges' use of it.
32pub const BLE_HELLO_CHAR_UUID: &str = "6E400006-B5A3-F393-E0A9-E50E24DCCA9E";
33
34/// Fallback fragment size used when no MTU has been negotiated for a peer.
35///
36/// Matches the historical iOS CoreBluetooth auto-negotiated minimum ATT MTU
37/// (iPhone 5/6 era). Modern iOS and Android links negotiate higher values,
38/// which are pushed into the transport via `BleTransport::set_peer_mtu`.
39pub const BLE_MAX_FRAGMENT_SIZE: usize = 185;
40
41/// Hard upper bound on a per-peer BLE fragment payload (bytes).
42///
43/// BLE 5 negotiates an ATT MTU up to 517 bytes; subtracting the 3-byte ATT
44/// header yields 514 usable payload bytes. We clamp at 512 for a small safety
45/// margin and to keep allocations friendly.
46pub const MAX_REASONABLE_BLE_PAYLOAD: usize = 512;
47
48/// Timeout for fragment reassembly in seconds.
49/// Fragments older than this are discarded.
50pub const BLE_FRAGMENT_TIMEOUT_SECS: u64 = 30;
51
52/// Maximum number of concurrent fragment assemblies.
53/// Prevents unbounded memory growth from incomplete fragments.
54pub const BLE_MAX_FRAGMENT_ASSEMBLIES: usize = 64;
55
56/// Maximum number of fragments per message.
57/// Prevents fragmentation attacks and memory exhaustion.
58pub const BLE_MAX_FRAGMENT_COUNT: usize = 512;
59
60/// Magic bytes identifying Offline Protocol fragments.
61pub const FRAGMENT_MAGIC: [u8; 2] = *b"OP"; // Offline Protocol
62
63/// Fragment protocol version.
64pub const FRAGMENT_VERSION: u8 = 1;
65
66/// Fixed size of fragment header in bytes.
67/// Format: magic (2) + version (1) + id_len (1) + index (2) + total (2) + data_len (2)
68pub const FRAGMENT_HEADER_FIXED: usize = 2 /*magic*/ + 1 /*version*/ + 1 /*id_len*/ + 2 /*index*/ + 2 /*total*/ + 2 /*data_len*/;
69
70/// ATT overhead bytes for BLE MTU calculation.
71pub const ATT_OVERHEAD_BYTES: usize = 3;
72
73/// Heuristic send queue capacity for congestion calculation.
74pub const HEURISTIC_SEND_CAPACITY: f32 = 50.0;
75
76/// Weight for new latency values in EMA calculation.
77pub const EMA_WEIGHT_NEW_LATENCY: f32 = 0.3;
78
79/// Weight for existing latency values in EMA calculation.
80pub const EMA_WEIGHT_EXISTING_LATENCY: f32 = 0.7;
81
82// Peer-stream (`wifi_direct` slot) Constants
83//
84// The chapter is `docs/spec/stream-framing.md`. A frame on a peer stream is a
85// `u32` big-endian length and that many bytes; the constants here are the
86// prefix, the ceiling the prefix is checked against, and the preamble floor.
87/// Length of the prefix on every peer-stream frame: a `u32`, big-endian.
88pub const PEER_STREAM_LENGTH_PREFIX_LEN: usize = 4;
89
90/// Ceiling on a peer-stream frame body, inclusive.
91///
92/// It is [`DEFAULT_MAX_MESSAGE_SIZE`], the bound every carrier applies to one
93/// message, read off the prefix before a byte of the body instead of measured
94/// after the fact. A frame is one message, so this is not a second limit; it
95/// is the existing one at the point where a hostile length would otherwise
96/// cost an allocation. A body of exactly this many bytes is a valid frame,
97/// which the conformance vectors pin from both sides.
98pub const PEER_STREAM_MAX_FRAME_BYTES: usize = DEFAULT_MAX_MESSAGE_SIZE;
99
100/// Floor on a peer-stream preamble body: the identity assertion's own floor.
101///
102/// A receiver may refuse a preamble on its prefix alone below this, without
103/// reading the body. The value has one home, in `offline-protocol-sealed`,
104/// beside the codec that defines the layout.
105pub const PEER_STREAM_PREAMBLE_FLOOR: usize = offline_protocol_sealed::IDENTITY_ASSERTION_MIN_LEN;
106
107/// Connection timeout for WiFi Direct in seconds.
108pub const WIFI_DIRECT_CONNECTION_TIMEOUT_SECS: u64 = 30;
109
110/// Default device name for Wi-Fi Direct.
111pub const DEFAULT_DEVICE_NAME: &str = "OfflineProtocolDevice";
112
113/// Default group owner intent for Wi-Fi Direct.
114pub const DEFAULT_GROUP_OWNER_INTENT: u8 = 7;
115
116// Internet Transport Constants
117// Connection timeouts and keepalive are owned by the platform bridge that
118// manages the actual socket (OkHttp pingInterval on Android, URLSession
119// ping on iOS) — no Rust-side constants for them.
120/// Default WebSocket server address for Internet transport.
121pub const INTERNET_DEFAULT_SERVER_ADDRESS: &str = "ws://localhost:8080";
122
123/// Timeout for pending Internet send confirmations in seconds.
124/// Messages awaiting platform confirmation beyond this duration are treated as failed.
125pub const INTERNET_PENDING_CONFIRMATION_TIMEOUT_SECS: u64 = 15;
126
127// Reticulum Transport Constants
128/// Timeout for pending Reticulum send confirmations (seconds).
129/// Higher than Internet because Reticulum paths can be high-latency
130/// (especially LoRa multi-hop).
131pub const RETICULUM_PENDING_CONFIRMATION_TIMEOUT_SECS: u64 = 120;
132
133// Nostr Transport Constants
134/// Connection timeout for Nostr relay WebSocket connections (seconds).
135pub const NOSTR_CONNECTION_TIMEOUT_SECS: u64 = 30;
136
137/// Timeout for pending Nostr send confirmations (seconds).
138/// Higher than Internet (relay propagation can be slower) but lower than
139/// Reticulum (no multi-hop mesh delays).
140pub const NOSTR_PENDING_CONFIRMATION_TIMEOUT_SECS: u64 = 30;
141
142/// Default maximum payload size for Nostr events (bytes).
143/// Nostr relays typically accept events up to 64KB–128KB.
144///
145/// Measured against the complete `["EVENT", {...}]` relay message, since that
146/// is what a relay accepts or rejects — not the protocol message inside it,
147/// which is smaller by the base64 (and, once the envelope is sealed, the
148/// encryption) overhead.
149pub const NOSTR_MAX_PAYLOAD_SIZE: usize = 65536;
150
151/// Cap on the stored events a relay returns for the initial Nostr subscription.
152///
153/// NIP-01 scopes `limit` to the initial query — relays MUST ignore it once
154/// they start streaming live events — so it bounds how much history each
155/// (re)connect replays without capping ongoing delivery.
156///
157/// Treat it as advisory in both directions: `limit` is a SHOULD, and NIP-11
158/// `max_limit` lets a relay clamp it silently, so a short result set is not
159/// evidence that the relay had nothing more to send.
160pub const NOSTR_INITIAL_QUERY_LIMIT: usize = 500;
161
162/// Backwards window the outgoing gift wrap's `created_at` is jittered into,
163/// and therefore the amount the subscription's `since` must reach back past
164/// the receive watermark.
165///
166/// **These two uses must stay the same number.** NIP-59 has the sender draw
167/// `created_at` uniformly from `[now − window, now]`, past-only — so an event
168/// published now can carry a timestamp up to a window old, and a `since`
169/// computed from "the newest `created_at` we have seen" would sit *above* it
170/// and skip it. Subtracting the window on the way out is what makes the
171/// watermark safe against that; if the wrap ever jitters further back than
172/// this, messages go missing silently.
173///
174/// The value is a straight trade: NIP-59's reference is 2 days, which buys
175/// unlinkability against timing correlation, but every hour of jitter is an
176/// hour of replay overlap on every reconnect. One hour keeps the overlap
177/// small; the anonymity difference against a relay that already sees arrival
178/// time is marginal.
179///
180/// Applied by the `since` computation since the watermark landed; the wrap
181/// side arrives with the sealed envelope.
182pub const NOSTR_CREATED_AT_JITTER_SECS: i64 = 3600;
183
184/// Extra slack subtracted from the subscription's `since`, on top of
185/// [`NOSTR_CREATED_AT_JITTER_SECS`], to absorb clock disagreement between the
186/// sending peer, the relay, and this device.
187///
188/// `created_at` is written by the *sender*, so a peer whose clock runs behind
189/// ours stamps events below where our watermark thinks "now" is. Without this
190/// margin those events fall under `since` and are never fetched.
191pub const NOSTR_CLOCK_SKEW_MARGIN_SECS: i64 = 300;
192
193/// How far back the first subscription reaches when no receive watermark
194/// exists yet — a fresh install, a `wipePersistedState` logout, or any
195/// subscription created before protocol-state storage has been restored.
196///
197/// It is deliberately *not* zero. A zero (or absent) `since` is exactly the
198/// unbounded filter this watermark exists to remove, and the no-watermark case
199/// is common rather than exotic: the bridges subscribe on every relay connect,
200/// which can happen before `initialize_mls` has restored anything.
201///
202/// A day of backfill is enough for the case that matters — the same user id
203/// reinstalling or signing back in, whose peers have been publishing to a
204/// routing tag nobody was listening on — and `NOSTR_INITIAL_QUERY_LIMIT`
205/// bounds what that window can actually pull down.
206pub const NOSTR_FIRST_RUN_BACKFILL_SECS: i64 = 86_400;
207
208/// How far ahead of local time an event's `created_at` may sit and still
209/// advance the receive watermark.
210///
211/// The routing tag is publicly derivable, so **anyone** can publish an event
212/// addressed to us, and `created_at` is attacker-chosen: a single event dated
213/// years ahead would otherwise pin the watermark to that value and every
214/// subsequent subscription would ask for events `since` the far future —
215/// silently receiving nothing, forever. Values beyond this tolerance are
216/// ignored outright rather than clamped: an absurd timestamp says nothing
217/// about how far our receive progress has actually reached.
218pub const NOSTR_FUTURE_DATED_TOLERANCE_SECS: i64 = 900;
219
220/// Maximum number of peers whose per-install Nostr public key the transport
221/// remembers for sealing.
222///
223/// The map is fed from key packages, whose sender id is wire-claimed, so it is
224/// bounded like every other wire-keyed map in the engine. It resets at capacity
225/// rather than evicting selectively: the consequence of forgetting a peer is
226/// that the next frame to them takes the bootstrap leg — a privacy degradation,
227/// never a delivery failure.
228///
229/// How long that degradation lasts depends on whether the peer has published
230/// key packages. A forgotten peer who has is re-resolved on the next miss, so
231/// the window is one frame. A peer who has not (an older build, or Nostr
232/// publication disabled) stays on the bootstrap leg until their key package
233/// arrives again by some other route, which for a flood-induced reset means
234/// until restart — the restore re-marks every peer from `PeerCapabilities` — or
235/// until re-exchange. That second case is what makes a reset-at-capacity flood
236/// a real, if bounded, downgrade vector rather than a one-frame blip.
237pub const NOSTR_MAX_TRACKED_PEER_KEYS: usize = 1000;
238
239/// Number of single-use MLS key packages published for cold contact.
240///
241/// Each occupies its own addressable slot (a distinct `d` tag), because an MLS
242/// key package's init key is consumed by the first peer who uses it: one
243/// replaceable record would mean a stranger who fetches it after it was spent
244/// builds a Welcome that can never be processed.
245///
246/// What the count buys is coverage of the *sequential* gap — packages consumed
247/// between one refresh and the next, which runs on the process tick. It does
248/// **not** absorb concurrent cold contacts: nothing distributes simultaneous
249/// fetchers across slots, so two strangers arriving at once generally race for
250/// the same init key and the loser recovers through the reverse key-package
251/// exchange, not through slot multiplicity. Do not raise this expecting
252/// concurrency headroom it does not provide.
253///
254/// Raising it costs one published event and one key package held in provider
255/// storage per slot; lowering it shortens the window a burst of sequential
256/// contacts can consume before every slot is stale, which degrades cold contact
257/// until the next tick refills rather than silently reusing a spent package.
258pub const NOSTR_KEY_PACKAGE_SLOTS: usize = 5;
259
260/// Maximum discovery records one relay may return for a username query.
261///
262/// A username resolves to the set of devices claiming it, so this bounds how
263/// many claimants a single relay can put in front of a user. Sized well above
264/// any real user's device count and well below
265/// `nostr::MAX_QUERY_EVENTS`, which bounds the whole query across every
266/// connected relay.
267///
268/// The cost of this being too low is not a dropped record but a *displaced*
269/// one: the tag is public, anyone may publish to it, and a squatter who floods
270/// it pushes legitimate claimants out of the answer. That is crowding, which
271/// the design accepts as the price of a non-authoritative directory — the user
272/// still confirms out of band, and the invite path is unaffected.
273pub const NOSTR_DISCOVERY_QUERY_LIMIT: usize = 16;
274
275// Transport-wide Constants
276/// Default maximum message size in bytes (1 MB).
277/// Applied at the transport layer before JSON deserialization to prevent
278/// memory exhaustion from oversized payloads.
279pub const DEFAULT_MAX_MESSAGE_SIZE: usize = 1_048_576;