This guide covers how to migrate an existing TFChain validator node from one machine to another without losing your validator status or causing equivocation.
- Never run two validator nodes with the same session keys at the same time. Running duplicate validators causes equivocation (signing conflicting blocks), which the network detects via
check_for_equivocationin the Aura consensus and GRANDPA'ssubmit_report_equivocation_unsigned_extrinsic. This can lead to loss of reputation and potential removal from the validator set. - The old node must be fully stopped before the new node starts validating. There must be zero overlap.
A validator migration involves:
- Preparing the new machine
- Syncing the new node (non-validator mode)
- Transferring or re-inserting session keys
- Stopping the old validator
- Starting the new node in validator mode
- Verifying the migration
- Access to both old and new machines
- The new machine meets the hardware requirements
- Your session key mnemonic (secret phrase) backed up from the original setup
- Your node key (network key) file if migrating a boot node
# Install system dependencies (not needed if using Docker)
sudo apt install make clang pkg-config libssl-dev build-essential
# Install NTP (required for validators)
# Docker containers share the host's kernel clock, so NTP must be
# configured on the host machine even when running via Docker.
sudo apt-get install ntp
timedatectl # Verify "System clock synchronized: yes"Either download the binary from a release, or build from source:
git clone https://github.com/threefoldtech/tfchain.git
cd tfchain/substrate-node
cargo build --release
# Binary is at ./target/release/tfchainOr use Docker (check the grid_deployment .env for the current recommended image version):
docker pull ghcr.io/threefoldtech/tfchain:<version>Note: The Docker examples below use
/path/to/storageas a placeholder for the host directory mounted into the container. If you are following the grid_deployment setup, use/srv/tfchain/to stay consistent with its docker-compose files and snapshot scripts.
Start the node in non-validator mode first to sync the chain without producing blocks. There are two approaches:
Start the node with --sync warp to download finality proofs and the latest state directly.
Using the binary:
./tfchain \
--base-path /storage \
--chain chainspecs/<NETWORK>/chainSpecRaw.json \
--name "YourNode-syncing" \
--blocks-pruning archive \
--state-pruning 1000 \
--sync warpUsing Docker:
docker run -d --name tfchain-sync \
--restart unless-stopped \
-v /path/to/storage:/storage \
-p 30333:30333 \
ghcr.io/threefoldtech/tfchain:<version> \
--base-path /storage \
--chain /etc/chainspecs/<NETWORK>/chainSpecRaw.json \
--name "YourNode-syncing" \
--port 30333 \
--blocks-pruning archive \
--state-pruning 1000 \
--sync warpThe grid_deployment infrastructure provides pre-built chain snapshots that can be downloaded via rsync. This is typically faster than warp sync, especially for mainnet.
# Create the database directory
mkdir -p /srv/tfchain/chains/tfchain_mainnet/db ~/grid_snapshots_tmp
# Download and extract the snapshot
cd ~/grid_snapshots_tmp
rsync -Lv --progress --partial rsync://bknd.snapshot.grid.tf:34873/gridsnapshots/tfchain-mainnet-validator-latest.tar.gz .
tar -I pigz -xf tfchain-mainnet-validator-latest.tar.gz -C /srv/tfchain/chains/tfchain_mainnet/db/
rm tfchain-mainnet-validator-latest.tar.gz
# Clean up
cd ~
rm -r ~/grid_snapshots_tmpNote:
pigzis required for parallel decompression (sudo apt install pigz). The snapshot path and name may differ per network — the example above is for mainnet. Adjust the path for other networks (e.g.,tfchain_testnet).The validator snapshot is a pruned state snapshot (
--state-pruning 1000 --blocks-pruning archive), matching the recommended validator configuration. It is not suitable for archive nodes that need full historical state.
After restoring the snapshot, start the node normally (without --sync warp) to catch up with any remaining blocks.
Replace <NETWORK> with your target network (main, test, qanet, or dev).
Monitor sync progress in the logs. Wait until the node is fully synced (best block matches finalized block on the network).
Stop the syncing node before proceeding to the next step.
TFChain validators use two session keys:
| Key Type | Scheme | Key Type ID |
|---|---|---|
| AURA | sr25519 | aura |
| GRANDPA | ed25519 | gran |
There are three ways to get session keys onto the new machine. All three result in the same key files in the keystore. Since the keys are the same, no on-chain setKeys transaction is needed after migration.
If you have the mnemonic (secret phrase) backed up from the original key generation, you can directly re-insert the same keys on the new machine. This is the cleanest and most reliable method.
The --suri parameter accepts a mnemonic phrase directly:
Using the binary:
# Insert AURA key
./tfchain key insert \
--base-path /storage \
--chain chainspecs/<NETWORK>/chainSpecRaw.json \
--key-type aura \
--suri "<your mnemonic phrase>" \
--scheme sr25519
# Insert GRANDPA key (same mnemonic, different scheme)
./tfchain key insert \
--base-path /storage \
--chain chainspecs/<NETWORK>/chainSpecRaw.json \
--key-type gran \
--suri "<your mnemonic phrase>" \
--scheme ed25519Using Docker:
# Insert AURA key
docker run --rm \
-v /path/to/storage:/storage \
ghcr.io/threefoldtech/tfchain:<version> \
key insert \
--base-path /storage \
--chain /etc/chainspecs/<NETWORK>/chainSpecRaw.json \
--key-type aura \
--suri "<your mnemonic phrase>" \
--scheme sr25519
# Insert GRANDPA key
docker run --rm \
-v /path/to/storage:/storage \
ghcr.io/threefoldtech/tfchain:<version> \
key insert \
--base-path /storage \
--chain /etc/chainspecs/<NETWORK>/chainSpecRaw.json \
--key-type gran \
--suri "<your mnemonic phrase>" \
--scheme ed25519Both AURA and GRANDPA keys are derived from the same mnemonic but use different cryptographic schemes (sr25519 vs ed25519), which produces different key pairs.
The key insert command creates key files in the keystore directory at <base-path>/chains/<chain_id>/keystore/ (or at --keystore-path if specified).
Security note: If you pass the mnemonic via an environment variable or a file (e.g.,
.secrets.envin a docker-compose setup), remove it immediately after key insertion. The mnemonic should only be stored encrypted in a password manager, never left in plaintext on disk.
You can directly copy the keystore files from the old machine to the new one. This is straightforward but requires access to the old machine's filesystem.
Keystore location (default): <base-path>/chains/<chain_id>/keystore/
For example, with --base-path /storage on mainnet: /storage/chains/tfchain_mainnet/keystore/
The chain ID depends on your network:
| Network | Chain ID |
|---|---|
| Mainnet | tfchain_mainnet |
| Testnet | tfchain_testnet |
| QA Net | tfchain_qa_net |
| Devnet | tfchain_devnet |
If using a custom --keystore-path (e.g., in Kubernetes deployments), the keys are at that path instead.
Steps:
# On the OLD machine - stop the validator first!
sudo systemctl stop tfchain # or docker stop <container>
# Copy the keystore to the new machine
scp -r /storage/chains/tfchain_mainnet/keystore/ user@new-machine:/storage/chains/tfchain_mainnet/keystore/What's inside the keystore:
Each key is stored as a single file:
- File name: hex-encoded key type ID + hex-encoded public key (no
0xprefix) - File content: the mnemonic (secret phrase)
For example, an AURA key file would be named:
61757261<hex_public_key>
Where 61757261 is the hex encoding of aura.
A GRANDPA key file would be named:
6772616E<hex_public_key>
Where 6772616E is the hex encoding of gran.
You should see exactly 2 files in the keystore (one for aura, one for gran).
If you have lost your mnemonic and cannot access the old keystore, you must generate new session keys. This requires an additional on-chain setKeys transaction.
Start the new node (temporarily with --rpc-methods Unsafe on localhost only):
./tfchain \
--base-path /storage \
--chain chainspecs/<NETWORK>/chainSpecRaw.json \
--validator \
--rpc-methods Unsafe \
--name "YourValidatorName" \
--blocks-pruning archive \
--state-pruning 1000Rotate keys via RPC:
echo '{"id":1,"jsonrpc":"2.0","method":"author_rotateKeys","params":[]}' | \
websocat -n1 -B 99999999 ws://127.0.0.1:9944The output result field contains the concatenated hex public keys for the new session keys.
Submit setKeys on-chain:
- Go to PolkadotJS Apps connected to TFChain
- Navigate to Developer -> Extrinsics
- Select your validator controller account
- Choose
session->setKeys(keys, proof) - Enter the new aura and grandpa hex public keys, proof:
0x00 - Submit the transaction
Then restart the node without --rpc-methods Unsafe.
Note: The new keys will become active after 2 sessions. Your validator may miss blocks during this transition.
This step is critical. The old validator must be completely stopped before the new one starts in validator mode.
# Using systemd
sudo systemctl stop tfchain
# Using Docker
docker stop tfchain-validatorVerify it is stopped:
# Check no tfchain process is running
ps aux | grep tfchain
# For Docker
docker ps | grep tfchainOnce the old node is stopped and session keys are in place on the new machine:
./tfchain \
--base-path /storage \
--chain chainspecs/<NETWORK>/chainSpecRaw.json \
--validator \
--node-key-file "<node_private_key_file>" \
--name "YourValidatorName" \
--telemetry-url 'wss://shard1.telemetry.tfchain.grid.tf/submit 1' \
--blocks-pruning archive \
--state-pruning 1000docker run -d --name tfchain-validator \
--restart unless-stopped \
-v /path/to/storage:/storage \
-p 30333:30333 \
-p 9615:9615 \
ghcr.io/threefoldtech/tfchain:<version> \
--base-path /storage \
--chain /etc/chainspecs/<NETWORK>/chainSpecRaw.json \
--validator \
--node-key "<node_private_key>" \
--name "YourValidatorName" \
--port 30333 \
--prometheus-external \
--telemetry-url 'wss://shard1.telemetry.tfchain.grid.tf/submit 1' \
--blocks-pruning archive \
--state-pruning 1000Consider setting up a systemd service to auto-restart the node on failure or reboot.
For docker-compose based deployments, see the grid_deployment validator guide which provides ready-made compose files and init scripts.
Look for block authoring messages in the logs. You should see your node producing blocks:
# Binary
journalctl -u tfchain -f
# Docker
docker logs -f tfchain-validatorUsing PolkadotJS Apps connected to TFChain:
- Verify your validator is in the active set: Query
validatorSet -> validatorsand confirm your controller account SS58 address is listed. - Verify session keys are registered: Query
session -> queuedKeysand confirm your account has the correct aura and grandpa public keys.
After confirming the new validator is working correctly:
# Remove the old keystore (contains mnemonics!)
rm -rf /storage/chains/<chain_id>/keystore/
# Optionally remove all chain data
rm -rf /storage/If your validator also serves as a boot node (listed in the chain spec), you must also migrate the node key (network key). The node key determines the node's PeerId, which is referenced in the chain spec's bootNodes list.
- Copy the node key file from the old machine to the new one
- Use
--node-key-fileto point to it when starting the new node - Update DNS records to point the boot node domain (e.g.,
01.bootnode.main.grid.tf) to the new machine's IP address
If you lose the node key, your node will get a new PeerId and the old boot node address in the chain spec will stop working. Other nodes will still function if other boot nodes are available, but the chain spec should be updated.
- New machine meets hardware requirements
- NTP is configured and synchronized
- TFChain binary or Docker image installed
- New node synced to latest block
- Session keys transferred (mnemonic re-insert, keystore copy, or key rotation)
- Old validator completely stopped
- New validator started with
--validatorflag - Logs show block production
- On-chain validator status confirmed
- Old machine keystore securely wiped
- (If boot node) Node key migrated and DNS updated