Skip to content

Mainnet archive RPC + snapshot publisher ​

Stand up the mainnet equivalent of testnet's snapshot-publisher service. This node serves two roles:

  1. Archive RPC at archive.sanect.com — full history, used by explorer-backend (NODE_URL) and staking-app (NEXT_PUBLIC_SCAN_RPC_URL) so heavy eth_getLogs scans never hit the primary validator (per the 2026-06-20 incident).
  2. Snapshot publisher — uploads compressed data/ tarballs to R2 every 6h so new joiners can fast-sync.

Both roles run on the same node because both want PRUNING=nothing and neither participates in consensus.


Why it matters ​

  • Incident 2026-06-14: snapshot upload on the primary validator triggered I/O contention, cascading-failure across all 4 testnet nodes, full state loss. Same hardware class as mainnet → same risk.
  • Incident 2026-06-20: wallet Balance panel hit primary with genesis-to-tip eth_getLogs, OOM-killed the node, 312s consensus halt.

Both fixed by routing heavy reads + snapshot uploads to a dedicated archive node. Mainnet must have this from Day 1 or we'll re-hit one or both incidents under public load.


Step 1 — Cloudflare R2 bucket ​

  1. Cloudflare dashboard → R2 → Create bucket sanect-snapshots-mainnet
  2. R2 → Manage R2 API Tokens → Create token, scope Object Read & Write to that bucket. Save Access Key + Secret.
  3. Bucket → Settings → Public access → enable, OR add custom domain snapshots.sanect.com (CNAME → bucket's public R2.dev URL).

Cost: ~$5/mo for ~50GB of rolling snapshots.


Step 2 — DNS for archive.sanect.com ​

Cloudflare DNS → add CNAME:

  • Name: archive
  • Target: (Railway public URL of the service you're about to create — fill in after Step 3 returns it)
  • Proxy: Orange-cloud (proxied) — this is HTTPS RPC, Cloudflare proxy is fine

Step 3 — Railway service ​

  1. New Service from the sanect/sanect-node repo
  2. Branch: main
  3. Dockerfile: docker/Dockerfile
  4. TARGET_EVM_CHAIN_ID=7628 (required). Add it as a normal service variable: Railway passes service variables to Dockerfile builds as build arguments, and the Railway UI no longer has a separate Build args panel. The value is baked into the binary at build time (scripts/fork-cosmos-evm.sh), so after adding or changing it you must redeploy (rebuild). Verify in the service Console: curl -s -X POST localhost:8080/ -H 'content-type: application/json' -d '{"jsonrpc":"2.0","method":"eth_chainId","params":[],"id":1}' must return 0x1dcc (7628); 0x129ff (76287) means the build did not get it. If it was wrong, also wipe the /data volume, because state computed with the wrong chain ID is not repaired by a rebuild. Confirmed 2026-10-04 on snapshot-builder-archive (variable set, eth_chainId = 0x1dcc).
  5. Volume: mount at /data (separate volume — do NOT share with primary)
  6. Custom domain: archive.sanect.com
  7. Service name: archive-snapshot-publisher

Env vars ​

# Chain identity (mainnet)
NETWORK=mainnet
CHAIN_ID=sanect_7628-1
EVM_CHAIN_ID=7628
HOMEDIR=/data/.sanectd
DENOM=asnct
DISPLAY=snct
MONIKER=archive-snapshot-publisher

# Join existing chain (don't bootstrap genesis)
JOIN_NETWORK=true
SEED_NODE_URL=https://rpc.sanect.com
SEED_NODE_ID=<primary's CometBFT node id — get from `sanectd cometbft show-node-id` on the primary Railway shell>
SEED_PEER_HOST=p2p.sanect.com:<port from primary's TCP proxy>

# Archive — keep all history
PRUNING=nothing

# Snapshot publisher role (deletes priv_validator_key.json on every boot
# so the safety gate is satisfied and the upload cron runs)
SNAPSHOT_PUBLISHER=true
SNAPSHOT_UPLOAD_INTERVAL_HOURS=6
SNAPSHOT_KEEP=3

# R2 (set ONLY here, never on validators)
R2_ACCOUNT_ID=<32-char Cloudflare account id>
R2_ACCESS_KEY_ID=<from Step 1>
R2_SECRET_ACCESS_KEY=<from Step 1>
R2_BUCKET=sanect-snapshots-mainnet
R2_PUBLIC_URL=https://snapshots.sanect.com

After deploy, copy the Railway-assigned public URL and set it as the DNS target in Step 2.


Step 4 — Remove R2 vars from primary ​

After the archive node is healthy:

  1. Primary Railway service → Variables tab
  2. 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

This guarantees the snapshot cron can never fire on the primary, regardless of future env edits.


Step 5 — Wire downstream services to archive.sanect.com ​

ServiceVariableValue
explorer-backendNODE_URLhttp://archive-snapshot-publisher.railway.internal:8080 (private mesh; same project) OR https://archive.sanect.com (cross-project)
staking-appNEXT_PUBLIC_SCAN_RPC_URLhttps://archive.sanect.com (must be public — baked into browser bundle)
staking-appNEXT_PUBLIC_POOL_DEPLOY_BLOCKblock number of ShieldedPool v1.4 deploy on mainnet (find in deploy log)

Redeploy both services after setting.


Step 6 — Verify ​

bash
# Archive RPC reachable
curl -s https://archive.sanect.com -X POST -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","method":"eth_chainId","id":1}' | jq
# expect: {"jsonrpc":"2.0","id":1,"result":"0x1dcc"}    (0x1dcc = 7628)

# Archive node caught up
curl -s https://archive.sanect.com/rpc/status | jq '.result.sync_info.catching_up'
# expect: false

# Snapshot published (wait ≤6h after first boot)
curl -s https://snapshots.sanect.com/LATEST.json | jq
# expect: { height, filename, url, size_bytes, created_at, chain_id: "sanect_7628-1" }

If LATEST.json is fresh (created_at within last 6h), archive is healthy. If not, check Railway logs for the snapshot-upload.sh cron output.


Cost ​

  • Railway service: ~$10/mo (archive needs more disk than a pruning node)
  • R2 storage: ~$5/mo
  • R2 egress: free (Cloudflare R2 doesn't charge egress)
  • Total: ~$15/mo — same as testnet. Worth it.

Future joiners use this ​

Validator/RPC operators joining mainnet set on their service:

SNAPSHOT_MANIFEST_URL=https://snapshots.sanect.com/LATEST.json

Entrypoint streams + restores the snapshot in 3–5 min instead of replaying from block 0.