예제 #1
0
class ScenarioRunner:
    def __init__(
        self,
        account: Account,
        chain_urls: Dict[str, List[str]],
        auth: str,
        data_path: Path,
        scenario_file: Path,
        task_state_callback: Optional[
            Callable[["ScenarioRunner", "Task", "TaskState"], None]
        ] = None,
    ):
        from scenario_player.node_support import RaidenReleaseKeeper, NodeController

        self.auth = auth

        self.release_keeper = RaidenReleaseKeeper(data_path.joinpath("raiden_releases"))

        self.task_count = 0
        self.running_task_count = 0
        self.task_cache = {}
        self.task_state_callback = task_state_callback
        # Storage for arbitrary data tasks might need to persist
        self.task_storage = defaultdict(dict)

        scenario_name = scenario_file.stem
        self.data_path = data_path.joinpath("scenarios", scenario_name)
        self.data_path.mkdir(exist_ok=True, parents=True)
        self.yaml = ScenarioYAML(scenario_file, self.data_path)
        log.debug("Data path", path=self.data_path)

        # Determining the run number requires :attr:`.data_path`
        self.run_number = self.determine_run_number()

        self.node_controller = NodeController(self, self.yaml.nodes)

        self.protocol = "http"

        self.gas_limit = GAS_LIMIT_FOR_TOKEN_CONTRACT_CALL * 2

        self.chain_name, chain_urls = self.select_chain(chain_urls)
        self.eth_rpc_urls = chain_urls
        self.client = JSONRPCClient(
            Web3(HTTPProvider(chain_urls[0])),
            privkey=account.privkey,
            gas_price_strategy=self.yaml.settings.gas_price_strategy,
        )
        self.chain_id = int(self.client.web3.net.version)

        self.contract_manager = ContractManager(contracts_precompiled_path())

        balance = self.client.balance(account.address)
        if balance < OWN_ACCOUNT_BALANCE_MIN:
            raise ScenarioError(
                f"Insufficient balance ({balance / 10 ** 18} Eth) "
                f'in account {to_checksum_address(account.address)} on chain "{self.chain_name}"'
            )

        self.session = Session()
        if auth:
            self.session.auth = tuple(auth.split(":"))
        self.session.mount("http", TimeOutHTTPAdapter(timeout=self.yaml.settings.timeout))
        self.session.mount("https", TimeOutHTTPAdapter(timeout=self.yaml.settings.timeout))

        self.service_session = ServiceInterface(self.yaml.spaas)
        # Request an RPC Client instance ID from the RPC service and assign it to the runner.
        assign_rpc_instance_id(self, chain_urls[0], account.privkey, self.yaml.settings.gas_price)

        self.token = Token(self, data_path)
        self.udc = None

        self.token_network_address = None

        task_config = self.yaml.scenario.root_config
        task_class = self.yaml.scenario.root_class
        self.root_task = task_class(runner=self, config=task_config)

    def determine_run_number(self) -> int:
        """Determine the current run number.

        We check for a run number file, and use any number that is logged
        there after incrementing it.

        REFAC: Replace this with a property.
        """
        run_number = 0
        run_number_file = self.data_path.joinpath("run_number.txt")
        if run_number_file.exists():
            run_number = int(run_number_file.read_text()) + 1
        run_number_file.write_text(str(run_number))
        log.info("Run number", run_number=run_number)
        return run_number

    def select_chain(self, chain_urls: Dict[str, List[str]]) -> Tuple[str, List[str]]:
        """Select a chain and return its name and RPC URL.

        If the currently loaded scenario's designated chain is set to 'any',
        we randomly select a chain from the given `chain_urls`.
        Otherwise, we will return `ScenarioRunner.scenario.chain_name` and whatever value
        may be associated with this key in `chain_urls`.

        :raises ScenarioError:
            if ScenarioRunner.scenario.chain_name is not one of `('any', 'Any', 'ANY')`
            and it is not a key in `chain_urls`.
        """
        chain_name = self.yaml.settings.chain
        if chain_name in ("any", "Any", "ANY"):
            chain_name = random.choice(list(chain_urls.keys()))

        log.info("Using chain", chain=chain_name)
        try:
            return chain_name, chain_urls[chain_name]
        except KeyError:
            raise ScenarioError(
                f'The scenario requested chain "{chain_name}" for which no RPC-URL is known.'
            )

    def wait_for_token_network_discovery(self, node):
        """Check for token network discovery with the given `node`.

        By default exit the wait if the token has not been discovered after `n` seconds,
        where `n` is the value of :attr:`.timeout`.

        :raises TokenNetworkDiscoveryTimeout:
            If we waited a set time for the token network to be discovered, but it wasn't.
        """
        log.info("Waiting till new network is found by nodes")
        node_endpoint = API_URL_TOKEN_NETWORK_ADDRESS.format(
            protocol=self.protocol, target_host=node, token_address=self.token.address
        )

        started = time.monotonic()
        elapsed = 0
        while elapsed < self.yaml.settings.timeout:
            try:
                resp = self.session.get(node_endpoint)
                resp.raise_for_status()

            except HTTPError as e:
                # We explicitly handle 404 Not Found responses only - anything else is none
                # of our business.
                if e.response.status_code != 404:
                    raise

                # Wait before continuing, no sense in spamming the node.
                gevent.sleep(1)

                # Update our elapsed time tracker.
                elapsed = time.monotonic() - started
                continue

            else:
                # The node appears to have discovered our token network.
                data = resp.json()

                if not is_checksum_address(data):
                    # Something's amiss about this response. Notify a human.
                    raise TypeError(f"Unexpected response type from API: {data!r}")

                return data

        # We could not assert that our token network was registered within an
        # acceptable time frame.
        raise TokenNetworkDiscoveryTimeout

    def run_scenario(self):
        mint_gas = GAS_LIMIT_FOR_TOKEN_CONTRACT_CALL * 2

        fund_tx, node_starter, node_addresses, node_count = self._initialize_nodes()

        ud_token_tx, udc_ctr, should_deposit_ud_token = self._initialize_udc(
            gas_limit=mint_gas, node_count=node_count
        )

        mint_tx = self._initialize_scenario_token(
            node_addresses=node_addresses,
            udc_ctr=udc_ctr,
            should_deposit_ud_token=should_deposit_ud_token,
            gas_limit=mint_gas,
        )

        wait_for_txs(self.client, fund_tx | ud_token_tx | mint_tx)

        if node_starter is not None:
            log.debug("Waiting for nodes to finish starting")
            node_starter.get(block=True)

        first_node = self.get_node_baseurl(0)

        registered_tokens = set(
            self.session.get(
                API_URL_TOKENS.format(protocol=self.protocol, target_host=first_node)
            ).json()
        )
        if self.token.checksum_address not in registered_tokens:
            for _ in range(5):
                code, msg = self.register_token(self.token.checksum_address, first_node)
                if 199 < code < 300:
                    break
                gevent.sleep(1)
            else:
                log.error("Couldn't register token with network", code=code, message=msg)
                raise TokenRegistrationError(msg)

        last_node = self.node_controller[-1].base_url
        self.token_network_address = self.wait_for_token_network_discovery(last_node)

        log.info(
            "Received token network address", token_network_address=self.token_network_address
        )

        # Start root task
        root_task_greenlet = gevent.spawn(self.root_task)
        greenlets = {root_task_greenlet}
        greenlets.add(self.node_controller.start_node_monitor())
        try:
            gevent.joinall(greenlets, raise_error=True)
        except BaseException:
            if not root_task_greenlet.dead:
                # Make sure we kill the tasks if a node dies
                root_task_greenlet.kill()
            raise

    def _initialize_scenario_token(
        self,
        node_addresses: Set[ChecksumAddress],
        udc_ctr: Optional[ContractProxy],
        should_deposit_ud_token: bool,
        gas_limit: int,
    ) -> Set[TransactionHash]:
        """Deploy new token to the blockchain, or load an existing one's data from disk."""
        self.token.init()
        mint_tx = set()
        for address in node_addresses:
            tx = self.token.mint(address)
            if tx:
                mint_tx.add(tx)

            if not should_deposit_ud_token:
                continue
            deposit_tx = self.udc.deposit(address)
            if deposit_tx:
                mint_tx.add(deposit_tx)

        return mint_tx

    def _initialize_udc(
        self, gas_limit: int, node_count: int
    ) -> Tuple[Set[TransactionHash], Optional[ContractProxy], bool]:
        our_address = to_checksum_address(self.client.address)
        udc_settings = self.yaml.settings.services.udc
        udc_enabled = udc_settings.enable

        ud_token_tx = set()

        if not udc_enabled:
            return ud_token_tx, None, False

        udc_ctr, ud_token_ctr = get_udc_and_token(self)

        ud_token_address = to_checksum_address(ud_token_ctr.contract_address)
        udc_address = to_checksum_address(udc_ctr.contract_address)

        log.info("UDC enabled", contract_address=udc_address, token_address=ud_token_address)

        self.udc = UserDepositContract(self, udc_ctr, ud_token_ctr)

        should_deposit_ud_token = udc_enabled and udc_settings.token["deposit"]
        allowance_tx = self.udc.update_allowance()
        if allowance_tx:
            ud_token_tx.add(allowance_tx)
        if should_deposit_ud_token:

            tx = self.udc.mint(our_address)
            if tx:
                ud_token_tx.add(tx)

        return ud_token_tx, udc_ctr, should_deposit_ud_token

    def _initialize_nodes(
        self
    ) -> Tuple[Set[TransactionHash], gevent.Greenlet, Set[ChecksumAddress], int]:
        fund_tx = set()

        self.node_controller.initialize_nodes()
        node_addresses = self.node_controller.addresses
        node_count = len(self.node_controller)
        balance_per_node = {address: self.client.balance(address) for address in node_addresses}
        low_balances = {
            address: balance
            for address, balance in balance_per_node.items()
            if balance < NODE_ACCOUNT_BALANCE_MIN
        }
        if low_balances:
            log.info("Funding nodes", nodes=low_balances.keys())
            fund_tx = set()
            for address, balance in low_balances.items():
                params = {
                    "client_id": self.yaml.spaas.rpc.client_id,
                    "to": address,
                    "value": NODE_ACCOUNT_BALANCE_FUND - balance,
                    "startgas": 21_000,
                }
                resp = self.service_session.post("spaas://rpc/transactions", json=params)
                tx_hash = resp.json()["tx_hash"]
                fund_tx.add(tx_hash)

        node_starter = self.node_controller.start(wait=False)

        return fund_tx, node_starter, node_addresses, node_count

    def task_state_changed(self, task: "Task", state: "TaskState"):
        if self.task_state_callback:
            self.task_state_callback(self, task, state)

    def register_token(self, token_address, node):
        # TODO: Move this to :class:`scenario_player.utils.token.Token`.
        try:
            base_url = API_URL_TOKENS.format(protocol=self.protocol, target_host=node)
            url = "{}/{}".format(base_url, token_address)
            log.info("Registering token with network", url=url)
            resp = self.session.put(url)
            code = resp.status_code
            msg = resp.text
        except RequestException as ex:
            code = -1
            msg = str(ex)
        return code, msg

    @staticmethod
    def _spawn_and_wait(objects, callback):
        tasks = {obj: gevent.spawn(callback, obj) for obj in objects}
        gevent.joinall(set(tasks.values()))
        return {obj: task.get() for obj, task in tasks.items()}

    def get_node_address(self, index):
        return self.node_controller[index].address

    def get_node_baseurl(self, index):
        return self.node_controller[index].base_url
예제 #2
0
class ScenarioRunner(object):
    def __init__(
        self,
        account: Account,
        chain_urls: Dict[str, List[str]],
        auth: str,
        data_path: Path,
        scenario_file: Path,
    ):
        from scenario_player.tasks.base import get_task_class_for_type
        from scenario_player.node_support import RaidenReleaseKeeper, NodeController

        self.task_count = 0
        self.running_task_count = 0
        self.auth = auth
        self.release_keeper = RaidenReleaseKeeper(
            data_path.joinpath('raiden_releases'))
        self.task_cache = {}
        self.task_storage = defaultdict(dict)

        self.scenario_name = os.path.basename(
            scenario_file.name).partition('.')[0]
        self.scenario = yaml.load(scenario_file)
        self.scenario_version = self.scenario.get('version', 1)
        if self.scenario_version not in SUPPORTED_SCENARIO_VERSIONS:
            raise ScenarioError(
                f'Unexpected scenario version {self.scenario_version}')

        self.data_path = data_path.joinpath('scenarios', self.scenario_name)
        self.data_path.mkdir(exist_ok=True, parents=True)
        log.debug('Data path', path=self.data_path)

        self.run_number = 0
        run_number_file = self.data_path.joinpath('run_number.txt')
        if run_number_file.exists():
            self.run_number = int(run_number_file.read_text()) + 1
        run_number_file.write_text(str(self.run_number))
        log.info('Run number', run_number=self.run_number)

        nodes = self.scenario['nodes']

        node_mode = NodeMode.EXTERNAL.name
        if self.is_v2:
            node_mode = nodes.get('mode', '').upper()
            if not node_mode:
                raise ScenarioError(
                    'Version 2 scenarios require a "mode" in the "nodes" section.'
                )
        try:
            self.node_mode = NodeMode[node_mode]
        except KeyError:
            known_modes = ', '.join(mode.name.lower() for mode in NodeMode)
            raise ScenarioError(
                f'Unknown node mode "{node_mode}". Expected one of {known_modes}',
            ) from None

        if self.is_managed:
            self.node_controller = NodeController(
                self,
                nodes.get('raiden_version', 'LATEST'),
                nodes['count'],
                nodes.get('default_options', {}),
                nodes.get('node_options', {}),
            )
        else:
            if 'range' in nodes:
                range_config = nodes['range']
                template = range_config['template']
                self.raiden_nodes = [
                    template.format(i) for i in range(range_config['first'],
                                                      range_config['last'] + 1)
                ]
            else:
                self.raiden_nodes = nodes['list']
            self.node_commands = nodes.get('commands', {})

        settings = self.scenario.get('settings')
        if settings is None:
            settings = {}
        self.timeout = settings.get('timeout', TIMEOUT)
        if self.is_managed:
            self.protocol = 'http'
            if 'protocol' in settings:
                log.warning(
                    'The "protocol" setting is not supported in "managed" node mode.'
                )
        else:
            self.protocol = settings.get('protocol', 'http')
        self.notification_email = settings.get('notify')
        self.chain_name = settings.get('chain', 'any')

        if self.chain_name == 'any':
            self.chain_name = random.choice(list(chain_urls.keys()))
        elif self.chain_name not in chain_urls:
            raise ScenarioError(
                f'The scenario requested chain "{self.chain_name}" for which no RPC-URL is known.',
            )
        log.info('Using chain', chain=self.chain_name)
        self.eth_rpc_urls = chain_urls[self.chain_name]

        self.client = JSONRPCClient(
            Web3(HTTPProvider(chain_urls[self.chain_name][0])),
            privkey=account.privkey,
            gas_price_strategy=get_gas_price_strategy(
                settings.get('gas_price', 'fast')),
        )

        self.chain_id = self.client.web3.net.version

        balance = self.client.balance(account.address)
        if balance < OWN_ACCOUNT_BALANCE_MIN:
            raise ScenarioError(
                f'Insufficient balance ({balance / 10 ** 18} Eth) '
                f'in account {to_checksum_address(account.address)} on chain "{self.chain_name}"',
            )

        self.session = Session()
        # WTF? https://github.com/requests/requests/issues/2605
        if auth:
            self.session.auth = tuple(auth.split(":"))
        self.session.mount('http', TimeOutHTTPAdapter(timeout=self.timeout))
        self.session.mount('https', TimeOutHTTPAdapter(timeout=self.timeout))

        self._node_to_address = None
        self.token_address = None

        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)

    def run_scenario(self):
        fund_tx = []
        node_starter: gevent.Greenlet = None
        if self.is_managed:
            self.node_controller.initialize_nodes()
            node_addresses = self.node_controller.addresses
            node_balances = {
                address: self.client.balance(address)
                for address in node_addresses
            }
            low_balances = {
                address: balance
                for address, balance in node_balances.items()
                if balance < NODE_ACCOUNT_BALANCE_MIN
            }
            if low_balances:
                log.info('Funding nodes', nodes=low_balances.keys())
                fund_tx = [
                    self.client.send_transaction(
                        to=address,
                        startgas=21000,
                        value=NODE_ACCOUNT_BALANCE_FUND - balance,
                    ) for address, balance in low_balances.items()
                ]
            node_starter = self.node_controller.start(wait=False)
        else:
            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)
        token_address = self.token_address = to_checksum_address(
            token_ctr.contract_address)
        first_node = self.get_node_baseurl(0)

        token_settings = self.scenario.get('token') or {}
        token_balance_min = token_settings.get(
            'balance_min',
            DEFAULT_TOKEN_BALANCE_MIN,
        )
        token_balance_fund = token_settings.get(
            'balance_fund',
            DEFAULT_TOKEN_BALANCE_FUND,
        )

        mint_tx = []
        if self.is_managed:
            addresses = self.node_controller.addresses
        else:
            addresses = self.node_to_address.values()
        for address in addresses:
            balance = token_ctr.contract.functions.balanceOf(address).call()
            if balance < token_balance_min:
                mint_amount = token_balance_fund - balance
                startgas = GAS_LIMIT_FOR_TOKEN_CONTRACT_CALL
                log.debug("Minting tokens for",
                          address=address,
                          amount=mint_amount)
                mint_tx.append(
                    token_ctr.transact('mintFor', startgas, mint_amount,
                                       address))
            elif balance > token_balance_min:
                log.warning("Node is overfunded",
                            address=address,
                            balance=balance)

        wait_for_txs(self.client, mint_tx + fund_tx)

        if node_starter is not None:
            log.debug('Waiting for nodes to finish starting')
            node_starter.get(block=True)

        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)

        # Start root task
        root_task_greenlet = gevent.spawn(self.root_task)
        greenlets = [root_task_greenlet]
        if self.is_managed:
            greenlets.append(self.node_controller.start_node_monitor())
        try:
            gevent.joinall(greenlets, raise_error=True)
        except BaseException:
            if not root_task_greenlet.dead:
                # Make sure we kill the tasks if a node dies
                root_task_greenlet.kill()
            raise

    def register_token(self, token_address, node):
        try:
            base_url = API_URL_TOKENS.format(protocol=self.protocol,
                                             target_host=node)
            url = "{}/{}".format(base_url, token_address)
            log.info("Registering token with network", url=url)
            resp = self.session.put(url)
            code = resp.status_code
            msg = resp.text
        except RequestException as ex:
            code = -1
            msg = str(ex)
        return code, msg

    def _spawn_and_wait(self, objects, callback):
        tasks = {obj: gevent.spawn(callback, obj) for obj in objects}
        gevent.joinall(tasks.values())
        return {obj: task.get() for obj, task in tasks.items()}

    @property
    def is_v2(self):
        return self.scenario_version == 2

    @property
    def is_managed(self):
        return self.node_mode is NodeMode.MANAGED

    def get_node_address(self, index):
        if self.is_managed:
            return self.node_controller[index].address
        else:
            return self.node_to_address[self.raiden_nodes[index]]

    def get_node_baseurl(self, index):
        if self.is_managed:
            return self.node_controller[index].base_url
        else:
            return self.raiden_nodes[index]

    # Legacy for 'external' nodes
    def _get_node_addresses(self, nodes):
        def cb(node):
            log.debug("Getting node address", node=node)
            url = API_URL_ADDRESS.format(protocol=self.protocol,
                                         target_host=node)
            log.debug("Requesting", url=url)
            try:
                resp = self.session.get(url)
            except RequestException:
                log.error("Error fetching node address", url=url, node=node)
                return
            try:
                return resp.json().get('our_address', '')
            except ValueError:
                log.error(
                    "Error decoding response",
                    response=resp.text,
                    code=resp.status_code,
                    url=url,
                )

        ret = self._spawn_and_wait(nodes, cb)
        return ret

    @property
    def node_to_address(self):
        if not self.raiden_nodes:
            return {}
        if self._node_to_address is None:
            self._node_to_address = {
                node: address
                for node, address in self._get_node_addresses(
                    self.raiden_nodes).items()
            }
        return self._node_to_address
예제 #3
0
class ScenarioRunner:
    # TODO: #73 Drop support for version 1 scenario files.

    def __init__(
        self,
        account: Account,
        chain_urls: Dict[str, List[str]],
        auth: str,
        data_path: Path,
        scenario_file: Path,
        task_state_callback: Optional[Callable[
            ["ScenarioRunner", Task, TaskState], None]] = None,
    ):
        from scenario_player.node_support import RaidenReleaseKeeper, NodeController

        self.task_count = 0
        self.running_task_count = 0
        self.auth = auth
        self.release_keeper = RaidenReleaseKeeper(
            data_path.joinpath("raiden_releases"))
        self.task_cache = {}
        self.task_state_callback = task_state_callback
        # Storage for arbitrary data tasks might need to persist
        self.task_storage = defaultdict(dict)

        self.scenario = Scenario(scenario_file)
        self.scenario_name = self.scenario.name

        self.data_path = data_path.joinpath("scenarios", self.scenario.name)
        self.data_path.mkdir(exist_ok=True, parents=True)
        log.debug("Data path", path=self.data_path)

        self.run_number = self.determine_run_number()

        self.node_mode = self.scenario.nodes.mode

        if self.is_managed:
            self.node_controller = NodeController(
                self,
                self.scenario.nodes.raiden_version,
                self.scenario.nodes.count,
                self.scenario.nodes.default_options,
                self.scenario.nodes.node_options,
            )
        else:
            self.raiden_nodes = self.scenario.nodes
            self.node_commands = self.scenario.nodes.commands

        self.timeout = self.scenario.timeout
        self.protocol = self.scenario.protocol

        self.notification_email = self.scenario.notification_email

        self.chain_name, chain_urls = self.select_chain(chain_urls)
        self.eth_rpc_urls = chain_urls

        self.client = JSONRPCClient(
            Web3(HTTPProvider(chain_urls[0])),
            privkey=account.privkey,
            gas_price_strategy=self.scenario.gas_price_strategy,
        )

        self.chain_id = int(self.client.web3.net.version)
        self.contract_manager = ContractManager(contracts_precompiled_path())

        balance = self.client.balance(account.address)
        if balance < OWN_ACCOUNT_BALANCE_MIN:
            raise ScenarioError(
                f"Insufficient balance ({balance / 10 ** 18} Eth) "
                f'in account {to_checksum_address(account.address)} on chain "{self.chain_name}"'
            )

        self.session = Session()
        if auth:
            self.session.auth = tuple(auth.split(":"))
        self.session.mount("http", TimeOutHTTPAdapter(timeout=self.timeout))
        self.session.mount("https", TimeOutHTTPAdapter(timeout=self.timeout))

        self._node_to_address = None
        self.token_address = None
        self.token_deployment_block = 0
        self.token_network_address = None

        task_config = self.scenario.task_config
        task_class = self.scenario.task_class
        self.root_task = task_class(runner=self, config=task_config)

    def determine_run_number(self) -> int:
        """Determine the current run number.

        We check for a run number file, and use any number that is logged
        there after incrementing it.

        REFAC: Replace this with a property.
        """
        run_number = 0
        run_number_file = self.data_path.joinpath("run_number.txt")
        if run_number_file.exists():
            run_number = int(run_number_file.read_text()) + 1
        run_number_file.write_text(str(run_number))
        log.info("Run number", run_number=run_number)
        return run_number

    def select_chain(
            self, chain_urls: Dict[str, List[str]]) -> Tuple[str, List[str]]:
        """Select a chain and return its name and RPC URL.

        If the currently loaded scenario's designated chain is set to 'any',
        we randomly select a chain from the given `chain_urls`.
        Otherwise, we will return `ScenarioRunner.scenario.chain_name` and whatever value
        may be associated with this key in `chain_urls`.

        :raises ScenarioError:
            if ScenarioRunner.scenario.chain_name is not one of `('any', 'Any', 'ANY')`
            and it is not a key in `chain_urls`.
        """
        chain_name = self.scenario.chain_name
        if chain_name in ("any", "Any", "ANY"):
            chain_name = random.choice(list(chain_urls.keys()))

        log.info("Using chain", chain=chain_name)
        try:
            return chain_name, chain_urls[chain_name]
        except KeyError:
            raise ScenarioError(
                f'The scenario requested chain "{chain_name}" for which no RPC-URL is known.'
            )

    def run_scenario(self):
        mint_gas = GAS_LIMIT_FOR_TOKEN_CONTRACT_CALL * 2

        fund_tx, node_starter, node_addresses, node_count = self._initialize_nodes(
        )

        ud_token_tx, udc_ctr, should_deposit_ud_token = self._initialize_udc(
            gas_limit=mint_gas, node_count=node_count)

        mint_tx = self._initialize_scenario_token(
            node_addresses=node_addresses,
            udc_ctr=udc_ctr,
            should_deposit_ud_token=should_deposit_ud_token,
            gas_limit=mint_gas,
        )

        wait_for_txs(self.client, fund_tx | ud_token_tx | mint_tx)

        if node_starter is not None:
            log.debug("Waiting for nodes to finish starting")
            node_starter.get(block=True)

        first_node = self.get_node_baseurl(0)

        registered_tokens = set(
            self.session.get(
                API_URL_TOKENS.format(protocol=self.protocol,
                                      target_host=first_node)).json())
        if self.token_address not in registered_tokens:
            for _ in range(5):
                code, msg = self.register_token(self.token_address, first_node)
                if 199 < code < 300:
                    break
                gevent.sleep(1)
            else:
                log.error("Couldn't register token with network",
                          code=code,
                          message=msg)
                raise TokenRegistrationError(msg)

        # The nodes need some time to find the token, see
        # https://github.com/raiden-network/raiden/issues/3544
        # FIXME: Add proper check via API
        log.info("Waiting till new network is found by nodes")
        while self.token_network_address is None:
            self.token_network_address = self.session.get(
                API_URL_TOKEN_NETWORK_ADDRESS.format(
                    protocol=self.protocol,
                    target_host=first_node,
                    token_address=self.token_address,
                )).json()
            gevent.sleep(1)

        log.info("Received token network address",
                 token_network_address=self.token_network_address)

        # Start root task
        root_task_greenlet = gevent.spawn(self.root_task)
        greenlets = {root_task_greenlet}
        if self.is_managed:
            greenlets.add(self.node_controller.start_node_monitor())
        try:
            gevent.joinall(greenlets, raise_error=True)
        except BaseException:
            if not root_task_greenlet.dead:
                # Make sure we kill the tasks if a node dies
                root_task_greenlet.kill()
            raise

    def _initialize_scenario_token(
        self,
        node_addresses: Set[ChecksumAddress],
        udc_ctr: Optional[ContractProxy],
        should_deposit_ud_token: bool,
        gas_limit: int,
    ) -> Set[TransactionHash]:
        token_ctr, token_block = get_or_deploy_token(self)
        self.token_address = to_checksum_address(token_ctr.contract_address)
        self.token_deployment_block = token_block
        token_settings = self.scenario.get("token") or {}
        token_balance_min = token_settings.get("balance_min",
                                               DEFAULT_TOKEN_BALANCE_MIN)
        token_balance_fund = token_settings.get("balance_fund",
                                                DEFAULT_TOKEN_BALANCE_FUND)
        mint_tx = set()
        for address in node_addresses:
            tx = mint_token_if_balance_low(
                token_contract=token_ctr,
                target_address=address,
                min_balance=token_balance_min,
                fund_amount=token_balance_fund,
                gas_limit=gas_limit,
                mint_msg="Minting tokens for",
            )
            if tx:
                mint_tx.add(tx)

            if not should_deposit_ud_token:
                continue
            ud_deposit_balance = udc_ctr.contract.functions.effectiveBalance(
                address).call()
            if ud_deposit_balance < DEFAULT_TOKEN_BALANCE_MIN // 2:
                deposit_amount = (DEFAULT_TOKEN_BALANCE_FUND //
                                  2) - ud_deposit_balance
                log.debug("Depositing into UDC",
                          address=address,
                          amount=deposit_amount)
                mint_tx.add(
                    udc_ctr.transact("deposit", gas_limit, address,
                                     DEFAULT_TOKEN_BALANCE_FUND // 2))
        return mint_tx

    def _initialize_udc(
        self, gas_limit: int, node_count: int
    ) -> Tuple[Set[TransactionHash], Optional[ContractProxy], bool]:
        our_address = to_checksum_address(self.client.address)
        udc_settings = self.scenario.services.get("udc", {})
        udc_enabled = udc_settings.get("enable")

        ud_token_tx = set()

        if not udc_enabled:
            return ud_token_tx, None, False

        udc_ctr, ud_token_ctr = get_udc_and_token(self)

        ud_token_address = to_checksum_address(ud_token_ctr.contract_address)
        udc_address = to_checksum_address(udc_ctr.contract_address)

        log.info("UDC enabled",
                 contract_address=udc_address,
                 token_address=ud_token_address)

        should_deposit_ud_token = udc_enabled and udc_settings.get(
            "token", {}).get("deposit", False)
        if should_deposit_ud_token:
            tx = mint_token_if_balance_low(
                token_contract=ud_token_ctr,
                target_address=our_address,
                min_balance=DEFAULT_TOKEN_BALANCE_FUND * node_count,
                fund_amount=DEFAULT_TOKEN_BALANCE_FUND * 10 * node_count,
                gas_limit=gas_limit,
                mint_msg="Minting UD tokens",
                no_action_msg="UD token balance sufficient",
            )
            if tx:
                ud_token_tx.add(tx)

            udt_allowance = ud_token_ctr.contract.functions.allowance(
                our_address, udc_address).call()
            if udt_allowance < DEFAULT_TOKEN_BALANCE_FUND * node_count:
                allow_amount = (DEFAULT_TOKEN_BALANCE_FUND * 10 *
                                node_count) - udt_allowance
                log.debug("Updating UD token allowance",
                          allowance=allow_amount)
                ud_token_tx.add(
                    ud_token_ctr.transact("approve", gas_limit, udc_address,
                                          allow_amount))
            else:
                log.debug("UD token allowance sufficient",
                          allowance=udt_allowance)
        return ud_token_tx, udc_ctr, should_deposit_ud_token

    def _initialize_nodes(
        self
    ) -> Tuple[Set[TransactionHash], gevent.Greenlet, Set[ChecksumAddress],
               int]:
        fund_tx = set()
        node_starter: gevent.Greenlet = None
        if self.is_managed:
            self.node_controller.initialize_nodes()
            node_addresses = self.node_controller.addresses
            node_count = len(self.node_controller)
            node_balances = {
                address: self.client.balance(address)
                for address in node_addresses
            }
            low_balances = {
                address: balance
                for address, balance in node_balances.items()
                if balance < NODE_ACCOUNT_BALANCE_MIN
            }
            if low_balances:
                log.info("Funding nodes", nodes=low_balances.keys())
                fund_tx = {
                    self.client.send_transaction(
                        to=address,
                        startgas=21_000,
                        value=NODE_ACCOUNT_BALANCE_FUND - balance)
                    for address, balance in low_balances.items()
                }
            node_starter = self.node_controller.start(wait=False)

        else:
            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)}")
            node_addresses = set(self.node_to_address.values())
            node_count = len(self.node_to_address)
        return fund_tx, node_starter, node_addresses, node_count

    def task_state_changed(self, task: "Task", state: "TaskState"):
        if self.task_state_callback:
            self.task_state_callback(self, task, state)

    def register_token(self, token_address, node):
        try:
            base_url = API_URL_TOKENS.format(protocol=self.protocol,
                                             target_host=node)
            url = "{}/{}".format(base_url, token_address)
            log.info("Registering token with network", url=url)
            resp = self.session.put(url)
            code = resp.status_code
            msg = resp.text
        except RequestException as ex:
            code = -1
            msg = str(ex)
        return code, msg

    @staticmethod
    def _spawn_and_wait(objects, callback):
        tasks = {obj: gevent.spawn(callback, obj) for obj in objects}
        gevent.joinall(set(tasks.values()))
        return {obj: task.get() for obj, task in tasks.items()}

    @property
    def is_managed(self):
        return self.node_mode is NodeMode.MANAGED

    def get_node_address(self, index):
        if self.is_managed:
            return self.node_controller[index].address
        else:
            return self.node_to_address[self.raiden_nodes[index]]

    def get_node_baseurl(self, index):
        if self.is_managed:
            return self.node_controller[index].base_url
        else:
            return self.raiden_nodes[index]

    # Legacy for 'external' nodes
    def _get_node_addresses(self, nodes):
        def cb(node):
            log.debug("Getting node address", node=node)
            url = API_URL_ADDRESS.format(protocol=self.protocol,
                                         target_host=node)
            log.debug("Requesting", url=url)
            try:
                resp = self.session.get(url)
            except RequestException:
                log.error("Error fetching node address", url=url, node=node)
                return None
            try:
                return to_checksum_address(resp.json().get("our_address", ""))
            except ValueError:
                log.error("Error decoding response",
                          response=resp.text,
                          code=resp.status_code,
                          url=url)
            return None

        ret = self._spawn_and_wait(nodes, cb)
        return ret

    @property
    def node_to_address(self) -> Dict[str, ChecksumAddress]:
        if not self.raiden_nodes:
            return {}
        if self._node_to_address is None:
            self._node_to_address = {
                node: address
                for node, address in self._get_node_addresses(
                    self.raiden_nodes).items()
            }
        return self._node_to_address