diff --git a/docs/LIVE_VOICE_V1.md b/docs/LIVE_VOICE_V1.md new file mode 100644 index 00000000..0b717646 --- /dev/null +++ b/docs/LIVE_VOICE_V1.md @@ -0,0 +1,204 @@ +# Live voice v1 wire protocol + +This document defines the interoperable live push-to-talk protocol used by +Android, Wear OS, and iOS clients. Live packets are ephemeral and best-effort; +the ordinary voice note sent when the user releases the microphone remains the +reliable, persistent fallback. + +All multi-byte integers in this document use network byte order (big-endian). + +## Type assignments + +| Layer | Name | Value | Purpose | +| --- | --- | --- | --- | +| Outer mesh packet | `MessageType.VOICE_FRAME` | `0x29` | Signed public-mesh burst packet | +| Noise inner payload | `NoisePayloadType.VOICE_FRAME` | `0x08` | Encrypted direct-message burst packet | +| Outer private packet | `MessageType.NOISE_ENCRYPTED` | `0x11` | Existing recipient-directed Noise envelope | + +These values are canonical. Implementations must not emit live voice under a +different outer or inner type. + +## Transport envelopes + +### Public mesh + +The sender emits a normal mesh packet with: + +- version `1`; +- type `VOICE_FRAME (0x29)`; +- a broadcast recipient (either no recipient field or the eight-byte all-`FF` + broadcast value; receivers accept both encodings); +- the encoded `VoiceBurstPacket` as its payload; +- origin TTL `7`; and +- an Ed25519 signature over the canonical outer packet signing bytes. + +Receivers accept public live voice only from a known peer whose announcement +has established a verified nickname and signing key. They must reject an +unsigned packet, an invalid signature, a non-broadcast recipient, or a packet +whose timestamp is more than 30 seconds from the receiver's current time. A +rejected public voice packet must not be relayed. + +Public live voice is not added to gossip sync. Older clients ignore the unknown +`0x29` type and therefore remain compatible. + +### Direct message + +The sender requires an established Noise session. It encodes: + +```text +[NoisePayloadType.VOICE_FRAME: 0x08][VoiceBurstPacket] +``` + +and encrypts that value in a recipient-directed `NOISE_ENCRYPTED (0x11)` mesh +packet. The existing Noise session provides confidentiality and authentication. +Intermediate relays see only the ordinary Noise envelope and route it according +to the existing directed-packet rules. + +There are no delivery acknowledgements or retransmissions for live packets. +If no established session exists when recording begins, the client records and +sends only the ordinary voice note. + +## `VoiceBurstPacket` + +Every packet in one press-and-hold gesture shares an opaque, randomly generated +eight-byte burst ID. + +```text ++------------+-------------+-------------+------------------+ +| burstID | sequence | flags | payload | +| 8 bytes | UInt16 BE | UInt8 | variant-specific | ++------------+-------------+-------------+------------------+ +``` + +The fixed header is 11 bytes. `flags` is a complete discriminator, not a +bitset that may combine values. Unknown or combined flag values are invalid. + +### START (`flags = 0x01`) + +```text +[codec: UInt8] +``` + +Current senders use sequence `0`. The only v1 codec is: + +| Codec | Value | Encoded frames | +| --- | --- | --- | +| AAC-LC, 16 kHz, mono | `0x01` | Raw AAC access units without ADTS headers | + +Receivers must reject unsupported codec values. Because START can be lost on a +best-effort mesh, a receiver may establish a v1 assembly from a valid data +packet and use the sole v1 codec. + +### Data (`flags = 0x00`) + +The payload contains one to eight length-prefixed AAC access units: + +```text +[length: UInt16 BE][AAC access unit] ... +``` + +Each length must be non-zero and must not extend past the packet. A sender uses +sequence `1` for the first data packet and increments the value for each later +data packet. The sequence counts data packets, not individual AAC frames. + +The v1 sender budget is 210 bytes for the entire `VoiceBurstPacket`. This keeps +one live packet below the transport's fragmentation threshold after the Noise +type byte, authentication tag, and BLE padding are applied. An encoded frame +that cannot fit this budget is dropped rather than fragmented. + +### END (`flags = 0x02`) + +```text +[totalDataPackets: UInt16 BE][durationMs: UInt32 BE] +``` + +`totalDataPackets` is the number of data packets emitted for the burst; it does +not include START or END. `durationMs` describes the encoded audio duration. +The END packet uses the next sequence value after the final data packet. +Receivers use the total to account for tail loss before finalizing playback. + +### CANCELED (`flags = 0x04`) + +CANCELED has no payload and uses the next sequence value. A receiver stops live +playback, discards buffered audio, removes the transient message, and does not +wait for a finalized voice note. + +## Sender lifecycle + +A successful live gesture follows this order: + +1. Generate one eight-byte burst ID. +2. Emit START at sequence `0` after the first encoded AAC access unit becomes + available. +3. Emit data packets starting at sequence `1`. +4. On release, flush pending data and emit END. +5. Send the finalized M4A through the existing voice-note transfer path. + +The finalized file name is `voice_.m4a`, where the hexadecimal +component is the 16 lowercase characters representing the same eight-byte +burst ID. This lets the receiver replace the transient live capture with the +reliable final voice note without adding another wire field. + +If capture is canceled or does not produce a valid recording, the sender emits +CANCELED and does not send a final note. + +AAC-LC at 16 kHz uses 1,024 samples per access unit, so each encoded frame +represents 64 ms of audio. Live access units are ADTS-less; receivers may add an +ADTS header locally for streaming playback or temporary-file assembly. + +## Receiver safety and ordering + +Receivers must fail closed on malformed framing and must bound concurrent +assemblies, buffered out-of-order packets, bytes per burst, and inbound byte +rate. The Android/Wear OS v1 bounds are: + +- 8 concurrent assemblies; +- 384 KiB per burst; +- 6,000 inbound bytes per second per assembly, with a two-second initial + allowance; and +- 128 buffered out-of-order packets. + +Duplicate and already-delivered sequence values are ignored. Android/Wear OS +wait 550 ms for a sequence gap before skipping it and finalize an idle partial +burst after 3 seconds. These timers are receiver policy rather than additional +wire fields. + +## Relay behavior + +Public `VOICE_FRAME` packets use the normal mesh relay path only after signature, +sender, timestamp, recipient, and burst validation succeeds. Relays add a small +8–25 ms jitter. In a mesh larger than six peers, the relayed TTL is capped at +five after the normal per-hop decrement. Live packets remain excluded from +gossip sync and file-transfer retransmission. + +Private frames retain the existing `NOISE_ENCRYPTED` relay behavior because an +intermediate node cannot inspect the `0x08` inner type. + +## Golden vectors + +For burst ID `01 02 03 04 05 06 07 08`, the canonical encodings are: + +```text +# START, sequence 0, AAC-LC/16 kHz/mono +01 02 03 04 05 06 07 08 00 00 01 01 + +# One data packet, sequence 1, one three-byte frame DE AD BE +01 02 03 04 05 06 07 08 00 01 00 00 03 DE AD BE + +# END, sequence 42, 41 data packets, duration 2688 ms +01 02 03 04 05 06 07 08 00 2A 02 00 29 00 00 0A 80 + +# CANCELED, sequence 3 +01 02 03 04 05 06 07 08 00 03 04 +``` + +The corresponding executable Android vectors live in +`VoiceBurstPacketTest.encodedGoldenVectorsMatchIos`. + +## Implementation references + +- `app/src/main/java/com/bitchat/android/protocol/BinaryProtocol.kt` +- `app/src/main/java/com/bitchat/android/model/NoiseEncrypted.kt` +- `app/src/main/java/com/bitchat/android/features/voice/VoiceBurstPacket.kt` +- `app/src/main/java/com/bitchat/android/features/voice/LiveVoiceManager.kt` +- `app/src/test/java/com/bitchat/android/features/voice/VoiceBurstPacketTest.kt` diff --git a/docs/client-rewrite-contracts.md b/docs/client-rewrite-contracts.md index 0337ccf9..1abed243 100644 --- a/docs/client-rewrite-contracts.md +++ b/docs/client-rewrite-contracts.md @@ -14,7 +14,7 @@ The remaining implementation work and milestone progress are tracked in |---|---|---| | Outer mesh packet | v1/v2 header widths, big-endian fields, flags, section order, route placement, signature placement, padding, compression, signing bytes | `BinaryProtocolTest`, `ClientRewriteWireContractTest` | | Chat payload | Flag bits, millisecond timestamp, UTF-8 byte lengths, encrypted-content substitution, optional-field order | `ClientRewriteWireContractTest` | -| Inner payloads | Noise type bytes, private-message TLVs, peer-state TLVs, file-transfer TLVs, fragment header, sync request TLVs | `ClientRewriteWireContractTest`, `AuthenticatedPeerStateTest`, `PrivateMediaTransferPreparerTest`, `FragmentManagerTest` | +| Inner payloads | Noise type bytes, private-message TLVs, peer-state TLVs, file-transfer TLVs, live-voice bursts, fragment header, sync request TLVs | `ClientRewriteWireContractTest`, `AuthenticatedPeerStateTest`, `PrivateMediaTransferPreparerTest`, `VoiceBurstPacketTest`, `FragmentManagerTest` | | Identity/security | Announcement extensions, capability bitfield endianness, Noise static-key binding, handshake identity binding, signatures | `IdentityAnnouncementTest`, `NoiseSessionManagerIdentityBindingTest`, `ClientRewritePrimitiveContractTest` | | Sync/routing | Stable packet IDs, GCS bitstream, replay collapse, TTL handling, relay choice, confirmed graph edges | `ClientRewritePrimitiveContractTest`, `GCSFilterTest`, `PacketRelayManagerTest`, `MeshGraphServiceTest`, `TransportBridgeServiceTest` | | Nostr | Bech32, secp256k1 key derivation, NIP-01 event IDs/signatures, NIP-44 authenticated encryption, NIP-13 PoW, authenticated NIP-17 seals | `ClientRewriteNostrContractTest`, `NostrProtocolTest` |