diff --git a/utils/backup/local_backup_restore.sh b/utils/backup/local_backup_restore.sh new file mode 100644 index 0000000000..5d2aa48a83 --- /dev/null +++ b/utils/backup/local_backup_restore.sh @@ -0,0 +1,266 @@ +#!/usr/bin/env bash +# local_backup_restore.sh +# +# Restore a GenesisL1/Cosmos-style snapshot from a local .tar.lz4 file into ~/.genesis. +# Expectation: the archive contains a top-level "data/" directory. +# +# Validator safety: +# By default, this script preserves existing data/priv_validator_state.json. +# This helps avoid double-sign/slashing risk when restoring on a validator. + +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: + local_backup_restore.sh -i /path/to/data_snapshot.tar.lz4 [options] + +Required: + -i, --input FILE Path to local .tar.lz4 snapshot file + +Options: + -H, --home-dir DIR Genesis home directory (default: $HOME/.genesis) + -d, --daemon NAME Systemd service to stop/start (default: genesisd) + -s, --stop-start Stop daemon before restore and start after + -r, --remove-existing Remove existing "data/" before restore + -n, --dry-run Print what would be done, no changes + +Validator state options: + --keep-validator-state Preserve existing data/priv_validator_state.json (default) + --no-keep-validator-state + Use snapshot's priv_validator_state.json instead + + -h, --help Show this help + +Examples: + ./local_backup_restore.sh -i /mnt/backup/data_12654149.tar.lz4 + ./local_backup_restore.sh -i ./data_12654149.tar.lz4 -s -r + ./local_backup_restore.sh -i ./data_12654149.tar.lz4 -H /srv/genesis/.genesis -s -d genesisd + ./local_backup_restore.sh -i ./data_12654149.tar.lz4 -s -r --no-keep-validator-state +EOF +} + +INPUT="" +HOME_DIR="${HOME}/.genesis" +DAEMON="genesisd" +DO_STOP_START=0 +REMOVE_EXISTING=0 +DRY_RUN=0 +KEEP_VALIDATOR_STATE=1 +SYSTEMCTL=(systemctl) + +require_value() { + local flag="$1" + local value="${2:-}" + + if [[ -z "$value" ]]; then + echo "ERROR: ${flag} requires a value" >&2 + usage + exit 2 + fi +} + +quote_cmd() { + printf '%q ' "$@" +} + +run() { + if [[ "$DRY_RUN" -eq 1 ]]; then + echo "[dry-run] $(quote_cmd "$@")" + else + "$@" + fi +} + +prepare_systemctl() { + if [[ "$DO_STOP_START" -eq 0 ]]; then + return 0 + fi + + command -v systemctl >/dev/null 2>&1 || { + echo "ERROR: systemctl not found in PATH" >&2 + exit 127 + } + + if [[ "${EUID}" -eq 0 ]]; then + SYSTEMCTL=(systemctl) + return 0 + fi + + if command -v sudo >/dev/null 2>&1; then + SYSTEMCTL=(sudo systemctl) + + if [[ "$DRY_RUN" -eq 0 ]]; then + echo "==> sudo is required to manage ${DAEMON}.service" + + if ! sudo -v; then + echo "ERROR: sudo authentication failed or was cancelled." >&2 + exit 1 + fi + + echo + fi + + return 0 + fi + + echo "ERROR: stopping/starting ${DAEMON}.service requires root privileges." >&2 + echo " Install sudo, run systemctl manually, or run without -s." >&2 + exit 1 +} + +# --- args --- +while [[ $# -gt 0 ]]; do + case "$1" in + -i|--input) + require_value "$1" "${2:-}" + INPUT="$2" + shift 2 + ;; + -H|--home-dir) + require_value "$1" "${2:-}" + HOME_DIR="$2" + shift 2 + ;; + -d|--daemon) + require_value "$1" "${2:-}" + DAEMON="$2" + shift 2 + ;; + -s|--stop-start) + DO_STOP_START=1 + shift + ;; + -r|--remove-existing) + REMOVE_EXISTING=1 + shift + ;; + -n|--dry-run) + DRY_RUN=1 + shift + ;; + --keep-validator-state) + KEEP_VALIDATOR_STATE=1 + shift + ;; + --no-keep-validator-state) + KEEP_VALIDATOR_STATE=0 + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Unknown arg: $1" >&2 + usage + exit 2 + ;; + esac +done + +# --- checks --- +if [[ -z "$INPUT" ]]; then + echo "ERROR: --input is required" >&2 + usage + exit 2 +fi + +if [[ ! -f "$INPUT" ]]; then + echo "ERROR: input file not found: $INPUT" >&2 + exit 2 +fi + +command -v lz4 >/dev/null 2>&1 || { + echo "ERROR: lz4 not found in PATH" >&2 + exit 127 +} + +command -v tar >/dev/null 2>&1 || { + echo "ERROR: tar not found in PATH" >&2 + exit 127 +} + +PVS="${HOME_DIR}/data/priv_validator_state.json" +PVS_BACKUP="" + +echo "==> Restoring FULL data folder snapshot from local archive" +echo " input : ${INPUT}" +echo " home-dir : ${HOME_DIR}" +echo " daemon : ${DAEMON}" +echo " stop/start : ${DO_STOP_START}" +echo " rm data/ : ${REMOVE_EXISTING}" +echo " keep validator state : ${KEEP_VALIDATOR_STATE}" +echo + +prepare_systemctl + +run mkdir -p "$HOME_DIR" + +if [[ "$DO_STOP_START" -eq 1 ]]; then + run "${SYSTEMCTL[@]}" stop "$DAEMON" +fi + +# Preserve validator signing state before wiping/extracting. +if [[ "$KEEP_VALIDATOR_STATE" -eq 1 ]]; then + if [[ -f "$PVS" ]]; then + PVS_BACKUP="${HOME_DIR}/priv_validator_state.$(date -u +%Y%m%dT%H%M%SZ).bak.json" + + echo "==> Preserving validator signing state" + echo " from: ${PVS}" + echo " to : ${PVS_BACKUP}" + run cp -a "$PVS" "$PVS_BACKUP" + echo + else + echo "==> No existing priv_validator_state.json found to preserve" + echo " If this is a validator, make sure this is intentional." + echo + fi +else + echo "WARNING: --no-keep-validator-state selected." + echo " The snapshot's priv_validator_state.json will be used if present." + echo " Do NOT use this on a validator unless you know it is safe." + echo +fi + +if [[ "$REMOVE_EXISTING" -eq 1 ]]; then + run rm -rf "${HOME_DIR}/data" +fi + +# Sanity check without grep/head SIGPIPE causing a false warning under pipefail. +if [[ "$DRY_RUN" -eq 0 ]]; then + echo "==> Checking archive contains top-level 'data/' ..." + + listing="$(lz4 -dc "$INPUT" 2>/dev/null | tar -tf - 2>/dev/null | head -n 50 || true)" + + if ! grep -qE '^data/' <<<"$listing"; then + echo "WARNING: Could not confirm 'data/' at archive root from the first entries." >&2 + echo " If your tar stores 'data/' later, this warning can be ignored." >&2 + fi + + echo +fi + +echo "==> Extracting into ${HOME_DIR} ..." + +if [[ "$DRY_RUN" -eq 1 ]]; then + echo "[dry-run] lz4 -dc $(printf '%q' "$INPUT") | tar -xvf - -C $(printf '%q' "$HOME_DIR")" +else + lz4 -dc "$INPUT" | tar -xvf - -C "$HOME_DIR" +fi + +echo + +# Restore preserved validator signing state after snapshot extraction. +if [[ "$KEEP_VALIDATOR_STATE" -eq 1 && -n "$PVS_BACKUP" ]]; then + echo "==> Restoring preserved validator signing state" + run mkdir -p "$(dirname "$PVS")" + run mv -f "$PVS_BACKUP" "$PVS" + echo +fi + +if [[ "$DO_STOP_START" -eq 1 ]]; then + run "${SYSTEMCTL[@]}" start "$DAEMON" +fi + +echo "==> Done."