Skip to content
TAURION
14Building & Running a GSP

Building & Running a GSP

Compile tauriond and sync your own node against Polygon.

In plain terms (for players): Taurion is a fully on-chain game. There is no central game server that anyone "owns" — instead, every player's moves are written to the Polygon blockchain, and a piece of software called the GSP (Game State Processor, the program tauriond) reads those moves and works out the resulting game world by applying the rules documented in this folder. Anyone can run their own GSP; they will all compute the same world from the same chain, which is what makes the game trustless. The client you play in (the official web client) just asks a GSP "what does the world look like right now?" and draws the answer. This page explains how to build and run that GSP yourself; if you only want to play, you do not need any of this.

How to build the Taurion Game State Processor (tauriond) and run it against the live Polygon chain. The GSP is the authoritative game engine: it reads Xaya moves (player commands encoded as on-chain transactions) from the blockchain, applies the rules documented in this folder, and serves the resulting game state over JSON-RPC (a simple "send JSON request, get JSON back" web protocol) to any client.

Architecture#

Polygon chain (XayaAccounts contract 0x8C12253F71091b9582908C8a44F78870Ec6F304F)
        │  eth RPC (your own node or e.g. https://polygon-node.xaya.io)
        ▼
   XayaX bridge (xaya/xayax, "eth" mode)
        │  Xaya-Core-compatible RPC (:8000) + ZMQ block notifications (:28555)
        ▼
   tauriond (this repo)
        │  game state in SQLite (/xayagame/tn/polygon/storage.sqlite)
        ▼
   JSON-RPC :8600 (state queries) + REST :8700 (state/healthz/bootstrap)  →  game clients

(ZMQ = ZeroMQ, a lightweight messaging library; XayaX uses it to push a notification to the GSP the moment a new block arrives, instead of the GSP having to poll. SQLite = the single-file embedded database the GSP stores the whole world in. The game ID inside the data directory is tn, set in main.cpp (SQLiteMain (config, "tn", rules), src/main.cpp:175), and polygon is the chain name — see ChainToString, libxayagame/xayagame/gamelogic.cpp:30.)

tauriond never talks to Polygon directly — the XayaX bridge translates Polygon blocks and move events of the XayaAccounts ERC-721 contract into the interface a Xaya GSP expects (--xaya_rpc_protocol=2).

1. Run a XayaX bridge#

docker run -d --name xayax --network <your-net> \
  xaya/xayax \
  eth \
  --eth_rpc_url=https://polygon-node.xaya.io \
  --accounts_contract=0x8C12253F71091b9582908C8a44F78870Ec6F304F \
  --port=8000 \
  --listen_locally=false \
  --zmq_address=tcp://xayax:28555 \
  --logtostderr

Notes:

  • Use your own Polygon node if you can; public endpoints rate-limit.
  • ZMQ binding gotcha: --zmq_address=tcp://xayax:28555 resolves the hostname once at container start and binds only that interface. The GSP container must be attached to the same Docker network that hostname resolves on, otherwise RPC works but block notifications never arrive and the GSP sits at "disconnected" forever.
  • XayaX prunes old block data; during a long catch-up its RPC responses can take >10 s, which the GSP treats as a timeout (see "Sync" below).

2. Build the GSP images#

Two-stage Docker build from the repo root. N is the make -j parallelism — size it to your machine (high values produce heavy IO/CPU load):

docker build -t tn-base --build-arg N=4 -f docker/base/Dockerfile .
docker build -t taurion-gsp -f docker/image/Dockerfile .

The base image (docker/base/Dockerfile) starts from a pinned xaya/libxayagame digest (all Xaya deps preinstalled), also builds and installs Google's benchmark library from source, then builds Taurion with autotools (./autogen.sh && ./configure && make -j${N} && make install-strip). The final image (docker/image/Dockerfile) is a minimal debian:13-slim runtime containing only the tauriond binary plus the shared libraries collected by cpld. A native (non-Docker) build needs the whole libxayagame stack and is not the supported path.

Note: the base Dockerfile does not run the unit tests — testing uses a separate docker/testing image. Build that if you want to run the suite.

The runtime image's entrypoint (docker/image/entrypoint.sh) hard-codes several flags before appending the ones from command:, so you normally do not pass them yourself:

exec /usr/local/bin/tauriond \
  --datadir="${XAYAGAME_DIR}" \     # /xayagame (set in the image)
  --enable_pruning=1000 \           # keep only 1000 blocks of undo data
  --game_rpc_port=8600 \            # JSON-RPC state port
  --rest_port=8700 \                # REST port (state / healthz / bootstrap)
  "$@"                              # ← compose `command:` flags land here

Pruning = throwing away the undo data older than N blocks; it keeps the database small but means the GSP can no longer roll back past that depth if the chain reorganises very deeply. All these flags are defined in src/main.cpp:44-66.

3. Run tauriond#

docker/docker-compose.yml in this repo is a working example:

services:
  tauriond:
    image: taurion-gsp
    container_name: tauriond
    restart: unless-stopped
    networks: [taurion-net]
    ports:
      - "127.0.0.1:8601:8600"   # game JSON-RPC
      - "127.0.0.1:8701:8700"   # REST (state / healthz / bootstrap)
    volumes:
      - tauriond-data:/xayagame
      - tauriond-logs:/log
    environment:
      - GLOG_alsologtostderr=1   # also write glog output to stderr
    command:
      - "--xaya_rpc_url=http://xayax:8000"
      - "--xaya_rpc_protocol=2"
      - "--game_rpc_listen_locally=false"
      - "--xaya_zmq_staleness_ms=30000"

Remember to attach the container to the network where the XayaX ZMQ socket is bound (see gotcha above): docker network connect <xayax-net> tauriond.

4. Genesis and state#

  • The Polygon genesis block (the first chain block the game pays attention to — earlier blocks are ignored — given as a height plus a block hash) is hard-coded in src/logic.cpp (PXLogic::GetInitialStateBlock, POLYGON case, src/logic.cpp:141-145):

    • height 88343264
    • hash c57f860246925a3f9c37b9be461f506a2539f0e224f6e1aacd3eef540be62272 InitialiseState (src/logic.cpp:159-203) builds the initial game world at exactly that block: it calls InitialiseBuildings to place every entry of the config's initial_buildings list as an ANCIENT-faction building (src/buildings.cpp:104-121) — these are the neutral ancient buildings and the three faction starter zones — and initialises the money supply (MoneySupply::InitialiseDatabase).
  • Genesis includes a test-account hack. InitialiseState (src/logic.cpp:175-202) unconditionally pre-stocks three named accounts — snailbrain, johnv5, and johnagent — in one of the Reubo starter-city buildings (building ID 5), giving each 100,000,000 units of every refinable ore (e.g. Trimideum (raw a)), one blueprint-original of every craftable item (<code> bpo), two of every fitment and every vehicle (e.g. Raider (rv st)), and 100 of every reverse-engineering artefact. The source comment flags this as a testing FIXME ("should not be released in a production version"), so treat its presence on the live chain as a known quirk rather than intended balance.

  • Game state lives in /xayagame/tn/polygon/storage.sqlite inside the data volume (<datadir>/<gameid>/<chain>/storage.sqlite, built in libxayagame/xayagame/defaultmain.cpp:50-96).

  • Changing anything in InitialiseState or the genesis block requires a full state wipe + resync, in this order:

    1. docker stop tauriond && docker rm tauriond
    2. recreate the container from the new image (docker compose create)
    3. wipe the state: docker run --rm -v <data-volume>:/xayagame alpine rm -rf /xayagame/tn
    4. docker start tauriond

    (Starting an old container after the wipe re-initialises genesis with the old binary — recreate first.)

5. Sync behaviour and monitoring#

Check sync state — either via the game JSON-RPC port (getnullstate, the cheapest probe, returns no game data, just status):

curl -s -X POST -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"getnullstate","params":[]}' \
  http://127.0.0.1:8601

…or via the lighter REST port, which needs no JSON body:

curl -s http://127.0.0.1:8701/state     # JSON: gameid, chain, state, height, ...
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8701/healthz  # 200 = healthy, 500 = not

The state field can be any of seven values (Game::StateToString, libxayagame/xayagame/game.cpp:131-148): unknown, pregenesis (chain hasn't reached the genesis height yet), out-of-sync, catching-up (actively syncing), at-target, up-to-date (fully synced — this is what you want), and disconnected (lost the XayaX ZMQ feed; see the gotcha in step 1). Both /state and getnullstate also report the current processed height.

Expect roughly a few thousand Polygon blocks per minute during catch-up (Polygon produces ~1 block per 2 s, so stay reasonably close to the genesis height or budget time accordingly).

Known catch-up failure mode: bulk requests against a pruning XayaX can exceed the GSP's RPC timeout, after which the ZMQ subscription dies and the height stops advancing even though the process is healthy. --xaya_zmq_staleness_ms (libxayagame flag, default 120000 = 2 min; the compose file lowers it to 30000 so a dead feed is detected sooner) mitigates but does not fully cure this. A docker restart tauriond resumes from the last processed height; a simple watchdog that restarts the container when the height stalls for ~2 minutes gets through any catch-up. Not an issue once at the chain tip.

6. Serving clients#

  • Game JSON-RPC (:8600): all state queries (see RPC & REST API). Browser clients need a reverse proxy for TLS/CORS (the official web client's dev server proxies /gsp to the GSP port, for example).
  • REST API (:8700, --rest_port, implemented in src/rest.cpp): a small, read-only HTTP interface, not an admin/RPC channel. It serves exactly three endpoints:
    • /state — the same status object as getnullstate.
    • /healthz — HTTP 200 if up-to-date, 500 otherwise (handy for load balancers and container health checks).
    • /bootstrap.json.gz — a gzipped snapshot of the full game state, cached and refreshed every --rest_bootstrap_refresh_seconds (default 3600). New clients fetch this once to load the world quickly instead of issuing many RPC calls. None of these mutate state or shut anything down, so the REST port is safe to put behind a public reverse proxy if you want to serve /bootstrap.json.gz to clients. (There is no "god-mode" admin port on mainnet; admin/dev commands only exist in local testing setups — see RPC & REST API.)
  • Moves are not sent through the GSP: the GSP only ever reads. Clients submit moves as transactions to the XayaAccounts contract (0x8C12253F71091b9582908C8a44F78870Ec6F304F) on Polygon (e.g. via MetaMask), and the GSP picks them up from the chain like any other observer.