Skip to content

IFM Packet Specification

Overview

The IFM packet is the fundamental unit of communication. Everything—text, voice, video, files, control messages—is encoded as a packet. The protocol is packet-based, not stream-based, which keeps the core simple and flexible.


Packet Structure (Wire Format)

Field-by-Field Specification

OffsetFieldTypeSizeDescription
0versionu81Protocol version (1)
1–32frequency[u8; 32]32BLAKE3 hash of canonical frequency
33–64packet_id[u8; 32]32BLAKE3 hash of packet (excl. signature)
65–96sender[u8; 32]32Sender's Ed25519 public key
97–104timestampu648Unix ms since epoch (little-endian)
105ttlu81Hops remaining (initial: 16)
106–113sequenceu648Per-sender monotonic counter
114payload_typeu81See Payload Types table
115–118payload_lengthu324Payload bytes (little-endian)
119–NpayloadVec<u8>VariableEncrypted/compressed data
N+1–N+64signature[u8; 64]64Ed25519 signature over bytes 0..N

Total header overhead: 119 bytes + payload + 64 bytes signature


Payload Types

ValueNameDescriptionTypical Size
0x00TEXTUTF-8 text message< 1 KB
0x01VOICEOpus frame (20ms)200–800 bytes
0x02VIDEOVideo frame (AV1/VP9/H.264)1–50 KB
0x03IMAGEJPEG/PNG/WebP image10–500 KB
0x04FILEFile chunk (see File Transfer)64 KB
0x05JSONStructured data< 10 KB
0x06PINGKeepalive/latency probe~32 bytes
0x07PRESENCEJoin/leave/status update< 500 bytes
0x08CONTROLProtocol control (HELLO, etc.)< 1 KB
0x09PLUGINPlugin-defined payloadVariable

Packet ID Generation

The packet_id is a BLAKE3 hash of the entire packet excluding the signature:

rust
fn compute_packet_id(packet: &PacketWithoutSignature) -> [u8; 32] {
    let mut hasher = blake3::Hasher::new();
    hasher.update(&[packet.version]);
    hasher.update(&packet.frequency);
    hasher.update(&packet.sender);
    hasher.update(&packet.timestamp.to_le_bytes());
    // NOTE: TTL is deliberately NOT part of the id — relays decrement it
    // without re-signing, so the id must stay stable across hops for
    // mesh-wide duplicate detection.
    hasher.update(&packet.sequence.to_le_bytes());
    hasher.update(&[packet.payload_type]);
    hasher.update(&packet.payload_length.to_le_bytes());
    hasher.update(&packet.payload);
    *hasher.finalize().as_bytes()
}

Properties

  • Deterministic: Same packet → same ID
  • Collision-resistant: 256-bit output
  • Tamper-evident: Any change → different ID
  • Used for: Deduplication cache, Store & Forward indexing

Signature

Every packet is signed with the sender's Ed25519 private key:

rust
fn sign_packet(private_key: &[u8; 32], packet_bytes: &[u8]) -> [u8; 64] {
    // packet_bytes = all fields EXCEPT signature
    ed25519::sign(private_key, packet_bytes)
}

Verification (performed by EVERY relay and recipient)

rust
fn verify_packet(public_key: &[u8; 32], packet_bytes: &[u8], signature: &[u8; 64]) -> bool {
    ed25519::verify(public_key, packet_bytes, signature)
}

Rules:

  • Relays MUST verify before forwarding
  • Recipients MUST verify before processing
  • Invalid signature → drop packet, do NOT relay
  • The signature covers every field EXCEPT id, signature, and ttl — TTL is hop metadata: relays decrement it when forwarding without re-signing, so a signed TTL would make every relayed copy fail verification. Excluding it also keeps the packet id stable across hops, so duplicate detection works mesh-wide.

TTL (Time To Live)

ParameterValue
Initial TTL16 hops
Decrement1 per relay
ExpiryDrop when TTL = 0

TTL Flow

Important: Relays forward the packet with TTL decremented and do NOT re-sign. Because TTL is excluded from the signed view, the signature stays valid and the packet id is identical at every hop — so relayed copies verify and deduplicate correctly across the whole mesh.


Sequence Numbers

  • Per-sender: Each sender maintains a monotonically increasing u64 counter
  • Purpose: Replay protection, ordering, nonce for encryption
  • Wraparound: At u64::MAX (practically never), reset with new identity
  • Verification: Recipients track highest seen sequence per sender; reject ≤ seen

Timestamp

  • Format: Unix milliseconds since epoch (u64, little-endian)
  • Source: Sender's local clock
  • Usage:
    • Freshness check (reject packets older than 5 min by default)
    • Latency measurement (PING/PONG)
    • Sequence tiebreaker
  • Clock skew: Tolerated ±30 seconds

Payload Encoding

TEXT (0x00)

Raw UTF-8 bytes

VOICE (0x01)

  • Frame size: ~200–800 bytes depending on bitrate
  • No additional header — Opus is self-delimiting

VIDEO (0x02)

CodecValue
AV10x01
VP90x02
H.2640x03
H.2650x04
FlagBitMeaning
Keyframe0I-frame (random access point)
Config1Contains codec config (extradata)

IMAGE (0x03)

FormatValue
JPEG0x01
PNG0x02
WebP0x03
AVIF0x04

FILE (0x04) — See File Transfer Section

JSON (0x05)

Raw UTF-8 JSON bytes (no BOM)

PING (0x06)

json
{
  "timestamp": 1699999999999,
  "nonce": "random16bytes"
}

PRESENCE (0x07)

json
{
  "action": "join | leave | update",
  "node_id": "12D3KooW...",
  "display_name": "optional",
  "status": "available | busy | away",
  "frequencies": ["91.700", "chat.general"]
}

CONTROL (0x08) — See Protocol Spec

PLUGIN (0x09)


File Transfer

Large files are split into chunks, each sent as a separate FILE packet.

File Manifest (First Packet)

json
{
  "type": "manifest",
  "file_id": "BLAKE3 hash of entire file",
  "filename": "video.mp4",
  "size": 104857600,
  "chunk_size": 65536,
  "total_chunks": 1600,
  "mime_type": "video/mp4",
  "encryption": "none | chacha20poly1305"
}

File Chunk (Subsequent Packets)

Every chunk carries the file_id so reassembly is keyed per transfer — two senders broadcasting files on the same frequency concurrently can never attribute a chunk to the wrong file, and chunks that arrive before the manifest are held until it lands.

Reassembly

  1. Receive manifest → allocate buffer / temp file (per file_id)
  2. Receive chunks in any order — each carries its file_id
  3. Verify each chunk hash
  4. Write chunk at offset = index × chunk_size
  5. When all chunks received → verify full file hash
  6. Deliver to application

Compression

  • Algorithm: zstd (level 3 default)
  • Applied: Only to payloads > 1 KB
  • Indicator: Compressed flag in packet header (future: use payload_type high bit)
  • Dictionary: None (standard zstd)
rust
fn maybe_compress(payload: &[u8], payload_type: u8) -> Vec<u8> {
    if payload.len() > 1024 && is_compressible_type(payload_type) {
        zstd::encode(payload, 3)
    } else {
        payload.to_vec()
    }
}

Compressible types: TEXT, JSON, FILE chunks, PLUGIN Non-compressible: VOICE (Opus), VIDEO, IMAGE (already compressed)


Encryption (Protected/Hidden Frequencies)

Applied at payload level before signing:

rust
fn encrypt_payload(payload: &[u8], key: &[u8; 32], sequence: u64, aad: &[u8]) -> Vec<u8> {
    let nonce = build_nonce(nonce_prefix, sequence);
    chacha20_poly1305_encrypt(key, &nonce, payload, aad)
}
  • Algorithm: ChaCha20-Poly1305 (AEAD)
  • Key: Derived from shared secret + canonical frequency (HKDF)
  • Nonce: 12-byte prefix + 4-byte sequence (big-endian)
  • AAD: Canonical frequency string
  • Result: Ciphertext + 16-byte auth tag appended

Important: Signature covers the encrypted payload. Verification happens after decryption (recipient) or on ciphertext (relay — relay cannot decrypt protected frequencies, only verifies signature on ciphertext).


Maximum Limits

LimitValueEnforcement
Max packet size1,048,576 bytes (1 MB)Drop larger
Max payload size1,048,400 bytesHeader + sig overhead
Max frequency name256 bytesParse error
Dedup cache size100,000 entriesLRU eviction
Sequence window2^32 per senderTrack highest seen

Packet Processing Pipeline

Sender

1. Application calls broadcast(data)
2. SDK determines payload_type, encodes payload
3. Core assigns sequence, timestamp, TTL=16
4. If protected freq: encrypt payload
5. If compressible: compress payload
6. Compute packet_id = BLAKE3(header + payload)
7. Sign packet with Ed25519 private key
8. Add to local dedup cache
9. Publish to GossipSub topic

Relay

Recipient


Mermaid: Packet Lifecycle


Example: Minimal TEXT Packet (Hex Dump)

# Header (119 bytes)
01                                              # version = 1
a1 b2 c3 d4 ... (32 bytes)                     # frequency hash
e5 f6 78 90 ... (32 bytes)                     # packet_id
11 22 33 44 ... (32 bytes)                     # sender public key
00 00 00 00 00 65 4a 8b                        # timestamp = 1699999999999 (le)
10                                             # ttl = 16
01 00 00 00 00 00 00 00                        # sequence = 1
00                                             # payload_type = TEXT (0)
05 00 00 00                                    # payload_length = 5

# Payload (5 bytes)
48 65 6c 6c 6f                                 # "Hello"

# Signature (64 bytes)
9a 8b 7c 6d 5e 4f 3a 2b ... (64 bytes)         # Ed25519 signature

Versioning & Extensibility

  • Version 1: Current specification
  • New payload types: Add to registry, unknown types → forward opaque
  • New fields: Use PLUGIN payload or extend via version negotiation
  • Breaking changes: Increment version, nodes reject unknown versions

Released under the MIT License.