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:28555resolves 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/testingimage. 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
c57f860246925a3f9c37b9be461f506a2539f0e224f6e1aacd3eef540be62272InitialiseState(src/logic.cpp:159-203) builds the initial game world at exactly that block: it callsInitialiseBuildingsto place every entry of the config'sinitial_buildingslist 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).
- height
Genesis includes a test-account hack.
InitialiseState(src/logic.cpp:175-202) unconditionally pre-stocks three named accounts —snailbrain,johnv5, andjohnagent— in one of the Reubo starter-city buildings (building ID5), 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.sqliteinside the data volume (<datadir>/<gameid>/<chain>/storage.sqlite, built inlibxayagame/xayagame/defaultmain.cpp:50-96).Changing anything in
InitialiseStateor the genesis block requires a full state wipe + resync, in this order:docker stop tauriond && docker rm tauriond- recreate the container from the new image (
docker compose create) - wipe the state:
docker run --rm -v <data-volume>:/xayagame alpine rm -rf /xayagame/tn 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/gspto the GSP port, for example). - REST API (
:8700,--rest_port, implemented insrc/rest.cpp): a small, read-only HTTP interface, not an admin/RPC channel. It serves exactly three endpoints:/state— the same status object asgetnullstate./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.gzto 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.