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=<contracts> 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":{"<gameid>":{...}}}. 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 <gamedir>/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.