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

  1. Serialize minimal header (flags | seq | ack | ackBits | nonce).
  2. Pack delta payload(s) against the active baseline.
  3. Run LZ4 on the payload segment.
  4. Seal with AEAD (AES-GCM) using the channel’s nonce discipline.
  5. Send via UDP. Receiver verifies, inflates, applies, acks.

The Loop (SyncServer)

  1. Ingest inputs for tick T.
  2. Advance simulation (authoritative).
  3. Produce snapshot: baseline (periodic) + delta (per-tick).
  4. 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::atomic guarded; 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.
Read Prev DevLog Read Next DevLog