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.
Files changed (198) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +121 -2
  3. package/assets/common/bond-inspect.sh +368 -0
  4. package/assets/common/config.sh +263 -0
  5. package/assets/common/extract-data.sh +197 -0
  6. package/assets/common/harden-host.sh +37 -0
  7. package/assets/common/install-deps.sh +93 -0
  8. package/assets/common/node-enroll.sh +65 -0
  9. package/assets/common/node-setup.sh +150 -0
  10. package/assets/common/node-start.sh +90 -0
  11. package/assets/common/node-stop.sh +18 -0
  12. package/assets/common/node-upgrade.sh +45 -0
  13. package/assets/common/pre-run.sh +497 -0
  14. package/assets/common/promote-validator.sh +104 -0
  15. package/assets/deploy/10-prepare-host.sh +98 -0
  16. package/assets/deploy/20-install-toolchain.sh +93 -0
  17. package/assets/deploy/30-setup-node.sh +82 -0
  18. package/assets/deploy/40-start-node.sh +75 -0
  19. package/assets/deploy/50-verify.sh +259 -0
  20. package/assets/deploy/60-enroll.sh +304 -0
  21. package/assets/deploy/70-promote.sh +152 -0
  22. package/assets/deploy/lib.sh +398 -0
  23. package/assets/manage/10-bond-increase.sh +149 -0
  24. package/assets/manage/10-diagnose-node.sh +169 -0
  25. package/assets/manage/20-exit-validator.sh +259 -0
  26. package/assets/manage/25-recover-node.sh +291 -0
  27. package/assets/manage/30-status.sh +64 -0
  28. package/assets/manage/40-exit-withdraw.sh +133 -0
  29. package/assets/manage/bond_inc.sh +318 -0
  30. package/assets/manage/dashboard_monitor/app.js +1544 -0
  31. package/assets/manage/dashboard_monitor/assets/v2.2/css/base.css +113 -0
  32. package/assets/manage/dashboard_monitor/assets/v2.2/css/components/content.css +133 -0
  33. package/assets/manage/dashboard_monitor/assets/v2.2/css/components/controls.css +229 -0
  34. package/assets/manage/dashboard_monitor/assets/v2.2/css/components/data.css +374 -0
  35. package/assets/manage/dashboard_monitor/assets/v2.2/css/components/icons.css +35 -0
  36. package/assets/manage/dashboard_monitor/assets/v2.2/css/components/motion.css +57 -0
  37. package/assets/manage/dashboard_monitor/assets/v2.2/css/components/shell.css +51 -0
  38. package/assets/manage/dashboard_monitor/assets/v2.2/css/components/sidebar.css +242 -0
  39. package/assets/manage/dashboard_monitor/assets/v2.2/css/components/topbar.css +138 -0
  40. package/assets/manage/dashboard_monitor/assets/v2.2/css/fonts.css +30 -0
  41. package/assets/manage/dashboard_monitor/assets/v2.2/css/synteq-ui-v2.css +25 -0
  42. package/assets/manage/dashboard_monitor/assets/v2.2/css/theme-dark.css +38 -0
  43. package/assets/manage/dashboard_monitor/assets/v2.2/css/theme-light.css +36 -0
  44. package/assets/manage/dashboard_monitor/assets/v2.2/css/tokens.css +61 -0
  45. package/assets/manage/dashboard_monitor/assets/v2.2/fonts/GoogleSansCode-Medium.ttf +0 -0
  46. package/assets/manage/dashboard_monitor/assets/v2.2/fonts/GoogleSansCode-Regular.ttf +0 -0
  47. package/assets/manage/dashboard_monitor/assets/v2.2/fonts/GoogleSansCode-SemiBold.ttf +0 -0
  48. package/assets/manage/dashboard_monitor/assets/v2.2/js/synteq-ui-v2.js +869 -0
  49. package/assets/manage/dashboard_monitor/deploy-dashboard.sh +230 -0
  50. package/assets/manage/dashboard_monitor/favicon.svg +13 -0
  51. package/assets/manage/dashboard_monitor/index.html +1197 -0
  52. package/assets/manage/dashboard_monitor/nginx-octra.conf +76 -0
  53. package/assets/manage/dashboard_monitor/octra-dashboard.service +29 -0
  54. package/assets/manage/dashboard_monitor/octra-node-prune.cron +12 -0
  55. package/assets/manage/dashboard_monitor/octra-node-prune.service +16 -0
  56. package/assets/manage/dashboard_monitor/octra-node-prune.timer +12 -0
  57. package/assets/manage/dashboard_monitor/prune-node-junk.sh +329 -0
  58. package/assets/manage/dashboard_monitor/server.py +1887 -0
  59. package/assets/manage/dashboard_monitor/ssl-certbot.sh +90 -0
  60. package/assets/manage/dashboard_monitor/styles.css +327 -0
  61. package/assets/manage/lib.sh +347 -0
  62. package/assets/manifest.json +143 -0
  63. package/assets/migrate/00-backup-identity.sh +67 -0
  64. package/assets/migrate/01-prepare-target.sh +145 -0
  65. package/assets/migrate/02-authorize-pull.sh +43 -0
  66. package/assets/migrate/03-pull-state.sh +102 -0
  67. package/assets/migrate/04-stop-source.sh +85 -0
  68. package/assets/migrate/05-finalize-target.sh +140 -0
  69. package/assets/migrate/06-start-target.sh +57 -0
  70. package/assets/migrate/07-verify.sh +237 -0
  71. package/assets/migrate/08-rollback.sh +54 -0
  72. package/assets/migrate/lib.sh +149 -0
  73. package/assets/upgrade/10-preflight.sh +120 -0
  74. package/assets/upgrade/20-upgrade.sh +142 -0
  75. package/assets/upgrade/30-manual-upgrade.sh +129 -0
  76. package/assets/upgrade/40-storage-prune.sh +65 -0
  77. package/assets/upgrade/50-status.sh +58 -0
  78. package/assets/upgrade/lib.sh +232 -0
  79. package/bin/ovtools.js +32 -0
  80. package/dist/api/checks.d.ts +6 -0
  81. package/dist/api/checks.d.ts.map +1 -0
  82. package/dist/api/checks.js +173 -0
  83. package/dist/api/checks.js.map +1 -0
  84. package/dist/api/hostkeys.d.ts +10 -0
  85. package/dist/api/hostkeys.d.ts.map +1 -0
  86. package/dist/api/hostkeys.js +58 -0
  87. package/dist/api/hostkeys.js.map +1 -0
  88. package/dist/api/jobs.d.ts +33 -0
  89. package/dist/api/jobs.d.ts.map +1 -0
  90. package/dist/api/jobs.js +483 -0
  91. package/dist/api/jobs.js.map +1 -0
  92. package/dist/api/nodes.d.ts +16 -0
  93. package/dist/api/nodes.d.ts.map +1 -0
  94. package/dist/api/nodes.js +126 -0
  95. package/dist/api/nodes.js.map +1 -0
  96. package/dist/api/server.d.ts +3 -0
  97. package/dist/api/server.d.ts.map +1 -0
  98. package/dist/api/server.js +443 -0
  99. package/dist/api/server.js.map +1 -0
  100. package/dist/cli.d.ts +3 -0
  101. package/dist/cli.d.ts.map +1 -0
  102. package/dist/cli.js +301 -0
  103. package/dist/cli.js.map +1 -0
  104. package/dist/constants.d.ts +76 -0
  105. package/dist/constants.d.ts.map +1 -0
  106. package/dist/constants.js +150 -0
  107. package/dist/constants.js.map +1 -0
  108. package/dist/index.d.ts +18 -0
  109. package/dist/index.d.ts.map +1 -0
  110. package/dist/index.js +60 -0
  111. package/dist/index.js.map +1 -0
  112. package/dist/orchestrator/base.d.ts +52 -0
  113. package/dist/orchestrator/base.d.ts.map +1 -0
  114. package/dist/orchestrator/base.js +219 -0
  115. package/dist/orchestrator/base.js.map +1 -0
  116. package/dist/orchestrator/deploy.d.ts +33 -0
  117. package/dist/orchestrator/deploy.d.ts.map +1 -0
  118. package/dist/orchestrator/deploy.js +313 -0
  119. package/dist/orchestrator/deploy.js.map +1 -0
  120. package/dist/orchestrator/manage.d.ts +28 -0
  121. package/dist/orchestrator/manage.d.ts.map +1 -0
  122. package/dist/orchestrator/manage.js +267 -0
  123. package/dist/orchestrator/manage.js.map +1 -0
  124. package/dist/orchestrator/migrate.d.ts +31 -0
  125. package/dist/orchestrator/migrate.d.ts.map +1 -0
  126. package/dist/orchestrator/migrate.js +442 -0
  127. package/dist/orchestrator/migrate.js.map +1 -0
  128. package/dist/ssh/client.d.ts +39 -0
  129. package/dist/ssh/client.d.ts.map +1 -0
  130. package/dist/ssh/client.js +418 -0
  131. package/dist/ssh/client.js.map +1 -0
  132. package/dist/test/core.test.d.ts +2 -0
  133. package/dist/test/core.test.d.ts.map +1 -0
  134. package/dist/test/core.test.js +188 -0
  135. package/dist/test/core.test.js.map +1 -0
  136. package/dist/types.d.ts +267 -0
  137. package/dist/types.d.ts.map +1 -0
  138. package/dist/types.js +3 -0
  139. package/dist/types.js.map +1 -0
  140. package/dist/utils/assets.d.ts +28 -0
  141. package/dist/utils/assets.d.ts.map +1 -0
  142. package/dist/utils/assets.js +583 -0
  143. package/dist/utils/assets.js.map +1 -0
  144. package/dist/utils/browser.d.ts +2 -0
  145. package/dist/utils/browser.d.ts.map +1 -0
  146. package/dist/utils/browser.js +23 -0
  147. package/dist/utils/browser.js.map +1 -0
  148. package/dist/utils/dashboard.d.ts +16 -0
  149. package/dist/utils/dashboard.d.ts.map +1 -0
  150. package/dist/utils/dashboard.js +177 -0
  151. package/dist/utils/dashboard.js.map +1 -0
  152. package/dist/utils/helpers.d.ts +29 -0
  153. package/dist/utils/helpers.d.ts.map +1 -0
  154. package/dist/utils/helpers.js +204 -0
  155. package/dist/utils/helpers.js.map +1 -0
  156. package/dist/utils/scripts.d.ts +60 -0
  157. package/dist/utils/scripts.d.ts.map +1 -0
  158. package/dist/utils/scripts.js +174 -0
  159. package/dist/utils/scripts.js.map +1 -0
  160. package/dist/utils/state.d.ts +12 -0
  161. package/dist/utils/state.d.ts.map +1 -0
  162. package/dist/utils/state.js +44 -0
  163. package/dist/utils/state.js.map +1 -0
  164. package/dist/utils/tar.d.ts +11 -0
  165. package/dist/utils/tar.d.ts.map +1 -0
  166. package/dist/utils/tar.js +93 -0
  167. package/dist/utils/tar.js.map +1 -0
  168. package/dist/utils/update.d.ts +9 -0
  169. package/dist/utils/update.d.ts.map +1 -0
  170. package/dist/utils/update.js +134 -0
  171. package/dist/utils/update.js.map +1 -0
  172. package/dist/utils/websocket.d.ts +10 -0
  173. package/dist/utils/websocket.d.ts.map +1 -0
  174. package/dist/utils/websocket.js +203 -0
  175. package/dist/utils/websocket.js.map +1 -0
  176. package/package.json +55 -4
  177. package/public/app.js +2712 -0
  178. package/public/assets/v2.2/css/base.css +113 -0
  179. package/public/assets/v2.2/css/components/content.css +133 -0
  180. package/public/assets/v2.2/css/components/controls.css +229 -0
  181. package/public/assets/v2.2/css/components/data.css +373 -0
  182. package/public/assets/v2.2/css/components/icons.css +35 -0
  183. package/public/assets/v2.2/css/components/motion.css +57 -0
  184. package/public/assets/v2.2/css/components/shell.css +51 -0
  185. package/public/assets/v2.2/css/components/sidebar.css +242 -0
  186. package/public/assets/v2.2/css/components/topbar.css +138 -0
  187. package/public/assets/v2.2/css/fonts.css +30 -0
  188. package/public/assets/v2.2/css/synteq-ui-v2.css +25 -0
  189. package/public/assets/v2.2/css/theme-dark.css +38 -0
  190. package/public/assets/v2.2/css/theme-light.css +36 -0
  191. package/public/assets/v2.2/css/tokens.css +61 -0
  192. package/public/assets/v2.2/fonts/GoogleSansCode-Medium.ttf +0 -0
  193. package/public/assets/v2.2/fonts/GoogleSansCode-Regular.ttf +0 -0
  194. package/public/assets/v2.2/fonts/GoogleSansCode-SemiBold.ttf +0 -0
  195. package/public/assets/v2.2/js/synteq-ui-v2.js +869 -0
  196. package/public/favicon.svg +7 -0
  197. package/public/index.html +1080 -0
  198. 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