The op byte
One byte says what a record is. Values 0 to 217 are moves: the byte is the move code itself. Everything above is a command.
| op | Record | Args | Valid when |
|---|---|---|---|
0..217 | MOVE (the code) | signer's turn, the code is legal, the clock has not run out | |
218 | RESIGN | game running | |
219 / 220 / 221 | DRAW_OFFER / ACCEPT / DECLINE | accept only an offer that is standing and in the signer's seen | |
222 / 223 / 224 | TAKEBACK_OFFER / ACCEPT / DECLINE | unrated games, the offerer has moved | |
225 | ABORT | fewer than two moves made | |
226 | FLAGGED | "my own clock ran out" | |
227 | FLAG_CLAIM | "your clock ran out, as of my at" (peer clocks) | |
229 | END | result, termination | after the game ended: "I sign this result" |
230 | ACK | always: "I applied your record seen at my time at" | |
231 | DISPUTE | reason, subject seq | a live time check failed |
232 | RESUME | continue after an outage beyond the budget (both must send it) | |
233 | CANCEL | 1 leave, 2 opponent gone | game running |
234 | LINK | peer clocks, no settle open: "the path between us changed", restarts the transit estimate (see Clocks) | |
240 / 241 / 242 | VERDICT_FLAG / ABANDON / RESULT | color · result, clocks | server only, game has an arbiter |
228, 235..239, 243..255 | reserved | always invalid |
Chat, rematch offers and presence are not records: they never change the game.
On the wire
What actually travels between players is tiny. The signer is not sent (it is the peer on a direct link, and the container says it everywhere else), and neither is anything the receiver can rebuild.
Varints are unsigned LEB128 and must be minimal, so a record has exactly one byte form. A server record has a second seen (one per player).
Header and body
The receiver rebuilds two structures from its own state and the wire bytes. They are never sent, only signed.
body_hash = SHA-256(body)
record id = SHA-256(header)
signature = Ed25519(device key, header)
Why two layers
The signature covers only the short header, and the header holds only a hash of the body. So anyone with the genesis can check a record's head (signer, seq, body_hash, signature) without seeing the moves, and that head still commits to the whole game so far, through prev (the signer's history) and seen_hash (the opponent's history up to what the signer saw). Anchors and stored games use exactly this.
Why ply and legal
A move code only means something in a known position. If two engines disagree about which moves are legal (a bug, an old version), they would silently play different moves from the same byte. Putting the number of legal moves into the signed body turns that into a signature failure on the spot.
Identity is the header, not the bytes
A record is identified by its record id, the hash of its header, never by its wire bytes. A dishonest signer can produce a second valid signature for the same header. Two copies with equal headers and different signature bytes are the same record: a duplicate, not a fork.
ACKs: where every turn starts
Every received record except an ACK is answered at once with an ACK. Its seen names the record it applied, its at the moment it applied it. ACKs never get acknowledged and do nothing on the board, but they give every turn a start and an end on the mover's own clock. See Clocks.
These are the records of the conformance vectors. The at values come from two different clocks, so they are never compared directly: Black thought from its ACK (74 239) to its move (75 439), 1.2 seconds on its own clock.