From e93fd69381c48b83e27bdc63a6bd80f48ad453de Mon Sep 17 00:00:00 2001 From: cordt-sei <165932662+cordt-sei@users.noreply.github.com> Date: Sat, 25 May 2024 23:54:42 -0600 Subject: [PATCH] Update run-node.py (#1617) fixes + improvements fixes: - app hash due to incorrect versioning - state import failure due to legacy db adds: - mainnet support - per-network version control - error handling and dependency checks improved slightly - db backend selectable at prompt --- scripts/run-node.py | 365 ++++++++++++++++++++++++++++---------------- 1 file changed, 232 insertions(+), 133 deletions(-) diff --git a/scripts/run-node.py b/scripts/run-node.py index b69b502325..93debef127 100644 --- a/scripts/run-node.py +++ b/scripts/run-node.py @@ -1,112 +1,168 @@ -import os +#!/usr/bin/env python3 + import subprocess -import json +import sys +import os import requests +import json import zipfile - from io import BytesIO -# Mapping of env to chain_id -ENV_TO_CHAIN_ID = { - "local": None, - "devnet": "arctic-1", - "testnet": "atlantic-2" +# req packages +dependencies = ['requests'] + +for package in dependencies: + try: + __import__(package) + except ImportError: + print(f"Installing the '{package}' package...") + subprocess.check_call([sys.executable, "-m", "pip", "install", package]) + +# advanced user configs +moniker = "pynode" # optional custom moniker for the node +trust_height_delta = 20000 # negative height offset for state sync +enable_unsafe_reset = True # wipe database and keys before setup +version_override = False # override version fetching. if true, specify version(s) below + +# chain binary version ["version_override" must be true to use] +MAINNET_VERSION = "v3.9.0" +DEVNET_VERSION = "v5.2.2" +TESTNET_VERSION = "v5.2.2" + +# map env to chain ID and optional manual version override +ENV_TO_CONFIG = { + "local": {"chain_id": None, "version": "latest"}, + "devnet": {"chain_id": "arctic-1", "version": DEVNET_VERSION}, + "testnet": {"chain_id": "atlantic-2", "version": TESTNET_VERSION}, + "mainnet": {"chain_id": "pacific-1", "version": MAINNET_VERSION} } def print_ascii_and_intro(): print(""" - ..:=++****++=:. - .:+*##############*+:. - .=*#####+:....:+#######+. - .-*#####=. .... .+###*:. ... - ..+#####=.. .=####=. .... .-*#=. - .+#####+. .=########+:...:=*####=. - =########*#######################- - .#################=:...=###########. - ... ..-*######+.. .:*########: - ..=-. -###- -####. :+######: - :#####+: .=########: .+####: - .########+:.:=#############=-######. - =################################- - .+#####*-.. ..-########+.. ..-*#=. - ..+##*-. ..... .-*###-. ...... .. - .--. .:*###*:. ... .+###*-. - .:+#######*-:..::*#####=. - .-+###############*+:. - ..-+********+-.. - + ..:=++****++=:. + .:+*##############*+:. + .=*#####+:....:+#######+. + .-*#####=. .... .+###*:. ... + ..+#####=.. .=####=. .... .-*#=. + .+#####+. .=########+:...:=*####=. + =########*#######################- + .#################=:...=###########. + ... ..-*######+.. .:*########: + ..=-. -###- -####. :+######: + :#####+: .=########: .+####: + .########+:.:=#############=-######. + =################################- + .+#####*-.. ..-########+.. ..-*#=. + ..+##*-. ..... .-*###-. ...... .. + .--. .:*###*:. ... .+###*-. + .:+#######*-:..::*#####=. + .-+###############*+:. + ..-+********+-. + Welcome to the Sei node installer! For more information please visit docs.sei.io Please make sure you have the following installed locally: -\t- golang 1.19 (as well as your GOPATH set up correctly) +\t- golang 1.21 (with PATH and GOPATH set properly) \t- make \t- gcc \t- docker -This tool will build from scratch seid and wipe away existing state. +This tool will build from scratch seid and wipe away existing state. Please backup any important existing data before proceeding. """) +# user setup prompts +def take_manual_inputs(): + env = input("Choose an environment (1: local, 2: devnet, 3: testnet, 4: mainnet): ") + while env not in ['1', '2', '3', '4']: + print("Invalid input. Please enter '1', '2', '3', or '4'.") + env = input("Choose an environment: ") -def install_latest_release(): - response = requests.get("https://api.github.com/repos/sei-protocol/sei-chain/releases/latest") - if response.status_code != 200: - raise Exception(f"Error getting latest version: {response.status_code}") - latest_version = response.json()["tag_name"] - response = requests.get(f"https://github.com/sei-protocol/sei-chain/archive/refs/tags/{latest_version}.zip") - if response.status_code != 200: - raise Exception(f"Error downloading sei binary {latest_version}") - zip_file = zipfile.ZipFile(BytesIO(response.content)) - zip_file.extractall(".") - os.chdir(zip_file.namelist()[0]) - run_command("make install") - + env = ["local", "devnet", "testnet", "mainnet"][int(env) - 1] + db_choice = input("Choose the database backend (1: legacy [default], 2: sei-db): ").strip() or "1" + if db_choice not in ["1", "2"]: + db_choice = "1" # Default to "1" if the input is invalid or empty + return env, db_choice -# Grab RPC from chain-registry +# fetch chain data def get_rpc_server(chain_id): chains_json_url = "https://raw.githubusercontent.com/sei-protocol/chain-registry/main/chains.json" response = requests.get(chains_json_url) - chains = response.json() - rpcs = [] - for chain in chains: - if chains[chain]['chainId'] == chain_id: - rpcs = chains[chain]['rpc'] - break - # check connectivity + if response.status_code != 200: + print("Failed to retrieve chain information.") + return None + + try: + chains = response.json() + except json.JSONDecodeError: + print("JSON decoding failed") + return None + + # fetch chain info by chain_id + chain_info = chains.get(chain_id) + if not chain_info: + print("Chain ID not found in the registry.") + return None + + # fetch and use first rpc that responds + rpcs = chain_info.get('rpc', []) for rpc in rpcs: + rpc_url = rpc.get('url') try: - response = requests.get(rpc['url']) - if response.status_code == 200: - return rpc['url'] - except Exception: - pass + if requests.get(rpc_url).status_code == 200: + return rpc_url + except requests.RequestException as e: + print(f"Failed to connect to RPC server {rpc_url}: {e}") + continue # try next url if current one fails return None +# install release based on version tag. +def install_release(version): + try: + zip_url = f"https://github.com/sei-protocol/sei-chain/archive/refs/tags/{version}.zip" + response = requests.get(zip_url) + response.raise_for_status() + zip_file = zipfile.ZipFile(BytesIO(response.content)) + zip_file.extractall(".") -def take_manual_inputs(): - # Manual input prompts - print( - """Please choose an environment: - 1. local - 2. devnet (arctic-1) - 3. testnet (atlantic-2)""" - ) - choice = input("Enter choice:") - while choice not in ['1', '2', '3']: - print("Invalid input. Please enter '1', '2' or '3'.") - choice = input("Enter choice:") - - env = "" - if choice == "1": - env = "local" - elif choice == "2": - env = "devnet" - elif choice == "3": - env = "testnet" - return env + os.chdir(zip_file.namelist()[0]) + subprocess.run("make install", shell=True, check=True) + print("Successfully installed version:", version) + + except requests.exceptions.HTTPError as e: + print(f"HTTP error occurred: {e}") # handle http error + sys.exit(1) + except requests.exceptions.RequestException as e: + print(f"Error downloading files: {e}") # handle other errors + sys.exit(1) + except zipfile.BadZipFile: + print("Error unzipping file. The downloaded file may be corrupt.") + sys.exit(1) + except subprocess.CalledProcessError as e: + print(f"Installation failed during 'make install': {e}") + sys.exit(1) + except Exception as e: + print(f"An unexpected error occurred: {e}") + sys.exit(1) +# fetch version from RPC unless "version_override = true" +def fetch_node_version(rpc_url): + if not version_override: + try: + response = requests.get(f"{rpc_url}/abci_info") + response.raise_for_status() + version = response.json()['response']['version'] + print(f"Fetched node version {version} from {rpc_url}") + return version + except Exception as e: + print(f"Failed to fetch node version from RPC URL {rpc_url}: {e}") + return None + else: + print("Using user-specified version override.") + return None + +# fetch state sync params def get_state_sync_params(rpc_url): - trust_height_delta = 40000 # may need to tune response = requests.get(f"{rpc_url}/status") latest_height = int(response.json()['sync_info']['latest_block_height']) sync_block_height = latest_height - trust_height_delta if latest_height > trust_height_delta else latest_height @@ -114,6 +170,7 @@ def get_state_sync_params(rpc_url): sync_block_hash = response.json()['block_id']['hash'] return sync_block_height, sync_block_hash +# fetch peers list def get_persistent_peers(rpc_url): with open(os.path.expanduser('~/.sei/config/node_key.json'), 'r') as f: self_id = json.load(f)['id'] @@ -122,65 +179,107 @@ def get_persistent_peers(rpc_url): persistent_peers = ','.join(peers) return persistent_peers -def get_genesis_file(chain_id): +# fetch and write genesis file directly from source +def write_genesis_file(chain_id): genesis_url = f"https://raw.githubusercontent.com/sei-protocol/testnet/main/{chain_id}/genesis.json" response = requests.get(genesis_url) - return response - - + if response.status_code == 200: + genesis_path = os.path.expanduser('~/.sei/config/genesis.json') + with open(genesis_path, 'wb') as file: + file.write(response.content) + print("Genesis file written successfully.") + else: + print(f"Failed to download genesis file: HTTP {response.status_code}") def run_command(command): - subprocess.run(command, shell=True, check=True) + try: + subprocess.run(command, shell=True, check=True) + except subprocess.CalledProcessError as e: + print(f"Command '{command}' failed with return code {e.returncode}") + except KeyboardInterrupt: + print("Process interrupted by user. Exiting gracefully...") +def ensure_file_path(file_path): + os.makedirs(os.path.dirname(file_path), exist_ok=True) + if not os.path.exists(file_path): + open(file_path, 'a').close() + print(f"Created missing file: {file_path}") def main(): - print_ascii_and_intro() - env = take_manual_inputs() - moniker = "demo" - print(f"Setting up a node in {env}") - chain_id = ENV_TO_CHAIN_ID[env] - # Install binary - install_latest_release() - # Short circuit and run init local script - if env == "local": - run_command("chmod +x scripts/initialize_local_chain.sh") - run_command("scripts/initialize_local_chain.sh") - - rpc_url = get_rpc_server(chain_id) - - # Remove previous sei data - run_command("rm -rf $HOME/.sei") - # Init seid - run_command(f"seid init --chain-id {chain_id} {moniker}") - sync_block_height, sync_block_hash = get_state_sync_params(rpc_url) - persistent_peers = get_persistent_peers(rpc_url) - genesis_file = get_genesis_file(chain_id) - - # Set genesis and configs - config_path = os.path.expanduser('~/.sei/config/config.toml') - - genesis_path = os.path.join(os.path.dirname(config_path), 'genesis.json') - with open(genesis_path, 'wb') as f: - f.write(genesis_file.content) - - config_path = os.path.expanduser('~/.sei/config/config.toml') - with open(config_path, 'r') as file: - config_data = file.read() - - config_data = config_data.replace('rpc-servers = ""', f'rpc-servers = "{rpc_url},{rpc_url}"') - config_data = config_data.replace('trust-height = 0', f'trust-height = {sync_block_height}') - config_data = config_data.replace('trust-hash = ""', f'trust-hash = "{sync_block_hash}"') - config_data = config_data.replace('persistent-peers = ""', f'persistent-peers = "{persistent_peers}"') - config_data = config_data.replace('enable = false', 'enable = true') - config_data = config_data.replace('db-sync-enable = true', 'db-sync-enable = false') - config_data = config_data.replace('use-p2p = false', 'use-p2p = true') - - with open(config_path, 'w') as file: - file.write(config_data) - - # Start seid - print("Starting seid...") - run_command("seid start") - - -main() \ No newline at end of file + try: + print_ascii_and_intro() + env, db_choice = take_manual_inputs() + print(f"Setting up a node in {env}") + + # fetch chain_id and version from ENV_TO_CONFIG + config = ENV_TO_CONFIG[env] + chain_id = config['chain_id'] + version = config['version'] + + # determine version by RPC or override + rpc_url = get_rpc_server(chain_id) if chain_id else "http://localhost:26657" + dynamic_version = fetch_node_version(rpc_url) if not version_override else None + version = dynamic_version or version # Use the fetched version if not overridden + + # install selected release + install_release(version) + + # unsafe-reset-all only if directory exists, and by config at top of script + if enable_unsafe_reset and os.path.exists(os.path.expanduser('~/.sei')): + try: + subprocess.run("seid tendermint unsafe-reset-all", shell=True, check=True) + except subprocess.CalledProcessError as e: + print(f"Failed to execute 'seid tendermint unsafe-reset-all': {e}") + sys.exit(1) + + # clean up previous data, init seid with given chain ID and moniker + subprocess.run(f"rm -rf $HOME/.sei && seid init {moniker} --chain-id {chain_id}", shell=True, check=True) + + # fetch state-sync params and persistent peers + sync_block_height, sync_block_hash = get_state_sync_params(rpc_url) + persistent_peers = get_persistent_peers(rpc_url) + + # fetch and write genesis + write_genesis_file(chain_id) + + # config changes + config_path = os.path.expanduser('~/.sei/config/config.toml') + app_config_path = os.path.expanduser('~/.sei/config/app.toml') + + # confirm exists before modifying config files + ensure_file_path(config_path) + ensure_file_path(app_config_path) + + # read and modify config.toml + with open(config_path, 'r') as file: + config_data = file.read() + config_data = config_data.replace('rpc-servers = ""', f'rpc-servers = "{rpc_url},{rpc_url}"') + config_data = config_data.replace('trust-height = 0', f'trust-height = {sync_block_height}') + config_data = config_data.replace('trust-hash = ""', f'trust-hash = "{sync_block_hash}"') + config_data = config_data.replace('persistent-peers = ""', f'persistent-peers = "{persistent_peers}"') + config_data = config_data.replace('enable = false', 'enable = true') + config_data = config_data.replace('db-sync-enable = true', 'db-sync-enable = false') + config_data = config_data.replace('use-p2p = false', 'use-p2p = true') + with open(config_path, 'w') as file: + file.write(config_data) + + # read modify and write app.toml if sei-db is selected + if db_choice == "2": + with open(app_config_path, 'r') as file: + app_data = file.read() + app_data = app_data.replace('sc-enable = false', 'sc-enable = true') + app_data = app_data.replace('ss-enable = false', 'ss-enable = true') + with open(app_config_path, 'w') as file: + file.write(app_data) + + # start seid + print("Starting seid...") + run_command("seid start") + except KeyboardInterrupt: + print("Main process interrupted by user. Exiting gracefully...") + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + print("Script interrupted by user. Exiting gracefully...")