Upgrades are coordinated at a specific block height. Nodes running old binaries halt at that height and must be upgraded before they can continue.
For validators, the primary risk during upgrades is double-signing. This happens when more than one active validator signer uses the same validator key at the same time (or with inconsistent signer state), which can lead to slashing/jailing.
This guide is a security-first runbook for all run stacks:
systemctlcosmovisordocker- raw binary upgrades
- source-built upgrades
Upgrade heights and target versions for Mainnet/Testnet/Devnet are listed in Networks.
Follow these rules for every validator upgrade:
- One signer only: keep a single active validator signer for
priv_validator_key.json. - Stop before replace: fully stop old process/container/service before changing binaries.
- Preserve signer state: never delete or roll back
priv_validator_state.json. - Never clone signing state to a second active host.
- Verify single active instance before restart.
Do not start a second node (VM, container, backup host, or old binary) with the same validator key while your primary validator is still running. This is the most common path to double-signing and can tombstone your validator, meaning that validator key can never become an active validator again.
Use this exact sequence for validators, regardless of stack.
Use your actual node home path in commands below:
- If you run as root defaults:
~/.exrpd - If you followed hardened
systemd/cosmovisorsetup:/var/lib/exrpd/.exrpd
- Read target height/version from Networks.
- Download the target release artifact from XRPL EVM node releases.
- Use the on-chain upgrade name from
upgrade-info.jsonfor Cosmovisor folder naming. Do not assume upgrade proposal name equals release tag (for example, proposal namev10.0.0may require binaryv10.0.1orv10.0.2).
exrpd statusRecord current height and ensure your node is healthy before starting.
Stop your runtime stack and confirm the process is gone.
pgrep -fa exrpdExpected: no active validator signer exrpd process.
NODE_HOME=${NODE_HOME:-~/.exrpd}
mkdir -p ~/exrpd-upgrade-backup
cp "$NODE_HOME"/config/priv_validator_key.json ~/exrpd-upgrade-backup/
cp "$NODE_HOME"/data/priv_validator_state.json ~/exrpd-upgrade-backup/
cp "$NODE_HOME"/config/node_key.json ~/exrpd-upgrade-backup/Do not edit these files manually.
cd /tmp
TARGET_TAG=<target-tag>
wget "https://github.com/xrplevm/node/releases/download/${TARGET_TAG}/node_${TARGET_TAG#v}_Linux_amd64.tar.gz"
tar -xzf "node_${TARGET_TAG#v}_Linux_amd64.tar.gz"
sudo mv bin/exrpd /usr/local/bin/exrpd
sudo chmod +x /usr/local/bin/exrpd
exrpd versioncd /tmp
rm -rf node
git clone https://github.com/xrplevm/node.git
cd node
git checkout <target-tag>
make build
sudo mv build/exrpd /usr/local/bin/exrpd
sudo chmod +x /usr/local/bin/exrpd
exrpd versionIf you build from source, check required Go version first:
REQUIRED_GO=$(curl -fsSL https://raw.githubusercontent.com/xrplevm/node/main/go.mod | awk '/^go /{print $2; exit}')
echo "Required Go: ${REQUIRED_GO}"
go versionAfter installing the target binary and before starting the node, ensure evm-chain-id is present under [evm] in app.toml:
- Mainnet (
xrplevm_1440000-1):evm-chain-id = "1440000" - Testnet (
xrplevm_1449000-1):evm-chain-id = "1449000"
File location depends on your home path:
~/.exrpd/config/app.toml- or
/var/lib/exrpd/.exrpd/config/app.toml
Example check:
NODE_HOME=${NODE_HOME:-~/.exrpd}
awk '/^\[evm\]/{f=1;next} /^\[/{f=0} f && /^evm-chain-id/{print;found=1} END{if(!found) print "MISSING"}' "$NODE_HOME"/config/app.tomlIf evm-chain-id is missing or wrong for your network, your node can fail to participate correctly in consensus after upgrade height.
Start your runtime stack (only one instance) and follow logs.
exrpd status
curl -s localhost:26657/status | jq .result.sync_infoConfirm blocks are advancing and no upgrade halt error remains.
sudo systemctl stop exrpd
pgrep -fa exrpd
# replace /usr/local/bin/exrpd with target binary
sudo systemctl start exrpd
sudo journalctl -u exrpd -fPlace the new binary in the upgrade folder matching the on-chain upgrade name:
DAEMON_HOME=${DAEMON_HOME:-~/.exrpd}
mkdir -p "$DAEMON_HOME"/cosmovisor/upgrades/<upgrade-name>/bin
cp /usr/local/bin/exrpd "$DAEMON_HOME"/cosmovisor/upgrades/<upgrade-name>/bin/exrpd
chmod +x "$DAEMON_HOME"/cosmovisor/upgrades/<upgrade-name>/bin/exrpdIf upgrade-info.json is present, you can derive the folder name directly:
DAEMON_HOME=${DAEMON_HOME:-~/.exrpd}
UPGRADE_NAME=$(jq -r '.name // empty' "$DAEMON_HOME"/data/upgrade-info.json 2>/dev/null || true)
echo "$UPGRADE_NAME"UPGRADE_NAME is the folder to use under cosmovisor/upgrades/, even when the required release binary version differs from that name.
Then run:
sudo systemctl restart cosmovisor-exrpd
sudo journalctl -u cosmovisor-exrpd -fUse ~/.exrpd/... (not ~/.exprd/...).
docker pull peersyst/exrp:<target-tag>
docker stop xrplevm-node
docker rm xrplevm-node
docker run -d \
--name xrplevm-node \
--restart unless-stopped \
-v /root/.exrpd:/root/.exrpd \
--entrypoint exrpd \
peersyst/exrp:<target-tag> \
start
docker logs -f xrplevm-nodeDo not keep an old validator container running in parallel.
Use this section only when governance/core team announces an incident hotfix and your node/validator was already running and impacted.
If you are a fresh node setup, skip this section and follow the normal install flow for your network's current version.
- Stop all signer instances that could use the same validator key, then verify no signer is running:
sudo systemctl stop exrpd 2>/dev/null || true
sudo systemctl stop cosmovisor-exrpd 2>/dev/null || true
docker stop xrplevm-node 2>/dev/null || true
pgrep -fa exrpdExpected: no active validator signer process.
- Install the announced hotfix binary (do not start yet).
- Backup signer state:
cp ~/.exrpd/data/priv_validator_state.json ~/.exrpd/priv_validator_state.json- Restore state:
- Recommended: restore from a compatible snapshot provider (for example PolkaChu, Cumulo, or XRPL EVM snapshot S3).
- Alternative (resource-heavy):
exrpd rollback
- Apply required config changes announced with the hotfix.
If governance requires an EVM chain-id update, set it in app.toml under [evm]:
[evm]
evm-chain-id = "<network-evm-chain-id>"- Restore signer state file and permissions:
cp ~/.exrpd/priv_validator_state.json ~/.exrpd/data/priv_validator_state.json
chmod 600 ~/.exrpd/data/priv_validator_state.jsonBefore start, confirm only one host/container will be allowed to sign with this validator key.
Start exactly one runtime stack:
exrpd startNever run two active instances with the same priv_validator_key.json at the same time (primary + backup, old + new binary, systemd + docker, etc.). During hotfixes, this is the highest-risk mistake and can lead to slashing/jailing and tombstoning (permanent validator removal for that key).
During the Testnet v10 incident, affected nodes moved from v10.0.0 to v10.0.1 and required evm-chain-id = "1449000".
If restoring from a pre-upgrade snapshot with Cosmovisor, stage the required binary in the upgrade folder from upgrade-info.json before service start.
Cosmovisor can reduce manual intervention during upgrades. It can also increase operational risk if misconfigured, so validator operators should test in non-production first.
Install:
sudo apt-get update
sudo apt-get install -y golang-go
sudo env GOBIN=/usr/local/bin go install cosmossdk.io/tools/cosmovisor/cmd/cosmovisor@latest
/usr/local/bin/cosmovisor --help >/dev/nullRecommended environment variables:
export DAEMON_NAME=exrpd
export DAEMON_HOME=$HOME/.exrpd
export DAEMON_RESTART_AFTER_UPGRADE=true
export UNSAFE_SKIP_BACKUP=false
export DAEMON_ALLOW_DOWNLOAD_BINARIES=falseFor validators, prefer DAEMON_ALLOW_DOWNLOAD_BINARIES=false and stage binaries manually. Auto-download introduces an external dependency at upgrade time.
Before starting after upgrade, confirm:
- old process is fully stopped
- no second host/container is signing with same key
priv_validator_state.jsonis intact and not rolled back- target binary version is installed
- logs show normal block progression