Skip to content

Dedicated snapshot-publisher service

Stand-alone node that publishes R2 snapshots without producing blocks. Prevents the 2026-06-14 cascade where snapshot upload competed with consensus on the primary validator.

Architecture

   sanect chain

        │ (block-sync over P2P, no consensus participation)

  ┌──────────────────────┐
  │ snapshot-publisher   │
  │ (RPC node, archive)  │
  │                      │
  │  - JOIN_NETWORK=true │
  │  - PRUNING=nothing   │
  │  - No validator key  │ ← this is what makes it safe to upload
  │  - SNAPSHOT_UPLOAD_  │
  │    INTERVAL_HOURS=6  │
  └──────────────────────┘

        │ tar | lz4 | rclone  (every 6h)

   Cloudflare R2
   sanect-snapshots.testnet.sanect.com

Why this is safe

  • No priv_validator_key.json → entrypoint's safety gate (PR after 2026-06-14) refuses to even start the snapshot cron on validators. This node is intentionally NOT a validator.
  • No consensus participation → tar+lz4 I/O can saturate disk without affecting block production
  • PRUNING=nothing → keeps the full chain so snapshots include all historical state
  • Crash isolation → if this service goes down, validators don't notice. They're not on it.

Railway setup

  1. Create a new Railway service from this repo
  2. Root Directory: leave empty (uses repo root)
  3. Dockerfile: docker/Dockerfile (the sanectd node image — same as validators)
  4. Volume: mount at /data (separate from any validator volume — don't share)
  5. Custom domain: none needed (this isn't user-facing)

Env vars on the new service

env
# Chain identity (same as validators)
CHAIN_ID=sanect_76287-1
EVM_CHAIN_ID=76287
HOMEDIR=/data/.sanectd
MONIKER=snapshot-publisher

# Join the network (don't bootstrap genesis)
JOIN_NETWORK=true
SEED_NODE_URL=https://rpc.testnet.sanect.com
SEED_NODE_ID=<primary's node id>
SEED_PEER_HOST=p2p.testnet.sanect.com:46430

# Archive node — keep all history
PRUNING=nothing

# DON'T set EXPLORER_HEARTBEAT_URL or PUBLIC_RPC_URL — this service is
# internal infrastructure, no need to register on the operator dashboard

# Snapshot upload schedule
SNAPSHOT_UPLOAD_INTERVAL_HOURS=6

# Tells the entrypoint to delete the auto-generated priv_validator_key.json
# on every boot. sanectd init creates one regardless of intent, and the
# post-2026-06-14 safety gate refuses snapshot uploads when that file
# exists. Setting SNAPSHOT_PUBLISHER=true makes the node literally unable
# to validate, so the gate is satisfied without bypass flags.
SNAPSHOT_PUBLISHER=true

# R2 credentials (only set here, NOT on validators)
R2_ACCOUNT_ID=<32-char Cloudflare account id>
R2_ACCESS_KEY_ID=<R2 token access key>
R2_SECRET_ACCESS_KEY=<R2 token secret>
R2_BUCKET=sanect-snapshots
R2_PUBLIC_URL=https://sanect-snapshots.testnet.sanect.com
SNAPSHOT_KEEP=3

# Mempool/timing tunings inherited from defaults

Critical — do NOT add these env vars

These would turn the publisher into something it shouldn't be:

  • priv_validator_key.json mounted from anywhere → would make it a validator
  • SNAPSHOT_UPLOAD_ALLOW_ON_VALIDATOR=true → bypasses the safety gate
  • ❌ Heartbeat / public-rpc envs → unnecessarily exposes internal infra

Remove R2 envs from validator services

After the publisher is up + running:

  1. Go to sanect-node (primary) Railway service
  2. Variables tab → delete:
    • R2_ACCOUNT_ID
    • R2_ACCESS_KEY_ID
    • R2_SECRET_ACCESS_KEY
    • R2_BUCKET
    • R2_PUBLIC_URL
    • SNAPSHOT_UPLOAD_INTERVAL_HOURS
    • SNAPSHOT_KEEP
  3. Save → Redeploy

Repeat for 2nd-node, 3rd-node, and Vultr.

This makes sure the cron can NEVER fire on a validator, no matter what env was forgotten.

Verify the publisher works

After the publisher's first scheduled cron tick (up to 6 hours):

bash
curl -s https://sanect-snapshots.testnet.sanect.com/LATEST.json | jq
# expect:
# {
#   "height": <some number near current tip>,
#   "filename": "sanect-<height>.tar.lz4",
#   "url": "https://sanect-snapshots.testnet.sanect.com/sanect-<height>.tar.lz4",
#   "size_bytes": <several GB>,
#   "created_at": "2026-06-15T...",
#   "chain_id": "sanect_76287-1"
# }

If LATEST.json is fresh (created_at within last 6 hours), the publisher is healthy.

Future joiners use this

Operators joining the chain set:

env
SNAPSHOT_MANIFEST_URL=https://sanect-snapshots.testnet.sanect.com/LATEST.json

The entrypoint streams the snapshot, restores it, and joins in ~3–5 minutes instead of replaying from block 0 (which would take days at 1M+ block heights).

Cost

  • Railway: ~$5–10/month for the small node
  • Cloudflare R2: ~$5/month for ~50GB of snapshots
  • Total: ~$10–15/month for permanent snapshot infrastructure

That's the cost of safety. The 2026-06-14 disaster cost a day of work and the entire testnet history. Worth it.