celestia-rs is a single binary that runs a Celestia consensus node
(validator or full node): the Rust state machine (celestia-app v10.4.0
parity), Rust consensus, P2P, CAT mempool, block / state sync, CometBFT
RPC, the app's gRPC + REST servers, and the operator CLI (init, keys,
genesis, tx, query, config, rollback, export). It uses the
same home directory layout and file formats as Go celestia-appd, so an
operator can switch between the two binaries.
This guide targets the reference machine, a Mac Mini M4 (16 GB RAM, 512 GB SSD) on macOS; Linux works the same way (systemd instead of launchd).
Running a Mocha validator on a Mac: section 11. Mainnet: section 12.
Status: following a public network over HTTPS RPC (
follow, section 10) is verified against Mocha and mainnet. On Mocha,startover P2P (state sync from Go peers, block sync, consensus with outbound-only peering) is verified on the reference Mac (2026-10-15, section 11.5); mainnet and Arabica P2P have only been exercised on local devnets. Everything else below is covered bycrates/tier-5-node/celestia-rs/tests/andconformance/devnet/operator.sh(Rust validator + Go full node).
Install (tagged releases) with the installer from the README:
curl --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/celestiaorg/celestia-rs/main/scripts/install.sh | shIt puts the binary at ~/.celestia-rs/bin/celestia-rs (the path section 8
uses) and adds that directory to PATH. Re-run it to update, or pass
sh -s -- --version <tag> to pin or roll back. It verifies the archive's
SHA-256, keeps the binary it replaces as celestia-rs.prev, and swaps the
new one in with a rename, so a running node keeps its binary until you
restart it. sh -s -- --help lists the options.
Download by hand instead. Each archive has a .sha256 next to it and
the release carries SHA256SUMS:
V=v0.1.0 # the tag to install
T=aarch64-apple-darwin # or x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu
curl -fLO https://github.com/celestiaorg/celestia-rs/releases/download/$V/celestia-rs-$V-$T.tar.gz
curl -fLO https://github.com/celestiaorg/celestia-rs/releases/download/$V/celestia-rs-$V-$T.tar.gz.sha256
shasum -a 256 -c celestia-rs-$V-$T.tar.gz.sha256
tar -xzf celestia-rs-$V-$T.tar.gz
sudo install -m 755 celestia-rs-$V-$T/celestia-rs /usr/local/bin/celestia-rs
celestia-rs version --longThe archive also holds packaging/ (launchd plist, systemd unit,
check-apphash.sh). The macOS binary is ad-hoc signed by the linker
(required on Apple silicon); gh and curl do not set the quarantine
attribute, so Gatekeeper does not interfere (after a browser download:
xattr -d com.apple.quarantine celestia-rs).
Build from source:
# Rust via rustup (rust-toolchain.toml pins the stable channel) and a
# C/C++ toolchain + libclang for RocksDB: `xcode-select --install` on
# macOS; `build-essential clang` on Debian/Ubuntu.
git clone https://github.com/celestiaorg/celestia-rs && cd celestia-rs
git checkout <tag or commit>
cargo build --profile dist --locked -p celestia-rs
sudo install -m 755 target/dist/celestia-rs /usr/local/bin/celestia-rs
celestia-rs version --longProfiles (Cargo.toml): release is what the tests, benchmarks and the
followers run (panic = "unwind", overflow-checks = true: the semantics
every test checks); dist is release plus fat LTO and one codegen unit,
for the binaries you deploy (the release workflow builds with it).
cargo build --release -p celestia-rs (target/release/celestia-rs) is
fine too and builds faster. The first build compiles RocksDB from source
(a clean dist build took 4 minutes on the Mac Mini M4, 2026-10-15). Only
curl is needed at runtime (genesis download, HTTPS RPC).
Targets: aarch64-apple-darwin (the reference Mac Mini M4; CI builds and
smoke-tests it on macos-14), x86_64-unknown-linux-gnu,
aarch64-unknown-linux-gnu. jemalloc is the allocator everywhere (on
macOS for Rust allocations only; RocksDB's C++ uses the system allocator
there).
RocksDB keeps up to 512 SST files open per database, plus P2P and RPC sockets. macOS defaults to 256 open files per process; the node warns at start-up when the limit is below 4096:
ulimit -n 65536 # in the shell that starts the nodeUnder launchd set SoftResourceLimits / HardResourceLimits →
NumberOfFiles (the plist in section 8 does).
# A single-validator devnet in one command (home ~/.celestia-rs by default):
celestia-rs start --network local --home ~/.celestia-rs/local
# A Mocha full node: initializes the home on first start (chain ID, seeds,
# peers), downloads and verifies the genesis, then block-syncs.
celestia-rs start --network mocha --home ~/.celestia-rs/mocha--network knows these chains (from celestia-app v10.4.0):
| network | chain ID | genesis SHA-256 | seeds / peers |
|---|---|---|---|
mainnet |
celestia |
9727aac9…7793 |
2 seeds + 2 peers (checked live 2026-10-18; see §12) |
mocha |
mocha-5 |
82db3ac5…5d33 |
2 seeds + 1 peer (scripts/mocha.sh) |
arabica |
arabica-11 |
not pinned upstream (chain ID checked) | none built in: pass --p2p.seeds |
local |
celestia-rs-local |
generated | — |
Genesis files are fetched from the celestiaorg/networks repository and
rejected unless their SHA-256 (and chain ID) match. celestia-rs download-genesis --network mocha retries the download for an existing
home.
Configuration precedence: built-in defaults (Go's init templates) <
config.toml / app.toml < environment (CELESTIA_RS_HOME,
CELESTIA_RS_NETWORK, CELESTIA_RS_LOG_LEVEL) < command-line flags.
Every command mirrors celestia-appd:
H=~/.celestia-rs/mocha
celestia-rs init my-validator --network mocha --home $H
celestia-rs keys add operator --keyring-backend file --home $H # prints the mnemonic once
celestia-rs keys show operator -a --bech val --home $H --keyring-backend file
# … fund the operator account, wait for the node to catch up, then:
celestia-rs tx staking delegate <celestiavaloper…> 1000000utia --from operator \
--keyring-backend file --home $H --fees 2000utia --waitThen create the validator from a JSON file in Go's format (the consensus
public key from celestia-rs comet show-validator --home $H):
cat > validator.json <<JSON
{"pubkey": <output of comet show-validator>, "amount": "1000000utia",
"moniker": "my-validator", "commission-rate": "0.1", "commission-max-rate": "0.2",
"commission-max-change-rate": "0.01", "min-self-delegation": "1"}
JSON
celestia-rs tx staking create-validator validator.json --from operator \
--keyring-backend file --home $H --gas auto --gas-adjustment 1.3 --gas-prices 0.004utia --waittx staking edit-validator (--new-moniker, --identity, --website,
--security-contact, --details, --commission-rate,
--min-self-delegation), unbond and redelegate work like Go's.
Keyrings: test (unencrypted, <home>/keyring-test) and file
(passphrase-encrypted, <home>/keyring-file, passphrase read from stdin
like Go — or CELESTIA_RS_KEYRING_PASSPHRASE for automation) are
byte-compatible with Go. The os backend (macOS Keychain) is not
supported: use celestia-appd keys export / celestia-rs keys import to
move a key.
For a new network, the genesis ceremony is the same as with Go:
celestia-rs genesis add-genesis-account operator 1000000000utia --home $H --keyring-backend test
celestia-rs genesis gentx operator 5000000000utia --home $H --keyring-backend test --ip <public-ip>
celestia-rs genesis collect-gentxs --home $H
celestia-rs genesis validate --home $H # runs InitChain in memoryGentx files are byte-identical to Go's for the same key and inputs.
The home layout is the same, so migration means pointing celestia-rs at
the Go home (or a copy of it):
| file | used by celestia-rs |
|---|---|
config/config.toml, config/app.toml, config/client.toml |
yes (Go keys; unknown keys ignored) |
config/genesis.json, config/node_key.json |
yes (same formats) |
config/priv_validator_key.json |
yes |
data/priv_validator_state.json |
yes — the double-sign guard |
keyring-file/, keyring-test/ |
yes |
data/application.db, blockstore.db, state.db, cs.wal (Go LevelDB/PebbleDB) |
no — celestia-rs uses its own RocksDB stores |
Because the databases differ, the Rust node starts from an empty state:
state-sync it ([statesync] in config.toml, as for a fresh Go node) or
block-sync from genesis. Procedure:
- Stop the Go node and make sure it stays stopped (disable the systemd unit / launchd job, remove it from cosmovisor).
- Back up
config/anddata/priv_validator_state.json. - Move the Go data away (
mv data data.goand keep onlydata/priv_validator_state.json:mkdir data && cp data.go/priv_validator_state.json data/). - Enable state sync in config.toml (
enable = true,rpc_servers,trust_height,trust_hash) or rely on block sync. celestia-rs start --home <home>.
data/priv_validator_state.json records the last height / round / step
the validator signed. Both binaries refuse to sign anything at or below it
(except an identical re-sign), which is what makes a migration safe:
- Never run two nodes with the same
priv_validator_key.json— not even briefly, not one Go and one Rust. Stop and disable the old process first; check withpsthat it is gone. - Always carry over the latest
priv_validator_state.jsonfrom the node that signed last. Never delete it, never replace it with an older copy or with the{"height": "0", …}fileinitwrites. If in doubt, wait until the network is a few blocks past the last height the old node signed (itspriv_validator_state.jsonheight) before starting the new one. - Do not use
celestia-appd comet unsafe-reset-allon a validator home: it resets the sign state.celestia-rs rollbackkeeps it. celestia-rswrites and fsyncs the sign state before a signature leaves the process, in the same format as Go, so switching back to Go later is safe under the same rules.
| port | what | config |
|---|---|---|
| 26656 | P2P | config.toml [p2p] laddr |
| 26657 | CometBFT RPC (JSON-RPC, URI, WebSocket) | [rpc] laddr |
| 9098 | celestia-core gRPC (BlockAPI, BlobstreamAPI) | [rpc] grpc_laddr |
| 9090 | app gRPC (+ BlockAPI) | app.toml [grpc] address (enable = true) |
| 1317 | REST API | app.toml [api] address (enable = true) |
| 26660 | Prometheus metrics | [instrumentation] prometheus = true, prometheus_listen_addr |
Only 26656 needs to be reachable from the internet; keep the others on localhost or behind a firewall.
| setting | default | notes |
|---|---|---|
app.toml pruning |
default |
celestia-rs maps default to the validator-minimal policy (keep 100 versions, prune every commit, ADR 0002) instead of Go's 362,880 versions; nothing, everything and custom (pruning-keep-recent, pruning-interval) follow Go |
app.toml min-retain-blocks |
3000 | blocks below height - min-retain-blocks (and below the state-sync snapshot window) are pruned from the block store via ResponseCommit.retain_height; values 1–2999 are raised to 3000 like celestia-app; 0 keeps all blocks |
app.toml state-sync.snapshot-interval |
1500 | 0 disables snapshots; snapshots are written in the background (§11.13) |
app.toml iavl-cache-size |
781250 | decoded IAVL nodes (~250 B each) |
| flags | --pruning, --pruning-keep-recent, --pruning-interval, --min-retain-blocks, --db-cache-mib (512) |
override app.toml |
With the defaults a mainnet validator stays well inside the 350 GB/year
budget of docs/hardware.md.
Consensus WAL. data/cs.wal/ holds the consensus write-ahead log in
Go's format and layout: the head file wal is rotated into wal.000,
wal.001, ... once it reaches 10 MB (checked every 5 s), and the oldest
rotated files are deleted while the directory holds more than 1 GB, so
it never needs manual pruning (Go has no setting for these limits and
neither does celestia-rs). Every vote and proposal the node signs is
written and fsynced to the head before it is sent; everything else is
fsynced every 2 s. On start the node replays the records after the last
#ENDHEIGHT marker, searching the files newest to oldest. A WAL from
builds before rotation (a lone data/cs.wal/wal) is read as is: it
becomes wal.000 at the first rotation.
packaging/macos/org.celestia.celestia-rs.plist is a LaunchDaemon (it
runs from boot without a login session, as your user): KeepAlive
restarts the node after a crash or a non-zero exit but not after a clean
stop (SIGTERM, halt-height), ThrottleInterval waits 30 s between
restarts, ExitTimeOut gives SIGTERM 120 s before SIGKILL, the
open-files limit is raised to 4096, and logs go to
~/Library/Logs/celestia-rs.log.
NODE_HOME=$HOME/.celestia-rs/mocha
sed -e "s|__USER__|$(id -un)|g" -e "s|__NODE_HOME__|$NODE_HOME|g" \
packaging/macos/org.celestia.celestia-rs.plist \
| sudo tee /Library/LaunchDaemons/org.celestia.celestia-rs.plist >/dev/null
sudo chown root:wheel /Library/LaunchDaemons/org.celestia.celestia-rs.plist
sudo chmod 644 /Library/LaunchDaemons/org.celestia.celestia-rs.plist
plutil -lint /Library/LaunchDaemons/org.celestia.celestia-rs.plist
sudo launchctl bootstrap system /Library/LaunchDaemons/org.celestia.celestia-rs.plist # start now and at every boot
sudo launchctl print system/org.celestia.celestia-rs | grep -E 'state|pid|last exit'
tail -f ~/Library/Logs/celestia-rs.log
sudo launchctl bootout system/org.celestia.celestia-rs # stop (SIGTERM, clean shutdown)
sudo launchctl kickstart -k system/org.celestia.celestia-rs # restart
sudo launchctl disable system/org.celestia.celestia-rs # never start at boot (undo: enable)launchd opens the log in append mode, so it can be trimmed while the node
runs: cp ~/Library/Logs/celestia-rs.log{,.1} && : > ~/Library/Logs/celestia-rs.log.
A Mac used as a validator must not sleep and should come back after a power cut:
sudo pmset -a sleep 0 disksleep 0 powernap 0 autorestart 1 womp 1
pmset -g | grep -E ' sleep|autorestart'With this setting, FileVault off (fdesetup status; with FileVault on, the
Mac waits at the unlock screen after a power cut) and the LaunchDaemons
installed, the nodes and the watchdog start at boot without a login.
Turn off automatic installation of macOS updates (System Settings ->
General -> Software Update -> Automatic Updates) so the machine never
reboots unannounced; reboot deliberately with the node stopped. Keep "Set
time and date automatically" on: consensus uses wall-clock timestamps
(sntp time.apple.com prints the offset).
So that upgrades and restarts need no password (for an operator script
or an agent on the Mac), keep the LaunchDaemon but run a binary you own
and allow exactly the four launchctl commands for this job
(packaging/macos/celestia-rs.sudoers; one-time sudo):
mkdir -p ~/.celestia-rs/bin && install -m 755 target/dist/celestia-rs ~/.celestia-rs/bin/celestia-rs
sed -e "s|__USER__|$(id -un)|g" -e "s|__NODE_HOME__|$NODE_HOME|g" \
-e "s|<string>/usr/local/bin/celestia-rs</string>|<string>$HOME/.celestia-rs/bin/celestia-rs</string>|" \
packaging/macos/org.celestia.celestia-rs.plist > /tmp/org.celestia.celestia-rs.plist
sudo install -m 644 -o root -g wheel /tmp/org.celestia.celestia-rs.plist /Library/LaunchDaemons/org.celestia.celestia-rs.plist
sed "s|__USER__|$(id -un)|g" packaging/macos/celestia-rs.sudoers > /tmp/celestia-rs.sudoers
sudo visudo -cf /tmp/celestia-rs.sudoers && \
sudo install -m 440 -o root -g wheel /tmp/celestia-rs.sudoers /etc/sudoers.d/celestia-rs
sudo ln -sf ~/.celestia-rs/bin/celestia-rs /usr/local/bin/celestia-rs # the CLI = the node's binaryAfterwards an upgrade is install -m 755 <new> ~/.celestia-rs/bin/celestia-rs
followed by sudo -n launchctl bootout … / sudo -n launchctl bootstrap …
(no prompt). The daemon already runs as your user, so a user-owned binary
adds no privilege; it does let anything running as you restart the node.
Verified on the Mocha validator 2026-10-16.
packaging/macos/celestia-rs-auto-update.sh deploys the latest commit on
main once every CI check on it is green. It builds the commit in
~/.celestia-rs/src and swaps the binary in for the Mocha node first. It
restarts the node and waits until the node is caught up and advancing,
matches the network's app hash, and (if bonded) signs at least 15 of the
20 commits after it is back in consensus (the heights it missed while
restarting and block-syncing do not count). It then soaks Mocha for 15 minutes and does the same for
the mainnet node (org.celestia.celestia-rs-mainnet, binary
~/.celestia-rs/bin/celestia-rs-mainnet). On any failure it restores
<binary>.prev, restarts, records the commit in
~/.celestia-rs/auto-update/bad-commits and stops, so the later node never
gets it. packaging/macos/org.celestia.celestia-rs-auto-update.plist runs
it hourly; its header has the install steps, and the sudoers file above
must include the labels it restarts. celestia-rs-auto-update.sh check
health-checks every node without changing anything; DRY_RUN=1 builds but
deploys nothing. Each deploy restarts each node once (a few missed blocks
on a validator), and a validator then runs code no one reviewed on that
machine before it signed with it: weigh that before enabling it on a
mainnet validator.
After a network outage, a node that kept running catches up through
consensus gossip. This is only a little faster than the chain, so a
3-hour outage can leave it hours behind for most of a day, and a
validator gets jailed. A restart starts with block sync, which catches
up in about a minute (verified on both nodes 2026-10-20).
celestia-rs-auto-update.sh watchdog does this restart for you. It
restarts a node with kickstart -k when the node is more than 50 blocks
behind its reference RPC on 3 passes in a row, and it restarts each node
at most once every 30 minutes (WATCHDOG_MAX_LAG, WATCHDOG_STRIKES,
WATCHDOG_COOLDOWN). It leaves alone a node that is down, is already
block-syncing, or is being deployed (auto-update lock). It also does
nothing while the reference RPC cannot be reached.
packaging/macos/org.celestia.celestia-rs-watchdog.plist runs it every
minute, and its header has the install steps. It logs to
~/Library/Logs/celestia-rs-watchdog.log only when a node is behind or
jailed.
The watchdog also unjails a validator, but only for a node that has a
file ~/.celestia-rs/unjail/<name> (mocha, mainnet). The file holds
the keyring passphrase of the operator key, and the watchdog types it at
the passphrase prompt. Leave the file empty for an empty passphrase or a
test keyring. When the node is in sync with its reference RPC, its
validator is jailed, the jail period is over and it is not tombstoned, the
watchdog sends tx slashing unjail --from operator --wait. It sends at
most one tx per node every 10 minutes (UNJAIL_COOLDOWN), and it writes
the tx output to ~/.celestia-rs/auto-update/watchdog/<name>.unjail.log.
A passphrase in the file lets anyone who can read it move the operator's
funds, so keep it mode 600 and leave it out of backups:
mkdir -m 700 -p ~/.celestia-rs/unjail
(umask 077; : > ~/.celestia-rs/unjail/mocha) # empty passphrase or test keyring
# a passphrase; run in a terminal (without one, read gets nothing):
(umask 077; read -rsp 'passphrase: ' p && printf '%s' "$p" > ~/.celestia-rs/unjail/mainnet)
celestia-rs-auto-update.sh watchdog # no "cannot read key" line: the passphrase worksWithout the file, unjail by hand after the restart (§11.9).
packaging/linux/celestia-rs.service: user celestia, home
/var/lib/celestia-rs/mocha, Restart=on-failure (a clean exit is not
restarted), TimeoutStopSec=120, LimitNOFILE=65536, and it may only
write below its home.
sudo useradd --system --create-home --home-dir /var/lib/celestia-rs celestia
sudo -u celestia celestia-rs init <moniker> --network mocha --home /var/lib/celestia-rs/mocha
sudo cp packaging/linux/celestia-rs.service /etc/systemd/system/
sudo systemctl daemon-reload && sudo systemctl enable --now celestia-rs
journalctl -u celestia-rs -f
sudo systemctl stop celestia-rs # SIGTERM, clean shutdownSIGTERM / SIGINT (launchctl bootout, systemctl stop, Ctrl-C) stop
consensus between steps, flush the consensus WAL (data/cs.wal), the
sign state and the stores, log node stopped and exit 0. A second signal
exits at once (exit 130); use it only if the first has not finished after
a couple of minutes. The sign state is written before every signature, so
even a SIGKILL or a power cut cannot make the node double sign when it
restarts.
The double-sign guard is data/priv_validator_state.json (Go's
format): height, round and step of the last signature and its sign bytes.
It is written and fsynced before a vote or proposal leaves the process,
and the signer refuses anything that goes backwards (height regression,
round regression, step regression) or different data at the same
height/round/step (conflicting data). Never delete it and never replace
it with an older copy (rules in §5). In addition, config.toml [consensus] double_sign_check_height = N (Go's option, default 0) makes
the node refuse to start (consensus start: found signature from the same key) when the validator's key signed one of the last N committed
blocks: set it to e.g. 20 for the first start after moving a validator,
then back to 0 (otherwise every restart has to wait N blocks).
Backups. What identifies a validator:
| file | what | backup |
|---|---|---|
config/priv_validator_key.json |
consensus (block-signing) key | once, offline (encrypted USB stick, password manager); never present on two running machines |
data/priv_validator_state.json |
last signed height/round/step | do not restore old copies; copy it from the stopped node only when moving the validator |
keyring-file/ |
operator account key (staking, unjail, signal) | the mnemonic, written down offline |
config/node_key.json |
P2P identity | optional (a new one only changes the node ID) |
# Back up the consensus key (the node may be running):
cp ~/.celestia-rs/mocha/config/priv_validator_key.json /Volumes/<usb>/mocha-priv_validator_key.json
# Restore / move to another machine:
# 1. stop the old node and make sure it cannot come back
# (launchctl disable / systemctl disable, remove its key);
# 2. copy priv_validator_key.json and the old node's current
# data/priv_validator_state.json into the new home (same paths);
# 3. start the new node. If the old state file is lost, set
# double_sign_check_height = 20 and start only once the chain is well
# past the last height the old node signed.- Logs:
--log_level info|debug|error|noneor Go's module form (consensus:debug,*:error),--log_format plain|json; defaults come from config.toml (log_level,log_format).RUST_LOGoverrides both. - Shutdown: SIGINT / SIGTERM stop consensus between steps, flush the WAL and the stores, and exit 0; a second signal exits immediately.
- Halting for an upgrade:
--halt-height N(orapp.toml halt-height) stops the node cleanly before executing block N, exactly like Go;--halt-time(Unix seconds) likewise. - Signalled upgrades: when the chain reaches an app-version upgrade
this binary has no handler for, the node stops consensus at the upgrade
height with
UPGRADE "vN" NEEDED at height: H, writesdata/upgrade-info.json(cosmovisor's trigger, Go's format) and keeps RPC up, like a Go node. Stop it and start the upgraded binary. - Rollback:
celestia-rs rollback [--hard]rolls the consensus and app state back one height (Go'scelestia-appd rollback), e.g. after an app-hash mismatch caused by a bug; the next start re-executes the block. - Export:
celestia-rs export [--height H] [--output-document f]writes the app state as a genesis file, identical tocelestia-appd exportat the same height. - Config:
celestia-rs config view|get|set config|app|client <key> [value]edits the TOML files in place (comments kept). - Status:
celestia-rs status,celestia-rs query block, the RPC on 26657, and Prometheus metrics with celestia-core's names (cometbft_consensus_height,cometbft_consensus_rounds,cometbft_consensus_block_interval_seconds,cometbft_consensus_missing_validators,cometbft_consensus_validator_missed_blocks,cometbft_p2p_peers,cometbft_mempool_size, …, labelledchain_id), so Go dashboards and alerts work unchanged. - Transactions:
celestia-rs tx bank send,tx staking delegate|unbond|redelegate|create-validator|edit-validator,tx blob pay-for-blob,tx signal signal|try-upgrade,tx slashing unjail(sign mode direct;--feesor--gas-prices(fee = ceil(price x gas), default 0.004 utia/gas);--gas autosimulates on the node and applies--gas-adjustment;--note/--memo;--waitpollstx_status). Any other message:celestia-rs tx <module> <msg> --json '<proto3 JSON fields>'(or--json @file), e.g.tx distribution withdraw-delegator-reward --json '{"delegator_address":"...","validator_address":"..."}'. Each command's--generate-onlyoutput is identical tocelestia-appd's (conformance/devnet/workloads/cli.sh); with acelestia1…address as--fromit never opens the keyring (no passphrase, no terminal needed), as Go.
A node that lags behind the chain — just restarted, recovering from a
partition, or still block-/state-syncing — runs CheckTx against its own
stale state. It can accept a tx (code 0) whose account sequence the chain
has already consumed, or hand a client a stale sequence from its auth account query. Such a tx is never included: once the node catches up, the
mempool recheck evicts it (sequence mismatch) and nothing is reported to the
submitter. celestia-appd behaves identically; this is not a celestia-rs
divergence (the soak harness attributes these losses to Go and Rust nodes
alike, conformance/devnet/soak).
To avoid it, submit through a node that is in sync:
- Check
celestia-rs status --node <rpc>:sync_info.catching_upmust befalseandsync_info.latest_block_timewithin a few block times of the wall clock (or itslatest_block_heightlevel with a reference node / explorer). The same fields are in the RPC/status; Prometheuscometbft_consensus_heightagainst a peer's gives an alert. - Query the account sequence from the same synced node you broadcast to.
To detect a lost tx, poll celestia-core tx_status (--wait does this):
PENDING that never turns COMMITTED, then EVICTED (dropped on recheck)
or UNKNOWN (dropped without trace, e.g. node restarted). Re-query the
account sequence from a synced node and re-sign; a tx re-broadcast with the
old sequence fails CheckTx with code 32 (account sequence mismatch, expected N, got M).
celestia-rs follow tracks a public network through celestia-core
JSON-RPC (HTTP or HTTPS, e.g. a provider's https://rpc… endpoint)
instead of P2P, and checks block by block that the Rust state machine
computes what the network committed. It is the tool for verifying
parity against a live chain, and the way to run a full node where P2P
ports are not reachable.
# First start: restore a Go state-sync snapshot (a Go node's
# data/snapshots/<height>/3/<chunk>), then follow.
celestia-rs follow --home ~/.celestia-rs/mocha-follow --chain-id mocha-5 \
--rpc https://rpc.celestia-mocha.com,https://celestia-testnet-rpc.polkachu.com \
--snapshot /path/to/data/snapshots [--trust-hash <header hash at the snapshot height>]
# Later starts resume from the home (no --snapshot needed).
celestia-rs follow --home ~/.celestia-rs/mocha-follow --chain-id mocha-5 \
--rpc https://rpc.celestia-mocha.com,https://celestia-testnet-rpc.polkachu.comPer height h the follower
- fetches
/block(many heights ahead in parallel,--prefetch, default 24; blockh+1carries the canonical commit ofh), from the first server that has it; - compares header
hwith the state its own execution ofh-1produced:app_hash,last_results_hash(tx codes, data, gas),validators_hash/next_validators_hash(validator updates),consensus_hash(param updates) and the app version; - verifies header
hwith the light client against the previous trusted header (light::verify_adjacent) and checks that the block id (header hash and part set, so every tx) is the one 2/3+ of the validator set signed; then runsValidateBlockand the app'sProcessProposal(--no-verify-dataskips the latter); - executes the block through the same block executor as
start(FinalizeBlock with the txs, misbehavior and DecidedLastCommit, then Commit).
Every --witness-interval heights (default 100) the header hash is
compared with the other servers that still have that height.
First start. The snapshot is verified like state sync: the light
client takes the trusted header at the snapshot height (--trust-hash,
or the primary's with a warning), OfferSnapshot carries the app hash
of header height+1, and the restored app hash and app version must
match. Polkachu's pruned Mocha tarballs
(snapshots.polkachu.com/testnet-snapshots/celestia/) contain such a
snapshot under data/snapshots/; only that directory is needed
(about 170 MB for mocha-5).
Divergence. The first mismatch stops the follower with a non-zero
exit and writes <home>/follow-diagnostics/<height>/: summary.json
(the mismatching header fields, a per-tx comparison of code / gas / data
with the network's, and every store's root hash from the network — read
from ICS-23 proofs of abci_query at that height — next to ours),
our_block_results.json, network_block_results.json and block.json.
A rejected ProcessProposal writes the same bundle.
Disk. The follower keeps --keep-blocks blocks (default 1000; Go's
3000 floor does not apply since it serves no peers), --pruning-keep-recent
app versions (default 100) and takes no snapshots.
Header-only mode. --light-only verifies headers without executing
blocks (for a network whose app version this binary cannot execute yet):
sequential light-client verification from --trust-height (default: the
tip minus 1000) to --stop-at-height, then a skipping jump to the tip.
celestia-rs follow --light-only --chain-id celestia \
--rpc https://rpc.celestia.pops.one,https://celestia-rpc.publicnode.comHTTPS goes through the system curl (TLS, HTTPS_PROXY and CA settings
from the environment), as do --node https://… for tx / query.
Exact steps for the reference machine (Mac Mini M4, macOS, Apple silicon)
to run a mocha-5 validator. Mocha runs app version 10 (celestia-app
v10.4.0-mocha). Every command below was checked against this binary's
--help; the ones marked (verified) were also run against Mocha's
public RPC from the development container, which cannot reach P2P ports
(OPEN_QUESTIONS.md #1); the P2P steps were first run on the Mac on
2026-10-15 (11.5, route A).
Mocha's parameters (queried 2026-10-15; re-check with the commands in
11.8): min_commission_rate 0.20, max_validators 100 (87 bonded,
so any bonded validator is in the active set), unbonding 14 days,
signed_blocks_window 10,000 with min_signed_per_window 0.001 (jailed
after missing 9,990 of the last 10,000 blocks), downtime_jail_duration
60 s, no downtime slash, 2% double-sign slash plus tombstoning.
# Used throughout:
export NODE_HOME=$HOME/.celestia-rs/mocha
export RPC=https://rpc.celestia-mocha.com # archival public RPC
export RPC2=https://celestia-testnet-rpc.polkachu.com # second providerxcode-select --install # C/C++ toolchain, only to build from source
brew install jq lz4 # jq: the checks below; lz4: route B
sudo pmset -a sleep 0 disksleep 0 powernap 0 autorestart 1 womp 1
ulimit -n 4096 # in every shell that runs the node by handInstall the binary (§1: release download or cargo build --profile dist --locked -p celestia-rs), then celestia-rs version --long.
Network: allow inbound TCP 26656 (the only port that must be public).
On the router, forward 26656 to the Mac; in the macOS firewall allow the
binary: sudo /usr/libexec/ApplicationFirewall/socketfilterfw --add /usr/local/bin/celestia-rs && sudo /usr/libexec/ApplicationFirewall/socketfilterfw --unblockapp /usr/local/bin/celestia-rs. Without inbound connections the
node still works through its outbound peers, but more peers make
proposals and votes arrive sooner. Outbound-only was enough on Mocha
(2026-10-15): with 11 persistent peers the node reached 13 peers within
two minutes and followed consensus. If macOS asks to "allow incoming
connections", Deny only blocks inbound 26656; the local RPC and
Prometheus are unaffected.
celestia-rs init "<your-moniker>" --network mocha --home $NODE_HOME > /dev/nullLike Go's init, it prints the whole genesis (~78 MB) to stdout, hence
> /dev/null; errors still go to the terminal.
This writes config/{config,app,client}.toml (chain ID mocha-5, the
built-in seeds and persistent peer), a new consensus key
config/priv_validator_key.json, data/priv_validator_state.json
(height 0), config/node_key.json, and downloads the 56 MB genesis,
rejected unless its SHA-256 is 82db3ac5…5d33.
Then:
H=$NODE_HOME
celestia-rs config set config instrumentation.prometheus true --home $H
celestia-rs config set config instrumentation.prometheus_listen_addr "127.0.0.1:26660" --home $H
celestia-rs config set config p2p.external_address "<public-ip>:26656" --home $H # if 26656 is forwarded
celestia-rs config set client node "tcp://127.0.0.1:26657" --home $H
celestia-rs config set client keyring-backend file --home $H
celestia-rs config view config --home $H | grep -E '^(seeds|persistent_peers|laddr|external_address|prometheus)'init already set client.toml chain-id = "mocha-5"; RPC stays on
127.0.0.1:26657 and P2P listens on 0.0.0.0:26656.
More peers if the built-in ones are slow (live peers of a public node;
without inbound connections only these and PEX keep the node connected,
so it is worth checking that each accepts a TCP connection, e.g.
nc -z -G 3 <ip> <port>, and keeping the built-in itrocket peer in the
list):
PEERS=$(curl -s $RPC2/net_info | jq -r '[.result.peers[] | select(.remote_ip | test(":") | not)
| "\(.node_info.id)@\(.remote_ip):\(.node_info.listen_addr | split(":") | last)"][:10] | join(",")')
celestia-rs config set config p2p.persistent_peers "$PEERS" --home $HBack up config/priv_validator_key.json now (§8, "Backups").
Created on the Mac, never copied elsewhere. The file keyring encrypts
it with a passphrase (asked on the terminal; CELESTIA_RS_KEYRING_PASSPHRASE
for scripts):
celestia-rs keys add operator --keyring-backend file --home $H
# -> prints the address and the 24-word mnemonic ONCE: write it down offline.
celestia-rs keys show operator -a --keyring-backend file --home $H # celestia1… (asks the passphrase)
celestia-rs keys show operator -a --bech val --keyring-backend file --home $H # celestiavaloper1…Fund the celestia1… address from the Mocha faucet: the web faucet at
https://mocha.celenium.io/faucet, or the #mocha-faucet channel of the
Celestia Discord ($request celestia1…). You need the self-delegation
(1 TIA is enough to be in the active set today) plus ~1 TIA for fees.
celestia-rs query bank balances <celestia1…> --node $RPC # (verified)A fresh node can block-sync Mocha from genesis, but that replays 1.7M+ blocks. Two faster routes; try A first.
Route A — state sync from Mocha peers (P2P). The node discovers Go nodes' state-sync snapshots over P2P, verifies them with the light client against the RPC servers, restores the app state and block-syncs the rest:
LATEST=$(curl -s $RPC/block | jq -r .result.block.header.height)
TRUST_HEIGHT=$((LATEST - 3000))
TRUST_HASH=$(curl -s "$RPC/block?height=$TRUST_HEIGHT" | jq -r .result.block_id.hash)
curl -s "$RPC2/block?height=$TRUST_HEIGHT" | jq -r .result.block_id.hash # must print the same hash
celestia-rs config set config statesync.enable true --home $H
celestia-rs config set config statesync.rpc_servers "$RPC:443,$RPC2:443" --home $H
celestia-rs config set config statesync.trust_height $TRUST_HEIGHT --home $H
celestia-rs config set config statesync.trust_hash $TRUST_HASH --home $H
ulimit -n 4096 && celestia-rs start --home $H 2>&1 | tee ~/mocha-first-start.logExpect Discovering snapshots, Offering snapshot, Applied snapshot chunk, Snapshot restored, then block sync. State sync over P2P is at
parity with Go on local devnets (conformance/devnet/statesync-rust.sh)
and worked against Mocha's peers on the first try (2026-10-15, Mac Mini
M4, outbound-only, 11 persistent peers): first peer after 0.8 s, Go
format-3 snapshots discovered after 3 s, the 1,716,000 snapshot (18
chunks) offered when the 15 s discovery window closed and restored with
its app hash verified in 7 s, then 221 blocks block-synced (~80 blocks/s)
and consensus 47 s after start. If no snapshot is found or
restored within ~15 minutes, stop the node (Ctrl-C), remove the partial
state (rm -rf $H/data/*.rocksdb $H/data/cs.wal $H/data/evidence* —
keep data/priv_validator_state.json), set statesync.enable false and
use route B.
Route B — Polkachu snapshot + follow, then P2P. The route the
HTTPS follower uses (§10): restore the Go SDK snapshot inside Polkachu's
pruned Mocha tarball, catch up over HTTPS with full verification, then
switch to P2P. follow and start share the stores of a home, so the
validator home itself is bootstrapped:
# Current file name: https://polkachu.com/testnets/celestia/snapshots
URL=https://snapshots.polkachu.com/testnet-snapshots/celestia/celestia_<height>.tar.lz4
mkdir -p ~/polkachu && curl -L "$URL" | lz4 -dc - | tar -x -C ~/polkachu data/snapshots
ls ~/polkachu/data/snapshots # <height>/3/<chunks>, ~170 MB
SNAP=$(ls ~/polkachu/data/snapshots | grep -E '^[0-9]+$' | sort -n | tail -1)
celestia-rs follow --home $H --chain-id mocha-5 --rpc $RPC,$RPC2 \
--snapshot ~/polkachu/data/snapshots \
--trust-hash $(curl -s "$RPC/block?height=$SNAP" | jq -r .result.block_id.hash) # $RPC is archivalfollow restores the snapshot (the app hash must equal the network's),
then executes every block to the tip, comparing app hash, results and
validator sets per height (stops with a diagnostic bundle on any
difference). When it logs heights near the tip (celestia-rs status --node $RPC for the tip), stop it with Ctrl-C and start the node; block
sync over P2P fetches the remaining blocks.
As of 2026-10-15 the snapshot inside Polkachu's tarball
(celestia_1702915.tar.lz4) is still at height 1,624,500, so
follow executes ~90k heights to reach the tip (7-14 blocks/s on a
loaded 4-core cloud VM; expect an hour or two on the M4). The light
client only trusts a header younger than the trusting period
(--trust-period-hours, default 336 = Mocha's 14-day unbonding period):
once that snapshot is older than 14 days, route B needs a newer snapshot.
Verified from the development container: init --network mocha, the
streamed extraction, follow --snapshot (restored app hash equal to the
network's), then start on the same home (handshake at the follow
height, RPC up, check-apphash.sh OK, block sync started; P2P peers are
unreachable there).
rm -rf ~/polkachu
ulimit -n 4096 && celestia-rs start --home $H 2>&1 | tee ~/mocha-first-start.logWhichever route: the first start runs in the foreground. Once it is caught up, stop it (Ctrl-C) and run it under launchd (§8):
celestia-rs status --home $H | jq .sync_info.catching_up # false when caught up
celestia-rs status --home $H | jq -r .sync_info.latest_block_height
celestia-rs status --node $RPC | jq -r .sync_info.latest_block_height
curl -s localhost:26657/net_info | jq .result.n_peerscatching_up turns false when block sync hands over to consensus; the
node then logs committed block every ~6 s.
packaging/check-apphash.sh http://127.0.0.1:26657 $RPC # OK height=… app_hash=…It compares the app hash our node computed for its latest height H
(/abci_info) with the one the network committed in header H+1 (block
sync and consensus already reject a block whose app hash differs, so a
mismatch means the node has stopped advancing). Independently, the light
client can verify the network's headers from the Mac:
celestia-rs follow --light-only --chain-id mocha-5 --rpc $RPC,$RPC2 --home /tmp/mocha-light # (verified)Only when 11.7 is OK and the account is funded:
celestia-rs comet show-validator --home $H # {"@type":"/cosmos.crypto.ed25519.PubKey","key":"…"}
cat > ~/validator.json <<JSON
{
"pubkey": $(celestia-rs comet show-validator --home $H),
"amount": "1000000utia",
"moniker": "<your-moniker>",
"identity": "",
"website": "",
"security": "",
"details": "celestia-rs (Rust) validator",
"commission-rate": "0.20",
"commission-max-rate": "0.30",
"commission-max-change-rate": "0.01",
"min-self-delegation": "1000000"
}
JSON
# Inspect first (nothing signed or broadcast). With the operator's address
# as --from, --generate-only never opens the keyring, so no passphrase is
# asked and it works without a terminal (as Go); a key name (`--from
# operator`) opens the file keyring and asks the passphrase, also as Go:
OPERATOR=$(celestia-rs keys show operator -a --keyring-backend file --home $H) # asks the passphrase once
celestia-rs tx staking create-validator ~/validator.json --from $OPERATOR \
--keyring-backend file --home $H --chain-id mocha-5 --generate-only | jq .
# Sign and broadcast through the local synced node:
celestia-rs tx staking create-validator ~/validator.json --from operator \
--keyring-backend file --home $H --chain-id mocha-5 \
--gas auto --gas-adjustment 1.3 --gas-prices 0.004utia --wait--gas auto simulates with the key's public key, so --generate-only --gas auto with an address --from fails with .info: key not found
(as Go); give --gas <n> there, or use the key name.
Commission: Mocha (and mainnet) enforce min_commission_rate 0.20, so
0.20 is the lowest accepted rate; commission-max-rate can never change
later and commission-max-change-rate bounds daily changes.
min-self-delegation is in utia (1 TIA here): if the self-delegation
drops below it the validator is jailed. Check the result:
VALOPER=$(celestia-rs keys show operator -a --bech val --keyring-backend file --home $H)
celestia-rs query staking validator $VALOPER --node $RPC | jq '.validator | {status, jailed, tokens}' # (verified)
celestia-rs query staking params --node $RPC # (verified)status becomes BOND_STATUS_BONDED at the end of the block that
included the tx (height H); the node signs from height H+2 (the
validator-set update delay). Signed votes are logged at debug only
(like Go), so check the commits instead: the last_commit of block N+1
lists our consensus address with block_id_flag 2 (COMMIT) when we
signed N. Verified on Mocha 2026-10-15: tx at 1,720,896, first
signature at 1,720,898, then every block.
CONS=$(jq -r .address $H/config/priv_validator_key.json) # the address field only
curl -s localhost:26657/block | jq --arg a "$CONS" \
'.result.block.last_commit.signatures[] | select(.validator_address==$a) | .block_id_flag' # 2-
Grafana dashboards and alerts:
packaging/macos/monitoring/install.shsets up Prometheus, Grafana and node_exporter for the Mocha and mainnet nodes on the Mac. It adds a dashboard and alert rules for every check below, and serves Grafana on the tailnet withtailscale serve(packaging/macos/monitoring/README.md). -
Prometheus (
instrumentation.prometheus = true, port 26660):curl -s localhost:26660/metrics | grep -E 'consensus_height|missed_blocks|p2p_peers'. Alert on:cometbft_consensus_heightnot increasing for 1 minute;cometbft_consensus_validator_missed_blocks(per-validator counter, present after the first miss) or a stalecometbft_consensus_validator_last_signed_height, both labelledvalidator_address= our consensus address in hex (jq -r .address $H/config/priv_validator_key.json);cometbft_p2p_peers< 3. -
Missed blocks from the chain (the slashing window counter):
VALCONS=$(celestia-rs comet show-address --home $H) celestia-rs query slashing signing-info $VALCONS --home $H | jq .val_signing_info # (verified) celestia-rs query slashing params --node $RPC | jq .params # (verified)
missed_blocks_counterover the lastsigned_blocks_window(10,000).jailed_untilis only the earliest block time at which unjail is allowed: a validator stays jailed past it until an unjail tx. Whether it is jailed is staking's flag:celestia-rs query staking validator $VALOPER --home $H | jq .validator.jailed. -
App hash: run
packaging/check-apphash.shevery few minutes (cron or launchdStartInterval); exit code 1 is a parity bug — stop the validator and keep~/Library/Logs/celestia-rs.log. -
Disk/memory:
du -sh $H/data,ps -o rss= -p $(pgrep -x celestia-rs)(budgets indocs/hardware.md).
Jailed (downtime): fix the cause (node down, clock, peers), wait until
it is caught up (11.6) and downtime_jail_duration (60 s on Mocha) has
passed, then:
celestia-rs tx slashing unjail --from operator \
--keyring-backend file --home $H --chain-id mocha-5 \
--gas auto --gas-adjustment 1.3 --gas-prices 0.004utia --wait
celestia-rs query staking validator $VALOPER --node $RPC | jq .validator.jailed # falseA validator jailed for double signing is tombstoned and can never be unjailed: create a new validator with a new consensus key.
sudo launchctl bootout system/org.celestia.celestia-rs # SIGTERM; waits for the clean exit
grep -E 'node stopped|shutting down' ~/Library/Logs/celestia-rs.log | tail -2
pgrep -x celestia-rs || echo stopped
sudo install -m 755 ./celestia-rs /usr/local/bin/celestia-rs # the new binary
celestia-rs version --long
sudo launchctl bootstrap system /Library/LaunchDaemons/org.celestia.celestia-rs.plist
tail -f ~/Library/Logs/celestia-rs.log # handshake, then committed blocksKeep the previous binary (cp /usr/local/bin/celestia-rs ~/celestia-rs.prev
before installing) to roll back the same way. A stop costs a few missed
blocks, far from the jail threshold. Never start a second copy of the
node "to test" with the same priv_validator_key.json.
After a missed block, a refused signature or any consensus surprise, save the WAL before it rotates away (it keeps about 1 GB, which is hours of history on Mocha, less under load):
H=~/.celestia-rs/mocha
tar -C $H/data -czf ~/cs.wal-$(date -u +%Y%m%dT%H%M%SZ).tgz cs.wal # safe while running
grep -E 'Searching for height|Found|Catchup by replaying' ~/Library/Logs/celestia-rs.log | tailCopying while the node runs is safe (the files are only appended to,
renamed and deleted; at worst the last record of the copied head is cut
short). The files are in Go's format: Go's wal2json (celestia-core
scripts/wal2json) reads them one file at a time, oldest (lowest
number) first, the head wal last. Never edit or delete files in cs.wal/ on a
running node; on a stopped node a damaged head is repaired on start
(wal.CORRUPTED keeps the original, as Go).
Log lines failed signing vote … height regression / round regression /
step regression / conflicting data, or the start-up error consensus start: found signature from the same key, mean the node refused to sign
because the key already signed at this or a later position. The node does
not sign until the chain moves past it. Do this:
- Stop and check for a second signer. Is another node (an old Mac
setup, a Go
celestia-appd, a test copy) running with thispriv_validator_key.json? Stop and disable it first. Two signers is the one way to get slashed 2% and tombstoned. - If no other signer exists, the state file is simply ahead of the
node's chain state (e.g. after
celestia-rs rollback, or a restore ofdata/from an older copy while keeping the newer state file): start the node again and let it catch up; it resumes signing once the chain height passes theheightindata/priv_validator_state.json. - Never delete or edit
priv_validator_state.jsonto make the error go away, and never rununsafe-reset-all-style resets on a validator home. - For
found signature from the same key(only withdouble_sign_check_height> 0): waitNblocks with the node stopped after making sure of step 1, then start it again.
One failed signing vote … conflicting data right after a restart, at the
height the old process was stopped in, with signing resuming a few blocks
later, was a known bug in builds before the fix for it (2026-10-15): the
old process signed its delayed precommit after SIGTERM (Go aborts that
wait on shutdown), so the restarted node's nil precommit at that height
conflicted. The guard refused correctly; it cost 3 missed blocks on Mocha
at height 1,721,185. Builds with the fix stop without signing: a SIGTERM
timed 0.37 s into the precommit wait at height 1,722,751 left the sign
state at the prevote, the node stopped in 66 ms, and after the restart
there was no refusal and only that one height was missed.
A second restart bug had the same symptom (Mocha heights 1,838,650 and 2,179,423, both auto-update restarts): a SIGTERM that came after the precommit reached the WAL but before the block committed. The restarted node replayed its prevote and precommit, then +2/3 precommits from peers arrived before the block, and it asked the privval for a nil precommit at the same height and round. The guard refused it, and the signed precommit was still gossiped. Builds with the fix (2026-10-31) do not sign a vote of a type the round already holds from this node. After the fix, every refusal is worth checking with step 1.
Celestia upgrades the app version by validator signalling (no governance
proposal): when a celestia-rs release supports app version N+1,
validators signal, anyone tallies, and the upgrade happens at the height
try-upgrade scheduled.
celestia-rs tx signal signal 11 --from operator --keyring-backend file --home $H \
--chain-id mocha-5 --gas auto --gas-adjustment 1.3 --gas-prices 0.004utia --wait
celestia-rs tx signal try-upgrade --from operator --keyring-backend file --home $H \
--chain-id mocha-5 --gas auto --gas-adjustment 1.3 --gas-prices 0.004utia --wait # once 5/6 signalledcelestia-rs is a single binary for every app version it supports (v9 and
v10 today; the v9→v10 switch happens inside Commit exactly like Go's
multiplexer), so the normal path is to install the release that supports
the new version before the upgrade height (11.10); the node then
crosses the upgrade without stopping. If the running binary has no
handler for the new version, it stops consensus at the upgrade height,
logs UPGRADE "vN" NEEDED at height: H, writes data/upgrade-info.json
(Go/cosmovisor format, {"name":"vN","height":H,…}) and keeps RPC up:
install the new binary and restart (11.10); it resumes at H.
halt-height stops the node cleanly before executing a given block, for
a coordinated halt announced by the Mocha operators:
celestia-rs config set app halt-height <H> --home $H # takes effect at the next start
sudo launchctl kickstart -k system/org.celestia.celestia-rs
# … the node exits 0 before block <H> (launchd does not restart a clean exit)
celestia-rs config set app halt-height 0 --home $H
# install the new binary, then bootstrap the job again (11.10)Builds before 2026-10-16 wrote the state-sync snapshot inside Commit: on
Mocha at height 1,722,000 (snapshot-interval 1500) consensus stopped for
~10 s between finalizing commit and committed block, the validator
missed heights 1,722,001–1,722,003 and RSS rose from ~0.7 to 1.9 GB.
On those builds set snapshot-interval = 0 (celestia-rs config set app state-sync.snapshot-interval 0 --home $H, then restart).
Current builds write the snapshot on a background thread like Go's
SnapshotIfApplicable: Commit returns at once, the snapshot height is kept
from pruning until the snapshot is done, a second snapshot is skipped while
one runs, and a failure is only logged (failed to create state snapshot). Look for creating state snapshot / completed state snapshot in the log. A stop during a snapshot abandons it; its partial
directory is removed and never offered to peers. After upgrading, the
interval can go back to 1500.
Mainnet (celestia) runs app version 9 (celestia-app v9.0.8, ADR
0004); celestia-rs executes v9 blocks at parity (followed over HTTPS with
every app hash matching, docs/milestones-completed.md S3) and v10 blocks, and performs
the v9→v10 upgrade inside Commit like Go's multiplexer
(devnet-conformance v9-upgrade). So the same binary runs a mainnet
validator before and after the v10 upgrade, and no binary swap is needed
at the upgrade height; when mainnet's validators signal v10 (tx signal signal 10) and try-upgrade schedules it, sign the signal from your
operator key like everyone else and keep the node running.
The procedure is section 11 with:
export NODE_HOME=$HOME/.celestia-rs/mainnet,--network mainnet(2 built-in seeds and 2 persistent peers),--chain-id celestia, RPCshttps://rpc.celestia.pops.oneandhttps://celestia-rpc.polkachu.com.- Real TIA (no faucet). Parameters (2026-10-15):
min_commission_rate0.20,max_validators100 with 96 bonded — the active set is nearly full, so the stake needed to enter it is whatever the 100th validator has at that time:curl -s "https://celestia-rest.publicnode.com/cosmos/staking/v1beta1/validators?status=BOND_STATUS_BONDED&pagination.limit=200" | jq '[.validators[].tokens | tonumber] | sort | .[0]'(utia;query staking validatorsreturns too much for public RPCs). - Public mainnet seeds die often: on 2026-10-18 all 13 seeds in
celestia-app's
scripts/mainnet.shwere dead or unusable. If the log showsCouldn't connect to any seedsor state sync finds no snapshots, take live peers from a public RPC's/net_infoand set them aspersistent_peersin$NODE_HOME/config/config.toml, then restart:curl -s https://rpc.celestia.pops.one/net_info | jq -r '.result.peers[] | "\(.node_info.id)@\(.remote_ip):\(.node_info.listen_addr | split(":") | last)"' | head -12 | paste -sd, -(12 such peers were enough for state sync from height 15,086,000). - Route B with Polkachu's mainnet snapshot
(
snapshots.polkachu.com/snapshots/celestia/, ~640 MB snapshot inside). - A separate machine or home from Mocha, its own consensus key and
operator key. A hardware-backed or offline operator key
(
--generate-only+ offline signing works) is the safer choice. For the canary the owner accepted (2026-10-20) afilekeyring on the Mac with its passphrase in~/.celestia-rs/unjail/mainnet, so that the watchdog can unjail (§8); revisit before the stake grows past canary size. - Slashing (queried 2026-10-19):
signed_blocks_window10,000,min_signed_per_window0.001,downtime_jail_duration60 s, no downtime slash, 2% double-sign slash plus tombstoning. - The v9 → v10 upgrade (celestia-app v10.4.0) is scheduled at height
15,116,658 (
curl -s https://celestia-rest.publicnode.com/signal/v1/upgrade; ~2026-10-20 03:40 UTC). Bring the M13 full node to the tip before it. - M14 is a small-stake canary (stake near the active-set minimum). It
starts only after the M13 soak and mainnet full-node run and the C1
canary gate (
MILESTONES.md); more stake waits for F1.
An AppHashMismatch alert (check-apphash.sh prints MISMATCH), or a
node that stops at one height while the network moves on, means the Rust
execution of some block differs from Go's. The node rejects every block
after it (its app hash is in the next header), so it is not at risk of
signing a fork. It cannot sign either, and it is jailed for downtime
after missing 9,990 of 10,000 blocks (about 16 hours on mainnet, with no
slash). This is a consensus finding to fix, not an outage to clear. Do
this, in order:
-
Pause the automation. The watchdog restarts a node that is behind, and the auto-updater deploys new commits. Neither must touch the node until the finding is understood:
sudo launchctl bootout system/org.celestia.celestia-rs-watchdog sudo launchctl bootout system/org.celestia.celestia-rs-auto-update
-
Stop the node (SIGTERM, a clean shutdown):
sudo launchctl bootout system/org.celestia.celestia-rs-mainnet. -
Keep the evidence. Copy, do not move, the state that produced the mismatch, before the WAL rotates and before pruning removes the height:
H=<mismatched height>; D=~/apphash-incident-$H; mkdir -p $D cp -a $NODE_HOME/data $NODE_HOME/config $D/ cp ~/Library/Logs/celestia-rs-mainnet.log $D/ ~/.celestia-rs/bin/celestia-rs-mainnet version --long >$D/version.txt packaging/check-apphash.sh http://127.0.0.1:26757 https://rpc.celestia.pops.one >$D/check-apphash.txt 2>&1
The copy holds a consensus key: keep it on the Mac, mode 700, and never start a node from it.
-
Do not reset anything. Never delete or edit
data/priv_validator_state.json, never run a rollback or anunsafe-reset-all-style reset, and never restoredata/from the copy into the home (§11.11). The sign state must stay where the node left it. -
Find the split. Reproduce the height with
celestia-rs follow(§10) from a snapshot before it, compare every tx result and store root with Go, then fix the code and pin the input as a regression test (crates/fuzz-diff/tests/regressions.jsonlor a unit test). Record the finding indocs/security.md. -
Restart on the fixed build only. Install the fixed binary (§11.10), bootstrap the node, and wait until
check-apphash.shis OK at the tip. Only then unjail and bootstrap the watchdog and auto-updater again (the watchdog unjails by itself while~/.celestia-rs/unjail/mainnetexists). The old build, if started instead, stops at the same height again.