#!/usr/bin/env bash # 空跑验全链:真连交易所读规则/持仓,**不下任何真单**。 # # 上真单之前必须过这一步。它验的是那些"上线才会暴露、且暴露方式是花钱"的 # 环节:密钥能不能用、IP 白名单对不对、时钟同不同步、搬运通不通、 # 数量与价位取整合不合交易所规则。 set -euo pipefail APP=/opt/chan CONF=/etc/chan-live/live.env STATE=/var/lib/chan-live USER_NAME=chan SECS="${SECS:-90}" die() { echo "⛔ $*" >&2; exit 1; } ok() { echo " ✓ $*"; } [[ $EUID -eq 0 ]] || die "要 root:sudo $0" [[ -f "$CONF" ]] || die "缺 $CONF,先跑 install.sh" set -a; . "$CONF"; set +a echo "── 1. 配置自检 ──" for v in BITGET_API_KEY BITGET_API_SECRET BITGET_PASSPHRASE SHIP_FROM; do [[ -n "${!v:-}" ]] || die "$CONF 里 $v 还是空的" done ok "密钥三项与 SHIP_FROM 都已填" [[ "$LIVE_HOME" != /opt/chan* ]] || die "LIVE_HOME 不能指到仓库里(会被 git 清掉)" ok "LIVE_HOME=$LIVE_HOME 在仓库外" echo "── 2. 时钟 ──" # staleness 闸靠两机时钟一致才有意义。这里只能验本机;跨机偏差由 # ship_signals 在收到信号时报「时间倒流」 if command -v chronyc >/dev/null; then chronyc tracking | grep -E "Reference ID|System time|Leap status" | sed 's/^/ /' src="$(chronyc tracking | awk '/Reference ID/{print $NF}')" [[ "$src" != "()" && -n "$src" ]] || die "chrony 还没同步上,等一会再跑" ok "chrony 已同步" else die "没装 chrony。staleness 闸不可信,先 apt install chrony" fi echo "── 3. 出口 IP 是否在白名单内 ──" myip="$(curl -s --max-time 10 https://api.ipify.org || true)" [[ -n "$myip" ]] && echo " 本机出口 IP:$myip" || echo " ⚠ 取不到出口 IP" echo " 对照 Bitget 后台该 key 的 IP 白名单,不一致下面会报 40018 之类" echo "── 4. 能否 ssh 到采集机 ──" # 不能用 ssh 'echo ok' 来试:采集机那侧的 authorized_keys 用了强制命令 # (把这把 key 锁成只能跑 tail,拿不到 shell),任何请求都会变成 tail -F, # 而它**永不返回**——ConnectTimeout 只管建连不管命令时长,测试会永久挂住。 # 所以照 ship_signals 的真实用法读流:tail -c +0 会先把整个文件吐出来, # 数一下就等于对端总线的条数,之后它挂着等新内容,由 timeout 收掉。 rb="${SHIP_REMOTE_BUS:-\$HOME/chan-live/state/signals_live.jsonl}" err="$(mktemp)"; outf="$(mktemp)" # timeout 要套在 sudo **里面**:套外面时 SIGTERM 只到 sudo,未必传给 ssh, # 于是该被收掉的 tail 会继续挂着 sudo -u "$USER_NAME" timeout 12 ssh -T -o BatchMode=yes -o ConnectTimeout=10 \ "$SHIP_FROM" "tail -c +0 -F $rb" >"$outf" 2>"$err" || true n="$(wc -l < "$outf")" if grep -qi "Host key verification failed" "$err"; then fp="(在采集机上跑 ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub 拿)" rm -f "$err" "$outf" die "主机指纹没确认过。这不是 key 的问题,装 key 也修不了。 $USER_NAME 的 known_hosts 是空的,而 BatchMode=yes 不允许交互确认。 **不要**用 StrictHostKeyChecking=no 糊过去,那等于放弃中间人防护。 正确做法:在采集机上读出权威指纹 $fp, 再在这台上写入并核对: sudo -u $USER_NAME ssh-keyscan -t ed25519 <采集机IP> \\ | sudo -u $USER_NAME tee -a $STATE/home/.ssh/known_hosts sudo -u $USER_NAME ssh-keygen -lf $STATE/home/.ssh/known_hosts" elif grep -qiE "Permission denied|publickey" "$err"; then rm -f "$err" "$outf" die "认证被拒。指纹是通的,是 key 没装到采集机上: sudo -u $USER_NAME ssh-keygen -t ed25519 -N '' -f $STATE/home/.ssh/id_ed25519 再把 $STATE/home/.ssh/id_ed25519.pub 加到采集机的 authorized_keys" elif [[ -s "$err" ]] && ! grep -q "^" "$outf" 2>/dev/null; then msg="$(head -3 "$err")"; rm -f "$err" "$outf" die "ssh $SHIP_FROM 不通:$msg" else ok "ssh $SHIP_FROM 通" echo " 对端总线现有 $n 条信号" [[ "$n" -gt 0 ]] || echo " ⚠ 对端总线是空的。信号 6.8 个/天,刚重启过就是空的很正常;但要确认采集机接了总线" rm -f "$err" "$outf" fi echo "── 5. 执行器空跑 ${SECS}s(真连交易所,不下单)──" # --dry-run 下只读不写:下单一律只打印。合约规则那个端点是**公开**的、不验签, # 所以空跑里专门补了一次带签名的 account() —— 否则这一步会"通过"却根本没测到 # 密钥与 IP 白名单,等第一个真信号来时才暴露,而那时信号正在过期 # 让 chan 自己 source 配置($CONF 是 640 root:chan,它读得到)。 # 不用 `env "$(grep ...)"` 那种拼法:值里有空格就会被切开 set +e sudo -u "$USER_NAME" bash -c \ "set -a; . '$CONF'; set +a; exec timeout $SECS \ '$APP/.venv/bin/python' '$APP/live/live_exec.py' --dry-run" rc=$? set -e # timeout 到点是 124,属于预期 [[ $rc -eq 124 || $rc -eq 0 ]] || die "空跑退出码 $rc,看上面报错" ok "空跑没有报错退出" echo echo "空跑过了。真跑:" echo " sudo systemctl enable --now chan-live-ship chan-live-exec" echo " $APP/live/deploy/status.sh"