def __init__( self, runner: scenario_runner.ScenarioRunner, config: Any, parent: Task = None, abort_on_fail: bool = True, ) -> None: super().__init__(runner, config, parent, abort_on_fail) required_keys = {"channel_info_key"} if not required_keys.issubset(config.keys()): raise ScenarioError( f'Not all required keys provided. Required: {", ".join(required_keys)}' ) self.web3 = self._runner.client.web3 self.contract_name = CONTRACT_MONITORING_SERVICE # get the MS contract address contract_data = get_contracts_deployment_info( chain_id=self._runner.chain_id, version=DEVELOPMENT_CONTRACT_VERSION ) try: contract_info = contract_data["contracts"][self.contract_name] self.contract_address = contract_info["address"] except KeyError: raise ScenarioError(f"Unknown contract name: {self.contract_name}")
def __init__(self, runner: scenario_runner.ScenarioRunner, config: Any, parent: Task = None) -> None: super().__init__(runner, config, parent) required_keys = {"channel_info_key"} if not required_keys.issubset(config.keys()): raise ScenarioError( f'Not all required keys provided. Required: {", ".join(required_keys)}' ) self.web3 = self._runner.client.web3 self.contract_name = CONTRACT_MONITORING_SERVICE # get the MS contract address contract_data = get_contracts_deployment_info( chain_id=self._runner.definition.settings.chain_id, version=RAIDEN_CONTRACT_VERSION, development_environment=self._runner.environment. development_environment, ) assert contract_data try: contract_info = contract_data["contracts"][self.contract_name] self.contract_address = contract_info["address"] except KeyError: raise ScenarioError(f"Unknown contract name: {self.contract_name}")
def check_scenario_config(self): if not self._scenario_runner.definition.nodes.reuse_accounts: raise ScenarioError("Snapshots aren't supported when 'nodes.reuse_accounts' is False.") token_config = self._scenario_runner.definition.token if (not token_config.should_reuse_token) and token_config.address is None: raise ScenarioError( "Snapshots are only supported when token reuse is enabled " "or a fixed token address is configured." )
def run_scenario(self): from scenario_player.tasks.base import get_task_class_for_type log.info("Fetching node addresses") unreachable_nodes = [node for node, addr in self.node_to_address.items() if not addr] if not self.node_to_address or unreachable_nodes: raise NodesUnreachableError(f"Raiden nodes unreachable: {','.join(unreachable_nodes)}") token_ctr = get_or_deploy_token(self.client, self.scenario) token_address = self.token_address = to_checksum_address(token_ctr.contract_address) first_node = first(self.raiden_nodes) token_settings = self.scenario.get('token') or {} token_balance_min = token_settings.get( 'balance_min', DEFAULT_TOKEN_BALANCE_MIN, ) mint_tx = [] for node, address in self.node_to_address.items(): balance = token_ctr.contract.functions.balanceOf(address).call() if balance < token_balance_min: mint_amount = token_balance_min - balance log.debug("Minting tokens for", address=address, node=node, amount=mint_amount) mint_tx.append(token_ctr.transact('mintFor', mint_amount, address)) elif balance > token_balance_min: log.warning("Node is overfunded", address=address, node=node, balance=balance) wait_for_txs(self.client, mint_tx) registered_tokens = set( self.session.get( API_URL_TOKENS.format(protocol=self.protocol, target_host=first_node), ).json(), ) if token_address not in registered_tokens: code, msg = self.register_token(token_address, first_node) if not 199 < code < 300: log.error("Couldn't register token with network", code=code, message=msg) raise TokenRegistrationError(msg) scenario_config = self.scenario.get('scenario') if not scenario_config: raise ScenarioError("Invalid scenario definition. Missing 'scenario' key.") try: (root_task_type, root_task_config), = scenario_config.items() except ValueError: # will be thrown if it's not a 1-element dict raise ScenarioError( "Invalid scenario definition. " "Exactly one root task is required below the 'scenario' key.", ) from None task_class = get_task_class_for_type(root_task_type) self.root_task = task_class(runner=self, config=root_task_config) self.root_task()
def __init__(self, runner: scenario_runner.ScenarioRunner, config: Any, parent: Task = None) -> None: super().__init__(runner, config, parent) if "source" not in config: raise ScenarioError( "Not all required keys provided. Required: source ") if not any(k in config for k in ["iou_exists", "amount"]): raise ScenarioError("Expected either iou_exists or amount.")
def _url_params(self): pfs_url = self._runner.scenario.services.get('pfs', {}).get('url') if not pfs_url: raise ScenarioError( 'PFS tasks require settings.services.pfs.url to be set.') source = self._config['source'] if isinstance(source, str) and len(source) == 42: source_address = source else: source_address = self._runner.get_node_address(source) extra_params = '' if 'target' in self._config: target = self._config['target'] if isinstance(target, str) and len(target) == 42: target_address = target else: target_address = self._runner.get_node_address(target) extra_params = f'/{target_address}' params = dict( pfs_url=pfs_url, token_network_address=self._runner.token_network_address, source_address=source_address, extra_params=extra_params, ) return params
def start(self): log.info( "Starting node", node=self._index, address=self.address, port=self.api_address.rpartition(":")[2], ) log.debug("Node start command", command=self.executor.command) self._output_files["stdout"] = self._stdout_file.open("at", 1) self._output_files["stderr"] = self._stderr_file.open("at", 1) for file in self._output_files.values(): file.write("--------- Starting ---------\n") self._output_files["stdout"].write( f"Command line: {self.executor.command}\n") begin = time.monotonic() try: ret = self.executor.start(**self._output_files) except ProcessExitedWithError as ex: raise ScenarioError( f"Failed to start Raiden node {self._index}") from ex self.state = NodeState.STARTED duration = str(timedelta(seconds=time.monotonic() - begin)) log.info("Node started", node=self._index, duration=duration) return ret
def _url_params(self): pfs_url = self._runner.scenario.services.get("pfs", {}).get("url") if not pfs_url: raise ScenarioError("PFS tasks require settings.services.pfs.url to be set.") params = dict(pfs_url=pfs_url, token_network_address=self._runner.token_network_address) return params
def start(self): log.info( 'Starting node', node=self._index, address=self.address, port=self.api_address.rpartition(':')[2], ) log.debug('Node start command', command=self.executor.command) self._output_files['stdout'] = self._stdout_file.open('at', 1) self._output_files['stderr'] = self._stderr_file.open('at', 1) for file in self._output_files.values(): file.write('--------- Starting ---------\n') self._output_files['stdout'].write( f'Command line: {self.executor.command}\n') begin = time.monotonic() try: ret = self.executor.start(**self._output_files) except ProcessExitedWithError as ex: raise ScenarioError( f'Failed to start Raiden node {self._index}') from ex self.state = NodeState.STARTED duration = str(timedelta(seconds=time.monotonic() - begin)) log.info('Node started', node=self._index, duration=duration) return ret
def __init__(self, runner: scenario_runner.ScenarioRunner, config: Any, parent: Task = None) -> None: super().__init__(runner, config, parent) if "key" not in config: raise ScenarioError('Required config "key" not found')
def __init__(self, runner: ScenarioRunner, index: int, raiden_version, options: dict): self._runner = runner self._index = index self._raiden_version = raiden_version self._options = options self._datadir = runner.data_path.joinpath(f'node_{index:03d}') self._address = None self._eth_rpc_endpoint = None self._executor = None self._api_address = None self.state: NodeState = NodeState.STOPPED self._output_files = {} if options.pop('_clean', False): shutil.rmtree(self._datadir) self._datadir.mkdir(parents=True, exist_ok=True) for option_name, option_value in options.items(): if option_name.startswith('no-'): option_name = option_name.replace('no-', '') if option_name in MANAGED_CONFIG_OPTIONS: raise ScenarioError( f'Raiden node option "{option_name}" is managed by the scenario player ' f'and cannot be changed.', ) if option_name in MANAGED_CONFIG_OPTIONS_OVERRIDABLE: log.warning( 'Overriding managed option', option_name=option_name, option_value=option_value, node=self._index, )
def _run(self, *args, **kwargs): # pylint: disable=unused-argument # get the correct contract address # this has to be done in `_run`, otherwise `_runner` is not initialized yet contract_data = get_contracts_deployment_info( chain_id=self._runner.chain_id, version=DEVELOPMENT_CONTRACT_VERSION ) if self.contract_name == CONTRACT_TOKEN_NETWORK: self.contract_address = self._runner.token_network_address else: try: contract_info = contract_data["contracts"][self.contract_name] self.contract_address = contract_info["address"] except KeyError: raise ScenarioError(f"Unknown contract name: {self.contract_name}") events = query_blockchain_events( web3=self.web3, contract_manager=self._runner.contract_manager, contract_address=self.contract_address, contract_name=self.contract_name, topics=[], from_block=BlockNumber(self._runner.token.deployment_block), to_block=BlockNumber(self.web3.eth.blockNumber), ) # Filter matching events events = [e for e in events if e["event"] == self.event_name] # Raise exception when events do not match if not self.num_events == len(events): raise ScenarioAssertionError( f"Expected number of events ({self.num_events}) did not match the number " f"of events found ({len(events)})" )
def _process_response(self, response_dict: dict): response_dict = super()._process_response(response_dict) for field in ["balance", "total_deposit", "state"]: if field not in self._config: continue if field not in response_dict: raise ScenarioError( f'Field "{field}" is missing in channel: {response_dict}') allow_error = self._config.get("allow_" + field + "_error") if allow_error: success = (int(response_dict[field]) - allow_error <= self._config[field] <= int(response_dict[field]) + allow_error) log.info("allow_error", allow_error=allow_error, error_success=success) else: success = str(response_dict[field]) == str(self._config[field]) if not success: raise ScenarioAssertionError( f'Value mismatch for "{field}". ' f'Should: "{self._config[field]}" ' f'Is: "{response_dict[field]}" ' f"Channel: {response_dict}") return response_dict
def _url_params(self): pfs_url = self._runner.scenario.services.get("pfs", {}).get("url") if not pfs_url: raise ScenarioError("PFS tasks require settings.services.pfs.url to be set.") source = self._config["source"] if isinstance(source, str) and len(source) == 42: source_address = source else: source_address = self._runner.get_node_address(source) extra_params = "" if "target" in self._config: target = self._config["target"] if isinstance(target, str) and len(target) == 42: target_address = target else: target_address = self._runner.get_node_address(target) extra_params = f"/{target_address}" params = dict( pfs_url=pfs_url, token_network_address=self._runner.token_network_address, source_address=source_address, extra_params=extra_params, ) return params
def update_options(self, new_options: Dict[str, Any]): if self.state is not NodeState.STOPPED: raise ScenarioError( "Can't update node options while node is running.") self._validate_options(new_options) self._options.update(new_options) self._executor = None
def _run(self, *args, **kwargs) -> Dict[str, Any]: # pylint: disable=unused-argument # get the correct contract address # this has to be done in `_run`, otherwise `_runner` is not initialized yet contract_data = get_contracts_deployment_info( chain_id=self._runner.definition.settings.chain_id, version=RAIDEN_CONTRACT_VERSION, development_environment=self._runner.environment. development_environment, ) if self.contract_name == CONTRACT_TOKEN_NETWORK: self.contract_address = self._runner.token_network_address else: try: assert contract_data contract_info = contract_data["contracts"][self.contract_name] self.contract_address = to_checksum_address( contract_info["address"]) except KeyError: raise ScenarioError( f"Unknown contract name: {self.contract_name}") assert self.contract_address, "Contract address not set" events = query_blockchain_events( web3=self.web3, contract_manager=self._runner.contract_manager, contract_address=to_canonical_address(self.contract_address), contract_name=self.contract_name, topics=[], from_block=BlockNumber(self._runner.block_execution_started), to_block=BlockNumber(self.web3.eth.blockNumber), ) # Filter matching events events = [e for e in events if e["event"] == self.event_name] if self.event_args: for key, value in self.event_args.items(): if "participant" in key: if isinstance(value, int) or (isinstance(value, str) and value.isnumeric()): # Replace node index with eth address self.event_args[key] = self._runner.get_node_address( int(value)) event_args_items = self.event_args.items() # Filter the events by the given event args. # `.items()` produces a set like object which supports intersection (`&`) events = [ e for e in events if e["args"] and event_args_items & e["args"].items() ] # Raise exception when events do not match if not self.num_events == len(events): raise ScenarioAssertionError( f"Expected number of events ({self.num_events}) did not match the number " f"of events found ({len(events)})") return {"events": events}
def __init__(self, runner: ScenarioRunner, config: Any, parent: Task = None, abort_on_fail=True) -> None: super().__init__(runner, config, parent, abort_on_fail) if "key" not in config: raise ScenarioError('Required config "key" not found')
def configuration(self): """Return the scenario's configuration. :raises ScenarioError: if no 'scenario' key is present in the yaml file. """ try: return self._config["scenario"] except KeyError: raise ScenarioError("Invalid scenario definition. Missing 'scenario' key.")
def _run(self, *args, **kwargs) -> Dict[str, Any]: # pylint: disable=unused-argument channel_infos = self._runner.task_storage[ STORAGE_KEY_CHANNEL_INFO].get(self._config["channel_info_key"]) if channel_infos is None: raise ScenarioError( f"No stored channel info found for key '{self._config['channel_info_key']}'." ) # calculate reward_id assert "token_network_address" in channel_infos.keys() assert "channel_identifier" in channel_infos.keys() reward_id = bytes( Web3.soliditySha3( # pylint: disable=no-value-for-parameter ["uint256", "address"], [ int(channel_infos["channel_identifier"]), channel_infos["token_network_address"], ], )) log.info("Calculated reward ID", reward_id=encode_hex(reward_id)) events = query_blockchain_events( web3=self.web3, contract_manager=self._runner.contract_manager, contract_address=to_canonical_address(self.contract_address), contract_name=self.contract_name, topics=[], from_block=BlockNumber(self._runner.block_execution_started), to_block=BlockNumber(self.web3.eth.blockNumber), ) # Filter matching events def match_event(event: Dict): if not event["event"] == MonitoringServiceEvent.REWARD_CLAIMED: return False event_reward_id = bytes(event["args"]["reward_identifier"]) return event_reward_id == reward_id events = [e for e in events if match_event(e)] log.info("Matching events", events=events) must_claim = self._config.get("must_claim", True) found_events = len(events) > 0 # Raise exception when no event was found if must_claim and not found_events: raise ScenarioAssertionError( "No RewardClaimed event found for this channel.") elif not must_claim and found_events: raise ScenarioAssertionError( "Unexpected RewardClaimed event found for this channel.") return {"events": events}
def get_or_deploy_token(runner: 'ScenarioRunner') -> ContractProxy: """ Deploy or reuse """ contract_manager = ContractManager(CONTRACTS_PRECOMPILED_PATH) token_contract = contract_manager.get_contract(CONTRACT_CUSTOM_TOKEN) token_config = runner.scenario.get('token', {}) if not token_config: token_config = {} address = token_config.get('address') reuse = token_config.get('reuse', False) token_address_file = runner.data_path.joinpath('token.addr') if reuse: if address: raise ScenarioError( 'Token settings "address" and "reuse" are mutually exclusive.') if token_address_file.exists(): address = token_address_file.read_text() if address: check_address_has_code(runner.client, address, 'Token') token_ctr = runner.client.new_contract_proxy(token_contract['abi'], address) log.debug( "Reusing token", address=to_checksum_address(address), name=token_ctr.contract.functions.name().call(), symbol=token_ctr.contract.functions.symbol().call(), ) return token_ctr token_id = uuid.uuid4() now = datetime.now() name = token_config.get( 'name', f"Scenario Test Token {token_id!s} {now:%Y-%m-%dT%H:%M}") symbol = token_config.get('symbol', f"T{token_id!s:.3}") decimals = token_config.get('decimals', 0) log.debug("Deploying token", name=name, symbol=symbol, decimals=decimals) token_ctr = runner.client.deploy_solidity_contract( 'CustomToken', contract_manager.contracts, constructor_parameters=(0, decimals, name, symbol), confirmations=1, ) contract_checksum_address = to_checksum_address(token_ctr.contract_address) if reuse: token_address_file.write_text(contract_checksum_address) log.info( "Deployed token", address=contract_checksum_address, name=name, symbol=symbol, ) return token_ctr
def _process_response(self, response_dict: dict): response_dict = super()._process_response(response_dict) channel_count = len(response_dict) for field in ["balance", "total_deposit", "state"]: # The task parameter field names are the plural of the channel field names assert_field = f"{field}s" if assert_field not in self._config: continue try: channel_field_values = [ channel[field] for channel in response_dict ] except KeyError: raise ScenarioError( f'Field "{field}" is missing in at least one channel: {response_dict}' ) assert_field_value_count = len(self._config[assert_field]) if assert_field_value_count != channel_count: direction = ["many", "few"][assert_field_value_count < channel_count] raise ScenarioError( f'Assertion field "{field}" has too {direction} values. ' f"Have {channel_count} channels but {assert_field_value_count} values." ) channel_field_values_all = channel_field_values[:] for value in self._config[assert_field]: try: channel_field_values.remove(str(value)) except ValueError: channel_field_values_str = ", ".join( str(val) for val in channel_field_values_all) assert_field_values_str = ", ".join( str(val) for val in self._config[assert_field]) raise ScenarioAssertionError( f'Expected value "{value}" for field "{field}" not found in any channel. ' f"Existing values: {channel_field_values_str} " f"Expected values: {assert_field_values_str} " f"Channels: {response_dict}") from None if len(channel_field_values) != 0: raise ScenarioAssertionError( f'Value mismatch for field "{field}". ' f"Not all values consumed, remaining: {channel_field_values}" ) return response_dict
def _monitor(runner: NodeRunner): while not self._runner.root_task.done: if runner.state is NodeState.STARTED: try: runner.executor.check_subprocess() except ProcessExitedWithError as ex: raise ScenarioError( f'Raiden node {runner._index} died with non-zero exit status', ) from ex gevent.sleep(.5)
def _validate_options(self, options: Dict[str, Any]): for option_name, option_value in options.items(): if option_name.startswith("no-"): option_name = option_name.replace("no-", "") if option_name in MANAGED_CONFIG_OPTIONS: raise ScenarioError( f'Raiden node option "{option_name}" is managed by the scenario player ' f"and cannot be changed." ) if option_name in MANAGED_CONFIG_OPTIONS_OVERRIDABLE: log.warning( "Overriding managed option", option_name=option_name, option_value=option_value, node=self._index, ) if option_name not in KNOWN_OPTIONS: raise ScenarioError(f'Unknown option "{option_name}" supplied.')
def get_or_deploy_token(runner) -> Tuple[ContractProxy, int]: """ Deploy or reuse """ token_contract = runner.contract_manager.get_contract(CONTRACT_CUSTOM_TOKEN) token_config = runner.yaml.token if not token_config: token_config = {} address = token_config.get("address") block = token_config.get("block", 0) reuse = token_config.get("reuse", False) token_address_file = runner.data_path.joinpath("token.infos") if reuse: if address: raise ScenarioError('Token settings "address" and "reuse" are mutually exclusive.') if token_address_file.exists(): token_data = json.loads(token_address_file.read_text()) address = token_data["address"] block = token_data["block"] if address: check_address_has_code(runner.client, address, "Token") token_ctr = runner.client.new_contract_proxy(token_contract["abi"], address) log.debug( "Reusing token", address=to_checksum_address(address), name=token_ctr.contract.functions.name().call(), symbol=token_ctr.contract.functions.symbol().call(), ) return token_ctr, block token_id = uuid.uuid4() now = datetime.now() name = token_config.get("name", f"Scenario Test Token {token_id!s} {now:%Y-%m-%dT%H:%M}") symbol = token_config.get("symbol", f"T{token_id!s:.3}") decimals = token_config.get("decimals", 0) log.debug("Deploying token", name=name, symbol=symbol, decimals=decimals) token_ctr, receipt = runner.client.deploy_single_contract( "CustomToken", runner.contract_manager.contracts["CustomToken"], constructor_parameters=(1, decimals, name, symbol), ) contract_deployment_block = receipt["blockNumber"] contract_checksum_address = to_checksum_address(token_ctr.contract_address) if reuse: token_address_file.write_text( json.dumps({"address": contract_checksum_address, "block": contract_deployment_block}) ) log.info("Deployed token", address=contract_checksum_address, name=name, symbol=symbol) return token_ctr, contract_deployment_block
def _run(self, *args, **kwargs): command = self._runner.node_commands.get(self._command) if not command: raise ScenarioError( 'Invalid scenario definition. ' f'The {self._command}_node task requires ' f'nodes.commands.{self._command} to be set.', ) command = command.format(self._config) log.debug('Command', type_=self._command, command=command) greenlet = gevent.spawn(subprocess.run, shlex.split(command), check=True) self._handle_process(greenlet)
def _url_params(self): pfs_url = self._runner.scenario.services.get("pfs", {}).get("url") if not pfs_url: raise ScenarioError("PFS tasks require settings.services.pfs.url to be set.") source = self._config["source"] if isinstance(source, str) and len(source) == 42: source_address = source else: source_address = self._runner.get_node_address(source) params = dict(pfs_url=pfs_url, source_address=source_address) return params
def get_snapshot_info(self) -> Tuple[bool, List[Path]]: snapshot_dirs = self._get_snapshot_dirs() dirs_exist = [snapshot_dir.exists() for snapshot_dir in snapshot_dirs] if any(dirs_exist) and not all(dirs_exist): missing_nodes = ", ".join(snapshot_dir.name for snapshot_dir in snapshot_dirs if not snapshot_dir.exists()) raise ScenarioError( f"Inconsistent snapshot. " f"Snapshot dirs for nodes {missing_nodes} are missing. " f"Use --delete-snapshots to clean.") elif not all(dirs_exist): return False, snapshot_dirs return True, snapshot_dirs
def check(self): if self.state is not NodeState.STARTED: return try: try: self.executor.check_subprocess() except ProcessExitedWithError as ex: raise ScenarioError( f"Raiden node {self._index} died with non-zero exit status: {ex.exit_code}" ) from ex except BaseException: # We do this nested handling to log the proper re-raised ScenarioError # exception and message. log.exception("Node error") raise
def _validate_options(self, options: Dict[str, Any]): for option_name, option_value in options.items(): if option_name.startswith('no-'): option_name = option_name.replace('no-', '') if option_name in MANAGED_CONFIG_OPTIONS: raise ScenarioError( f'Raiden node option "{option_name}" is managed by the scenario player ' f'and cannot be changed.', ) if option_name in MANAGED_CONFIG_OPTIONS_OVERRIDABLE: log.warning( 'Overriding managed option', option_name=option_name, option_value=option_value, node=self._index, )
def mode(self): if self._scenario_version == 2: try: mode = self._config["mode"].upper() except KeyError: raise MissingNodesConfiguration( 'Version 2 scenarios require a "mode" in the "nodes" section.' ) try: return NodeMode[mode] except KeyError: known_modes = ", ".join(mode.name.lower() for mode in NodeMode) raise ScenarioError( f'Unknown node mode "{mode}". Expected one of {known_modes}' ) from None return NodeMode.EXTERNAL