summaryrefslogtreecommitdiffstats
path: root/docs/adr/0004-protocol-v5.md
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-11-08 03:29:35 +0100
committersalmonik <salmonik@tuta.io>2026-11-08 03:29:35 +0100
commit058432130628d00764663ecf819a9adc6793768d (patch)
treed3f49f111fbfaa48647cca2600adcf9017af2d30 /docs/adr/0004-protocol-v5.md
parentweb: serve latest.php through PHP and refuse stray PHP files (diff)
downloadzagan-bet-058432130628d00764663ecf819a9adc6793768d.tar.gz
zagan-bet-058432130628d00764663ecf819a9adc6793768d.zip
v5.0.0: protocol v5 and the browser frontend
Protocol v5 replaces newline lines with bounded [u32 len][text] frames and REQ/RES/EVT envelopes. Requests carry correlation ids and the server replays the last 256 replies per connection, so a gateway retry cannot double-debit and a late reply cannot clobber a newer request. HELLO/PONG carry server time; crash rounds announce started_at and push coalesced 10 Hz ticks. Crash now commits sha256(seed) before a round and reveals the seed when it ends, so clients recompute the crash point. Adds the webd/ gateway: one blocking thread, one ClientCore and one pinned upstream TLS connection per session, an HttpOnly session cookie, takeover, and a 60-second grace window. The browser sends Command JSON and renders View JSON; it never speaks the game protocol or stores credentials. Adds the vanilla TypeScript browser app at /play/ (login, lobby, all four games, history, notices, crash verification), the nginx /play and /ws routes, and the headless DOM smoke test. webd and the app stay out of the TUI/GUI/server build graph. The client/server version moves to 5.0.0 in lockstep; v4 peers are refused at HELLO.
Diffstat (limited to '')
-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.