diff --git a/README.md b/README.md index c577d8b4..7fa8fa0a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ![GitHub stars](https://img.shields.io/github/stars/ton-blockchain/mytonctrl?style=flat-square&logo=github) ![GitHub forks](https://img.shields.io/github/forks/ton-blockchain/mytonctrl?style=flat-square&logo=github) ![GitHub issues](https://img.shields.io/github/issues/ton-blockchain/mytonctrl?style=flat-square&logo=github) ![GitHub pull requests](https://img.shields.io/github/issues-pr/ton-blockchain/mytonctrl?style=flat-square&logo=github) ![GitHub last commit](https://img.shields.io/github/last-commit/ton-blockchain/mytonctrl?style=flat-square&logo=github) ![GitHub license](https://img.shields.io/github/license/ton-blockchain/mytonctrl?style=flat-square&logo=github) - + # MyTonCtrl @@ -58,12 +58,6 @@ Mytonctrl's documentation can be found at https://docs.ton.org/participate/run-n - [x] Show offers - [x] Vote for the proposal - [x] Automatic voting for previously voted proposals -- [x] Domain management - - [x] Rent a new domain - - [x] Show rented domains - - [x] Show domain status - - [x] Delete domain - - [ ] Automatic domain renewal - [x] Controlling the validator - [x] Participate in the election of a validator - [x] Return bet + reward diff --git a/modules/__init__.py b/modules/__init__.py index 8b92c260..cd1b0076 100644 --- a/modules/__init__.py +++ b/modules/__init__.py @@ -8,6 +8,8 @@ from modules.validator import ValidatorModule from modules.controller import ControllerModule from modules.liteserver import LiteserverModule +from modules.alert_bot import AlertBotModule +from modules.prometheus import PrometheusModule MODES = { @@ -15,7 +17,9 @@ 'nominator-pool': NominatorPoolModule, 'single-nominator': SingleNominatorModule, 'liquid-staking': ControllerModule, - 'liteserver': LiteserverModule + 'liteserver': LiteserverModule, + 'alert-bot': AlertBotModule, + 'prometheus': PrometheusModule } @@ -54,6 +58,11 @@ class Setting: 'useDefaultCustomOverlays': Setting(None, True, 'Participate in default custom overlays node eligible to'), 'defaultCustomOverlaysUrl': Setting(None, 'https://ton-blockchain.github.io/fallback_custom_overlays.json', 'Default custom overlays config url'), 'debug': Setting(None, False, 'Debug mtc console mode. Prints Traceback on errors'), + 'subscribe_tg_channel': Setting('validator', False, 'Disables warning about subscribing to the `TON STATUS` channel'), + 'auto_backup': Setting('validator', None, 'Make validator backup every election'), + 'auto_backup_path': Setting('validator', '/tmp/mytoncore/auto_backups/', 'Path to store auto-backups'), + 'prometheus_url': Setting('prometheus', None, 'Prometheus pushgateway url'), + 'onlyNode': Setting(None, None, 'MyTonCtrl will work only for collecting validator telemetry (if `sendTelemetry` is True), without participating in Elections and etc.') } diff --git a/modules/alert_bot.py b/modules/alert_bot.py new file mode 100644 index 00000000..bcb833e3 --- /dev/null +++ b/modules/alert_bot.py @@ -0,0 +1,478 @@ +import dataclasses +import time +import requests + +from modules.module import MtcModule +from mypylib.mypylib import get_timestamp, print_table, color_print +from mytoncore import get_hostname +from mytonctrl.utils import timestamp2utcdatetime + + +@dataclasses.dataclass +class Alert: + severity: str + description: str + text: str + timeout: int + + +HOUR = 3600 +VALIDATION_PERIOD = 65536 +FREEZE_PERIOD = 32768 +ELECTIONS_START_BEFORE = 8192 + + +ALERTS = {} + + +def init_alerts(): + global ALERTS + ALERTS = { + "low_wallet_balance": Alert( + "low", + "Validator's wallet balance is less than 10 TON", + "Validator's wallet {wallet} balance is less than 10 TON: {balance} TON.", + 18 * HOUR + ), + "db_usage_80": Alert( + "high", + "Node's db usage is more than 80%", + """TON DB usage > 80%. Clean the TON database: + https://docs.ton.org/participate/nodes/node-maintenance-and-security#database-grooming + or (and) set node\'s archive ttl to lower value.""", + 24 * HOUR + ), + "db_usage_95": Alert( + "critical", + "Node's db usage is more than 95%", + """TON DB usage > 95%. Disk is almost full, clean the TON database immediately: + https://docs.ton.org/participate/nodes/node-maintenance-and-security#database-grooming + or (and) set node\'s archive ttl to lower value.""", + 6 * HOUR + ), + "low_efficiency": Alert( + "high", + "Validator had efficiency less than 90% in the validation round", + """Validator efficiency is less than 90%: {efficiency}%.""", + VALIDATION_PERIOD // 3 + ), + "out_of_sync": Alert( + "critical", + "Node is out of sync on more than 20 sec", + "Node is out of sync on more than 20 sec: {sync} sec.", + 300 + ), + "service_down": Alert( + "critical", + "Node is not running (service is down)", + "validator.service is down.", + 300 + ), + "adnl_connection_failed": Alert( + "high", + "Node is not answering to ADNL connection", + "ADNL connection to node failed", + 3 * HOUR + ), + "zero_block_created": Alert( + "critical", + f"Validator has not created any blocks in the {int(VALIDATION_PERIOD // 6 // 3600)} hours", + "Validator has not created any blocks in the last {hours} hours.", + VALIDATION_PERIOD // 6 + ), + "validator_slashed": Alert( + "high", + "Validator has been slashed in the previous validation round", + "Validator has been slashed in previous round for {amount} TON", + FREEZE_PERIOD + ), + "stake_not_accepted": Alert( + "high", + "Validator's stake has not been accepted", + "Validator's stake has not been accepted", + ELECTIONS_START_BEFORE + ), + "stake_accepted": Alert( + "info", + "Validator's stake has been accepted (info alert with no sound)", + "Validator's stake {stake} TON has been accepted", + ELECTIONS_START_BEFORE + ), + "stake_returned": Alert( + "info", + "Validator's stake has been returned (info alert with no sound)", + "Validator's stake {stake} TON has been returned on address {address}. The reward amount is {reward} TON.", + 60 + ), + "stake_not_returned": Alert( + "high", + "Validator's stake has not been returned", + "Validator's stake has not been returned on address {address}.", + 60 + ), + "voting": Alert( + "high", + "There is an active network proposal that has many votes (more than 50% of required) but is not voted by the validator", + "Found proposals with hashes `{hashes}` that have significant amount of votes, but current validator didn't vote for them. Please check @tonstatus for more details.", + VALIDATION_PERIOD + ), + "initial_sync_completed": Alert( + "info", + "Initial sync has been completed (info alert with no sound)", + "Node initial sync has been completed", + 0 + ) + } + + +class AlertBotModule(MtcModule): + + description = 'Telegram bot alerts' + default_value = False + + def __init__(self, ton, local, *args, **kwargs): + super().__init__(ton, local, *args, **kwargs) + self.validator_module = None + self.inited = False + self.hostname = None + self.ip = None + self.adnl = None + self.token = None + self.chat_id = None + self.last_db_check = 0 + self.initial_sync = None + + def send_message(self, text: str, silent: bool = False, disable_web_page_preview: bool = False): + if self.token is None: + raise Exception("send_message error: token is not initialized") + if self.chat_id is None: + raise Exception("send_message error: chat_id is not initialized") + request_url = f"https://api.telegram.org/bot{self.token}/sendMessage" + data = {'chat_id': self.chat_id, 'text': text, 'parse_mode': 'HTML', 'disable_notification': silent, 'link_preview_options': {'is_disabled': disable_web_page_preview}} + response = requests.post(request_url, json=data, timeout=3) + if response.status_code != 200: + raise Exception(f"send_message error: {response.text}") + response = response.json() + if not response['ok']: + raise Exception(f"send_message error: {response}") + + def send_alert(self, alert_name: str, *args, **kwargs): + if not self.alert_is_enabled(alert_name): + return + last_sent = self.get_alert_sent(alert_name) + time_ = timestamp2utcdatetime(int(time.time())) + alert = ALERTS.get(alert_name) + if alert is None: + raise Exception(f"Alert {alert_name} not found") + alert_name_readable = alert_name.replace('_', ' ').title() + + for key, value in kwargs.items(): + if isinstance(value, (int, float)): + kwargs[key] = f'{value:,}'.replace(',', ' ') # make space separator for thousands + + text = '🆘' if alert.severity != 'info' else '' + text += f''' Node {self.hostname}: {alert_name_readable} + +{alert.text.format(*args, **kwargs)} + +Hostname: {self.hostname} +Node IP: {self.ip} +ADNL: {self.adnl}''' + + if self.ton.using_validator(): + text += f"\nWallet: {self.wallet}" + + text += f''' +Time: {time_} ({int(time.time())}) +Alert name: {alert_name} +Severity: {alert.severity} +''' + if time.time() - last_sent > alert.timeout: + self.send_message(text, alert.severity == "info") # send info alerts without sound + self.set_alert_sent(alert_name) + + def set_global_vars(self): + # set global vars for correct alerts timeouts for current network + config15 = self.ton.GetConfig15() + global VALIDATION_PERIOD, FREEZE_PERIOD, ELECTIONS_START_BEFORE + VALIDATION_PERIOD = config15["validatorsElectedFor"] + FREEZE_PERIOD = config15["stakeHeldFor"] + ELECTIONS_START_BEFORE = config15["electionsStartBefore"] + + def init(self): + if not self.ton.get_mode_value('alert-bot'): + return + self.token = self.ton.local.db.get("BotToken") + self.chat_id = self.ton.local.db.get("ChatId") + if self.token is None or self.chat_id is None: + raise Exception("BotToken or ChatId is not set") + from modules.validator import ValidatorModule + self.validator_module = ValidatorModule(self.ton, self.local) + self.hostname = get_hostname() + adnl = self.ton.GetAdnlAddr() + self.adnl = adnl + self.wallet = self.ton.GetValidatorWallet().addrB64 + self.ip = self.ton.get_node_ip() + self.set_global_vars() + self.initial_sync = self.ton.in_initial_sync() + init_alerts() + self.inited = True + + def get_alert_from_db(self, alert_name: str): + if 'alerts' not in self.ton.local.db: + self.ton.local.db['alerts'] = {} + if alert_name not in self.ton.local.db['alerts']: + self.ton.local.db['alerts'][alert_name] = {'sent': 0, 'enabled': True} + return self.ton.local.db['alerts'][alert_name] + + def set_alert_sent(self, alert_name: str): + alert = self.get_alert_from_db(alert_name) + alert['sent'] = int(time.time()) + + def get_alert_sent(self, alert_name: str): + alert = self.get_alert_from_db(alert_name) + return alert.get('sent', 0) + + def alert_is_enabled(self, alert_name: str): + alert = self.get_alert_from_db(alert_name) + return alert.get('enabled', True) # default is True + + def set_alert_enabled(self, alert_name: str, enabled: bool): + alert = self.get_alert_from_db(alert_name) + alert['enabled'] = enabled + self.ton.local.save() + + def enable_alert(self, args): + if len(args) != 1: + raise Exception("Usage: enable_alert ") + alert_name = args[0] + self.set_alert_enabled(alert_name, True) + color_print("enable_alert - {green}OK{endc}") + + def disable_alert(self, args): + if len(args) != 1: + raise Exception("Usage: disable_alert ") + alert_name = args[0] + self.set_alert_enabled(alert_name, False) + color_print("disable_alert - {green}OK{endc}") + + def print_alerts(self, args): + init_alerts() + table = [['Name', 'Enabled', 'Last sent']] + for alert_name in ALERTS: + alert = self.get_alert_from_db(alert_name) + table.append([alert_name, alert['enabled'], alert['sent']]) + print_table(table) + + def test_alert(self, args): + if not self.inited: + self.init() + self.send_message('Test alert') + + def setup_alert_bot(self, args): + if len(args) != 2: + raise Exception("Usage: setup_alert_bot ") + self.token = args[0] + self.chat_id = args[1] + init_alerts() + try: + self.send_welcome_message() + self.ton.local.db['BotToken'] = args[0] + self.ton.local.db['ChatId'] = args[1] + color_print("setup_alert_bot - {green}OK{endc}") + except Exception as e: + self.local.add_log(f"Error while sending welcome message: {e}", "error") + self.local.add_log(f"If you want the bot to write to a multi-person chat group, make sure the bot is added to that chat group. If it is not - do it and run the command `setup_alert_bot ` again.", "info") + color_print("setup_alert_bot - {red}Error{endc}") + + def send_welcome_message(self): + message = f""" +This is alert bot. You have connected validator with ADNL {self.ton.GetAdnlAddr()}. + +I don't process any commands, I only send notifications. + +Current notifications enabled: + +""" + for alert in ALERTS.values(): + message += f"- {alert.description}\n" + + message += """ +If you want, you can disable some notifications in mytonctrl by the instruction. + +Full bot documentation here. +""" + self.send_message(text=message, disable_web_page_preview=True) + + def check_db_usage(self): + if time.time() - self.last_db_check < 600: + return + self.last_db_check = time.time() + usage = self.ton.GetDbUsage() + if usage > 95: + self.send_alert("db_usage_95") + elif usage > 80: + self.send_alert("db_usage_80") + + def check_validator_wallet_balance(self): + if not self.ton.using_validator(): + return + validator_status = self.ton.GetValidatorStatus() + if not validator_status.is_working or validator_status.out_of_sync >= 20: + return + validator_wallet = self.ton.GetValidatorWallet() + validator_account = self.ton.GetAccount(validator_wallet.addrB64) + if validator_account.balance < 10: + self.send_alert("low_wallet_balance", wallet=validator_wallet.addrB64, balance=validator_account.balance) + + def check_efficiency(self): + if not self.ton.using_validator(): + return + validator = self.validator_module.find_myself(self.ton.GetValidatorsList()) + if validator is None or validator.efficiency is None: + return + config34 = self.ton.GetConfig34() + if (time.time() - config34.startWorkTime) / (config34.endWorkTime - config34.startWorkTime) < 0.8: + return # less than 80% of round passed + if validator.is_masterchain is False: + if validator.efficiency != 0: + return + if validator.efficiency < 90: + self.send_alert("low_efficiency", efficiency=validator.efficiency) + + def check_validator_working(self): + validator_status = self.ton.GetValidatorStatus() + if not self.initial_sync and not validator_status.is_working: + self.send_alert("service_down") + + def check_sync(self): + validator_status = self.ton.GetValidatorStatus() + if not self.initial_sync and validator_status.is_working and validator_status.out_of_sync >= 20: + self.send_alert("out_of_sync", sync=validator_status.out_of_sync) + + def check_zero_blocks_created(self): + if not self.ton.using_validator(): + return + ts = get_timestamp() + period = VALIDATION_PERIOD // 6 # 3h for mainnet, 40m for testnet + start, end = ts - period, ts - 60 + config34 = self.ton.GetConfig34() + if start < config34.startWorkTime: # round started recently + return + validators = self.ton.GetValidatorsList(start=start, end=end) + validator = self.validator_module.find_myself(validators) + if validator is None or validator.blocks_created > 0: + return + self.send_alert("zero_block_created", hours=round(period // 3600, 1)) + + def check_slashed(self): + if not self.ton.using_validator(): + return + c = self.validator_module.get_my_complaint() + if c is not None: + self.send_alert("validator_slashed", amount=int(c['suggestedFine'])) + + def check_adnl_connection_failed(self): + from modules.utilities import UtilitiesModule + utils_module = UtilitiesModule(self.ton, self.local) + ok, error = utils_module.check_adnl_connection() + if not ok: + self.local.add_log(error, "warning") + self.send_alert("adnl_connection_failed") + + def get_myself_from_election(self, config: dict): + if not config["validators"]: + return + adnl = self.ton.GetAdnlAddr() + save_elections = self.ton.GetSaveElections() + elections = save_elections.get(str(config["startWorkTime"])) + if elections is None: + return + if adnl not in elections: # didn't participate in elections + return + validator = self.validator_module.find_myself(config["validators"]) + if validator is None: + return False + validator['stake'] = elections[adnl].get('stake') + validator['walletAddr'] = elections[adnl].get('walletAddr') + return validator + + def check_stake_sent(self): + if not self.ton.using_validator(): + return + config = self.ton.GetConfig36() + res = self.get_myself_from_election(config) + if res is None: + return + if res is False: + self.send_alert("stake_not_accepted") + return + self.send_alert("stake_accepted", stake=round(res.get('stake'))) + + def check_stake_returned(self): + if not self.ton.using_validator(): + return + config = self.ton.GetConfig32() + if not (config['endWorkTime'] + FREEZE_PERIOD + 1800 <= time.time() < config['endWorkTime'] + FREEZE_PERIOD + 1860): # check between 30th and 31st minutes after stakes have been unfrozen + return + res = self.get_myself_from_election(config) + if not res: + return + trs = self.ton.GetAccountHistory(self.ton.GetAccount(res["walletAddr"]), limit=10) + + for tr in trs: + if tr.time >= config['endWorkTime'] + FREEZE_PERIOD and tr.srcAddr == '3333333333333333333333333333333333333333333333333333333333333333' and tr.body.startswith('F96F7324'): # Elector Recover Stake Response + self.send_alert("stake_returned", stake=round(tr.value), address=res["walletAddr"], reward=round(tr.value - res.get('stake', 0), 2)) + return + self.send_alert("stake_not_returned", address=res["walletAddr"]) + + def check_voting(self): + if not self.ton.using_validator(): + return + validator_index = self.ton.GetValidatorIndex() + if validator_index == -1: + return + config = self.ton.GetConfig34() + if time.time() - config['startWorkTime'] < 600: # less than 10 minutes passed since round start + return + need_to_vote = [] + offers = self.ton.GetOffers() + for offer in offers: + if not offer['isPassed'] and offer['approvedPercent'] >= 50 and validator_index not in offer['votedValidators']: + need_to_vote.append(offer['hash']) + if need_to_vote: + self.send_alert("voting", hashes=' '.join(need_to_vote)) + + def check_initial_sync(self): + if not self.initial_sync: + return + if not self.ton.in_initial_sync(): + self.initial_sync = False + self.send_alert("initial_sync_completed") + + def check_status(self): + if not self.ton.using_alert_bot(): + return + + if not self.inited or self.token != self.ton.local.db.get("BotToken") or self.chat_id != self.ton.local.db.get("ChatId"): + self.init() + + self.local.try_function(self.check_db_usage) + self.local.try_function(self.check_validator_wallet_balance) + self.local.try_function(self.check_efficiency) # todo: alert if validator is going to be slashed + self.local.try_function(self.check_validator_working) + self.local.try_function(self.check_zero_blocks_created) + self.local.try_function(self.check_sync) + self.local.try_function(self.check_slashed) + self.local.try_function(self.check_adnl_connection_failed) + self.local.try_function(self.check_stake_sent) + self.local.try_function(self.check_stake_returned) + self.local.try_function(self.check_voting) + self.local.try_function(self.check_initial_sync) + + def add_console_commands(self, console): + console.AddItem("enable_alert", self.enable_alert, self.local.translate("enable_alert_cmd")) + console.AddItem("disable_alert", self.disable_alert, self.local.translate("disable_alert_cmd")) + console.AddItem("list_alerts", self.print_alerts, self.local.translate("list_alerts_cmd")) + console.AddItem("test_alert", self.test_alert, self.local.translate("test_alert_cmd")) + console.AddItem("setup_alert_bot", self.setup_alert_bot, self.local.translate("setup_alert_bot_cmd")) diff --git a/modules/backups.py b/modules/backups.py new file mode 100644 index 00000000..8ea0b2fa --- /dev/null +++ b/modules/backups.py @@ -0,0 +1,91 @@ +import os +import shutil +import subprocess +import time + +import pkg_resources + +from modules.module import MtcModule +from mypylib.mypylib import color_print, ip2int, run_as_root, parse +from mytoninstaller.config import get_own_ip + + +class BackupModule(MtcModule): + + def create_keyring(self, dir_name): + keyring_dir = dir_name + '/keyring' + self.ton.validatorConsole.Run(f'exportallprivatekeys {keyring_dir}') + + def create_tmp_ton_dir(self): + result = self.ton.validatorConsole.Run("getconfig") + text = parse(result, "---------", "--------") + dir_name = self.ton.tempDir + f'/ton_backup_{int(time.time() * 1000)}' + dir_name_db = dir_name + '/db' + os.makedirs(dir_name_db) + with open(dir_name_db + '/config.json', 'w') as f: + f.write(text) + self.create_keyring(dir_name_db) + return dir_name + + @staticmethod + def run_create_backup(args): + backup_script_path = pkg_resources.resource_filename('mytonctrl', 'scripts/create_backup.sh') + return subprocess.run(["bash", backup_script_path] + args, timeout=5) + + def create_backup(self, args): + if len(args) > 1: + color_print("{red}Bad args. Usage:{endc} create_backup [filename]") + return + tmp_dir = self.create_tmp_ton_dir() + command_args = ["-m", self.ton.local.buffer.my_work_dir, "-t", tmp_dir] + if len(args) == 1: + command_args += ["-d", args[0]] + process = self.run_create_backup(command_args) + + if process.returncode == 0: + color_print("create_backup - {green}OK{endc}") + else: + color_print("create_backup - {red}Error{endc}") + shutil.rmtree(tmp_dir) + return process.returncode + # end define + + @staticmethod + def run_restore_backup(args): + restore_script_path = pkg_resources.resource_filename('mytonctrl', 'scripts/restore_backup.sh') + return run_as_root(["bash", restore_script_path] + args) + + def restore_backup(self, args): + if len(args) == 0 or len(args) > 3: + color_print("{red}Bad args. Usage:{endc} restore_backup [-y] [--skip-create-backup]") + return + if '-y' not in args: + res = input( + f'This action will overwrite existing configuration with contents of backup archive, please make sure that donor node is not in operation prior to this action. Proceed [y/n]') + if res.lower() != 'y': + print('aborted.') + return + else: + args.pop(args.index('-y')) + if '--skip-create-backup' in args: + args.pop(args.index('--skip-create-backup')) + else: + print('Before proceeding, mtc will create a backup of current configuration.') + try: + self.create_backup([]) + except: + color_print("{red}Could not create backup{endc}") + + ip = str(ip2int(get_own_ip())) + command_args = ["-m", self.ton.local.buffer.my_work_dir, "-n", args[0], "-i", ip] + + if self.run_restore_backup(command_args) == 0: + color_print("restore_backup - {green}OK{endc}") + self.local.exit() + else: + color_print("restore_backup - {red}Error{endc}") + # end define + + def add_console_commands(self, console): + console.AddItem("create_backup", self.create_backup, self.local.translate("create_backup_cmd")) + console.AddItem("restore_backup", self.restore_backup, self.local.translate("restore_backup_cmd")) diff --git a/modules/nominator_pool.py b/modules/nominator_pool.py index 14f4fbdd..d00708b0 100644 --- a/modules/nominator_pool.py +++ b/modules/nominator_pool.py @@ -87,7 +87,45 @@ def update_validator_set(self, args): self.ton.PoolUpdateValidatorSet(pool_addr, wallet) color_print("UpdateValidatorSet - {green}OK{endc}") + def do_deposit_to_pool(self, pool_addr, amount): + wallet = self.ton.GetValidatorWallet() + bocPath = self.ton.local.buffer.my_temp_dir + wallet.name + "validator-deposit-query.boc" + fiftScript = self.ton.contractsDir + "nominator-pool/func/validator-deposit.fif" + args = [fiftScript, bocPath] + result = self.ton.fift.Run(args) + resultFilePath = self.ton.SignBocWithWallet(wallet, bocPath, pool_addr, amount) + self.ton.SendFile(resultFilePath, wallet) + + def deposit_to_pool(self, args): + try: + poll_addr = args[0] + amount = float(args[1]) + except: + color_print("{red}Bad args. Usage:{endc} deposit_to_pool ") + return + self.do_deposit_to_pool(poll_addr, amount) + color_print("DepositToPool - {green}OK{endc}") + + def do_withdraw_from_pool(self, pool_addr, amount): + pool_data = self.ton.GetPoolData(pool_addr) + if pool_data["state"] == 0: + self.ton.WithdrawFromPoolProcess(pool_addr, amount) + else: + self.ton.PendWithdrawFromPool(pool_addr, amount) + + def withdraw_from_pool(self, args): + try: + pool_addr = args[0] + amount = float(args[1]) + except: + color_print("{red}Bad args. Usage:{endc} withdraw_from_pool ") + return + self.do_withdraw_from_pool(pool_addr, amount) + color_print("WithdrawFromPool - {green}OK{endc}") + def add_console_commands(self, console): console.AddItem("new_pool", self.new_pool, self.local.translate("new_pool_cmd")) console.AddItem("activate_pool", self.activate_pool, self.local.translate("activate_pool_cmd")) console.AddItem("update_validator_set", self.update_validator_set, self.local.translate("update_validator_set_cmd")) + console.AddItem("withdraw_from_pool", self.withdraw_from_pool, self.local.translate("withdraw_from_pool_cmd")) + console.AddItem("deposit_to_pool", self.deposit_to_pool, self.local.translate("deposit_to_pool_cmd")) diff --git a/modules/pool.py b/modules/pool.py index ba37b16c..16b481c5 100644 --- a/modules/pool.py +++ b/modules/pool.py @@ -58,45 +58,7 @@ def check_download_pool_contract_scripts(self): if not os.path.isdir(contract_path): self.ton.DownloadContract("https://github.com/ton-blockchain/nominator-pool") - def do_deposit_to_pool(self, pool_addr, amount): - wallet = self.ton.GetValidatorWallet() - bocPath = self.ton.local.buffer.my_temp_dir + wallet.name + "validator-deposit-query.boc" - fiftScript = self.ton.contractsDir + "nominator-pool/func/validator-deposit.fif" - args = [fiftScript, bocPath] - result = self.ton.fift.Run(args) - resultFilePath = self.ton.SignBocWithWallet(wallet, bocPath, pool_addr, amount) - self.ton.SendFile(resultFilePath, wallet) - - def deposit_to_pool(self, args): - try: - poll_addr = args[0] - amount = float(args[1]) - except: - color_print("{red}Bad args. Usage:{endc} deposit_to_pool ") - return - self.do_deposit_to_pool(poll_addr, amount) - color_print("DepositToPool - {green}OK{endc}") - - def do_withdraw_from_pool(self, pool_addr, amount): - pool_data = self.ton.GetPoolData(pool_addr) - if pool_data["state"] == 0: - self.ton.WithdrawFromPoolProcess(pool_addr, amount) - else: - self.ton.PendWithdrawFromPool(pool_addr, amount) - - def withdraw_from_pool(self, args): - try: - pool_addr = args[0] - amount = float(args[1]) - except: - color_print("{red}Bad args. Usage:{endc} withdraw_from_pool ") - return - self.do_withdraw_from_pool(pool_addr, amount) - color_print("WithdrawFromPool - {green}OK{endc}") - def add_console_commands(self, console): console.AddItem("pools_list", self.print_pools_list, self.local.translate("pools_list_cmd")) console.AddItem("delete_pool", self.delete_pool, self.local.translate("delete_pool_cmd")) console.AddItem("import_pool", self.import_pool, self.local.translate("import_pool_cmd")) - console.AddItem("deposit_to_pool", self.deposit_to_pool, self.local.translate("deposit_to_pool_cmd")) - console.AddItem("withdraw_from_pool", self.withdraw_from_pool, self.local.translate("withdraw_from_pool_cmd")) diff --git a/modules/prometheus.py b/modules/prometheus.py new file mode 100644 index 00000000..e88e1737 --- /dev/null +++ b/modules/prometheus.py @@ -0,0 +1,118 @@ +from modules.module import MtcModule +import dataclasses +import requests + + +@dataclasses.dataclass +class Metric: + name: str + description: str + type: str + + def to_format(self, value): + return f""" +# HELP {self.name} {self.description} +# TYPE {self.name} {self.type} +{self.name} {value} +""" + + +METRICS = { + 'master_out_of_sync': Metric('validator_masterchain_out_of_sync_seconds', 'Time difference between current time and timestamp of the last known block', 'gauge'), + 'shard_out_of_sync': Metric('validator_shardchain_out_of_sync_blocks', 'Number of blocks node\'s shardclient is behind the last known block', 'gauge'), + 'out_of_ser': Metric('validator_out_of_serialization', 'Number of blocks last state serialization was ago', 'gauge'), + 'vc_up': Metric('validator_console_up', 'Is `validator-console` up', 'gauge'), + 'validator_id': Metric('validator_index', 'Validator index', 'gauge'), + 'stake': Metric('validator_stake', 'Validator stake', 'gauge'), + 'celldb_gc_block': Metric('validator_celldb_gc_block', 'Celldb GC block latency', 'gauge'), + 'celldb_gc_state': Metric('validator_celldb_gc_state', 'Celldb GC queue size', 'gauge'), + 'collated_master_ok': Metric('validator_blocks_collated_master_ok', 'Number of masterchain blocks successfully collated', 'gauge'), + 'collated_master_err': Metric('validator_blocks_collated_master_err', 'Number of masterchain blocks failed to collate', 'gauge'), + 'collated_shard_ok': Metric('validator_blocks_collated_shard_ok', 'Number of shardchain blocks successfully collated', 'gauge'), + 'collated_shard_err': Metric('validator_blocks_collated_shard_err', 'Number of shardchain blocks failed to collate', 'gauge'), + 'validated_master_ok': Metric('validator_blocks_validated_master_ok', 'Number of masterchain blocks successfully validated', 'gauge'), + 'validated_master_err': Metric('validator_blocks_validated_master_err', 'Number of masterchain blocks failed to validate', 'gauge'), + 'validated_shard_ok': Metric('validator_blocks_validated_shard_ok', 'Number of shardchain blocks successfully validated', 'gauge'), + 'validated_shard_err': Metric('validator_blocks_validated_shard_err', 'Number of shardchain blocks failed to validate', 'gauge'), + 'validator_groups_master': Metric('validator_active_groups_master', 'Number of masterchain validation groups validator participates in', 'gauge'), + 'validator_groups_shard': Metric('validator_active_groups_shard', 'Number of shardchain validation groups validator participates in', 'gauge'), + 'ls_queries_ok': Metric('validator_ls_queries_ok', 'Number of Liteserver successful queries', 'gauge'), + 'ls_queries_err': Metric('validator_ls_queries_err', 'Number of Liteserver failed queries', 'gauge'), +} + + +class PrometheusModule(MtcModule): + + description = 'Prometheus format data exporter' + default_value = False + + def __init__(self, ton, local, *args, **kwargs): + super().__init__(ton, local, *args, **kwargs) + + def get_validator_status_metrics(self, result: list): + status = self.ton.GetValidatorStatus() + is_working = status.is_working or (status.unixtime is not None) + if status.masterchain_out_of_sync is not None: + result.append(METRICS['master_out_of_sync'].to_format(status.masterchain_out_of_sync)) + if status.shardchain_out_of_sync is not None: + result.append(METRICS['shard_out_of_sync'].to_format(status.shardchain_out_of_sync)) + if status.stateserializerenabled and status.masterchain_out_of_ser is not None and status.stateserializermasterchainseqno != 0: + result.append(METRICS['out_of_ser'].to_format(status.masterchain_out_of_ser)) + if status.masterchainblock is not None and status.gcmasterchainblock is not None: + result.append(METRICS['celldb_gc_block'].to_format(status.masterchainblock - status.gcmasterchainblock)) + if status.gcmasterchainblock is not None and status.last_deleted_mc_state is not None: + if status.last_deleted_mc_state != 0: + result.append(METRICS['celldb_gc_state'].to_format(status.gcmasterchainblock - status.last_deleted_mc_state)) + else: + result.append(METRICS['celldb_gc_state'].to_format(-1)) + if status.validator_groups_master is not None: + result.append(METRICS['validator_groups_master'].to_format(status.validator_groups_master)) + result.append(METRICS['validator_groups_shard'].to_format(status.validator_groups_shard)) + result.append(METRICS['vc_up'].to_format(int(is_working))) + + def get_validator_validation_metrics(self, result: list): + index = self.ton.GetValidatorIndex() + result.append(METRICS['validator_id'].to_format(index)) + config = self.ton.GetConfig34() + save_elections = self.ton.GetSaveElections() + elections = save_elections.get(str(config["startWorkTime"])) + if elections is not None: + adnl = self.ton.GetAdnlAddr() + stake = elections.get(adnl, {}).get('stake') + if stake: + result.append(METRICS['stake'].to_format(round(stake, 2))) + + def get_node_stats_metrics(self, result: list): + stats = self.ton.get_node_statistics() + if stats and 'ls_queries' in stats: + if stats['ls_queries']['time'] < 50: + self.local.add_log(f'Liteserver queries time is too low: {stats}') + return + result.append(METRICS['ls_queries_ok'].to_format(stats['ls_queries']['ok'])) + result.append(METRICS['ls_queries_err'].to_format(stats['ls_queries']['error'])) + if stats and 'collated' in stats: + result.append(METRICS['collated_master_ok'].to_format(stats['collated']['master']['ok'])) + result.append(METRICS['collated_master_err'].to_format(stats['collated']['master']['error'])) + result.append(METRICS['collated_shard_ok'].to_format(stats['collated']['shard']['ok'])) + result.append(METRICS['collated_shard_err'].to_format(stats['collated']['shard']['error'])) + if stats and 'validated' in stats: + result.append(METRICS['validated_master_ok'].to_format(stats['validated']['master']['ok'])) + result.append(METRICS['validated_master_err'].to_format(stats['validated']['master']['error'])) + result.append(METRICS['validated_shard_ok'].to_format(stats['validated']['shard']['ok'])) + result.append(METRICS['validated_shard_err'].to_format(stats['validated']['shard']['error'])) + + def push_metrics(self): + if not self.ton.using_prometheus(): + return + + url = self.ton.local.db.get('prometheus_url') + if url is None: + raise Exception('Prometheus url is not set') + metrics = [] + self.local.try_function(self.get_validator_status_metrics, args=[metrics]) + self.local.try_function(self.get_validator_validation_metrics, args=[metrics]) + self.local.try_function(self.get_node_stats_metrics, args=[metrics]) + requests.post(url, data='\n'.join(metrics).encode()) + + def add_console_commands(self, console): + ... diff --git a/modules/utilities.py b/modules/utilities.py new file mode 100644 index 00000000..640f160d --- /dev/null +++ b/modules/utilities.py @@ -0,0 +1,399 @@ +import json +import random +import subprocess +import time + +import requests + +from mypylib.mypylib import color_print, print_table, color_text, timeago, bcolors +from modules.module import MtcModule + + +class UtilitiesModule(MtcModule): + + description = '' + default_value = False + + def view_account_status(self, args): + try: + addrB64 = args[0] + except: + color_print("{red}Bad args. Usage:{endc} vas ") + return + addrB64 = self.ton.get_destination_addr(addrB64) + account = self.ton.GetAccount(addrB64) + version = self.ton.GetVersionFromCodeHash(account.codeHash) + statusTable = list() + statusTable += [["Address", "Status", "Balance", "Version"]] + statusTable += [[addrB64, account.status, account.balance, version]] + codeHashTable = list() + codeHashTable += [["Code hash"]] + codeHashTable += [[account.codeHash]] + historyTable = self.get_history_table(addrB64, 10) + print_table(statusTable) + print() + print_table(codeHashTable) + print() + print_table(historyTable) + # end define + + def get_history_table(self, addr, limit): + addr = self.ton.get_destination_addr(addr) + account = self.ton.GetAccount(addr) + history = self.ton.GetAccountHistory(account, limit) + table = list() + typeText = color_text("{red}{bold}{endc}") + table += [["Time", typeText, "Coins", "From/To"]] + for message in history: + if message.srcAddr is None: + continue + srcAddrFull = f"{message.srcWorkchain}:{message.srcAddr}" + destAddFull = f"{message.destWorkchain}:{message.destAddr}" + if srcAddrFull == account.addrFull: + type = color_text("{red}{bold}>>>{endc}") + fromto = destAddFull + else: + type = color_text("{blue}{bold}<<<{endc}") + fromto = srcAddrFull + fromto = self.ton.AddrFull2AddrB64(fromto) + # datetime = timestamp2datetime(message.time, "%Y.%m.%d %H:%M:%S") + datetime = timeago(message.time) + table += [[datetime, type, message.value, fromto]] + return table + # end define + + def view_account_history(self, args): + try: + addr = args[0] + limit = int(args[1]) + except: + color_print("{red}Bad args. Usage:{endc} vah ") + return + table = self.get_history_table(addr, limit) + print_table(table) + # end define + + def create_new_bookmark(self, args): + try: + name = args[0] + addr = args[1] + except: + color_print("{red}Bad args. Usage:{endc} nb ") + return + if not self.ton.IsAddr(addr): + raise Exception("Incorrect address") + # end if + + bookmark = dict() + bookmark["name"] = name + bookmark["addr"] = addr + self.ton.AddBookmark(bookmark) + color_print("CreatNewBookmark - {green}OK{endc}") + # end define + + def print_bookmarks_list(self, args): + data = self.ton.GetBookmarks() + if data is None or len(data) == 0: + print("No data") + return + table = list() + table += [["Name", "Address", "Balance / Exp. date"]] + for item in data: + name = item.get("name") + addr = item.get("addr") + bookmark_data = item.get("data") + table += [[name, addr, bookmark_data]] + print_table(table) + # end define + + def delete_bookmark(self, args): + try: + name = args[0] + except: + color_print("{red}Bad args. Usage:{endc} db ") + return + self.ton.DeleteBookmark(name) + color_print("DeleteBookmark - {green}OK{endc}") + # end define + + @staticmethod + def reduct(item): + item = str(item) + if item is None: + result = None + else: + end = len(item) + result = item[0:6] + "..." + item[end - 6:end] + return result + # end define + + def print_offers_list(self, args): + data = self.ton.GetOffers() + if data is None or len(data) == 0: + print("No data") + return + if "--json" in args: + text = json.dumps(data, indent=2) + print(text) + else: + table = list() + table += [["Hash", "Config", "Votes", "W/L", "Approved", "Is passed"]] + for item in data: + hash = item.get("hash") + votedValidators = len(item.get("votedValidators")) + wins = item.get("wins") + losses = item.get("losses") + wl = "{0}/{1}".format(wins, losses) + approvedPercent = item.get("approvedPercent") + approvedPercent_text = "{0}%".format(approvedPercent) + isPassed = item.get("isPassed") + if "hash" not in args: + hash = self.reduct(hash) + if isPassed == True: + isPassed = bcolors.green_text("true") + if isPassed == False: + isPassed = bcolors.red_text("false") + table += [[hash, item.config.id, votedValidators, wl, approvedPercent_text, isPassed]] + print_table(table) + # end define + + def get_offer_diff(self, offer_hash): + self.local.add_log("start GetOfferDiff function", "debug") + offer = self.ton.GetOffer(offer_hash) + config_id = offer["config"]["id"] + config_value = offer["config"]["value"] + + if '{' in config_value or '}' in config_value: + start = config_value.find('{') + 1 + end = config_value.find('}') + config_value = config_value[start:end] + # end if + + args = [self.ton.liteClient.appPath, "--global-config", self.ton.liteClient.configPath, "--verbosity", "0"] + process = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + time.sleep(1) + + fullConfigAddr = self.ton.GetFullConfigAddr() + cmd = "runmethodfull {fullConfigAddr} list_proposals".format(fullConfigAddr=fullConfigAddr) + process.stdin.write(cmd.encode() + b'\n') + process.stdin.flush() + time.sleep(1) + + cmd = "dumpcellas ConfigParam{configId} {configValue}".format(configId=config_id, configValue=config_value) + process.stdin.write(cmd.encode() + b'\n') + process.stdin.flush() + time.sleep(1) + + process.terminate() + text = process.stdout.read().decode() + + lines = text.split('\n') + b = len(lines) + for i in range(b): + line = lines[i] + if "dumping cells as values of TLB type" in line: + a = i + 2 + break + # end for + + for i in range(a, b): + line = lines[i] + if '(' in line: + start = i + break + # end for + + for i in range(a, b): + line = lines[i] + if '>' in line: + end = i + break + # end for + + buff = lines[start:end] + text = "".join(buff) + newData = self.ton.Tlb2Json(text) + newFileName = self.ton.tempDir + "data1diff" + file = open(newFileName, 'wt') + newText = json.dumps(newData, indent=2) + file.write(newText) + file.close() + + oldData = self.ton.GetConfig(config_id) + oldFileName = self.ton.tempDir + "data2diff" + file = open(oldFileName, 'wt') + oldText = json.dumps(oldData, indent=2) + file.write(oldText) + file.close() + + print(oldText) + args = ["diff", "--color", oldFileName, newFileName] + subprocess.run(args) + # end define + + def offer_diff(self, args): + try: + offer_hash = args[0] + offer_hash = offer_hash + except: + color_print("{red}Bad args. Usage:{endc} od ") + return + self.get_offer_diff(offer_hash) + # end define + + def print_complaints_list(self, args): + past = "past" in args + data = self.ton.GetComplaints(past=past) + if data is None or len(data) == 0: + print("No data") + return + if "--json" in args: + text = json.dumps(data, indent=2) + print(text) + else: + table = list() + table += [["Election id", "ADNL", "Fine (part)", "Votes", "Approved", "Is passed"]] + for key, item in data.items(): + electionId = item.get("electionId") + adnl = item.get("adnl") + suggestedFine = item.get("suggestedFine") + suggestedFinePart = item.get("suggestedFinePart") + Fine_text = "{0} ({1})".format(suggestedFine, suggestedFinePart) + votedValidators = len(item.get("votedValidators")) + approvedPercent = item.get("approvedPercent") + approvedPercent_text = "{0}%".format(approvedPercent) + isPassed = item.get("isPassed") + if "adnl" not in args: + adnl = self.reduct(adnl) + if isPassed: + isPassed = bcolors.green_text("true") + if not isPassed: + isPassed = bcolors.red_text("false") + table += [[electionId, adnl, Fine_text, votedValidators, approvedPercent_text, isPassed]] + print_table(table) + # end define + + def print_election_entries_list(self, args): + past = "past" in args + data = self.ton.GetElectionEntries(past=past) + if data is None or len(data) == 0: + print("No data") + return + if "--json" in args: + text = json.dumps(data, indent=2) + print(text) + else: + table = list() + table += [["ADNL", "Pubkey", "Wallet", "Stake", "Max-factor"]] + for key, item in data.items(): + adnl = item.get("adnlAddr") + pubkey = item.get("pubkey") + walletAddr = item.get("walletAddr") + stake = item.get("stake") + maxFactor = item.get("maxFactor") + if "adnl" not in args: + adnl = self.reduct(adnl) + if "pubkey" not in args: + pubkey = self.reduct(pubkey) + if "wallet" not in args: + walletAddr = self.reduct(walletAddr) + table += [[adnl, pubkey, walletAddr, stake, maxFactor]] + print_table(table) + # end define + + def print_validator_list(self, args): + past = "past" in args + fast = "fast" in args + data = self.ton.GetValidatorsList(past=past, fast=fast) + if data is None or len(data) == 0: + print("No data") + return + if "--json" in args: + text = json.dumps(data, indent=2) + print(text) + else: + table = list() + table += [["id", "ADNL", "Pubkey", "Wallet", "Stake", "Efficiency", "Online"]] + for i, item in enumerate(data): + adnl = item.get("adnlAddr") + pubkey = item.get("pubkey") + walletAddr = item.get("walletAddr") + efficiency = item.get("efficiency") + online = item.get("online") + stake = item.get("stake") + if "adnl" not in args: + adnl = self.reduct(adnl) + if "pubkey" not in args: + pubkey = self.reduct(pubkey) + if "wallet" not in args: + walletAddr = self.reduct(walletAddr) + if "offline" in args and online != False: + continue + if online: + online = bcolors.green_text("true") + if not online: + online = bcolors.red_text("false") + table += [[str(i), adnl, pubkey, walletAddr, stake, efficiency, online]] + print_table(table) + # end define + + def check_adnl_connection(self): + telemetry = self.ton.local.db.get("sendTelemetry", False) + check_adnl = self.ton.local.db.get("checkAdnl", telemetry) + if not check_adnl: + return True, '' + self.local.add_log('Checking ADNL connection to local node', 'info') + hosts = ['45.129.96.53', '5.154.181.153', '2.56.126.137', '91.194.11.68', '45.12.134.214', '138.124.184.27', + '103.106.3.171'] + hosts = random.sample(hosts, k=3) + data = self.ton.get_local_adnl_data() + error = '' + ok = True + for host in hosts: + url = f'http://{host}/adnl_check' + try: + response = requests.post(url, json=data, timeout=5).json() + except Exception as e: + ok = False + error = f'Failed to check ADNL connection to local node: {type(e)}: {e}' + continue + result = response.get("ok") + if result: + ok = True + break + if not result: + ok = False + error = f'Failed to check ADNL connection to local node: {response.get("message")}' + return ok, error + + def get_pool_data(self, args): + try: + pool_name = args[0] + except: + color_print("{red}Bad args. Usage:{endc} get_pool_data ") + return + if self.ton.IsAddr(pool_name): + pool_addr = pool_name + else: + pool = self.ton.GetLocalPool(pool_name) + pool_addr = pool.addrB64 + pool_data = self.ton.GetPoolData(pool_addr) + print(json.dumps(pool_data, indent=4)) + # end define + + def add_console_commands(self, console): + console.AddItem("vas", self.view_account_status, self.local.translate("vas_cmd")) + console.AddItem("vah", self.view_account_history, self.local.translate("vah_cmd")) + + console.AddItem("nb", self.create_new_bookmark, self.local.translate("nb_cmd")) + console.AddItem("bl", self.print_bookmarks_list, self.local.translate("bl_cmd")) + console.AddItem("db", self.delete_bookmark, self.local.translate("db_cmd")) + + console.AddItem("ol", self.print_offers_list, self.local.translate("ol_cmd")) + console.AddItem("od", self.offer_diff, self.local.translate("od_cmd")) + + console.AddItem("el", self.print_election_entries_list, self.local.translate("el_cmd")) + console.AddItem("vl", self.print_validator_list, self.local.translate("vl_cmd")) + console.AddItem("cl", self.print_complaints_list, self.local.translate("cl_cmd")) + + console.AddItem("get_pool_data", self.get_pool_data, self.local.translate("get_pool_data_cmd")) diff --git a/modules/validator.py b/modules/validator.py index 7a6b9ad3..a0fdfa9c 100644 --- a/modules/validator.py +++ b/modules/validator.py @@ -54,15 +54,18 @@ def check_efficiency(self, args): end_time = timestamp2utcdatetime(config32.endWorkTime) color_print(f"Previous round time: {{yellow}}from {start_time} to {end_time}{{endc}}") if validator: - if validator.is_masterchain == False: - print("Validator index is greater than 100 in the previous round - no efficiency data.") - elif validator.get('efficiency') is None: + if validator.get('efficiency') is None: print('Failed to get efficiency for the previous round') + elif validator.is_masterchain is False and validator.get('efficiency') != 0: + print(f"Validator index is greater than {config32['mainValidators']} in the previous round - no efficiency data.") else: efficiency = 100 if validator.efficiency > 100 else validator.efficiency color_efficiency = GetColorInt(efficiency, 90, logic="more", ending="%") - created = validator.blocks_created - expected = validator.blocks_expected + created = validator.master_blocks_created + expected = validator.master_blocks_expected + if created is None: # there is no updated prev round info in cache + created = validator.blocks_created + expected = validator.blocks_expected color_print(f"Previous round efficiency: {color_efficiency} {{yellow}}({created} blocks created / {round(expected, 1)} blocks expected){{endc}}") else: print("Couldn't find this validator in the previous round") @@ -71,8 +74,8 @@ def check_efficiency(self, args): end_time = timestamp2utcdatetime(int(get_timestamp())) color_print(f"Current round time: {{green}}from {start_time} to {end_time}{{endc}}") if validator: - if validator.is_masterchain == False: - print("Validator index is greater than 100 in the current round - no efficiency data.") + if validator.is_masterchain is False and validator.efficiency != 0: + print(f"Validator index is greater than {config34['mainValidators']} in the current round - no efficiency data.") elif (time.time() - config34.startWorkTime) / (config34.endWorkTime - config34.startWorkTime) < 0.8: print("The validation round has started recently, there is not enough data yet. " "The efficiency evaluation will become more accurate towards the end of the round.") @@ -81,13 +84,24 @@ def check_efficiency(self, args): else: efficiency = 100 if validator.efficiency > 100 else validator.efficiency color_efficiency = GetColorInt(efficiency, 90, logic="more", ending="%") - created = validator.blocks_created - expected = validator.blocks_expected + created = validator.master_blocks_created + expected = validator.master_blocks_expected color_print(f"Current round efficiency: {color_efficiency} {{yellow}}({created} blocks created / {round(expected, 1)} blocks expected){{endc}}") else: print("Couldn't find this validator in the current round") # end define + def get_my_complaint(self): + config32 = self.ton.GetConfig32() + save_complaints = self.ton.GetSaveComplaints() + complaints = save_complaints.get(str(config32['startWorkTime'])) + if not complaints: + return + for c in complaints.values(): + if c["adnl"] == self.ton.GetAdnlAddr() and c["isPassed"]: + return c + # end define + def add_console_commands(self, console): console.AddItem("vo", self.vote_offer, self.local.translate("vo_cmd")) console.AddItem("ve", self.vote_election_entry, self.local.translate("ve_cmd")) diff --git a/modules/wallet.py b/modules/wallet.py new file mode 100644 index 00000000..e4bc1c86 --- /dev/null +++ b/modules/wallet.py @@ -0,0 +1,205 @@ +import base64 +import os + +from modules.module import MtcModule +from mypylib.mypylib import color_print, print_table + + +class WalletModule(MtcModule): + + description = '' + default_value = False + + def create_new_wallet(self, args): + version = "v1" + try: + if len(args) == 0: + walletName = self.ton.GenerateWalletName() + workchain = 0 + else: + workchain = int(args[0]) + walletName = args[1] + if len(args) > 2: + version = args[2] + if len(args) == 4: + subwallet = int(args[3]) + else: + subwallet = 698983191 + workchain # 0x29A9A317 + workchain + except: + color_print("{red}Bad args. Usage:{endc} nw [ ]") + return + wallet = self.ton.CreateWallet(walletName, workchain, version, subwallet=subwallet) + table = list() + table += [["Name", "Workchain", "Address"]] + table += [[wallet.name, wallet.workchain, wallet.addrB64_init]] + print_table(table) + # end define + + def _wallets_check(self): + self.local.add_log("start WalletsCheck function", "debug") + wallets = self.get_wallets() + for wallet in wallets: + if os.path.isfile(wallet.bocFilePath): + account = self.ton.GetAccount(wallet.addrB64) + if account.balance > 0: + self.ton.SendFile(wallet.bocFilePath, wallet) + # end define + + def activate_wallet(self, args): + try: + walletName = args[0] + except Exception as err: + walletName = "all" + if walletName == "all": + self._wallets_check() + else: + wallet = self.ton.GetLocalWallet(walletName) + self.ton.ActivateWallet(wallet) + color_print("ActivateWallet - {green}OK{endc}") + # end define + + def get_wallets(self): + self.local.add_log("start GetWallets function", "debug") + wallets = list() + wallets_name_list = self.ton.GetWalletsNameList() + for walletName in wallets_name_list: + wallet = self.ton.GetLocalWallet(walletName) + wallets.append(wallet) + return wallets + # end define + + def print_wallets_list(self, args): + table = list() + table += [["Name", "Status", "Balance", "Ver", "Wch", "Address"]] + data = self.get_wallets() + if data is None or len(data) == 0: + print("No data") + return + for wallet in data: + account = self.ton.GetAccount(wallet.addrB64) + if account.status != "active": + wallet.addrB64 = wallet.addrB64_init + table += [[wallet.name, account.status, account.balance, wallet.version, wallet.workchain, wallet.addrB64]] + print_table(table) + # end define + + def do_import_wallet(self, addr_b64, key): + addr_bytes = self.ton.addr_b64_to_bytes(addr_b64) + pk_bytes = base64.b64decode(key) + wallet_name = self.ton.GenerateWalletName() + wallet_path = self.ton.walletsDir + wallet_name + with open(wallet_path + ".addr", 'wb') as file: + file.write(addr_bytes) + with open(wallet_path + ".pk", 'wb') as file: + file.write(pk_bytes) + return wallet_name + # end define + + def import_wallet(self, args): + try: + addr = args[0] + key = args[1] + except: + color_print("{red}Bad args. Usage:{endc} iw ") + return + name = self.do_import_wallet(addr, key) + print("Wallet name:", name) + # end define + + def set_wallet_version(self, args): + try: + addr = args[0] + version = args[1] + except: + color_print("{red}Bad args. Usage:{endc} swv ") + return + self.ton.SetWalletVersion(addr, version) + color_print("SetWalletVersion - {green}OK{endc}") + # end define + + def do_export_wallet(self, wallet_name): + wallet = self.ton.GetLocalWallet(wallet_name) + with open(wallet.privFilePath, 'rb') as file: + data = file.read() + key = base64.b64encode(data).decode("utf-8") + return wallet.addrB64, key + # end define + + def export_wallet(self, args): + try: + name = args[0] + except: + color_print("{red}Bad args. Usage:{endc} ew ") + return + addr, key = self.do_export_wallet(name) + print("Wallet name:", name) + print("Address:", addr) + print("Secret key:", key) + # end define + + def delete_wallet(self, args): + try: + wallet_name = args[0] + except: + color_print("{red}Bad args. Usage:{endc} dw ") + return + if input("Are you sure you want to delete this wallet (yes/no): ") != "yes": + print("Cancel wallet deletion") + return + wallet = self.ton.GetLocalWallet(wallet_name) + wallet.Delete() + color_print("DeleteWallet - {green}OK{endc}") + # end define + + def move_coins(self, args): + try: + wallet_name = args[0] + destination = args[1] + amount = args[2] + flags = args[3:] + except: + color_print("{red}Bad args. Usage:{endc} mg ") + return + wallet = self.ton.GetLocalWallet(wallet_name) + destination = self.ton.get_destination_addr(destination) + self.ton.MoveCoins(wallet, destination, amount, flags=flags) + color_print("MoveCoins - {green}OK{endc}") + # end define + + def do_move_coins_through_proxy(self, wallet, dest, coins): + self.local.add_log("start MoveCoinsThroughProxy function", "debug") + wallet1 = self.ton.CreateWallet("proxy_wallet1", 0) + wallet2 = self.ton.CreateWallet("proxy_wallet2", 0) + self.ton.MoveCoins(wallet, wallet1.addrB64_init, coins) + self.ton.ActivateWallet(wallet1) + self.ton.MoveCoins(wallet1, wallet2.addrB64_init, "alld") + self.ton.ActivateWallet(wallet2) + self.ton.MoveCoins(wallet2, dest, "alld", flags=["-n"]) + wallet1.Delete() + wallet2.Delete() + # end define + + def move_coins_through_proxy(self, args): + try: + wallet_name = args[0] + destination = args[1] + amount = args[2] + except: + color_print("{red}Bad args. Usage:{endc} mgtp ") + return + wallet = self.ton.GetLocalWallet(wallet_name) + destination = self.ton.get_destination_addr(destination) + self.do_move_coins_through_proxy(wallet, destination, amount) + color_print("MoveCoinsThroughProxy - {green}OK{endc}") + # end define + + def add_console_commands(self, console): + console.AddItem("nw", self.create_new_wallet, self.local.translate("nw_cmd")) + console.AddItem("aw", self.activate_wallet, self.local.translate("aw_cmd")) + console.AddItem("wl", self.print_wallets_list, self.local.translate("wl_cmd")) + console.AddItem("iw", self.import_wallet, self.local.translate("iw_cmd")) + console.AddItem("swv", self.set_wallet_version, self.local.translate("swv_cmd")) + console.AddItem("ew", self.export_wallet, self.local.translate("ex_cmd")) + console.AddItem("dw", self.delete_wallet, self.local.translate("dw_cmd")) + console.AddItem("mg", self.move_coins, self.local.translate("mg_cmd")) + console.AddItem("mgtp", self.move_coins_through_proxy, self.local.translate("mgtp_cmd")) diff --git a/mypyconsole b/mypyconsole index a8f9f569..9b730946 160000 --- a/mypyconsole +++ b/mypyconsole @@ -1 +1 @@ -Subproject commit a8f9f56972192247f37ca8f43a5e723e29994c60 +Subproject commit 9b730946fc7998a6b294fe9adb7a77ce1b2b6eef diff --git a/mypylib b/mypylib index 049205ee..3508d3aa 160000 --- a/mypylib +++ b/mypylib @@ -1 +1 @@ -Subproject commit 049205eee1650d7d847497d78acba68c5a33cc37 +Subproject commit 3508d3aa07ec3ea1a47690a3e7477b2b8e677c4a diff --git a/mytoncore/functions.py b/mytoncore/functions.py index 5ae4602a..c39f2c42 100755 --- a/mytoncore/functions.py +++ b/mytoncore/functions.py @@ -55,8 +55,8 @@ def Event(local, event_name): ValidatorDownEvent(local) elif event_name == "enable_ton_storage_provider": enable_ton_storage_provider_event(local) - elif event_name == "enable_liteserver_mode": - enable_liteserver_mode(local) + elif event_name.startswith("enable_mode"): + enable_mode(local, event_name) local.exit() # end define @@ -93,10 +93,12 @@ def enable_ton_storage_provider_event(local): #end define -def enable_liteserver_mode(local): +def enable_mode(local, event_name): ton = MyTonCore(local) - ton.disable_mode('validator') - ton.enable_mode('liteserver') + mode = event_name.split("_")[-1] + if mode == "liteserver": + ton.disable_mode('validator') + ton.enable_mode(mode) #end define @@ -284,6 +286,64 @@ def CalculateNetworkStatistics(zerodata, data): # end define +def save_node_statistics(local, ton): + status = ton.GetValidatorStatus(no_cache=True) + if status.unixtime is None: + return + data = {'timestamp': status.unixtime} + + def get_ok_error(value: str): + ok, error = value.split() + return int(ok.split(':')[1]), int(error.split(':')[1]) + + if 'total.collated_blocks.master' in status: + master_ok, master_error = get_ok_error(status['total.collated_blocks.master']) + shard_ok, shard_error = get_ok_error(status['total.collated_blocks.shard']) + data['collated_blocks'] = { + 'master': {'ok': master_ok, 'error': master_error}, + 'shard': {'ok': shard_ok, 'error': shard_error}, + } + if 'total.validated_blocks.master' in status: + master_ok, master_error = get_ok_error(status['total.validated_blocks.master']) + shard_ok, shard_error = get_ok_error(status['total.validated_blocks.shard']) + data['validated_blocks'] = { + 'master': {'ok': master_ok, 'error': master_error}, + 'shard': {'ok': shard_ok, 'error': shard_error}, + } + if 'total.ext_msg_check' in status: + ok, error = get_ok_error(status['total.ext_msg_check']) + data['ext_msg_check'] = {'ok': ok, 'error': error} + if 'total.ls_queries_ok' in status and 'total.ls_queries_error' in status: + data['ls_queries'] = {} + for k in status['total.ls_queries_ok'].split(): + if k.startswith('TOTAL'): + data['ls_queries']['ok'] = int(k.split(':')[1]) + for k in status['total.ls_queries_error'].split(): + if k.startswith('TOTAL'): + data['ls_queries']['error'] = int(k.split(':')[1]) + statistics = local.db.get("statistics", dict()) + + if time.time() - int(status.start_time) <= 60: # was node restart <60 sec ago, resetting node statistics + statistics['node'] = [] + + # statistics['node'] = [stats_from_election_id, stats_from_prev_min, stats_now] + + election_id = ton.GetConfig34()['startWorkTime'] + if 'node' not in statistics or len(statistics['node']) == 0: + statistics['node'] = [None, data] + elif len(statistics['node']) < 3: + statistics['node'].append(data) + if len(statistics['node']) == 3: + if statistics['node'][0] is None: + if 0 < data['timestamp'] - election_id < 90: + statistics['node'][0] = data + elif statistics['node'][0]['timestamp'] < election_id: + statistics['node'][0] = data + statistics['node'] = statistics.get('node', []) + [data] + statistics['node'].pop(1) + local.db["statistics"] = statistics + + def ReadTransData(local, scanner): transData = local.buffer.transData SetToTimeData(transData, scanner.transNum) @@ -384,12 +444,6 @@ def Offers(local, ton): ton.VoteOffer(offer_hash) # end define - -def Domains(local, ton): - pass -# end define - - def Telemetry(local, ton): sendTelemetry = local.db.get("sendTelemetry") if sendTelemetry is not True: @@ -548,6 +602,17 @@ def ScanLiteServers(local, ton): # end define +def check_initial_sync(local, ton): + if not ton.in_initial_sync(): + return + validator_status = ton.GetValidatorStatus() + if validator_status.initial_sync: + return + if validator_status.out_of_sync < 20: + ton.set_initial_sync_off() + return + + def General(local): local.add_log("start General function", "debug") ton = MyTonCore(local) @@ -555,8 +620,14 @@ def General(local): # scanner.Run() # Start threads - local.start_cycle(Elections, sec=600, args=(local, ton, )) local.start_cycle(Statistics, sec=10, args=(local, )) + local.start_cycle(Telemetry, sec=60, args=(local, ton, )) + local.start_cycle(OverlayTelemetry, sec=7200, args=(local, ton, )) + if local.db.get("onlyNode"): # mytoncore service works only for telemetry + thr_sleep() + return + + local.start_cycle(Elections, sec=600, args=(local, ton, )) local.start_cycle(Offers, sec=600, args=(local, ton, )) local.start_cycle(save_past_events, sec=300, args=(local, ton, )) @@ -566,14 +637,22 @@ def General(local): local.start_cycle(Complaints, sec=t, args=(local, ton, )) local.start_cycle(Slashing, sec=t, args=(local, ton, )) - local.start_cycle(Domains, sec=600, args=(local, ton, )) - local.start_cycle(Telemetry, sec=60, args=(local, ton, )) - local.start_cycle(OverlayTelemetry, sec=7200, args=(local, ton, )) local.start_cycle(ScanLiteServers, sec=60, args=(local, ton,)) + local.start_cycle(save_node_statistics, sec=60, args=(local, ton, )) + from modules.custom_overlays import CustomOverlayModule local.start_cycle(CustomOverlayModule(ton, local).custom_overlays, sec=60, args=()) + from modules.alert_bot import AlertBotModule + local.start_cycle(AlertBotModule(ton, local).check_status, sec=60, args=()) + + from modules.prometheus import PrometheusModule + local.start_cycle(PrometheusModule(ton, local).push_metrics, sec=30, args=()) + + if ton.in_initial_sync(): + local.start_cycle(check_initial_sync, sec=120, args=(local, ton)) + thr_sleep() # end define diff --git a/mytoncore/models.py b/mytoncore/models.py index 474fa276..db803d9b 100644 --- a/mytoncore/models.py +++ b/mytoncore/models.py @@ -41,15 +41,6 @@ def __init__(self, workchain, addr): #end class -class Domain(dict): - def __init__(self): - self["name"] = None - self["adnlAddr"] = None - self["walletName"] = None - #end define -#end class - - class Block(): def __init__(self, str=None): self.workchain = None diff --git a/mytoncore/mytoncore.py b/mytoncore/mytoncore.py index 5fffd8e8..31190663 100644 --- a/mytoncore/mytoncore.py +++ b/mytoncore/mytoncore.py @@ -19,7 +19,6 @@ from mytoncore.models import ( Wallet, Account, - Domain, Block, Trans, Message, @@ -31,7 +30,7 @@ get_timestamp, timestamp2datetime, dec2hex, - Dict + Dict, int2ip ) @@ -116,13 +115,26 @@ def CheckConfigFile(self, fift, liteClient): self.local.add_log("Restoring the configuration file", "info") args = ["cp", backup_path, mconfig_path] subprocess.run(args) + self.dbFile = mconfig_path self.Refresh() - elif os.path.isfile(backup_path) == False: - self.local.add_log("Create backup config file", "info") - args = ["cp", mconfig_path, backup_path] - subprocess.run(args) + elif not os.path.isfile(backup_path) or time.time() - os.path.getmtime(backup_path) > 3600: + self.local.try_function(self.create_self_db_backup) #end define + def create_self_db_backup(self): + self.local.add_log("Create backup config file", "info") + mconfig_path = self.local.buffer.db_path + backup_path = mconfig_path + ".backup" + backup_tmp_path = backup_path + '.tmp' + subprocess.run(["cp", mconfig_path, backup_tmp_path]) + try: + with open(backup_tmp_path, "r") as file: + json.load(file) + os.rename(backup_tmp_path, backup_path) # atomic opetation + except: + self.local.add_log("Could not update backup, backup_tmp file is broken", "warning") + os.remove(backup_tmp_path) + def GetVarFromWorkerOutput(self, text, search): if ':' not in search: search += ':' @@ -390,48 +402,6 @@ def GetComment(self, body): return result #end define - def GetDomainAddr(self, domainName): - cmd = "dnsresolve {domainName} -1".format(domainName=domainName) - result = self.liteClient.Run(cmd) - if "not found" in result: - raise Exception("GetDomainAddr error: domain \"{domainName}\" not found".format(domainName=domainName)) - resolver = parse(result, "next resolver", '\n') - buff = resolver.replace(' ', '') - buffList = buff.split('=') - fullHexAddr = buffList[0] - addr = buffList[1] - return addr - #end define - - def GetDomainEndTime(self, domainName): - self.local.add_log("start GetDomainEndTime function", "debug") - buff = domainName.split('.') - subdomain = buff.pop(0) - dnsDomain = ".".join(buff) - dnsAddr = self.GetDomainAddr(dnsDomain) - - cmd = "runmethodfull {addr} getexpiration \"{subdomain}\"".format(addr=dnsAddr, subdomain=subdomain) - result = self.liteClient.Run(cmd) - result = parse(result, "result:", '\n') - result = parse(result, "[", "]") - result = result.replace(' ', '') - result = int(result) - return result - #end define - - def GetDomainAdnlAddr(self, domainName): - self.local.add_log("start GetDomainAdnlAddr function", "debug") - cmd = "dnsresolve {domainName} 1".format(domainName=domainName) - result = self.liteClient.Run(cmd) - lines = result.split('\n') - for line in lines: - if "adnl address" in line: - adnlAddr = parse(line, "=", "\n") - adnlAddr = adnlAddr.replace(' ', '') - adnlAddr = adnlAddr - return adnlAddr - #end define - def GetLocalWallet(self, walletName, version=None, subwallet=None): self.local.add_log("start GetLocalWallet function", "debug") if walletName is None: @@ -806,16 +776,24 @@ def GetShardsNumber(self, block=None): return shardsNum #end define - def GetValidatorStatus(self): + def parse_stats_from_vc(self, output: str, result: dict): + for line in output.split('\n'): + if len(line.split('\t\t\t')) == 2: + name, value = line.split('\t\t\t') # https://github.com/ton-blockchain/ton/blob/master/validator-engine-console/validator-engine-console-query.cpp#L648 + if name not in result: + result[name] = value + + def GetValidatorStatus(self, no_cache=False): # Get buffer bname = "validator_status" buff = self.GetFunctionBuffer(bname) - if buff: + if buff and not no_cache: return buff #end if self.local.add_log("start GetValidatorStatus function", "debug") status = Dict() + result = None try: # Parse status.is_working = True @@ -838,10 +816,20 @@ def GetValidatorStatus(self): status.masterchain_out_of_ser = status.masterchainblock - status.stateserializermasterchainseqno status.out_of_sync = status.masterchain_out_of_sync if status.masterchain_out_of_sync > status.shardchain_out_of_sync else status.shardchain_out_of_sync status.out_of_ser = status.masterchain_out_of_ser + status.last_deleted_mc_state = int(parse(result, "last_deleted_mc_state", '\n')) + status.stateserializerenabled = parse(result, "stateserializerenabled", '\n') == "true" + self.local.try_function(self.parse_stats_from_vc, args=[result, status]) + if 'active_validator_groups' in status: + groups = status.active_validator_groups.split() # master:1 shard:2 + status.validator_groups_master = int(groups[0].split(':')[1]) + status.validator_groups_shard = int(groups[1].split(':')[1]) except Exception as ex: self.local.add_log(f"GetValidatorStatus warning: {ex}", "warning") status.is_working = False + if result is not None: + self.local.try_function(self.parse_stats_from_vc, args=[result, status]) #end try + status.initial_sync = status.get("process.initial_sync") # old vars status.outOfSync = status.out_of_sync @@ -1269,8 +1257,13 @@ def WaitTransaction(self, wallet, timeout=30): steps = timeout // timesleep for i in range(steps): time.sleep(timesleep) - seqno = self.GetSeqno(wallet) + try: + seqno = self.GetSeqno(wallet) + except: + self.local.add_log("WaitTransaction error: Can't get seqno", "warning") + continue if seqno != wallet.oldseqno: + self.local.add_log("WaitTransaction success", "info") return raise Exception("WaitTransaction error: time out") #end define @@ -1492,21 +1485,47 @@ def ElectionEntry(self, args=None): self.local.add_log("ElectionEntry completed. Start work time: " + str(startWorkTime)) self.clear_tmp() + self.make_backup(startWorkTime) #end define - def clear_tmp(self): + def clear_dir(self, dir_name): start = time.time() count = 0 week_ago = 60 * 60 * 24 * 7 - dir = self.tempDir - for f in os.listdir(dir): - ts = os.path.getmtime(os.path.join(dir, f)) + for f in os.listdir(dir_name): + ts = os.path.getmtime(os.path.join(dir_name, f)) if ts < time.time() - week_ago: count += 1 - os.remove(os.path.join(dir, f)) + if os.path.isfile(os.path.join(dir_name, f)): + os.remove(os.path.join(dir_name, f)) + self.local.add_log(f"Removed {count} old files from {dir_name} directory for {int(time.time() - start)} seconds", "info") + + def clear_tmp(self): + self.clear_dir(self.tempDir) - self.local.add_log(f"Removed {count} old files from tmp dir for {int(time.time() - start)} seconds", "info") + def make_backup(self, election_id: str): + if not self.local.db.get("auto_backup"): + return + from modules.backups import BackupModule + module = BackupModule(self, self.local) + args = [] + name = f"/mytonctrl_backup_elid{election_id}.zip" + backups_dir = self.tempDir + "/auto_backups" + if self.local.db.get("auto_backup_path"): + backups_dir = self.local.db.get("auto_backup_path") + os.makedirs(backups_dir, exist_ok=True) + args.append(backups_dir + name) + self.clear_dir(backups_dir) + exit_code = module.create_backup(args) + if exit_code != 0: + self.local.add_log(f"Backup failed with exit code {exit_code}", "error") + # try one more time + exit_code = module.create_backup(args) + if exit_code != 0: + self.local.add_log(f"Backup failed with exit code {exit_code}", "error") + if exit_code == 0: + self.local.add_log(f"Backup created successfully", "info") def GetValidatorKeyByTime(self, startWorkTime, endWorkTime): self.local.add_log("start GetValidatorKeyByTime function", "debug") @@ -1662,7 +1681,7 @@ def CreateWallet(self, name, workchain=0, version="v1", **kwargs): if os.path.isfile(wallet_path + ".pk") and "v3" not in version: self.local.add_log("CreateWallet error: Wallet already exists: " + name, "warning") else: - fift_args = self.get_new_wallet_fift_args(version, workchain=workchain, + fift_args = self.get_new_wallet_fift_args(version, workchain=workchain, wallet_path=wallet_path, subwallet=subwallet) result = self.fift.Run(fift_args) if "Creating new" not in result: @@ -1705,18 +1724,6 @@ def ActivateWallet(self, wallet): self.SendFile(wallet.bocFilePath, wallet, remove=False) #end define - def ImportWallet(self, addr_b64, key): - addr_bytes = self.addr_b64_to_bytes(addr_b64) - pk_bytes = base64.b64decode(key) - wallet_name = self.GenerateWalletName() - wallet_path = self.walletsDir + wallet_name - with open(wallet_path + ".addr", 'wb') as file: - file.write(addr_bytes) - with open(wallet_path + ".pk", 'wb') as file: - file.write(pk_bytes) - return wallet_name - #end define - def import_wallet_with_version(self, key, version, **kwargs): wallet_name = kwargs.get("wallet_name") workchain = kwargs.get("workchain", 0) @@ -1731,7 +1738,7 @@ def import_wallet_with_version(self, key, version, **kwargs): wallet_path = self.walletsDir + wallet_name with open(wallet_path + ".pk", 'wb') as file: file.write(pk_bytes) - fift_args = self.get_new_wallet_fift_args(version, workchain=workchain, + fift_args = self.get_new_wallet_fift_args(version, workchain=workchain, wallet_path=wallet_path, subwallet=subwallet) result = self.fift.Run(fift_args) if "Creating new" not in result: @@ -1768,14 +1775,6 @@ def addr_b64_to_bytes(self, addr_b64): return result #end define - def ExportWallet(self, walletName): - wallet = self.GetLocalWallet(walletName) - with open(wallet.privFilePath, 'rb') as file: - data = file.read() - key = base64.b64encode(data).decode("utf-8") - return wallet.addrB64, key - #end define - def GetWalletsNameList(self): self.local.add_log("start GetWalletsNameList function", "debug") walletsNameList = list() @@ -1789,16 +1788,6 @@ def GetWalletsNameList(self): return walletsNameList #end define - def GetWallets(self): - self.local.add_log("start GetWallets function", "debug") - wallets = list() - walletsNameList = self.GetWalletsNameList() - for walletName in walletsNameList: - wallet = self.GetLocalWallet(walletName) - wallets.append(wallet) - return wallets - #end define - def GenerateWalletName(self): self.local.add_log("start GenerateWalletName function", "debug") index = 1 @@ -1821,16 +1810,6 @@ def GenerateWalletName(self): return walletName #end define - def WalletsCheck(self): - self.local.add_log("start WalletsCheck function", "debug") - wallets = self.GetWallets() - for wallet in wallets: - if os.path.isfile(wallet.bocFilePath): - account = self.GetAccount(wallet.addrB64) - if account.balance > 0: - self.SendFile(wallet.bocFilePath, wallet) - #end define - def GetValidatorConfig(self): #self.local.add_log("start GetValidatorConfig function", "debug") result = self.validatorConsole.Run("getconfig") @@ -1937,18 +1916,7 @@ def MoveCoins(self, wallet, dest, coins, **kwargs): self.SendFile(savedFilePath, wallet, timeout=timeout) #end define - def MoveCoinsThroughProxy(self, wallet, dest, coins): - self.local.add_log("start MoveCoinsThroughProxy function", "debug") - wallet1 = self.CreateWallet("proxy_wallet1", 0) - wallet2 = self.CreateWallet("proxy_wallet2", 0) - self.MoveCoins(wallet, wallet1.addrB64_init, coins) - self.ActivateWallet(wallet1) - self.MoveCoins(wallet1, wallet2.addrB64_init, "alld") - self.ActivateWallet(wallet2) - self.MoveCoins(wallet2, dest, "alld", flags=["-n"]) - wallet1.Delete() - wallet2.Delete() - #end define + def MoveCoinsFromHW(self, wallet, destList, **kwargs): self.local.add_log("start MoveCoinsFromHW function", "debug") @@ -2140,80 +2108,6 @@ def GetOffers(self): return offers #end define - def GetOfferDiff(self, offerHash): - self.local.add_log("start GetOfferDiff function", "debug") - offer = self.GetOffer(offerHash) - configId = offer["config"]["id"] - configValue = offer["config"]["value"] - - if '{' in configValue or '}' in configValue: - start = configValue.find('{') + 1 - end = configValue.find('}') - configValue = configValue[start:end] - #end if - - args = [self.liteClient.appPath, "--global-config", self.liteClient.configPath, "--verbosity", "0"] - process = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - time.sleep(1) - - fullConfigAddr = self.GetFullConfigAddr() - cmd = "runmethodfull {fullConfigAddr} list_proposals".format(fullConfigAddr=fullConfigAddr) - process.stdin.write(cmd.encode() + b'\n') - process.stdin.flush() - time.sleep(1) - - cmd = "dumpcellas ConfigParam{configId} {configValue}".format(configId=configId, configValue=configValue) - process.stdin.write(cmd.encode() + b'\n') - process.stdin.flush() - time.sleep(1) - - process.terminate() - text = process.stdout.read().decode() - - lines = text.split('\n') - b = len(lines) - for i in range(b): - line = lines[i] - if "dumping cells as values of TLB type" in line: - a = i + 2 - break - #end for - - for i in range(a, b): - line = lines[i] - if '(' in line: - start = i - break - #end for - - for i in range(a, b): - line = lines[i] - if '>' in line: - end = i - break - #end for - - buff = lines[start:end] - text = "".join(buff) - newData = self.Tlb2Json(text) - newFileName = self.tempDir + "data1diff" - file = open(newFileName, 'wt') - newText = json.dumps(newData, indent=2) - file.write(newText) - file.close() - - oldData = self.GetConfig(configId) - oldFileName = self.tempDir + "data2diff" - file = open(oldFileName, 'wt') - oldText = json.dumps(oldData, indent=2) - file.write(oldText) - file.close() - - print(oldText) - args = ["diff", "--color", oldFileName, newFileName] - subprocess.run(args) - #end define - def GetComplaints(self, electionId=None, past=False): # Get buffer bname = "complaints" + str(past) @@ -2473,6 +2367,7 @@ def get_valid_complaints(self, complaints: dict, election_id: int): continue exists = False + vload = None for item in validators_load.values(): if 'fileName' not in item: continue @@ -2482,15 +2377,16 @@ def get_valid_complaints(self, complaints: dict, election_id: int): pseudohash = pubkey + str(election_id) if pseudohash == complaint['pseudohash']: exists = True - vid = item['id'] + vload = item break if not exists: self.local.add_log(f"complaint {complaint['hash_hex']} declined: complaint info was not found, probably it's wrong", "info") continue - if vid >= config32['mainValidators']: - self.local.add_log(f"complaint {complaint['hash_hex']} declined: complaint created for non masterchain validator", "info") + if (vload["id"] >= config32['mainValidators'] and + vload["masterBlocksCreated"] + vload["workBlocksCreated"] > 0): + self.local.add_log(f"complaint {complaint['hash_hex']} declined: complaint created for non masterchain validator that created more than zero blocks", "info") continue # check complaint fine value @@ -2518,7 +2414,7 @@ def GetOnlineValidators(self): def GetValidatorsLoad(self, start, end, saveCompFiles=False) -> dict: # Get buffer - bname = f"validatorsLoad{start}{end}" + bname = f"validatorsLoad{start}{end}{saveCompFiles}" buff = self.GetFunctionBuffer(bname, timeout=60) if buff: return buff @@ -2562,7 +2458,10 @@ def GetValidatorsLoad(self, start, end, saveCompFiles=False) -> dict: wr = 0 else: wr = workBlocksCreated / workBlocksExpected - r = (mr + wr) / 2 + if masterBlocksExpected > 0: # show only masterchain efficiency for masterchain validator + r = mr + else: + r = (mr + wr) / 2 efficiency = round(r * 100, 2) if efficiency > 10: online = True @@ -2598,21 +2497,23 @@ def GetValidatorsLoad(self, start, end, saveCompFiles=False) -> dict: return data #end define - def GetValidatorsList(self, past=False, fast=False): + def GetValidatorsList(self, past=False, fast=False, start=None, end=None): # Get buffer - bname = "validatorsList" + str(past) + bname = "validatorsList" + str(past) + str(start) + str(end) buff = self.GetFunctionBuffer(bname, timeout=60) if buff: return buff #end if - timestamp = get_timestamp() - end = timestamp - 60 config = self.GetConfig34() - if fast: - start = end - 1000 - else: - start = config.get("startWorkTime") + if end is None: + timestamp = get_timestamp() + end = timestamp - 60 + if start is None: + if fast: + start = end - 1000 + else: + start = config.get("startWorkTime") if past: config = self.GetConfig32() start = config.get("startWorkTime") @@ -2635,6 +2536,8 @@ def GetValidatorsList(self, past=False, fast=False): validator["wr"] = validatorsLoad[vid]["wr"] validator["efficiency"] = validatorsLoad[vid]["efficiency"] validator["online"] = validatorsLoad[vid]["online"] + validator["master_blocks_created"] = validatorsLoad[vid]["masterBlocksCreated"] + validator["master_blocks_expected"] = validatorsLoad[vid]["masterBlocksExpected"] validator["blocks_created"] = validatorsLoad[vid]["masterBlocksCreated"] + validatorsLoad[vid]["workBlocksCreated"] validator["blocks_expected"] = validatorsLoad[vid]["masterBlocksExpected"] + validatorsLoad[vid]["workBlocksExpected"] validator["is_masterchain"] = False @@ -2644,6 +2547,8 @@ def GetValidatorsList(self, past=False, fast=False): validator["efficiency"] = round(validator["wr"] * 100, 2) if saveElectionEntries and adnlAddr in saveElectionEntries: validator["walletAddr"] = saveElectionEntries[adnlAddr]["walletAddr"] + validator["stake"] = saveElectionEntries[adnlAddr].get("stake") + validator["stake"] = int(validator["stake"]) if validator["stake"] else None #end for # Set buffer @@ -2659,6 +2564,8 @@ def CheckValidators(self, start, end): electionId = start complaints = self.GetComplaints(electionId) valid_complaints = self.get_valid_complaints(complaints, electionId) + voted_complaints = self.GetVotedComplaints(complaints) + voted_complaints_pseudohashes = [complaint['pseudohash'] for complaint in voted_complaints.values()] data = self.GetValidatorsLoad(start, end, saveCompFiles=True) fullElectorAddr = self.GetFullElectorAddr() wallet = self.GetValidatorWallet(mode="vote") @@ -2678,9 +2585,9 @@ def CheckValidators(self, start, end): var2 = item.get("var2") pubkey = item.get("pubkey") pseudohash = pubkey + str(electionId) - if pseudohash in valid_complaints: + if pseudohash in valid_complaints or pseudohash in voted_complaints_pseudohashes: # do not create complaints that already created or voted by ourself continue - if item['id'] >= config['mainValidators']: # do not create complaints for non-masterchain validators + if item['id'] >= config['mainValidators'] and item["masterBlocksCreated"] + item["workBlocksCreated"] > 0: # create complaints for non-masterchain validators only if they created 0 blocks continue # Create complaint fileName = self.remove_proofs_from_complaint(fileName) @@ -2766,24 +2673,6 @@ def GetDbSize(self, exceptions="log"): return result #end define - def check_adnl(self): - telemetry = self.local.db.get("sendTelemetry", False) - check_adnl = self.local.db.get("checkAdnl", telemetry) - if not check_adnl: - return - url = 'http://45.129.96.53/adnl_check' - try: - data = self.get_local_adnl_data() - response = requests.post(url, json=data, timeout=5).json() - except Exception as e: - self.local.add_log(f'Failed to check adnl connection: {type(e)}: {e}', 'error') - return False - result = response.get("ok") - if not result: - self.local.add_log(f'Failed to check adnl connection to local node: {response.get("message")}', 'error') - return result - #end define - def get_local_adnl_data(self): def int2ip(dec): @@ -2938,90 +2827,6 @@ def GetItemFromDict(self, data, search): return None #end define - def GetDomainFromAuction(self, walletName, addr): - wallet = self.GetLocalWallet(walletName) - bocPath = self.local.buffer.my_temp_dir + "get_dns_data.boc" - bocData = bytes.fromhex("b5ee9c7241010101000e0000182fcb26a20000000000000000f36cae4d") - with open(bocPath, 'wb') as file: - file.write(bocData) - resultFilePath = self.SignBocWithWallet(wallet, bocPath, addr, 0.1) - self.SendFile(resultFilePath, wallet) - #end define - - def NewDomain(self, domain): - self.local.add_log("start NewDomain function", "debug") - domainName = domain["name"] - buff = domainName.split('.') - subdomain = buff.pop(0) - dnsDomain = ".".join(buff) - dnsAddr = self.GetDomainAddr(dnsDomain) - wallet = self.GetLocalWallet(domain["walletName"]) - expireInSec = 700000 # fix me - catId = 1 # fix me - - # Check if domain is busy - domainEndTime = self.GetDomainEndTime(domainName) - if domainEndTime > 0: - raise Exception("NewDomain error: domain is busy") - #end if - - fileName = self.tempDir + "dns-msg-body.boc" - args = ["auto-dns.fif", dnsAddr, "add", subdomain, expireInSec, "owner", wallet.addrB64, "cat", catId, "adnl", domain["adnlAddr"], "-o", fileName] - result = self.fift.Run(args) - resultFilePath = parse(result, "Saved to file ", ')') - resultFilePath = self.SignBocWithWallet(wallet, resultFilePath, dnsAddr, 1.7) - self.SendFile(resultFilePath, wallet) - self.AddDomain(domain) - #end define - - def AddDomain(self, domain): - if "domains" not in self.local.db: - self.local.db["domains"] = list() - #end if - self.local.db["domains"].append(domain) - self.local.save() - #end define - - def GetDomains(self): - domains = self.local.db.get("domains", list()) - for domain in domains: - domainName = domain.get("name") - domain["endTime"] = self.GetDomainEndTime(domainName) - return domains - #end define - - def GetDomain(self, domainName): - domain = dict() - domain["name"] = domainName - domain["adnlAddr"] = self.GetDomainAdnlAddr(domainName) - domain["endTime"] = self.GetDomainEndTime(domainName) - return domain - #end define - - def DeleteDomain(self, domainName): - domains = self.local.db.get("domains") - for domain in domains: - if (domainName == domain.get("name")): - domains.remove(domain) - self.local.save() - return - raise Exception("DeleteDomain error: Domain not found") - #end define - - def GetAutoTransferRules(self): - autoTransferRules = self.local.db.get("autoTransferRules") - if autoTransferRules is None: - autoTransferRules = list() - self.local.db["autoTransferRules"] = autoTransferRules - return autoTransferRules - #end define - - def AddAutoTransferRule(self, rule): - autoTransferRules = self.GetAutoTransferRules() - autoTransferRules.append(rule) - self.local.save() - #end define - def AddBookmark(self, bookmark): if "bookmarks" not in self.local.db: self.local.db["bookmarks"] = list() @@ -3038,23 +2843,11 @@ def GetBookmarks(self): return bookmarks #end define - def GetBookmarkAddr(self, type, name): - bookmarks = self.local.db.get("bookmarks", list()) - for bookmark in bookmarks: - bookmarkType = bookmark.get("type") - bookmarkName = bookmark.get("name") - bookmarkAddr = bookmark.get("addr") - if (bookmarkType == type and bookmarkName == name): - return bookmarkAddr - raise Exception("GetBookmarkAddr error: Bookmark not found") - #end define - - def DeleteBookmark(self, name, type): + def DeleteBookmark(self, name): bookmarks = self.local.db.get("bookmarks") for bookmark in bookmarks: - bookmarkType = bookmark.get("type") - bookmarkName = bookmark.get("name") - if (type == bookmarkType and name == bookmarkName): + bookmark_name = bookmark.get("name") + if name == bookmark_name: bookmarks.remove(bookmark) self.local.save() return @@ -3062,23 +2855,12 @@ def DeleteBookmark(self, name, type): #end define def WriteBookmarkData(self, bookmark): - type = bookmark.get("type") - if type == "account": - addr = bookmark.get("addr") - account = self.GetAccount(addr) - if account.status == "empty": - data = "empty" - else: - data = account.balance - elif type == "domain": - domainName = bookmark.get("addr") - endTime = self.GetDomainEndTime(domainName) - if endTime == 0: - data = "free" - else: - data = timestamp2datetime(endTime, "%d.%m.%Y") + addr = bookmark.get("addr") + account = self.GetAccount(addr) + if account.status == "empty": + data = "empty" else: - data = "null" + data = account.balance bookmark["data"] = data #end define @@ -3125,18 +2907,18 @@ def GetVotedComplaints(self, complaints: dict): return result #end define - def GetDestinationAddr(self, destination): + def get_destination_addr(self, destination): if self.IsAddrB64(destination): pass elif self.IsAddrFull(destination): destination = self.AddrFull2AddrB64(destination) else: - walletsNameList = self.GetWalletsNameList() - if destination in walletsNameList: + wallets_name_list = self.GetWalletsNameList() + if destination in wallets_name_list: wallet = self.GetLocalWallet(destination) destination = wallet.addrB64 return destination - #end define + # end define def AddrFull2AddrB64(self, addrFull, bounceable=True): if addrFull is None or "None" in addrFull: @@ -3272,6 +3054,48 @@ def GetStatistics(self, name, statistics=None): return data #end define + def get_node_statistics(self): + """ + :return: stats for collated/validated blocks since round beggining and stats for ls queries for the last minute + """ + stats = self.local.db.get('statistics', {}).get('node') + result = {} + if stats is not None and len(stats) == 3 and stats[0] is not None: + for k in ['master', 'shard']: + result = { + 'collated': { + 'ok': 0, + 'error': 0, + }, + 'validated': { + 'ok': 0, + 'error': 0, + } + } + collated_ok = stats[2]['collated_blocks'][k]['ok'] - stats[0]['collated_blocks'][k]['ok'] + collated_error = stats[2]['collated_blocks'][k]['error'] - stats[0]['collated_blocks'][k]['error'] + validated_ok = stats[2]['validated_blocks'][k]['ok'] - stats[0]['validated_blocks'][k]['ok'] + validated_error = stats[2]['validated_blocks'][k]['error'] - stats[0]['validated_blocks'][k]['error'] + result['collated'][k] = { + 'ok': collated_ok, + 'error': collated_error, + } + result['validated'][k] = { + 'ok': validated_ok, + 'error': validated_error, + } + result['collated']['ok'] += collated_ok + result['collated']['error'] += collated_error + result['validated']['ok'] += validated_ok + result['validated']['error'] += validated_error + if stats is not None and len(stats) >= 2 and stats[0] is not None: + result['ls_queries'] = { + 'ok': stats[-1]['ls_queries']['ok'] - stats[-2]['ls_queries']['ok'], + 'error': stats[-1]['ls_queries']['error'] - stats[-2]['ls_queries']['error'], + 'time': stats[-1].get('timestamp', 0) - stats[-2].get('timestamp', 0), + } + return result + def GetSettings(self, name): # self.local.load_db() result = self.local.db.get(name) @@ -3284,6 +3108,7 @@ def SetSettings(self, name, data): except: pass self.local.db[name] = data self.local.save() + self.create_self_db_backup() #end define def migrate_to_modes(self): @@ -3325,6 +3150,9 @@ def check_enable_mode(self, name): if self.using_liteserver(): raise Exception(f'Cannot enable validator mode while liteserver mode is enabled. ' f'Use `disable_mode liteserver` first.') + if name == 'liquid-staking': + from mytoninstaller.settings import enable_ton_http_api + enable_ton_http_api(self.local) def enable_mode(self, name): if name not in MODES: @@ -3365,6 +3193,19 @@ def using_validator(self): def using_liteserver(self): return self.get_mode_value('liteserver') + def using_alert_bot(self): + return self.get_mode_value('alert-bot') + + def using_prometheus(self): + return self.get_mode_value('prometheus') + + def in_initial_sync(self): + return self.local.db.get('initialSync', False) + + def set_initial_sync_off(self): + self.local.db.pop('initialSync', None) + self.local.save() + def Tlb2Json(self, text): # Заменить скобки start = 0 @@ -3561,9 +3402,8 @@ def PendWithdrawFromPool(self, poolAddr, amount): #end define def HandlePendingWithdraw(self, pendingWithdraws, poolAddr): - amount = pendingWithdraws.get(poolAddr) + amount = pendingWithdraws.pop(poolAddr) self.WithdrawFromPoolProcess(poolAddr, amount) - pendingWithdraws.pop(poolAddr) #end define def GetPendingWithdraws(self): @@ -3785,7 +3625,8 @@ def GetController(self, mode): #end define def GetControllerRequiredBalanceForLoan(self, controllerAddr, credit, interest): - cmd = f"runmethodfull {controllerAddr} required_balance_for_loan {credit} {interest}" + credit_nano_tons = credit * 10**9 + cmd = f"runmethodfull {controllerAddr} required_balance_for_loan {credit_nano_tons} {interest}" result = self.liteClient.Run(cmd) data = self.Result2List(result) if data is None: @@ -3877,7 +3718,7 @@ def CalculateLoanAmount(self, min_loan, max_loan, max_interest): print(f"CalculateLoanAmount data: {data}") url = "http://127.0.0.1:8801/runGetMethod" - res = requests.post(url, json=data) + res = requests.post(url, json=data, timeout=3) res_data = res.json() if res_data.get("ok") is False: error = res_data.get("error") @@ -4063,6 +3904,16 @@ def GetNetworkName(self): return "unknown" #end define + def get_node_ip(self): + try: + config = self.GetValidatorConfig() + return int2ip(config['addrs'][0]['ip']) + except: + return None + + def get_validator_engine_ip(self): + return self.validatorConsole.addr.split(':')[0] + def GetFunctionBuffer(self, name, timeout=10): timestamp = get_timestamp() buff = self.local.buffer.get(name) diff --git a/mytoncore/utils.py b/mytoncore/utils.py index 0a8bdc91..a31e299c 100644 --- a/mytoncore/utils.py +++ b/mytoncore/utils.py @@ -1,6 +1,7 @@ import base64 import json import re +import subprocess def str2b64(s): @@ -97,3 +98,6 @@ def parse_db_stats(path: str): result[s[0]] = {k: float(v) for k, v in items} return result # end define + +def get_hostname(): + return subprocess.run(["hostname", "-f"], stdout=subprocess.PIPE).stdout.decode().strip() diff --git a/mytonctrl/mytonctrl.py b/mytonctrl/mytonctrl.py index 3811c7a4..9bad6550 100755 --- a/mytonctrl/mytonctrl.py +++ b/mytonctrl/mytonctrl.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf_8 -*- import base64 +import random import subprocess import json import psutil @@ -10,6 +11,8 @@ from functools import partial +import requests + from mypylib.mypylib import ( int2ip, get_git_author_and_repo, @@ -29,7 +32,7 @@ color_text, bcolors, Dict, - MyPyClass + MyPyClass, ip2int ) from mypyconsole.mypyconsole import MyPyConsole @@ -42,10 +45,12 @@ ) from mytoncore.telemetry import is_host_virtual from mytonctrl.migrate import run_migrations -from mytonctrl.utils import GetItemFromList, timestamp2utcdatetime, fix_git_config, GetColorInt +from mytonctrl.utils import GetItemFromList, timestamp2utcdatetime, fix_git_config, is_hex, GetColorInt import sys, getopt, os +from mytoninstaller.config import get_own_ip + def Init(local, ton, console, argv): # Load translate table @@ -66,6 +71,7 @@ def inject_globals(func): console.name = "MyTonCtrl" console.startFunction = inject_globals(PreUp) console.debug = ton.GetSettings("debug") + console.local = local console.AddItem("update", inject_globals(Update), local.translate("update_cmd")) console.AddItem("upgrade", inject_globals(Upgrade), local.translate("upgrade_cmd")) @@ -80,56 +86,18 @@ def inject_globals(func): console.AddItem("set", inject_globals(SetSettings), local.translate("set_cmd")) console.AddItem("rollback", inject_globals(rollback_to_mtc1), local.translate("rollback_cmd")) - console.AddItem("seqno", inject_globals(Seqno), local.translate("seqno_cmd")) - console.AddItem("getconfig", inject_globals(GetConfig), local.translate("getconfig_cmd")) - console.AddItem("get_pool_data", inject_globals(GetPoolData), local.translate("get_pool_data_cmd")) - - console.AddItem("nw", inject_globals(CreatNewWallet), local.translate("nw_cmd")) - console.AddItem("aw", inject_globals(ActivateWallet), local.translate("aw_cmd")) - console.AddItem("wl", inject_globals(PrintWalletsList), local.translate("wl_cmd")) - console.AddItem("iw", inject_globals(ImportWallet), local.translate("iw_cmd")) - console.AddItem("swv", inject_globals(SetWalletVersion), local.translate("swv_cmd")) - console.AddItem("ew", inject_globals(ExportWallet), local.translate("ex_cmd")) - console.AddItem("dw", inject_globals(DeleteWallet), local.translate("dw_cmd")) - - console.AddItem("vas", inject_globals(ViewAccountStatus), local.translate("vas_cmd")) - console.AddItem("vah", inject_globals(ViewAccountHistory), local.translate("vah_cmd")) - console.AddItem("mg", inject_globals(MoveCoins), local.translate("mg_cmd")) - console.AddItem("mgtp", inject_globals(MoveCoinsThroughProxy), local.translate("mgtp_cmd")) - - console.AddItem("nb", inject_globals(CreatNewBookmark), local.translate("nb_cmd")) - console.AddItem("bl", inject_globals(PrintBookmarksList), local.translate("bl_cmd")) - console.AddItem("db", inject_globals(DeleteBookmark), local.translate("db_cmd")) - - # console.AddItem("nr", inject_globals(CreatNewAutoTransferRule), local.translate("nr_cmd")) # "Добавить правило автопереводов в расписание / Create new auto transfer rule" - # console.AddItem("rl", inject_globals(PrintAutoTransferRulesList), local.translate("rl_cmd")) # "Показать правила автопереводов / Show auto transfer rule list" - # console.AddItem("dr", inject_globals(DeleteAutoTransferRule), local.translate("dr_cmd")) # "Удалить правило автопереводов из расписания / Delete auto transfer rule" - - # console.AddItem("nd", inject_globals(NewDomain), local.translate("nd_cmd")) - # console.AddItem("dl", inject_globals(PrintDomainsList), local.translate("dl_cmd")) - # console.AddItem("vds", inject_globals(ViewDomainStatus), local.translate("vds_cmd")) - # console.AddItem("dd", inject_globals(DeleteDomain), local.translate("dd_cmd")) - # console.AddItem("gdfa", inject_globals(GetDomainFromAuction), local.translate("gdfa_cmd")) - - console.AddItem("ol", inject_globals(PrintOffersList), local.translate("ol_cmd")) - console.AddItem("od", inject_globals(OfferDiff), local.translate("od_cmd")) - - console.AddItem("el", inject_globals(PrintElectionEntriesList), local.translate("el_cmd")) - console.AddItem("vl", inject_globals(PrintValidatorList), local.translate("vl_cmd")) - console.AddItem("cl", inject_globals(PrintComplaintsList), local.translate("cl_cmd")) - #console.AddItem("xrestart", inject_globals(Xrestart), local.translate("xrestart_cmd")) #console.AddItem("xlist", inject_globals(Xlist), local.translate("xlist_cmd")) #console.AddItem("gpk", inject_globals(GetPubKey), local.translate("gpk_cmd")) #console.AddItem("ssoc", inject_globals(SignShardOverlayCert), local.translate("ssoc_cmd")) #console.AddItem("isoc", inject_globals(ImportShardOverlayCert), local.translate("isoc_cmd")) - from modules.custom_overlays import CustomOverlayModule - module = CustomOverlayModule(ton, local) + from modules.backups import BackupModule + module = BackupModule(ton, local) module.add_console_commands(console) - from modules.collator_config import CollatorConfigModule - module = CollatorConfigModule(ton, local) + from modules.custom_overlays import CustomOverlayModule + module = CustomOverlayModule(ton, local) module.add_console_commands(console) if ton.using_validator(): @@ -137,6 +105,18 @@ def inject_globals(func): module = ValidatorModule(ton, local) module.add_console_commands(console) + from modules.collator_config import CollatorConfigModule + module = CollatorConfigModule(ton, local) + module.add_console_commands(console) + + from modules.wallet import WalletModule + module = WalletModule(ton, local) + module.add_console_commands(console) + + from modules.utilities import UtilitiesModule + module = UtilitiesModule(ton, local) + module.add_console_commands(console) + if ton.using_pool(): # add basic pool functions (pools_list, delete_pool, import_pool) from modules.pool import PoolModule module = PoolModule(ton, local) @@ -157,9 +137,13 @@ def inject_globals(func): module = ControllerModule(ton, local) module.add_console_commands(console) - console.AddItem("cleanup", inject_globals(cleanup_validator_db), local.translate("cleanup_cmd")) + if ton.using_alert_bot(): + from modules.alert_bot import AlertBotModule + module = AlertBotModule(ton, local) + module.add_console_commands(console) + console.AddItem("benchmark", inject_globals(run_benchmark), local.translate("benchmark_cmd")) - console.AddItem("activate_ton_storage_provider", inject_globals(activate_ton_storage_provider), local.translate("activate_ton_storage_provider_cmd")) + # console.AddItem("activate_ton_storage_provider", inject_globals(activate_ton_storage_provider), local.translate("activate_ton_storage_provider_cmd")) # Process input parameters opts, args = getopt.getopt(argv,"hc:w:",["config=","wallets="]) @@ -247,7 +231,6 @@ def PreUp(local: MyPyClass, ton: MyTonCore): CheckMytonctrlUpdate(local) check_installer_user(local) check_vport(local, ton) - ton.check_adnl() warnings(local, ton) # CheckTonUpdate() #end define @@ -257,7 +240,7 @@ def Installer(args): # args = ["python3", "/usr/src/mytonctrl/mytoninstaller.py"] cmd = ["python3", "-m", "mytoninstaller"] if args: - cmd += ["-c", *args] + cmd += ["-c", " ".join(args)] subprocess.run(cmd) #end define @@ -315,7 +298,7 @@ def check_git(input_args, default_repo, text, default_branch='master'): need_branch = data.get("branch") # Check if remote repo is different from default - if ((need_author is None and local_author != default_author) or + if ((need_author is None and local_author != default_author) or (need_repo is None and local_repo != default_repo)): remote_url = f"https://github.com/{local_author}/{local_repo}/tree/{need_branch if need_branch else local_branch}" raise Exception(f"{text} error: You are on {remote_url} remote url, to update to the tip use `{text} {remote_url}` command") @@ -334,8 +317,11 @@ def check_git(input_args, default_repo, text, default_branch='master'): #end define def check_branch_exists(author, repo, branch): + if len(branch) >= 6 and is_hex(branch): + print('Hex name detected, skip branch existence check.') + return url = f"https://github.com/{author}/{repo}.git" - args = ["git", "ls-remote", "--heads", url, branch] + args = ["git", "ls-remote", "--heads", "--tags", url, branch] process = subprocess.run(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=3) output = process.stdout.decode("utf-8") if branch not in output: @@ -384,14 +370,6 @@ def Upgrade(ton, args): upgrade_script_path = pkg_resources.resource_filename('mytonctrl', 'scripts/upgrade.sh') runArgs = ["bash", upgrade_script_path, "-a", author, "-r", repo, "-b", branch] exitCode = run_as_root(runArgs) - if ton.using_validator(): - try: - from mytoninstaller.mytoninstaller import set_node_argument, get_node_args - node_args = get_node_args() - if node_args.get('--state-ttl') == '604800': - set_node_argument(ton.local, ["--state-ttl", "-d"]) - except Exception as e: - color_print(f"{{red}}Failed to set node argument: {e} {{endc}}") if exitCode == 0: text = "Upgrade - {green}OK{endc}" else: @@ -418,12 +396,6 @@ def rollback_to_mtc1(local, ton, args): local.exit() #end define -def cleanup_validator_db(ton, args): - cleanup_script_path = pkg_resources.resource_filename('mytonctrl', 'scripts/cleanup.sh') - run_args = ["bash", cleanup_script_path] - exit_code = run_as_root(run_args) -#end define - def run_benchmark(ton, args): timeout = 200 benchmark_script_path = pkg_resources.resource_filename('mytonctrl', 'scripts/benchmark.sh') @@ -468,6 +440,9 @@ def check_disk_usage(local, ton): def check_sync(local, ton): validator_status = ton.GetValidatorStatus() + if validator_status.initial_sync or ton.in_initial_sync(): + print_warning(local, "initial_sync_warning") + return if not validator_status.is_working or validator_status.out_of_sync >= 20: print_warning(local, "sync_warning") #end define @@ -494,11 +469,40 @@ def check_vps(local, ton): color_print(f"Virtualization detected: {data['product_name']}") #end define +def check_tg_channel(local, ton): + if ton.using_validator() and ton.local.db.get("subscribe_tg_channel") is None: + print_warning(local, "subscribe_tg_channel_warning") +#end difine + +def check_slashed(local, ton): + validator_status = ton.GetValidatorStatus() + if not ton.using_validator() or not validator_status.is_working or validator_status.out_of_sync >= 20: + return + from modules import ValidatorModule + validator_module = ValidatorModule(ton, local) + c = validator_module.get_my_complaint() + if c: + warning = local.translate("slashed_warning").format(int(c['suggestedFine'])) + print_warning(local, warning) +#end define + +def check_adnl(local, ton): + from modules.utilities import UtilitiesModule + utils_module = UtilitiesModule(ton, local) + ok, error = utils_module.check_adnl_connection() + if not ok: + error = "{red}" + error + "{endc}" + print_warning(local, error) +#end define + def warnings(local, ton): - check_disk_usage(local, ton) - check_sync(local, ton) - check_validator_balance(local, ton) - check_vps(local, ton) + local.try_function(check_disk_usage, args=[local, ton]) + local.try_function(check_sync, args=[local, ton]) + local.try_function(check_adnl, args=[local, ton]) + local.try_function(check_validator_balance, args=[local, ton]) + local.try_function(check_vps, args=[local, ton]) + local.try_function(check_tg_channel, args=[local, ton]) + local.try_function(check_slashed, args=[local, ton]) #end define def CheckTonUpdate(local): @@ -508,21 +512,15 @@ def CheckTonUpdate(local): color_print(local.translate("ton_update_available")) #end define -def PrintTest(local, args): - print(json.dumps(local.buffer, indent=2)) -#end define - -def sl(ton, args): - Slashing(ton.local, ton) -#end define - - def mode_status(ton, args): from modules import get_mode modes = ton.get_modes() table = [["Name", "Status", "Description"]] for mode_name in modes: mode = get_mode(mode_name) + if mode is None: + color_print(f"{{red}}Mode {mode_name} not found{{endc}}") + continue status = color_text('{green}enabled{endc}' if modes[mode_name] else '{red}disabled{endc}') table.append([mode_name, status, mode.description]) print_table(table) @@ -575,7 +573,7 @@ def PrintStatus(local, ton, args): config34 = ton.GetConfig34() config36 = ton.GetConfig36() totalValidators = config34["totalValidators"] - + if opt != "fast": onlineValidators = ton.GetOnlineValidators() # validator_efficiency = ton.GetValidatorEfficiency() @@ -586,28 +584,28 @@ def PrintStatus(local, ton, args): if oldStartWorkTime is None: oldStartWorkTime = config34.get("startWorkTime") shardsNumber = ton.GetShardsNumber() - + config15 = ton.GetConfig15() config17 = ton.GetConfig17() fullConfigAddr = ton.GetFullConfigAddr() fullElectorAddr = ton.GetFullElectorAddr() startWorkTime = ton.GetActiveElectionId(fullElectorAddr) validator_index = ton.GetValidatorIndex() - + offersNumber = ton.GetOffersNumber() complaintsNumber = ton.GetComplaintsNumber() - + tpsAvg = ton.GetStatistics("tpsAvg", statistics) - + if validator_wallet is not None: validator_account = ton.GetAccount(validator_wallet.addrB64) #end if if all_status: PrintTonStatus(local, network_name, startWorkTime, totalValidators, onlineValidators, shardsNumber, offersNumber, complaintsNumber, tpsAvg) - PrintLocalStatus(local, adnl_addr, validator_index, validator_efficiency, validator_wallet, validator_account, validator_status, + PrintLocalStatus(local, ton, adnl_addr, validator_index, validator_efficiency, validator_wallet, validator_account, validator_status, db_size, db_usage, memory_info, swap_info, net_load_avg, disks_load_avg, disks_load_percent_avg, fullnode_adnl) - if all_status: + if all_status and ton.using_validator(): PrintTonConfig(local, fullConfigAddr, fullElectorAddr, config15, config17) PrintTimes(local, rootWorkchainEnabledTime_int, startWorkTime, oldStartWorkTime, config15) #end define @@ -656,7 +654,7 @@ def PrintTonStatus(local, network_name, startWorkTime, totalValidators, onlineVa print() #end define -def PrintLocalStatus(local, adnlAddr, validatorIndex, validatorEfficiency, validatorWallet, validatorAccount, validator_status, dbSize, dbUsage, memoryInfo, swapInfo, netLoadAvg, disksLoadAvg, disksLoadPercentAvg, fullnode_adnl): +def PrintLocalStatus(local, ton, adnlAddr, validatorIndex, validatorEfficiency, validatorWallet, validatorAccount, validator_status, dbSize, dbUsage, memoryInfo, swapInfo, netLoadAvg, disksLoadAvg, disksLoadPercentAvg, fullnode_adnl): if validatorWallet is None: return walletAddr = validatorWallet.addrB64 @@ -734,14 +732,50 @@ def PrintLocalStatus(local, adnlAddr, validatorIndex, validatorEfficiency, valid validatorStatus_color = GetColorStatus(validatorStatus_bool) mytoncoreStatus_text = local.translate("local_status_mytoncore_status").format(mytoncoreStatus_color, mytoncoreUptime_text) validatorStatus_text = local.translate("local_status_validator_status").format(validatorStatus_color, validatorUptime_text) - validator_out_of_sync_text = local.translate("local_status_validator_out_of_sync").format(GetColorInt(validator_status.out_of_sync, 20, logic="less", ending=" s")) - validator_out_of_ser_text = local.translate("local_status_validator_out_of_ser").format(f'{validator_status.out_of_ser} blocks ago') + validator_initial_sync_text = '' + validator_out_of_sync_text = '' + + if validator_status.initial_sync: + validator_initial_sync_text = local.translate("local_status_validator_initial_sync").format(validator_status['process.initial_sync']) + elif ton.in_initial_sync(): # states have been downloaded, now downloading blocks + validator_initial_sync_text = local.translate("local_status_validator_initial_sync").format( + f'Syncing blocks, last known block was {validator_status.out_of_sync} s ago' + ) + else: + validator_out_of_sync_text = local.translate("local_status_validator_out_of_sync").format(GetColorInt(validator_status.out_of_sync, 20, logic="less")) + master_out_of_sync_text = local.translate("local_status_master_out_of_sync").format(GetColorInt(validator_status.masterchain_out_of_sync, 20, logic="less", ending=" sec")) + shard_out_of_sync_text = local.translate("local_status_shard_out_of_sync").format(GetColorInt(validator_status.shardchain_out_of_sync, 5, logic="less", ending=" blocks")) + + validator_out_of_ser_text = None + + if validator_status.stateserializerenabled: + validator_out_of_ser_text = local.translate("local_status_validator_out_of_ser").format(f'{validator_status.out_of_ser} blocks ago') + + active_validator_groups = None + + if ton.using_validator() and validator_status.validator_groups_master and validator_status.validator_groups_shard: + active_validator_groups = local.translate("active_validator_groups").format(validator_status.validator_groups_master, validator_status.validator_groups_shard) + + collated, validated = None, None + ls_queries = None + if ton.using_validator(): + node_stats = ton.get_node_statistics() + if node_stats and 'collated' in node_stats and 'validated' in node_stats: + collated = local.translate('collated_blocks').format(node_stats['collated']['ok'], node_stats['collated']['error']) + validated = local.translate('validated_blocks').format(node_stats['validated']['ok'], node_stats['validated']['error']) + else: + collated = local.translate('collated_blocks').format('collecting data...', 'wait for the next validation round') + validated = local.translate('validated_blocks').format('collecting data...', 'wait for the next validation round') + if ton.using_liteserver(): + node_stats = ton.get_node_statistics() + if node_stats and 'ls_queries' in node_stats: + ls_queries = local.translate('ls_queries').format(node_stats['ls_queries']['time'], node_stats['ls_queries']['ok'], node_stats['ls_queries']['error']) dbSize_text = GetColorInt(dbSize, 1000, logic="less", ending=" Gb") dbUsage_text = GetColorInt(dbUsage, 80, logic="less", ending="%") dbStatus_text = local.translate("local_status_db").format(dbSize_text, dbUsage_text) - + # Mytonctrl and validator git hash mtcGitPath = "/usr/src/mytonctrl" validatorGitPath = "/usr/src/ton" @@ -760,21 +794,42 @@ def PrintLocalStatus(local, adnlAddr, validatorIndex, validatorEfficiency, valid validatorVersion_text = local.translate("local_status_version_validator").format(validatorGitHash_text, validatorGitBranch_text) color_print(local.translate("local_status_head")) - print(validatorIndex_text) - # print(validatorEfficiency_text) + node_ip = ton.get_validator_engine_ip() + is_node_remote = node_ip != '127.0.0.1' + if is_node_remote: + nodeIpAddr_text = local.translate("node_ip_address").format(node_ip) + color_print(nodeIpAddr_text) + if ton.using_validator(): + print(validatorIndex_text) + # print(validatorEfficiency_text) print(adnlAddr_text) print(fullnode_adnl_text) - print(walletAddr_text) - print(walletBalance_text) + if ton.using_validator(): + print(walletAddr_text) + print(walletBalance_text) print(cpuLoad_text) print(netLoad_text) print(memoryLoad_text) - + print(disksLoad_text) print(mytoncoreStatus_text) - print(validatorStatus_text) - print(validator_out_of_sync_text) - print(validator_out_of_ser_text) + if not is_node_remote: + print(validatorStatus_text) + if validator_initial_sync_text: + print(validator_initial_sync_text) + if validator_out_of_sync_text: + print(validator_out_of_sync_text) + print(master_out_of_sync_text) + print(shard_out_of_sync_text) + if validator_out_of_ser_text: + print(validator_out_of_ser_text) + if active_validator_groups: + print(active_validator_groups) + if collated and validated: + print(collated) + print(validated) + if ls_queries: + print(ls_queries) print(dbStatus_text) print(mtcVersion_text) print(validatorVersion_text) @@ -867,511 +922,6 @@ def GetColorTime(datetime, timestamp): return result #end define -def Seqno(ton, args): - try: - walletName = args[0] - except: - color_print("{red}Bad args. Usage:{endc} seqno ") - return - wallet = ton.GetLocalWallet(walletName) - seqno = ton.GetSeqno(wallet) - print(walletName, "seqno:", seqno) -#end define - -def CreatNewWallet(ton, args): - version = "v1" - try: - if len(args) == 0: - walletName = ton.GenerateWalletName() - workchain = 0 - else: - workchain = int(args[0]) - walletName = args[1] - if len(args) > 2: - version = args[2] - if len(args) == 4: - subwallet = int(args[3]) - else: - subwallet = 698983191 + workchain # 0x29A9A317 + workchain - except: - color_print("{red}Bad args. Usage:{endc} nw [ ]") - return - wallet = ton.CreateWallet(walletName, workchain, version, subwallet=subwallet) - table = list() - table += [["Name", "Workchain", "Address"]] - table += [[wallet.name, wallet.workchain, wallet.addrB64_init]] - print_table(table) -#end define - -def ActivateWallet(local, ton, args): - try: - walletName = args[0] - except Exception as err: - walletName = "all" - if walletName == "all": - ton.WalletsCheck() - else: - wallet = ton.GetLocalWallet(walletName) - ton.ActivateWallet(wallet) - color_print("ActivateWallet - {green}OK{endc}") -#end define - -def PrintWalletsList(ton, args): - table = list() - table += [["Name", "Status", "Balance", "Ver", "Wch", "Address"]] - data = ton.GetWallets() - if (data is None or len(data) == 0): - print("No data") - return - for wallet in data: - account = ton.GetAccount(wallet.addrB64) - if account.status != "active": - wallet.addrB64 = wallet.addrB64_init - table += [[wallet.name, account.status, account.balance, wallet.version, wallet.workchain, wallet.addrB64]] - print_table(table) -#end define - -def ImportWallet(ton, args): - try: - addr = args[0] - key = args[1] - except: - color_print("{red}Bad args. Usage:{endc} iw ") - return - name = ton.ImportWallet(addr, key) - print("Wallet name:", name) -#end define - -def SetWalletVersion(ton, args): - try: - addr = args[0] - version = args[1] - except: - color_print("{red}Bad args. Usage:{endc} swv ") - return - ton.SetWalletVersion(addr, version) - color_print("SetWalletVersion - {green}OK{endc}") -#end define - -def ExportWallet(ton, args): - try: - name = args[0] - except: - color_print("{red}Bad args. Usage:{endc} ew ") - return - addr, key = ton.ExportWallet(name) - print("Wallet name:", name) - print("Address:", addr) - print("Secret key:", key) -#end define - -def DeleteWallet(ton, args): - try: - walletName = args[0] - except: - color_print("{red}Bad args. Usage:{endc} dw ") - return - if input("Are you sure you want to delete this wallet (yes/no): ") != "yes": - print("Cancel wallet deletion") - return - wallet = ton.GetLocalWallet(walletName) - wallet.Delete() - color_print("DeleteWallet - {green}OK{endc}") -#end define - -def ViewAccountStatus(ton, args): - try: - addrB64 = args[0] - except: - color_print("{red}Bad args. Usage:{endc} vas ") - return - addrB64 = ton.GetDestinationAddr(addrB64) - account = ton.GetAccount(addrB64) - version = ton.GetVersionFromCodeHash(account.codeHash) - statusTable = list() - statusTable += [["Address", "Status", "Balance", "Version"]] - statusTable += [[addrB64, account.status, account.balance, version]] - codeHashTable = list() - codeHashTable += [["Code hash"]] - codeHashTable += [[account.codeHash]] - historyTable = GetHistoryTable(ton, addrB64, 10) - print_table(statusTable) - print() - print_table(codeHashTable) - print() - print_table(historyTable) -#end define - -def ViewAccountHistory(ton, args): - try: - addr = args[0] - limit = int(args[1]) - except: - color_print("{red}Bad args. Usage:{endc} vah ") - return - table = GetHistoryTable(ton, addr, limit) - print_table(table) -#end define - -def GetHistoryTable(ton, addr, limit): - addr = ton.GetDestinationAddr(addr) - account = ton.GetAccount(addr) - history = ton.GetAccountHistory(account, limit) - table = list() - typeText = color_text("{red}{bold}{endc}") - table += [["Time", typeText, "Coins", "From/To"]] - for message in history: - if message.srcAddr is None: - continue - srcAddrFull = f"{message.srcWorkchain}:{message.srcAddr}" - destAddFull = f"{message.destWorkchain}:{message.destAddr}" - if srcAddrFull == account.addrFull: - type = color_text("{red}{bold}>>>{endc}") - fromto = destAddFull - else: - type = color_text("{blue}{bold}<<<{endc}") - fromto = srcAddrFull - fromto = ton.AddrFull2AddrB64(fromto) - #datetime = timestamp2datetime(message.time, "%Y.%m.%d %H:%M:%S") - datetime = timeago(message.time) - table += [[datetime, type, message.value, fromto]] - return table -#end define - -def MoveCoins(ton, args): - try: - walletName = args[0] - destination = args[1] - amount = args[2] - flags = args[3:] - except: - color_print("{red}Bad args. Usage:{endc} mg ") - return - wallet = ton.GetLocalWallet(walletName) - destination = ton.GetDestinationAddr(destination) - ton.MoveCoins(wallet, destination, amount, flags=flags) - color_print("MoveCoins - {green}OK{endc}") -#end define - -def MoveCoinsThroughProxy(ton, args): - try: - walletName = args[0] - destination = args[1] - amount = args[2] - except: - color_print("{red}Bad args. Usage:{endc} mgtp ") - return - wallet = ton.GetLocalWallet(walletName) - destination = ton.GetDestinationAddr(destination) - ton.MoveCoinsThroughProxy(wallet, destination, amount) - color_print("MoveCoinsThroughProxy - {green}OK{endc}") -#end define - -def CreatNewBookmark(ton, args): - try: - name = args[0] - addr = args[1] - except: - color_print("{red}Bad args. Usage:{endc} nb ") - return - if ton.IsAddr(addr): - type = "account" - else: - type = "domain" - #end if - - bookmark = dict() - bookmark["name"] = name - bookmark["type"] = type - bookmark["addr"] = addr - ton.AddBookmark(bookmark) - color_print("CreatNewBookmark - {green}OK{endc}") -#end define - -def PrintBookmarksList(ton, args): - data = ton.GetBookmarks() - if (data is None or len(data) == 0): - print("No data") - return - table = list() - table += [["Name", "Type", "Address / Domain", "Balance / Exp. date"]] - for item in data: - name = item.get("name") - type = item.get("type") - addr = item.get("addr") - bookmark_data = item.get("data") - table += [[name, type, addr, bookmark_data]] - print_table(table) -#end define - -def DeleteBookmark(ton, args): - try: - name = args[0] - type = args[1] - except: - color_print("{red}Bad args. Usage:{endc} db ") - return - ton.DeleteBookmark(name, type) - color_print("DeleteBookmark - {green}OK{endc}") -#end define - -# def CreatNewAutoTransferRule(args): -# try: -# name = args[0] -# addr = args[1] -# except: -# color_print("{red}Bad args. Usage:{endc} nr ") -# return -# rule = dict() -# rule["name"] = name -# rule["addr"] = addr -# ton.AddAutoTransferRule(rule) -# color_print("CreatNewAutoTransferRule - {green}OK{endc}") -# #end define - -# def PrintAutoTransferRulesList(args): -# data = ton.GetRules() -# if (data is None or len(data) == 0): -# print("No data") -# return -# table = list() -# table += [["Name", "fix me"]] -# for item in data: -# table += [[item.get("name"), item.get("fix me")]] -# print_table(table) -# #end define - -# def DeleteAutoTransferRule(args): -# print("fix me") -# #end define - -def PrintOffersList(ton, args): - data = ton.GetOffers() - if (data is None or len(data) == 0): - print("No data") - return - if "--json" in args: - text = json.dumps(data, indent=2) - print(text) - else: - table = list() - table += [["Hash", "Config", "Votes", "W/L", "Approved", "Is passed"]] - for item in data: - hash = item.get("hash") - votedValidators = len(item.get("votedValidators")) - wins = item.get("wins") - losses = item.get("losses") - wl = "{0}/{1}".format(wins, losses) - approvedPercent = item.get("approvedPercent") - approvedPercent_text = "{0}%".format(approvedPercent) - isPassed = item.get("isPassed") - if "hash" not in args: - hash = Reduct(hash) - if isPassed == True: - isPassed = bcolors.green_text("true") - if isPassed == False: - isPassed = bcolors.red_text("false") - table += [[hash, item.config.id, votedValidators, wl, approvedPercent_text, isPassed]] - print_table(table) -#end define - -def OfferDiff(ton, args): - try: - offerHash = args[0] - offerHash = offerHash - except: - color_print("{red}Bad args. Usage:{endc} od ") - return - ton.GetOfferDiff(offerHash) -#end define - -def GetConfig(ton, args): - try: - configId = args[0] - configId = int(configId) - except: - color_print("{red}Bad args. Usage:{endc} gc ") - return - data = ton.GetConfig(configId) - text = json.dumps(data, indent=2) - print(text) -#end define - -def PrintComplaintsList(ton, args): - past = "past" in args - data = ton.GetComplaints(past=past) - if (data is None or len(data) == 0): - print("No data") - return - if "--json" in args: - text = json.dumps(data, indent=2) - print(text) - else: - table = list() - table += [["Election id", "ADNL", "Fine (part)", "Votes", "Approved", "Is passed"]] - for key, item in data.items(): - electionId = item.get("electionId") - adnl = item.get("adnl") - suggestedFine = item.get("suggestedFine") - suggestedFinePart = item.get("suggestedFinePart") - Fine_text = "{0} ({1})".format(suggestedFine, suggestedFinePart) - votedValidators = len(item.get("votedValidators")) - approvedPercent = item.get("approvedPercent") - approvedPercent_text = "{0}%".format(approvedPercent) - isPassed = item.get("isPassed") - if "adnl" not in args: - adnl = Reduct(adnl) - if isPassed == True: - isPassed = bcolors.green_text("true") - if isPassed == False: - isPassed = bcolors.red_text("false") - table += [[electionId, adnl, Fine_text, votedValidators, approvedPercent_text, isPassed]] - print_table(table) -#end define - -def NewDomain(ton, args): - try: - domainName = args[0] - walletName = args[1] - adnlAddr = args[2] - except: - color_print("{red}Bad args. Usage:{endc} nd ") - return - domain = dict() - domain["name"] = domainName - domain["adnlAddr"] = adnlAddr - domain["walletName"] = walletName - ton.NewDomain(domain) - color_print("NewDomain - {green}OK{endc}") -#end define - -def PrintDomainsList(ton, args): - data = ton.GetDomains() - if (data is None or len(data) == 0): - print("No data") - return - table = list() - table += [["Domain", "Wallet", "Expiration date", "ADNL address"]] - for item in data: - domainName = item.get("name") - walletName = item.get("walletName") - endTime = item.get("endTime") - endTime = timestamp2datetime(endTime, "%d.%m.%Y") - adnlAddr = item.get("adnlAddr") - table += [[domainName, walletName, endTime, adnlAddr]] - print_table(table) -#end define - -def ViewDomainStatus(ton, args): - try: - domainName = args[0] - except: - color_print("{red}Bad args. Usage:{endc} vds ") - return - domain = ton.GetDomain(domainName) - endTime = domain.get("endTime") - endTime = timestamp2datetime(endTime, "%d.%m.%Y") - adnlAddr = domain.get("adnlAddr") - table = list() - table += [["Domain", "Expiration date", "ADNL address"]] - table += [[domainName, endTime, adnlAddr]] - print_table(table) -#end define - -def DeleteDomain(ton, args): - try: - domainName = args[0] - except: - color_print("{red}Bad args. Usage:{endc} dd ") - return - ton.DeleteDomain(domainName) - color_print("DeleteDomain - {green}OK{endc}") -#end define - -def GetDomainFromAuction(ton, args): - try: - walletName = args[0] - addr = args[1] - except: - color_print("{red}Bad args. Usage:{endc} gdfa ") - return - ton.GetDomainFromAuction(walletName, addr) - color_print("GetDomainFromAuction - {green}OK{endc}") -#end define - -def PrintElectionEntriesList(ton, args): - past = "past" in args - data = ton.GetElectionEntries(past=past) - if (data is None or len(data) == 0): - print("No data") - return - if "--json" in args: - text = json.dumps(data, indent=2) - print(text) - else: - table = list() - table += [["ADNL", "Pubkey", "Wallet", "Stake", "Max-factor"]] - for key, item in data.items(): - adnl = item.get("adnlAddr") - pubkey = item.get("pubkey") - walletAddr = item.get("walletAddr") - stake = item.get("stake") - maxFactor = item.get("maxFactor") - if "adnl" not in args: - adnl = Reduct(adnl) - if "pubkey" not in args: - pubkey = Reduct(pubkey) - if "wallet" not in args: - walletAddr = Reduct(walletAddr) - table += [[adnl, pubkey, walletAddr, stake, maxFactor]] - print_table(table) -#end define - -def PrintValidatorList(ton, args): - past = "past" in args - fast = "fast" in args - data = ton.GetValidatorsList(past=past, fast=fast) - if (data is None or len(data) == 0): - print("No data") - return - if "--json" in args: - text = json.dumps(data, indent=2) - print(text) - else: - table = list() - table += [["id", "ADNL", "Pubkey", "Wallet", "Efficiency", "Online"]] - for i, item in enumerate(data): - adnl = item.get("adnlAddr") - pubkey = item.get("pubkey") - walletAddr = item.get("walletAddr") - efficiency = item.get("efficiency") - online = item.get("online") - if "adnl" not in args: - adnl = Reduct(adnl) - if "pubkey" not in args: - pubkey = Reduct(pubkey) - if "wallet" not in args: - walletAddr = Reduct(walletAddr) - if "offline" in args and online != False: - continue - if online == True: - online = bcolors.green_text("true") - if online == False: - online = bcolors.red_text("false") - table += [[str(i), adnl, pubkey, walletAddr, efficiency, online]] - print_table(table) -#end define - -def Reduct(item): - item = str(item) - if item is None: - result = None - else: - end = len(item) - result = item[0:6] + "..." + item[end-6:end] - return result -#end define - def GetSettings(ton, args): try: name = args[0] @@ -1382,7 +932,7 @@ def GetSettings(ton, args): print(json.dumps(result, indent=2)) #end define -def SetSettings(ton, args): +def SetSettings(local, ton, args): try: name = args[0] value = args[1] @@ -1434,6 +984,7 @@ def disable_mode(local, ton, args): local.exit() #end define + def Xrestart(inputArgs): if len(inputArgs) < 2: color_print("{red}Bad args. Usage:{endc} xrestart ") @@ -1473,21 +1024,6 @@ def ImportShardOverlayCert(ton, args): ton.ImportShardOverlayCert() #end define -def GetPoolData(ton, args): - try: - pool_name = args[0] - except: - color_print("{red}Bad args. Usage:{endc} get_pool_data ") - return - if ton.IsAddr(pool_name): - pool_addr = pool_name - else: - pool = ton.GetLocalPool(pool_name) - pool_addr = pool.addrB64 - pool_data = ton.GetPoolData(pool_addr) - print(json.dumps(pool_data, indent=4)) -#end define - ### Start of the program def mytonctrl(): diff --git a/mytonctrl/resources/translate.json b/mytonctrl/resources/translate.json index 9ece2f83..70cbccca 100644 --- a/mytonctrl/resources/translate.json +++ b/mytonctrl/resources/translate.json @@ -129,26 +129,6 @@ "ru": "Удалить закладку", "zh_TW": "刪除書籤" }, - "nd_cmd": { - "en": "New domain", - "ru": "Арендовать новый домен", - "zh_TW": "新建域名" - }, - "dl_cmd": { - "en": "Show domain list", - "ru": "Показать арендованные домены", - "zh_TW": "顯示域名列表" - }, - "vds_cmd": { - "en": "View domain status", - "ru": "Показать статус домена", - "zh_TW": "查看域名狀態" - }, - "dd_cmd": { - "en": "Delete domain", - "ru": "Удалить домен", - "zh_TW": "刪除域名" - }, "ol_cmd": { "en": "Show offers list", "ru": "Показать действующие предложения", @@ -265,9 +245,14 @@ "zh_TW": "選舉狀態: {0}" }, "local_status_head": { - "en": "{cyan}===[ Local validator status ]==={endc}", - "ru": "{cyan}===[ Статус локального валидатора ]==={endc}", - "zh_TW": "{cyan}===[ 本地驗證者狀態 ]==={endc}" + "en": "{cyan}===[ Node status ]==={endc}", + "ru": "{cyan}===[ Статус ноды ]==={endc}", + "zh_TW": "{cyan}===[ 节点狀態 ]==={endc}" + }, + "node_ip_address": { + "en": "Node IP address: {{bold}}{0}{{endc}}", + "ru": "IP адрес Ноды: {{bold}}{0}{{endc}}", + "zh_TW": "節點 IP 地址: {{bold}}{0}{{endc}}" }, "local_status_validator_index": { "en": "Validator index: {0}", @@ -334,16 +319,51 @@ "ru": "Статус локального валидатора: {0}, {1}", "zh_TW": "本地驗證者狀態: {0}, {1}" }, + "local_status_validator_initial_sync": { + "en": "Local validator initial sync status: {0}", + "ru": "Статус начальной синхронизации локального валидатора: {0}", + "zh_TW": "本地驗證者初始同步狀態: {0}" + }, "local_status_validator_out_of_sync": { "en": "Local validator out of sync: {0}", "ru": "Рассинхронизация локального валидатора: {0}", "zh_TW": "本地驗證者不同步: {0}" }, + "local_status_master_out_of_sync": { + "en": "Masterchain out of sync: {0}", + "ru": "Рассинхронизация Мастерчейна локального валидатора: {0}", + "zh_TW": "主鏈不同步: {0}" + }, + "local_status_shard_out_of_sync": { + "en": "Shardchain out of sync: {0}", + "ru": "Рассинхронизация Шардчейна локального валидатора: {0}", + "zh_TW": "分片鏈不同步: {0}" + }, "local_status_validator_out_of_ser": { "en": "Local validator last state serialization: {0}", "ru": "Серализация стейта локального валидатора была: {0}", "zh_TW": "本地驗證者最後一次狀態序列化: {0}" }, + "active_validator_groups": { + "en": "Active validator groups (masterchain,shardchain): {0},{1}", + "ru": "Активные группы валидатора (мастерчейн, шардчейн): {0},{1}", + "zh_TW": "活动验证器组(主链、碎片链: {0},{1}" + }, + "collated_blocks": { + "en": "Collated blocks since validation round started (success/error): {0}/{1}", + "ru": "Собранные блоки с начала раунда валидации (успех/ошибка): {0}/{1}", + "zh_TW": "自驗證週期開始以來的彙編區塊 (成功/錯誤): {0}/{1}" + }, + "validated_blocks": { + "en": "Validated blocks since validation round started (success/error): {0}/{1}", + "ru": "Проверенные блоки с начала раунда валидации (успех/ошибка): {0}/{1}", + "zh_TW": "自驗證週期開始以來的驗證區塊 (成功/錯誤): {0}/{1}" + }, + "ls_queries": { + "en": "Liteserver queries for the past {0} sec (success/error): {1}/{2}", + "ru": "Запросы к лайтсерверу за последние {0} сек (успех/ошибка): {1}/{2}", + "zh_TW": "過去 {0} 秒的輕量級服務器查詢 (成功/錯誤): {1}/{2}" + }, "local_status_db": { "en": "Local validator database size: {0}, {1}", "ru": "Размер БД локального валидатора: {0}, {1}", @@ -430,40 +450,95 @@ "zh_TW": "{red}這個版本已經過時了。請更新至第二版本: `update mytonctrl2`{endc}" }, "disk_usage_warning": { - "en": "{red} Disk is almost full, clean the TON database immediately: https://docs.ton.org/participate/nodes/node-maintenance-and-security#database-grooming {endc}", - "ru": "{red} Диск почти заполнен, немедленно очистите базу данных TON: https://docs.ton.org/participate/nodes/node-maintenance-and-security#database-grooming {endc}", - "zh_TW": "{red} 磁盤幾乎滿了,立即清理 TON 數據庫: https://docs.ton.org/participate/nodes/node-maintenance-and-security#database-grooming {endc}" + "en": "{red}Disk is almost full, clean the TON database immediately: https://docs.ton.org/participate/nodes/node-maintenance-and-security#database-grooming {endc}", + "ru": "{red}Диск почти заполнен, немедленно очистите базу данных TON: https://docs.ton.org/participate/nodes/node-maintenance-and-security#database-grooming {endc}", + "zh_TW": "{red}磁盤幾乎滿了,立即清理 TON 數據庫: https://docs.ton.org/participate/nodes/node-maintenance-and-security#database-grooming {endc}" }, "ton_update_available": { "en": "{green}TON update available. {red}Please update it with `upgrade` command.{endc}", "ru": "{green}Доступно обновление TON. {red}Пожалуйста, обновите его с помощью команды `upgrade`.{endc}", "zh_TW": "{green}TON 有可用更新. {red}請使用 `upgrade` 命令進行更新.{endc}" }, - "disk_usage_warning": { - "en": "{red} Disk is almost full, clean the TON database immediately: https://docs.ton.org/participate/nodes/node-maintenance-and-security#database-grooming {endc}", - "ru": "{red} Диск почти заполнен, немедленно очистите базу данных TON: https://docs.ton.org/participate/nodes/node-maintenance-and-security#database-grooming {endc}", - "zh_TW": "{red} 磁盤幾乎滿了,立即清理 TON 數據庫: https://docs.ton.org/participate/nodes/node-maintenance-and-security#database-grooming {endc}" + "subscribe_tg_channel_warning": { + "en": "{red}Make sure you are subscribed to the TON validators channel on Telegram: https://t.me/tonstatus {endc}\nTo disable this warning use command `set subscribe_tg_channel true`", + "ru": "{red}Убедитесь, что вы подписаны на канал валидаторов TON в Telegram: https://t.me/tonstatus {endc}\nЧтобы отключить это предупреждение, используйте команду `set subscribe_tg_channel true`", + "zh_TW": "{red}確保您已訂閱了 Telegram 上的 TON 驗證者頻道: https://t.me/tonstatus {endc}\n要禁用此警告,請使用命令 `set subscribe_tg_channel true`" + }, + "initial_sync_warning": { + "en": "{red}Initial Node sync is not completed. The displayed status is incomplete. {endc}", + "ru": "{red}Начальная синхронизация ноды не завершена. Отображаемый статус не полный. {endc}", + "zh_TW": "{red}初始節點同步未完成。顯示的狀態不完整。 {endc}" }, "sync_warning": { - "en": "{red} Node is out of sync. The displayed status is incomplete. {endc}", - "ru": "{red} Нода не синхронизирована с сетью. Отображаемый статус не полный. {endc}", - "zh_TW": "{red} 节点不与网络同步。顯示的狀態不完整。 {endc}" + "en": "{red}Node is out of sync. The displayed status is incomplete. {endc}", + "ru": "{red}Нода не синхронизирована с сетью. Отображаемый статус не полный. {endc}", + "zh_TW": "{red}节点不与网络同步。顯示的狀態不完整。 {endc}" }, "validator_balance_warning": { - "en": "{red} Validator wallet balance is low. {endc}", - "ru": "{red} Баланс кошелька валидатора низкий. {endc}", - "zh_TW": "{red} 驗證者錢包餘額不足。 {endc}" + "en": "{red}Validator wallet balance is low. {endc}", + "ru": "{red}Баланс кошелька валидатора низкий. {endc}", + "zh_TW": "{red}驗證者錢包餘額不足。 {endc}" }, "vps_warning": { - "en": "{red} Validator is running on a VPS. Use a dedicated server for better performance. {endc}", - "ru": "{red} Валидатор работает на VPS. Используйте выделенный сервер (дедик) для лучшей производительности. {endc}", - "zh_TW": "{red} 驗證者在 VPS 上運行。使用專用服務器以獲得更好的性能。 {endc}" + "en": "{red}Validator is running on a VPS. Use a dedicated server for better performance. {endc}", + "ru": "{red}Валидатор работает на VPS. Используйте выделенный сервер (дедик) для лучшей производительности. {endc}", + "zh_TW": "{red}驗證者在 VPS 上運行。使用專用服務器以獲得更好的性能。 {endc}" }, "vport_error": { "en": "{red}Error - UDP port of the validator is not accessible from the outside.{endc}", "ru": "{red}Ошибка - UDP порт валидатора недоступен извне.{endc}", "zh_TW": "{red}錯誤 - 驗證器的 UDP 端口無法從外部訪問.{endc}" }, + "slashed_warning": { + "en": "{{red}}You were fined by {0} TON for low efficiency in the previous round.{{endc}}", + "ru": "{{red}}Вы были оштрафованы на {0} TON за низкую эффективность в предыдущем раунде.{{endc}}", + "zh_TW": "{{red}}您因上一輪效率低而被罰款 {0} TON。{{endc}}" + }, + "add_custom_overlay_cmd": { + "en": "Add custom overlay", + "ru": "Добавить пользовательский оверлей", + "zh_TW": "添加自定義覆蓋" + }, + "list_custom_overlays_cmd": { + "en": "List participating custom overlays", + "ru": "Список участвуемых пользовательских оверлеев", + "zh_TW": "列出參與的自定義覆蓋" + }, + "delete_custom_overlay_cmd": { + "en": "Delete custom overlay", + "ru": "Удалить пользовательский оверлей", + "zh_TW": "刪除自定義覆蓋" + }, + "enable_alert_cmd": { + "en": "Enable specific Telegram Bot alert", + "ru": "Включить определенное оповещение через Telegram Bot", + "zh_TW": "啟用特定的 Telegram Bot 警報" + }, + "disable_alert_cmd": { + "en": "Disable specific Telegram Bot alert", + "ru": "Отключить определенное оповещение через Telegram Bot", + "zh_TW": "禁用特定的 Telegram Bot 警報" + }, + "list_alerts_cmd": { + "en": "List all available Telegram Bot alerts", + "ru": "Список всех доступных оповещений через Telegram Bot", + "zh_TW": "列出所有可用的 Telegram Bot 警報" + }, + "test_alert_cmd": { + "en": "Send test alert via Telegram Bot", + "ru": "Отправить тестовое оповещение через Telegram Bot", + "zh_TW": "通過 Telegram Bot 發送測試警報" + }, + "setup_alert_bot_cmd": { + "en": "Setup Telegram Bot for alerts", + "ru": "Настроить Telegram Bot для оповещений", + "zh_TW": "設置 Telegram Bot 以接收警報" + }, + "benchmark_cmd": { + "en": "Run benchmark", + "ru": "Запустить бенчмарк", + "zh_TW": "運行基準測試" + }, "000a": { "en": "001", "ru": "002", diff --git a/mytonctrl/scripts/cleanup.sh b/mytonctrl/scripts/cleanup.sh deleted file mode 100644 index d3d10a2e..00000000 --- a/mytonctrl/scripts/cleanup.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/bin/bash -set -e - -# Проверить sudo -if [ "$(id -u)" != "0" ]; then - echo "Please run script as root" - exit 1 -fi - -# Цвета -COLOR='\033[92m' -ENDC='\033[0m' - -db_path=/var/ton-work/db - -function get_directory_size { - buff=$(du -sh ${db_path} | awk '{print $1}') - echo ${buff} -} - -echo -e "${COLOR}[1/7]${ENDC} Start node/validator DB cleanup process" -echo -e "${COLOR}[2/7]${ENDC} Stop node/validator" -systemctl stop validator - -echo -e "${COLOR}[3/7]${ENDC} Node/validator DB size before cleanup = $(get_directory_size)" -find /var/ton-work/db -name 'LOG.old*' -exec rm {} + - -echo -e "${COLOR}[4/7]${ENDC} Node/validator DB size after deleting old files = $(get_directory_size)" -rm -r /var/ton-work/db/files/packages/temp.archive.* - -echo -e "${COLOR}[5/7]${ENDC} Node/validator DB size after deleting temporary files = $(get_directory_size)" -echo -e "${COLOR}[6/7]${ENDC} Start node/validator" -systemctl start validator - -echo -e "${COLOR}[7/7]${ENDC} Node/validator DB cleanup process completed" diff --git a/mytonctrl/scripts/create_backup.sh b/mytonctrl/scripts/create_backup.sh new file mode 100644 index 00000000..dbcfe5b8 --- /dev/null +++ b/mytonctrl/scripts/create_backup.sh @@ -0,0 +1,46 @@ +dest="mytonctrl_backup_$(hostname)_$(date +%s).tar.gz" +mtc_dir="$HOME/.local/share/mytoncore" +user=$(logname) +ton_dir="/var/ton-work" +keys_dir="/var/ton-work/keys" +# Get arguments +while getopts d:m:t:k: flag +do + case "${flag}" in + d) dest=${OPTARG};; + m) mtc_dir=${OPTARG};; + t) ton_dir=${OPTARG};; + k) keys_dir=${OPTARG};; + *) + echo "Flag -${flag} is not recognized. Aborting" + exit 1 ;; + esac +done + +COLOR='\033[92m' +ENDC='\033[0m' + +tmp_dir="/tmp/mytoncore/backupv2" +rm -rf $tmp_dir +mkdir $tmp_dir +mkdir $tmp_dir/db + +cp $ton_dir/db/config.json ${tmp_dir}/db +cp -r $ton_dir/db/keyring ${tmp_dir}/db +cp -r $keys_dir ${tmp_dir} +cp -r $mtc_dir $tmp_dir + +python3 -c "import json;f=open('${tmp_dir}/db/config.json');json.load(f);f.close()" || exit 1 # Check if config.json is copied correctly +python3 -c "import json;f=open('${tmp_dir}/mytoncore/mytoncore.db');json.load(f);f.close()" || exit 2 # Check if mytoncore.db is copied correctly + +echo -e "${COLOR}[1/2]${ENDC} Copied files to ${tmp_dir}" + +tar -zcf $dest -C $tmp_dir . + +chown $user:$user $dest + +echo -e "${COLOR}[2/2]${ENDC} Backup successfully created in ${dest}!" + +rm -rf $tmp_dir + +echo -e "If you wish to use archive package to migrate node to different machine please make sure to stop validator and mytoncore on donor (this) host prior to migration." diff --git a/mytonctrl/scripts/restore_backup.sh b/mytonctrl/scripts/restore_backup.sh new file mode 100644 index 00000000..042657d7 --- /dev/null +++ b/mytonctrl/scripts/restore_backup.sh @@ -0,0 +1,75 @@ +name="backup.tar.gz" +mtc_dir="$HOME/.local/share/mytoncore" +ip=0 +user=$(logname) +# Get arguments +while getopts n:m:i:u: flag +do + case "${flag}" in + n) name=${OPTARG};; + m) mtc_dir=${OPTARG};; + i) ip=${OPTARG};; + u) user=${OPTARG};; + *) + echo "Flag -${flag} is not recognized. Aborting" + exit 1 ;; + esac +done + +if [ ! -f "$name" ]; then + echo "Backup file not found, aborting." + exit 1 +fi + +COLOR='\033[92m' +ENDC='\033[0m' + +systemctl stop validator +systemctl stop mytoncore + +echo -e "${COLOR}[1/4]${ENDC} Stopped validator and mytoncore" + + +tmp_dir="/tmp/mytoncore/backup" +rm -rf $tmp_dir +mkdir $tmp_dir +tar -xvzf $name -C $tmp_dir + +if [ ! -d ${tmp_dir}/db ]; then + echo "Old version of backup detected" + mkdir ${tmp_dir}/db + mv ${tmp_dir}/config.json ${tmp_dir}/db + mv ${tmp_dir}/keyring ${tmp_dir}/db + +fi + +rm -rf /var/ton-work/db/keyring + +chown -R $user:$user ${tmp_dir}/mytoncore +chown -R $user:$user ${tmp_dir}/keys +chown validator:validator ${tmp_dir}/keys +chown -R validator:validator ${tmp_dir}/db + +cp -rfp ${tmp_dir}/db /var/ton-work +cp -rfp ${tmp_dir}/keys /var/ton-work +cp -rfpT ${tmp_dir}/mytoncore $mtc_dir + +chown -R validator:validator /var/ton-work/db/keyring + +echo -e "${COLOR}[2/4]${ENDC} Extracted files from archive" + +rm -r /var/ton-work/db/dht-* + +if [ $ip -ne 0 ]; then + echo "Replacing IP in node config" + python3 -c "import json;path='/var/ton-work/db/config.json';f=open(path);d=json.load(f);f.close();d['addrs'][0]['ip']=int($ip);f=open(path, 'w');f.write(json.dumps(d, indent=4));f.close()" +else + echo "IP is not provided, skipping IP replacement" +fi + +echo -e "${COLOR}[3/4]${ENDC} Deleted DHT files" + +systemctl start validator +systemctl start mytoncore + +echo -e "${COLOR}[4/4]${ENDC} Started validator and mytoncore" diff --git a/mytonctrl/scripts/update.sh b/mytonctrl/scripts/update.sh index 463d422c..c9de406b 100644 --- a/mytonctrl/scripts/update.sh +++ b/mytonctrl/scripts/update.sh @@ -12,7 +12,7 @@ author="ton-blockchain" repo="mytonctrl" branch="master" srcdir="/usr/src/" -bindir="/usr/bin/" +tmpdir="/tmp/mytonctrl_src/" # Get arguments while getopts a:r:b: flag @@ -28,17 +28,19 @@ done COLOR='\033[92m' ENDC='\033[0m' -# Go to work dir -cd ${srcdir} +mkdir -p ${tmpdir} +cd ${tmpdir} +rm -rf ${tmpdir}/${repo} +echo "https://github.com/${author}/${repo}.git -> ${branch}" +git clone --recursive https://github.com/${author}/${repo}.git || exit 1 -# uninstall previous version rm -rf ${srcdir}/${repo} pip3 uninstall -y mytonctrl # Update code -echo "https://github.com/${author}/${repo}.git -> ${branch}" -git clone --branch ${branch} --recursive https://github.com/${author}/${repo}.git -cd ${repo} +cd ${srcdir} +cp -rf ${tmpdir}/${repo} ${srcdir} +cd ${repo} && git checkout ${branch} pip3 install -U . systemctl daemon-reload diff --git a/mytonctrl/scripts/upgrade.sh b/mytonctrl/scripts/upgrade.sh index 68062226..fe952562 100644 --- a/mytonctrl/scripts/upgrade.sh +++ b/mytonctrl/scripts/upgrade.sh @@ -13,6 +13,7 @@ repo="ton" branch="master" srcdir="/usr/src/" bindir="/usr/bin/" +tmpdir="/tmp/ton_src/" # Get arguments while getopts a:r:b: flag @@ -29,7 +30,7 @@ COLOR='\033[92m' ENDC='\033[0m' # Установить дополнительные зависимости -apt-get install -y libsecp256k1-dev libsodium-dev ninja-build fio rocksdb-tools liblz4-dev libjemalloc-dev +apt-get install -y libsecp256k1-dev libsodium-dev ninja-build fio rocksdb-tools liblz4-dev libjemalloc-dev automake libtool # bugfix if the files are in the wrong place wget "https://ton-blockchain.github.io/global.config.json" -O global.config.json @@ -59,13 +60,22 @@ else opensslPath=${bindir}/openssl_3 fi +rm -rf ${tmpdir}/${repo} +mkdir -p ${tmpdir}/${repo} +cd ${tmpdir}/${repo} +echo "https://github.com/${author}/${repo}.git -> ${branch}" +git clone --recursive https://github.com/${author}/${repo}.git . || exit 1 + # Go to work dir cd ${srcdir}/${repo} ls -A1 | xargs rm -rf # Update code -echo "https://github.com/${author}/${repo}.git -> ${branch}" -git clone --branch ${branch} --recursive https://github.com/${author}/${repo}.git . +cp -rfT ${tmpdir}/${repo} . +git checkout ${branch} + +git submodule sync --recursive +git submodule update export CC=/usr/bin/clang export CXX=/usr/bin/clang++ diff --git a/mytonctrl/utils.py b/mytonctrl/utils.py index ebb46ca6..49f9ceb5 100644 --- a/mytonctrl/utils.py +++ b/mytonctrl/utils.py @@ -17,6 +17,14 @@ def GetItemFromList(data, index): pass +def is_hex(s): + try: + int(s, 16) + return True + except ValueError: + return False + + def fix_git_config(git_path: str): args = ["git", "status"] try: diff --git a/mytoninstaller/config.py b/mytoninstaller/config.py index 83039853..bf66a127 100644 --- a/mytoninstaller/config.py +++ b/mytoninstaller/config.py @@ -43,15 +43,6 @@ def backup_config(local, config_path): #end define -def BackupVconfig(local): - local.add_log("Backup validator config file 'config.json' to 'config.json.backup'", "debug") - vconfig_path = local.buffer.vconfig_path - backupPath = vconfig_path + ".backup" - args = ["cp", vconfig_path, backupPath] - subprocess.run(args) -#end define - - def BackupMconfig(local): local.add_log("Backup mytoncore config file 'mytoncore.db' to 'mytoncore.db.backup'", "debug") mconfig_path = local.buffer.mconfig_path diff --git a/mytoninstaller/mytoninstaller.py b/mytoninstaller/mytoninstaller.py index 160b9210..c820efc9 100644 --- a/mytoninstaller/mytoninstaller.py +++ b/mytoninstaller/mytoninstaller.py @@ -24,17 +24,16 @@ EnableLiteServer, EnableDhtServer, EnableJsonRpc, - EnableTonHttpApi, + enable_ton_http_api, DangerousRecoveryValidatorConfigFile, CreateSymlinks, enable_ls_proxy, enable_ton_storage, enable_ton_storage_provider, - EnableMode + EnableMode, ConfigureFromBackup, ConfigureOnlyNode, SetInitialSync ) from mytoninstaller.config import ( CreateLocalConfig, - BackupVconfig, BackupMconfig, ) @@ -133,16 +132,18 @@ def Status(local, args): node_args = get_node_args() color_print("{cyan}===[ Node arguments ]==={endc}") for key, value in node_args.items(): - print(f"{key}: {value}") + for v in value: + print(f"{key}: {v}") #end define def set_node_argument(local, args): if len(args) < 1: - color_print("{red}Bad args. Usage:{endc} set_node_argument [arg-value] [-d (to delete)]") + color_print("{red}Bad args. Usage:{endc} set_node_argument [arg-value] [-d (to delete)].\n" + "Examples: 'set_node_argument --archive-ttl 86400' or 'set_node_argument --archive-ttl -d' or 'set_node_argument -M' or 'set_node_argument --add-shard 0:2000000000000000 0:a000000000000000'") return arg_name = args[0] - args = [arg_name, args[1] if len(args) > 1 else ""] + args = [arg_name, " ".join(args[1:])] script_path = pkg_resources.resource_filename('mytoninstaller.scripts', 'set_node_argument.py') run_as_root(['python3', script_path] + args) color_print("set_node_argument - {green}OK{endc}") @@ -194,8 +195,8 @@ def PrintLiteServerConfig(local, args): def CreateLocalConfigFile(local, args): initBlock = GetInitBlock() initBlock_b64 = dict2b64(initBlock) - user = local.buffer["user"] - args = ["python3", "-m", "mytoninstaller", "-u", local.buffer.user, "-e", "clc", "-i", initBlock_b64] + user = local.buffer.user or os.environ.get("USER", "root") + args = ["python3", "-m", "mytoninstaller", "-u", user, "-e", "clc", "-i", initBlock_b64] run_as_root(args) #end define @@ -223,7 +224,7 @@ def Event(local, name): if name == "enableJR": EnableJsonRpc(local) if name == "enableTHA": - EnableTonHttpApi(local) + enable_ton_http_api(local) if name == "enableLSP": enable_ls_proxy(local) if name == "enableTSP": @@ -259,7 +260,7 @@ def General(local, console): Refresh(local) if "-c" in sys.argv: cx = sys.argv.index("-c") - args = sys.argv[cx+1:] + args = sys.argv[cx+1].split() Command(local, args, console) if "-e" in sys.argv: ex = sys.argv.index("-e") @@ -277,16 +278,29 @@ def General(local, console): mx = sys.argv.index("-m") mode = sys.argv[mx+1] local.buffer.mode = mode + if "--only-mtc" in sys.argv: + ox = sys.argv.index("--only-mtc") + local.buffer.only_mtc = str2bool(sys.argv[ox+1]) + if "--only-node" in sys.argv: + ox = sys.argv.index("--only-node") + local.buffer.only_node = str2bool(sys.argv[ox+1]) + if "--backup" in sys.argv: + bx = sys.argv.index("--backup") + backup = sys.argv[bx+1] + if backup != "none": + local.buffer.backup = backup #end if FirstMytoncoreSettings(local) FirstNodeSettings(local) EnableValidatorConsole(local) EnableLiteServer(local) - BackupVconfig(local) BackupMconfig(local) CreateSymlinks(local) EnableMode(local) + ConfigureFromBackup(local) + ConfigureOnlyNode(local) + SetInitialSync(local) #end define diff --git a/mytoninstaller/node_args.py b/mytoninstaller/node_args.py index ec99eea3..cd8c6e47 100644 --- a/mytoninstaller/node_args.py +++ b/mytoninstaller/node_args.py @@ -2,34 +2,31 @@ def get_validator_service(): path = '/etc/systemd/system/validator.service' - with open(path, 'r') as f: - return f.read() + with open(path, 'r') as file: + return file.read() #end define def get_node_start_command(): service = get_validator_service() for line in service.split('\n'): - if 'ExecStart' in line: + if line.startswith('ExecStart'): return line.split('=')[1].strip() #end define +def get_node_args(start_command: str = None): + if start_command is None: + start_command = get_node_start_command() + #end if -def get_node_args(command: str = None): - if command is None: - command = get_node_start_command() - result = {} - key = '' - for c in command.split(' ')[1:]: - if c.startswith('--') or c.startswith('-'): - if key: - result[key] = '' - key = c - elif key: - result[key] = c - key = '' - if key: - result[key] = '' + result = dict() # {key: [value1, value2]} + node_args = start_command.split(' ')[1:] + key = None + for item in node_args: + if item.startswith('-'): + key = item + result[key] = list() + else: + result[key].append(item) return result #end define - diff --git a/mytoninstaller/scripts/jsonrpcinstaller.sh b/mytoninstaller/scripts/jsonrpcinstaller.sh index e6338480..926d61e1 100755 --- a/mytoninstaller/scripts/jsonrpcinstaller.sh +++ b/mytoninstaller/scripts/jsonrpcinstaller.sh @@ -28,7 +28,7 @@ ENDC='\033[0m' # Установка компонентов python3 echo -e "${COLOR}[1/4]${ENDC} Installing required packages" -pip3 install Werkzeug json-rpc cloudscraper pyotp +pip3 install Werkzeug json-rpc cloudscraper pyotp jsonpickle # todo: set versions # Клонирование репозиториев с github.com echo -e "${COLOR}[2/4]${ENDC} Cloning github repository" diff --git a/mytoninstaller/scripts/set_node_argument.py b/mytoninstaller/scripts/set_node_argument.py index 19fe67a5..bad88016 100644 --- a/mytoninstaller/scripts/set_node_argument.py +++ b/mytoninstaller/scripts/set_node_argument.py @@ -11,20 +11,35 @@ def set_node_arg(arg_name: str, arg_value: str = ''): """ assert arg_name.startswith('-'), 'arg_name must start with "-" or "--"' service = get_validator_service() - command = get_node_start_command() - if command.split(' ')[0] != '/usr/bin/ton/validator-engine/validator-engine': - raise Exception('Invalid node start command in service file') - if command is None: + start_command = get_node_start_command() + if start_command is None: raise Exception('Cannot find node start command in service file') - args = get_node_args(command) + first_arg = start_command.split(' ')[0] + if first_arg != '/usr/bin/ton/validator-engine/validator-engine': + raise Exception('Invalid node start command in service file') + #end if + + node_args = get_node_args(start_command) if arg_value == '-d': - args.pop(arg_name, None) + node_args.pop(arg_name, None) else: - args[arg_name] = arg_value - new_command = command.split(' ')[0] + ' ' + ' '.join([f'{k} {v}' for k, v in args.items()]) - new_service = service.replace(command, new_command) - with open('/etc/systemd/system/validator.service', 'w') as f: - f.write(new_service) + if ' ' in arg_value: + node_args[arg_name] = arg_value.split() + else: + node_args[arg_name] = [arg_value] + #end if + + buffer = list() + buffer.append(first_arg) + for key, value_list in node_args.items(): + if len(value_list) == 0: + buffer.append(f"{key}") + for value in value_list: + buffer.append(f"{key} {value}") + new_start_command = ' '.join(buffer) + new_service = service.replace(start_command, new_start_command) + with open('/etc/systemd/system/validator.service', 'w') as file: + file.write(new_service) restart_node() #end define diff --git a/scripts/ton_http_api_installer.sh b/mytoninstaller/scripts/ton_http_api_installer.sh similarity index 60% rename from scripts/ton_http_api_installer.sh rename to mytoninstaller/scripts/ton_http_api_installer.sh index 9327f870..a5eab69c 100644 --- a/scripts/ton_http_api_installer.sh +++ b/mytoninstaller/scripts/ton_http_api_installer.sh @@ -1,7 +1,7 @@ #!/bin/bash set -e -# Проверить sudo +# check sudo if [ "$(id -u)" != "0" ]; then echo "Please run script as root" exit 1 @@ -11,30 +11,36 @@ fi COLOR='\033[92m' ENDC='\033[0m' -# Установка компонентов python3 -pip3 install virtualenv +# install python3 packages +pip3 install virtualenv==20.27.1 -# Подготовить папку с виртуальным окружением +# prepare the virtual environment echo -e "${COLOR}[1/4]${ENDC} Preparing the virtual environment" venv_path="/opt/virtualenv/ton_http_api" virtualenv ${venv_path} -# Установка компонентов python3 +# install python3 packages echo -e "${COLOR}[2/4]${ENDC} Installing required packages" user=$(logname) venv_pip3="${venv_path}/bin/pip3" ${venv_pip3} install ton-http-api chown -R ${user}:${user} ${venv_path} -# Прописать автозагрузку +# add to startup echo -e "${COLOR}[3/4]${ENDC} Add to startup" venv_ton_http_api="${venv_path}/bin/ton-http-api" tonlib_path="/usr/bin/ton/tonlib/libtonlibjson.so" -ls_config="/usr/bin/ton/localhost.config.json" -cmd="from sys import path; path.append('/usr/src/mytonctrl/'); from mypylib.mypylib import add2systemd; add2systemd(name='ton_http_api', user='${user}', start='${venv_ton_http_api} --host 127.0.0.1 --port 8801 --liteserver-config ${ls_config} --cdll-path ${tonlib_path} --tonlib-keystore /tmp/tonlib_keystore/')" +ls_config="/usr/bin/ton/local.config.json" +cmd="from sys import path; path.append('/usr/src/mytonctrl/'); from mypylib.mypylib import add2systemd; add2systemd(name='ton_http_api', user='${user}', start='${venv_ton_http_api} --logs-level=INFO --host 127.0.0.1 --port 8801 --liteserver-config ${ls_config} --cdll-path ${tonlib_path} --tonlib-keystore /tmp/tonlib_keystore/')" python3 -c "${cmd}" +systemctl daemon-reload systemctl restart ton_http_api -# Конец +# check connection +echo -e "Requesting masterchain info from local ton http api" +sleep 5 +curl http://127.0.0.1:8801/getMasterchainInfo + +# end echo -e "${COLOR}[4/4]${ENDC} ton_http_api service installation complete" exit 0 diff --git a/mytoninstaller/scripts/tonhttpapiinstaller.sh b/mytoninstaller/scripts/tonhttpapiinstaller.sh deleted file mode 100755 index c22b98e0..00000000 --- a/mytoninstaller/scripts/tonhttpapiinstaller.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/bash -set -e - -# Проверить sudo -if [ "$(id -u)" != "0" ]; then - echo "Please run script as root" - exit 1 -fi - -# Get arguments -while getopts u: flag -do - case "${flag}" in - u) user=${OPTARG};; - esac -done - -# Цвета -COLOR='\033[92m' -ENDC='\033[0m' - -# Установка компонентов python3 -echo -e "${COLOR}[1/3]${ENDC} Installing required packages" -pip3 install -U ton-http-api - -# Установка модуля -echo -e "${COLOR}[2/3]${ENDC} Add to startup" -mkdir -p /var/ton-http-api/ton_keystore/ -chown -R $user /var/ton-http-api/ - -SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) -cmd="ton-http-api --port=8000 --logs-level=INFO --cdll-path=/usr/bin/ton/tonlib/libtonlibjson.so --liteserver-config /usr/bin/ton/local.config.json --tonlib-keystore=/var/ton-http-api/ton_keystore/ --parallel-requests-per-liteserver=1024" -${SCRIPT_DIR}/add2systemd.sh -n ton-http-api -s "${cmd}" -u ${user} -g ${user} -systemctl restart ton-http-api - -# Конец -echo -e "${COLOR}[3/3]${ENDC} TonHttpApi installation complete" -exit 0 diff --git a/mytoninstaller/settings.py b/mytoninstaller/settings.py index d04c5673..4f798120 100644 --- a/mytoninstaller/settings.py +++ b/mytoninstaller/settings.py @@ -9,12 +9,12 @@ import pkg_resources from mypylib.mypylib import ( - add2systemd, - get_dir_from_path, - run_as_root, - color_print, + add2systemd, + get_dir_from_path, + run_as_root, + color_print, ip2int, - Dict + Dict, int2ip ) from mytoninstaller.utils import StartValidator, StartMytoncore, start_service, stop_service, get_ed25519_pubkey from mytoninstaller.config import SetConfig, GetConfig, get_own_ip, backup_config @@ -22,6 +22,9 @@ def FirstNodeSettings(local): + if local.buffer.only_mtc: + return + local.add_log("start FirstNodeSettings fuction", "debug") # Создать переменные @@ -34,7 +37,11 @@ def FirstNodeSettings(local): validatorAppPath = local.buffer.validator_app_path globalConfigPath = local.buffer.global_config_path vconfig_path = local.buffer.vconfig_path - archive_ttl = 2592000 if local.buffer.mode == 'liteserver' else 86400 + + if os.getenv('ARCHIVE_TTL'): + archive_ttl = int(os.getenv('ARCHIVE_TTL')) + else: + archive_ttl = 2592000 if local.buffer.mode == 'liteserver' else 86400 # Проверить конфигурацию if os.path.isfile(vconfig_path): @@ -59,7 +66,14 @@ def FirstNodeSettings(local): # Прописать автозагрузку cpus = psutil.cpu_count() - 1 cmd = f"{validatorAppPath} --threads {cpus} --daemonize --global-config {globalConfigPath} --db {ton_db_dir} --logname {tonLogPath} --archive-ttl {archive_ttl} --verbosity 1" - add2systemd(name="validator", user=vuser, start=cmd) # post="/usr/bin/python3 /usr/src/mytonctrl/mytoncore.py -e \"validator down\"" + + if os.getenv('ADD_SHARD'): + add_shard = os.getenv('ADD_SHARD') + cmd += f' -M' + for shard in add_shard.split(): + cmd += f' --add-shard {shard}' + + add2systemd(name="validator", user=vuser, start=cmd, pre='/bin/sleep 2') # post="/usr/bin/python3 /usr/src/mytonctrl/mytoncore.py -e \"validator down\"" # Получить внешний ip адрес ip = get_own_ip() @@ -84,33 +98,52 @@ def FirstNodeSettings(local): StartValidator(local) #end define +def is_testnet(local): + testnet_zero_state_root_hash = "gj+B8wb/AmlPk1z1AhVI484rhrUpgSr2oSFIh56VoSg=" + with open(local.buffer.global_config_path) as f: + config = json.load(f) + if config['validator']['zero_state']['root_hash'] == testnet_zero_state_root_hash: + return True + return False def DownloadDump(local): - dump = local.buffer.dump - if dump == False: - return - #end if - - local.add_log("start DownloadDump fuction", "debug") - url = "https://dump.ton.org" - dumpSize = requests.get(url + "/dumps/latest.tar.size.archive.txt").text - print("dumpSize:", dumpSize) - needSpace = int(dumpSize) * 3 - diskSpace = psutil.disk_usage("/var") - if needSpace > diskSpace.free: - return - #end if - - # apt install - cmd = "apt install plzip pv curl -y" - os.system(cmd) - - # download dump - cmd = "curl -s {url}/dumps/latest.tar.lz | pv | plzip -d -n8 | tar -xC /var/ton-work/db".format(url=url) - os.system(cmd) + dump = local.buffer.dump + if dump is False: + return + #end if + + local.add_log("start DownloadDump function", "debug") + url = "https://dump.ton.org/dumps/latest" + if is_testnet(local): + url += '_testnet' + dumpSize = requests.get(url + ".tar.size.archive.txt").text + print("dumpSize:", dumpSize) + needSpace = int(dumpSize) * 3 + diskSpace = psutil.disk_usage("/var") + if needSpace > diskSpace.free: + return + #end if + + # apt install + cmd = "apt install plzip pv aria2 curl -y" + os.system(cmd) + + # download dump using aria2c to a temporary file + temp_file = "/tmp/latest.tar.lz" + cmd = f"aria2c -x 8 -s 8 -c {url}.tar.lz -d / -o {temp_file}" + os.system(cmd) + + # process the downloaded file + cmd = f"pv {temp_file} | plzip -d -n8 | tar -xC /var/ton-work/db" + os.system(cmd) + + # clean up the temporary file after processing + if os.path.exists(temp_file): + os.remove(temp_file) + local.add_log(f"Temporary file {temp_file} removed", "debug") + #end if #end define - def FirstMytoncoreSettings(local): local.add_log("start FirstMytoncoreSettings fuction", "debug") user = local.buffer.user @@ -185,6 +218,8 @@ def FirstMytoncoreSettings(local): #end define def EnableValidatorConsole(local): + if local.buffer.only_mtc: + return local.add_log("start EnableValidatorConsole function", "debug") # Create variables @@ -287,6 +322,9 @@ def EnableValidatorConsole(local): #end define def EnableLiteServer(local): + if local.buffer.only_mtc: + return + local.add_log("start EnableLiteServer function", "debug") # Create variables @@ -447,16 +485,36 @@ def EnableJsonRpc(local): color_print(text) #end define -def EnableTonHttpApi(local): - local.add_log("start EnablePytonv3 function", "debug") - user = local.buffer.user +def tha_exists(): + try: + resp = requests.get('http://127.0.0.1:8801/healthcheck', timeout=3) + except: + return False + if resp.status_code == 200 and resp.text == '"OK"': + return True + return False +#end define - ton_http_api_installer_path = pkg_resources.resource_filename('mytoninstaller.scripts', 'tonhttpapiinstaller.sh') - exit_code = run_as_root(["bash", ton_http_api_installer_path, "-u", user]) +def enable_ton_http_api(local): + try: + if not tha_exists(): + do_enable_ton_http_api(local) + except Exception as e: + local.add_log(f"Error in enable_ton_http_api: {e}", "warning") + pass +#end define + +def do_enable_ton_http_api(local): + local.add_log("start do_enable_ton_http_api function", "debug") + if not os.path.exists('/usr/bin/ton/local.config.json'): + from mytoninstaller.mytoninstaller import CreateLocalConfigFile + CreateLocalConfigFile(local, []) + ton_http_api_installer_path = pkg_resources.resource_filename('mytoninstaller.scripts', 'ton_http_api_installer.sh') + exit_code = run_as_root(["bash", ton_http_api_installer_path]) if exit_code == 0: - text = "EnableTonHttpApi - {green}OK{endc}" + text = "do_enable_ton_http_api - {green}OK{endc}" else: - text = "EnableTonHttpApi - {red}Error{endc}" + text = "do_enable_ton_http_api - {red}Error{endc}" color_print(text) #end define @@ -892,9 +950,80 @@ def CreateSymlinks(local): def EnableMode(local): args = ["python3", "-m", "mytoncore", "-e"] - if local.buffer.mode == 'liteserver': - args.append("enable_liteserver_mode") + if local.buffer.mode and local.buffer.mode != "none": + args.append("enable_mode_" + local.buffer.mode) else: return args = ["su", "-l", local.buffer.user, "-c", ' '.join(args)] subprocess.run(args) + + +def set_external_ip(local, ip): + mconfig_path = local.buffer.mconfig_path + + mconfig = GetConfig(path=mconfig_path) + + mconfig.liteClient.liteServer.ip = ip + mconfig.validatorConsole.addr = f'{ip}:{mconfig.validatorConsole.addr.split(":")[1]}' + + # write mconfig + local.add_log("write mconfig", "debug") + SetConfig(path=mconfig_path, data=mconfig) + + +def ConfigureFromBackup(local): + if not local.buffer.backup: + return + from modules.backups import BackupModule + mconfig_path = local.buffer.mconfig_path + mconfig_dir = get_dir_from_path(mconfig_path) + local.add_log("start ConfigureFromBackup function", "info") + backup_file = local.buffer.backup + + os.makedirs(local.buffer.ton_work_dir, exist_ok=True) + if not local.buffer.only_mtc: + ip = str(ip2int(get_own_ip())) + BackupModule.run_restore_backup(["-m", mconfig_dir, "-n", backup_file, "-i", ip]) + + if local.buffer.only_mtc: + BackupModule.run_restore_backup(["-m", mconfig_dir, "-n", backup_file]) + local.add_log("Installing only mtc", "info") + vconfig_path = local.buffer.vconfig_path + vconfig = GetConfig(path=vconfig_path) + try: + node_ip = int2ip(vconfig['addrs'][0]['ip']) + except: + local.add_log("Can't get ip from validator", "error") + return + set_external_ip(local, node_ip) + + +def ConfigureOnlyNode(local): + if not local.buffer.only_node: + return + from modules.backups import BackupModule + mconfig_path = local.buffer.mconfig_path + mconfig_dir = get_dir_from_path(mconfig_path) + local.add_log("start ConfigureOnlyNode function", "info") + + process = BackupModule.run_create_backup(["-m", mconfig_dir, ]) + if process.returncode != 0: + local.add_log("Backup creation failed", "error") + return + local.add_log("Backup successfully created. Use this file on the controller server with `--only-mtc` flag on installation.", "info") + + mconfig = GetConfig(path=mconfig_path) + mconfig.onlyNode = True + SetConfig(path=mconfig_path, data=mconfig) + + start_service(local, 'mytoncore') + + +def SetInitialSync(local): + mconfig_path = local.buffer.mconfig_path + + mconfig = GetConfig(path=mconfig_path) + mconfig.initialSync = True + SetConfig(path=mconfig_path, data=mconfig) + + start_service(local, 'mytoncore') diff --git a/requirements.txt b/requirements.txt index f7705c47..01f7a310 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,5 @@ -crc16 -requests -psutil -fastcrc -jsonpickle -pynacl +crc16==0.1.1 +requests==2.32.3 +psutil==6.1.0 +fastcrc==0.3.2 +pynacl==1.5.0 diff --git a/scripts/install.py b/scripts/install.py new file mode 100644 index 00000000..bfb569b4 --- /dev/null +++ b/scripts/install.py @@ -0,0 +1,101 @@ +import os +import subprocess +import inquirer + + +def run_cli(): + questions = [ + inquirer.List( + "mode", + message="Select installation mode (More on https://docs.ton.org/participate/nodes/node-types)", + choices=["validator", "liteserver"], + ), + inquirer.List( + "network", + message="Select network", + choices=["Mainnet", "Testnet", "Other"], + ), + inquirer.Text( + "config", + message="Provide network config uri", + ignore=lambda x: x["network"] != "Other", # do not ask this question if network is not 'Other' + validate=lambda _, x: x.startswith("http"), + ), + inquirer.Text( + "archive-ttl", + message="Send the number of seconds to keep the block data in the node database. Default is 2592000 (30 days)", + ignore=lambda x: x["mode"] != "liteserver", # do not ask this question if mode is not liteserver + validate=lambda _, x: not x or x.isdigit(), # must be empty string or a number + # default=2592000 + ), + inquirer.List( + "validator-mode", + message="Select mode for validator usage. You can skip and set up this later", + ignore=lambda x: x["mode"] != "validator", # do not ask this question if mode is not validator + choices=["Validator wallet", "Nominator pool", "Single pool", "Liquid Staking", "Skip"], + ), + inquirer.Confirm( + "dump", + message="Do you want to download blockchain's dump? " + "This reduces synchronization time but requires to download a large file", + ), + inquirer.Text( + "add-shard", + message="Set shards node will sync. Skip to sync all shards. " + "Format: :. Divide multiple shards with space. " + "Example: `0:2000000000000000 0:6000000000000000`", + validate=lambda _, x: not x or all([":" in i for i in x.split()]) + ) + ] + + answers = inquirer.prompt(questions) + + return answers + + +def parse_args(answers: dict): + mode = answers["mode"] + network = answers["network"].lower() + config = answers["config"] + archive_ttl = answers["archive-ttl"] + add_shard = answers["add-shard"] + validator_mode = answers["validator-mode"] + dump = answers["dump"] + + res = f' -n {network}' + + if network not in ('mainnet', 'testnet'): + res += f' -c {config}' + + if archive_ttl: + os.putenv('ARCHIVE_TTL', archive_ttl) # set env variable + if add_shard: + os.putenv('ADD_SHARD', add_shard) + + if validator_mode and validator_mode not in ('Skip', 'Validator wallet'): + if validator_mode == 'Nominator pool': + validator_mode = 'nominator-pool' + elif validator_mode == 'Single pool': + validator_mode = 'single-nominator' + elif validator_mode == 'Liquid Staking': + validator_mode = 'liquid-staking' + res += f' -m {validator_mode}' + else: + res += f' -m {mode}' + + if dump: + res += ' -d' + + return res + + +def main(): + answers = run_cli() + command = parse_args(answers) + # subprocess.run('bash scripts/install.sh ' + command, shell=True) + print('bash install.sh ' + command) + subprocess.run(['bash', 'install.sh'] + command.split()) + + +if __name__ == '__main__': + main() diff --git a/scripts/install.sh b/scripts/install.sh index cfe84c76..1b483411 100644 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -15,19 +15,27 @@ fi author="ton-blockchain" repo="mytonctrl" branch="master" -mode="validator" +network="mainnet" +ton_node_version="master" # Default version + show_help_and_exit() { - echo 'Supported argumets:' - echo ' -c PATH Provide custom config for toninstaller.sh' - echo ' -t Disable telemetry' - echo ' -i Ignore minimum reqiurements' - echo ' -d Use pre-packaged dump. Reduces duration of initial synchronization.' - echo ' -a Set MyTonCtrl git repo author' - echo ' -r Set MyTonCtrl git repo' - echo ' -b Set MyTonCtrl git repo branch' - echo ' -m MODE Install MyTonCtrl with specified mode (validator or liteserver)' - echo ' -h Show this help' + echo 'Supported arguments:' + echo ' -c PATH Provide custom config for toninstaller.sh' + echo ' -t Disable telemetry' + echo ' -i Ignore minimum requirements' + echo ' -d Use pre-packaged dump. Reduces duration of initial synchronization.' + echo ' -a Set MyTonCtrl git repo author' + echo ' -r Set MyTonCtrl git repo' + echo ' -b Set MyTonCtrl git repo branch' + echo ' -m MODE Install MyTonCtrl with specified mode (validator or liteserver)' + echo ' -n NETWORK Specify the network (mainnet or testnet)' + echo ' -v VERSION Specify the ton node version (commit, branch, or tag)' + echo ' -u USER Specify the user to be used for MyTonCtrl installation' + echo ' -p PATH Provide backup file for MyTonCtrl installation' + echo ' -o Install only MyTonCtrl. Must be used with -p' + echo ' -l Install only TON node' + echo ' -h Show this help' exit } @@ -40,26 +48,58 @@ config="https://ton-blockchain.github.io/global.config.json" telemetry=true ignore=false dump=false +only_mtc=false +only_node=false +backup=none +mode=none +cpu_required=16 +mem_required=64000000 # 64GB in KB + +while getopts ":c:tidola:r:b:m:n:v:u:p:h" flag; do + case "${flag}" in + c) config=${OPTARG};; + t) telemetry=false;; + i) ignore=true;; + d) dump=true;; + a) author=${OPTARG};; + r) repo=${OPTARG};; + b) branch=${OPTARG};; + m) mode=${OPTARG};; + n) network=${OPTARG};; + v) ton_node_version=${OPTARG};; + u) user=${OPTARG};; + o) only_mtc=true;; + l) only_node=true;; + p) backup=${OPTARG};; + h) show_help_and_exit;; + *) + echo "Flag -${flag} is not recognized. Aborting" + exit 1 ;; + esac +done +if [ "$only_mtc" = true ] && [ "$backup" = "none" ]; then + echo "Backup file must be provided if only mtc installation" + exit 1 +fi + -while getopts c:tida:r:b:m: flag -do - case "${flag}" in - c) config=${OPTARG};; - t) telemetry=false;; - i) ignore=true;; - d) dump=true;; - a) author=${OPTARG};; - r) repo=${OPTARG};; - b) branch=${OPTARG};; - m) mode=${OPTARG};; - h) show_help_and_exit;; - *) - echo "Flag -${flag} is not recognized. Aborting" - exit 1 ;; - esac -done +if [ "${mode}" = "none" ] && [ "$backup" = "none" ]; then # no mode or backup was provided + echo "Running cli installer" + wget https://raw.githubusercontent.com/${author}/${repo}/${branch}/scripts/install.py + python3 -m pip install --upgrade pip + pip3 install inquirer==3.4.0 --break-system-packages + python3 install.py + exit +fi + +# Set config based on network argument +if [ "${network}" = "testnet" ]; then + config="https://ton-blockchain.github.io/testnet-global.config.json" + cpu_required=8 + mem_required=16000000 # 16GB in KB +fi # check machine configuration echo -e "${COLOR}[1/5]${ENDC} Checking system requirements" @@ -68,9 +108,9 @@ cpus=$(lscpu | grep "CPU(s)" | head -n 1 | awk '{print $2}') memory=$(cat /proc/meminfo | grep MemTotal | awk '{print $2}') echo "This machine has ${cpus} CPUs and ${memory}KB of Memory" -if [ "$ignore" = false ] && ([ "${cpus}" -lt 16 ] || [ "${memory}" -lt 64000000 ]); then - echo "Insufficient resources. Requires a minimum of 16 processors and 64Gb RAM." - exit 1 +if [ "$ignore" = false ] && ([ "${cpus}" -lt "${cpu_required}" ] || [ "${memory}" -lt "${mem_required}" ]); then + echo "Insufficient resources. Requires a minimum of "${cpu_required}" processors and "${mem_required}" RAM." + exit 1 fi echo -e "${COLOR}[2/5]${ENDC} Checking for required TON components" @@ -79,20 +119,30 @@ BIN_DIR=/usr/bin # create dirs for OSX if [[ "$OSTYPE" =~ darwin.* ]]; then - SOURCES_DIR=/usr/local/src - BIN_DIR=/usr/local/bin - mkdir -p ${SOURCES_DIR} + SOURCES_DIR=/usr/local/src + BIN_DIR=/usr/local/bin + mkdir -p ${SOURCES_DIR} fi + +if [ ! -f ~/.config/pip/pip.conf ]; then # create pip config + mkdir -p ~/.config/pip +cat > ~/.config/pip/pip.conf < ${version_path} chown ${user}:${user} ${version_dir} ${version_path} +# create symbolic link if branch not eq mytonctrl +if [ "${repo}" != "mytonctrl" ]; then + ln -sf ${SOURCES_DIR}/${repo} ${SOURCES_DIR}/mytonctrl +fi + echo -e "${COLOR}[5/5]${ENDC} Mytonctrl installation completed" exit 0 diff --git a/scripts/ton_installer.sh b/scripts/ton_installer.sh index 500a92f3..5dc962cf 100644 --- a/scripts/ton_installer.sh +++ b/scripts/ton_installer.sh @@ -8,15 +8,20 @@ if [ "$(id -u)" != "0" ]; then exit 1 fi -# Get arguments -config=https://ton-blockchain.github.io/global.config.json -while getopts c: flag -do - case "${flag}" in - c) config=${OPTARG};; - esac +while getopts ":c:v:h" flag; do + case "${flag}" in + c) config=${OPTARG};; + v) ton_node_version=${OPTARG};; + h) show_help_and_exit;; + *) + echo "Flag -${flag} is not recognized. Aborting" + exit 1 ;; + esac done +echo "config: ${config}" +echo "checkout to ${ton_node_version}" + # Цвета COLOR='\033[95m' ENDC='\033[0m' @@ -50,7 +55,7 @@ if [ "$OSTYPE" == "linux-gnu" ]; then elif [ -f /etc/debian_version ]; then echo "Ubuntu/Debian Linux detected." apt-get update - apt-get install -y build-essential curl git cmake clang libgflags-dev zlib1g-dev libssl-dev libreadline-dev libmicrohttpd-dev pkg-config libgsl-dev python3 python3-dev python3-pip libsecp256k1-dev libsodium-dev liblz4-dev libjemalloc-dev + apt-get install -y build-essential curl git cmake clang libgflags-dev zlib1g-dev libssl-dev libreadline-dev libmicrohttpd-dev pkg-config libgsl-dev python3 python3-dev python3-pip libsecp256k1-dev libsodium-dev liblz4-dev libjemalloc-dev automake libtool # Install ninja apt-get install -y ninja-build @@ -71,7 +76,7 @@ elif [[ "$OSTYPE" =~ darwin.* ]]; then echo "Please, write down your username, because brew package manager cannot be run under root user:" read LOCAL_USERNAME - + su $LOCAL_USERNAME -c "brew update" su $LOCAL_USERNAME -c "brew install openssl cmake llvm" elif [ "$OSTYPE" == "freebsd"* ]; then @@ -87,7 +92,7 @@ else fi # Установка компонентов python3 -pip3 install psutil crc16 requests +pip3 install psutil==6.1.0 crc16==0.1.1 requests==2.32.3 # build openssl 3.0 echo -e "${COLOR}[2/6]${ENDC} Building OpenSSL 3.0" @@ -104,6 +109,20 @@ echo -e "${COLOR}[3/6]${ENDC} Preparing for compilation" cd $SOURCES_DIR rm -rf $SOURCES_DIR/ton git clone --recursive https://github.com/ton-blockchain/ton.git + +echo "checkout to ${ton_node_version}" + +if [ "${ton_node_version}" != "master" ]; then + cd $SOURCES_DIR/ton + git checkout ${ton_node_version} + cd ../ +fi + +cd $SOURCES_DIR/ton +git submodule sync --recursive +git submodule update +cd ../ + git config --global --add safe.directory $SOURCES_DIR/ton # Подготавливаем папки для компиляции diff --git a/tools/extract_backup_node_keys.sh b/tools/extract_backup_node_keys.sh new file mode 100644 index 00000000..adc4448c --- /dev/null +++ b/tools/extract_backup_node_keys.sh @@ -0,0 +1,48 @@ +name="backup.tar.gz" +dest="cleared_backup_$(hostname)_$(date +%s).tar.gz" +ton_db="" +tmp_dir="tmp/backup" +user=$(logname) + + +# Get arguments +while getopts n:d:t: flag +do + case "${flag}" in + n) name=${OPTARG};; + d) dest=${OPTARG};; + t) ton_db=${OPTARG};; + *) + echo "Flag -${flag} is not recognized. Aborting" + exit 1 ;; + esac +done + +rm -rf $tmp_dir +mkdir tmp/backup -p + +if [ ! -z "$ton_db" ]; then + mkdir ${tmp_dir}/db + cp -r "$ton_db"/db/keyring ${tmp_dir}/db + cp "$ton_db"/db/config.json ${tmp_dir}/db +else + tar -xzf $name -C $tmp_dir +fi + +rm -rf ${tmp_dir}/mytoncore +rm -rf ${tmp_dir}/keys +mv ${tmp_dir}/db/keyring ${tmp_dir}/db/old_keyring +mkdir ${tmp_dir}/db/keyring + +keys=$(python3 -c "import json;import base64;f=open('${tmp_dir}/db/config.json');config=json.load(f);f.close();keys=set();[([keys.add(base64.b64decode(key['key']).hex().upper()) for key in v['temp_keys']], [keys.add(base64.b64decode(adnl['id']).hex().upper()) for adnl in v['adnl_addrs']]) for v in config['validators']];print('\n'.join(list(keys)))") + +for key in $keys; do + mv ${tmp_dir}/db/old_keyring/${key} ${tmp_dir}/db/keyring +done + +rm -rf ${tmp_dir}/db/old_keyring + +tar -zcf $dest -C $tmp_dir . +chown $user:$user $dest + +echo -e "Node keys backup successfully created in ${dest}!" diff --git a/tools/inject_backup_node_keys.py b/tools/inject_backup_node_keys.py new file mode 100644 index 00000000..9566c788 --- /dev/null +++ b/tools/inject_backup_node_keys.py @@ -0,0 +1,49 @@ +import argparse +import base64 +import json +import subprocess +tmp_dir = "tmp/cleared_backup" + + +def b64tohex(b64str: str): + return base64.b64decode(b64str).hex().upper() + + +def run_vc(cmd: str): + args = ['/usr/bin/ton/validator-engine-console/validator-engine-console', '-k', '/var/ton-work/keys/client', '-p', '/var/ton-work/keys/server.pub', '-a', vc_address, '--cmd', cmd] + subprocess.run(args) + + +parser = argparse.ArgumentParser() +parser.add_argument('-n') +parser.add_argument('-a') + +args = parser.parse_args() +name = args.n +vc_address = args.a + +if not name or not vc_address: + print("Usage: inject_backup_node_keys.py -n -a ") + exit(1) + + +subprocess.run(f"rm -rf {tmp_dir}", shell=True) +subprocess.run(f"mkdir -p {tmp_dir}", shell=True) + +subprocess.run(f'tar -xzf {name} -C {tmp_dir}', shell=True) + +subprocess.run(f'cp -rf {tmp_dir}/db/keyring /var/ton-work/db/', shell=True) +subprocess.run(f'chown -R validator:validator /var/ton-work/db/keyring', shell=True) + +with open(f'{tmp_dir}/db/config.json', 'r') as f: + config = json.load(f) + +for v in config['validators']: + run_vc(f'addpermkey {b64tohex(v["id"])} {v["election_date"]} {v["expire_at"]}') + for tkey in v['temp_keys']: + run_vc(f'addtempkey {b64tohex(v["id"])} {b64tohex(tkey["key"])} {v["expire_at"]}') + for adnl in v['adnl_addrs']: + run_vc(f'addadnl {b64tohex(adnl["id"])} 0') + run_vc(f'addvalidatoraddr {b64tohex(v["id"])} {b64tohex(adnl["id"])} {v["expire_at"]}') + +subprocess.run(f'systemctl restart validator', shell=True)