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.comWhy 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
- Create a new Railway service from this repo
- Root Directory: leave empty (uses repo root)
- Dockerfile:
docker/Dockerfile(the sanectd node image — same as validators) - Volume: mount at
/data(separate from any validator volume — don't share) - Custom domain: none needed (this isn't user-facing)
Env vars on the new service
# 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 defaultsCritical — do NOT add these env vars
These would turn the publisher into something it shouldn't be:
- ❌
priv_validator_key.jsonmounted 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:
- Go to sanect-node (primary) Railway service
- Variables tab → delete:
R2_ACCOUNT_IDR2_ACCESS_KEY_IDR2_SECRET_ACCESS_KEYR2_BUCKETR2_PUBLIC_URLSNAPSHOT_UPLOAD_INTERVAL_HOURSSNAPSHOT_KEEP
- 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):
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:
SNAPSHOT_MANIFEST_URL=https://sanect-snapshots.testnet.sanect.com/LATEST.jsonThe 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.