summaryrefslogtreecommitdiffstats
path: root/docs/adr/0004-protocol-v5.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/adr/0004-protocol-v5.md')
-rw-r--r--docs/adr/0004-protocol-v5.md119
1 files changed, 119 insertions, 0 deletions
diff --git a/docs/adr/0004-protocol-v5.md b/docs/adr/0004-protocol-v5.md
new file mode 100644
index 0000000..c74ad83
--- /dev/null
+++ b/docs/adr/0004-protocol-v5.md
@@ -0,0 +1,119 @@
1# 0004 — Protocol v5: frames, envelopes, time and commit–reveal
2
3Status: accepted, 2026-11-08
4
5## Context
6
7The current wire is newline-delimited text over TLS. It has served the TUI and
8GUI well, but four problems are now worth fixing before a browser gateway is
9built on top:
10
11- `LineBuffer` grows without bound if a peer never sends a newline, and there
12 is no explicit maximum message size.
13- A reply is assumed to answer the request that is currently pending. A late
14 reply, a retry or a second in-flight request can clobber state.
15- There is no idempotency: a gateway retry after a timeout can charge a bet
16 twice.
17- The crash multiplier is anchored to each frontend's local clock, and the
18 crash point is only "trust the server".
19
20The web gateway makes the first three worse (it retries and multiplexes) and
21the browser makes the fourth visible (a page and the desktop can disagree).
22
23## Decision
24
25This is one wire change and one `MAJOR` release (`5.0.0`); there is no
26compatibility mode.
27
28### Framing
29
30- Every packet is `[u32 length][payload]`, length big-endian.
31- `payload` is UTF-8 text, at most 64 KiB. A larger declared length is a
32 protocol error and the connection closes; buffers never grow past one frame.
33- The payload has no trailing newline.
34
35### Envelope
36
37- `REQ <id> [idem=<token>] <command>` — client request. `<id>` is a `u64`
38 chosen by the client, monotonically increasing on the connection.
39- `RES <id> <body>` — the reply to that request.
40- `EVT <seq> <body>` — a server-initiated event. `<seq>` is a `u64`,
41 incremented once per event on the connection.
42
43The existing text grammar is the `<command>` and `<body>`; `HELLO 5.0.0`,
44`PLAY_START crash 50`, `GAME_RESULT ...` keep their meaning.
45
46`HELLO` is the first framed request. The server ignores every other command
47before a successful handshake. The reply is
48`HELLO <version> latest=<v> url=<url> time=<unix_ms>`.
49
50### Correlation and idempotency
51
52- The client keeps a request id until its `RES` arrives. Replies are routed by
53 id, never by "the next line".
54- The server keeps a bounded 256-entry FIFO of `(id, payload, reply)` on each
55 connection. A repeated id with the same payload returns the stored reply
56 without executing the command again. A repeated id with a different payload
57 returns `ERROR Request id reused with a different command`.
58- The cache is per connection and in memory. A reconnect is a new game session
59 by design; durable idempotency is deferred.
60- `idem=<token>` is optional metadata for logs and for a future cross-
61 connection cache. The request id is the cache key.
62
63### Clock and ticks
64
65- `HELLO` and `PONG` carry the server's Unix time in milliseconds. `PING`
66 replies `PONG <unix_ms>`.
67- Clients keep an offset between server time and their own clock. The offset is
68 for display and interpolation only; the server clock settles a cash-out.
69- A crash start carries `started_at=<unix_ms>` and `commit=<hex>`.
70- While a crash round runs, the server pushes at most one
71 `EVT <seq> crash.tick session=<id> t=<unix_ms> m=<multiplier>` per 100 ms.
72 The client interpolates between ticks and may keep rendering locally.
73- `CRASH_BUSTED` remains an event, now with the reveal attached.
74
75### Commit–reveal crash
76
77- At round start the server draws 32 random bytes as the round seed and sends
78 `commit = sha256(seed)` in hex. It also sends the crash point's start time.
79- The crash point is a pure function of the seed:
80 `u = u64_be(seed[0..8]) / 2^64`, then
81 `crash_point_from_uniform(u)`. The seed never reaches the client before the
82 round ends.
83- When the round ends (`CRASH_STATE` after cash-out, or `CRASH_BUSTED`), the
84 server includes `seed=<hex>`. A client verifies `sha256(seed) == commit` and
85 recomputes the crash point. The UI can show "verified".
86- This proves the crash point was fixed before the player acted. It does not
87 prove the seed was not ground to a favourable value; a hash chain with client
88 entropy is the documented next step if that matters.
89
90## Consequences
91
92- The TUI, GUI, server and gateway all move to v5 together; a v4 peer is
93 refused at `HELLO`.
94- `LineBuffer` is replaced by a bounded `FrameBuffer`. Existing newline-based
95 tests and helpers are rewritten around frames.
96- `ClientCore` allocates request ids and routes replies by id. Its `Effect`
97 carries the id, so an adapter writes the envelope and hands the `RES` back
98 with the same id.
99- Crash rendering can be smoother and consistent across frontends, and the
100 crash result is independently checkable from the revealed seed.
101- The per-connection cache is deliberately small and forgets on reconnect; a
102 retry after a reconnect is a new action, not a replay.
103
104## Alternatives considered
105
106- **JSON or binary frames**: cleaner types or smaller payloads, but a rewrite
107 of every parser for no scale need. Text inside a bounded frame keeps logs
108 readable and the change small.
109- **Newline-delimited frames with a maximum line length**: still needs a
110 separate envelope and does not bound the buffer as cleanly; a declared length
111 is simpler.
112- **Durable idempotency keyed by `(user, idem)`**: survives reconnects, but the
113 gateway owns retries on one connection and a reconnect is a fresh session.
114 Deferred.
115- **Server seed plus client seed without a commit round-trip**: the server
116 sees the client seed before committing and can bias the pair; a half-measure
117 is worse than an honest per-round commitment.
118- **Hash-chain epoch**: stronger anti-grinding, but requires persistent chain
119 state and a restart story. Deferred.