ovtools 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +121 -2
- package/assets/common/bond-inspect.sh +368 -0
- package/assets/common/config.sh +263 -0
- package/assets/common/extract-data.sh +197 -0
- package/assets/common/harden-host.sh +37 -0
- package/assets/common/install-deps.sh +93 -0
- package/assets/common/node-enroll.sh +65 -0
- package/assets/common/node-setup.sh +150 -0
- package/assets/common/node-start.sh +90 -0
- package/assets/common/node-stop.sh +18 -0
- package/assets/common/node-upgrade.sh +45 -0
- package/assets/common/pre-run.sh +497 -0
- package/assets/common/promote-validator.sh +104 -0
- package/assets/deploy/10-prepare-host.sh +98 -0
- package/assets/deploy/20-install-toolchain.sh +93 -0
- package/assets/deploy/30-setup-node.sh +82 -0
- package/assets/deploy/40-start-node.sh +75 -0
- package/assets/deploy/50-verify.sh +259 -0
- package/assets/deploy/60-enroll.sh +304 -0
- package/assets/deploy/70-promote.sh +152 -0
- package/assets/deploy/lib.sh +398 -0
- package/assets/manage/10-bond-increase.sh +149 -0
- package/assets/manage/10-diagnose-node.sh +169 -0
- package/assets/manage/20-exit-validator.sh +259 -0
- package/assets/manage/25-recover-node.sh +291 -0
- package/assets/manage/30-status.sh +64 -0
- package/assets/manage/40-exit-withdraw.sh +133 -0
- package/assets/manage/bond_inc.sh +318 -0
- package/assets/manage/dashboard_monitor/app.js +1544 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/css/base.css +113 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/css/components/content.css +133 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/css/components/controls.css +229 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/css/components/data.css +374 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/css/components/icons.css +35 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/css/components/motion.css +57 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/css/components/shell.css +51 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/css/components/sidebar.css +242 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/css/components/topbar.css +138 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/css/fonts.css +30 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/css/synteq-ui-v2.css +25 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/css/theme-dark.css +38 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/css/theme-light.css +36 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/css/tokens.css +61 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/fonts/GoogleSansCode-Medium.ttf +0 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/fonts/GoogleSansCode-Regular.ttf +0 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/fonts/GoogleSansCode-SemiBold.ttf +0 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/js/synteq-ui-v2.js +869 -0
- package/assets/manage/dashboard_monitor/deploy-dashboard.sh +230 -0
- package/assets/manage/dashboard_monitor/favicon.svg +13 -0
- package/assets/manage/dashboard_monitor/index.html +1197 -0
- package/assets/manage/dashboard_monitor/nginx-octra.conf +76 -0
- package/assets/manage/dashboard_monitor/octra-dashboard.service +29 -0
- package/assets/manage/dashboard_monitor/octra-node-prune.cron +12 -0
- package/assets/manage/dashboard_monitor/octra-node-prune.service +16 -0
- package/assets/manage/dashboard_monitor/octra-node-prune.timer +12 -0
- package/assets/manage/dashboard_monitor/prune-node-junk.sh +329 -0
- package/assets/manage/dashboard_monitor/server.py +1887 -0
- package/assets/manage/dashboard_monitor/ssl-certbot.sh +90 -0
- package/assets/manage/dashboard_monitor/styles.css +327 -0
- package/assets/manage/lib.sh +347 -0
- package/assets/manifest.json +143 -0
- package/assets/migrate/00-backup-identity.sh +67 -0
- package/assets/migrate/01-prepare-target.sh +145 -0
- package/assets/migrate/02-authorize-pull.sh +43 -0
- package/assets/migrate/03-pull-state.sh +102 -0
- package/assets/migrate/04-stop-source.sh +85 -0
- package/assets/migrate/05-finalize-target.sh +140 -0
- package/assets/migrate/06-start-target.sh +57 -0
- package/assets/migrate/07-verify.sh +237 -0
- package/assets/migrate/08-rollback.sh +54 -0
- package/assets/migrate/lib.sh +149 -0
- package/assets/upgrade/10-preflight.sh +120 -0
- package/assets/upgrade/20-upgrade.sh +142 -0
- package/assets/upgrade/30-manual-upgrade.sh +129 -0
- package/assets/upgrade/40-storage-prune.sh +65 -0
- package/assets/upgrade/50-status.sh +58 -0
- package/assets/upgrade/lib.sh +232 -0
- package/bin/ovtools.js +32 -0
- package/dist/api/checks.d.ts +6 -0
- package/dist/api/checks.d.ts.map +1 -0
- package/dist/api/checks.js +173 -0
- package/dist/api/checks.js.map +1 -0
- package/dist/api/hostkeys.d.ts +10 -0
- package/dist/api/hostkeys.d.ts.map +1 -0
- package/dist/api/hostkeys.js +58 -0
- package/dist/api/hostkeys.js.map +1 -0
- package/dist/api/jobs.d.ts +33 -0
- package/dist/api/jobs.d.ts.map +1 -0
- package/dist/api/jobs.js +483 -0
- package/dist/api/jobs.js.map +1 -0
- package/dist/api/nodes.d.ts +16 -0
- package/dist/api/nodes.d.ts.map +1 -0
- package/dist/api/nodes.js +126 -0
- package/dist/api/nodes.js.map +1 -0
- package/dist/api/server.d.ts +3 -0
- package/dist/api/server.d.ts.map +1 -0
- package/dist/api/server.js +443 -0
- package/dist/api/server.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +301 -0
- package/dist/cli.js.map +1 -0
- package/dist/constants.d.ts +76 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +150 -0
- package/dist/constants.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +60 -0
- package/dist/index.js.map +1 -0
- package/dist/orchestrator/base.d.ts +52 -0
- package/dist/orchestrator/base.d.ts.map +1 -0
- package/dist/orchestrator/base.js +219 -0
- package/dist/orchestrator/base.js.map +1 -0
- package/dist/orchestrator/deploy.d.ts +33 -0
- package/dist/orchestrator/deploy.d.ts.map +1 -0
- package/dist/orchestrator/deploy.js +313 -0
- package/dist/orchestrator/deploy.js.map +1 -0
- package/dist/orchestrator/manage.d.ts +28 -0
- package/dist/orchestrator/manage.d.ts.map +1 -0
- package/dist/orchestrator/manage.js +267 -0
- package/dist/orchestrator/manage.js.map +1 -0
- package/dist/orchestrator/migrate.d.ts +31 -0
- package/dist/orchestrator/migrate.d.ts.map +1 -0
- package/dist/orchestrator/migrate.js +442 -0
- package/dist/orchestrator/migrate.js.map +1 -0
- package/dist/ssh/client.d.ts +39 -0
- package/dist/ssh/client.d.ts.map +1 -0
- package/dist/ssh/client.js +418 -0
- package/dist/ssh/client.js.map +1 -0
- package/dist/test/core.test.d.ts +2 -0
- package/dist/test/core.test.d.ts.map +1 -0
- package/dist/test/core.test.js +188 -0
- package/dist/test/core.test.js.map +1 -0
- package/dist/types.d.ts +267 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/assets.d.ts +28 -0
- package/dist/utils/assets.d.ts.map +1 -0
- package/dist/utils/assets.js +583 -0
- package/dist/utils/assets.js.map +1 -0
- package/dist/utils/browser.d.ts +2 -0
- package/dist/utils/browser.d.ts.map +1 -0
- package/dist/utils/browser.js +23 -0
- package/dist/utils/browser.js.map +1 -0
- package/dist/utils/dashboard.d.ts +16 -0
- package/dist/utils/dashboard.d.ts.map +1 -0
- package/dist/utils/dashboard.js +177 -0
- package/dist/utils/dashboard.js.map +1 -0
- package/dist/utils/helpers.d.ts +29 -0
- package/dist/utils/helpers.d.ts.map +1 -0
- package/dist/utils/helpers.js +204 -0
- package/dist/utils/helpers.js.map +1 -0
- package/dist/utils/scripts.d.ts +60 -0
- package/dist/utils/scripts.d.ts.map +1 -0
- package/dist/utils/scripts.js +174 -0
- package/dist/utils/scripts.js.map +1 -0
- package/dist/utils/state.d.ts +12 -0
- package/dist/utils/state.d.ts.map +1 -0
- package/dist/utils/state.js +44 -0
- package/dist/utils/state.js.map +1 -0
- package/dist/utils/tar.d.ts +11 -0
- package/dist/utils/tar.d.ts.map +1 -0
- package/dist/utils/tar.js +93 -0
- package/dist/utils/tar.js.map +1 -0
- package/dist/utils/update.d.ts +9 -0
- package/dist/utils/update.d.ts.map +1 -0
- package/dist/utils/update.js +134 -0
- package/dist/utils/update.js.map +1 -0
- package/dist/utils/websocket.d.ts +10 -0
- package/dist/utils/websocket.d.ts.map +1 -0
- package/dist/utils/websocket.js +203 -0
- package/dist/utils/websocket.js.map +1 -0
- package/package.json +55 -4
- package/public/app.js +2712 -0
- package/public/assets/v2.2/css/base.css +113 -0
- package/public/assets/v2.2/css/components/content.css +133 -0
- package/public/assets/v2.2/css/components/controls.css +229 -0
- package/public/assets/v2.2/css/components/data.css +373 -0
- package/public/assets/v2.2/css/components/icons.css +35 -0
- package/public/assets/v2.2/css/components/motion.css +57 -0
- package/public/assets/v2.2/css/components/shell.css +51 -0
- package/public/assets/v2.2/css/components/sidebar.css +242 -0
- package/public/assets/v2.2/css/components/topbar.css +138 -0
- package/public/assets/v2.2/css/fonts.css +30 -0
- package/public/assets/v2.2/css/synteq-ui-v2.css +25 -0
- package/public/assets/v2.2/css/theme-dark.css +38 -0
- package/public/assets/v2.2/css/theme-light.css +36 -0
- package/public/assets/v2.2/css/tokens.css +61 -0
- package/public/assets/v2.2/fonts/GoogleSansCode-Medium.ttf +0 -0
- package/public/assets/v2.2/fonts/GoogleSansCode-Regular.ttf +0 -0
- package/public/assets/v2.2/fonts/GoogleSansCode-SemiBold.ttf +0 -0
- package/public/assets/v2.2/js/synteq-ui-v2.js +869 -0
- package/public/favicon.svg +7 -0
- package/public/index.html +1080 -0
- package/public/styles.css +552 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ===================================================================
|
|
3
|
+
# 10-diagnose-node.sh - WHY is this node not voting? (read-only)
|
|
4
|
+
#
|
|
5
|
+
# The state an operator reports as "ready / outside set" can mean four very
|
|
6
|
+
# different things, and they need different actions:
|
|
7
|
+
#
|
|
8
|
+
# rpc_slow the node answers /rpc but /status times out -> every tool that
|
|
9
|
+
# reads /status reports "rpc unavailable" (stat.sh, enroll.sh)
|
|
10
|
+
# frozen head_epoch does not move: catch-up cannot fetch ranges
|
|
11
|
+
# (`range_http_unavailable ... state_sync_range_busy`) and the
|
|
12
|
+
# consensus ignores every vote (`unresolved_validator_set`)
|
|
13
|
+
# behind head_epoch moves, but the node is still far from the network
|
|
14
|
+
# pm2_mismatch rejoin.sh refuses: "data directory is used outside PM2" - the
|
|
15
|
+
# running process and the pm2 entry disagree about who owns the
|
|
16
|
+
# data directory, so the node's own recovery tools refuse to run
|
|
17
|
+
# outside_set in sync and healthy, but not admitted by the set (ready/bonded,
|
|
18
|
+
# scheduled=false) -> it needs a fold, not a repair
|
|
19
|
+
# active fine
|
|
20
|
+
# recovery the local store failed its checkpoint check -> recover.sh
|
|
21
|
+
#
|
|
22
|
+
# Nothing here changes anything: it signs nothing, restarts nothing, moves no
|
|
23
|
+
# file. It prints the evidence and one machine line the console reads:
|
|
24
|
+
#
|
|
25
|
+
# NODE_DIAG state=<class> head=<n> network=<n> behind=<n> rpc=<ready|slow|down>
|
|
26
|
+
#
|
|
27
|
+
# Usage: sudo bash 10-diagnose-node.sh
|
|
28
|
+
# Env: HEAD_SAMPLE_SEC=<sec> how long the head is watched for movement (20)
|
|
29
|
+
# SYNC_LAG_MAX=<n> epochs behind that still count as "in sync" (5)
|
|
30
|
+
# ===================================================================
|
|
31
|
+
|
|
32
|
+
set -euo pipefail
|
|
33
|
+
source "$(dirname "$0")/lib.sh"
|
|
34
|
+
|
|
35
|
+
require_root
|
|
36
|
+
set_status RUNNING "diagnose-node"
|
|
37
|
+
|
|
38
|
+
ensure_tool jq jq || die "jq is missing and could not be installed"
|
|
39
|
+
ensure_tool curl curl || die "curl is missing and could not be installed"
|
|
40
|
+
[[ -f "${NODE_ENV}" ]] || die "node.env is missing (${NODE_ENV}) - is this the validator host?"
|
|
41
|
+
|
|
42
|
+
SAMPLE="${HEAD_SAMPLE_SEC:-20}"
|
|
43
|
+
LAG_MAX="${SYNC_LAG_MAX:-5}"
|
|
44
|
+
|
|
45
|
+
WALLET=$(wallet_address || echo unknown)
|
|
46
|
+
log "=== diagnose ${WALLET} on $(hostname) ==="
|
|
47
|
+
|
|
48
|
+
# --- 1. the process ------------------------------------------------------
|
|
49
|
+
PS=$(pm2_state)
|
|
50
|
+
log "pm2 : ${PS:-<no entry for $(pm2_name)>}"
|
|
51
|
+
PROC_STATUS=$(printf '%s' "${PS}" | awk '{print $2}')
|
|
52
|
+
case "${PROC_STATUS}" in
|
|
53
|
+
online) : ;;
|
|
54
|
+
"") warn "pm2 has no entry named '$(pm2_name)' - the node is not supervised" ;;
|
|
55
|
+
*) warn "pm2 reports '${PROC_STATUS}' for $(pm2_name)" ;;
|
|
56
|
+
esac
|
|
57
|
+
|
|
58
|
+
# --- 2. RPC: the small call vs the big snapshot --------------------------
|
|
59
|
+
RPC_STATE="down"
|
|
60
|
+
ENROLL=$(rpc_field octra_validatorEnrollment '[]' state)
|
|
61
|
+
HEAD=$(head_epoch)
|
|
62
|
+
CODE=$(status_code)
|
|
63
|
+
if [[ "${HEAD}" =~ ^[0-9]+$ ]]; then
|
|
64
|
+
RPC_STATE="ready"
|
|
65
|
+
[[ "${CODE}" == "200" ]] || RPC_STATE="slow"
|
|
66
|
+
fi
|
|
67
|
+
log "rpc : /rpc=${RPC_STATE} (/status HTTP ${CODE:-000})"
|
|
68
|
+
[[ "${RPC_STATE}" == "slow" ]] && warn "the /status snapshot times out, so stat.sh and enroll.sh status read 'RPC unavailable' even though the node answers /rpc"
|
|
69
|
+
log "enrollment : state=${ENROLL:-?} head_epoch=${HEAD:-?}"
|
|
70
|
+
BOND_RAW=$(rpc_field octra_validatorEnrollment '[]' bond)
|
|
71
|
+
[[ -n "${BOND_RAW}" ]] && log "bond : $(raw_to_oct "${BOND_RAW}") OCT"
|
|
72
|
+
|
|
73
|
+
# --- 3. is head moving? --------------------------------------------------
|
|
74
|
+
HEAD2="${HEAD}"
|
|
75
|
+
if [[ "${HEAD}" =~ ^[0-9]+$ ]] && (( SAMPLE > 0 )); then
|
|
76
|
+
sleep "${SAMPLE}"
|
|
77
|
+
HEAD2=$(head_epoch)
|
|
78
|
+
fi
|
|
79
|
+
NET_EPOCH=$(network_epoch)
|
|
80
|
+
BEHIND=""
|
|
81
|
+
if [[ "${HEAD2}" =~ ^[0-9]+$ && "${NET_EPOCH}" =~ ^[0-9]+$ ]]; then
|
|
82
|
+
BEHIND=$(( NET_EPOCH - HEAD2 ))
|
|
83
|
+
(( BEHIND < 0 )) && BEHIND=0
|
|
84
|
+
fi
|
|
85
|
+
MOVED=$(( ${HEAD2:-0} - ${HEAD:-0} ))
|
|
86
|
+
log "head : ${HEAD:-?} -> ${HEAD2:-?} in ${SAMPLE}s (network epoch ${NET_EPOCH:-?}, behind ${BEHIND:-?})"
|
|
87
|
+
|
|
88
|
+
# --- 4. what the node itself says about the set / catch-up ---------------
|
|
89
|
+
REASON=$(catchup_reason)
|
|
90
|
+
[[ -n "${REASON}" ]] && log "catch-up : ${REASON}"
|
|
91
|
+
POSTFAIL=$(post_hint)
|
|
92
|
+
[[ -n "${POSTFAIL}" ]] && log "duty post : ${POSTFAIL}"
|
|
93
|
+
|
|
94
|
+
# --- 4b. memory and startup progress ------------------------------------
|
|
95
|
+
# A node that opens a 38 GB store on a box that is too small spends its time in
|
|
96
|
+
# GC/swap and never finishes starting: process up, RPC never listening, and no new
|
|
97
|
+
# log line for many minutes (2026-11-06, .99: 5.9 GB RAM, node RSS 4.3 GB).
|
|
98
|
+
MEM_TOTAL=$(awk '/MemTotal/ {printf "%d", $2/1024}' /proc/meminfo 2>/dev/null || echo 0)
|
|
99
|
+
MEM_AVAIL=$(awk '/MemAvailable/ {printf "%d", $2/1024}' /proc/meminfo 2>/dev/null || echo 0)
|
|
100
|
+
NODE_PID=$(pgrep -x octra_node.exe 2>/dev/null | head -1 || true)
|
|
101
|
+
NODE_RSS=""
|
|
102
|
+
[[ -n "${NODE_PID}" ]] && NODE_RSS=$(awk '/VmRSS/ {printf "%d", $2/1024}' "/proc/${NODE_PID}/status" 2>/dev/null || true)
|
|
103
|
+
log "memory : total=${MEM_TOTAL:-?}MB available=${MEM_AVAIL:-?}MB node_rss=${NODE_RSS:-n/a}MB${NODE_PID:+ (pid ${NODE_PID})}"
|
|
104
|
+
if (( ${MEM_TOTAL:-0} > 0 && MEM_TOTAL < 7600 )); then
|
|
105
|
+
warn "less than 8 GB of RAM: the store needs ~8 GB (16 comfortable) - GC/swap makes the node slow or unable to finish starting"
|
|
106
|
+
fi
|
|
107
|
+
LOG_AGE=0
|
|
108
|
+
if [[ -f "${NODE_LOG}" ]]; then
|
|
109
|
+
LAST_TS=$(date -r "${NODE_LOG}" +%s 2>/dev/null || echo 0)
|
|
110
|
+
LOG_AGE=$(( $(date +%s) - LAST_TS ))
|
|
111
|
+
fi
|
|
112
|
+
if [[ -n "${NODE_PID}" && "${RPC_STATE}" != "ready" && ${LOG_AGE} -ge 300 ]]; then
|
|
113
|
+
warn "the node has been up for a while without a new log line (${LOG_AGE}s) and its RPC is not answering: startup is stalled"
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
# --- 5. the node's own recovery tool (it knows the set rules) ------------
|
|
117
|
+
REJOIN=$(node_script 180 "sh controls/rejoin.sh --check" || true)
|
|
118
|
+
REJOIN_LINE=$(printf '%s\n' "${REJOIN}" | grep -aE '^status = ' | tail -1 || true)
|
|
119
|
+
log "rejoin --check: ${REJOIN_LINE:-<no status line>}"
|
|
120
|
+
REJOIN_REFUSED=$(printf '%s' "${REJOIN_LINE}" | sed -n 's/.*reason = //p')
|
|
121
|
+
PM2_MISMATCH=0
|
|
122
|
+
printf '%s' "${REJOIN_REFUSED}" | grep -qi "used outside PM2" && PM2_MISMATCH=1
|
|
123
|
+
RECOVERY=0
|
|
124
|
+
|
|
125
|
+
# --- 6. classify ---------------------------------------------------------
|
|
126
|
+
STATE="unknown"
|
|
127
|
+
if (( RECOVERY )); then
|
|
128
|
+
STATE="recovery"
|
|
129
|
+
elif [[ -n "${NODE_PID}" && "${RPC_STATE}" != "ready" && ${LOG_AGE} -ge 300 ]]; then
|
|
130
|
+
STATE="startup_stalled"
|
|
131
|
+
elif [[ "${RPC_STATE}" == "down" ]]; then
|
|
132
|
+
STATE="rpc_down"
|
|
133
|
+
elif (( PM2_MISMATCH )); then
|
|
134
|
+
STATE="pm2_mismatch"
|
|
135
|
+
elif [[ "${RPC_STATE}" == "slow" && "${MOVED}" -le 0 ]]; then
|
|
136
|
+
STATE="frozen"
|
|
137
|
+
elif [[ -n "${BEHIND}" ]] && (( BEHIND > LAG_MAX )); then
|
|
138
|
+
STATE="behind"
|
|
139
|
+
elif (( MOVED <= 0 )); then
|
|
140
|
+
STATE="frozen"
|
|
141
|
+
elif printf '%s' "${REJOIN_LINE}" | grep -q "validator_active"; then
|
|
142
|
+
STATE="active"
|
|
143
|
+
elif printf '%s' "${REJOIN_LINE}" | grep -q "validator_scheduled"; then
|
|
144
|
+
STATE="scheduled"
|
|
145
|
+
elif [[ "${ENROLL}" =~ ^(ready|bonded)$ ]]; then
|
|
146
|
+
STATE="outside_set"
|
|
147
|
+
fi
|
|
148
|
+
|
|
149
|
+
case "${STATE}" in
|
|
150
|
+
active) log "verdict : this node is the ACTIVE validator - nothing to fix" ;;
|
|
151
|
+
scheduled) log "verdict : it is in the next set - waiting for the fold, nothing to fix" ;;
|
|
152
|
+
"outside_set") log "verdict : in sync and healthy, but not admitted yet (enrollment=${ENROLL}): it needs a set fold, not a repair" ;;
|
|
153
|
+
frozen) log "verdict : head_epoch is not moving, catch-up is blocked (${REASON:-no reason logged}) - a clean restart re-handshakes peers and asks for a fresh range" ;;
|
|
154
|
+
behind) log "verdict : still catching up (${BEHIND} epochs behind) - let it finish before touching the set" ;;
|
|
155
|
+
"pm2_mismatch") log "verdict : the node's own tool refuses to run because the data directory is 'used outside PM2' - the pm2 entry and the running process disagree, so the official rejoin path cannot work" ;;
|
|
156
|
+
"startup_stalled") log "verdict : the process is up but it stopped logging and its RPC never opened - the node cannot finish opening the store on this VM. Check the RAM line above: the store needs ~8 GB (16 comfortable). With less, GC/swap wins and the node never reaches the network (this is what happened on .99: 5.9 GB box, 4.3 GB node). Options: raise the RAM and start again, or move the node with Migrate node." ;;
|
|
157
|
+
"rpc_down") log "verdict : the local RPC does not answer at all" ;;
|
|
158
|
+
recovery) log "verdict : the local store failed its checkpoint check - the node's own tool asks for recover.sh" ;;
|
|
159
|
+
*) log "verdict : could not be classified from the evidence above" ;;
|
|
160
|
+
esac
|
|
161
|
+
|
|
162
|
+
printf 'NODE_DIAG state=%s head=%s network=%s behind=%s rpc=%s moved=%s enroll=%s rejoin=%s pm2=%s\n' \
|
|
163
|
+
"${STATE}" "${HEAD2:-none}" "${NET_EPOCH:-none}" "${BEHIND:-none}" "${RPC_STATE}" "${MOVED}" \
|
|
164
|
+
"${ENROLL:-none}" "$(printf '%s' "${REJOIN_LINE}" | sed -n 's/^status = \([a-z_]*\).*/\1/p')" \
|
|
165
|
+
"${PROC_STATUS:-none}"
|
|
166
|
+
|
|
167
|
+
set_status DONE "state=${STATE} head=${HEAD2:-?} behind=${BEHIND:-?} enrollment=${ENROLL:-?} rpc=${RPC_STATE}"
|
|
168
|
+
|
|
169
|
+
printf '%s' "${REJOIN_LINE}" | grep -qi "recovery_required" && RECOVERY=1
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ===================================================================
|
|
3
|
+
# 20-exit-validator.sh - leave the validator set and get the stake back
|
|
4
|
+
#
|
|
5
|
+
# The node's own controls do the signing; this step drives the whole sequence:
|
|
6
|
+
#
|
|
7
|
+
# 1. `controls/enroll.sh exit` -> exit intent + exit transaction
|
|
8
|
+
# (state becomes `exiting`, the duty loop stops publishing readiness)
|
|
9
|
+
# 2. report WHEN the money comes back: the exit epoch the chain recorded and
|
|
10
|
+
# the epoch the bond unlocks (`withdraw_epoch`), converted to a duration
|
|
11
|
+
# with the measured epoch cadence - the two numbers the operator asks for
|
|
12
|
+
# 3. if the unbonding is already over: `controls/enroll.sh withdraw` and wait
|
|
13
|
+
# until `validator_bond` is 0 (the stake is back in the wallet)
|
|
14
|
+
# 4. otherwise: arm octra-exit-withdraw.timer on the host. The timer retries
|
|
15
|
+
# the withdrawal every 15 minutes - also after a reboot and with the
|
|
16
|
+
# console closed - and disarms itself once the bond is gone. Nothing has to
|
|
17
|
+
# be re-run by hand, and no week-long wait happens inside a step.
|
|
18
|
+
#
|
|
19
|
+
# Usage: sudo bash 20-exit-validator.sh
|
|
20
|
+
# Env: EXIT_WAIT=<sec> wait for the set fold (default 900)
|
|
21
|
+
# WITHDRAW_WAIT=<sec> wait for the stake after the withdraw (default 900)
|
|
22
|
+
# STATUS_POLL=<sec> poll interval (default 20)
|
|
23
|
+
# EXIT_PROGRESS_SEC=<sec> progress line interval (default 120)
|
|
24
|
+
# EPOCH_SECONDS=<sec> epoch cadence (default: measured from the chain)
|
|
25
|
+
# Idempotent: a second run resumes wherever the first one stopped.
|
|
26
|
+
# ===================================================================
|
|
27
|
+
|
|
28
|
+
set -euo pipefail
|
|
29
|
+
source "$(dirname "$0")/lib.sh"
|
|
30
|
+
|
|
31
|
+
require_root
|
|
32
|
+
set_status RUNNING "exit-validator"
|
|
33
|
+
|
|
34
|
+
EXIT_WAIT="${EXIT_WAIT:-900}"
|
|
35
|
+
WITHDRAW_WAIT="${WITHDRAW_WAIT:-900}"
|
|
36
|
+
POLL="${STATUS_POLL:-20}"
|
|
37
|
+
|
|
38
|
+
ensure_tool jq jq || die "jq is missing and could not be installed"
|
|
39
|
+
[[ -f "${NODE_WALLET}" ]] || die "wallet.json is missing (${NODE_WALLET}) - is this the validator host?"
|
|
40
|
+
[[ -f "${NODE_ENV}" ]] || die "node.env is missing (${NODE_ENV})"
|
|
41
|
+
|
|
42
|
+
read_state
|
|
43
|
+
ADDR=$(wallet_address || echo unknown)
|
|
44
|
+
log "=== exit validator: ${ADDR} ==="
|
|
45
|
+
log "node: role=${ROLE:-?} process=${PROCESS:-?} rpc=${RPC:-?} voting=${VOTING:-?} epoch=${EPOCH:-?}"
|
|
46
|
+
log "membership: active=${ACTIVE:-?} scheduled=${SCHED:-?} enrollment=${ENROLL:-?} bond=$(raw_to_oct "${BOND:-0}") OCT"
|
|
47
|
+
|
|
48
|
+
# Already gone? (a previous run finished, or another host did it)
|
|
49
|
+
if [[ "${BOND:-0}" == "0" && "${ENROLL:-}" != "bonded" && "${ENROLL:-}" != "ready" && "${ENROLL:-}" != "exiting" ]]; then
|
|
50
|
+
log "this wallet holds no bond on chain (enrollment=${ENROLL:-absent}) - nothing to exit"
|
|
51
|
+
exit_watch_disarm
|
|
52
|
+
echo "EXIT_DONE bond=0 state=${ENROLL:-absent}"
|
|
53
|
+
set_status DONE "already exited (enrollment=${ENROLL:-absent}, bond 0)"
|
|
54
|
+
exit 0
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
status_events() { node_script 120 "sh controls/enroll.sh status" || true; }
|
|
58
|
+
|
|
59
|
+
# one line: state=exiting exit_epoch=123 withdraw_epoch=456 head=789 remaining=8000
|
|
60
|
+
enrollment_summary() {
|
|
61
|
+
local events state exit_ep withdraw_ep head remaining
|
|
62
|
+
events=$(status_events)
|
|
63
|
+
state=$(printf '%s\n' "${events}" | grep -oE 'committed_enrollment state = [a-z]+' | awk '{print $4}' | tail -1 || true)
|
|
64
|
+
exit_ep=$(printf '%s\n' "${events}" | grep -oE 'exit_epoch = [0-9]+' | awk '{print $3}' | tail -1 || true)
|
|
65
|
+
withdraw_ep=$(printf '%s\n' "${events}" | grep -oE 'withdraw_epoch = [0-9]+' | awk '{print $3}' | tail -1 || true)
|
|
66
|
+
head=$(printf '%s\n' "${events}" | grep -oE 'head_epoch = [0-9]+' | awk '{print $3}' | tail -1 || true)
|
|
67
|
+
remaining=$(printf '%s\n' "${events}" | grep -oE 'remaining_epochs = [0-9]+' | awk '{print $3}' | tail -1 || true)
|
|
68
|
+
printf 'state=%s exit_epoch=%s withdraw_epoch=%s head=%s remaining=%s\n' \
|
|
69
|
+
"${state:-unknown}" "${exit_ep:-none}" "${withdraw_ep:-none}" "${head:-0}" "${remaining:-none}"
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
summary_value() { printf '%s' "$1" | sed -n "s/.*$2=\([^ ]*\).*/\1/p"; }
|
|
73
|
+
|
|
74
|
+
# The one line the console reads: state, both epochs, epochs left and the
|
|
75
|
+
# duration that follows from the measured cadence. Printed as soon as the chain
|
|
76
|
+
# has recorded the exit, i.e. before any wait - the operator learns the answer
|
|
77
|
+
# at once instead of watching a spinner for a week.
|
|
78
|
+
announce_schedule() {
|
|
79
|
+
local summary="$1" state exit_ep withdraw_ep head remaining left rate secs eta
|
|
80
|
+
state=$(summary_value "${summary}" state)
|
|
81
|
+
exit_ep=$(summary_value "${summary}" exit_epoch)
|
|
82
|
+
withdraw_ep=$(summary_value "${summary}" withdraw_epoch)
|
|
83
|
+
head=$(summary_value "${summary}" head)
|
|
84
|
+
remaining=$(summary_value "${summary}" remaining)
|
|
85
|
+
rate=""
|
|
86
|
+
secs=""
|
|
87
|
+
eta=""
|
|
88
|
+
if [[ "${state}" == "exiting" && "${withdraw_ep}" =~ ^[0-9]+$ ]]; then
|
|
89
|
+
[[ "${remaining}" =~ ^[0-9]+$ ]] || remaining=$(( withdraw_ep - ${head:-0} ))
|
|
90
|
+
(( remaining < 0 )) && remaining=0
|
|
91
|
+
left="${remaining}"
|
|
92
|
+
rate=$(epoch_seconds || true)
|
|
93
|
+
if [[ -n "${rate}" ]]; then
|
|
94
|
+
secs=$(epoch_eta "${left}" "${rate}" || true)
|
|
95
|
+
[[ -n "${secs}" ]] && eta=$(fmt_duration "${secs}")
|
|
96
|
+
fi
|
|
97
|
+
log "the chain recorded the exit at epoch ${exit_ep}"
|
|
98
|
+
if (( remaining == 0 )); then
|
|
99
|
+
log "the unbonding period is over (unlock epoch ${withdraw_ep}, head ${head}) - the stake can be withdrawn"
|
|
100
|
+
else
|
|
101
|
+
log "the stake unlocks at epoch ${withdraw_ep}: ${remaining} epoch(s) from head ${head}${eta:+ - about ${eta} at ${rate}s per epoch}"
|
|
102
|
+
fi
|
|
103
|
+
fi
|
|
104
|
+
printf 'EXIT_SCHEDULE state=%s exit_epoch=%s withdraw_epoch=%s head=%s remaining=%s rate=%s eta_seconds=%s\n' \
|
|
105
|
+
"${state}" "${exit_ep}" "${withdraw_ep}" "${head:-0}" "${left:-none}" "${rate:-none}" "${secs:-none}"
|
|
106
|
+
SCHED_STATE="${state}" # the next caller can skip a second measurement
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
# --- 1. submit the exit ----------------------------------------------
|
|
110
|
+
SUMMARY=$(enrollment_summary)
|
|
111
|
+
log "enrollment: ${SUMMARY}"
|
|
112
|
+
if [[ "${SUMMARY}" == *"state=exiting"* ]]; then
|
|
113
|
+
log "the exit is already committed - not signing another one"
|
|
114
|
+
else
|
|
115
|
+
log "submitting the exit (controls/enroll.sh exit) - this pauses the validator duty"
|
|
116
|
+
set +e
|
|
117
|
+
OUT=$(node_script 600 "sh controls/enroll.sh exit")
|
|
118
|
+
RC=$?
|
|
119
|
+
set -e
|
|
120
|
+
printf '%s\n' "${OUT}" | sed 's/^/ /'
|
|
121
|
+
if (( RC != 0 )); then
|
|
122
|
+
if printf '%s' "${OUT}" | grep -q "already committed"; then
|
|
123
|
+
log "the exit was already committed on chain - continuing"
|
|
124
|
+
elif printf '%s' "${OUT}" | grep -qE "refused|retry same command|retained"; then
|
|
125
|
+
# the node keeps the signed transaction: the same command resumes it and the
|
|
126
|
+
# exit lands at the next fold. That is a schedule, not an error.
|
|
127
|
+
warn "the node kept the signed exit transaction: $(printf '%s' "${OUT}" | grep -oE 'reason = .*' | tail -1)"
|
|
128
|
+
warn "the exit lands at the next set fold - nothing else has to be signed"
|
|
129
|
+
elif printf '%s' "${OUT}" | grep -q "bond"; then
|
|
130
|
+
warn "controls/enroll.sh exit reports nothing to exit (or it is already exiting)"
|
|
131
|
+
else
|
|
132
|
+
die "controls/enroll.sh exit failed (exit ${RC}) - see the output above"
|
|
133
|
+
fi
|
|
134
|
+
fi
|
|
135
|
+
fi
|
|
136
|
+
|
|
137
|
+
read_state
|
|
138
|
+
SUMMARY=$(enrollment_summary)
|
|
139
|
+
announce_schedule "${SUMMARY}"
|
|
140
|
+
|
|
141
|
+
# --- 2. wait (bounded) until the set released us and the unbonding is over --
|
|
142
|
+
START=$(date +%s)
|
|
143
|
+
LAST=${START}
|
|
144
|
+
VERDICT=""
|
|
145
|
+
while :; do
|
|
146
|
+
read_state
|
|
147
|
+
SUMMARY=$(enrollment_summary)
|
|
148
|
+
STATE=$(summary_value "${SUMMARY}" state)
|
|
149
|
+
WITHDRAW_EP=$(summary_value "${SUMMARY}" withdraw_epoch)
|
|
150
|
+
HEAD=$(summary_value "${SUMMARY}" head)
|
|
151
|
+
NOW=$(date +%s)
|
|
152
|
+
|
|
153
|
+
if [[ "${STATE}" == "absent" && "${BOND:-0}" == "0" ]]; then
|
|
154
|
+
log "the wallet is out of the set and holds no bond any more"
|
|
155
|
+
exit_watch_disarm
|
|
156
|
+
echo "EXIT_DONE bond=0 state=absent"
|
|
157
|
+
set_status DONE "exited (enrollment absent, bond 0)"
|
|
158
|
+
exit 0
|
|
159
|
+
fi
|
|
160
|
+
|
|
161
|
+
# withdrawable: exiting + not in the set + unbonding epoch reached
|
|
162
|
+
if [[ "${STATE}" == "exiting" && "${ACTIVE:-false}" != "true" && "${SCHED:-false}" != "true" ]] \
|
|
163
|
+
&& { [[ -z "${WITHDRAW_EP}" || "${WITHDRAW_EP}" == "none" ]] \
|
|
164
|
+
|| { [[ "${HEAD:-0}" =~ ^[0-9]+$ ]] && [[ "${WITHDRAW_EP}" =~ ^[0-9]+$ ]] && (( HEAD >= WITHDRAW_EP )); }; }; then
|
|
165
|
+
VERDICT="withdrawable"
|
|
166
|
+
break
|
|
167
|
+
fi
|
|
168
|
+
|
|
169
|
+
# The exit is committed and the set already released this validator, but the
|
|
170
|
+
# unbonding period is still running (weeks). There is NOTHING to wait for here:
|
|
171
|
+
# the host timer retries until the unlock epoch arrives, so the step reports the
|
|
172
|
+
# schedule and stops instead of sleeping for 15 minutes and saying "waiting for
|
|
173
|
+
# the set" while the set has long released it (2026-11-06, seen on .231).
|
|
174
|
+
if [[ "${STATE}" == "exiting" && "${ACTIVE:-false}" != "true" && "${SCHED:-false}" != "true" ]]; then
|
|
175
|
+
VERDICT="unbonding"
|
|
176
|
+
break
|
|
177
|
+
fi
|
|
178
|
+
|
|
179
|
+
if (( NOW - START >= EXIT_WAIT )); then
|
|
180
|
+
VERDICT="pending"
|
|
181
|
+
break
|
|
182
|
+
fi
|
|
183
|
+
if (( NOW - LAST >= EXIT_PROGRESS_SEC )); then
|
|
184
|
+
HINT=$(post_hint)
|
|
185
|
+
REASON="the set has not released this validator yet"
|
|
186
|
+
[[ "${STATE}" == "exiting" ]] && REASON="the set is still folding the exit out"
|
|
187
|
+
log " waiting: ${REASON} (${SUMMARY}) active=${ACTIVE:-?} scheduled=${SCHED:-?}${HINT:+ (network: ${HINT})}"
|
|
188
|
+
LAST=${NOW}
|
|
189
|
+
fi
|
|
190
|
+
sleep "${POLL}"
|
|
191
|
+
done
|
|
192
|
+
|
|
193
|
+
# --- 3. not yet: hand the long part to the host timer ----------------
|
|
194
|
+
# `pending` (the set has not released us yet) and `unbonding` (it did, the epoch
|
|
195
|
+
# has not arrived) end the same way: report the schedule, arm the timer, stop.
|
|
196
|
+
if [[ "${VERDICT}" != "withdrawable" ]]; then
|
|
197
|
+
# the schedule was already printed right after the exit committed: do not print
|
|
198
|
+
# (and measure) it twice - only when the state really changed since then
|
|
199
|
+
if [[ "${SCHED_STATE:-}" != "$(summary_value "${SUMMARY}" state)" ]]; then
|
|
200
|
+
announce_schedule "${SUMMARY}"
|
|
201
|
+
fi
|
|
202
|
+
exit_watch_install
|
|
203
|
+
if [[ "${STATE}" == "exiting" ]]; then
|
|
204
|
+
set_status WAITING-WITHDRAW "exited at epoch $(summary_value "${SUMMARY}" exit_epoch); the stake unlocks at epoch $(summary_value "${SUMMARY}" withdraw_epoch) ($(summary_value "${SUMMARY}" remaining) epoch(s) left) - the host withdraws it automatically"
|
|
205
|
+
else
|
|
206
|
+
set_status WAITING-EXIT "exit submitted; the set releases this validator at the next fold, then the stake unlocks at epoch $(summary_value "${SUMMARY}" withdraw_epoch) - the host withdraws it automatically"
|
|
207
|
+
fi
|
|
208
|
+
log "nothing else to do now: the console keeps following the unbonding, and this host retries the"
|
|
209
|
+
log "withdrawal every 15 minutes (systemd timer ${EXIT_WATCH_UNIT}.timer) - re-running this phase is safe"
|
|
210
|
+
exit 2
|
|
211
|
+
fi
|
|
212
|
+
|
|
213
|
+
log "unbonding finished (${SUMMARY}) - withdrawing the stake"
|
|
214
|
+
set +e
|
|
215
|
+
OUT=$(node_script 900 "sh controls/enroll.sh withdraw")
|
|
216
|
+
RC=$?
|
|
217
|
+
set -e
|
|
218
|
+
printf '%s\n' "${OUT}" | sed 's/^/ /'
|
|
219
|
+
if (( RC != 0 )); then
|
|
220
|
+
if printf '%s' "${OUT}" | grep -qE "unbonding period incomplete|withdraw_epoch|waits for removal"; then
|
|
221
|
+
warn "the chain is not ready for the withdrawal yet - the host timer keeps retrying"
|
|
222
|
+
exit_watch_install
|
|
223
|
+
set_status WAITING-WITHDRAW "withdrawal not signed yet (network) - the host retries it automatically"
|
|
224
|
+
exit 2
|
|
225
|
+
fi
|
|
226
|
+
if printf '%s' "${OUT}" | grep -qE "already|previously_confirmed"; then
|
|
227
|
+
log "the withdrawal was already submitted"
|
|
228
|
+
else
|
|
229
|
+
die "controls/enroll.sh withdraw failed (exit ${RC}) - see the output above"
|
|
230
|
+
fi
|
|
231
|
+
fi
|
|
232
|
+
|
|
233
|
+
# --- 5. wait until the bond is back in the wallet ---------------------
|
|
234
|
+
START=$(date +%s)
|
|
235
|
+
LAST=${START}
|
|
236
|
+
while :; do
|
|
237
|
+
read_state
|
|
238
|
+
NOW=$(date +%s)
|
|
239
|
+
if [[ "${BOND:-0}" == "0" ]]; then
|
|
240
|
+
SUMMARY=$(enrollment_summary)
|
|
241
|
+
log "stake returned: validator_bond = 0 (${SUMMARY})"
|
|
242
|
+
exit_watch_disarm
|
|
243
|
+
echo "EXIT_DONE bond=0 state=$(summary_value "${SUMMARY}" state)"
|
|
244
|
+
set_status DONE "exited and withdrawn (bond 0, exit epoch $(summary_value "${SUMMARY}" exit_epoch))"
|
|
245
|
+
exit 0
|
|
246
|
+
fi
|
|
247
|
+
if (( NOW - START >= WITHDRAW_WAIT )); then
|
|
248
|
+
warn "the registry still shows a bond of $(raw_to_oct "${BOND:-0}") OCT after ${WITHDRAW_WAIT}s"
|
|
249
|
+
warn "the withdrawal transaction is on chain - the host timer keeps checking, this phase can be re-run"
|
|
250
|
+
exit_watch_install
|
|
251
|
+
set_status WAITING-WITHDRAW "withdraw submitted - bond still $(raw_to_oct "${BOND:-0}") OCT; the host keeps checking"
|
|
252
|
+
exit 2
|
|
253
|
+
fi
|
|
254
|
+
if (( NOW - LAST >= 60 )); then
|
|
255
|
+
log " waiting for the stake: bond $(raw_to_oct "${BOND:-0}") OCT ($(( NOW - START ))s of ${WITHDRAW_WAIT}s)"
|
|
256
|
+
LAST=${NOW}
|
|
257
|
+
fi
|
|
258
|
+
sleep "${POLL}"
|
|
259
|
+
done
|