Pawnshake v0.1.0 · draft

Protocol

Links

A game can run over Bluetooth and the server at the same time. Every record races on every open link, the first copy counts, and a direct link is sealed to the game before it carries anything.

Any path will do

Records are signed, so any path can carry them: Bluetooth LE or a LAN in the same room, iroh across the internet, or the player's live session with the server. All of them carry the same wire records. A link that keeps sending invalid records is closed.

A game may run over any number of links at the same time (a Bluetooth link and the server, say), and links may come and go during the game. Nothing about links is signed: which link carried a record cannot be proven, so the clock function never asks.

  1. Every record goes out on every open link. The moment it is stored, in seq order. Links race: there is no switch-over and no timeout. A record is about 70 bytes, a whole game a few tens of kilobytes per link.
  2. The first copy counts. A device applies the first copy that arrives, on any link, and stamps its ACK with that moment. Later copies have the same record id and are dropped. Two records with one seq and different headers are a fork, whichever links they came over.
  3. Each link is an ordered session. It starts with the link handshake, then the flush: the records the peer lacks, then the first PING. After that, records go out as they are signed. A link that breaks starts a new session when it comes back.
  4. Pings and live checks are per link. Each link has its own pings and round trip, and a check only pairs a record and a ping from the same session (see live checks). On the server's session the server relays the players' pings like their records.
  5. LINK. With peer clocks, a device signs a LINK record when its fastest open link (by ping round trip) changes, the round trip moves by more than j_min_ms, and that holds for a second (see path changes).
One record, two links. The first copy counts; the later one has the same record id and is dropped.
White Server Blackseq 7 over Bluetoothseq 7 on the server sessionfirst copy: applied, ACKseq 7 again: same id, dropped White Server Blackseq 7 over Bluetoothseq 7 on the server sessionfirst copy: applied, ACKseq 7 again: same id, dropped

A device may close a link that keeps losing the race, to save power. It keeps the server session whenever it is online: that is the arbiter's path, and a link in its own right. As a link it can carry a move, but it never holds one back: a direct link races it.

Link handshake

A direct link (Bluetooth, LAN, iroh) is authenticated against the genesis before it carries anything else. Records are signed, so no link can change one. Pings are not: anyone on the path could fake them and fail an honest device's live checks. So each side first sends a hello, signed with the device key the genesis names for its colour:

LINK_HELLO161 bytes
PSLINK01 8 B
genesis_hash 32 B
role 1 B 0 white, 1 black
eph_pk 32 B X25519, fresh for this session
nonce 16 B random
have_white 4 B highest seq held
have_black 4 B for the flush
sig 64 B Ed25519, the role's device key, over everything before

The nonce is random, so two sessions never derive the same keys, even when a device reuses an ephemeral key by mistake. A hello whose genesis, role or signature is wrong closes the link. Both then derive one key per direction:

shared = X25519(own eph_sk, peer eph_pk)
okm    = HKDF-SHA256(ikm    = shared,
                     salt   = genesis_hash,
                     length = 64,
                     info   = "pawnshake link"
                              | SHA-256(hello_white)
                              | SHA-256(hello_black))
k_wb   = okm[0..32]     frames white → black
k_bw   = okm[32..64]    frames black → white
nonce  = 0x00000000 | counter u64 BE

Every later frame (records, PING, PONG, resend requests) is sealed with ChaCha20-Poly1305 under its direction's key. The nonce is that direction's frame counter, from 0 and never reused; a session ends before it would wrap. A frame that does not open closes the link.

A new session: hellos both ways, the records the peer lacks, then the first PING.
White BlackHELLO · have 12, 11HELLO · have 11, 11keys derived: every frame sealed from hereflush: White's seq 12first PINGlive: records as they are signed, pings White BlackHELLO · have 12, 11HELLO · have 11, 11keys derived: every frame sealed from hereflush: White's seq 12first PINGlive: records as they are signed, pings
  • The server's session is authenticated by TLS and the player's token and needs no handshake.
  • The genesis messages themselves (CHALLENGE, ACCEPT, START over a direct link) are signed on their own and need no session.