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.
Several links at once
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.
- Every record goes out on every open link. The moment it is stored, in
seqorder. 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. - 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
seqand different headers are a fork, whichever links they came over. - 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.
- 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.
- 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).
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:
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.
- 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.