Refreshes a Substrate light-client chain spec's lightSyncState warp-sync
checkpoint from a live RPC node, at build time — not at runtime, and not by
tracking a third-party package's release cadence.
A light client's cold-start time is dominated by how stale its checkpoint is,
not by which chain or which client implementation you're using. Tracking
@polkadot-api/known-chains (or similar) only refreshes on that package's own
release cadence (observed: roughly every 1-3 weeks) — a hard ceiling on
freshness that has nothing to do with your own deploy cadence. Fetching fresh
at build time removes that ceiling: every image build gets a checkpoint that's
only as stale as time since that build, with zero added runtime cost or
network dependency at boot.
Same technique @polkadot-api/known-chains' own
update-sync-state.js
uses (a bare sync_state_genSyncSpec call against a real RPC node) — just run
on your own schedule instead of theirs.
chainspec-refresh --out <path> [--chain polkadot|kusama|westend|paseo] [--rpc <wss-url>] [--merge-light-sync-state] [--timeout <ms>]
--out(required): file to write.--chain(default:polkadot): which known relay chain's default RPC endpoint to use.--rpc: overrides--chain's default endpoint — needed for anything not in the known list (a parachain, a private/custom endpoint, etc.).--merge-light-sync-state: only replacelightSyncStatein the existing file at--out, leavingbootNodes/genesis/etc untouched (--outmust already exist). Without this flag, the full fetched spec overwrites--out.--timeout(default30000): milliseconds before giving up on the RPC.
The published binary is dynamically linked against musl's ld.so (not fully
static) and also needs libstdc++/libgcc at runtime — verified against a
plain alpine:latest, which is missing both. It runs correctly inside this
image (which has them), but don't copy the binary itself into your own
Dockerfile — a glibc-based image (Debian/Ubuntu, including oven/bun's
official image) won't be able to run it at all, and even another Alpine image
would need the same two packages installed.
Instead, use this image as an intermediate build stage and only copy out its output — a plain JSON file with no libc dependency at all:
FROM ghcr.io/bloque-app/fresh-light-chainspecs:<tag> AS chainspec
# For a merge-only refresh, the base file must already be present in this stage:
COPY chainspecs/kusama-relay.json /spec.json
RUN chainspec-refresh --chain kusama --out /spec.json --merge-light-sync-state
FROM <your-actual-base> AS builder
COPY --from=chainspec /spec.json ./chainspecs/kusama-relay.json
...Pin a specific tag or digest, not latest — treat a bump here like any other
dependency bump: a deliberate, reviewed step, not something that silently
changes what your build produces.
Every project should also keep its own committed default spec for local dev (a raw fetch for compiled/native projects, a last-known-good file for JS/TS ones) — this tool only overrides that default during the actual Docker build, it doesn't replace the need for one.
bun install
bun test
bun run typecheck
bun run build # compiles dist/chainspec-refresh (bun-linux-x64-musl)