# Pitfalls (consolidated) Hard-won lessons from running a fully-on-chain Xaya game — the libxayagame GSP, the XayaX bridge underneath it, and the day-2 operations around both. **Check this list before debugging anything that feels mysterious.** Format: one row each, symptom → fix. A row exists so you can *recognise* the failure fast, so the fix states the rule and then points at the file that owns the mechanism — SKILL.md (the platform foundation), GSP.md (building and configuring the daemon), OPS.md (deploy and the day-2 runbook), DETERMINISM.md (the law and how to prove it), FORK-TESTING.md (a forked Polygon chain with no real funds), EXAMPLES.md (real games to read). Pitfalls that belong to game channels — the off-chain signing prefix, disputes and timeouts, the `rules.wasm` judge, relay handshakes, the client-side real-time layer, fixed-point arithmetic and WCHI stake wagering — are deliberately not repeated here; load the building-channel-games skill for that list. ## On-chain & GSP | # | Pitfall | Fix | |---|---------|-----| | 1 | A `docker compose up` on a shared host shadows LAN routes and cuts off remote access | Docker picks a subnet for a new bridge network out of its own built-in address pools, and those pools reach into ranges real office and home LANs already use. ALWAYS pin an explicit subnet in the compose file — the `arcade-net` network in `arcade-platform/docker/docker-compose.yml` pins one and says why in its header — and check `docker network ls` / `docker network inspect` and `ip route` for collisions **before** the first `up`. OPS.md carries the shared-host rules. | | 2 | The GSP starts, logs no error, and never leaves its genesis height | Its ZMQ subscriber is dialling the wrong address. Whatever string XayaX is given as `--zmq_address` is the address it *advertises back* through `getzmqnotifications` (`Controller::RpcServer::getzmqnotifications` in `xayax/src/controller.cpp`), and the GSP dials that exact string from inside its own container (`Game::DetectZmqEndpoint` in `libxayagame/xayagame/game.cpp`) — so `tcp://localhost:...` points the GSP at itself. The published `xaya/xayax` image already gets this right: its `/usr/local/bin/entrypoint.sh` defaults the flag to `tcp://$(hostname -i):28555`, so the safest fix is not to pass it at all; if you do pass it, use the Docker service name. | | 3 | The GSP dies at startup on JSON-RPC errors against XayaX | libxayagame defaults `--xaya_rpc_protocol` to protocol 1 (the `xaya_rpc_protocol` flag in `libxayagame/mover/main.cpp`); XayaX's RPC server speaks 2.0 only. Pass `--xaya_rpc_protocol=2` (the `gsp` service command in `arcade-platform/docker/docker-compose.yml`). | | 4 | `waitforchange("")` "hangs" instead of returning the current block | An empty or unparseable hash means *always block*: the call waits for the next state change or for the `--xaya_waitforchange_timeout_ms` timeout, default 5,000 ms (the flag's definition in `libxayagame/xayagame/game.cpp`). Only a valid-but-stale block hash returns immediately (`Game::WaitForChange`, same file). For a non-blocking check, poll `getnullstate` against your own deadline instead. | | 5 | A crafted move JSON kills the GSP | Anyone can send any JSON to your game id, so an invalid move must be **ignored** — log and continue, never abort. The reference pattern is `MoverLogic::ProcessForwardInternal` in `libxayagame/mover/logic.cpp`: `LOG (WARNING) << "Ignoring invalid move"` then `continue`. A node that aborts on a parse failure is a network-wide halt that any player can trigger for free. | | 6 | A player's first move appears to take effect "one block early" | Moves are applied to the state and the per-block update then runs, both inside the same forward-processing call (`MoverLogic::ProcessForwardInternal` in `libxayagame/mover/logic.cpp`), so a new player's first move already advances one step in the block that carried it. Don't design around a block of delay that isn't there. | | 7 | C++ edits are silently not in the running binary | Docker's layer cache can reuse a stale compile step. Run `docker compose build --no-cache` after any source change you cannot see taking effect — this applies to **every** GSP variant you build from the same tree, a wagering GSP included — and then confirm from the container's own startup log, never from the host source. GSP.md has the build recipe. | | 8 | `getpendingstate` returns a "pending moves are not tracked" error | The GSP enables pending tracking only when the bridge advertises a `pubgamepending` ZMQ endpoint (`Game::DetectZmqEndpoint` in `libxayagame/xayagame/game.cpp`; the error itself in `Game::GetPendingJsonState`, same file), and XayaX advertises one only when it was started with `--watch_for_pending_moves=` over a WebSocket endpoint (the `watch_for_pending_moves` flag and its `EnablePending` call in `xayax/eth/main.cpp`). The GSP's own `--pending_moves=true` alone is harmless but inert. A common claim is that XayaX has no pending feed at all; the code says it has an opt-in one that is explicitly best-effort and drops moves whenever the base-chain RPC hiccups (`EthChain::NewPendingTx` in `xayax/eth/ethchain.cpp`) — either way, never make gameplay depend on a mempool view. | | 9 | The GSP aborts at startup with "Xaya Core is too old" | XayaX reports a synthetic Xaya Core version that sits below libxayagame's built-in minimum, and the check is a hard `CHECK_GE` (`VerifyXayaVersion` in `libxayagame/xayagame/defaultmain.cpp`). Lower the minimum-version field of your daemon configuration in `main.cpp`, or patch it pre-build. GSP.md's daemon-configuration section names the field and both version numbers — copy them from there. | | 10 | Moves land on chain but the GSP never sees them | The game id goes in the JSON body, not in the contract call. XayaX routes a player move by iterating the keys of the move's `"g"` object and publishing one message per game id (the `PerTxData` constructor in `xayax/src/zmqpub.cpp`), so every move must be wrapped `{"g":{"":{...}}}`. An unwrapped payload is a perfectly valid transaction that no GSP will ever route. | | 11 | `XayaAccounts.move()` / `register()` sends to the wrong place | The first argument is the **namespace**, always `'p'` for a player name — not the game id (`PLAYER_NAMESPACE` and the `move` / `register` entries of `xayaAccountsAbi` in `arcade-platform/sdk/src/lib/chain/xaya-accounts.ts`). XayaX drops anything whose namespace is not `p` before it even looks for moves (the `PerTxData` constructor in `xayax/src/zmqpub.cpp`), and `g`-namespace transactions are treated as admin commands instead (same constructor). Registration is permanent: a name, once taken, is never released. | | 12 | The GSP falls behind silently after a XayaX restart, still reporting `state=up-to-date` at a stale height | Its ZMQ subscriber went stale and nothing noticed. Pass **BOTH** `--xaya_zmq_staleness_ms` and `--xaya_connection_check_ms`: the watchdog thread that pings and reconnects only spawns when the check interval is non-zero, and libxayagame's default for it is `0` while the staleness default is `120000` (the two flags' definitions in `libxayagame/xayagame/game.cpp`), so a staleness threshold on its own is completely inert. Polygon produces a block every couple of seconds, so tens of seconds of silence already means a dead connection — the deployed pair is `--xaya_connection_check_ms=10000` with `--xaya_zmq_staleness_ms=30000` (the `gsp` service command in `arcade-platform/docker/docker-compose.yml`). OPS.md carries the wedge runbook. | | 13 | `storage.sqlite-wal` grows without bound beside a tiny main DB, and only a restart shrinks it | libxayagame **disables SQLite's own autocheckpoint** on purpose — it raced `sqlite3_snapshot_open()` into a fatal `SQLITE_BUSY` (the `sqlite3_wal_autocheckpoint (db, 0)` call in `libxayagame/xayagame/sqlitestorage.cpp`) — and by default nothing replaces it, because `--xaya_sqlite_wal_truncate_ms` defaults to `0` (the flag's definition in the same file). Set it non-zero to arm the framework's own snapshot-guarded periodic `wal_checkpoint(TRUNCATE)`, which skips gracefully on BUSY and is never fatal (the `sqlite3_wal_checkpoint_v2` call with `SQLITE_CHECKPOINT_TRUNCATE`, same file); success logs `Checkpointed and truncated WAL file successfully`. This is NOT a stray-reader problem — don't go chasing `PRAGMA journal_size_limit` first. | | 14 | Hand-run GSP RPC calls appear to hang while the frontend is up | `waitforchange` is a long-poll and a real-time UI holds one open permanently (row 4). A widespread claim explains this as "the RPC server is single-threaded"; the code does not say that: the daemon hands libjson-rpc-cpp nothing but a port (the `jsonrpc::HttpServer` construction in `libxayagame/xayagame/defaultmain.cpp`), so the concurrency you actually get is a property of your libjson-rpc-cpp build, not a libxayagame guarantee. Don't reason about it — stop the frontend before debugging over RPC by hand. | | 15 | A GSP is re-syncing from the game's genesis height and you don't know why | The GSP's entire state is the SQLite file under `--datadir` (the `sqlite` storage branch in `libxayagame/xayagame/defaultmain.cpp` builds `/storage.sqlite`); there is no migration path and no partial restore, so losing it means a full replay from genesis. Keep the datadir on a **named** volume — the `gsp` service in `arcade-platform/docker/docker-compose.yml` maps one against `--datadir=/xayagame` — which survives `docker compose up --force-recreate`; an anonymous volume or an in-image path does not, and `docker compose down -v` removes even a named one. **To reset deliberately, stop the container FIRST, then delete the DB file, then start it again** — deleting the file under a running daemon does nothing, because the live process still holds the open handle and the file simply reappears. OPS.md covers resync and genesis height. | ## Fixed-point & determinism Only one row of this category belongs to the platform foundation; the fixed-point arithmetic rows (overflow, angle clamping, format choice, float creeping across a JS boundary) are about code shipped to a browser and live in the building-channel-games skill's pitfall list. | # | Pitfall | Fix | |---|---------|-----| | 16 | Non-deterministic state — wall clock, external I/O, unseeded RNG, map iteration order, uninitialised memory | None of these may touch consensus-relevant state: every node must compute byte-identical state from the same ordered move sequence, or the network forks. DETERMINISM.md states the law and, more usefully, how to *prove* it — golden-replay and reorg suites baked into the image build as a deploy gate, and two independently synced GSPs returning byte-identical `gamestate` as the acceptance test. | Verified against `libxayagame/xayagame/{game.cpp,defaultmain.cpp,sqlitestorage.cpp}`, `libxayagame/mover/{main.cpp,logic.cpp}`, `xayax/{src/zmqpub.cpp,src/controller.cpp,eth/main.cpp,eth/ethchain.cpp}`, the published `xaya/xayax` image's `/usr/local/bin/entrypoint.sh`, `arcade-platform/docker/docker-compose.yml` and `arcade-platform/sdk/src/lib/chain/xaya-accounts.ts`.