DevLog #5 - How I Built RiftNet & the SyncServer (No Illusions, Only Signal)
Posted on August 21, 2025 by Brinn
Premise: build the transport I can trust. No outsourced truth, no visual band-aids. Compress, encrypt, acknowledge; then ship the bytes the server actually simulated.
Architecture in One Pass
- Raw sockets -> Channel -> Reliability -> Compressor -> Packer
- Channel: X25519 key exchange. Payloads stay blocked until keys are derived. Compress then encrypt.
- AEAD: pluggable; AES-GCM by default via libsodium.
- Reliability: custom reliable UDP with seq/ack, sliding window, dedupe.
- Timing: RFC-6298 RTT estimator (srtt, rttvar, rto) driving retransmits.
- Diagnostics: log every window: inputs, snapshot_ms, dispatch_ms, baseline/delta counts.
Handshake & Keys
- Client/Server exchange ephemeral X25519 pubkeys.
- Derive sharedTx/sharedRx keys. Until that’s set, any reliable/encrypted message is rejected.
- All pre-secure payloads (e.g., HELLO) live on a tiny side queue then drain post-init.
Packet Layout & Order of Operations
- Serialize minimal header (flags | seq | ack | ackBits | nonce).
- Pack delta payload(s) against the active baseline.
- Run LZ4 on the payload segment.
- Seal with AEAD (AES-GCM) using the channel’s nonce discipline.
- Send via UDP. Receiver verifies, inflates, applies, acks.
The Loop (SyncServer)
- Ingest inputs for tick
T. - Advance simulation (authoritative).
- Produce snapshot: baseline (periodic) + delta (per-tick).
- Dispatch to clients; update reliability state.
Target cadence: 200 Hz (DT_US = 5000).
Determinism Guard
For every received frame, compute a canonical FNV-1a hash on serialized entity fields after sorting IDs. Any divergence trips alarms in test - no "close enough."
Concurrency Notes
- Encryption/compression run on a worker path; hot structures are
std::atomicguarded; minimal mutex spans. - Per-client send rings to avoid cross-talk; preallocated buffers to keep the allocator cold.
- Back-pressure when a client falls behind; reliability takes precedence over stuffing more deltas.
What the Numbers Said
- Snapshot build: ~2-3 ms @ modest entity counts.
- Dispatch: < 0.5 ms per window.
- Frame size: ~26-94 bytes depending on movement/delta density.
- Scale: 50 -> 500 -> 5,000 connection soak (local env) without reliability collapse.
Beyond One Box
Spec'd a GlobalShardSyncServer to gossip shard truths. Implementation lands after zones/spawns, once there’s something worth gossiping.
Why It’s Built This Way
- Truth first: server is the source of truth; clients draw what the server simulates.
- No illusions: no hidden TCP, no jitter make-up stories. Reliable UDP, explicit.
- Security native: crypto/compression aren’t add-ons; they’re in the pipeline.