Pawnshake v0.1.0 · draft

Protocol

Clocks

Signatures prove what a player said, never when. Pawnshake gets time from the players' own signed timestamps, cross-checked live, or from the server's anchors.

Time on every record

Every record carries at: the signer's own monotonic clock in milliseconds, counted from the moment it handled START. Every received record (except an ACK) is answered at once with an ACK whose at is when it was applied. That gives every turn a start and an end on the mover's own clock:

think(move) = at(move) − at(mover's ACK of the opponent's previous move)
White
thinkstransitwaitingtransitthinks
Black
waitingtransitthinkstransitwaiting
thinking (charged)transit (free up to J)waitingpaused

A clock stops when its player signs a move and starts when that player acknowledges the opponent's move. Transit in between is charged to nobody, up to an allowance.

Both clock modes use think. Peer clocks (offline, hybrid) charge it directly. Anchored clocks (online) feed it to the lag quota as the player's claim.

Peer clocks: one function, no negotiation

Both devices, and the server when the game is reported, compute the clocks with the same function over the chain. Nothing is exchanged about time beyond the records, so the two screens always show the same clocks.

Each ACK k of a record r is one transit sample, measured across two different clocks:

u(r)   = at(k) − at(r)                 true transit + clock offset
m_wb   = smallest u, white to black    (last 16 samples)
m_bw   = smallest u, black to white
rtt    = m_wb + m_bw                   offsets cancel out
theta  = (m_wb − m_bw) / 2             black clock minus white clock
J      = max(2 × rtt, 30 ms)           allowance per message

The offset between the two clocks is unknown and does not matter: it cancels in the round trip. A window of recent samples follows crystal drift.

Excess and the pause budget

A message that took longer than the fastest one in its direction plus J has excess: an outage, or a lie. Excess becomes paused time, taken from one budget per game shared by both players (WhitePawn: 2 s bullet, 5 s blitz, 10 s rapid, 20 s classical).

honest
think≤ J
hiccup
thinkJpaused
outage
thinkJbudget gonecanceled
thinking (charged)transit (free up to J)waitingpaused

When an outage is longer than the budget left, the game is canceled unless both players sign RESUME, which turns the whole outage into paused time.

When the path changes: LINK

The floors m_wb and m_bw are the fastest transit in the window, so they belong to the fastest path the records took. When that path goes away (two phones lose their Bluetooth link and go on over the server), every message on the slower path shows the difference as excess until the fast samples have left the window. A 20 ms path replaced by a 200 ms one, with J at 80 ms, costs about 100 ms per message for 16 messages in each direction: more than a bullet game's whole pause budget.

So either player signs a LINK record (op 234) when its own links show the change (see Links). It carries nothing: which path a record took cannot be proven, and the clock function never asks. At LINK's point in the order:

  1. The windows are emptied. No sample from before LINK counts for the floors or rtt again.
  2. A settle opens. J and theta keep their values from before LINK (the clock offset does not change with the path). Every new sample is pending: no excess is charged yet.
  3. The settle closes. That happens once both directions have link_settle samples since LINK (WhitePawn: 4). The floors, rtt, theta and J come from those samples, and the excess of every pending sample, against the new floors, is taken from the pause budget. A game that ends with a settle open settles at its end.
A path change: the old fast samples are dropped, and the new floors come from a few samples per direction.
White BlackBluetooth: 20 ms each wayBluetooth dropsLINK, over the serverACK: first new samplesettle: 4 samples each way, pendingnew floors, pending excess charged White BlackBluetooth: 20 ms each wayBluetooth dropsLINK, over the serverACK: first new samplesettle: 4 samples each way, pendingnew floors, pending excess charged

A LINK while a settle is open is void, so both players noticing the change at once costs nothing, and a stream of LINKs cannot keep a settle open. LINK's own ACK is the first sample of the new window.

The clocks

remaining(X) = initial + increment × moves(X) − Σ think(X's moves)

The first move of each side is free, as on every chess server. A move whose think takes its signer below zero counts as FLAGGED. A FLAG_CLAIM is valid when, converted to the flagged player's clock with theta, its time lies more than remaining + J after that player's turn began. An honest client signs FLAGGED itself when its own clock runs out.

Peer clocks: live checks

The chain alone cannot tell a slow link from a lie. The devices can, while the game runs: each knows its own clock is true. They ping each other every 250 ms on every link that carries the records, inside that link's session (unsigned, never stored), and measure round trip, offset and clock rate NTP-style.

With more than one link, each link has its own pings and round trip, and a check only pairs a record and a ping that went over the same link session: order holds within one link, never across two. Copies that arrive in a session's flush (before its first PING) or as the answer to a resend request are late by design: the early-send check skips them, and their excess still counts. See Links.

CheckFails whenCatches
late ACKan ACK claims a receipt later than a pong that proves the record had already arriveda receiver stealing thinking time
early senda record claims an at earlier than a ping the sender sent after that momenta sender stopping its clock early while still pinging
futurea record claims an at later than its arrival allowsa flag claim dated in the future
clock ratethe opponent's clock runs more than 0.1 % fast or slowa slowed clock that hides think time
asymmetrythe chain's round trip exceeds the ping round trip rtt_ping by more than max(2 × rtt_ping, 30 ms)a steady lie that the chain alone reads as a slow link

For the asymmetry check a device notes, with every sample of the window, the best ping round trip it had at that moment (on its fastest open link). rtt_ping is the mean of the two directions' smallest noted values. The allowance comes from the pings, not from J: J grows with the chain's round trip, so a check against J could never fail. And using the pings of the same samples means a faster link coming up does not look like a lie.

The late-ACK check: a link session is ordered, so a pong echoing a later ping proves the move had already arrived.
Honest LiarMOVE · at 10 000PING t = 10 040PONG: got your pingACK: 'received only now'the pong proves it arrived earlier → DISPUTE Honest LiarMOVE · at 10 000PING t = 10 040PONG: got your pingACK: 'received only now'the pong proves it arrived earlier → DISPUTE

A failed check makes the device sign DISPUTE (with the reason and the offending record) and stop. The game ends disputed: no result, unrated. A device that simply stops pinging is an outage, nothing more.

Peer clocks: heartbeats

A player whose clock runs out on its own turn is flagged by the opponent's FLAG_CLAIM. Without more, it could stay silent, let the claim be signed, and report a move later, dated before its deadline: a concurrent move goes first in the order, so the claim would be void. Pings cannot stop that: they are neither signed nor kept.

So while a game with peer clocks runs, each device sends a signed heartbeat at least once a second, as the last field of a PING, on every open link: "at my clock at, my newest record was head_seq". The opponent keeps the latest one and puts it in its anchors and its report (layout on Byte layouts).

A record contradicts its signer's heartbeat when it has a higher seq than head_seq and an at before the heartbeat's, or a seq up to head_seq and an at after it. That counts as a fork: the signer loses. An honest client signs heartbeats and records under one lock from one clock, and never contradicts itself.

Silent past the flag: a heartbeat from after the deadline contradicts any move dated in time.
Flagged Opponentclock runs out at 60 000BEAT · head 41 · at 60 400FLAG_CLAIMlater: seq 42 MOVE · at 59 900seq 42 dated before a beat that says 41 → fork Flagged Opponentclock runs out at 60 000BEAT · head 41 · at 60 400FLAG_CLAIMlater: seq 42 MOVE · at 59 900seq 42 dated before a beat that says 41 → fork

Once the opponent holds a heartbeat from after the deadline, the flag stands. What is left is cutting every link while the own clock still runs, then reporting a move dated after the last heartbeat that got through. That move was made during an outage: the game is canceled, like any hop-off, never won.

What a peer-clock cheater can still gain

TrickMost it gains without a DISPUTE
date a move earlier than it was sentJ per move (tens of ms on a local link)
acknowledge later than receivedJ per move
run the clock slow0.1 % of its thinking time
fake an outagethe pause budget, once per game
sign LINK at willnothing more: one settle at a time, measured against its smallest samples
stay silent past its flag, then report a move dated in timenothing: its own heartbeat from after the deadline contradicts the move, a fork
anything biggernothing: DISPUTE or canceled, never a win

Anchored clocks (online)

With a full arbiter, both clients anchor every record they sign, right after sending it, and every record they receive. The server times each move from existence times:

interval = E(move) − E(opponent's previous move)
charge   = interval − refund(interval − think)

The refund comes from a lag quota, a port of lichess' LagTracker. A client that lies about its think time gains at most the quota; an honest client on a bad connection is not flagged by the network.

est   = initial + 40 × increment              seconds
gain  = min(1000 ms, est × 4 ms + 150 ms)     earned per move
quota = 3 × gain at the start, at most 7 × gain
refund = min(max(lag, 0), quota)
quota  = min(quota + gain − refund, 7 × gain)

Flags and abandonment are the server's decisions, signed as verdicts.