Skip to content

Migrating from Geth to Reth

This guide helps you migrate the execution layer of your 0G Aristotle node from geth to reth, adapted to the directory structure and configuration from our Validator Node Setup guide.

This migration is optional

You are not required to move to reth to stay on the network — geth is still supported and remains a fully valid node. reth is the recommended client going forward for better performance. Do not rush it; plan it as its own maintenance window.

Why Reth?

geth (0g-geth)reth (0g-reth)
LanguageGoRust
DatabaseLevelDBMDBX
Status on 0GSupportedRecommended going forward
Sync & storageBaselineFaster sync, more efficient storage
Switching costEL data must be rebuilt from scratch

What does not change during migration:

  • The consensus layer (0gchaind) is untouched — your validator keys, consensus state, and node identity are all preserved.
  • Only the execution layer database is rebuilt.

Validator Key & Double-Sign

During migration you will stop and restart 0gchaind. Never run the same priv_validator_key.json on two machines at once — fully stop the old process before starting the new one, or you risk a double-sign slashing penalty.


Prerequisites

Before you start, make sure you have:

  • The reth binary from the latest release package (v1.0.6+)
  • Enough disk space for backup + RLP export (estimate ~2x the size of geth-home)
  • Sufficient downtime window (export + import can take several hours depending on chain height and disk speed)

Migration Steps

Step 1: Download the Reth Binary

Download the latest release which includes the reth binary:

bash
cd $HOME
wget https://github.com/0gfoundation/0gchain-Aristotle/releases/download/v1.0.6/aristotle-v1.0.6.tar.gz
tar -xvzf aristotle-v1.0.6.tar.gz

Confirm the reth binary is present in the package:

bash
ls $HOME/aristotle-v1.0.6/bin/
# Expected: 0gchaind  geth  reth

Install reth to /usr/local/bin:

bash
chmod +x $HOME/aristotle-v1.0.6/bin/reth
sudo cp $HOME/aristotle-v1.0.6/bin/reth /usr/local/bin/reth
reth --version

Step 2: Note the Current Chain Head

Do this while geth is still running, before stopping it:

bash
journalctl -u geth --no-pager | grep "Chain head was updated" | tail -1

You will see a line like:

Chain head was updated  number=36,687,726 hash=eeef21..5f4517 ...

To extract just the block number:

bash
journalctl -u geth --no-pager | grep "Chain head was updated" | tail -1 | grep -oP 'number=\K[\d,]+' | tr -d ','

Note that number down — you will need it in Step 5.


Step 3: Stop Services

Stop the CL first, then the EL. Both must be fully stopped before backing up — copying LevelDB files while geth is running risks a corrupt backup.

bash
sudo systemctl stop 0gchaind
sudo systemctl stop geth

Step 4: Back Up Existing Data

bash
BACKUP_DIR="$HOME/.0gchaind/backup-$(date +%Y%m%d-%H%M%S)"
mkdir -p $BACKUP_DIR

# CL data (small, required for rollback)
cp -r $HOME/.0gchaind/0g-home/0gchaind-home $BACKUP_DIR/0gchaind-home

# geth EL data (large — skip if disk is tight,
# but without this backup you cannot roll back to geth)
cp -r $HOME/.0gchaind/0g-home/geth-home $BACKUP_DIR/geth-home

INFO

Backing up geth-home is strongly recommended — this is the data that will be deleted in Step 6. Without it, rolling back to geth will not be possible. Backing up 0gchaind-home is optional since the CL is untouched during migration, but it serves as a safety net in case of unexpected issues.


Step 5: Export Chain Data from Geth

INFO

This process can take a long time depending on chain height and disk speed. Run it inside a tmux or screen session to avoid interruption.

Export the full chain from block 1 up to the chain head noted in Step 2. This takes a long time — run it inside a tmux or screen session.

bash
geth export \
  --datadir $HOME/.0gchaind/0g-home/geth-home \
  $HOME/.0gchaind/chain-export.rlp \
  1 <chain_head>

Replace <chain_head> with the block number from Step 2, e.g. 1 39048765

Monitor the export file size:

bash
ls -lh $HOME/.0gchaind/chain-export.rlp

Step 6: Remove the Geth Data Directory

Destructive Step

Only run this after confirming the backup in Step 3 is complete and the RLP export file exists.

bash
rm -rf $HOME/.0gchaind/0g-home/geth-home

Step 7: Initialize Reth

bash
reth init --chain $HOME/.0gchaind/geth-genesis.json \
    --datadir $HOME/.0gchaind/0g-home/reth-home

Verify the reth data directory was created:

bash
ls $HOME/.0gchaind/0g-home/reth-home/
# Expected: db/  and other reth state directories

Step 8: Trim the RLP Export (Skip the Genesis Block)

reth cannot import block 0 (genesis) from the RLP file — it conflicts with the genesis already written during reth init. Use the script below to strip block 0 from the export.

Save as $HOME/trim_export.py:

bash
cat > $HOME/trim_export.py << 'EOF'
import sys

import os
base_dir = sys.argv[2] if len(sys.argv) > 2 else os.path.join(os.path.expanduser("~"), ".0gchaind")
input_file = base_dir + "/chain-export.rlp"
output_file = base_dir + "/chain-export-from-{start}.rlp"

start_block = int(sys.argv[1]) if len(sys.argv) > 1 else 1
output_file = output_file.format(start=start_block)

print(f"Trimming blocks before {start_block}, output: {output_file}")

def read_rlp_length(f):
    first = f.read(1)
    if not first:
        return None, 0
    b = first[0]
    if b < 0xc0:
        return None, 0
    elif b <= 0xf7:
        return first, b - 0xc0
    else:
        len_bytes_count = b - 0xf7
        len_bytes = f.read(len_bytes_count)
        return first + len_bytes, int.from_bytes(len_bytes, 'big')

def get_block_number(block_data):
    offset = 0
    b = block_data[offset]
    offset += 1 if b <= 0xf7 else 1 + (b - 0xf7)
    b = block_data[offset]
    offset += 1 if b <= 0xf7 else 1 + (b - 0xf7)
    for _ in range(8):
        b = block_data[offset]
        if b <= 0x80:
            offset += 1
        elif b <= 0xb7:
            offset += 1 + (b - 0x80)
        elif b <= 0xbf:
            n = b - 0xb7
            offset += 1 + n + int.from_bytes(block_data[offset+1:offset+1+n], 'big')
        elif b <= 0xf7:
            offset += 1 + (b - 0xc0)
        else:
            n = b - 0xf7
            offset += 1 + n + int.from_bytes(block_data[offset+1:offset+1+n], 'big')
    b = block_data[offset]
    if b == 0x80: return 0
    if b < 0x80: return b
    length = b - 0x80
    return int.from_bytes(block_data[offset+1:offset+1+length], 'big')

block_count = 0
skipped = 0

with open(input_file, "rb") as fin, open(output_file, "wb") as fout:
    while True:
        header_bytes, length = read_rlp_length(fin)
        if header_bytes is None:
            break
        block_body = fin.read(length)
        if len(block_body) < length:
            break
        full_block = header_bytes + block_body
        try:
            block_number = get_block_number(full_block)
        except Exception as e:
            print(f"Warning: could not parse block at index {block_count + skipped}, writing anyway: {e}")
            fout.write(full_block)
            block_count += 1
            continue
        if block_number < start_block:
            skipped += 1
            if skipped % 100000 == 0:
                print(f"Skipped {skipped} blocks (current: {block_number})...")
        else:
            fout.write(full_block)
            block_count += 1
            if block_count % 100000 == 0:
                print(f"Written {block_count} blocks (current: {block_number})...")

print(f"Done. Skipped {skipped}, wrote {block_count} blocks to {output_file}")
EOF

Run (skip genesis only):

bash
python3 $HOME/trim_export.py 1 $HOME/.0gchaind

The output file will be named: $HOME/.0gchaind/chain-export-from-1.rlp


Step 9: Import into Reth

Run in the background — this can take several hours:

bash
mkdir -p $HOME/.0gchaind/0g-home/log

nohup reth import \
  --chain $HOME/.0gchaind/geth-genesis.json \
  --datadir $HOME/.0gchaind/0g-home/reth-home \
  $HOME/.0gchaind/chain-export-from-1.rlp \
  >> $HOME/.0gchaind/0g-home/log/reth-import.log 2>&1 &

Monitor progress:

bash
tail -f $HOME/.0gchaind/0g-home/log/reth-import.log

If the import fails mid-way with block number X does not match parent block number Y:

bash
# Check the last successfully written height
grep "latest_block" $HOME/.0gchaind/0g-home/log/reth-import.log | tail -3

# Re-trim from that height (replace <Y+1> with the correct number)
python3 $HOME/trim_export.py <Y+1> $HOME/.0gchaind

# Re-run import with the new trimmed file
nohup reth import \
  --chain $HOME/.0gchaind/geth-genesis.json \
  --datadir $HOME/.0gchaind/0g-home/reth-home \
  $HOME/.0gchaind/chain-export-from-<Y+1>.rlp \
  >> $HOME/.0gchaind/0g-home/log/reth-import.log 2>&1 &

If the import fails with mismatched block state root, this is a compatibility issue between 0G's geth fork and reth fork. Record the failing block number and state root values from the log and report to the 0G development team. Do not attempt to skip the block manually.


Step 10: Verify the Import is Complete

Once the import finishes, confirm reth's block height matches the chain head before starting 0gchaind:

bash
# Check reth block height (adjust port to match your OG_PORT, e.g. 55)
curl -s -X POST http://localhost:${OG_PORT}545 \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \
  | jq -r '.result' | xargs printf "%d\n"

# Compare against the public chain head
curl -s -X POST https://evmrpc.0g.ai \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \
  | jq -r '.result' | xargs printf "%d\n"

The two values should be equal or within a few blocks. If reth is still behind, it will catch up via P2P after 0gchaind is started.

Wait for reth to sync

Do not proceed until reth height is at or near chain head. Starting 0gchaind while reth is still syncing will cause a -38002 Invalid forkchoice state panic.


Step 11: Create the Reth Systemd Service

Remove the old geth service and create a new reth service:

bash
sudo systemctl disable geth
sudo rm /etc/systemd/system/geth.service

Create reth.service:

bash
sudo tee /etc/systemd/system/reth.service > /dev/null <<EOF
[Unit]
Description=0g Reth Node Service
After=network-online.target

[Service]
User=$USER
WorkingDirectory=$HOME/.0gchaind
ExecStart=/usr/local/bin/reth node \
  --chain $HOME/.0gchaind/geth-genesis.json \
  --http \
  --http.addr 127.0.0.1 \
  --http.port ${OG_PORT}545 \
  --http.api eth,net,admin \
  --ws \
  --ws.addr 127.0.0.1 \
  --ws.port ${OG_PORT}546 \
  --authrpc.addr 127.0.0.1 \
  --authrpc.port ${OG_PORT}551 \
  --authrpc.jwtsecret $HOME/.0gchaind/jwt.hex \
  --datadir $HOME/.0gchaind/0g-home/reth-home \
  --ipcpath $HOME/.0gchaind/0g-home/reth-home/eth-engine.ipc \
  --engine.persistence-threshold 0 \
  --engine.memory-block-buffer-target 0 \
  --port ${OG_PORT}303 \
  --bootnodes "enode://2bf74c837a98c94ad0fa8f5c58a428237d2040f9269fe622c3dbe4fef68141c28e2097d7af6ebaa041194257543dc112514238361a6498f9a38f70fd56493f96@8.221.140.134:30303" \
  --nat extip:$(curl -4 -s ifconfig.me)
Restart=always
RestartSec=3
LimitNOFILE=65535

[Install]
WantedBy=multi-user.target
EOF

Note on HTTP Host:
The service above uses 127.0.0.1 (local access only). Change to 0.0.0.0 if you need public access and have secured it with a firewall or reverse proxy.


Step 12: Update app.toml for 0gchaind

Since reth uses authrpc as its engine API (replacing the AuthPort in geth-config.toml), make sure app.toml points to the correct port:

bash
CONFIG="$HOME/.0gchaind/0g-home/0gchaind-home/config"
sed -i "s|^rpc-dial-url *=.*|rpc-dial-url = \"http://localhost:${OG_PORT}551\"|" $CONFIG/app.toml

Step 13: Start Reth, then 0gchaind

Start reth first, wait for the engine API to be ready, then start 0gchaind:

bash
sudo systemctl daemon-reload
sudo systemctl enable reth
sudo systemctl start reth

Confirm the engine API is listening before starting the CL:

bash
ss -tlnp | grep ${OG_PORT}551

Once confirmed, start 0gchaind:

bash
sudo systemctl start 0gchaind

Step 14: Verify

bash
# Check reth logs
journalctl -u reth -f

# Check 0gchaind logs
journalctl -u 0gchaind -f

# Check both at once
journalctl -u 0gchaind -u reth -f

# Check sync status
curl -s localhost:${OG_PORT}657/status | jq .result.sync_info

The node is running normally when:

  • reth logs show syncing messages or "Starting consensus engine"
  • 0gchaind logs show "Committed state" with no -38002 errors
  • catching_up: false in the sync status output

Rollback to Geth (If Migration Fails)

If the migration fails and you need to restore geth:

bash
# Stop all processes
sudo systemctl stop 0gchaind
sudo systemctl stop reth

# Restore geth data from backup
rm -rf $HOME/.0gchaind/0g-home/geth-home
cp -r $BACKUP_DIR/geth-home $HOME/.0gchaind/0g-home/geth-home

# Re-enable geth.service (from the original validator setup)
# If the service file was removed, recreate it from the validator setup guide
sudo systemctl enable geth
sudo systemctl start geth
sudo systemctl start 0gchaind

Troubleshooting

-38002 Invalid forkchoice state — reth is still syncing when 0gchaind was started. Stop 0gchaind, wait for reth to reach chain head, then restart 0gchaind.

EL and CL not communicating (engine API errors) — confirm the jwt.hex used by --chaincfg.engine.jwt-secret-path and --authrpc.jwtsecret are the same file. They must be identical.

missing priv_validator_state.json — create an empty state file:

bash
echo '{}' > $HOME/.0gchaind/0g-home/0gchaind-home/data/priv_validator_state.json

reth has no peers — this is expected at first, as reth cannot peer with geth nodes. Wait for reth-compatible peers to become available on the network. Make sure port ${OG_PORT}303 TCP/UDP is open in your firewall:

bash
sudo ufw allow ${OG_PORT}303/tcp comment 'reth_p2p_port'
sudo ufw allow ${OG_PORT}303/udp comment 'reth_p2p_port'

Import fails with mismatched block state root — this is a compatibility issue between 0G's geth and reth forks. Record the block number and state root from the log and report to the 0G development team. Do not skip the block manually.


Cleanup (Optional)

After confirming reth has been running stably for at least 24–48 hours, you can remove the temporary files:

bash
# Remove the geth binary if no longer needed
# sudo rm /usr/local/bin/geth

# Remove the RLP export files (large)
rm -f $HOME/.0gchaind/chain-export.rlp
rm -f $HOME/.0gchaind/chain-export-from-*.rlp

# Remove the backup once you're confident
# rm -rf $BACKUP_DIR

⚠️ Do not remove the backup until you are confident reth is running stably.