08 — Authority¶
Authority is who gets to decide. Lattice distinguishes two kinds:
- State authority — the peer that owns and writes the simulation. In
SERVER/HOSTmodes this is the server; it's the only peer that maylattice_spawn, write the state block vialattice_object_state, andlattice_object_mark_dirty. - Input / ownership authority — the peer whose input drives a particular object (its
owner, the value you passed tolattice_spawn). A client can own its avatar without holding state authority over the world.
Verified against: reference/include/lattice/lattice.h:641-645 (spawn/state/mark-dirty are
authority operations, and spawn takes an owner); :548-563 (the two authority models).
Host-authoritative validation (the default model)¶
The default authority model is server/host-authoritative: the server owns every object's state
and clients receive snapshots. So the safe pattern for any client-initiated change is: clients
ask, the authority validates and applies. A client never writes shared state; it sends a
request (an event or an RPC to SERVER), and the authority decides.
Verified against: reference/include/lattice/lattice.h:689-694 ("The default authority model is
SERVER/HOST-authoritative: the server owns the state of every object and clients receive snapshots").
The recipe¶
- Client sends a request — an event to
LATTICE_EVENT_SERVER(or an RPC toLATTICE_RPC_SERVER). Prefer an event when the authority must know who asked, becauseon_eventcarries thesenderandon_rpcdoes not (chapter 07). - Authority validates against its own rules, using the trusted
senderidentity — never a value the client packed into the payload. - On success: write the state block +
lattice_object_mark_dirtyso it replicates. On failure: change nothing (optionally notify). - Clients only ever read replicated state and render it.
This keeps a cheating client from writing illegal state: it can only ask, and the authority is the single writer.
Verified against: reference/include/lattice/lattice.h:311-312 (on_event carries sender);
:507-508 (only the authority writes + marks dirty); the anti-cheat example enforces exactly this at
its apply step (below).
Ownership enforcement, concretely¶
The anti-cheat example server turns this recipe into an enforced rule: a client that RPCs an object
it does not own is denied at the authority's apply step, and the server's on_violation
callback fires with the offending connection. This is the ownership firewall — a client can act only
on objects it owns.
// The server spawned a server-owned object (owner = peer 0). The client does NOT own it.
lattice_rpc(client.runner, server_obj, RPC_SET_SCORE, LATTICE_RPC_SERVER, nullptr, 0, /*reliable*/1);
// … pump …
// The authority firewall denies it and fires on_violation(LATTICE_AC_OWNERSHIP):
server.count_violation(LATTICE_AC_OWNERSHIP) >= 1; // asserted true
uint64_t offender = server.offending_conn(); // learned from the callback, not hardcoded
Verified against: examples/anti-cheat-demo/main.cpp:103-118; the ownership gate is enabled by
c.enforce_ownership = 1 at examples/anti-cheat-demo/server_module.cpp:42-43.
The opt-in anti-cheat layer¶
Server-authoritative validation is powerful but manual. The core also ships an opt-in, server-side anti-cheat layer that runs at the authority's apply step and rejects impossible submissions — input bounds, move speed / teleport, RPC/event floods (token bucket), ownership, lag-comp rewind, fire-rate, aim-snap, and (for shared authority) a tampered state hash. It is default-off (a zero config behaves byte-for-byte as before) and additive.
lattice_result lattice_ac_configure(lattice_runner* r, const lattice_ac_config* cfg);
uint32_t lattice_ac_score(lattice_runner* r, uint64_t conn);
Verified against: reference/include/lattice/lattice.h:621-635; the layer's contract at :139-214.
You turn it on by zero-initializing a lattice_ac_config (all-zero == off), setting enabled = 1,
and setting the bounds you want — anything left 0 disables just that gate:
lattice_ac_config c{}; // all-zero == anti-cheat OFF; we opt in:
c.enabled = 1;
c.max_speed_per_tick = 10; // > 10 world-units/tick is a speedhack
c.max_teleport_dist = 100; // a single jump > 100 is a teleport
c.rpc_bucket_capacity = 3; // token bucket: burst 3, +1/tick
c.rpc_refill_per_tick = 1;
c.enforce_ownership = 1; // deny RPC/write to a non-owned object
c.min_fire_interval_ticks = 5; // >= 5 ticks between shots
c.warn_threshold = 2; // accumulate severity: warn at 2, kick at 6
c.kick_threshold = 6;
lattice_ac_configure(runner_, &c);
Verified against: examples/anti-cheat-demo/server_module.cpp:20-57 (make_ac_config);
installed at :142-143 (lattice_ac_configure).
When a gate rejects a submission, your on_violation callback fires (on the authority, inside
tick, like every callback) with the offending connection, the violation category, the severity it
added, and a diagnostic detail. Accumulated score crossing kick_threshold disconnects the peer
(you observe it via on_disconnected):
void (*on_violation)(void* user_data, uint64_t conn, lattice_ac_violation violation,
uint32_t severity, int64_t detail);
Verified against: reference/include/lattice/lattice.h:324-335; the demo records violations + a
kick at examples/anti-cheat-demo/server_module.cpp:82-98; the config-only opt-in ("A runner with no
anti-cheat config behaves byte-for-byte as before") at reference/include/lattice/lattice.h:146-152.
This is verified
The anti-cheat demo drives a malicious client against a real SERVER runner over the loopback
transport and asserts each defense fires: an RPC to a non-owned object is denied; an RPC flood is
token-bucket rate-limited and the peer is kicked at the threshold; speed-hack/teleport moves are
rejected with a rollback target; rapid-fire trips the fire-rate heuristic. It ends
ALL ANTI-CHEAT DEFENSES FIRED AS EXPECTED.
Verified against: examples/anti-cheat-demo/main.cpp:87-155 (the live runner path);
examples/README.md ("SUMMARY: 33 passed, 0 failed").
Distributed / shared authority¶
For games where authority over an object should move between peers (e.g. whoever grabbed the ball owns it), Lattice has a second first-class model: shared / distributed authority, which coexists per-object with server-authoritative objects in the same session. A shared object's state authority can be held by a specific client, which simulates it and broadcasts its state while every other peer converges.
The API:
lattice_result lattice_request_authority(lattice_runner* r, lattice_netid id);
uint64_t lattice_object_owner(lattice_runner* r, lattice_netid id); /* current holder */
uint32_t lattice_object_authority_tick(lattice_runner* r, lattice_netid id); /* token claim counter */
Verified against: reference/include/lattice/lattice.h:687-719.
Which peer am I?
Comparing lattice_object_owner() against your own id needs lattice_runner_local_peer_id() —
an authority is peer 0 (not 1), and a plaintext client honestly reports
LATTICE_LOCAL_PEER_UNKNOWN rather than guessing. See
Peer identity & local player.
lattice_request_authority is sent reliably to the host arbiter (the SHARED_HOST runner), which
mints a new ownership token (a tick strictly higher than anything in flight) and broadcasts the
grant. Once observed, on_authority_changed fires on every peer the tick it adopts the new
token — the granting host, the new owner, and every other peer — so game code can start/stop
simulating the object locally:
void (*on_authority_changed)(void* user_data, lattice_netid id, uint64_t new_owner,
uint32_t authority_tick);
static void on_authority_changed(void* user, lattice_netid id, uint64_t new_owner, uint32_t tick) {
// new_owner now holds state authority over `id`, as of `tick`.
// Start simulating it locally if new_owner == our peer id; stop if we just lost it.
}
Verified against: reference/include/lattice/lattice.h:313-322 (fires on every peer; never fires
for a server/host-authoritative object); :565-573 (the request → mint → broadcast flow). Conflicts
resolve by last-writer-wins with a tick tiebreak (higher tick wins; equal ticks → lower owner id
wins), which is deterministic.
Verified against: reference/include/lattice/lattice.h:697-698.
This path is intended for LATTICE_MODE_SHARED_HOST sessions.
Real ABI, but not exercised end-to-end by the runnable drivers
lattice_request_authority / on_authority_changed / lattice_object_owner /
lattice_object_authority_tick are genuine ABI functions with the documented semantics above.
The two runnable two-process drivers (persistent_server.cpp, two_process.cpp) are
single-authority (server/host), so they do not perform an authority transfer, and in the
reference skeleton SHARED_HOST behaves like HOST. Design against this surface for
shared-authority games and test the transfer in your own integration.
Verified against: reference/include/lattice/lattice.h:81 (SHARED_HOST "skeleton: like
HOST"); absence of any lattice_request_authority call in reference/tests/two_process.cpp and
reference/tests/persistent_server.cpp.
Next: 09 — Full walkthrough, where a minimal server + client come together end to end.