dApp Docs/节点状态同步与灾备指南
Development reference. Not independently verified for production.

MSG Chain 节点状态同步与灾难恢复指南

数据来源:MSG Chain 代码库核实

主网状态: No-Go — 当前 MSGChain 主网裁决为 No-Go,以下内容反映代码实际状态,不代表生产可用。


目录


1. 概述

1.1 为什么状态同步和灾备至关重要

区块链节点的核心职责是维护网络状态的完整副本。一旦节点数据损坏或丢失,将导致:

MSG Chain 作为基于 Cosmos SDK 构建的高性能公链,其节点状态管理涉及多层数据结构的协同工作。本指南系统性地阐述状态同步机制、备份策略、灾难恢复流程及高可用架构设计,帮助节点运维人员建立健壮的运营体系。

1.2 MSG Chain 状态存储架构

MSG Chain 的状态存储由以下组件构成:

组件 存储后端 路径 (默认) 说明
区块链状态 BadgerDB ~/.msgchain/data/ 应用层状态,键值对存储
CometBFT 共识状态 LevelDB ~/.msgchain/data/ 共识层 WAL、区块数据
键值存储 (KVStore) BadgerDB ~/.msgchain/data/badger/ Cosmos SDK 多存储实例
WASM 合约状态 BadgerDB ~/.msgchain/data/wasm/ 智能合约持久化数据
证据库 LevelDB ~/.msgchain/data/evidence.db/ 共识证据存储
交易索引 LevelDB ~/.msgchain/data/tx_index.db/ 交易查询索引

BadgerDB 是 MSG Chain 的核心存储引擎,支持:

1.3 状态同步类型

MSG Chain 节点支持以下四种状态同步机制:

类型 速度 带宽消耗 验证深度 适用场景
快照同步 极快 高 低 新节点加入、快速恢复
区块回放 慢 低 全量 对完整性要求极高的场景
节点区块同步 中 中 中 短时间掉线恢复
全量同步 (从创世) 极慢 高 完整 归档节点、审计节点

1.4 灾难场景分类

1.4.1 数据损坏

典型症状:

ERROR: failed to load database: value log corrupted at offset 12345
panic: runtime error: invalid memory address or nil pointer dereference
ERROR: failed to replay WAL: wal file corruption at seq 789

1.4.2 硬件故障

1.4.3 网络分区

1.4.4 51% 攻击

1.4.5 人为错误

1.5 恢复目标指标

指标 推荐值 说明
RTO < 30 分钟 从灾难发生到服务恢复的最大可接受时间
RPO < 1 小时 可接受的最大数据丢失时间窗口
MTBF > 90 天 系统可靠性指标
MTTR < 1 小时 从故障到完全恢复的平均时间

1.6 备份策略总览

备份层级示意图:

+--------------------------------------------------+
|                     基础层                         |
|  全量备份 (每日) + 增量备份 (每4小时)              |
+--------------------------------------------------+
                        |
                        v
+--------------------------------------------------+
|                     验证层                         |
|  备份校验 (SHA256) + 恢复测试 (每月)              |
+--------------------------------------------------+
                        |
                        v
+--------------------------------------------------+
|                     分发层                         |
|  本地存储 → 异地主存储 → 异地副本 (3-2-1 规则)   |
+--------------------------------------------------+

2. 状态同步机制

2.1 同步基础概念

Cosmos SDK 节点启动时的状态同步流程如下:

节点启动 → 加载本地数据库
    ↓
检查最后提交高度 (LatestHeight)
    ↓
若有本地状态 → 从本地高度开始同步
    ↓
若无本地状态 → 从创世开始同步 / 快照同步 / 状态同步
    ↓
P2P 网络发现对等节点 → 交换区块
    ↓
逐块验证签名 → 提交到应用层 (BeginBlock → DeliverTx → EndBlock → Commit)
    ↓
更新本地 BadgerDB 状态 → 继续下一高度

2.2 全量同步 (从创世区块)

全量同步从区块 0 开始,逐块下载、验证并执行所有交易。这是最完整的同步方式,但速度最慢。

2.2.1 初始化节点

# 初始化节点配置
msgchaind init <your-moniker> --chain-id msg-chain-1

# 下载创世文件
curl -fsSL https://github.com/msgchain/mainnet/raw/main/genesis.json \
  -o ~/.msgchain/config/genesis.json

# 验证创世文件哈希
sha256sum ~/.msgchain/config/genesis.json
# 预期输出: a1b2c3d4... 请与官方渠道公布的哈希值核对

# 添加种子节点
sed -i 's/seeds = ""/seeds = "<seed-id>@<seed-host>:26656"/' \
  ~/.msgchain/config/config.toml

# 添加持久对等节点
sed -i 's/persistent_peers = ""/persistent_peers = "<peer-id>@<peer-host>:26656"/' \
  ~/.msgchain/config/config.toml

2.2.2 启动全量同步

# 启动节点并观察同步进度
msgchaind start 2>&1 | tee ~/sync.log

# 监控同步高度 (另开终端)
watch -n 10 '
  curl -s localhost:26657/status | jq -r '\''
    "同步高度: " + .result.sync_info.latest_block_height,
    "已同步: " + (.result.sync_info.catching_up | tostring)
  '\''
'

2.2.3 全量同步耗时估算

硬件配置 区块高度 100万 区块高度 1000万
4C8G + SSD ~3 天 ~30 天
8C16G + NVMe ~1.5 天 ~15 天
16C32G + NVMe RAID ~12 小时 ~5 天

在生产环境中,全量同步仅建议用于归档节点。

2.3 快速同步 (Block Replay)

快速同步跳过交易执行的重放,仅下载并验证区块头和数据,然后提交到应用层。

2.3.1 配置快速同步

# ~/.msgchain/config/config.toml
[fastsync]
version = "v2"
allow_duplicate_ip = true

2.3.2 执行快速同步

# 设置快速同步模式并启动
msgchaind start --fast-sync

# 或通过环境变量
MSGCHAIND_FAST_SYNC=1 msgchaind start

# 监控同步进度
curl -s localhost:26657/status | jq '.result.sync_info'

快速同步的局限性:

2.4 信任同步 (Trust Sync)

信任同步允许节点从指定的信任高度和区块哈希开始同步,无需逐块验证整个历史。

2.4.1 信任同步原理

1. 运维人员从可信源获取 {高度, 区块哈希}
2. 节点下载该高度的区块头
3. 对比区块头哈希与信任哈希
4. 若匹配,信任该区块之前的所有区块
5. 从该高度开始同步后续区块(验证签名)

2.4.2 获取信任点和哈希

# 方法一:从已同步的节点获取
curl -s localhost:26657/status | jq '.result.sync_info.latest_block_height'
curl -s localhost:26657/block?height=<height> | jq -r '.result.block_id.hash'

# 方法二:从区块浏览器获取
# URL: https://scan.msgchain.org/block/<height>

# 方法三:从多个对等节点交叉验证
PEERS=("node1.msgchain.org" "node2.msgchain.org" "node3.msgchain.org")
for peer in "${PEERS[@]}"; do
  echo "=== $peer ==="
  curl -s "http://$peer:26657/status" | jq '.result.sync_info.latest_block_height'
  curl -s "http://$peer:26657/block?height=<height>" | jq -r '.result.block_id.hash'
done

2.4.3 配置信任同步

# 停止节点
systemctl stop msgchaind

# 设置信任高度和哈希
TRUST_HEIGHT=5000000
TRUST_HASH="A1B2C3D4E5F6..."

# 写入配置
sed -i 's/trust_height = 0/trust_height = 5000000/' ~/.msgchain/config/config.toml
sed -i 's/trust_hash = ""/trust_hash = "A1B2C3D4E5F6..."/' ~/.msgchain/config/config.toml

2.4.4 启动信任同步

# 使用 --trust-sync 标志启动
msgchaind start --trust-sync --trust-height 5000000 --trust-hash "A1B2C3D4E5F6..."

# 监控信任同步进度
watch -n 5 '
  STATUS=$(curl -s localhost:26657/status)
  echo "信任高度: 5000000"
  echo "当前高度: $(echo $STATUS | jq -r '\''.result.sync_info.latest_block_height'\'')"
  echo "追赶上: $(echo $STATUS | jq -r '\''.result.sync_info.catching_up'\'')"
'

2.5 快照同步 (Snapshot Sync)

快照同步是 MSG Chain 节点初始化或恢复的最快方式。节点从快照存档中直接恢复应用状态,然后从快照高度开始同步后续区块。

2.5.1 创建快照

# 基本快照创建
msgchaind snapshot create --height 1000000

# 指定输出目录
msgchaind snapshot create \
  --height 1000000 \
  --output-dir /data/snapshots

# 带压缩的快照
msgchaind snapshot create \
  --height 1000000 \
  --compress \
  --compress-algorithm zstd

# 跳过指定模块
msgchaind snapshot create \
  --height 1000000 \
  --skip-modules "auth,bank,staking"

# 创建快照并验证
msgchaind snapshot create \
  --height 1000000 \
  --verify \
  --output-dir /data/snapshots

echo "快照创建完成: $(ls -lh /data/snapshots/latest/)"

2.5.2 快照导出为可传输格式

# 将快照导出为 tar 存档
SNAPSHOT_DIR=$(ls -td ~/.msgchain/data/snapshots/*/ | head -1)

tar -I lz4 -cf /tmp/msgchain-snapshot-1000000.tar.lz4 \
  -C "$SNAPSHOT_DIR" .

# 计算校验和
sha256sum /tmp/msgchain-snapshot-1000000.tar.lz4 \
  > /tmp/msgchain-snapshot-1000000.tar.lz4.sha256

# 上传到快照服务器
scp /tmp/msgchain-snapshot-1000000.tar.lz4* \
  user@snapshots.msgchain.org:/var/www/snapshots/

2.5.3 从快照恢复

方法一:使用内置快照恢复

# 停止节点
systemctl stop msgchaind

# 清除旧数据 (谨慎!确保已备份)
rm -rf ~/.msgchain/data/

# 启动节点并指定快照高度
msgchaind start --snapshot-height 1000000

方法二:手动从存档恢复

# 停止节点
systemctl stop msgchaind

# 备份当前数据
mv ~/.msgchain/data ~/.msgchain/data_old_$(date +%Y%m%d)

# 创建数据目录
mkdir -p ~/.msgchain/data/

# 下载快照
wget -O /tmp/snapshot.tar.lz4 \
  https://snapshots.msgchain.org/msg-chain-1_1000000.tar.lz4

# 验证校验和
echo "<expected-sha256>  /tmp/snapshot.tar.lz4" | sha256sum -c -
if [ $? -ne 0 ]; then
  echo "错误: 快照校验和不匹配!"
  exit 1
fi

# 解压快照到数据目录
tar -I lz4 -xvf /tmp/snapshot.tar.lz4 \
  -C ~/.msgchain/data/

# 重置 CometBFT 数据
msgchaind tendermint unsafe-reset-all --keep-addr-book

# 启动节点
systemctl start msgchaind

# 检查日志
journalctl -u msgchaind -f -n 50

2.5.4 自动化快照脚本

#!/bin/bash
# 脚本: create_msgchain_snapshot.sh
# 功能: 自动创建 MSG Chain 节点快照并上传到快照服务器
# 用法: ./create_msgchain_snapshot.sh [高度] [输出目录]

set -euo pipefail

CHAIN_ID="msg-chain-1"
NODE_HOME="${HOME}/.msgchain"
SNAPSHOT_DIR="${2:-/data/snapshots}"
SSH_USER="snapshot-upload"
SSH_HOST="snapshots.msgchain.org"
SSH_PATH="/var/www/snapshots/"
RETENTION_DAYS=14

log_info()  { echo "[INFO]  $(date '+%Y-%m-%d %H:%M:%S')  $*"; }
log_error() { echo "[ERROR] $(date '+%Y-%m-%d %H:%M:%S')  $*" >&2; }

check_prereqs() {
  local cmds=("msgchaind" "tar" "lz4" "sha256sum" "scp" "curl")
  for cmd in "${cmds[@]}"; do
    if ! command -v "$cmd" &>/dev/null; then
      log_error "命令未找到: $cmd"
      exit 1
    fi
  done
}

get_target_height() {
  if [ -n "${1:-}" ]; then
    echo "$1"
  else
    local current
    current=$(curl -s localhost:26657/status \
      | jq -r '.result.sync_info.latest_block_height')
    echo $((current - 100))
  fi
}

cleanup_old_snapshots() {
  log_info "清理 ${RETENTION_DAYS} 天前的旧快照..."
  find "$SNAPSHOT_DIR" -name "*.tar.lz4" -mtime +${RETENTION_DAYS} -delete
  find "$SNAPSHOT_DIR" -name "*.sha256" -mtime +${RETENTION_DAYS} -delete
}

create_snapshot() {
  local height="$1"
  log_info "等待节点高度达到 ${height}..."
  while true; do
    local current_height
    current_height=$(curl -s localhost:26657/status \
      | jq -r '.result.sync_info.latest_block_height')
    if [ "$current_height" -ge "$height" ]; then
      break
    fi
    sleep 10
  done
  log_info "等待 ${height} 高度完成确认..."
  while true; do
    local current_height
    current_height=$(curl -s localhost:26657/status \
      | jq -r '.result.sync_info.latest_block_height')
    if [ "$current_height" -ge $((height + 3)) ]; then
      break
    fi
    sleep 5
  done
  log_info "创建快照: 高度 ${height}..."
  msgchaind snapshot create \
    --height "$height" \
    --output-dir "$SNAPSHOT_DIR" \
    --compress \
    --compress-algorithm zstd
  log_info "快照创建完成!"
}

export_snapshot() {
  local height="$1"
  local latest_snapshot
  latest_snapshot=$(ls -td "$SNAPSHOT_DIR"/*/ 2>/dev/null | head -1)
  if [ -z "$latest_snapshot" ]; then
    log_error "未找到快照目录!"
    exit 1
  fi
  local export_file="${SNAPSHOT_DIR}/${CHAIN_ID}_${height}.tar.lz4"
  log_info "导出快照到 ${export_file}..."
  tar -I lz4 -cf "$export_file" -C "$latest_snapshot" .
  log_info "计算校验和..."
  sha256sum "$export_file" > "${export_file}.sha256"
  log_info "导出完成!"
  ls -lh "$export_file" "${export_file}.sha256"
}

upload_snapshot() {
  local height="$1"
  local export_file="${SNAPSHOT_DIR}/${CHAIN_ID}_${height}.tar.lz4"
  if [ ! -f "$export_file" ]; then
    log_error "导出文件不存在: $export_file"
    exit 1
  fi
  log_info "上传快照到 ${SSH_HOST}:${SSH_PATH}..."
  scp "${export_file}" "${export_file}.sha256" \
    "${SSH_USER}@${SSH_HOST}:${SSH_PATH}"
  ssh "${SSH_USER}@${SSH_HOST}" \
    "ln -sf ${CHAIN_ID}_${height}.tar.lz4 ${SSH_PATH}latest.tar.lz4 && \
     ln -sf ${CHAIN_ID}_${height}.tar.lz4.sha256 ${SSH_PATH}latest.tar.lz4.sha256"
  log_info "上传完成!"
}

main() {
  check_prereqs
  local target_height
  target_height=$(get_target_height "$@")
  log_info "目标快照高度: ${target_height}"
  create_snapshot "$target_height"
  export_snapshot "$target_height"
  cleanup_old_snapshots
  log_info "快照创建流程全部完成!"
}

main "$@"

2.5.5 快照提供者配置

# 快照提供者的 config.toml 配置
[statesync]
enable = true
rpc_servers = "tcp://0.0.0.0:26657,tcp://0.0.0.0:26657"
trust_height = 0
trust_hash = ""
trust_period = "168h0m0s"

# 快照保留策略 (app.toml)
[state-sync]
snapshot-interval = 5000
snapshot-keep-recent = 10
snapshot-min-chunk-size = 4

2.5.6 快照恢复完整脚本

#!/bin/bash
# 脚本: restore_from_snapshot.sh
# 功能: 从快照完全恢复 MSG Chain 节点
# 用法: ./restore_from_snapshot.sh <snapshot_url>

set -euo pipefail

SNAPSHOT_URL="${1:-}"
NODE_HOME="${HOME}/.msgchain"
TEMP_DIR="/tmp/msgchain-restore-$(date +%s)"

log_info()  { echo "[INFO]  $(date '+%Y-%m-%d %H:%M:%S')  $*"; }
log_error() { echo "[ERROR] $(date '+%Y-%m-%d %H:%M:%S')  $*" >&2; }

if [ -z "$SNAPSHOT_URL" ]; then
  log_error "用法: $0 <snapshot_url>"
  exit 1
fi

for cmd in "msgchaind" "lz4" "tar" "wget" "sha256sum" "systemctl"; do
  if ! command -v "$cmd" &>/dev/null; then
    log_error "命令未找到: $cmd"
    exit 1
  fi
done

log_info "========== 快照恢复开始 =========="
log_info "快照地址: ${SNAPSHOT_URL}"
mkdir -p "$TEMP_DIR"

log_info "下载快照..."
SNAPSHOT_FILE="${TEMP_DIR}/snapshot.tar.lz4"
wget -O "$SNAPSHOT_FILE" "$SNAPSHOT_URL"

SHA_URL="${SNAPSHOT_URL}.sha256"
if wget -O "${TEMP_DIR}/snapshot.sha256" "$SHA_URL" 2>/dev/null; then
  log_info "验证快照完整性..."
  cd "$TEMP_DIR"
  sha256sum -c snapshot.sha256 || {
    log_error "快照校验和不匹配!"
    exit 1
  }
  cd - > /dev/null
fi

log_info "停止 msgchaind 服务..."
systemctl stop msgchaind || true
sleep 3

if [ -d "${NODE_HOME}/data" ]; then
  BACKUP_DIR="${NODE_HOME}/data_backup_$(date +%Y%m%d_%H%M%S)"
  log_info "备份当前数据到 ${BACKUP_DIR}..."
  mv "${NODE_HOME}/data" "$BACKUP_DIR"
fi

mkdir -p "${NODE_HOME}/data"

log_info "解压快照..."
if command -v pv &>/dev/null; then
  pv "$SNAPSHOT_FILE" | tar -I lz4 -xf - -C "${NODE_HOME}/data/"
else
  tar -I lz4 -xf "$SNAPSHOT_FILE" -C "${NODE_HOME}/data/"
fi

log_info "重置 CometBFT 数据..."
msgchaind tendermint unsafe-reset-all --keep-addr-book

log_info "验证创世文件..."
msgchaind validate-genesis || {
  log_error "创世文件验证失败"
  exit 1
}

log_info "启动 msgchaind 服务..."
systemctl start msgchaind

sleep 5
for i in $(seq 1 12); do
  STATUS=$(curl -s localhost:26657/status 2>/dev/null || true)
  if [ -n "$STATUS" ]; then
    HEIGHT=$(echo "$STATUS" | jq -r '.result.sync_info.latest_block_height')
    CATCHING_UP=$(echo "$STATUS" | jq -r '.result.sync_info.catching_up')
    log_info "高度: ${HEIGHT}, 追赶上: ${CATCHING_UP}"
    break
  fi
  sleep 10
done

log_info "清理临时文件..."
rm -rf "$TEMP_DIR"
log_info "========== 快照恢复完成 =========="

2.6 配置状态同步参数

# ~/.msgchain/config/config.toml

[p2p]
max_num_inbound_peers = 60
max_num_outbound_peers = 40
pex = true
persistent_peers_max_dial_period = "30s"
seeds = "seed1.msgchain.org:26656,seed2.msgchain.org:26656"
persistent_peers = "node1.msgchain.org:26656,node2.msgchain.org:26656"
addr_book_file = "/home/msgchain/.msgchain/config/addrbook.json"
addr_book_strict = true
max_connections = 100

[fastsync]
version = "v2"

[blocksync]
enable = true

[statesync]
enable = true
rpc_servers = "https://rpc.msgchain.org:443,https://rpc2.msgchain.org:443"
trust_height = 0
trust_hash = ""
trust_period = "168h0m0s"
discovery_time = "5s"

2.7 P2P 网络调优

# Linux 系统级网络优化 (需 root)

cat >> /etc/security/limits.conf << 'EOF'
msgchain    soft    nofile    65536
msgchain    hard    nofile    65536
EOF

cat >> /etc/sysctl.d/90-msgchain.conf << 'EOF'
net.netfilter.nf_conntrack_max = 1048576
net.core.rmem_max = 134217728
net.core.wmem_max = 134217728
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr
net.ipv4.tcp_keepalive_time = 600
net.ipv4.tcp_keepalive_intvl = 60
net.ipv4.tcp_keepalive_probes = 5
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_max_syn_backlog = 65536
net.core.somaxconn = 65536
net.ipv4.ip_local_port_range = 10240 65535
EOF

sysctl -p /etc/sysctl.d/90-msgchain.conf
lsmod | grep bbr
sysctl net.ipv4.tcp_congestion_control

2.8 同步状态诊断

#!/bin/bash
# 脚本: check_sync_status.sh
# 功能: 全面诊断 MSG Chain 节点同步状态

set -euo pipefail

RPC_ENDPOINT="${1:-http://localhost:26657}"

echo "=========================================="
echo " MSG Chain 节点同步状态诊断报告"
echo " 时间: $(date '+%Y-%m-%d %H:%M:%S')"
echo "=========================================="

echo "--- 节点基本信息 ---"
STATUS=$(curl -sf "${RPC_ENDPOINT}/status" | jq '.result')
echo "$STATUS" | jq '{node_id: .node_info.id, moniker: .node_info.moniker, network: .node_info.network, version: .node_info.version}'

echo "--- 同步信息 ---"
echo "$STATUS" | jq '{latest_block_height, latest_block_time, catching_up}'

echo "--- 对等节点 ---"
NET_INFO=$(curl -sf "${RPC_ENDPOINT}/net_info" | jq '.result')
echo "已连接对等节点数: $(echo "$NET_INFO" | jq '.n_peers')"
echo "$NET_INFO" | jq '.peers[:5][] | {node_id: .node_info.id, remote_ip: .remote_ip, is_outbound: .is_outbound}'

echo "--- 共识状态 ---"
curl -sf "${RPC_ENDPOINT}/consensus_state" | jq '.result.round_state | {height: .height, round: .round, step: .step}' 2>/dev/null || echo "N/A"

echo "--- 磁盘使用 ---"
DATA_DIR="${HOME}/.msgchain/data"
if [ -d "$DATA_DIR" ]; then
  du -sh "$DATA_DIR" 2>/dev/null
  df -h "$DATA_DIR" 2>/dev/null | tail -1
fi
echo "=========================================="

3. 数据备份策略

3.1 备份策略设计原则

3.1.1 3-2-1 备份规则

3 份副本 → 原始数据 + 本地备份 + 异地备份
2 种介质 → 本地磁盘 + 远程存储 (S3/NFS)
1 个异地 → 至少一份备份在不同物理位置

3.1.2 RPO/RTO 目标

备份类型 RPO RTO 频率
全量备份 24 小时 2 小时 每日
增量备份 4 小时 30 分钟 每 4 小时
实时备份 5 分钟 5 分钟 持续

3.2 全量冷备份

#!/bin/bash
# 脚本: full_backup_cold.sh
# 功能: MSG Chain 全量冷备份 (停止节点)
# 调度: cron 每日执行

set -euo pipefail

CHAIN_ID="msg-chain-1"
NODE_HOME="${HOME}/.msgchain"
BACKUP_BASE="/backup/msgchain"
RETENTION_DAYS=30
LOG_FILE="/var/log/msgchain/backup.log"
COMPRESSION_THREADS=$(nproc)
DATE_STAMP=$(date +%Y%m%d)
TIME_STAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="${BACKUP_BASE}/full/${DATE_STAMP}"

log_info()  { echo "[INFO]  $(date '+%Y-%m-%d %H:%M:%S')  $*" | tee -a "$LOG_FILE"; }
log_error() { echo "[ERROR] $(date '+%Y-%m-%d %H:%M:%S')  $*" | tee -a "$LOG_FILE" >&2; }

check_prerequisites() {
  DATA_SIZE=$(du -sb "$NODE_HOME/data" 2>/dev/null | cut -f1)
  DATA_SIZE=${DATA_SIZE:-0}
  BACKUP_AVAILABLE=$(df --output=avail "$BACKUP_BASE" 2>/dev/null | tail -1)
  BACKUP_AVAILABLE=${BACKUP_AVAILABLE:-0}
  BACKUP_AVAILABLE_BYTES=$((BACKUP_AVAILABLE * 1024))
  if [ "$BACKUP_AVAILABLE_BYTES" -lt "$((DATA_SIZE * 2))" ]; then
    log_error "磁盘空间不足!"
    exit 1
  fi
}

perform_cold_backup() {
  log_info "停止 msgchaind 服务..."
  systemctl stop msgchaind
  sleep 5

  mkdir -p "$BACKUP_DIR"

  log_info "压缩数据目录..."
  tar -I "zstd -T${COMPRESSION_THREADS}" \
    -cf "${BACKUP_DIR}/data.tar.zst" \
    -C "$NODE_HOME" data/

  log_info "备份配置目录..."
  tar -I zstd \
    -cf "${BACKUP_DIR}/config.tar.zst" \
    -C "$NODE_HOME" config/

  log_info "备份密钥..."
  tar -I zstd \
    -cf "${BACKUP_DIR}/keys.tar.zst" \
    -C "$NODE_HOME" keyring-file/

  cat > "${BACKUP_DIR}/backup_metadata.json" << EOF
{
  "chain_id": "${CHAIN_ID}",
  "timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
  "backup_type": "full_cold",
  "hostname": "$(hostname)"
}
EOF

  cd "$BACKUP_DIR"
  sha256sum *.tar.zst > checksums.sha256
  cd - > /dev/null

  log_info "冷备份完成!"
}

verify_backup() {
  log_info "验证备份完整性..."
  cd "$BACKUP_DIR"
  sha256sum -c checksums.sha256 || { log_error "校验失败"; return 1; }
  cd - > /dev/null
  tar -I zstd -tf "${BACKUP_DIR}/data.tar.zst" 2>/dev/null | head -5 || { log_error "无法读取"; return 1; }
  log_info "备份验证通过!"
  return 0
}

start_node() {
  log_info "启动 msgchaind 服务..."
  systemctl start msgchaind
  for i in $(seq 1 30); do
    if curl -sf localhost:26657/health > /dev/null 2>&1; then
      log_info "节点正常运行!"
      return 0
    fi
    sleep 2
  done
  log_error "节点启动超时"
  return 1
}

cleanup_old_backups() {
  log_info "清理超过 ${RETENTION_DAYS} 天的旧备份..."
  find "$BACKUP_BASE/full" -maxdepth 1 -type d -mtime +${RETENTION_DAYS} \
    -exec rm -rf {} \; -print 2>/dev/null
}

main() {
  log_info "========== 开始全量冷备份 =========="
  check_prerequisites
  perform_cold_backup
  verify_backup
  start_node
  cleanup_old_backups
  log_info "========== 备份完成 =========="
}

main "$@"

3.3 热备份 (节点运行中)

#!/bin/bash
# 脚本: hot_backup_badger.sh
# 功能: MSG Chain 热备份 (节点运行时)

set -euo pipefail

NODE_HOME="${HOME}/.msgchain"
BACKUP_DIR="/backup/msgchain/hot"
DATE_STAMP=$(date +%Y%m%d_%H%M%S)
BAK_DIR="${BACKUP_DIR}/${DATE_STAMP}"
LOG_FILE="/var/log/msgchain/hot_backup.log"

log_info()  { echo "[INFO]  $(date '+%Y-%m-%d %H:%M:%S')  $*" | tee -a "$LOG_FILE"; }
log_error() { echo "[ERROR] $(date '+%Y-%m-%d %H:%M:%S')  $*" | tee -a "$LOG_FILE" >&2; }

backup_leveldb() {
  log_info "备份 LevelDB 数据库..."
  mkdir -p "$BAK_DIR/leveldb"
  for db in evidence.db tx_index.db blockstore.db state.db; do
    if [ -d "${NODE_HOME}/data/${db}" ]; then
      cp -r "${NODE_HOME}/data/${db}" "$BAK_DIR/leveldb/"
    fi
  done
  log_info "LevelDB 备份完成"
}

backup_config_and_keys() {
  log_info "备份配置和密钥..."
  mkdir -p "$BAK_DIR/config"
  cp "${NODE_HOME}/config/"*.toml "$BAK_DIR/config/" 2>/dev/null || true
  cp "${NODE_HOME}/config/genesis.json" "$BAK_DIR/config/" 2>/dev/null || true
  log_info "配置备份完成"
}

compress_backup() {
  log_info "压缩热备份..."
  local archive="${BAK_DIR}.tar.zst"
  tar -I "zstd -T$(nproc)" -cf "$archive" -C "$BACKUP_DIR" "$DATE_STAMP"
  sha256sum "$archive" > "${archive}.sha256"
  rm -rf "$BAK_DIR"
  log_info "压缩完成: $(ls -lh $archive)"
}

verify_compressed_backup() {
  log_info "验证压缩备份..."
  local archive="${BAK_DIR}.tar.zst"
  cd "$BACKUP_DIR"
  sha256sum -c "${archive}.sha256" || { log_error "校验失败"; return 1; }
  tar -I zstd -tf "$archive" > /dev/null 2>&1 || { log_error "格式错误"; return 1; }
  log_info "验证通过"
  return 0
}

main() {
  log_info "========== 开始热备份 =========="
  backup_leveldb
  backup_config_and_keys
  compress_backup
  verify_compressed_backup
  log_info "========== 热备份完成 =========="
}

main "$@"

3.4 增量备份

#!/bin/bash
# 脚本: incremental_backup.sh
# 功能: MSG Chain 增量备份 (使用 rsync)
# 调度: cron 每 4 小时

set -euo pipefail

NODE_HOME="${HOME}/.msgchain"
BACKUP_BASE="/backup/msgchain/incremental"
SNAPSHOT_DIR="${BACKUP_BASE}/snapshot"
LAST_SNAPSHOT="${BACKUP_BASE}/last_snapshot"
CURRENT_BACKUP="${BACKUP_BASE}/current"
DATE_STAMP=$(date +%Y%m%d_%H%M%S)
LOG_FILE="/var/log/msgchain/incremental_backup.log"

log_info()  { echo "[INFO]  $(date '+%Y-%m-%d %H:%M:%S')  $*" | tee -a "$LOG_FILE"; }
log_error() { echo "[ERROR] $(date '+%Y-%m-%d %H:%M:%S')  $*" | tee -a "$LOG_FILE" >&2; }

mkdir -p "$BACKUP_BASE" "$SNAPSHOT_DIR"

CURRENT_HEIGHT=$(curl -sf localhost:26657/status \
  | jq -r '.result.sync_info.latest_block_height' 2>/dev/null || echo "0")

log_info "开始增量备份 (高度: ${CURRENT_HEIGHT})..."

rsync_common=(
  -avz --delete --progress
  --link-dest="$LAST_SNAPSHOT"
  --exclude="snapshots/*"
  --exclude="*.wal"
  --exclude="LOCK"
)

rsync "${rsync_common[@]}" \
  "${NODE_HOME}/data/" \
  "${CURRENT_BACKUP}/"

rm -f "$LAST_SNAPSHOT"
ln -s "$CURRENT_BACKUP" "$LAST_SNAPSHOT"

echo "$DATE_STAMP" > "${CURRENT_BACKUP}/.backup_timestamp"
echo "$CURRENT_HEIGHT" > "${CURRENT_BACKUP}/.backup_height"

BACKUP_SIZE=$(du -sh "$CURRENT_BACKUP" 2>/dev/null | cut -f1)
log_info "增量备份完成! 大小: ${BACKUP_SIZE}"

3.5 BadgerDB 数据库级修复

#!/bin/bash
# 脚本: badger_repair.sh
# 功能: 修复损坏的 BadgerDB 数据库

set -euo pipefail

NODE_HOME="${HOME}/.msgchain"
BADGER_DIR="${NODE_HOME}/data/badger"
BACKUP_DIR="${NODE_HOME}/data_backup_$(date +%Y%m%d_%H%M%S)"
LOG_FILE="/var/log/msgchain/badger_repair.log"

log_info()  { echo "[INFO]  $(date '+%Y-%m-%d %H:%M:%S')  $*" | tee -a "$LOG_FILE"; }
log_error() { echo "[ERROR] $(date '+%Y-%m-%d %H:%M:%S')  $*" | tee -a "$LOG_FILE" >&2; }

if [ ! -d "$BADGER_DIR" ]; then
  log_error "BadgerDB 目录不存在: ${BADGER_DIR}"
  exit 1
fi

log_info "备份当前数据到 ${BACKUP_DIR}..."
cp -r "$BADGER_DIR" "$BACKUP_DIR"

log_info "尝试 msgchaind 内置修复..."
msgchaind debug badger-repair --data-dir "$BADGER_DIR" 2>&1 | tee -a "$LOG_FILE" || true

if command -v badger &>/dev/null; then
  log_info "运行 badger 工具修复..."
  badger repair --dir "$BADGER_DIR" 2>&1 | tee -a "$LOG_FILE" || true
fi

log_info "检查 Value Log 文件..."
VLOG_FILES=$(find "$BADGER_DIR" -name "*.vlog" 2>/dev/null)
if [ -n "$VLOG_FILES" ]; then
  for vlog in $VLOG_FILES; do
    log_info "文件: $vlog ($(ls -lh "$vlog" | awk '{print $5}'))"
  done
fi

log_info "检查 SST 文件..."
SST_FILES=$(find "$BADGER_DIR" -name "*.sst" 2>/dev/null)
SST_COUNT=$(echo "$SST_FILES" | wc -l)
log_info "找到 ${SST_COUNT} 个 SST 文件"

log_info "尝试验证数据库完整性..."
msgchaind debug badger-verify --data-dir "$BADGER_DIR" 2>&1 | tee -a "$LOG_FILE" || {
  log_error "数据库验证失败,建议从备份恢复"
  exit 1
}

log_info "数据库修复完成!"

3.6 备份验证

#!/bin/bash
# 脚本: verify_backup.sh
# 功能: 验证 MSG Chain 备份完整性

set -euo pipefail

BACKUP_PATH="${1:-}"
VERIFY_DIR="/tmp/msgchain-verify-$(date +%s)"
LOG_FILE="/var/log/msgchain/backup_verify.log"

log_info()  { echo "[INFO]  $(date '+%Y-%m-%d %H:%M:%S')  $*" | tee -a "$LOG_FILE"; }
log_error() { echo "[ERROR] $(date '+%Y-%m-%d %H:%M:%S')  $*" | tee -a "$LOG_FILE" >&2; }

if [ -z "$BACKUP_PATH" ]; then
  log_error "用法: $0 <backup_path>"
  exit 1
fi

BACKUP_SIZE=$(du -h "$BACKUP_PATH" | cut -f1)
log_info "备份文件: ${BACKUP_PATH} (${BACKUP_SIZE})"

# 校验和验证
SHA_FILE="${BACKUP_PATH}.sha256"
if [ -f "$SHA_FILE" ]; then
  log_info "验证 SHA256 校验和..."
  cd "$(dirname "$BACKUP_PATH")"
  sha256sum -c "$SHA_FILE" || { log_error "校验失败"; exit 1; }
  cd - > /dev/null
fi

# 格式验证
log_info "验证文件格式..."
case "$BACKUP_PATH" in
  *.tar.zst) tar -I zstd -tf "$BACKUP_PATH" > /dev/null 2>&1 || { log_error "ZSTD验证失败"; exit 1; } ;;
  *.tar.lz4) tar -I lz4 -tf "$BACKUP_PATH" > /dev/null 2>&1 || { log_error "LZ4验证失败"; exit 1; } ;;
  *.tar.gz)  tar -tzf "$BACKUP_PATH" > /dev/null 2>&1 || { log_error "GZIP验证失败"; exit 1; } ;;
esac

# 内容抽查
mkdir -p "$VERIFY_DIR"
case "$BACKUP_PATH" in
  *.tar.zst) tar -I zstd -xf "$BACKUP_PATH" -C "$VERIFY_DIR" ;;esac

ls -la "$VERIFY_DIR"
for dir in "data/badger" "data/blockstore.db" "data/state.db"; do
  [ -d "${VERIFY_DIR}/${dir}" ] && log_info "✓ $dir" || log_info "? $dir"
done

rm -rf "$VERIFY_DIR"
log_info "========== 验证完成 =========="

3.7 异地备份 (Offsite)

#!/bin/bash
# 脚本: offsite_backup_sync.sh
# 功能: 将本地备份同步到异地存储

set -euo pipefail

BACKUP_SOURCE="${1:-/backup/msgchain}"
LOG_FILE="/var/log/msgchain/offsite_sync.log"
SYNC_METHOD="${SYNC_METHOD:-s3}"
S3_BUCKET="${S3_BUCKET:-msgchain-backup}"
S3_PREFIX="${S3_PREFIX:-backups/}"
SFTP_HOST="${SFTP_HOST:-backup.msgchain.org}"
SFTP_USER="${SFTP_USER:-backup}"
SFTP_PATH="${SFTP_PATH:-/backup/msgchain/}"

log_info()  { echo "[INFO]  $(date '+%Y-%m-%d %H:%M:%S')  $*" | tee -a "$LOG_FILE"; }
log_error() { echo "[ERROR] $(date '+%Y-%m-%d %H:%M:%S')  $*" | tee -a "$LOG_FILE" >&2; }

case "$SYNC_METHOD" in
  s3)
    aws s3 sync "$BACKUP_SOURCE" "s3://${S3_BUCKET}/${S3_PREFIX}" \
      --storage-class STANDARD_IA --delete
    ;;
  sftp)
    rsync -avz --delete -e "ssh -p ${SFTP_PORT:-22}" \
      "$BACKUP_SOURCE/" "${SFTP_USER}@${SFTP_HOST}:${SFTP_PATH}"
    ;;
  nfs)
    rsync -avz --delete "$BACKUP_SOURCE/" "${SFTP_PATH}/"
    ;;
esac

log_info "异地备份同步完成"

3.8 自动化调度 (Crontab)

# Crontab 配置
# 编辑: crontab -e

SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOME=/home/msgchain

# 每日 02:00 全量冷备份
0 2 * * * /usr/local/bin/full_backup_cold.sh >> /var/log/msgchain/backup_cron.log 2>&1

# 每 4 小时增量备份
0 */4 * * * /usr/local/bin/incremental_backup.sh >> /var/log/msgchain/incremental_cron.log 2>&1

# 每小时热备份
0 * * * * /usr/local/bin/hot_backup_badger.sh >> /var/log/msgchain/hot_cron.log 2>&1

# 每天 04:00 同步到异地
0 4 * * * /usr/local/bin/offsite_backup_sync.sh >> /var/log/msgchain/offsite_cron.log 2>&1

# 每天 06:00 验证备份
30 6 * * * /usr/local/bin/verify_backup.sh $(ls -t /backup/msgchain/full/*/data.tar.zst | head -1) >> /var/log/msgchain/verify_cron.log 2>&1

# 每周日 08:00 清理旧备份
0 8 * * 0 /usr/local/bin/cleanup_backups.sh >> /var/log/msgchain/cleanup_cron.log 2>&1

3.9 备份清理脚本

#!/bin/bash
# 脚本: cleanup_backups.sh

set -euo pipefail

BACKUP_BASE="/backup/msgchain"
LOG_FILE="/var/log/msgchain/cleanup.log"
FULL_RETENTION_DAYS=30
INCREMENTAL_RETENTION_DAYS=7
HOT_RETENTION_DAYS=3

log_info() { echo "[INFO] $(date '+%Y-%m-%d %H:%M:%S') $*" | tee -a "$LOG_FILE"; }

log_info "清理超过 ${FULL_RETENTION_DAYS} 天的全量备份..."
find "$BACKUP_BASE/full" -maxdepth 1 -type d -mtime +${FULL_RETENTION_DAYS} -exec rm -rf {} \; -print

log_info "清理超过 ${HOT_RETENTION_DAYS} 天的热备份..."
find "$BACKUP_BASE/hot" -name "*.tar.zst" -mtime +${HOT_RETENTION_DAYS} -delete -print

log_info "磁盘使用:"
du -sh "$BACKUP_BASE" 2>/dev/null
log_info "清理完成"

4. 灾难恢复流程

4.1 灾难恢复决策树

节点故障
    │
    ├── 数据目录是否存在?
    │   ├── 否 → 从快照恢复
    │   └── 是
    │         ↓
    ├── 节点能否启动?
    │   ├── 是 → 检查同步状态 → 正常 / 快速同步
    │   └── 否
    │         ↓
    ├── 数据库损坏?
    │   ├── 否 → 检查配置、权限、磁盘
    │   └── 是 → 尝试 BadgerDB 修复
    │               ├── 成功 → 重启节点
    │               └── 失败
    │                     ↓
    ├── 有可用备份?
    │   ├── 否 → 从快照恢复
    │   └── 是 → 全量恢复
    └──

4.2 从备份完整恢复

#!/bin/bash
# 脚本: full_recovery_from_backup.sh
# 用法: ./full_recovery_from_backup.sh <backup_date>

set -euo pipefail

NODE_HOME="${HOME}/.msgchain"
BACKUP_BASE="/backup/msgchain/full"
BACKUP_DATE="${1:-}"
LOG_FILE="/var/log/msgchain/recovery_$(date +%Y%m%d_%H%M%S).log"

log_info()  { echo "[INFO]  $(date '+%Y-%m-%d %H:%M:%S')  $*" | tee -a "$LOG_FILE"; }
log_error() { echo "[ERROR] $(date '+%Y-%m-%d %H:%M:%S')  $*" | tee -a "$LOG_FILE" >&2; }
log_warn()  { echo "[WARN]  $(date '+%Y-%m-%d %H:%M:%S')  $*" | tee -a "$LOG_FILE"; }

if [ -z "$BACKUP_DATE" ]; then
  log_error "用法: $0 <backup_date>"
  ls "$BACKUP_BASE" 2>/dev/null || echo "无可用备份"
  exit 1
fi

BACKUP_DIR="${BACKUP_BASE}/${BACKUP_DATE}"
[ -d "$BACKUP_DIR" ] || { log_error "备份目录不存在"; exit 1; }

log_info "========== MSG Chain 灾难恢复 =========="

# 阶段 1: 停止节点
log_info "停止 msgchaind 服务..."
systemctl stop msgchaind || true
sleep 5
pkill -9 -f msgchaind 2>/dev/null || true
sleep 3
log_info "节点已停止"

# 阶段 2: 备份当前数据 (回滚点)
CURRENT_BACKUP="${NODE_HOME}/data_rollback_$(date +%Y%m%d_%H%M%S)"
if [ -d "${NODE_HOME}/data" ]; then
  log_info "备份当前数据到 ${CURRENT_BACKUP}..."
  mv "${NODE_HOME}/data" "$CURRENT_BACKUP"
  log_info "回滚点: ${CURRENT_BACKUP}"
fi

# 阶段 3: 验证备份
log_info "验证备份完整性..."
cd "$BACKUP_DIR"
[ -f checksums.sha256 ] && sha256sum -c checksums.sha256 || log_warn "无校验和"
cd - > /dev/null

for f in "$BACKUP_DIR"/*.tar.zst; do
  tar -I zstd -tf "$f" &>/dev/null || { log_error "损坏: $f"; exit 1; }
done
log_info "所有备份文件验证通过"

# 阶段 4: 恢复数据
log_info "恢复数据目录..."
[ -f "${BACKUP_DIR}/data.tar.zst" ] && \
  tar -I zstd -xf "${BACKUP_DIR}/data.tar.zst" -C "$NODE_HOME"
log_info "数据目录恢复完成"

[ -f "${BACKUP_DIR}/config.tar.zst" ] && \
  tar -I zstd -xf "${BACKUP_DIR}/config.tar.zst" -C "$NODE_HOME"
log_info "配置目录恢复完成"

# 阶段 5: 重置 CometBFT
log_info "重置 CometBFT 状态..."
msgchaind tendermint unsafe-reset-all --keep-addr-book || msgchaind tendermint unsafe-reset-all
log_info "CometBFT 已重置"

# 阶段 6: 验证
log_info "验证恢复..."
msgchaind validate-genesis || { log_error "创世文件失败"; exit 1; }
[ -d "${NODE_HOME}/data" ] || { log_error "数据目录不存在"; exit 1; }

# 阶段 7: 启动
log_info "启动节点..."
systemctl start msgchaind
sleep 10

for i in $(seq 1 60); do
  if curl -sf localhost:26657/health > /dev/null 2>&1; then
    log_info "节点已响应!"
    break
  fi
  [ "$i" -eq 60 ] && { log_error "启动超时"; exit 1; }
  sleep 5
done

# 阶段 8: 监控同步
log_info "监控同步状态..."
for i in $(seq 1 12); do
  sleep 10
  STATUS=$(curl -sf localhost:26657/status 2>/dev/null || echo '{}')
  HEIGHT=$(echo "$STATUS" | jq -r '.result.sync_info.latest_block_height // "N/A"')
  CATCHING_UP=$(echo "$STATUS" | jq -r '.result.sync_info.catching_up // "N/A"')
  PEERS=$(curl -sf localhost:26657/net_info 2>/dev/null | jq -r '.result.n_peers // "0"')
  log_info "高度: ${HEIGHT}, 追赶上: ${CATCHING_UP}, 对等节点: ${PEERS}"
  [ "$CATCHING_UP" = "false" ] && { log_info "已完全同步!"; break; }
done

log_info "========== 恢复流程完成 =========="
log_info "检查: curl -s localhost:26657/status | jq ."
log_info "检查: journalctl -u msgchaind -n 50"

4.3 从快照恢复

#!/bin/bash
# 脚本: snapshot_recovery.sh
# 功能: 从快照恢复 MSG Chain 节点

set -euo pipefail

NODE_HOME="${HOME}/.msgchain"
SNAPSHOT_BASE_URL="https://snapshots.msgchain.org"
TEMP_DIR="/tmp/msgchain-snapshot-recovery-$(date +%s)"
LOG_FILE="/var/log/msgchain/snapshot_recovery_$(date +%Y%m%d_%H%M%S).log"

log_info()  { echo "[INFO]  $(date '+%Y-%m-%d %H:%M:%S')  $*" | tee -a "$LOG_FILE"; }
log_error() { echo "[ERROR] $(date '+%Y-%m-%d %H:%M:%S')  $*" | tee -a "$LOG_FILE" >&2; }

cleanup() { rm -rf "$TEMP_DIR"; }
trap cleanup EXIT

get_latest_snapshot() {
  log_info "获取最新快照信息..."
  LATEST_INFO=$(curl -sf "${SNAPSHOT_BASE_URL}/latest.json" 2>/dev/null || echo '{}')
  SNAPSHOT_URL=$(echo "$LATEST_INFO" | jq -r '.url // ""')
  [ -n "$SNAPSHOT_URL" ] && { echo "$SNAPSHOT_URL"; return; }
  echo "${SNAPSHOT_BASE_URL}/latest.tar.lz4"
}

perform_snapshot_restore() {
  local snapshot_url="$1"
  log_info "快照 URL: ${snapshot_url}"
  mkdir -p "$TEMP_DIR"

  log_info "停止节点..."
  systemctl stop msgchaind || true
  pkill -9 -f msgchaind 2>/dev/null || true
  sleep 3

  if [ -d "${NODE_HOME}/data" ]; then
    OLD_BACKUP="${NODE_HOME}/data_old_$(date +%Y%m%d_%H%M%S)"
    mv "${NODE_HOME}/data" "$OLD_BACKUP"
  fi
  mkdir -p "${NODE_HOME}/data"

  log_info "下载快照..."
  SNAPSHOT_FILE="${TEMP_DIR}/snapshot.tar.lz4"
  wget -O "$SNAPSHOT_FILE" "$snapshot_url" || { log_error "下载失败"; exit 1; }

  SHA_URL="${snapshot_url}.sha256"
  if wget -O "${TEMP_DIR}/snapshot.sha256" "$SHA_URL" 2>/dev/null; then
    cd "$TEMP_DIR" && sha256sum -c snapshot.sha256 || { log_error "校验失败"; exit 1; }
    cd - > /dev/null
  fi

  log_info "解压快照..."
  if command -v pv &>/dev/null; then
    pv "$SNAPSHOT_FILE" | tar -I lz4 -xf - -C "${NODE_HOME}/data/"
  else
    tar -I lz4 -xf "$SNAPSHOT_FILE" -C "${NODE_HOME}/data/"
  fi

  msgchaind tendermint unsafe-reset-all --keep-addr-book || true
  msgchaind validate-genesis || { log_error "创世文件失败"; exit 1; }

  systemctl start msgchaind
  sleep 10
  for i in $(seq 1 6); do
    sleep 10
    STATUS=$(curl -sf localhost:26657/status 2>/dev/null || echo '{}')
    HEIGHT=$(echo "$STATUS" | jq -r '.result.sync_info.latest_block_height // "N/A"')
    CATCHING_UP=$(echo "$STATUS" | jq -r '.result.sync_info.catching_up // "N/A"')
    log_info "高度: ${HEIGHT}, 追赶上: ${CATCHING_UP}"
  done
  log_info "========== 快照恢复完成 =========="
}

main() {
  local snapshot_url="${1:-$(get_latest_snapshot)}"
  perform_snapshot_restore "$snapshot_url"
}

main "$@"

4.4 从对等节点恢复 (状态同步)

#!/bin/bash
# 脚本: p2p_state_sync_recovery.sh

set -euo pipefail

NODE_HOME="${HOME}/.msgchain"
LOG_FILE="/var/log/msgchain/p2p_sync_$(date +%Y%m%d_%H%M%S).log"

log_info()  { echo "[INFO]  $(date '+%Y-%m-%d %H:%M:%S')  $*" | tee -a "$LOG_FILE"; }
log_error() { echo "[ERROR] $(date '+%Y-%m-%d %H:%M:%S')  $*" | tee -a "$LOG_FILE" >&2; }

TRUSTED_RPCS=("https://rpc.msgchain.org:443" "https://rpc2.msgchain.org:443")
DISCOVERY_PEERS=("peer1@peer1.msgchain.org:26656" "peer2@peer2.msgchain.org:26656")

get_trust_params() {
  heights=()
  for rpc in "${TRUSTED_RPCS[@]}"; do
    h=$(curl -sf "$rpc/status" 2>/dev/null | jq -r '.result.sync_info.latest_block_height // "0"')
    heights+=("$h")
  done
  sorted=$(printf '%s\n' "${heights[@]}" | sort -n)
  lowest=$(echo "$sorted" | head -1)
  trust_height=$((lowest - 1000))
  block_info=$(curl -sf "${TRUSTED_RPCS[0]}/block?height=${trust_height}" 2>/dev/null)
  trust_hash=$(echo "$block_info" | jq -r '.result.block_id.hash')
  echo "$trust_height $trust_hash"
}

configure_state_sync() {
  local height="$1" hash="$2"
  rpc_list=$(printf ",%s" "${TRUSTED_RPCS[@]}")
  rpc_list="${rpc_list:1}"
  sed -i.bak \
    -e "s/^enable = .*/enable = true/" \
    -e "s|^rpc_servers = .*|rpc_servers = \"${rpc_list}\"|" \
    -e "s/^trust_height = .*/trust_height = ${height}/" \
    -e "s/^trust_hash = .*/trust_hash = \"${hash}\"/" \
    "${NODE_HOME}/config/config.toml"
}

perform_state_sync() {
  log_info "========== P2P 状态同步恢复 =========="
  systemctl stop msgchaind || true
  sleep 3

  if [ -d "${NODE_HOME}/data" ]; then
    old_backup="${NODE_HOME}/data_old_$(date +%Y%m%d_%H%M%S)"
    mv "${NODE_HOME}/data" "$old_backup"
  fi
  mkdir -p "${NODE_HOME}/data"

  read -r height hash <<< "$(get_trust_params)"
  log_info "信任高度: ${height}, 哈希: ${hash}"

  configure_state_sync "$height" "$hash"
  peer_list=$(printf ",%s" "${DISCOVERY_PEERS[@]}")
  peer_list="${peer_list:1}"
  sed -i "s/^persistent_peers = .*/persistent_peers = \"${peer_list}\"/" \
    "${NODE_HOME}/config/config.toml"

  msgchaind tendermint unsafe-reset-all
  systemctl start msgchaind

  sleep 15
  for i in $(seq 1 30); do
    sleep 10
    STATUS=$(curl -sf localhost:26657/status 2>/dev/null || echo '{}')
    HEIGHT=$(echo "$STATUS" | jq -r '.result.sync_info.latest_block_height // "N/A"')
    CATCHING_UP=$(echo "$STATUS" | jq -r '.result.sync_info.catching_up // "N/A"')
    PEERS=$(curl -sf localhost:26657/net_info 2>/dev/null | jq -r '.result.n_peers // "0"')
    log_info "高度: ${HEIGHT} | 追赶上: ${CATCHING_UP} | 对等节点: ${PEERS}"
    [ "$CATCHING_UP" = "false" ] && { log_info "同步完成!"; break; }
  done
  log_info "========== P2P 同步完成 =========="
}

perform_state_sync

4.5 验证者密钥恢复

#!/bin/bash
# 脚本: validator_key_recovery.sh

set -euo pipefail

NODE_HOME="${HOME}/.msgchain"
PRIV_VAL_KEY="${NODE_HOME}/config/priv_validator_key.json"
STATE_FILE="${NODE_HOME}/config/priv_validator_state.json"
LOG_FILE="/var/log/msgchain/key_recovery_$(date +%Y%m%d_%H%M%S).log"

log_info()  { echo "[INFO]  $(date '+%Y-%m-%d %H:%M:%S')  $*" | tee -a "$LOG_FILE"; }
log_error() { echo "[ERROR] $(date '+%Y-%m-%d %H:%M:%S')  $*" | tee -a "$LOG_FILE" >&2; }

check_existing_keys() {
  if [ -f "$PRIV_VAL_KEY" ]; then
    log_info "验证者密钥文件存在"
    log_info "公钥: $(jq -r '.pub_key.value' "$PRIV_VAL_KEY" 2>/dev/null || echo '无法读取')"
    msgchaind tendermint show-validator &>/dev/null && log_info "✓ 密钥格式正确" || log_error "密钥损坏"
  else
    log_info "验证者密钥文件不存在"
    return 1
  fi
}

restore_key_from_backup() {
  local backup_source="${1:-}"
  [ -z "$backup_source" ] && { log_error "请指定备份来源"; return 1; }
  log_info "从备份恢复验证者密钥: ${backup_source}"

  case "$backup_source" in
    *.tar.zst)
      local tmp="/tmp/key_restore_$(date +%s)"
      mkdir -p "$tmp"
      tar -I zstd -xf "$backup_source" -C "$tmp"
      local kf=$(find "$tmp" -name "priv_validator_key.json" 2>/dev/null | head -1)
      [ -n "$kf" ] && { cp "$kf" "$PRIV_VAL_KEY"; rm -rf "$tmp"; log_info "密钥已恢复"; return 0; }
      rm -rf "$tmp"; log_error "未找到密钥文件"; return 1
      ;;
    *)
      [ -f "$backup_source/priv_validator_key.json" ] && { cp "$backup_source/priv_validator_key.json" "$PRIV_VAL_KEY"; return 0; }
      log_error "无效来源"; return 1
      ;;
  esac
}

verify_recovered_key() {
  log_info "验证恢复的密钥..."
  val_addr=$(msgchaind tendermint show-address 2>/dev/null || echo "N/A")
  val_pubkey=$(msgchaind tendermint show-validator 2>/dev/null || echo "N/A")
  log_info "验证者地址: ${val_addr}"
  log_info "验证者公钥: ${val_pubkey}"

  if curl -sf localhost:26657/status &>/dev/null; then
    chain_val=$(curl -sf "localhost:26657/validators?height=0" 2>/dev/null \
      | jq -r '.result.validators[] | select(.address == "'"$val_addr"'") | .address' 2>/dev/null || echo "")
    [ -n "$chain_val" ] && log_info "✓ 验证者在链上验证者集合中" || log_warn "验证者不在当前集合中"
  fi
}

reset_validator_state() {
  log_info "重置验证者状态..."
  cat > "$STATE_FILE" << EOF
{ "height": "0", "round": 0, "step": 0, "signature": "", "signbytes": "" }
EOF
  log_info "状态已重置"
}

show_menu() {
  echo ""
  echo "MSG Chain 验证者密钥恢复工具"
  echo "1. 检查现有密钥"
  echo "2. 从备份恢复密钥"
  echo "3. 重置验证者状态"
  echo "4. 验证密钥"
  echo "q. 退出"
  echo -n "请选择: "
}

main() {
  log_info "========== 验证者密钥恢复 =========="
  while true; do
    show_menu; read -r choice
    case "$choice" in
      1) check_existing_keys ;;
      2) echo -n "备份路径: "; read -r bp; restore_key_from_backup "$bp" ;;
      3) echo -n "确认? (yes/no): "; read -r c; [ "$c" = "yes" ] && reset_validator_state ;;
      4) verify_recovered_key ;;
      q|Q) exit 0 ;;
    esac
  done
}

main "$@"

4.6 哨兵节点恢复

#!/bin/bash
# 脚本: sentry_recovery.sh

set -euo pipefail

NODE_HOME="${HOME}/.msgchain"
SENTRY_CONFIG="${NODE_HOME}/config/config.toml"
LOG_FILE="/var/log/msgchain/sentry_recovery_$(date +%Y%m%d_%H%M%S).log"

log_info() { echo "[INFO] $(date '+%Y-%m-%d %H:%M:%S') $*" | tee -a "$LOG_FILE"; }
log_error() { echo "[ERROR] $(date '+%Y-%m-%d %H:%M:%S') $*" | tee -a "$LOG_FILE" >&2; }

recover_sentry() {
  log_info "========== 哨兵节点恢复 =========="

  if [ ! -f "${NODE_HOME}/config/node_key.json" ]; then
    log_info "初始化新节点..."
    msgchaind init "sentry-$(hostname)" --chain-id msg-chain-1
    curl -fsSL https://github.com/msgchain/mainnet/raw/main/genesis.json \
      -o "${NODE_HOME}/config/genesis.json"
  fi

  log_info "配置哨兵模式..."
  VALIDATOR_PEER_IDS="${1:-}"
  sed -i \
    -e "s/^pex = .*/pex = true/" \
    -e "s/^persistent_peers = .*/persistent_peers = \"${VALIDATOR_PEER_IDS}\"/" \
    -e "s/^private_peer_ids = .*/private_peer_ids = \"${VALIDATOR_PEER_IDS}\"/" \
    -e "s/^max_num_inbound_peers = .*/max_num_inbound_peers = 100/" \
    -e "s/^max_num_outbound_peers = .*/max_num_outbound_peers = 50/" \
    "$SENTRY_CONFIG"

  log_info "哨兵节点恢复完成"
}

recover_sentry "$@"

5. 高可用架构

5.1 架构设计原则

5.2 多节点集群架构

                         ┌───────────────────┐
                         │   负载均衡器       │
                         │ HAProxy / Nginx   │
                         └────────┬──────────┘
                                  │
        ┌─────────────────────────┼─────────────────────────┐
        v                         v                         v
┌────────────────┐   ┌────────────────┐   ┌────────────────┐
│  哨兵节点 1     │   │  哨兵节点 2     │   │  哨兵节点 3     │
│  P2P:26656     │   │  P2P:26656     │   │  P2P:26656     │
│  RPC:26657     │   │  RPC:26657     │   │  RPC:26657     │
└────────┬───────┘   └────────┬───────┘   └────────┬───────┘
         │                    │                    │
         │       ╔════════════╪════════════╗       │
         ╞═══════╣ 私有网络    │          ╟═══════╡
         │       ║            │          ║       │
         │       ║            v          ║       │
         │       ║  ┌─────────────────┐  ║       │
         └───────╢  │  验证者节点      │  ╟───────┘
                  ║  │ 共识参与        │  ║
                  ║  │ 密钥管理        │  ║
                  ║  └─────────────────┘  ║
                  ╚═══════════════════════╝

5.3 哨兵节点拓扑配置

5.3.1 验证者节点

# ~/.msgchain/config/config.toml (验证者)
pex = false
persistent_peers = "sentry1-id@sentry1.internal:26656,sentry2-id@sentry2.internal:26656,sentry3-id@sentry3.internal:26656"
unconditional_peer_ids = "sentry1-id,sentry2-id,sentry3-id"
addr_book_strict = false
private_peer_ids = "*"
seed_mode = false

5.3.2 哨兵节点

# ~/.msgchain/config/config.toml (哨兵)
pex = true
seeds = "seed1.msgchain.org:26656,seed2.msgchain.org:26656"
persistent_peers = "validator-node-id@validator.internal:26656"
private_peer_ids = "validator-node-id"
unconditional_peer_ids = "validator-node-id"
max_num_inbound_peers = 200
max_num_outbound_peers = 50

5.4 负载均衡器配置

5.4.1 HAProxy

# /etc/haproxy/haproxy.cfg
global
  log /dev/log local0
  maxconn 10000
  daemon

defaults
  mode tcp
  option tcplog
  timeout connect 5s
  timeout client 30s
  timeout server 30s
  retries 3

frontend rpc_frontend
  bind *:26657
  mode tcp
  default_backend rpc_backend

backend rpc_backend
  mode tcp
  balance leastconn
  option tcp-check
  tcp-check connect
  server sentry1 sentry1.internal:26657 check inter 5s fall 3 rise 2
  server sentry2 sentry2.internal:26657 check inter 5s fall 3 rise 2
  server sentry3 sentry3.internal:26657 check inter 5s fall 3 rise 2

frontend p2p_frontend
  bind *:26656
  mode tcp
  default_backend p2p_backend

backend p2p_backend
  mode tcp
  balance source
  server sentry1 sentry1.internal:26656 check inter 5s fall 3 rise 2
  server sentry2 sentry2.internal:26656 check inter 5s fall 3 rise 2
  server sentry3 sentry3.internal:26656 check inter 5s fall 3 rise 2

frontend grpc_frontend
  bind *:9090
  mode tcp
  default_backend grpc_backend

backend grpc_backend
  mode tcp
  balance leastconn
  server sentry1 sentry1.internal:9090 check inter 5s fall 3 rise 2
  server sentry2 sentry2.internal:9090 check inter 5s fall 3 rise 2
  server sentry3 sentry3.internal:9090 check inter 5s fall 3 rise 2

5.4.2 Nginx 替代配置

# /etc/nginx/conf.d/msgchain.conf
upstream msgchain_rpc {
  least_conn;
  server sentry1.internal:26657 max_fails=3 fail_timeout=30s;
  server sentry2.internal:26657 max_fails=3 fail_timeout=30s;
  server sentry3.internal:26657 max_fails=3 fail_timeout=30s;
  keepalive 32;
}

server {
  listen 26657;
  proxy_pass msgchain_rpc;
  proxy_http_version 1.1;
  proxy_set_header Upgrade $http_upgrade;
  proxy_set_header Connection "upgrade";
  proxy_set_header Host $host;

  location /websocket {
    proxy_pass http://msgchain_rpc;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_read_timeout 86400s;
  }
}

5.5 自动故障转移

#!/bin/bash
# 脚本: auto_failover.sh
# 功能: MSG Chain 哨兵节点自动故障转移

set -euo pipefail

SENTRY_NODES=("sentry1.internal:26657" "sentry2.internal:26657" "sentry3.internal:26657")
VALIDATOR_NODE="validator.internal:26657"
CHECK_INTERVAL=10
FAIL_THRESHOLD=3
STATUS_FILE="/tmp/msgchain_sentry_status.json"
LOG_FILE="/var/log/msgchain/failover.log"
NOTIFY_SCRIPT="/usr/local/bin/notify_alert.sh"

log_info()  { echo "[INFO]  $(date '+%Y-%m-%d %H:%M:%S')  $*" | tee -a "$LOG_FILE"; }
log_error() { echo "[ERROR] $(date '+%Y-%m-%d %H:%M:%S')  $*" | tee -a "$LOG_FILE" >&2; }

check_node_health() {
  local node="$1"
  local response=$(curl -sf "http://${node}/status" 2>/dev/null || echo '{}')
  local height=$(echo "$response" | jq -r '.result.sync_info.latest_block_height // "0"')
  local catching_up=$(echo "$response" | jq -r '.result.sync_info.catching_up // "true"')
  if [ "$height" != "0" ] && [ "$catching_up" = "false" ]; then echo "healthy"
  elif [ "$height" != "0" ]; then echo "syncing"
  else echo "unhealthy"
  fi
}

perform_failover() {
  local failed_node="$1"
  log_info "执行故障转移: ${failed_node}"
  if [ -S /run/haproxy/admin.sock ]; then
    local node_name=$(echo "$failed_node" | cut -d: -f1)
    echo "disable server rpc_backend/${node_name}" | socat stdio /run/haproxy/admin.sock || true
    echo "disable server p2p_backend/${node_name}" | socat stdio /run/haproxy/admin.sock || true
    log_info "已禁用 HAProxy 中的节点: ${node_name}"
  fi
}

notify_failure() {
  local subject="$1"
  [ -x "$NOTIFY_SCRIPT" ] && $NOTIFY_SCRIPT "MSG Chain 故障: ${subject}" "时间: $(date)"
}

main_loop() {
  log_info "========== 故障转移守护进程启动 =========="
  while true; do
    for node in "${SENTRY_NODES[@]}"; do
      health=$(check_node_health "$node")
      [ "$health" = "unhealthy" ] && { perform_failover "$node"; notify_failure "$node"; }
    done
    sleep "$CHECK_INTERVAL"
  done
}

main_loop "$@"

5.6 Systemd 服务配置

# /etc/systemd/system/msgchaind.service
[Unit]
Description=MSG Chain Node
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=msgchain
Group=msgchain
WorkingDirectory=/home/msgchain
ExecStart=/usr/local/bin/msgchaind start --home /home/msgchain/.msgchain --trace
Restart=always
RestartSec=10
StartLimitBurst=5
LimitNOFILE=65536
LimitNPROC=65536
MemoryHigh=32G
MemoryMax=40G
CPUQuota=400%
NoNewPrivileges=true
ProtectSystem=full
ProtectHome=read-only
PrivateTmp=true
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

5.7 DRBD 块级复制

# /etc/drbd.d/msgchain_data.res
resource msgchain_data {
  protocol C;
  on node1.internal {
    device    /dev/drbd0;
    disk      /dev/sdb1;
    address   10.0.1.1:7788;
    meta-disk internal;
  }
  on node2.internal {
    device    /dev/drbd0;
    disk      /dev/sdb1;
    address   10.0.1.2:7788;
    meta-disk internal;
  }
  syncer { rate 1000M; }
}

# 初始化
drbdadm create-md msgchain_data
drbdadm up msgchain_data
drbdadm primary --force msgchain_data
mkfs.ext4 /dev/drbd0
mount /dev/drbd0 /home/msgchain/.msgchain/data/

6. 监控与告警

6.1 监控指标体系

类别 指标 告警阈值
同步状态 catching_up true 超过 1 小时
区块高度 latest_block_height 落后超过 100 区块
对等节点 n_peers 少于 5 个
磁盘使用 disk_usage_percent > 80% 警告, > 90% 严重
内存使用 memory_usage_bytes > 32GB
CPU 使用 cpu_percent > 80% 持续 10 分钟
共识投票 missed_blocks > 10 连续错过
磁盘 IO `disk_reads writes`

6.2 Prometheus 配置

# /etc/prometheus/prometheus.yml
global:
  scrape_interval: 15s
  evaluation_interval: 15s

alerting:
  alertmanagers:
    - static_configs:
        - targets:
          - 'localhost:9093'

rule_files:
  - '/etc/prometheus/rules/*.yml'

scrape_configs:
  - job_name: 'msgchain-node'
    scrape_interval: 10s
    static_configs:
      - targets:
        - 'node1.internal:26660'
        - 'node2.internal:26660'
        - 'node3.internal:26660'
    metrics_path: '/metrics'

  - job_name: 'msgchain-validator'
    scrape_interval: 10s
    static_configs:
      - targets:
        - 'validator.internal:26660'
    metrics_path: '/metrics'

  - job_name: 'node-exporter'
    static_configs:
      - targets:
        - 'node1.internal:9100'
        - 'node2.internal:9100'
        - 'node3.internal:9100'
        - 'validator.internal:9100'

6.3 告警规则

# /etc/prometheus/rules/msgchain_alerts.yml
groups:
  - name: msgchain_node
    interval: 30s
    rules:
      - alert: NodeDown
        expr: up{job="msgchain-node"} == 0
        for: 1m
        labels: { severity: critical }
        annotations:
          summary: "MSG Chain 节点 {{ $labels.instance }} 停机"
          description: "{{ $labels.instance }} 已宕机超过 1 分钟"

      - alert: NodeCatchingUp
        expr: msgchain_catching_up{job="msgchain-node"} == 1
        for: 30m
        labels: { severity: warning }
        annotations:
          summary: "节点 {{ $labels.instance }} 正在追赶同步"
          description: "节点已同步超过 30 分钟仍未完成"

      - alert: NodeHeightDrift
        expr: abs(msgchain_latest_block_height{job="msgchain-node"} - msgchain_latest_block_height{job="msgchain-validator"}) > 100
        for: 5m
        labels: { severity: warning }
        annotations:
          summary: "节点高度偏差超过 100"

      - alert: LowPeers
        expr: msgchain_peers{job="msgchain-node"} < 5
        for: 5m
        labels: { severity: warning }
        annotations:
          summary: "对等节点数少于 5"
          description: "当前对等节点数: {{ $value }}"

      - alert: DiskSpaceWarning
        expr: (node_filesystem_avail_bytes{mountpoint="/data"} / node_filesystem_size_bytes{mountpoint="/data"}) * 100 < 20
        for: 10m
        labels: { severity: warning }
        annotations:
          summary: "磁盘空间不足 20%"
          description: "剩余: {{ $value | humanizePercentage }}"

      - alert: DiskSpaceCritical
        expr: (node_filesystem_avail_bytes{mountpoint="/data"} / node_filesystem_size_bytes{mountpoint="/data"}) * 100 < 10
        for: 5m
        labels: { severity: critical }
        annotations:
          summary: "磁盘空间严重不足"
          description: "剩余: {{ $value | humanizePercentage }}"

      - alert: BackupFreshness
        expr: time() - max(msgchain_backup_timestamp{type="full"}) > 86400
        for: 1h
        labels: { severity: warning }
        annotations:
          summary: "备份超过 24 小时未更新"
          description: "最后一次全量备份: {{ $value | humanizeDuration }} 前"

  - name: msgchain_validator
    interval: 30s
    rules:
      - alert: ValidatorMissedBlocks
        expr: increase(msgchain_missed_blocks[10m]) > 10
        for: 1m
        labels: { severity: critical }
        annotations:
          summary: "验证者错过过多区块"
          description: "过去 10 分钟错过 {{ $value }} 个区块"

      - alert: ValidatorNotSigning
        expr: msgchain_signed_blocks_total{job="msgchain-validator"} == 0
        for: 5m
        labels: { severity: critical }
        annotations:
          summary: "验证者未签名区块"
          description: "过去 5 分钟未签名任何区块"

      - alert: ValidatorJailed
        expr: msgchain_validator_jailed == 1
        for: 0m
        labels: { severity: critical }
        annotations:
          summary: "验证者被监禁!"
          description: "验证者 {{ $labels.instance }} 已被网络监禁"

      - alert: HighMemoryUsage
        expr: node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes * 100 < 10
        for: 10m
        labels: { severity: warning }
        annotations:
          summary: "内存不足"
          description: "可用内存 < 10%"

6.4 备份健康监控

#!/bin/bash
# 脚本: monitor_backup_health.sh
# 功能: 监控备份健康状况

set -euo pipefail

BACKUP_BASE="/backup/msgchain"
ALERT_EMAIL="ops@msgchain.org"
PAGERDUTY_KEY="${PAGERDUTY_KEY:-}"

check_backup_freshness() {
  local latest_full=$(ls -td "$BACKUP_BASE"/full/*/ 2>/dev/null | head -1)
  if [ -z "$latest_full" ]; then
    echo "CRITICAL: 无全量备份!"
    return 2
  fi
  local backup_time=$(stat -c %Y "$latest_full")
  local now=$(date +%s)
  local age=$(( (now - backup_time) / 3600 ))
  if [ "$age" -gt 48 ]; then
    echo "WARNING: 最新备份已 ${age} 小时 (超过 48 小时阈值)"
    return 1
  fi
  echo "OK: 最新备份 ${age} 小时前"
  return 0
}

check_backup_size() {
  local latest_full=$(ls -td "$BACKUP_BASE"/full/*/ 2>/dev/null | head -1)
  [ -z "$latest_full" ] && return 1
  local size=$(du -sb "$latest_full" | cut -f1)
  local data_size=$(du -sb "${HOME}/.msgchain/data" 2>/dev/null | cut -f1)
  local ratio=$(( size * 100 / (data_size + 1) ))
  if [ "$ratio" -lt 10 ] || [ "$ratio" -gt 300 ]; then
    echo "WARNING: 备份大小异常 (数据 ${data_size}, 备份 ${size}, 比例 ${ratio}%)"
    return 1
  fi
  echo "OK: 备份大小正常 (${ratio}% 数据大小)"
  return 0
}

check_disk_space() {
  local available=$(df --output=avail "$BACKUP_BASE" 2>/dev/null | tail -1)
  local available_gb=$(( available / 1024 / 1024 ))
  if [ "$available_gb" -lt 50 ]; then
    echo "WARNING: 备份磁盘剩余 ${available_gb}GB (不足 50GB)"
    return 1
  fi
  echo "OK: 备份磁盘剩余 ${available_gb}GB"
  return 0
}

main() {
  echo "MSG Chain 备份健康检查 - $(date)"
  echo "================================"
  failures=0
  check_backup_freshness || ((failures++))
  check_backup_size || ((failures++))
  check_disk_space || ((failures++))
  echo ""
  [ "$failures" -gt 0 ] && echo "发现 ${failures} 个问题" || echo "所有检查通过"
}

main "$@"

6.5 数据完整性检查

#!/bin/bash
# 脚本: data_integrity_check.sh
# 功能: MSG Chain 数据完整性检查

set -euo pipefail

NODE_HOME="${HOME}/.msgchain"
CHECKSUMS_DB="${NODE_HOME}/data/.integrity_checksums"
LOG_FILE="/var/log/msgchain/integrity_check.log"

log_info() { echo "[INFO] $(date '+%Y-%m-%d %H:%M:%S') $*" | tee -a "$LOG_FILE"; }
log_error() { echo "[ERROR] $(date '+%Y-%m-%d %H:%M:%S') $*" | tee -a "$LOG_FILE" >&2; }

create_checksums() {
  log_info "创建数据校验和..."
  find "$NODE_HOME/data" -type f \( -name "*.sst" -o -name "*.vlog" -o -name "*.ldb" -o -name "*.log" \) \
    -exec sha256sum {} \; > "$CHECKSUMS_DB.new"
  mv "$CHECKSUMS_DB.new" "$CHECKSUMS_DB"
  log_info "校验和已创建 ($(wc -l < "$CHECKSUMS_DB") 个文件)"
}

verify_checksums() {
  log_info "验证数据完整性..."
  local failures=0
  while IFS= read -r line; do
    local expected_hash=$(echo "$line" | cut -d' ' -f1)
    local filepath=$(echo "$line" | cut -d' ' -f2-)
    if [ -f "$filepath" ]; then
      local actual_hash=$(sha256sum "$filepath" | cut -d' ' -f1)
      if [ "$expected_hash" != "$actual_hash" ]; then
        log_error "文件损坏: ${filepath}"
        log_error "期望: ${expected_hash}, 实际: ${actual_hash}"
        ((failures++))
      fi
    else
      log_error "文件丢失: ${filepath}"
      ((failures++))
    fi
  done < "$CHECKSUMS_DB"
  [ "$failures" -eq 0 ] && log_info "完整性检查通过 (所有文件正常)" || log_error "发现 ${failures} 个问题"
  return "$failures"
}

main() {
  local action="${1:-check}"
  case "$action" in
    create)
      create_checksums
      ;;
    check)
      [ -f "$CHECKSUMS_DB" ] || { log_info "首次运行: 创建校验和"; create_checksums; }
      verify_checksums
      ;;
    *)
      echo "用法: $0 {create|check}"
      exit 1
      ;;
  esac
}

main "$@"

6.6 Grafana 仪表板指标

{
  "title": "MSG Chain 节点监控",
  "panels": [
    {
      "title": "区块高度",
      "type": "graph",
      "targets": [
        { "expr": "msgchain_latest_block_height{job=\"msgchain-validator\"}", "legendFormat": "验证者" },
        { "expr": "avg(msgchain_latest_block_height{job=\"msgchain-node\"})", "legendFormat": "哨兵平均" }
      ]
    },
    {
      "title": "同步状态",
      "type": "stat",
      "targets": [
        { "expr": "msgchain_catching_up{job=\"msgchain-validator\"}", "legendFormat": "同步中" }
      ]
    },
    {
      "title": "对等节点数",
      "type": "graph",
      "targets": [
        { "expr": "msgchain_peers{job=\"msgchain-node\"}", "legendFormat": "{{instance}}" }
      ]
    },
    {
      "title": "磁盘使用率",
      "type": "gauge",
      "targets": [
        { "expr": "(1 - node_filesystem_avail_bytes{mountpoint=\"/data\"} / node_filesystem_size_bytes{mountpoint=\"/data\"}) * 100" }
      ]
    }
  ]
}

6.7 日常巡检脚本

#!/bin/bash
# 脚本: daily_checks.sh
# 功能: MSG Chain 节点日常巡检

set -euo pipefail

RPC="http://localhost:26657"
LOG_FILE="/var/log/msgchain/daily_checks.log"

log_info() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }

echo "=========================================="
echo " MSG Chain 日常巡检报告"
echo " 时间: $(date)"
echo "=========================================="

# 1. 节点状态
echo -e "\n--- 节点状态 ---"
STATUS=$(curl -sf "${RPC}/status" | jq '.result' 2>/dev/null || echo "N/A")
echo "$STATUS" | jq '{moniker: .node_info.moniker, network: .node_info.network, version: .node_info.version, blocks: .sync_info.latest_block_height, catching_up: .sync_info.catching_up}' 2>/dev/null

# 2. 对等节点
echo -e "\n--- 对等节点 ---"
NETINFO=$(curl -sf "${RPC}/net_info" | jq '.result' 2>/dev/null || echo '{}')
echo "连接数: $(echo "$NETINFO" | jq '.n_peers // 0')"

# 3. 磁盘使用
echo -e "\n--- 磁盘使用 ---"
DATA_DIR="${HOME}/.msgchain/data"
[ -d "$DATA_DIR" ] && du -sh "$DATA_DIR" || echo "数据目录不存在"
df -h "$DATA_DIR" | tail -1

# 4. 系统资源
echo -e "\n--- 系统资源 ---"
echo "CPU: $(top -bn1 | grep 'Cpu(s)' | awk '{print $8}')% idle"
echo "内存: $(free -h | grep Mem | awk '{print $3 "/" $2}')"
echo "负载: $(uptime | awk -F'load average:' '{print $2}')"

# 5. 共识检查
echo -e "\n--- 共识状态 ---"
CONSENSUS=$(curl -sf "${RPC}/consensus_state" 2>/dev/null | jq '.result.round_state | {height: .height, round: .round, step: .step}' 2>/dev/null || echo "N/A")
echo "$CONSENSUS"

# 6. 进程状态
echo -e "\n--- 进程状态 ---"
systemctl is-active msgchaind
journalctl -u msgchaind --since "1 hour ago" --no-pager | grep -i "error\|panic\|corrupt\|fail" | tail -5 || echo "无错误日志"

echo -e "\n=========================================="
echo " 巡检完成"
echo "=========================================="

7. 验证者专用

7.1 验证者密钥安全

验证者密钥是节点的最高敏感资产。泄露或丢失将导致:

7.1.1 密钥存储层次

验证者密钥安全层级:

Level 0: 内存 (节点运行时)
Level 1: 本地磁盘加密 (软件默认)
Level 2: HSM/密码管理器
Level 3: 离线冷存储 (Shamir 备份)
Level 4: 多方分片 (社会恢复)

7.1.2 HSM 配置

#!/bin/bash
# 脚本: configure_hsm.sh
# 功能: 配置 HSM (硬件安全模块) 用于验证者签名
# 支持: YubiHSM, Ledger, 自定义 PKCS#11

set -euo pipefail

NODE_HOME="${HOME}/.msgchain"

configure_yubihsm() {
  log_info "配置 YubiHSM..."

  # 安装 YubiHSM 工具
  # apt install yubihsm-shell libyubihsm-dev

  # 初始化 HSM
  yubihsm-shell --action create-session \
    --auth-key 1 --password "change_me_immediately"

  # 生成验证者密钥 (在 HSM 内)
  yubihsm-shell --action generate-asymmetric-key \
    --key-algo ec-p256 \
    --label "MSG Chain Validator Key" \
    --domains 1 \
    --capabilities sign-ecdsa

  # 导出公钥
  yubihsm-shell --action get-public-key \
    --key-id 0x1234 \
    --outfile /tmp/validator_pubkey.pem

  # 配置 Cosmos SDK 使用 HSM
  cat > "${NODE_HOME}/config/hsm_config.yaml" << EOF
hsm:
  module: /usr/lib/x86_64-linux-gnu/pkcs11/yubihsm_pkcs11.so
  slot: 0
  key_id: 0x1234
  pin: "${YUBIHSM_PIN}"
  algorithm: "ecdsa-secp256r1"
EOF

  log_info "YubiHSM 配置完成"
}

configure_ledger() {
  log_info "配置 Ledger 硬件钱包..."
  log_info "1. 连接 Ledger 设备并解锁"
  log_info "2. 打开 Cosmos App"
  log_info "3. 运行: msgchaind keys add validator --ledger --hd-path \"44'/118'/0'/0/0\""
  log_info "4. 在 Ledger 上确认操作"
}

# 主流程
main() {
  case "${1:-}" in
    yubihsm) configure_yubihsm ;;
    ledger) configure_ledger ;;
    *) echo "用法: $0 {yubihsm|ledger}" ;;
  esac
}

main "$@"

7.2 双重签名保护

#!/bin/bash
# 脚本: double_sign_protection.sh
# 功能: 配置双重签名预防机制

set -euo pipefail

NODE_HOME="${HOME}/.msgchain"
VALIDATOR_KEY="${NODE_HOME}/config/priv_validator_key.json"
BACKUP_VAL_KEY="${NODE_HOME}/config/priv_validator_key.json.backup"

log_info() { echo "[INFO] $(date) $*"; }
log_error() { echo "[ERROR] $(date) $*" >&2; }

# 1. 备份原始密钥
backup_original_key() {
  [ -f "$VALIDATOR_KEY" ] && cp "$VALIDATOR_KEY" "$BACKUP_VAL_KEY"
  log_info "原始密钥已备份到 ${BACKUP_VAL_KEY}"
}

# 2. 配置 TMKMS (远程签名)
configure_tmkms() {
  log_info "配置 TMKMS 远程签名..."

  # 安装 TMKMS
  # git clone https://github.com/iqlusioninc/tmkms.git
  # cd tmkms && cargo build --release

  # 创建 TMKMS 配置
  cat > "${NODE_HOME}/config/tmkms.toml" << EOF
[[chain]]
id = "msg-chain-1"
key_format = { type = "cosmos-json", path = "${VALIDATOR_KEY}" }

[[providers]]
type = "softsign"
path = "${VALIDATOR_KEY}"

[[validator]]
addr = "tcp://127.0.0.1:26659"
chain_id = "msg-chain-1"
protocol = "secret-connection"
EOF

  # 配置节点使用外部签名
  sed -i 's/^priv_validator_laddr = .*/priv_validator_laddr = "tc[未公开路径]"/' \
    "${NODE_HOME}/config/config.toml"

  log_info "TMKMS 配置完成"
  log_info "启动: tmkms start -c ${NODE_HOME}/config/tmkms.toml"
}

# 3. 哨兵检查 (停止相同公钥的另一个节点)
sentry_prevention() {
  log_info "配置哨兵式双重签名预防..."

  cat > "${NODE_HOME}/config/double_sign_check.sh" << 'SCRIPT'
#!/bin/bash
# 在启动前检查是否有另一实例使用相同密钥运行
KEY_HASH=$(sha256sum "${NODE_HOME}/config/priv_validator_key.json" | cut -d' ' -f1)

# 扫描本地网络中的节点
for host in $(seq 1 10); do
  ADDR="192.168.1.${host}:26657"
  STATUS=$(curl -sf "http://${ADDR}/status" 2>/dev/null || echo "")
  if [ -n "$STATUS" ]; then
    echo "发现活跃节点: ${ADDR}"
    # 比较公钥 (需要扩展接口支持)
  fi
done
echo "双重签名检查完成: ${KEY_HASH}"
SCRIPT
  chmod +x "${NODE_HOME}/config/double_sign_check.sh"
}

# 4. 启动前验证脚本
create_startup_hook() {
  cat > "${NODE_HOME}/config/pre_start_check.sh" << 'SCRIPT'
#!/bin/bash
# 节点启动前置检查

NODE_HOME="${HOME}/.msgchain"
ERRORS=0

check() {
  local msg="$1"; shift
  if "$@"; then echo "  ✓ $msg"; else echo "  ✗ $msg"; ((ERRORS++)); fi
}

echo "MSG Chain 节点启动前检查..."

check "验证者密钥存在" [ -f "${NODE_HOME}/config/priv_validator_key.json" ]
check "密钥格式有效" msgchaind tendermint show-validator &>/dev/null
check "创世文件存在" [ -f "${NODE_HOME}/config/genesis.json" ]
check "创世文件有效" msgchaind validate-genesis &>/dev/null
check "config.toml 存在" [ -f "${NODE_HOME}/config/config.toml" ]
check "app.toml 存在" [ -f "${NODE_HOME}/config/app.toml" ]
check "数据目录可写" [ -w "${NODE_HOME}/data" ]
check "磁盘空间充足" [ "$(df --output=avail "${NODE_HOME}/data" | tail -1)" -gt 10485760 ]

# 双重签名检查
if [ -f "${NODE_HOME}/config/double_sign_check.sh" ]; then
  bash "${NODE_HOME}/config/double_sign_check.sh"
fi

echo ""
[ "$ERRORS" -eq 0 ] && echo "所有检查通过,启动节点..." || echo "发现 ${ERRORS} 个问题,请修复后重试"
exit "$ERRORS"
SCRIPT
  chmod +x "${NODE_HOME}/config/pre_start_check.sh"

  # 添加到 systemd 单元
  mkdir -p /etc/systemd/system/msgchaind.service.d/
  cat > /etc/systemd/system/msgchaind.service.d/pre_check.conf << EOF
[Service]
ExecStartPre=${NODE_HOME}/config/pre_start_check.sh
EOF
  systemctl daemon-reload
  log_info "启动前检查已配置"
}

# 主流程
main() {
  backup_original_key
  configure_tmkms
  sentry_prevention
  create_startup_hook
  log_info "双重签名保护配置完成"
}

main

7.3 签名密钥轮换

#!/bin/bash
# 脚本: rotate_validator_key.sh
# 功能: 验证者签名密钥轮换
# 警告: 这是一个高风险操作,请在生产环境外充分测试

set -euo pipefail

NODE_HOME="${HOME}/.msgchain"
OLD_KEY="${NODE_HOME}/config/priv_validator_key.json.old"
LOG_FILE="/var/log/msgchain/key_rotation.log"

log_info() { echo "[INFO] $(date '+%Y-%m-%d %H:%M:%S') $*" | tee -a "$LOG_FILE"; }
log_error() { echo "[ERROR] $(date '+%Y-%m-%d %H:%M:%S') $*" | tee -a "$LOG_FILE" >&2; }

# 阶段 1: 准备新密钥
prepare_new_key() {
  log_info "准备新验证者密钥..."

  # 方法 1: 使用 msgchaind 生成
  msgchaind tendermint gen-validator > "${NODE_HOME}/config/new_validator_key.json"
  log_info "新密钥已生成"

  # 获取新旧公钥
  OLD_PUBKEY=$(jq -r '.pub_key.value' "${NODE_HOME}/config/priv_validator_key.json")
  NEW_PUBKEY=$(jq -r '.pub_key.value' "${NODE_HOME}/config/new_validator_key.json")

  log_info "旧公钥: ${OLD_PUBKEY}"
  log_info "新公钥: ${NEW_PUBKEY}"
}

# 阶段 2: 备份旧密钥
backup_old_key() {
  log_info "备份旧密钥..."
  cp "${NODE_HOME}/config/priv_validator_key.json" "$OLD_KEY"
  cp "${NODE_HOME}/config/priv_validator_state.json" "${NODE_HOME}/config/priv_validator_state.json.old"
  log_info "旧密钥已备份到 ${OLD_KEY}"
}

# 阶段 3: 替换密钥
swap_keys() {
  log_info "替换验证者密钥..."
  mv "${NODE_HOME}/config/new_validator_key.json" \
     "${NODE_HOME}/config/priv_validator_key.json"

  # 重置状态 (避免使用旧签名的计数器)
  cat > "${NODE_HOME}/config/priv_validator_state.json" << EOF
{ "height": "0", "round": 0, "step": 0 }
EOF
  log_info "密钥已替换"
}

# 阶段 4: 重启节点
restart_node() {
  log_info "重启节点以加载新密钥..."
  systemctl restart msgchaind
  sleep 10

  for i in $(seq 1 30); do
    if curl -sf localhost:26657/status > /dev/null 2>&1; then
      log_info "节点已重启"
      return 0
    fi
    sleep 5
  done
  log_error "节点重启失败"
  return 1
}

# 阶段 5: 链上更新
update_on_chain() {
  log_info "请手动更新链上验证者信息:"

  NEW_PUBKEY=$(jq -r '.pub_key.value' "${NODE_HOME}/config/priv_validator_key.json")

  cat << EOF
=== 链上更新步骤 ===
1. 更新验证者公钥:
   msgchaind tx staking edit-validator \\
     --new-pubkey '{"@type":"/cosmos.crypto.ed25519.PubKey","key":"${NEW_PUBKEY}"}' \\
     --from validator \\
     --chain-id msg-chain-1 \\
     --gas auto

2. 监控更新状态:
   msgchaind query tx <tx_hash>

=== 备用策略 ===
如果密钥轮换后验证者状态异常:
1. 停止节点
2. 恢复旧密钥: cp ${OLD_KEY} ${NODE_HOME}/config/priv_validator_key.json
3. 重启节点: systemctl restart msgchaind
4. 调查原因后重试
EOF
}

# 主流程
main() {
  log_info "========== 验证者密钥轮换 =========="
  echo ""
  echo "⚠️  警告: 密钥轮换是一个高风险操作!"
  echo "错误的操作可能导致:"
  echo "  - 双重签名惩罚 (如果旧节点仍在线签名)"
  echo "  - 验证者离线 (如果新密钥无效)"
  echo "  - 需要等待 unbonding 周期"
  echo ""
  read -p "确认继续? (输入 'YES' 确认): " confirm
  [ "$confirm" != "YES" ] && { log_info "已取消"; exit 0; }

  prepare_new_key
  backup_old_key
  swap_keys
  restart_node
  update_on_chain

  log_info "========== 密钥轮换完成 =========="
}

main "$@"

7.4 验证者状态监控脚本

#!/bin/bash
# 脚本: validator_monitor.sh
# 功能: 验证者节点专项监控

set -euo pipefail

RPC_ENDPOINT="${1:-http://localhost:26657}"
VALIDATOR_ADDRESS="${2:-}"
LOG_FILE="/var/log/msgchain/validator_monitor.log"

log_info() { echo "[INFO] $(date '+%Y-%m-%d %H:%M:%S') $*" | tee -a "$LOG_FILE"; }
log_warn() { echo "[WARN] $(date '+%Y-%m-%d %H:%M:%S') $*" | tee -a "$LOG_FILE"; }
log_error() { echo "[ERROR] $(date '+%Y-%m-%d %H:%M:%S') $*" | tee -a "$LOG_FILE" >&2; }

# 获取验证者地址 (如果未提供)
if [ -z "$VALIDATOR_ADDRESS" ]; then
  VALIDATOR_ADDRESS=$(msgchaind tendermint show-address 2>/dev/null || echo "")
  [ -z "$VALIDATOR_ADDRESS" ] && { log_error "无法获取验证者地址"; exit 1; }
fi

# 1. 检查签名活动
check_signing() {
  local status=$(curl -sf "${RPC_ENDPOINT}/status" | jq '.result' 2>/dev/null || echo '{}')
  local height=$(echo "$status" | jq -r '.sync_info.latest_block_height // "0"')

  # 获取最近 100 个区块的签名信息
  local signed=0
  local missed=0
  for h in $(seq $((height - 100)) $height); do
    local block=$(curl -sf "${RPC_ENDPOINT}/block?height=${h}" 2>/dev/null || echo '{}')
    local proposer=$(echo "$block" | jq -r '.result.block.header.proposer_address // ""' 2>/dev/null)
    local signs=$(echo "$block" | jq '.result.block.last_commit.signatures | length' 2>/dev/null || echo "0")
    if [ "$proposer" = "$VALIDATOR_ADDRESS" ]; then
      ((signed++))
    fi
  done

  local missed_blocks=$((100 - signed))
  log_info "最近 100 区块: 签名 ${signed}, 错过 ${missed_blocks}"

  if [ "$missed_blocks" -gt 10 ]; then
    log_error "高错过率! ${missed_blocks}/100 区块错过签名"
    return 1
  fi
  return 0
}

# 2. 检查验证者状态
check_validator_status() {
  local val_info=$(curl -sf "${RPC_ENDPOINT}/validators?height=0" 2>/dev/null || echo '{}')
  local in_set=$(echo "$val_info" | jq -r ".result.validators[] | select(.address == \"$VALIDATOR_ADDRESS\") | .address" 2>/dev/null || echo "")

  if [ -n "$in_set" ]; then
    log_info "✓ 验证者在活跃验证者集合中"
  else
    log_warn "? 验证者不在活跃集合中 (可能被监禁或 unbonding)"
  fi
}

# 3. 检查共识轮次
check_consensus_round() {
  local cons=$(curl -sf "${RPC_ENDPOINT}/consensus_state" 2>/dev/null || echo '{}')
  local height=$(echo "$cons" | jq -r '.result.round_state.height // "N/A"' 2>/dev/null)
  local round=$(echo "$cons" | jq -r '.result.round_state.round // "N/A"' 2>/dev/null)
  local step=$(echo "$cons" | jq -r '.result.round_state.step // "N/A"' 2>/dev/null)

  log_info "共识: 高度=${height}, 轮次=${round}, 步骤=${step}"

  # 如果轮次过高,可能存在问题
  if [ "$round" != "N/A" ] && [ "$round" -gt 10 ]; then
    log_warn "共识轮次较高 (${round}),可能网络不稳定"
  fi
}

# 4. 检查投票权
check_voting_power() {
  local status=$(curl -sf "${RPC_ENDPOINT}/status" | jq '.result' 2>/dev/null || echo '{}')
  local vp=$(echo "$status" | jq -r '.validator_info.voting_power // "0"' 2>/dev/null)
  log_info "投票权: ${vp}"
  [ "$vp" = "0" ] && log_warn "投票权为零 (节点可能被监禁)"
}

# 主循环
main() {
  log_info "========== 验证者监控 =========="
  log_info "验证者地址: ${VALIDATOR_ADDRESS}"
  log_info "RPC 节点: ${RPC_ENDPOINT}"
  echo ""

  check_signing || true
  check_validator_status
  check_consensus_round
  check_voting_power

  log_info "========== 监控完成 =========="
}

main "$@"

7.5 自动解监禁脚本

#!/bin/bash
# 脚本: auto_unjail.sh
# 功能: 自动检测验证者监禁状态并解监禁

set -euo pipefail

VALIDATOR_ADDRESS="${VALIDATOR_ADDRESS:-}"
CHAIN_ID="msg-chain-1"
NODE_HOME="${HOME}/.msgchain"
LOG_FILE="/var/log/msgchain/unjail.log"
LAST_UNJAIL_FILE="/tmp/msgchain_last_unjail"

log_info() { echo "[INFO] $(date '+%Y-%m-%d %H:%M:%S') $*" | tee -a "$LOG_FILE"; }
log_error() { echo "[ERROR] $(date '+%Y-%m-%d %H:%M:%S') $*" | tee -a "$LOG_FILE" >&2; }

get_validator_address() {
  if [ -n "$VALIDATOR_ADDRESS" ]; then
    echo "$VALIDATOR_ADDRESS"
  else
    msgchaind tendermint show-address 2>/dev/null || echo ""
  fi
}

check_jailed() {
  local val_addr="$1"
  # 通过 RPC 查询验证者信息
  local val_info=$(msgchaind query staking validator "$val_addr" \
    --output json 2>/dev/null || echo '{}')
  local jailed=$(echo "$val_info" | jq -r '.jailed // "false"' 2>/dev/null)
  echo "$jailed"
}

perform_unjail() {
  log_info "执行 unjail 操作..."

  # 检查频率限制 (每 10 分钟最多一次)
  if [ -f "$LAST_UNJAIL_FILE" ]; then
    local last=$(cat "$LAST_UNJAIL_FILE")
    local now=$(date +%s)
    if [ $((now - last)) -lt 600 ]; then
      log_warn "上一次 unjail 在 10 分钟内,跳过"
      return 0
    fi
  fi

  msgchaind tx slashing unjail \
    --from validator \
    --chain-id "$CHAIN_ID" \
    --gas auto \
    --gas-prices "1000000000attoMSG" \
    -y 2>&1 | tee -a "$LOG_FILE"

  local result=$?
  if [ $result -eq 0 ]; then
    date +%s > "$LAST_UNJAIL_FILE"
    log_info "unjail 交易已提交"
  else
    log_error "unjail 失败"
  fi

  return $result
}

main() {
  log_info "========== 自动解监禁检查 =========="

  local val_addr=$(get_validator_address)
  [ -z "$val_addr" ] && { log_error "无法获取验证者地址"; exit 1; }

  local jailed=$(check_jailed "$val_addr")
  log_info "验证者 ${val_addr}: jailed=${jailed}"

  if [ "$jailed" = "true" ]; then
    log_error "验证者被监禁! 尝试解监禁..."
    perform_unjail
  else
    log_info "验证者状态正常"
  fi

  log_info "========== 检查完成 =========="
}

main "$@"

8. 测试灾难恢复

8.1 灾难恢复演练计划

8.1.1 演练频率

演练类型 频率 参与者 持续时间
桌面上演练 每季度 运维团队 2 小时
单节点恢复 每月 运维人员 1 小时
完整 DR 演练 每半年 全体团队 4 小时
密钥恢复 每季度 安全团队 1 小时

8.1.2 演练范围

场景编号 场景描述 演练类型 复杂度
DR-01 数据目录误删 单节点 简单
DR-02 BadgerDB 损坏 单节点 中等
DR-03 磁盘完全故障 单节点 中等
DR-04 哨兵节点批量故障 集群 复杂
DR-05 验证者密钥丢失 关键 高复杂
DR-06 验证者节点完全损毁 关键 高复杂
DR-07 数据中心级故障 跨区 极高

8.2 RTO/RPO 测试

#!/bin/bash
# 脚本: rto_rpo_test.sh
# 功能: 测试 MSG Chain 节点的恢复时间和数据丢失

set -euo pipefail

NODE_HOME="${HOME}/.msgchain"
BACKUP_BASE="/backup/msgchain"
TEST_DIR="/tmp/msgchain-dr-test-$(date +%s)"
REPORT_FILE="/var/log/msgchain/rto_rpo_report.log"

log_info() { echo "[INFO] $(date '+%Y-%m-%d %H:%M:%S') $*" | tee -a "$REPORT_FILE"; }
log_error() { echo "[ERROR] $(date '+%Y-%m-%d %H:%M:%S') $*" | tee -a "$REPORT_FILE" >&2; }

cleanup() { rm -rf "$TEST_DIR"; }
trap cleanup EXIT

# 阶段 1: 记录基准状态
phase1_baseline() {
  log_info "阶段 1: 记录基准状态..."

  BASELINE_HEIGHT=$(curl -sf localhost:26657/status | jq -r '.result.sync_info.latest_block_height')
  BASELINE_TIME=$(date +%s)
  BASELINE_BLOCK_TIME=$(curl -sf "localhost:26657/block?height=${BASELINE_HEIGHT}" | jq -r '.result.block.header.time')

  log_info "基准高度: ${BASELINE_HEIGHT}"
  log_info "基准区块时间: ${BASELINE_BLOCK_TIME}"
  log_info "基准时间戳: ${BASELINE_TIME}"

  echo "$BASELINE_HEIGHT" > "${TEST_DIR}/baseline_height.txt"
  echo "$BASELINE_TIME" > "${TEST_DIR}/baseline_time.txt"
}

# 阶段 2: 模拟灾难
phase2_simulate_disaster() {
  log_info "阶段 2: 模拟灾难 (删除数据目录)..."
  DISASTER_TIME=$(date +%s)

  systemctl stop msgchaind || true
  sleep 3

  # 模拟数据损坏:重命名数据目录
  if [ -d "${NODE_HOME}/data" ]; then
    mv "${NODE_HOME}/data" "${NODE_HOME}/data_disaster_$(date +%s)"
    log_info "数据目录已移除 (模拟灾难)"
  fi

  mkdir -p "${NODE_HOME}/data"
  echo "$DISASTER_TIME" > "${TEST_DIR}/disaster_time.txt"
}

# 阶段 3: 执行恢复
phase3_perform_recovery() {
  log_info "阶段 3: 执行恢复..."

  RECOVERY_START_TIME=$(date +%s)

  # 从最新备份恢复
  LATEST_BACKUP=$(ls -td "$BACKUP_BASE"/full/*/ 2>/dev/null | head -1)
  if [ -z "$LATEST_BACKUP" ]; then
    log_error "无可用备份,使用快照恢复"
    # 这里调用快照恢复脚本
    LATEST_SNAPSHOT=$(curl -sf "https://snapshots.msgchain.org/latest.json" | jq -r '.url')
    SNAPSHOT_FILE="${TEST_DIR}/snapshot.tar.lz4"
    wget -O "$SNAPSHOT_FILE" "$LATEST_SNAPSHOT"
    tar -I lz4 -xf "$SNAPSHOT_FILE" -C "${NODE_HOME}/data/"
    RECOVERY_TYPE="snapshot"
  else
    log_info "使用备份: ${LATEST_BACKUP}"
    tar -I zstd -xf "${LATEST_BACKUP}/data.tar.zst" -C "$NODE_HOME"
    RECOVERY_TYPE="backup"
  fi

  # 重置 CometBFT
  msgchaind tendermint unsafe-reset-all --keep-addr-book || true

  # 启动节点
  systemctl start msgchaind

  # 等待节点健康
  for i in $(seq 1 60); do
    if curl -sf localhost:26657/health > /dev/null 2>&1; then
      RECOVERY_READY_TIME=$(date +%s)
      log_info "节点恢复可用!"
      break
    fi
    sleep 5
  done

  # 等待同步完成
  for i in $(seq 1 120); do
    sleep 5
    local status=$(curl -sf localhost:26657/status 2>/dev/null || echo '{}')
    local catching_up=$(echo "$status" | jq -r '.result.sync_info.catching_up // "true"')
    if [ "$catching_up" = "false" ]; then
      RECOVERY_FULL_TIME=$(date +%s)
      SYNCED_HEIGHT=$(echo "$status" | jq -r '.result.sync_info.latest_block_height')
      log_info "节点完全同步!"
      break
    fi
  done

  echo "$RECOVERY_START_TIME" > "${TEST_DIR}/recovery_start.txt"
  echo "${RECOVERY_READY_TIME:-$RECOVERY_START_TIME}" > "${TEST_DIR}/recovery_ready.txt"
  echo "${RECOVERY_FULL_TIME:-$RECOVERY_START_TIME}" > "${TEST_DIR}/recovery_full.txt"
  echo "$RECOVERY_TYPE" > "${TEST_DIR}/recovery_type.txt"
}

# 阶段 4: 计算指标
phase4_calculate_metrics() {
  log_info "阶段 4: 计算 RTO/RPO 指标..."

  BASELINE_HEIGHT=$(cat "${TEST_DIR}/baseline_height.txt")
  BASELINE_TIME=$(cat "${TEST_DIR}/baseline_time.txt")
  DISASTER_TIME=$(cat "${TEST_DIR}/disaster_time.txt")
  RECOVERY_START=$(cat "${TEST_DIR}/recovery_start.txt")
  RECOVERY_READY=$(cat "${TEST_DIR}/recovery_ready.txt")
  RECOVERY_FULL=$(cat "${TEST_DIR}/recovery_full.txt")

  # RPO 计算: 从备份时间到灾难时间的差额
  BACKUP_TIME=$(stat -c %Y "$(ls -td "$BACKUP_BASE"/full/*/ 2>/dev/null | head -1)"/backup_metadata.json 2>/dev/null || echo "$DISASTER_TIME")
  RPO=$(( (DISASTER_TIME - BACKUP_TIME) / 60 ))
  log_info ""
  log_info "========== 测试结果 =========="
  log_info "恢复类型: $(cat "${TEST_DIR}/recovery_type.txt")"
  log_info "备份时间: $(date -d "@$BACKUP_TIME")"
  log_info "灾难时间: $(date -d "@$DISASTER_TIME")"
  log_info "恢复开始: $(date -d "@$RECOVERY_START")"
  log_info "服务恢复: $(date -d "@$RECOVERY_READY")"
  log_info "完全同步: $(date -d "@$RECOVERY_FULL")"
  log_info ""
  log_info "--- RPO ---"
  log_info "理论 RPO: ${RPO} 分钟"
  log_info "数据丢失窗口: ${RPO} 分钟"
  log_info ""
  log_info "--- RTO ---"
  RTO_READY=$(( (RECOVERY_READY - RECOVERY_START) / 60 ))
  RTO_FULL=$(( (RECOVERY_FULL - RECOVERY_START) / 60 ))
  log_info "服务恢复 RTO: ${RTO_READY} 分钟"
  log_info "完全同步 RTO: ${RTO_FULL} 分钟"
  log_info ""
  log_info "--- 高度差异 ---"
  log_info "基准高度: ${BASELINE_HEIGHT}"
  log_info "同步后高度: $(cat "${TEST_DIR}/recovery_full.txt" 2>/dev/null || echo "N/A")"
  log_info "================================"
}

# 主流程
main() {
  log_info "========== RTO/RPO 测试开始 =========="
  mkdir -p "$TEST_DIR"

  phase1_baseline
  phase2_simulate_disaster
  phase3_perform_recovery
  phase4_calculate_metrics

  log_info "========== RTO/RPO 测试完成 =========="
  log_info "报告文件: ${REPORT_FILE}"
}

main "$@"

8.3 桌面积上演练

# MSG Chain 灾难恢复桌面积上演练脚本

## 场景 DR-01: 数据目录误删

**演练目标**: 验证从误删数据目录到节点恢复的全流程

**参与角色**:
- 操作员: 执行恢复操作
- 观察员: 记录时间线和操作步骤
- 审批人: 批准恢复操作

**时间线**:

| 时间 | 事件 | 操作 |
|------|------|------|
| T+0 | 告警: 节点停机 | 操作员收到宕机告警 |
| T+5 | 确认: 数据目录被误删 | 操作员检查 journalctl 日志 |
| T+10 | 决策: 从快照恢复 | 审批人批准恢复方案 |
| T+15 | 执行: 停止节点 | systemctl stop msgchaind |
| T+20 | 执行: 下载最新快照 | wget snapshot |
| T+30 | 执行: 解压快照 | tar -I lz4 -xf |
| T+35 | 执行: 重置 CometBFT | msgchaind tendermint unsafe-reset-all |
| T+38 | 执行: 启动节点 | systemctl start msgchaind |
| T+45 | 验证: 节点健康 | curl /health |
| T+60 | 验证: 完全同步 | curl /status (catching_up=false) |

**检查清单**:

- [ ] 告警通知是否及时送达
- [ ] 恢复操作是否遵循标准流程
- [ ] 快照下载是否顺利
- [ ] 校验和验证是否通过
- [ ] 节点启动是否正常
- [ ] 同步完成后交易是否可以正常查询
- [ ] RTO 是否在目标范围内
- [ ] RPO 是否在目标范围内

**复盘问题**:

1. 数据目录是何时、如何被误删的?
2. 如何防止类似事件再次发生?
3. 恢复过程中是否有可优化的环节?
4. 是否需要更新标准操作流程 (SOP)?

---

## 场景 DR-05: 验证者密钥丢失

**演练目标**: 测试验证者密钥恢复流程

**前置条件**:
- 密钥已通过安全方式备份
- 备份的完整性已定期验证

**恢复步骤**:

1. 确认密钥丢失范围 (仅节点本地 / 备份也丢失)
2. 如果本地密钥仅损坏 → 从备份恢复
3. 如果本地和备份均丢失:
   a. 使用助记词恢复账户密钥
   b. 生成新的验证者密钥
   c. 提交验证者公钥更新交易
   d. 等待 unbonding 周期 (21 天)
   e. 重新加入验证者集合

**风险**:
- 如果无法恢复密钥,验证者将永久失去验证者身份
- unbonding 周期内没有质押收益
- 需要确保新旧密钥没有同时签名 (防止双签)

---

## 场景 DR-07: 数据中心级故障

**演练目标**: 验证跨数据中心的完整 DR 流程

**场景描述**: 主数据中心完全不可用 (火灾/电力中断)

**恢复方案**:

1. 在备用数据中心启动节点集群
2. 从异地备份恢复数据
3. 更新 DNS 记录指向备用中心
4. 验证验证者状态并重新加入共识
5. 主数据中心恢复后,同步并回切

**观测指标**:
- 备用中心启动时间
- 数据恢复时间
- DNS 切换延迟
- 共识重新加入时间
- 总 RTO/RPO

8.4 DR 演练报告模板

# MSG Chain 灾难恢复演练报告

## 基本信息

| 项目 | 内容 |
|------|------|
| 演练编号 | DR-2026-Q2-001 |
| 演练日期 | 2026-XX-XX |
| 演练场景 | [场景编号和名称] |
| 演练类型 | ☐ 桌面 ☐ 单节点 ☐ 集群 ☐ 完整DR |
| 参与人员 | [姓名列表] |
| 演练负责人 | [姓名] |

## 演练时间线

| 操作 | 计划时间 | 实际时间 | 偏差 |
|------|----------|----------|------|
| 开始 | | | |
| 阶段1: 准备 | | | |
| 阶段2: 灾难模拟 | | | |
| 阶段3: 恢复操作 | | | |
| 阶段4: 验证 | | | |
| 阶段5: 回切 | | | |
| 结束 | | | |

## 指标结果

| 指标 | 目标值 | 实际值 | 达标 |
|------|--------|--------|------|
| RTO | < 30 分钟 | ___ 分钟 | ☐ 是 ☐ 否 |
| RPO | < 1 小时 | ___ 分钟 | ☐ 是 ☐ 否 |
| 操作步骤数 | | ___ 步 | |
| 人工干预 | | ___ 次 | |

## 问题与改进

### 发现的问题

| # | 问题描述 | 严重程度 | 责任人 | 计划修复日期 |
|---|----------|----------|--------|-------------|
| 1 | | | | |
| 2 | | | | |

### 改进建议

1. 
2. 
3. 

## 审批

| 角色 | 姓名 | 签名 | 日期 |
|------|------|------|------|
| 演练负责人 | | | |
| 运维负责人 | | | |
| 安全负责人 | | | |

8.5 自动化 DR 测试

#!/bin/bash
# 脚本: automated_dr_test.sh
# 功能: 自动化灾难恢复测试套件

set -euo pipefail

TEST_SUITE="${1:-all}"
REPORT_DIR="/var/log/msgchain/dr_tests"
mkdir -p "$REPORT_DIR"

log_suite() { echo "=== Suite: $1 ==="; }
log_test() { echo "  Test: $1"; }
pass() { echo "    ✓ PASS"; }
fail() { echo "    ✗ FAIL: $1"; }

# 测试 1: 备份完整性
test_backup_integrity() {
  log_test "备份完整性测试"
  local latest=$(ls -t /backup/msgchain/full/*/data.tar.zst 2>/dev/null | head -1)
  [ -z "$latest" ] && { fail "无可用备份"; return 1; }
  tar -I zstd -tf "$latest" > /dev/null 2>&1 || { fail "备份损坏"; return 1; }
  pass
}

# 测试 2: 快照下载
test_snapshot_download() {
  log_test "快照下载测试"
  local tmp=/tmp/dr_test_snapshot
  mkdir -p "$tmp"
  local url=$(curl -sf "https://snapshots.msgchain.org/latest.json" | jq -r '.url // ""' 2>/dev/null)
  [ -z "$url" ] && { url="https://snapshots.msgchain.org/latest.tar.lz4"; }
  timeout 120 wget -q -O "$tmp/snapshot.tar.lz4" "$url" 2>/dev/null || { fail "下载超时"; rm -rf "$tmp"; return 1; }
  tar -I lz4 -tf "$tmp/snapshot.tar.lz4" > /dev/null 2>&1 || { fail "格式错误"; rm -rf "$tmp"; return 1; }
  rm -rf "$tmp"
  pass
}

# 测试 3: 节点启动
test_node_startup() {
  log_test "节点启动测试"
  systemctl start msgchaind 2>/dev/null || { fail "启动失败"; return 1; }
  sleep 10
  curl -sf localhost:26657/health > /dev/null 2>&1 || { fail "健康检查失败"; return 1; }
  systemctl stop msgchaind 2>/dev/null || true
  pass
}

# 测试 4: 密钥验证
test_key_validation() {
  log_test "验证者密钥验证"
  msgchaind tendermint show-validator > /dev/null 2>&1 || { fail "密钥无效"; return 1; }
  pass
}

# 测试 5: 配置完整性
test_config_integrity() {
  log_test "配置完整性测试"
  local home="${HOME}/.msgchain"
  for f in config.toml app.toml genesis.json; do
    [ -f "${home}/config/${f}" ] || { fail "缺少 ${f}"; return 1; }
  done
  msgchaind validate-genesis > /dev/null 2>&1 || { fail "创世文件无效"; return 1; }
  pass
}

# 运行特定套件或全部
run_all() {
  log_suite "完整 DR 测试套件"
  test_backup_integrity
  test_snapshot_download
  test_node_startup
  test_key_validation
  test_config_integrity
}

case "$TEST_SUITE" in
  backup)    test_backup_integrity ;;
  snapshot)  test_snapshot_download ;;
  node)      test_node_startup ;;
  key)       test_key_validation ;;
  config)    test_config_integrity ;;
  all)       run_all ;;
  *)         echo "未知测试套件: $TEST_SUITE"; exit 1 ;;
esac

附录

A. 关键命令速查

操作 命令
初始化节点 msgchaind init <moniker> --chain-id msg-chain-1
启动节点 msgchaind start
停止节点 systemctl stop msgchaind
重启节点 systemctl restart msgchaind
查看日志 journalctl -u msgchaind -f -n 100
检查同步状态 curl -s localhost:26657/status | jq .result.sync_info
检查对等节点 curl -s localhost:26657/net_info | jq '.result.n_peers'
查看验证者 msgchaind tendermint show-validator
重置 CometBFT msgchaind tendermint unsafe-reset-all
创建快照 msgchaind snapshot create --height <h>
验证创世文件 msgchaind validate-genesis
查询账户余额 msgchaind query bank balances <addr>
提交 unjail msgchaind tx slashing unjail --from <key> --chain-id msg-chain-1

B. 目录结构

~/.msgchain/
├── config/
│   ├── config.toml          # CometBFT 配置
│   ├── app.toml             # 应用配置
│   ├── genesis.json         # 创世文件
│   ├── node_key.json        # 节点 P2P 密钥
│   ├── priv_validator_key.json  # 验证者密钥
│   ├── priv_validator_state.json # 验证者状态
│   └── addrbook.json        # 对等节点地址簿
├── data/
│   ├── badger/              # BadgerDB 应用状态
│   │   ├── *.sst            # SST 文件
│   │   └── *.vlog           # 值日志文件
│   ├── blockstore.db/       # 区块存储 (LevelDB)
│   ├── state.db/            # 共识状态 (LevelDB)
│   ├── evidence.db/         # 证据存储 (LevelDB)
│   ├── tx_index.db/         # 交易索引 (LevelDB)
│   ├── cs.wal/              # 共识 WAL
│   └── snapshots/           # 快照目录
├── keyring-file/            # 密钥环文件
└── cosmovisor/              # Cosmovisor 升级 (可选)

C. 端口参考

端口 协议 用途 安全建议
26656 TCP P2P 通信 公网开放
26657 TCP RPC 接口 内部网络 / TLS
26658 TCP CometBFT gRPC 内部网络
26660 TCP Prometheus 指标 内部网络
9090 TCP Cosmos gRPC 内部网络 / TLS
1317 TCP REST API 内部网络 / LB
9091 TCP gRPC-web 内部网络

D. 环境变量

变量 默认值 说明
CHAIN_ID msg-chain-1 链标识符
NODE_HOME ~/.msgchain 节点家目录
MONIKER (hostname) 节点名称
SNAPSHOT_INTERVAL 5000 快照创建间隔
SNAPSHOT_KEEP_RECENT 10 保留快照数量
MIN_GAS_PRICES 1000000000attoMSG 最小 gas 价格
PERSISTENT_PEERS `` 持久对等节点列表
SEEDS `` 种子节点列表

E. 常见问题排查

E.1 节点无法启动 - "value log corrupted"

# 停止节点
systemctl stop msgchaind

# 备份损坏数据
cp -r ~/.msgchain/data/badger ~/.msgchain/data/badger_corrupted

# 运行 BadgerDB 修复工具
msgchaind debug badger-repair --data-dir ~/.msgchain/data/badger

# 如果修复失败,从备份恢复

E.2 同步停滞

# 检查对等节点连接
curl -s localhost:26657/net_info | jq '.result.n_peers'

# 如果对等节点不足,添加更多持久节点
# 检查是否被限速
journalctl -u msgchaind --since "10 min ago" | grep -i "rate\|limit\|throttle"

E.3 "too many open files"

# 检查当前限制
ulimit -n
# 增大限制 (需要 root)
echo "fs.file-max = 65536" >> /etc/sysctl.conf
sysctl -p
# 更新 systemd 服务配置

E.4 磁盘空间不足

# 检查磁盘使用
df -h ~/.msgchain/data
du -sh ~/.msgchain/data/* | sort -rh | head -10

# 清理不需要的快照
rm -rf ~/.msgchain/data/snapshots/*

# 清理旧交易索引 (如果不需要历史查询)
# 或添加磁盘后迁移数据目录

F. 推荐硬件规格

节点类型 CPU 内存 磁盘 网络
验证者 8 核 32 GB 2 TB NVMe 1 Gbps
哨兵 8 核 32 GB 2 TB NVMe 1 Gbps
RPC 节点 16 核 64 GB 4 TB NVMe 10 Gbps
归档节点 32 核 128 GB 10+ TB NVMe 10 Gbps

G. 相关资源

H. 版本历史

版本 日期 修改内容 作者
1.0 2026-07-06 初版创建 MSG Chain 运维团队