Pawnshake v0.1.0 · draft

Protocol

Records

Every move and every command is one record: a few varints and a byte on the wire, a 77-byte header under the signature, and a body the header commits to.

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.

opRecordArgsValid when
0..217MOVE (the code)signer's turn, the code is legal, the clock has not run out
218RESIGNgame running
219 / 220 / 221DRAW_OFFER / ACCEPT / DECLINEaccept only an offer that is standing and in the signer's seen
222 / 223 / 224TAKEBACK_OFFER / ACCEPT / DECLINEunrated games, the offerer has moved
225ABORTfewer than two moves made
226FLAGGED"my own clock ran out"
227FLAG_CLAIM"your clock ran out, as of my at" (peer clocks)
229ENDresult, terminationafter the game ended: "I sign this result"
230ACKalways: "I applied your record seen at my time at"
231DISPUTEreason, subject seqa live time check failed
232RESUMEcontinue after an outage beyond the budget (both must send it)
233CANCEL1 leave, 2 opponent gonegame running
234LINKpeer clocks, no settle open: "the path between us changed", restarts the transit estimate (see Clocks)
240 / 241 / 242VERDICT_FLAG / ABANDON / RESULTcolor · result, clocksserver only, game has an arbiter
228, 235..239, 243..255reservedalways 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.

Wire record~70 bytes
seq varint signer's number, from 1
seen varint opponent's newest seq applied
at varint ms since START
op 1 B
args 0+
signature 64 B Ed25519 over the header

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.

Body76 bytes for a move
prev 32 B id of my seq-1 (genesis hash for seq 1)
seen 4 B
seen_hash 32 B id of the opponent's record #seen
at 4 B
op 1 B
args 0+
ply 2 B moves only
legal 1 B moves only
Header (this is what gets signed)77 bytes
PSREC001 8 B
genesis_hash 32 B
signer 1 B 0 white, 1 black, 2 server
seq 4 B
body_hash 32 B SHA-256(body)
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.

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.

Each move is answered by an ACK: a timestamped receipt signed by the other player.
genesishashe4at 1 000ACKat 2 251 · saw e5ACKat 74 239 · saw e4e5at 75 439WhiteBlackgenesishashe4at 1 000ACKat 2 251 · saw e5ACKat 74 239 · saw e4e5at 75 439WhiteBlack

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.