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,318 @@
1
+ #!/bin/sh
2
+ set -eu
3
+
4
+ ROOT=$(pwd -P)
5
+ exec python3 -B - "$ROOT" "$@" <<'PY'
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ ROOT = Path(sys.argv.pop(1)).resolve()
10
+ modules = ROOT / "controls/lib"
11
+ for name in ("validator_common", "validator_rpc", "validator_enroll", "validator_exit"):
12
+ if not (modules / (name + ".py")).is_file():
13
+ print("event = bond_inc status = refused reason = run_from_node_directory", file = sys.stderr)
14
+ raise SystemExit(1)
15
+ sys.path.insert(0, str(modules))
16
+
17
+
18
+ # sp import if (not sure btw, will check later)
19
+ import base64
20
+ import hashlib
21
+ import json
22
+ import math
23
+ import re
24
+
25
+ from nacl.signing import VerifyKey
26
+
27
+ from validator_common import ValidatorError
28
+ from validator_exit import BOND_ESCROW, valid_hash
29
+
30
+ FEE = 1000
31
+ FIELDS = ("from", "to_", "amount", "nonce", "ou", "timestamp", "op_type")
32
+
33
+ def amount_raw(text):
34
+ if not isinstance(text, str) or len(text) > 40 or not re.fullmatch(
35
+ r"[0-9]+(?:\.[0-9]{1,6})?", text
36
+ ):
37
+ raise ValidatorError("amount must be OCT with at most six decimal places")
38
+ whole, _, fraction = text.partition(".")
39
+ value = int(whole) * 1000000 + int(fraction.ljust(6, "0"))
40
+ if value < 1000000:
41
+ raise ValidatorError("bond increase must be at least 1 OCT")
42
+ return value
43
+
44
+ def wire(fields, stamp):
45
+ encoded = json.dumps(fields, separators = (",", ":"), ensure_ascii = True)
46
+ return re.sub(r'"timestamp":[0-9.eE+\-]+', '"timestamp":' + stamp,
47
+ encoded, count = 1).encode()
48
+
49
+ def payment(record, wallet, chain, amount):
50
+ tx = record.get("tx")
51
+ stamp = record.get("stamp")
52
+ if not isinstance(tx, dict) or not isinstance(stamp, str) or not re.fullmatch(
53
+ r"[0-9]+(?:\.[0-9]+)?(?:[eE][+\-]?[0-9]+)?", stamp
54
+ ):
55
+ raise ValidatorError("saved bond payment is incomplete")
56
+ expected = {"from": wallet["address"], "to_": BOND_ESCROW,
57
+ "public_key": wallet["pub"], "op_type": "validator_bond",
58
+ "amount": str(amount), "ou": str(FEE)}
59
+
60
+ if any(tx.get(key) != value for key, value in expected.items()) or (
61
+ tx.get("encrypted_data") is not None
62
+ or set(tx) - set(FIELDS) - {"public_key", "signature", "message", "encrypted_data"}
63
+ or type(tx.get("nonce")) is not int or tx["nonce"] < 1
64
+ or type(tx.get("timestamp")) not in (int, float)
65
+ or not math.isfinite(tx["timestamp"]) or tx["timestamp"] <= 0
66
+ or float(stamp) != tx["timestamp"] or not valid_hash(record.get("tx_hash"))
67
+ ):
68
+ raise ValidatorError("saved bond payment differs")
69
+ try:
70
+
71
+
72
+
73
+
74
+ public = base64.b64decode(wallet["pub"], validate = True)
75
+ key = VerifyKey(public)
76
+ signed = {name: tx[name] for name in FIELDS}
77
+ signed["message"] = tx["message"]
78
+
79
+
80
+ key.verify(wire(signed, stamp), base64.b64decode(tx["signature"], validate = True))
81
+ proof = json.loads(tx["message"])
82
+
83
+
84
+
85
+ # as a temporary solution - ok (will remember to include it in git after the release but it's better to do ring approval , so pls check 12 id before!!!!
86
+ if set(proof) != {"consensus_pubkey", "proof"} or proof["consensus_pubkey"] != wallet["pub"]:
87
+ raise ValueError("bond key differs")
88
+ parts = [b"octra:validator_bond:v1", chain.encode(), wallet["address"].encode(),
89
+ str(amount).encode(), str(tx["nonce"]).encode(), public]
90
+
91
+
92
+ # !!!!!!! here also
93
+ message = b"".join(str(len(part)).encode() + b":" + part for part in parts)
94
+ key.verify(message, base64.b64decode(proof["proof"], validate = True))
95
+ except Exception as error:
96
+ raise ValidatorError("bond payment signature or network proof is invalid") from error
97
+ fields = {name: tx[name] for name in FIELDS[:-1]}
98
+ fields.update({"signature": tx["signature"], "op_type": tx["op_type"],
99
+ "public_key": tx["public_key"], "message": tx["message"], "encrypted_data": None})
100
+ if hashlib.sha256(wire(fields, stamp)).hexdigest() != record["tx_hash"]:
101
+ raise ValidatorError("bond transaction hash differs")
102
+ return tx
103
+
104
+ def receipt(value, record, head):
105
+ tx = record["tx"]
106
+ fields = {"tx_hash": record["tx_hash"], "from": tx["from"], "to": tx["to_"],
107
+ "amount_raw": tx["amount"], "nonce": tx["nonce"], "op_type": tx["op_type"]}
108
+ if not isinstance(value, dict) or any(
109
+ type(value.get(key)) is not type(expected) or value[key] != expected
110
+ for key, expected in fields.items()
111
+ ):
112
+ raise ValidatorError("bond transaction receipt differs")
113
+ status = value.get("status")
114
+ if status == "confirmed" and (
115
+ type(value.get("epoch")) is not int or not record["head"] <= value["epoch"] <= head
116
+ ):
117
+ raise ValidatorError("bond confirmation epoch differs")
118
+ if status not in {"confirmed", "pending", "staging", "rejected", "dropped"}:
119
+ raise ValidatorError("bond transaction status is invalid")
120
+ return status
121
+
122
+ import argparse
123
+ import json
124
+ import os
125
+ from pathlib import Path
126
+ import re
127
+ import stat
128
+ import subprocess
129
+ import sys
130
+ import time
131
+
132
+
133
+ # !!!!!! checked
134
+ from validator_common import ValidatorError, load_wallet, parse_env, private_mode, sha256_file, write_private_json
135
+ from validator_enroll import EnrollmentState, committed_enrollment, local_rpc
136
+ from validator_exit import command_lock, control_view, directory, read_outbox, require_prepare, sync_directory
137
+ from validator_rpc import call, transaction
138
+
139
+
140
+
141
+ # ONLY FOR DEVNET!!!!!!!!!!!!!!!!!!!!!!!!!
142
+ CHAIN = "octra-devnet-9871-cluster"
143
+ # !!!!!!!!!!!!!!!!!
144
+
145
+ def number(value, name):
146
+ if type(value) is int and value >= 0:
147
+ return value
148
+ if isinstance(value, str) and value.isascii() and value.isdecimal():
149
+ return int(value)
150
+ raise ValidatorError("invalid " + name)
151
+
152
+ def view(values, wallet):
153
+ if values.get("OCTRA_CHAIN_ID") != CHAIN:
154
+ raise ValidatorError("this bond tool requires devnet")
155
+ url = local_rpc(values)
156
+ runtime = call(url, "octra_runtimeVersion", [])
157
+ if not isinstance(runtime, dict) or runtime.get("chain_id") != CHAIN or (
158
+ runtime.get("validator") != wallet["address"]
159
+ ):
160
+ raise ValidatorError("local node network or validator differs")
161
+ raw = call(url, "octra_validatorEnrollment", [])
162
+ entry = committed_enrollment(values, wallet, raw)
163
+ if entry.state is not EnrollmentState.READY or entry.membership is None or (
164
+ not entry.membership.active or entry.withdraw_epoch is not None
165
+ ):
166
+ raise ValidatorError("active ready validator required; no join or exit will be sent")
167
+ control = control_view(raw, values, wallet, entry.bonded_epoch)
168
+ if control["exit_requested"]:
169
+ raise ValidatorError("validator exit is requested; nothing signed")
170
+ balance = call(url, "octra_balance", [wallet["address"]])
171
+ if not isinstance(balance, dict) or balance.get("address") != wallet["address"]:
172
+ raise ValidatorError("local balance address differs")
173
+ return {"head": entry.head_epoch, "bond": entry.bond, "bonded": entry.bonded_epoch,
174
+ "ready": entry.ready_epoch, "nonce": number(balance.get("nonce"), "account nonce"),
175
+ "pending": number(balance.get("pending_nonce"), "pending nonce"),
176
+ "balance": number(balance.get("balance_raw"), "account balance")}
177
+
178
+ def available(current, amount):
179
+ if current["pending"] != current["nonce"]:
180
+ raise ValidatorError("account has pending transactions; nothing signed or submitted")
181
+ if current["balance"] < amount + FEE:
182
+ raise ValidatorError("insufficient public balance for increase plus 0.001 OCT fee")
183
+
184
+ def save(path, value):
185
+ write_private_json(path, value)
186
+ sync_directory(path.parent)
187
+
188
+ def prepare(values, wallet, wallet_path, amount, current):
189
+ binary = Path(values["OCTRA_OPERATOR_CONTROL_BINARY"])
190
+ if sha256_file(binary) != values["OCTRA_OPERATOR_CONTROL_BINARY_HASH"]:
191
+ raise ValidatorError("validator control binary hash mismatch")
192
+ require_prepare(binary)
193
+ result = subprocess.run([str(binary), "--wallet", str(wallet_path),
194
+ "--rpc", local_rpc(values), "--chain-id", CHAIN, "--op", "validator_bond",
195
+ "--amount", str(amount), "--nonce", str(current["nonce"] + 1),
196
+ "--ou", str(FEE), "--prepare"], capture_output = True, text = True, timeout = 30)
197
+ if result.returncode != 0:
198
+ raise ValidatorError("bond preparation failed; nothing submitted")
199
+ try:
200
+ prepared = json.loads(result.stdout)
201
+ stamps = re.findall(r'"timestamp"\s*:\s*([0-9.eE+\-]+)', result.stdout)
202
+ if len(stamps) != 1:
203
+ raise ValueError("timestamp missing")
204
+ record = {"chain": CHAIN, "address": wallet["address"], "pubkey": wallet["pub"],
205
+ "amount": amount, "before": current["bond"], "head": current["head"],
206
+ "bonded": current["bonded"], "ready": current["ready"], "stage": "prepared",
207
+ "tx": prepared["tx"], "tx_hash": prepared["tx_hash"], "stamp": stamps[0]}
208
+ except (ValueError, KeyError, TypeError) as error:
209
+ raise ValidatorError("invalid prepared bond transaction; nothing submitted") from error
210
+ tx = payment(record, wallet, CHAIN, amount)
211
+ if tx["nonce"] != current["nonce"] + 1 or abs(time.time() - tx["timestamp"]) > 60:
212
+ raise ValidatorError("prepared bond nonce or timestamp differs; nothing submitted")
213
+ return record
214
+
215
+ def identity(record, wallet, current, amount):
216
+ expected = {"chain": CHAIN, "address": wallet["address"], "pubkey": wallet["pub"],
217
+ "amount": amount, "bonded": current["bonded"], "ready": current["ready"]}
218
+ if any(type(record.get(key)) is not type(value) or record[key] != value
219
+ for key, value in expected.items()) or (
220
+ type(record.get("before")) is not int or record["before"] < 1
221
+ or type(record.get("head")) is not int or not 0 <= record["head"] <= current["head"]
222
+ or record.get("stage") not in {"prepared", "sent", "accepted"}
223
+ ):
224
+ raise ValidatorError("saved increase differs; inspect before requesting another increase")
225
+ return payment(record, wallet, CHAIN, amount)
226
+
227
+ def increase(values, wallet, wallet_path, amount, *, new = False, retry = False):
228
+ current = view(values, wallet)
229
+ root = directory(values)
230
+ path = root / "bond-increase.json"
231
+ record = read_outbox(path)
232
+ if record is None and any(root.glob("bond-increase-*.json")):
233
+ raise ValidatorError("saved increase missing; restore control records")
234
+ if record is not None:
235
+ identity(record, wallet, current, record.get("amount") if new else amount)
236
+ observed = transaction(local_rpc(values), record["tx_hash"])
237
+ state = receipt(observed, record, current["head"]) if observed is not None else None
238
+ if state == "confirmed":
239
+ if current["bond"] < record["before"] + record["amount"]:
240
+ raise ValidatorError("confirmed increase is absent from current registry")
241
+ if not new:
242
+ return record["tx_hash"], "confirmed"
243
+ elif new:
244
+ raise ValidatorError("previous increase is not confirmed; nothing new signed")
245
+ elif state in {"pending", "staging"}:
246
+ return record["tx_hash"], state
247
+ elif state in {"rejected", "dropped"}:
248
+ raise ValidatorError("saved increase refused; inspect tx = " + record["tx_hash"])
249
+ elif record["stage"] != "prepared" and not retry:
250
+ raise ValidatorError("submission unresolved; inspect saved tx = " + record["tx_hash"]
251
+ + "; --retry submits only this same transaction")
252
+ elif retry or new:
253
+ raise ValidatorError("no saved increase for --retry or --new")
254
+ available(current, amount)
255
+ if record is None or new:
256
+ prepared = prepare(values, wallet, wallet_path, amount, current)
257
+ if record is not None:
258
+ archive = root / ("bond-increase-" + record["tx_hash"] + ".json")
259
+ prior = read_outbox(archive)
260
+ if prior is not None and prior != record:
261
+ raise ValidatorError("saved increase archive differs")
262
+ save(archive, record)
263
+ save(path, prepared)
264
+ record = prepared
265
+ tx = identity(record, wallet, current, amount)
266
+ fresh = view(values, wallet)
267
+ available(fresh, amount)
268
+ if any(fresh[key] != current[key] for key in ("bond", "bonded", "ready", "nonce")) or (
269
+ tx["nonce"] != fresh["nonce"] + 1 or fresh["bond"] != record["before"]
270
+ or not 0 <= time.time() - tx["timestamp"] <= 300
271
+ ):
272
+ raise ValidatorError("account, registry or transaction time changed; inspect tx = " + record["tx_hash"])
273
+ record = {**record, "stage": "sent"}
274
+ save(path, record)
275
+ try:
276
+ reply = call(local_rpc(values), "octra_submit", [tx], timeout = 30)
277
+ except ValidatorError as error:
278
+ raise ValidatorError("submission unresolved; signed payment retained; tx = " + record["tx_hash"]) from error
279
+ if not isinstance(reply, dict) or reply.get("tx_hash") != record["tx_hash"] or (
280
+ reply.get("status") not in {"accepted", "pending"}
281
+ ):
282
+ raise ValidatorError("submission not acknowledged; inspect tx = " + record["tx_hash"])
283
+ save(path, {**record, "stage": "accepted"})
284
+ return record["tx_hash"], "accepted"
285
+
286
+ def main():
287
+ parser = argparse.ArgumentParser(prog = "bond_inc.sh",
288
+ description = "Add OCT to an active devnet validator bond")
289
+ parser.add_argument("-a", "--amount", required = True, type = amount_raw,
290
+ help = "Additional OCT, at least 1, at most six decimal places")
291
+ parser.add_argument("-w", "--wallet", required = True, type = Path)
292
+ parser.add_argument("--config", type = Path, default = Path(os.getenv(
293
+ "OCTRA_OPERATOR_CONFIG", str(ROOT / ".keys/validator/node.env"))))
294
+ group = parser.add_mutually_exclusive_group()
295
+ group.add_argument("--new", action = "store_true", help = "Start another increase after confirmed prior payment")
296
+ group.add_argument("--retry", action = "store_true", help = "Resubmit the exact saved payment after an unknown outcome")
297
+ args = parser.parse_args()
298
+ private_mode(args.config)
299
+ values = parse_env(args.config)
300
+ info = args.wallet.lstat()
301
+ if not stat.S_ISREG(info.st_mode) or info.st_uid != os.getuid():
302
+ raise ValidatorError("wallet must be a regular file owned by the node user")
303
+ wallet = load_wallet(args.wallet)
304
+ local = load_wallet(Path(values["OCTRA_DATA_DIR"]) / "wallet.json")
305
+ if any(wallet[key] != local[key] for key in ("address", "pub")):
306
+ raise ValidatorError("wallet does not belong to the configured validator")
307
+ with command_lock(values):
308
+ tx_hash, status = increase(values, wallet, args.wallet, args.amount, new = args.new, retry = args.retry)
309
+ print(f"event = bond_inc status = {status} amount_raw = {args.amount}", file = sys.stderr)
310
+ print("tx = " + tx_hash)
311
+
312
+ if __name__ == "__main__":
313
+ try:
314
+ main()
315
+ except (ValidatorError, OSError, KeyError, ValueError, subprocess.SubprocessError) as error:
316
+ print("event = bond_inc status = refused reason = " + json.dumps(str(error)), file = sys.stderr)
317
+ raise SystemExit(1)
318
+ PY