Playground — disposable chain, test WCHI, wiped without warning

Pitfalls

The on-chain and GSP failures that bite hardest, with the symptom first.

Raw markdown →

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

#PitfallFix
1A docker compose up on a shared host shadows LAN routes and cuts off remote accessDocker 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.
2The GSP starts, logs no error, and never leaves its genesis heightIts 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.
3The GSP dies at startup on JSON-RPC errors against XayaXlibxayagame 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).
4waitforchange("") "hangs" instead of returning the current blockAn 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.
5A crafted move JSON kills the GSPAnyone 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.
6A 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.
7C++ edits are silently not in the running binaryDocker'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.
8getpendingstate returns a "pending moves are not tracked" errorThe 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.
9The 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.
10Moves land on chain but the GSP never sees themThe 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.
11XayaAccounts.move() / register() sends to the wrong placeThe 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.
12The GSP falls behind silently after a XayaX restart, still reporting state=up-to-date at a stale heightIts 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.
13storage.sqlite-wal grows without bound beside a tiny main DB, and only a restart shrinks itlibxayagame 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.
14Hand-run GSP RPC calls appear to hang while the frontend is upwaitforchange 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.
15A GSP is re-syncing from the game's genesis height and you don't know whyThe 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.

#PitfallFix
16Non-deterministic state — wall clock, external I/O, unseeded RNG, map iteration order, uninitialised memoryNone 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.