|
| 1 | +# Reth Backup Helper |
| 2 | + |
| 3 | +Script to snapshot the `ev-reth` MDBX database while the node keeps running and |
| 4 | +record the block height contained in the snapshot. |
| 5 | + |
| 6 | +The script supports two execution modes: |
| 7 | + |
| 8 | +- **local**: Backup a reth instance running directly on the host machine |
| 9 | +- **docker**: Backup a reth instance running in a Docker container |
| 10 | + |
| 11 | +## Prerequisites |
| 12 | + |
| 13 | +### Common requirements |
| 14 | + |
| 15 | +- The `mdbx_copy` binary available in the target environment (see [libmdbx |
| 16 | + documentation](https://libmdbx.dqdkfa.ru/)). |
| 17 | +- `jq` installed on the host to parse the JSON output. |
| 18 | + |
| 19 | +### Docker mode |
| 20 | + |
| 21 | +- Docker access to the container running `ev-reth` (defaults to the service name |
| 22 | + `ev-reth` from `docker-compose`). |
| 23 | + |
| 24 | +### Local mode |
| 25 | + |
| 26 | +- Direct filesystem access to the reth datadir. |
| 27 | +- Sufficient permissions to read the database files. |
| 28 | + |
| 29 | +## Usage |
| 30 | + |
| 31 | +### Local mode |
| 32 | + |
| 33 | +When reth is running directly on your machine: |
| 34 | + |
| 35 | +```bash |
| 36 | +./scripts/reth-backup/backup.sh \ |
| 37 | + --mode local \ |
| 38 | + --datadir /var/lib/reth \ |
| 39 | + --mdbx-copy /usr/local/bin/mdbx_copy \ |
| 40 | + /path/to/backups |
| 41 | +``` |
| 42 | + |
| 43 | +### Docker mode |
| 44 | + |
| 45 | +When reth is running in a Docker container: |
| 46 | + |
| 47 | +```bash |
| 48 | +./scripts/reth-backup/backup.sh \ |
| 49 | + --mode docker \ |
| 50 | + --container ev-reth \ |
| 51 | + --datadir /home/reth/eth-home \ |
| 52 | + --mdbx-copy /tmp/libmdbx/build/mdbx_copy \ |
| 53 | + /path/to/backups |
| 54 | +``` |
| 55 | + |
| 56 | +### Output structure |
| 57 | + |
| 58 | +Both modes create a timestamped folder under `/path/to/backups` with: |
| 59 | + |
| 60 | +- `db/mdbx.dat` – consistent MDBX snapshot. |
| 61 | +- `db/mdbx.lck` – placeholder lock file (empty). |
| 62 | +- `static_files/` – static files copied from the node. |
| 63 | +- `stage_checkpoints.json` – raw StageCheckpoints table. |
| 64 | +- `height.txt` – extracted block height (from the `Finish` stage). |
| 65 | + |
| 66 | +Additional flags: |
| 67 | + |
| 68 | +- `--tag LABEL` to override the timestamped folder name. |
| 69 | +- `--keep-remote` to leave the temporary snapshot in the target environment |
| 70 | + (useful for debugging). |
| 71 | + |
| 72 | +The script outputs the height at the end so you can coordinate other backups |
| 73 | +with the same block number. |
| 74 | + |
| 75 | +## Architecture |
| 76 | + |
| 77 | +The backup script is split into two components: |
| 78 | + |
| 79 | +- **`backup-lib.sh`**: Abstract execution layer providing a common interface for |
| 80 | + different execution modes (local, docker). This library defines functions like |
| 81 | + `exec_remote`, `copy_from_remote`, `copy_to_remote`, and `cleanup_remote` |
| 82 | + that are implemented differently for each backend. |
| 83 | +- **`backup.sh`**: Main script that uses the library and orchestrates the backup |
| 84 | + workflow. It's mode-agnostic and works with any backend that implements the |
| 85 | + required interface. |
| 86 | + |
| 87 | +This separation allows easy extension to support additional execution |
| 88 | +environments (SSH, Kubernetes, etc.) without modifying the core backup logic. |
| 89 | + |
| 90 | +## End-to-end workflow with `apps/evm/single` (Docker mode) |
| 91 | + |
| 92 | +### Prerequisites |
| 93 | + |
| 94 | +1. Build the reth image with MDBX tooling: |
| 95 | + |
| 96 | + ```bash |
| 97 | + docker build -t ghcr.io/evstack/ev-reth:latest scripts/reth-backup |
| 98 | + ``` |
| 99 | + |
| 100 | +2. Build the ev-node image with backup/restore commands: |
| 101 | + |
| 102 | + ```bash |
| 103 | + docker build -t ghcr.io/evstack/ev-node-evm-single:main -f apps/evm/single/Dockerfile . |
| 104 | + ``` |
| 105 | + |
| 106 | +3. Start the stack: |
| 107 | + |
| 108 | + ```bash |
| 109 | + cd apps/evm/single && docker compose up -d |
| 110 | + ``` |
| 111 | + |
| 112 | +### Backup |
| 113 | + |
| 114 | +1. Backup reth (captures MDBX snapshot at current height): |
| 115 | + |
| 116 | + ```bash |
| 117 | + ./scripts/reth-backup/backup.sh --mode docker backups/full-run/reth |
| 118 | + ``` |
| 119 | + |
| 120 | + Note the printed TAG (e.g., `20251013-104816`) and height. |
| 121 | + |
| 122 | +2. Backup ev-node (captures complete Badger datastore): |
| 123 | + |
| 124 | + ```bash |
| 125 | + TAG=<TAG> # from previous step |
| 126 | + HEIGHT=$(cat backups/full-run/reth/${TAG}/height.txt) |
| 127 | + |
| 128 | + mkdir -p backups/full-run/ev-node |
| 129 | + |
| 130 | + docker exec evolveevm-ev-node-evm-single-1 \ |
| 131 | + evm-single backup \ |
| 132 | + --output /tmp/backup-${TAG}.badger \ |
| 133 | + --force |
| 134 | + |
| 135 | + docker cp evolveevm-ev-node-evm-single-1:/tmp/backup-${TAG}.badger \ |
| 136 | + backups/full-run/ev-node/ |
| 137 | + |
| 138 | + echo ${HEIGHT} > backups/full-run/ev-node/target-height.txt |
| 139 | + ``` |
| 140 | + |
| 141 | +### Restore |
| 142 | + |
| 143 | +1. Stop services and recreate containers: |
| 144 | + |
| 145 | + ```bash |
| 146 | + cd apps/evm/single |
| 147 | + docker compose down |
| 148 | + docker compose up --no-start |
| 149 | + ``` |
| 150 | + |
| 151 | +2. Restore reth volume: |
| 152 | + |
| 153 | + ```bash |
| 154 | + TAG=<TAG> |
| 155 | + |
| 156 | + # From apps/evm/single directory, use relative path to backups |
| 157 | + docker run --rm \ |
| 158 | + --volumes-from ev-reth \ |
| 159 | + -v "$PWD/../../backups/full-run/reth/${TAG}:/backup:ro" \ |
| 160 | + alpine:3.18 \ |
| 161 | + sh -c 'rm -rf /home/reth/eth-home/db /home/reth/eth-home/static_files && \ |
| 162 | + mkdir -p /home/reth/eth-home/db /home/reth/eth-home/static_files && \ |
| 163 | + cp /backup/db/mdbx.dat /home/reth/eth-home/db/ && \ |
| 164 | + cp /backup/db/mdbx.lck /home/reth/eth-home/db/ && \ |
| 165 | + cp -a /backup/static_files/. /home/reth/eth-home/static_files/ || true' |
| 166 | + ``` |
| 167 | + |
| 168 | +3. Restore ev-node volume: |
| 169 | + |
| 170 | + ```bash |
| 171 | + TAG=<TAG> |
| 172 | + |
| 173 | + # From apps/evm/single directory, use relative path to backups |
| 174 | + docker run --rm \ |
| 175 | + --volumes-from evolveevm-ev-node-evm-single-1 \ |
| 176 | + -v "$PWD/../../backups/full-run/ev-node:/backup:ro" \ |
| 177 | + ghcr.io/evstack/ev-node-evm-single:main \ |
| 178 | + restore \ |
| 179 | + --input /backup/backup-${TAG}.badger \ |
| 180 | + --home /root/.evm-single \ |
| 181 | + --app-name evm-single \ |
| 182 | + --force |
| 183 | + ``` |
| 184 | + |
| 185 | +4. Align ev-node to reth height using rollback (before starting): |
| 186 | + |
| 187 | + ```bash |
| 188 | + HEIGHT=$(cat backups/full-run/ev-node/target-height.txt) |
| 189 | + |
| 190 | + docker run --rm \ |
| 191 | + --volumes-from evolveevm-ev-node-evm-single-1 \ |
| 192 | + ghcr.io/evstack/ev-node-evm-single:main \ |
| 193 | + rollback \ |
| 194 | + --home /root/.evm-single \ |
| 195 | + --height ${HEIGHT} \ |
| 196 | + --sync-node |
| 197 | + ``` |
| 198 | + |
| 199 | + > **Note:** The rollback may report errors for p2p header/data stores with invalid |
| 200 | + > ranges. This is expected and can be ignored. The main state will be correctly |
| 201 | + > rolled back to the target height. The `--sync-node` flag is required for |
| 202 | + > non-aggregator mode rollback. |
| 203 | +
|
| 204 | +5. Start reth and local-da services: |
| 205 | + |
| 206 | + ```bash |
| 207 | + docker compose start ev-reth local-da |
| 208 | + ``` |
| 209 | + |
| 210 | +6. Start ev-node with cache cleared (first time only): |
| 211 | + |
| 212 | + ```bash |
| 213 | + # Remove the stopped container and start with --evnode.clear_cache |
| 214 | + docker rm evolveevm-ev-node-evm-single-1 |
| 215 | + |
| 216 | + docker run -d \ |
| 217 | + --name evolveevm-ev-node-evm-single-1 \ |
| 218 | + --network evolveevm_evolve-network \ |
| 219 | + -p 7676:7676 -p 7331:7331 \ |
| 220 | + -v evolveevm_evm-single-data:/root/.evm-single/ \ |
| 221 | + -e EVM_ENGINE_URL=http://ev-reth:8551 \ |
| 222 | + -e EVM_ETH_URL=http://ev-reth:8545 \ |
| 223 | + -e EVM_JWT_SECRET=f747494bb0fb338a0d71f5f9fe5b5034c17cc988c229b59fd71e005ee692e9bf \ |
| 224 | + -e EVM_GENESIS_HASH=0x2b8bbb1ea1e04f9c9809b4b278a8687806edc061a356c7dbc491930d8e922503 \ |
| 225 | + -e EVM_BLOCK_TIME=1s \ |
| 226 | + -e EVM_SIGNER_PASSPHRASE=secret \ |
| 227 | + -e DA_ADDRESS=http://local-da:7980 \ |
| 228 | + ghcr.io/evstack/ev-node-evm-single:main \ |
| 229 | + start --evnode.clear_cache |
| 230 | + ``` |
| 231 | + |
| 232 | + > **Important:** Use `--evnode.clear_cache` on first start after restore to clear |
| 233 | + > any cached p2p data that may be inconsistent after rollback. On subsequent restarts, |
| 234 | + > you can use `docker compose up -d` normally. |
| 235 | +
|
| 236 | +7. Verify both nodes are at the same height: |
| 237 | + |
| 238 | + ```bash |
| 239 | + HEIGHT=$(cat backups/full-run/ev-node/target-height.txt) |
| 240 | + echo "Expected restored height: ${HEIGHT}" |
| 241 | + |
| 242 | + # Check ev-node is producing blocks from the restored height |
| 243 | + docker logs evolveevm-ev-node-evm-single-1 2>&1 | grep "produced block" | head -10 |
| 244 | + |
| 245 | + # Check reth current height |
| 246 | + docker exec ev-reth curl -s -X POST -H "Content-Type: application/json" \ |
| 247 | + --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \ |
| 248 | + http://localhost:8545 | jq -r '.result' | xargs printf "%d\n" |
| 249 | + ``` |
| 250 | + |
| 251 | +## Known Limitations |
| 252 | + |
| 253 | +### Rollback P2P Store Errors |
| 254 | + |
| 255 | +When rolling back to a height significantly lower than the current state, the p2p |
| 256 | +header and data sync stores may report "invalid range" errors. This occurs because |
| 257 | +these stores track sync progress independently. The errors can be safely ignored as: |
| 258 | + |
| 259 | +1. The main blockchain state is correctly rolled back |
| 260 | +2. Using `--evnode.clear_cache` on restart clears the inconsistent cache |
| 261 | +3. The node will resync p2p data from the restored height |
| 262 | + |
| 263 | +### Timestamp Consistency |
| 264 | + |
| 265 | +After a restore, if significant real-world time has passed since the backup was created, |
| 266 | +you may encounter timestamp validation errors when the node attempts to continue block |
| 267 | +production. This occurs because: |
| 268 | + |
| 269 | +- Reth stores block timestamps based on when blocks were originally created |
| 270 | +- After restore, the restored timestamps may be in the past relative to system time |
| 271 | +- Block validators may reject new blocks with timestamps earlier than parent blocks |
| 272 | + |
| 273 | +**Workaround:** In production environments, coordinate restore operations to minimize |
| 274 | +time between backup and restore, or ensure the entire network is restored simultaneously. |
| 275 | + |
| 276 | +## Summary |
| 277 | + |
| 278 | +This backup/restore workflow enables point-in-time recovery for both reth (MDBX) and |
| 279 | +ev-node (Badger) datastores. Key points: |
| 280 | + |
| 281 | +- **Backup**: Hot backup while nodes are running (no downtime) |
| 282 | +- **Restore**: Requires stopping services, restoring volumes, and aligning heights |
| 283 | +- **Rollback**: May show p2p store errors that can be safely ignored |
| 284 | +- **Production**: Test the full workflow in staging before deploying to production |
| 285 | + |
| 286 | +The process has been validated to correctly restore state and resume block production |
| 287 | +from the backup point, with known limitations around p2p store consistency and timestamp |
| 288 | +validation that can be mitigated with proper operational procedures. |
0 commit comments