diff --git a/test/integration/framework/.gitignore b/test/integration/framework/.gitignore index 5cc945694d..a88f54d394 100644 --- a/test/integration/framework/.gitignore +++ b/test/integration/framework/.gitignore @@ -1,4 +1,5 @@ /venv/ /build/ /src/siftool/__pycache__/ +/test/__pycache__/ /.lock diff --git a/test/integration/framework/src/siftool/chainbuilder.py b/test/integration/framework/src/siftool/chainbuilder.py new file mode 100644 index 0000000000..881e77c5c9 --- /dev/null +++ b/test/integration/framework/src/siftool/chainbuilder.py @@ -0,0 +1,48 @@ +import sys + +from siftool import common, command, environments, project, sifchain + + +def __brutally_terminate_processes(cmd): + project.get_project().pkill() + + +# This is used for bulding sifchain-testnet-2 +def install_testnet(cmd: command.Command, base_dir: str, chain_id: str): + prj = project.get_project(cmd) + mnemonics = prj.read_peruser_config_file("mnemonics") + faucet_mnemonic = mnemonics["sif1zk8wxg65k6702hu9lrxqqnf8vn74ykznf5e7hk"] + node0_admin_mnemonic = mnemonics["sif1kkfa9f2h933uj0u6dexcnucjen6m32vh0jlnq6"] + node1_admin_mnemonic = mnemonics["sif1q0ahne0jkuxf2lj52c35evt6kglsyqlcenwtuk"] + node2_admin_mnemonic = mnemonics["sif1m0dh2e4vyle6svxjukp7e688n340uzqprf4k77"] + node3_admin_mnemonic = mnemonics["sif1q3s9pxvxze0vwkwa34x3jukgumnaefnq24zs36"] + external_host = "147.135.105.196" + extra_denoms = {"testtoken-{}".format(i): 10**30 for i in range(0)} # Caner: we don't want any dummy tokens on testnet + env = environments.SifnodedEnvironment(cmd, chain_id=chain_id, sifnoded_home_root=base_dir) + env.add_validator(moniker="node-0", admin_mnemonic=node0_admin_mnemonic, external_host=external_host, pruning="default") + env.add_validator(moniker="node-1", admin_mnemonic=node1_admin_mnemonic, external_host=external_host, pruning="nothing") + env.add_validator(moniker="node-2", admin_mnemonic=node2_admin_mnemonic, external_host=external_host, pruning="everything") + env.add_validator(moniker="node-3", admin_mnemonic=node3_admin_mnemonic, external_host=external_host, pruning="everything") + env.init(faucet_balance={sifchain.ROWAN: 10**30, sifchain.STAKE: 10**30} | extra_denoms, faucet_mnemonic=faucet_mnemonic) + env.start() + sifnoded = env._client_for() + + # Initial configuration of token registry. The method `token_registry_register_batch` already checks the result. + # Compared to https://www.notion.so/sifchain/TestNet-2-7b3df383906c40cd8d4ded7bb5532252?pvs=4#dc261e1451df45e3b06cb0f99c9c692f + # our defaults are display_name = external_symbol = "ROWAN". + sifnoded.token_registry_register_batch(env.clp_admin, + tuple(sifnoded.create_tokenregistry_entry(denom, denom, 18) for denom in [sifchain.ROWAN, sifchain.STAKE])) + + sifnoded.wait_for_block(sifnoded.get_current_block() + 10) + __brutally_terminate_processes(cmd) + + +def main(*argv): + cmd = command.Command() + base_dir = argv[0] + chain_id = argv[1] + install_testnet(cmd, base_dir, chain_id) + + +if __name__ == "__main__": + main(*sys.argv[1:]) diff --git a/test/integration/framework/src/siftool/cosmos.py b/test/integration/framework/src/siftool/cosmos.py index dd853f093d..036e147822 100644 --- a/test/integration/framework/src/siftool/cosmos.py +++ b/test/integration/framework/src/siftool/cosmos.py @@ -8,6 +8,7 @@ Balance = Mapping[str, int] CompatBalance = Union[LegacyBalance, Balance] Address = str +AccountName = str Bank = Mapping[Address, Balance] BechAddress = str KeyName = str # Name of key in the keyring @@ -132,8 +133,18 @@ def transfer(cmd, channel, address, amount, from_addr, chain_id, node, gas_price args = [akash_binary, "tx", "ibc-transfer", "transfer", "transfer", channel, address, amount, "--from", from_addr, "--keyring-backend", keyring_backend, "--chain-id", chain_id, "--node", node, "-y", "--gas-prices", gas_prices, - "--gas", gas, "--packet-timeout-timestam[", str(packet_timeout_timestamp)] + "--gas", gas, "--packet-timeout-timestamp", str(packet_timeout_timestamp)] res = cmd.execst(args) return res # + + +# Example: "channel-141", "uosmo" -> "ibc/14F9BC3E44B8A9C1BE1FB08980FAB87034C9905EF17CF2F5008FC085218811CC" +# The channel_id is for the side that is receiving the denom (not the counterparty). +# See https://tutorials.cosmos.network/tutorials/6-ibc-dev/ +def derive_ibc_denom_hash(channel: str, denom: str) -> str: + port = "transfer" # Seems to be a constant for IBC token transfers + s = "{}/{}/{}".format(port, channel, denom) + import hashlib + return "ibc/{}".format(hashlib.sha256(s.encode("UTF-8")).digest().hex().upper()) diff --git a/test/integration/framework/src/siftool/environments.py b/test/integration/framework/src/siftool/environments.py index 8abdbc3bd6..df33ca9d4e 100644 --- a/test/integration/framework/src/siftool/environments.py +++ b/test/integration/framework/src/siftool/environments.py @@ -1,3 +1,4 @@ +import json from typing import Tuple from siftool.common import * from siftool.sifchain import ROWAN, ROWAN_DECIMALS, STAKE @@ -5,8 +6,9 @@ # Environment for load test test_many_pools_and_liquidity_providers and for testing min commission/max voting power -# Just sifnode, no ethereum -# Multi-node support +# Supports multiple sifnoded validators, no Ethereum support yet +# +# class SifnodedEnvironment: def __init__(self, cmd: command.Command, chain_id: Optional[str] = None, sifnoded_home_root: Optional[str] = None): self.cmd = cmd @@ -15,22 +17,29 @@ def __init__(self, cmd: command.Command, chain_id: Optional[str] = None, sifnode self.chain_id = chain_id or "localnet" self.staking_denom = ROWAN self.default_binary = "sifnoded" + self.port_offset = 0 self.node_info: List[JsonDict] = [] self.clp_admin: Optional[cosmos.Address] = None + self.oracle_admin: Optional[cosmos.Address] = None + self.whitelister_admin: Optional[cosmos.Address] = None self.faucet: Optional[cosmos.Address] = None self.running_processes = [] self.open_log_files = [] self._state = 0 + self.sifnoded_connect_host = LOCALHOST # The hostname for building sifnoded '--node' argument self.sifnoded = sifchain.Sifnoded(self.cmd, home=self.keyring_dir, chain_id=self.chain_id) - def add_validator(self, /, binary: Optional[str] = None, admin_name: Optional[str] = None, + def add_validator(self, /, binary: Optional[str] = None, admin_name: Optional[str] = None, is_validator: bool = True, admin_mnemonic: Optional[Sequence[str]] = None, moniker: Optional[str] = None, home: Optional[str] = None, - staking_amount: Optional[int] = None, initial_balance: Optional[cosmos.Balance] = None, - commission_rate: Optional[float] = None, commission_max_rate: Optional[float] = None, - commission_max_change_rate: Optional[float] = None, min_self_delegation: Optional[int] = None, - ports: Mapping[str, int] = None, log_level: Optional[str] = None, log_file: Optional[str] = None + external_host: Optional[str] = None, external_port: Optional[int] = None, staking_amount: Optional[int] = None, + initial_balance: Optional[cosmos.Balance] = None, commission_rate: Optional[float] = None, + commission_max_rate: Optional[float] = None, commission_max_change_rate: Optional[float] = None, + min_self_delegation: Optional[int] = None, ports: Mapping[str, int] = None, pruning: Optional[str] = None, + pruning_keep_recent: Optional[int] = None, pruning_keep_every: Optional[int] = None, + pruning_interval: Optional[int] = None, log_level: Optional[str] = None, log_file: Optional[str] = None ): next_id = len(self.node_info) + assert is_validator or (next_id > 0), "First node must be a validator" binary = binary if binary is not None else self.default_binary moniker = moniker if moniker is not None else "sifnoded-{}".format(next_id) @@ -45,12 +54,16 @@ def add_validator(self, /, binary: Optional[str] = None, admin_name: Optional[s ports = ports if ports else self.ports_for_node(next_id) log_level = log_level if log_level is not None else "debug" log_file = log_file if log_file is not None else os.path.join(home, "sifnoded.log") + external_host = external_host if external_host is not None else LOCALHOST + external_port = external_port if external_port is not None else ports["rpc"] node_info = { "binary": binary, "moniker": moniker, "home": home, - "host": LOCALHOST, + "external_host": external_host, + "external_port": external_port, + "is_validator": is_validator, "admin_name": admin_name, "staking_amount": staking_amount, "initial_balance": initial_balance, @@ -64,12 +77,20 @@ def add_validator(self, /, binary: Optional[str] = None, admin_name: Optional[s } if admin_mnemonic is not None: node_info["admin_mnemonic"] = admin_mnemonic + if pruning is not None: + node_info["pruning"] = pruning + if pruning_keep_recent is not None: + node_info["pruning_keep_recent"] = pruning_keep_recent + if pruning_keep_every is not None: + node_info["pruning_keep_every"] = pruning_keep_every + if pruning_interval is not None: + node_info["pruning_interval"] = pruning_interval next_index = len(self.node_info) is_first = next_index == 0 peers = [] if is_first else [self.node_info[0]] - self._create_validator_home(node_info) + self._create_sifnoded_home(node_info) self._update_configuration_files(node_info, peers) if self._state == 1: @@ -85,30 +106,32 @@ def add_validator(self, /, binary: Optional[str] = None, admin_name: Optional[s sifnoded_i = self._sifnoded_for(node_info) sifnoded_i.save_genesis_json(sifnoded.load_genesis_json()) self._sifnoded_start(node_info) - self._broadcast_create_validator_msg(node_info) + if is_validator: + self._broadcast_create_validator_msg(node_info) self.node_info.append(node_info) def ports_for_node(self, i: int) -> JsonDict: assert i < 10, "Change port configuration for 10 or more nodes" return { - "p2p": 10276 + i, - "grpc": 10909 + i, - "grpc_web": 10919 + i, - "address": 10276 + i, - "rpc": 10286 + i, - "api": 10131 + i, - "pprof": 10606 + i, + "rpc": 10286 + self.port_offset + i, + "api": 10131 + self.port_offset + i, + "grpc": 10909 + self.port_offset + i, + "grpc_web": 10919 + self.port_offset + i, + "p2p": 10276 + self.port_offset + i, + "pprof": 10606 + self.port_offset + i, } - def init(self, faucet_balance: Optional[cosmos.Balance] = None, extra_accounts: Optional[cosmos.Bank] = None, - min_deposit: Optional[int] = None + def init(self, faucet_balance: Optional[cosmos.Balance] = None, faucet_mnemonic: Optional[Sequence[str]] = None, + extra_accounts: Optional[cosmos.Bank] = None, min_deposit: Optional[int] = None, + genesis_clp_admin: Optional[cosmos.Address] = None, genesis_oracle_admin: Optional[cosmos.Address] = None, + genesis_whitelister_admin: Optional[cosmos.Address] = None ): # We must have at least one validator defined. The fist validator will be the default (i.e. it will be a peer # for all others, it will be used as the source of genesis file, it will host the faucet account) assert self.node_info - self.faucet = self.sifnoded.create_addr("faucet") + self.faucet = self.sifnoded.create_addr("faucet", mnemonic=faucet_mnemonic) faucet_balance = faucet_balance if faucet_balance is not None else {ROWAN: 10**30, STAKE: 10**30} # Setup genesis on initial validator @@ -116,19 +139,22 @@ def init(self, faucet_balance: Optional[cosmos.Balance] = None, extra_accounts: sifnoded0 = self._sifnoded_for(node_info0) for node_info in self.node_info: - sifnoded = self._sifnoded_for(node_info) - admin_addr = node_info["admin_addr"] - admin_bech = sifnoded.get_val_address(admin_addr) - validator_balance = cosmos.balance_add({self.staking_denom: node_info["staking_amount"]}, node_info["initial_balance"]) - sifnoded0.add_genesis_account(admin_addr, validator_balance) - sifnoded0.add_genesis_validators(admin_bech) + if node_info["is_validator"]: + sifnoded = self._sifnoded_for(node_info) + admin_addr = node_info["admin_addr"] + admin_bech = sifnoded.get_val_address(admin_addr) + validator_balance = cosmos.balance_add({self.staking_denom: node_info["staking_amount"]}, node_info["initial_balance"]) + sifnoded0.add_genesis_account(admin_addr, validator_balance) + sifnoded0.add_genesis_validators(admin_bech) admin0_addr = node_info0["admin_addr"] - admin0_name = node_info0["admin_name"] - self.clp_admin = admin0_addr - sifnoded0.add_genesis_clp_admin(admin0_addr) - sifnoded0.set_genesis_oracle_admin(admin0_name) - sifnoded0.set_genesis_whitelister_admin(admin0_name) + # admin0_name = node_info0["admin_name"] + self.clp_admin = genesis_clp_admin or admin0_addr + self.oracle_admin = genesis_oracle_admin or admin0_addr + self.whitelister_admin = genesis_whitelister_admin or admin0_addr + sifnoded0.add_genesis_clp_admin(self.clp_admin) + sifnoded0.set_genesis_oracle_admin(self.oracle_admin) + sifnoded0.set_genesis_whitelister_admin(self.whitelister_admin) extra_genesis_balances = cosmos.balance_sum_by_address({self.faucet: faucet_balance}, extra_accounts if extra_accounts is not None else {}) @@ -188,12 +214,14 @@ def start(self): sifnoded0.wait_for_last_transaction_to_be_mined() for node_info in other_validators: - self._broadcast_create_validator_msg(node_info) + if node_info["is_validator"]: + self._broadcast_create_validator_msg(node_info) self.sifnoded = sifchain.Sifnoded(self.cmd, home=self.keyring_dir, chain_id=self.chain_id, - node=sifchain.format_node_url(self.node_info[0]["host"], self.node_info[0]["ports"]["rpc"]), - binary = self.node_info[0]["binary"]) + node=sifchain.format_node_url(self.node_info[0]["external_host"], self.node_info[0]["external_port"]), + binary=self.node_info[0]["binary"]) + self.cmd.write_text_file(os.path.join(self.sifnoded_home_root, "summary.json"), json.dumps(self._get_summary_dict(), indent=4)) self._state = 2 def fund(self, address: cosmos.Address, amounts: cosmos.Balance): @@ -241,7 +269,7 @@ def upgrade(self, new_version: str, new_binary: str, upgrade_height: int, deposi assert sifnoded.version() == new_version self.sifnoded = sifchain.Sifnoded(self.cmd, home=self.keyring_dir, chain_id=self.chain_id, - node=sifchain.format_node_url(self.node_info[0]["host"], self.node_info[0]["ports"]["rpc"]), + node=sifchain.format_node_url(self.node_info[0]["external_host"], self.node_info[0]["external_port"]), binary = self.node_info[0]["binary"]) # Adjust configuration files for i != 0node. @@ -254,10 +282,21 @@ def _update_configuration_files(self, node_info, peers_node_info): app_toml = sifnoded.load_app_toml() config_toml = sifnoded.load_config_toml() app_toml["minimum-gas-prices"] = sif_format_amount(0.5, ROWAN) + if node_info.get("pruning", None) is not None: + app_toml["pruning"] = node_info["pruning"] + if node_info.get("pruning_keep_recent", None) is not None: + app_toml["pruning-keep-recent"] = str(node_info["pruning_keep_recent"]) + if node_info.get("pruning_keep_every", None) is not None: + app_toml["pruning-keep-every"] = str(node_info["pruning_keep_every"]) + if node_info.get("pruning_interval", None) is not None: + app_toml["pruning-interval"] = str(node_info["pruning_interval"]) app_toml['api']['enable'] = True - app_toml["api"]["address"] = sifchain.format_node_url(ANY_ADDR, node_info["ports"]["api"]) - config_toml["log_level"] = node_info["log_level"] # TODO Probably redundant - config_toml['p2p']["external_address"] = "{}:{}".format(node_info["host"], node_info["ports"]["p2p"]) + app_toml["api"]["address"] = sifchain.format_node_url(ANY_ADDR, node_info["ports"]["api"]) # default "tcp://0.0.0.0:1317" + app_toml["grpc"]["address"] = "{}:{}".format(ANY_ADDR, node_info["ports"]["grpc"]) # default "0.0.0.0:9090" + app_toml["grpc-web"]["address"] = "{}:{}".format(ANY_ADDR, node_info["ports"]["grpc_web"]) # default "0.0.0.0:9091" + config_toml["rpc"]["laddr"] = "tcp://{}:{}".format(ANY_ADDR, node_info["ports"]["rpc"]) # default "tcp://127.0.0.1:26657" + config_toml["p2p"]["laddr"] = "{}:{}".format(ANY_ADDR, node_info["ports"]["p2p"]) # default "tcp://0.0.0.0:26656" + config_toml['p2p']["external_address"] = "{}:{}".format(node_info["external_host"], node_info["external_port"]) # host:port that is announced to peers if peers: config_toml["p2p"]["persistent_peers"] = ",".join(peers) config_toml['p2p']['max_num_inbound_peers'] = 50 @@ -265,9 +304,20 @@ def _update_configuration_files(self, node_info, peers_node_info): config_toml['p2p']['allow_duplicate_ip'] = True config_toml["rpc"]["pprof_laddr"] = "{}:{}".format(LOCALHOST, node_info["ports"]["pprof"]) config_toml['moniker'] = node_info["moniker"] + config_toml["log_level"] = node_info["log_level"] sifnoded.save_app_toml(app_toml) sifnoded.save_config_toml(config_toml) + def _get_summary_dict(self): + return { + "chain_id": self.chain_id, + "faucet": self.faucet, + "clp_admin": self.clp_admin, + "oracle_admin": self.oracle_admin, + "whitelister_admin": self.whitelister_admin, + "validators": self.node_info, + } + # This constructs a sifnoded CLI wrapper with values for --home, --chain_id and --node taken from corresponding # node_info. Typically you want a CLI wrapper associated with a single running validator, but in some cases such as # delegation or creating a new validator you want to use validator's own --home, but --node pointing to a @@ -276,20 +326,26 @@ def _sifnoded_for(self, node_info: JsonDict, to_node_info: Optional[JsonDict] = binary = node_info["binary"] home = node_info["home"] to_node_info = to_node_info if to_node_info is not None else node_info - node = sifchain.format_node_url(to_node_info["host"], to_node_info["ports"]["rpc"]) + node = sifchain.format_node_url(self.sifnoded_connect_host, to_node_info["external_port"]) return sifchain.Sifnoded(self.cmd, binary=binary, home=home, chain_id=self.chain_id, node=node) + # Returns a Sifnoded that uses a default binary with the "keyring" profile and pointing to specific validator. + def _client_for(self, node_index: int = 0) -> sifchain.Sifnoded: + to_node_info = self.node_info[node_index] + node = sifchain.format_node_url(self.sifnoded_connect_host, to_node_info["external_port"]) + return sifchain.Sifnoded(self.cmd, home=self.keyring_dir, chain_id=self.chain_id, node=node) + def _sifnoded_start(self, node_info: JsonDict): sifnoded = self._sifnoded_for(node_info) - ports = node_info["ports"] log_file_path = node_info["log_file"] - log_level = node_info["log_level"] log_file = open(log_file_path, "w") - process = sifnoded.sifnoded_start(log_file=log_file, log_level=log_level, trace=True, - tcp_url="tcp://{}:{}".format(ANY_ADDR, ports["rpc"]), p2p_laddr="{}:{}".format(ANY_ADDR, ports["p2p"]), - grpc_address="{}:{}".format(ANY_ADDR, ports["grpc"]), - grpc_web_address="{}:{}".format(ANY_ADDR, ports["grpc_web"]), - address="tcp://{}:{}".format(ANY_ADDR, ports["address"])) + process = sifnoded.sifnoded_start(log_file=log_file, trace=True) + # , log_level=log_level, + # address="tcp://{}:{}".format(ANY_ADDR, ports["api"]), # app.toml->api->address + # grpc_address="{}:{}".format(ANY_ADDR, ports["grpc"]), # app.toml->grpc->address + # grpc_web_address="{}:{}".format(ANY_ADDR, ports["grpc_web"]), # app.toml->grpc-web->address + # tcp_url="tcp://{}:{}".format(ANY_ADDR, ports["rpc"]), # config.toml->rpc->laddr + # p2p_laddr="{}:{}".format(ANY_ADDR, ports["p2p"])) # config.toml->p2p->laddr sifnoded._wait_up() return log_file, process @@ -333,27 +389,31 @@ def _broadcast_create_validator_msg(self, node_info: JsonDict): assert float(new_validator["commission"]["commission_rates"]["max_rate"]) == commission_max_rate assert float(new_validator["commission"]["commission_rates"]["max_change_rate"]) == commission_max_change_rate - def _create_validator_home(self, node_info: JsonDict): - # Create admin account. We want this account both in validator's home as well as in self.keyring_dir: - # - it has to be in validator's home because "set-genesis-oracle-admin" requires it and there is no separate - # "--keyring-dir" CLI option. Otherwise, we would prefer all accounts to be separated from validator home. - # - because it is also in self.keyring_dir all admin names have to be unique. - admin_name = node_info["admin_name"] - admin_mnemonic = node_info.get("admin_mnemonic", None) - sifnoded = sifchain.Sifnoded(self.cmd, home=self.keyring_dir) - admin_acct, admin_mnemonic = sifnoded._keys_add(admin_name, mnemonic=admin_mnemonic) - admin_addr = admin_acct["address"] - node_info["admin_addr"] = admin_addr - - sifnoded_i = self._sifnoded_for(node_info) + def _create_sifnoded_home(self, node_info: JsonDict): + is_validator = node_info["is_validator"] moniker = node_info["moniker"] - sifnoded_i.init(moniker) - admin_account_copy = sifnoded_i.keys_add(admin_name, mnemonic=admin_mnemonic) - assert admin_account_copy["address"] == admin_addr - node_id = sifnoded_i.tendermint_show_node_id() # Taken from ${sifnoded_home}/config/node_key.json - pubkey = sifnoded_i.tendermint_show_validator() # Taken from ${sifnoded_home}/config/priv_validator_key.json - node_info["node_id"] = node_id - node_info["pubkey"] = pubkey + sifnoded_i = self._sifnoded_for(node_info) + + if is_validator: + # Create admin account. We want this account both in validator's home and in self.keyring_dir: + # - it has to be in validator's home because "set-genesis-oracle-admin" requires it and there is no separate + # "--keyring-dir" CLI option. Otherwise, we would prefer all accounts to be separated from validator home. + # - because it is also in self.keyring_dir all admin names have to be unique. + admin_name = node_info["admin_name"] + admin_mnemonic = node_info.get("admin_mnemonic", None) + sifnoded_client = sifchain.Sifnoded(self.cmd, home=self.keyring_dir) + admin_acct, admin_mnemonic = sifnoded_client._keys_add(admin_name, mnemonic=admin_mnemonic) # Creates if none given + admin_addr = admin_acct["address"] + sifnoded_i.init(moniker) + admin_account_copy = sifnoded_i.keys_add(admin_name, mnemonic=admin_mnemonic) + assert admin_account_copy["address"] == admin_addr + node_id = sifnoded_i.tendermint_show_node_id() # Taken from ${sifnoded_home}/config/node_key.json + pubkey = sifnoded_i.tendermint_show_validator() # Taken from ${sifnoded_home}/config/priv_validator_key.json + node_info["admin_addr"] = admin_addr + node_info["node_id"] = node_id + node_info["pubkey"] = pubkey + else: + sifnoded_i.init(moniker) def close(self): for p in self.running_processes: diff --git a/test/integration/framework/src/siftool/project.py b/test/integration/framework/src/siftool/project.py index d4ee2026d0..069babab1e 100644 --- a/test/integration/framework/src/siftool/project.py +++ b/test/integration/framework/src/siftool/project.py @@ -20,6 +20,10 @@ def killall(processes): p.wait() +def get_project(cmd: Command): + return Project(cmd, project_dir()) + + class Project: """Represents a checked out copy of a project in a particular directory.""" @@ -168,14 +172,14 @@ def write_vagrantenv_sh(self, state_vars, data_dir, ethereum_websocket_address, self.cmd.write_text_file(vagrantenv_path, joinlines(format_as_shell_env_vars(env))) self.cmd.write_text_file(project_dir("test/integration/vagrantenv.json"), json.dumps(env)) - def get_peruser_config_dir(self): + def get_peruser_config_dir(self) -> str: return self.cmd.get_user_home(".config", "siftool") - def get_user_env_vars(self): + def get_user_env_vars(self) -> JsonDict: env_file = os.environ["SIFTOOL_ENV_FILE"] return json.loads(self.cmd.read_text_file(env_file)) - def read_peruser_config_file(self, name): + def read_peruser_config_file(self, name) -> Optional[JsonDict]: path = os.path.join(self.get_peruser_config_dir(), name + ".json") if self.cmd.exists(path): return json.loads(self.cmd.read_text_file(path)) diff --git a/test/integration/framework/src/siftool/sifchain.py b/test/integration/framework/src/siftool/sifchain.py index 7b50df7e53..0c1cee52bc 100644 --- a/test/integration/framework/src/siftool/sifchain.py +++ b/test/integration/framework/src/siftool/sifchain.py @@ -6,7 +6,7 @@ import re import toml import web3 # TODO Remove dependency -from typing import Mapping, Any, Tuple, AnyStr +from typing import Mapping, Any, Tuple from siftool import command, cosmos, eth from siftool.common import * @@ -372,13 +372,13 @@ def add_genesis_validators_peggy(self, evm_network_descriptor: int, valoper: cos self._home_args() self.sifnoded_exec(args) - def set_genesis_oracle_admin(self, address): + def set_genesis_oracle_admin(self, address: Union[cosmos.AccountName, cosmos.Address]): self.sifnoded_exec(["set-genesis-oracle-admin", address] + self._home_args() + self._keyring_backend_args()) def set_genesis_token_registry_admin(self, address): self.sifnoded_exec(["set-genesis-token-registry-admin", address] + self._home_args()) - def set_genesis_whitelister_admin(self, address): + def set_genesis_whitelister_admin(self, address: Union[cosmos.AccountName, cosmos.Address]): self.sifnoded_exec(["set-genesis-whitelister-admin", address] + self._home_args() + self._keyring_backend_args()) def set_gen_denom_whitelist(self, denom_whitelist_file): @@ -441,7 +441,7 @@ def query_staking_validators(self) -> JsonObj: # See scripts/ibc/tokenregistration for more information and examples. # JSON file can be generated with "sifnoded q tokenregistry generate" def create_tokenregistry_entry(self, symbol: str, sifchain_symbol: str, decimals: int, - permissions: Iterable[str] = None + permissions: Iterable[str] = None, external_symbol: Optional[str] = None, display_name: Optional[str] = None ) -> TokenRegistryParams: permissions = permissions if permissions is not None else ["CLP", "IBCEXPORT", "IBCIMPORT"] upper_symbol = symbol.upper() # Like "USDT" @@ -452,11 +452,11 @@ def create_tokenregistry_entry(self, symbol: str, sifchain_symbol: str, decimals "path": "", "ibc_channel_id": "", "ibc_counterparty_channel_id": "", - "display_name": upper_symbol, + "display_name": display_name if display_name is not None else upper_symbol, "display_symbol": "", "network": "", "address": "", - "external_symbol": upper_symbol, + "external_symbol": external_symbol if external_symbol is not None else upper_symbol, "transfer_limit": "", "permissions": list(permissions), "unit_denom": "", @@ -1022,6 +1022,20 @@ def version(self) -> str: def gov_submit_software_upgrade(self, version: str, from_acct: cosmos.Address, deposit: cosmos.Balance, upgrade_height: int, upgrade_info: str, title: str, description: str, broadcast_mode: Optional[str] = None ): + # Example: + # sifnoded tx gov submit-proposal software-upgrade 1.2.0-beta \ + # --title "Sifchain 1.2.0-beta Upgrade" \ + # --description "Sifchain 1.2.0-beta Upgrade" \ + # --upgrade-height BLOCK_HEIGHT \ + # --deposit 50000000000000000000000rowan \ + # --from ACCOUNT \ + # --keyring-backend test \ + # --chain-id=sifchain-1 \ + # --node https://sifchain-rpc.polkachu.com:443 \ + # --broadcast-mode block \ + # --fees 10000000000000000000000rowan \ + # --gas 200000 \ + # --yes args = ["tx", "gov", "submit-proposal", "software-upgrade", version, "--from", from_acct, "--deposit", cosmos.balance_format(deposit), "--upgrade-height", str(upgrade_height), "--upgrade-info", upgrade_info, "--title", title, "--description", description] + self._home_args() + self._keyring_backend_args() + \ @@ -1092,7 +1106,7 @@ def _rpc_get(self, host, port, relative_url): log.debug("Result for {}: {} bytes".format(url, len(http_result_payload))) return json.loads(http_result_payload.decode("UTF-8")) - def wait_for_last_transaction_to_be_mined(self, count: int = 1, disable_log: bool = True, timeout: int = 90): + def wait_for_last_transaction_to_be_mined(self, count: int = 1, timeout: int = 90): log.debug("Waiting for last sifnode transaction to be mined...") start_time = time.time() initial_block = self.get_current_block() diff --git a/test/integration/framework/test/test_environments.py b/test/integration/framework/test/test_environments.py index 57003de2b1..c19bcddb6d 100644 --- a/test/integration/framework/test/test_environments.py +++ b/test/integration/framework/test/test_environments.py @@ -1,4 +1,3 @@ -import contextlib from siftool.common import * from siftool import command, environments, project, sifchain, cosmos @@ -13,10 +12,31 @@ def test_transfer(env): sifnoded.send_and_check(env.faucet, sifnoded.create_addr(), {sifchain.ROWAN: 10 ** sifchain.ROWAN_DECIMALS}) -def assert_validators_working(env, expected_monikers): - assert set(get_validators(env)) == expected_monikers - for i in range(len(env.node_info)): - test_transfer(env) +def assert_validators_working(env): + expected_monikers = [node_info["moniker"] for node_info in env.node_info if node_info["is_validator"]] + assert set(get_validators(env)) == set(expected_monikers) + number_of_nodes = len(env.node_info) + for i in range(number_of_nodes): + # Send transactions using "keyring" profile that has all the private keys, but is directed at a particular node. + # Cross check balaces on all nodes. + sifnoded = env._client_for(i) + from_address = env.faucet + to_address = sifnoded.create_addr() # This pollutes "client" keyring with temporary addresses, but we don't care + from_balance_before = sifnoded.get_balance(from_address) + to_balance_before = sifnoded.get_balance(to_address) + for j in range(number_of_nodes): + sifnoded_crosscheck = env._client_for(j) + assert cosmos.balance_equal(sifnoded_crosscheck.get_balance(from_address), from_balance_before) + assert cosmos.balance_equal(sifnoded_crosscheck.get_balance(to_address), to_balance_before) + sifnoded.send_and_check(from_address, to_address, {sifchain.ROWAN: 10 ** sifchain.ROWAN_DECIMALS}) + at_block = sifnoded.get_current_block() + from_balance_after = sifnoded.get_balance(from_address) + to_balance_after = sifnoded.get_balance(to_address) + for j in range(number_of_nodes): + sifnoded_crosscheck = env._client_for(j) + sifnoded_crosscheck.wait_for_block(at_block) # Make sure the change has propagated + assert cosmos.balance_equal(sifnoded_crosscheck.get_balance(from_address), from_balance_after) + assert cosmos.balance_equal(sifnoded_crosscheck.get_balance(to_address), to_balance_after) class TestSifnodedEnvironment: @@ -32,20 +52,44 @@ def teardown_method(self): prj = project.Project(self.cmd, project_dir()) prj.pkill() - def test_environment_setup_basic(self): + def test_environment_setup_basic_1_validator(self): env = environments.SifnodedEnvironment(self.cmd, sifnoded_home_root=self.sifnoded_home_root) env.add_validator() env.start() - assert_validators_working(env, set("sifnoded-{}".format(i) for i in range(1))) + assert_validators_working(env) + + def test_environment_setup_basic_4_validators(self): + env = environments.SifnodedEnvironment(self.cmd, chain_id="cownet-2", sifnoded_home_root=self.sifnoded_home_root) + assert len(env.node_info) == 0 + env.add_validator(moniker="ant") + env.add_validator(moniker="bee") + env.add_validator(moniker="cat") + env.add_validator(moniker="dog") + assert len(env.node_info) == 4 + env.start() + assert_validators_working(env) + + def test_environment_setup_mix_of_nodes_and_validators(self): + env = environments.SifnodedEnvironment(self.cmd, chain_id="cownet-2", sifnoded_home_root=self.sifnoded_home_root) + assert len(env.node_info) == 0 + env.add_validator(moniker="ant", is_validator=True) + env.add_validator(moniker="bee", is_validator=False) + env.add_validator(moniker="cat", is_validator=True) + assert len(env.node_info) == 3 + env.start() + assert_validators_working(env) def test_add_validator_before_and_after_start(self): env = environments.SifnodedEnvironment(self.cmd, sifnoded_home_root=self.sifnoded_home_root) + assert len(env.node_info) == 0 env.add_validator() env.add_validator() + assert len(env.node_info) == 2 env.init() env.start() env.add_validator() - assert_validators_working(env, set("sifnoded-{}".format(i) for i in range(3))) + assert len(env.node_info) == 3 + assert_validators_working(env) def test_environment_fails_to_start_if_commission_rate_is_over_max(self): env = environments.SifnodedEnvironment(self.cmd, sifnoded_home_root=self.sifnoded_home_root) diff --git a/test/integration/framework/test/test_environments_experimental_ibc_transfers.py b/test/integration/framework/test/test_environments_experimental_ibc_transfers.py new file mode 100644 index 0000000000..4d4dc3bf59 --- /dev/null +++ b/test/integration/framework/test/test_environments_experimental_ibc_transfers.py @@ -0,0 +1,47 @@ +from siftool.common import * +from siftool import command, environments, project + + +# The goal of this test is to setup an local environment for testing IBC transfers. +# We are trying to spin up single-node sifnoded validator + single-node axelard validator, and then setup and run +# and Osmosis relayer ("rly") to relay IBC tokens between the chains. +# In addition to sifnoded, axelard and rly must be in PATH. +# TODO Not working yet +class TestSifnodedEnvironmentWithIBCTransfers: + def setup_method(self): + self.cmd = command.Command() + self.sifnoded_home_root = self.cmd.tmpdir("siftool.tmp") + self.cmd.rmdir(self.sifnoded_home_root) + self.cmd.mkdir(self.sifnoded_home_root) + prj = project.Project(self.cmd, project_dir()) + prj.pkill() + + def teardown_method(self): + prj = project.Project(self.cmd, project_dir()) + prj.pkill() + + def test_ibc_setup_and_transfers(self): + env = environments.SifnodedEnvironment(self.cmd, sifnoded_home_root=self.sifnoded_home_root) + env.add_validator() + env.start() + + axelard = Axelard(home=os.path.join(env.sifnoded_home_root + "axelard")) + + +class Axelard: + def __init__(self, /, chain_id: Optional[str] = None, binary: Optional[str] = None, home: Optional[str] = None): + self.chain_id = chain_id or "axelar-localnet" + self.binary = binary or "axelard" + self.home = home + self.ports = None + + def start(self): + pass + # For generic Cosmos-based chain: + # - keys add + # - init + # - add-genesis-account ... + # - add-genesis-validator ... <--- no such command in axelard + # - gentx --home --keyring-backend test --chain-id + # - collect-gentxs --home + # - validate-genesis diff --git a/test/integration/framework/test/test_sifnoded.py b/test/integration/framework/test/test_sifnoded.py index d107b88e66..908168aded 100644 --- a/test/integration/framework/test/test_sifnoded.py +++ b/test/integration/framework/test/test_sifnoded.py @@ -38,7 +38,7 @@ def test_batching_and_paged_reads(self): clp_admin = validator0_admin sifnoded = sifchain.Sifnoded(self.cmd, home=tmpdir, chain_id=env.chain_id, - node=sifchain.format_node_url(env.node_info[0]["host"], env.node_info[0]["ports"]["rpc"])) + node=sifchain.format_node_url(env.node_info[0]["external_host"], env.node_info[0]["external_port"])) test_addr_actual_balance = sifnoded.get_balance(test_addr) assert test_addr_actual_balance == test_addr_balance