Skip to content

Tutorial Mover Part 1 Setup

Andy Colosimo edited this page Jun 12, 2026 · 1 revision

Tutorial: Mover on Polygon — Part 1: Setup

In this first part of the Mover tutorial you'll build and run a complete Game State Processor (GSP) for Mover, the official Xaya example game, against Polygon mainnet. You'll clone the game's source from libxayagame, apply a small patch that teaches it about Polygon, build it into a Docker image, and launch it together with the XayaX bridge using Docker Compose. At the end you'll have a fully synced GSP answering JSON-RPC queries on your machine — ready for Part 2, where you'll register a name and play.

What you'll build

Mover is deliberately tiny: players live on an infinite 2D plane and send moves like {"d":"k","n":2} ("go up 2 steps"). Its game ID is mv. Despite its simplicity, running it exercises the entire Xaya stack:

flowchart LR
    subgraph polygon["Polygon PoS mainnet"]
        acc["XayaAccounts contract<br/>0x8C12...304F"]
    end
    subgraph dockernet["Docker network: mover-net"]
        xayax["xayax container<br/>(image: xaya/xayax)"]
        moverd["moverd container<br/>(built in this tutorial)"]
    end
    you["Your machine<br/>(curl, scripts, anything JSON-RPC)"]
    acc -- "move events via<br/>https://polygon-node.xaya.io" --> xayax
    xayax -- "JSON-RPC :8000 + ZMQ :28555" --> moverd
    xayax -. "127.0.0.1:8101" .-> you
    moverd -- "127.0.0.1:8602" --> you
Loading

Data flows one way: chain → XayaX → GSP → your tools. The GSP never writes to the chain — that's what your wallet is for, and we'll get to it in Part 2.

If you haven't read What Makes a GSP and Xaya Architecture yet, they explain the concepts behind everything we do here. This page is hands-on.

Prerequisites

  • Docker Engine with the Docker Compose plugin (docker compose, not the old docker-compose).
  • An internet connection. You do not need your own Polygon node — by default we use the public RPC endpoint run by the Xaya team, https://polygon-node.xaya.io.
  • About 10 minutes: a few minutes for the image build and roughly two minutes for the initial sync.

No wallet, tokens, or blockchain accounts are needed for this part. Running a GSP is pure read-only observation of the chain.

Step 1: Get the code

The Mover sources live in the libxayagame repository. We'll clone it once and copy just the files we need into a fresh project directory, so our patched copy stays cleanly separated from the upstream sources.

cd ~
git clone https://github.com/xaya/libxayagame
mkdir -p ~/mover-polygon/mover/proto ~/mover-polygon/docker

Copy the Mover game files:

cp ~/libxayagame/mover/logic.cpp \
   ~/libxayagame/mover/logic.hpp \
   ~/libxayagame/mover/moves.cpp \
   ~/libxayagame/mover/moves.hpp \
   ~/libxayagame/mover/pending.cpp \
   ~/libxayagame/mover/pending.hpp \
   ~/libxayagame/mover/main.cpp \
   ~/mover-polygon/mover/

cp ~/libxayagame/mover/proto/mover.proto ~/mover-polygon/mover/proto/

Your project should now look like this (the two docker/ files are created in steps 4 and 5):

~/mover-polygon/
├── docker/
│   ├── Dockerfile            ← created in step 4
│   └── docker-compose.yml    ← created in step 5
└── mover/
    ├── logic.cpp             ← we'll patch this one
    ├── logic.hpp
    ├── main.cpp
    ├── moves.cpp
    ├── moves.hpp
    ├── pending.cpp
    ├── pending.hpp
    └── proto/
        └── mover.proto

What each file does (covered in depth in Part 3):

File Role
logic.cpp/hpp The GameLogic subclass: initial state, forward/backward processing, JSON output
moves.cpp/hpp Parsing and validating the {"d":...,"n":...} move format
pending.cpp/hpp Pending-move (mempool) tracking — unused on Polygon, but needed to link
main.cpp Wires everything into DefaultMain and produces the moverd daemon
proto/mover.proto Protobuf definitions for the game state and undo data

Step 2: Why a patch is needed

Two reasons you can't just run the stock binaries:

  1. The stock Mover doesn't know about Polygon. GetInitialStateInternal in logic.cpp has cases for the original Xaya Core chains (MAIN, TEST, REGTEST) and then hits LOG (FATAL) << "Unexpected chain". Connect an unpatched moverd to a Polygon XayaX and it dies on startup.
  2. The prebuilt moverd in the xaya/libxayagame Docker image is unusable. It was linked against libmover.so, which isn't installed in the image, so it can't even start. You must build your own.

The fix is a single new case. Open ~/mover-polygon/mover/logic.cpp, find the switch (chain) block in GetInitialStateInternal, and add this after the Chain::REGTEST case (and before default:):

    case Chain::POLYGON:
      /* The game state starts at a recent Polygon block.  Moves sent
         before this height are ignored.  The hash is left empty so that
         any block at this height is accepted (we trust the connected
         XayaX instance rather than pinning an exact block hash).  */
      height = 88365000;
      hashHex = "";
      break;

After the edit, the switch should read:

    case Chain::REGTEST:
      height = 0;
      hashHex
          = "6f750b36d22f1dc3d0a6e483af45301022646dfc3b3ba2187865f5a7d6d83ab1";
      break;

    case Chain::POLYGON:
      /* The game state starts at a recent Polygon block.  Moves sent
         before this height are ignored.  The hash is left empty so that
         any block at this height is accepted (we trust the connected
         XayaX instance rather than pinning an exact block hash).  */
      height = 88365000;
      hashHex = "";
      break;

    default:
      LOG (FATAL) << "Unexpected chain: " << ChainToString (chain);

Chain::POLYGON is already defined in the installed xayagame/gamelogic.hpp, so no other changes are needed.

Two design decisions in this hunk deserve explanation:

  • The start height defines your game's genesis. Moves sent before this height are invisible to the game — the GSP simply starts processing at this block. Pick a recent Polygon block from polygonscan.com so your GSP doesn't waste time scanning years of history. We used 88365000, which was a few thousand blocks behind the tip when this tutorial was verified. You can keep that value to reproduce the outputs below exactly, or pick a newer block (your heights and genesis hash will then differ).
  • The empty-hash trick. The original chains pin an exact block hash for the genesis block, so a GSP refuses to sync against a chain that doesn't match. Leaving hashHex empty tells libxayagame to accept whatever block the connected XayaX reports at that height — it fetches the hash itself and logs it. This is convenient for a tutorial (any honest XayaX works); a production game would typically pin the hash once its genesis block is final.

Step 3: The Dockerfile

Create ~/mover-polygon/docker/Dockerfile with exactly this content:

# Build the Mover game daemon (moverd) for Polygon.
#
# Uses xaya/libxayagame as the build base, which has all Xaya libraries
# (libxayagame, libxayautil, ...) and their dependencies pre-installed.
#
# XayaX (the Polygon bridge) reports a lower version number than a native
# Xaya Core node, so we patch the MinXayaVersion check in defaultmain.hpp
# before building.

FROM xaya/libxayagame AS build

RUN apt update && apt -y install \
  build-essential \
  pkg-config

# Patch the version check to accept XayaX's reported version.
RUN sed -i 's/unsigned MinXayaVersion = 1010200/unsigned MinXayaVersion = 1000000/' \
    /usr/local/include/xayagame/defaultmain.hpp

WORKDIR /usr/src/mover-polygon
COPY mover/ mover/

# Generate the protobuf C++ files for the game state / undo data.
RUN protoc -Imover --cpp_out=mover mover/proto/mover.proto

# main.cpp includes config.h (normally generated by the build system).
RUN echo '#define PACKAGE_VERSION "polygon-tutorial"' > config.h

# Compile moverd against the installed libraries.
RUN set -e && \
    CXXFLAGS="-std=c++14 -I. -Imover \
      $(pkg-config --cflags jsoncpp libglog protobuf sqlite3)" && \
    g++ $CXXFLAGS -c mover/proto/mover.pb.cc -o mover.pb.o && \
    g++ $CXXFLAGS -c mover/moves.cpp -o moves.o && \
    g++ $CXXFLAGS -c mover/logic.cpp -o logic.o && \
    g++ $CXXFLAGS -c mover/pending.cpp -o pending.o && \
    g++ $CXXFLAGS -c mover/main.cpp -o main.o && \
    g++ -o moverd main.o logic.o moves.o pending.o mover.pb.o \
      -L/usr/local/lib -lxayagame -lxayautil \
      $(pkg-config --libs jsoncpp libglog gflags protobuf sqlite3 \
        libcurl libzmq libmicrohttpd openssl zlib lmdb \
        libjsonrpccpp-client libjsonrpccpp-server libjsonrpccpp-common)

# Collect the binary and its shared library dependencies into /jail
# (cpld is a helper script provided by the xaya/libxayagame image).
WORKDIR /jail
RUN mkdir -p bin lib64 && \
    cp /usr/src/mover-polygon/moverd bin/ && \
    cpld bin/moverd lib64

# Minimal runtime image.
FROM debian:13-slim

COPY --from=build /jail /usr/local/
ENV LD_LIBRARY_PATH="/usr/local/lib64"
VOLUME ["/xayagame", "/log"]
ENV GLOG_log_dir="/log"
RUN mkdir -p /log /xayagame

EXPOSE 8600

ENTRYPOINT ["/usr/local/bin/moverd"]
CMD ["--xaya_rpc_url=http://xayax:8000", \
     "--xaya_rpc_protocol=2", \
     "--game_rpc_port=8600", \
     "--game_rpc_listen_locally=false", \
     "--storage_type=sqlite", \
     "--datadir=/xayagame", \
     "--enable_pruning=1000", \
     "--pending_moves=false"]

Walkthrough

Base image. xaya/libxayagame ships every library a GSP needs — libxayagame, libxayautil, jsoncpp, glog, protobuf, jsonrpccpp, ZMQ, and the rest — already compiled and installed under /usr/local. We only add build-essential and pkg-config on top to get a compiler.

The MinXayaVersion patch. libxayagame's DefaultMain checks the version of the connected Xaya daemon and refuses anything older than 1010200. XayaX reports version 1000000, so an unpatched build would refuse to talk to the bridge. The sed line lowers the minimum in the installed header before we compile — this is a header-level constant, so patching the header is all it takes.

protoc. Mover stores its game state and undo data as protobuf messages. protoc -Imover --cpp_out=mover mover/proto/mover.proto generates mover.pb.cc/mover.pb.h next to the proto file, which we then compile like any other source.

The config.h stub. main.cpp includes config.h, which the upstream autotools build generates. Since we're compiling by hand, a one-line stub defining PACKAGE_VERSION is enough.

Compile and link. Each .cpp is compiled with -std=c++14 and the include flags pkg-config reports for jsoncpp, glog, protobuf and sqlite3. The link step pulls in -lxayagame -lxayautil plus the full dependency list — libxayagame's DefaultMain touches curl, ZMQ, microhttpd, OpenSSL, zlib, LMDB and all three jsonrpccpp libraries, so all of them must be on the link line.

cpld and the slim runtime. The build image is large; we don't want to ship it. cpld is a helper script provided by the base image that walks the binary's shared-library closure and copies every .so it needs into a folder. We collect moverd plus its libraries into /jail and copy just that into a fresh debian:13-slim stage, pointing LD_LIBRARY_PATH at the copied libraries. The result is a minimal image with exactly one binary in it.

Runtime defaults. The CMD flags are the same ones the compose file passes explicitly — see the next section for what each one means. /xayagame (game database) and /log (glog files) are declared as volumes so state survives container recreation.

Step 4: docker-compose.yml

Create ~/mover-polygon/docker/docker-compose.yml with exactly this content:

name: ${COMPOSE_PROJECT_NAME:-mover}

# Mover on Polygon: XayaX bridge + moverd GSP.
#
# Run with the bundled XayaX bridge:
#   docker compose --profile xayax up -d --build
#
# If you already run a XayaX instance elsewhere, omit the profile and set
# XAYA_RPC_URL to its endpoint.  Note that the ZMQ addresses advertised by
# XayaX (getzmqnotifications) must be reachable from the moverd container.

services:
  xayax:
    image: xaya/xayax
    profiles:
      - xayax
    restart: unless-stopped
    networks:
      - mover-net
    ports:
      - "127.0.0.1:${XAYAX_PORT:-8101}:8000"
    volumes:
      - xayax-data:/xayax
    command:
      - "eth"
      - "--eth_rpc_url=${ETH_RPC_URL:-https://polygon-node.xaya.io}"
      - "--accounts_contract=${ACCOUNTS_CONTRACT:-0x8C12253F71091b9582908C8a44F78870Ec6F304F}"
      - "--port=8000"
      - "--listen_locally=false"
      - "--zmq_address=tcp://xayax:28555"
      - "--logtostderr"

  moverd:
    build:
      context: ..
      dockerfile: docker/Dockerfile
    restart: unless-stopped
    networks:
      - mover-net
    ports:
      - "127.0.0.1:${MOVERD_PORT:-8602}:8600"
    volumes:
      - moverd-data:/xayagame
      - moverd-logs:/log
    environment:
      - GLOG_alsologtostderr=1
    command:
      - "--xaya_rpc_url=${XAYA_RPC_URL:-http://xayax:8000}"
      - "--xaya_rpc_protocol=2"
      - "--game_rpc_port=8600"
      - "--game_rpc_listen_locally=false"
      - "--storage_type=sqlite"
      - "--datadir=/xayagame"
      - "--enable_pruning=1000"
      - "--pending_moves=false"

networks:
  mover-net:
    ipam:
      config:
        # Pinned to avoid Docker auto-assigning 192.168.0.0/20, which
        # shadows the host's 192.168.x LAN routes.
        - subnet: 172.16.0.0/24

volumes:
  xayax-data: {}
  moverd-data: {}
  moverd-logs: {}

Walkthrough

The xayax profile. The bridge service sits behind a Compose profile named xayax. Run with --profile xayax to get a bundled bridge, or omit the profile and set XAYA_RPC_URL to point moverd at a XayaX you already run (for example one shared by several GSPs — see Running XayaX). One caveat when using an external bridge: XayaX advertises its ZMQ address via getzmqnotifications, and that address must be reachable from the moverd container.

Ports. Both RPC ports are published on 127.0.0.1 only, so nothing is exposed to your network:

Host port Container What it serves
127.0.0.1:8101 xayax:8000 XayaX's Xaya-Core-style JSON-RPC (override with XAYAX_PORT)
127.0.0.1:8602 moverd:8600 The GSP's own JSON-RPC — the GSP RPC interface (override with MOVERD_PORT)

Volumes. xayax-data keeps the bridge's database, moverd-data keeps the GSP's SQLite state, and moverd-logs keeps glog files. With these in place, docker compose down && up resumes from where it left off instead of resyncing.

The pinned subnet. Without IPAM config, Docker may auto-assign 192.168.0.0/20 to a new network, which silently shadows routes to a real 192.168.x.x LAN — a classic "why can't my container reach the internet / why did my SSH session die" footgun. Pinning 172.16.0.0/24 avoids it.

XayaX flags. eth selects the EVM connector; --eth_rpc_url is the Polygon node it follows (the public https://polygon-node.xaya.io by default; substitute your own via ETH_RPC_URL if you have one); --accounts_contract is the XayaAccounts contract on Polygon, 0x8C12253F71091b9582908C8a44F78870Ec6F304F (see Names and Moves); --listen_locally=false makes it bind 0.0.0.0 inside the container so moverd and the published port can reach it. The crucial one is --zmq_address=tcp://xayax:28555: this is the address XayaX advertises to subscribers, so it must be the Docker service name — localhost would make moverd try to connect to itself.

moverd flags, one by one:

Flag Why
--xaya_rpc_url=http://xayax:8000 The XayaX endpoint, via the Docker service name
--xaya_rpc_protocol=2 JSON-RPC 2.0 — required when talking to XayaX
--game_rpc_port=8600 Port for the GSP's own JSON-RPC server
--game_rpc_listen_locally=false Bind 0.0.0.0 inside the container, so the published port works
--storage_type=sqlite Persistent state on disk (memory and lmdb are the other options)
--datadir=/xayagame Data directory; the GSP creates per-game/per-chain subdirs, here /xayagame/mv/polygon
--enable_pruning=1000 Keep undo data only for the last 1000 blocks (enough for any realistic reorg)
--pending_moves=false XayaX doesn't provide a pending-moves feed, so don't track mempool state

(Setting --pending_moves=true is harmless: moverd logs Not subscribing to pending moves because XayaX advertises no pending ZMQ endpoint, and pending state simply stays empty.)

GLOG_alsologtostderr=1 mirrors the glog output to stderr so docker logs shows everything.

Step 5: Build and launch

From the docker/ directory:

cd ~/mover-polygon/docker
docker compose --profile xayax up -d --build

The first run builds the moverd image — a few minutes on a normal machine, dominated by the C++ compile — then starts both containers on the mover-net network. Subsequent runs reuse the cached image.

Step 6: Watch it sync

Follow the GSP's log:

docker compose --profile xayax logs -f moverd

You should see something like (key lines, in order):

Connected to RPC daemon with chain polygon
Connected to Xaya Core version 1000000
Detected ZMQ blocks endpoint: tcp://xayax:28555
Got genesis height from game: 88365000
Game did not specify genesis hash, retrieved 701ca380fb3ec9bf7e0abd3d4f89895dae229e1138c2fd471245b486568dba10
Retrieving 0 detach and 128 attach steps with reqtoken = request_461, ...

Reading those lines back against what we built:

  • chain polygon / version 1000000 — moverd is talking to XayaX, and the MinXayaVersion patch is doing its job (1000000 would be rejected by an unpatched build).
  • Detected ZMQ blocks endpoint: tcp://xayax:28555 — the GSP auto-discovered the block-notification feed from the address XayaX advertises; this is why that flag must use the service name.
  • Got genesis height from game: 88365000 — our patched Chain::POLYGON case.
  • Game did not specify genesis hash, retrieved 701ca380... — the empty-hash trick: libxayagame fetched the hash for block 88365000 from XayaX and pinned it itself. (If you chose your own start height, you'll see a different hash here.)
  • The Retrieving ... attach steps lines repeat as libxayagame catches up in batches of blocks. In our verified run, catching up the ~5300 blocks between genesis 88365000 and the chain tip took about 2 minutes.

A note on the bridge side: a fresh XayaX starts near the current Polygon tip and is typically serving RPC within ~2 minutes, then backfills older blocks on demand when a GSP asks for them. That's exactly what happens here — our bridge started at tip ~88370000 and served the GSP's genesis block 88365000 on request. You can watch it with docker compose --profile xayax logs -f xayax.

You can also query the bridge directly to confirm it's healthy:

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

You should see something like:

{"id":1,"jsonrpc":"2.0","result":{"bestblockhash":"...","blocks":88370214,"chain":"polygon"}}

with blocks near the current Polygon tip (Polygon produces a block roughly every 2 seconds, so the number climbs fast).

Step 7: Confirm the GSP is up to date

The cheapest sync check is getnullstate — the standard state envelope without the (potentially large) game state attached:

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

You should see something like:

{"id":1,"jsonrpc":"2.0","result":{"blockhash":"6a4c517105b5b97a7052848c812f62c5ed6dca59950877caa27b285e5d309e09","chain":"polygon","gameid":"mv","height":88370225,"state":"up-to-date"}}

The fields to check:

  • "state":"up-to-date" — the GSP has processed every block XayaX knows about. While syncing you'll see "catching-up" instead; just wait and query again.
  • "gameid":"mv", "chain":"polygon" — you're talking to the right daemon on the right chain.
  • height / blockhash — the Polygon block the current game state corresponds to; it advances every couple of seconds.

That's it — you're running a full Game State Processor for Mover on Polygon mainnet. If you ask getcurrentstate, the game state starts out empty ({"players":{}}) until someone sends a Mover move after your chosen genesis block — if you kept our start height, you may already see players that other readers (or we, while verifying Part 2) have moved since. The full set of RPC methods your GSP now serves is documented in the GSP RPC interface reference.

Housekeeping

A few commands you'll want later:

# Stop everything (state is kept in the named volumes):
docker compose --profile xayax down

# Start again (no rebuild needed; resumes from stored state):
docker compose --profile xayax up -d

# Rebuild after changing logic.cpp (e.g. a new start height):
docker compose --profile xayax up -d --build

Note that the game state is derived deterministically, so you can always throw the volumes away (docker compose --profile xayax down -v) and resync from scratch — you'll get bit-identical state back.

Next: play the game

Your GSP is watching the chain, but the board is empty. In Part 2 you'll register a Xaya name (an ERC-721 on Polygon), send a real move through the XayaAccounts contract, and watch your player appear in the game state — for well under a cent in gas. Then Part 3 walks through the code you just compiled.

Clone this wiki locally