示例#1
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
示例#2
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
示例#3
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
示例#4
0
class ScenarioRunner:
    def __init__(
        self,
        account: Account,
        auth: str,
        data_path: Path,
        scenario_file: Path,
        environment: EnvironmentConfig,
        success: Event,
        raiden_client: Optional[str],
        task_state_callback: Optional[
            Callable[["ScenarioRunner", "Task", "TaskState"], None]
        ] = None,
        smoketest_deployment_data: DeployedContracts = None,
        delete_snapshots: bool = False,
    ) -> None:
        self.success = success

        self.smoketest_deployment_data = smoketest_deployment_data
        self.delete_snapshots = delete_snapshots

        self.data_path = data_path
        self.environment = environment

        # Set the client executable to use
        if raiden_client is not None:
            self.environment.raiden_client = raiden_client

        self.task_count = 0
        self.running_task_count = 0
        self.task_cache: Dict[str, Task] = {}
        self.task_state_callback = task_state_callback
        # Storage for arbitrary data tasks might need to persist
        self.task_storage: Dict[str, dict] = defaultdict(dict)

        self.definition = ScenarioDefinition(scenario_file, data_path, self.environment)

        log.debug("Local seed", seed=self.local_seed)

        self.run_number = determine_run_number(self.definition.scenario_dir)

        log.info("Run number", run_number=self.run_number)

        self.protocol = "http"
        self.session = make_session(auth, self.definition.settings, self.definition.nodes)

        web3 = Web3(HTTPProvider(environment.eth_rpc_endpoints[0], session=self.session))
        self.chain_id = ChainID(web3.eth.chainId)
        self.definition.settings.eth_rpc_endpoint_iterator = environment.eth_rpc_endpoint_iterator
        self.definition.settings.chain_id = self.chain_id

        assert account.privkey, "Account not unlockable"
        self.client = JSONRPCClient(
            web3=web3,
            privkey=account.privkey,
            gas_price_strategy=faster_gas_price_strategy,
            block_num_confirmations=DEFAULT_NUMBER_OF_BLOCK_CONFIRMATIONS,
        )

        assert account.address, "Account not loaded"
        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)} "
                f'on chain "{self.definition.settings.chain_id}"'
                f" - it needs additional {(OWN_ACCOUNT_BALANCE_MIN - balance) / 10 ** 18} Eth ("
                f"that is {OWN_ACCOUNT_BALANCE_MIN - balance} Wei)."
            )

        self.node_controller = NodeController(
            runner=self,
            config=self.definition.nodes,
            raiden_client=self.environment.raiden_client,
            delete_snapshots=self.delete_snapshots,
        )
        task_config = self.definition.scenario.root_config
        task_class = self.definition.scenario.root_class
        self.root_task = task_class(runner=self, config=task_config)

    @property
    def local_seed(self) -> str:
        """Return a persistent random seed value.

        We need a unique seed per scenario player 'installation'.
        This is used in the node private key generation to prevent re-use of node keys between
        multiple users of the scenario player.

        The seed is stored in a file inside the ``.definition.settings.sp_root_dir``.
        """
        assert self.definition.settings.sp_root_dir
        seed_file = self.definition.settings.sp_root_dir.joinpath("seed.txt")
        if not seed_file.exists():
            seed = str(encode_hex(bytes(random.randint(0, 255) for _ in range(20))))
            seed_file.write_text(seed)
        else:
            seed = seed_file.read_text().strip()
        return seed

    def ensure_token_network_discovery(
        self, token: CustomToken, token_network_addresses: TokenNetworkAddress
    ) -> None:
        """Ensure that all our nodes have discovered the same token network."""
        for node in self.node_controller:  # type: ignore
            node_endpoint = API_URL_TOKEN_NETWORK_ADDRESS.format(
                protocol=self.protocol,
                target_host=node.base_url,
                token_address=to_checksum_address(token.address),
            )
            address = wait_for_token_network_discovery(
                node_endpoint, self.definition.settings, self.session
            )
            if to_canonical_address(address) != Address(token_network_addresses):
                raise RuntimeError(
                    f"Nodes diverged on the token network address, there should be "
                    f"exactly one token network available for all nodes. Current "
                    f"values : {to_hex(token_network_addresses)}"
                )

    def run_scenario(self) -> None:
        with Janitor() as nursery:
            self.node_controller.set_nursery(nursery)
            self.node_controller.initialize_nodes()

            try:
                for node_runner in self.node_controller._node_runners:
                    node_runner.start()
            except Exception:
                log.error("failed to start", exc_info=True)
                raise

            node_addresses = self.node_controller.addresses

            scenario = nursery.spawn_under_watch(
                self.setup_environment_and_run_main_task, node_addresses
            )
            scenario.name = "orchestration"

            # Wait for either a crash in one of the Raiden nodes or for the
            # scenario to exit (successfully or not).
            greenlets = {scenario}
            gevent.joinall(greenlets, raise_error=True, count=1)
        self.success.set()

    def setup_environment_and_run_main_task(self, node_addresses: Set[ChecksumAddress]) -> None:
        """This will first make sure the on-chain state is setup properly, and
        then execute the scenario.

        The on-chain state consists of:

        - Deployment of the test CustomToken
        - For each of the Raiden nodes, make sure they have enough:

            - Ether to pay for the transactions.
            - Utility token balances in the user deposit smart contract.
            - Tokens to be used with the test token network.
        """
        block_execution_started = self.client.block_number()

        settings = self.definition.settings
        udc_settings = settings.services.udc

        smoketesting = False
        if self.chain_id != CHAINNAME_TO_ID["smoketest"]:
            deploy = get_contracts_deployment_info(
                self.chain_id,
                RAIDEN_CONTRACT_VERSION,
                development_environment=self.environment.development_environment,
            )
        else:
            smoketesting = True
            deploy = self.smoketest_deployment_data

        msg = "There is no deployment details for the given chain_id and contracts version pair"
        assert deploy, msg

        proxy_manager = get_proxy_manager(self.client, deploy)

        # Tracking pool to synchronize on all concurrent transactions
        pool = Pool()

        log.debug("Funding Raiden node's accounts with ether")
        self.setup_raiden_nodes_ether_balances(pool, node_addresses)

        if is_udc_enabled(udc_settings):
            (
                userdeposit_proxy,
                user_token_proxy,
            ) = get_udc_and_corresponding_token_from_dependencies(
                udc_address=udc_settings.address,
                chain_id=settings.chain_id,
                proxy_manager=proxy_manager,
                development_environment=self.environment.development_environment,
            )

            log.debug("Minting utility tokens and /scheduling/ transfers to the nodes")
            mint_greenlets = self.setup_mint_user_deposit_tokens_for_distribution(
                pool, userdeposit_proxy, user_token_proxy, node_addresses
            )
            self.setup_raiden_nodes_with_sufficient_user_deposit_balances(
                pool, userdeposit_proxy, node_addresses, mint_greenlets
            )

        # This is a blocking call. If the token has to be deployed it will
        # block until mined and confirmed, since that is a requirement for the
        # following setup calls.
        token_proxy = self.setup_token_contract_for_token_network(proxy_manager)
        if smoketesting:
            token_network_registry_proxy = get_token_network_registry_from_dependencies(
                settings=settings,
                proxy_manager=proxy_manager,
                smoketest_deployment_data=deploy,
                development_environment=self.environment.development_environment,
            )
        else:
            token_network_registry_proxy = get_token_network_registry_from_dependencies(
                settings=settings,
                proxy_manager=proxy_manager,
                development_environment=self.environment.development_environment,
            )

        self.setup_raiden_token_balances(pool, token_proxy, node_addresses)

        # Wait for all the transactions
        # - Move ether from the orcheastration account (the scenario player),
        # to the raiden nodes.
        # - Mint enough utility tokens (user deposit tokens) for the
        # orchestration account to transfer for the nodes.
        # - Mint network tokens for the nodes to use in the scenarion.
        # - Deposit utility tokens for the raiden nodes in the user deposit
        # contract.
        log.debug("Waiting for funding transactions to be mined")
        pool.join(raise_error=True)

        log.debug("Registering token to create the network")
        token_network_address = maybe_create_token_network(
            token_network_registry_proxy, token_proxy
        )

        log.debug("Waiting for the REST APIs")
        wait_for_nodes_to_be_ready(self.node_controller._node_runners, self.session)

        log.info("Making sure all nodes have the same token network")
        self.ensure_token_network_discovery(token_proxy, token_network_address)

        log.info(
            "Setup done, running scenario",
            token_network_address=to_checksum_address(token_network_address),
        )

        # Expose attributes used by the tasks
        self.token = token_proxy
        self.contract_manager = proxy_manager.contract_manager
        self.token_network_address = to_checksum_address(token_network_address)
        self.block_execution_started = block_execution_started

        self.root_task()

    def setup_raiden_nodes_ether_balances(
        self, pool: Pool, node_addresses: Set[ChecksumAddress]
    ) -> Set[Greenlet]:
        """ Makes sure every Raiden node has at least `NODE_ACCOUNT_BALANCE_MIN`. """

        greenlets: Set[Greenlet] = set()
        for address in node_addresses:
            g = pool.spawn(
                eth_maybe_transfer,
                orchestration_client=self.client,
                target=to_canonical_address(address),
                minimum_balance=NODE_ACCOUNT_BALANCE_MIN,
                maximum_balance=NODE_ACCOUNT_BALANCE_FUND,
            )
            greenlets.add(g)

        return greenlets

    def setup_mint_user_deposit_tokens_for_distribution(
        self,
        pool: Pool,
        userdeposit_proxy: UserDeposit,
        token_proxy: CustomToken,
        node_addresses: Set[ChecksumAddress],
    ) -> Set[Greenlet]:
        """Ensures the scenario player account has enough tokens and allowance
        to fund the Raiden nodes.
        """
        settings = self.definition.settings
        udc_settings = settings.services.udc
        balance_per_node = settings.services.udc.token.balance_per_node

        msg = "udc is not enabled, this function should not be called"
        assert is_udc_enabled(udc_settings), msg

        node_count = len(node_addresses)
        required_allowance = balance_per_node * node_count

        allowance_greenlet = pool.spawn(
            userdeposit_maybe_increase_allowance,
            token_proxy=token_proxy,
            userdeposit_proxy=userdeposit_proxy,
            orchestrator_address=self.client.address,
            minimum_allowance=required_allowance,
            maximum_allowance=UINT256_MAX,
        )

        mint_greenlet = pool.spawn(
            token_maybe_mint,
            token_proxy=token_proxy,
            target_address=to_checksum_address(self.client.address),
            minimum_balance=required_allowance,
            maximum_balance=ORCHESTRATION_MAXIMUM_BALANCE,
        )

        return {allowance_greenlet, mint_greenlet}

    def setup_raiden_token_balances(
        self, pool: Pool, token_proxy: CustomToken, node_addresses: Set[ChecksumAddress]
    ) -> Set[Greenlet]:
        """Mint the necessary amount of tokens from `token_proxy` for every `node_addresses`.

        This will use the scenario player's account, therefore it doesn't have
        to wait for the ether transfers to finish.
        """
        token_min_amount = self.definition.token.min_balance
        token_max_amount = self.definition.token.max_funding

        greenlets: Set[Greenlet] = set()
        for address in node_addresses:
            g = pool.spawn(
                token_maybe_mint,
                token_proxy=token_proxy,
                target_address=address,
                minimum_balance=token_min_amount,
                maximum_balance=token_max_amount,
            )
            greenlets.add(g)

        return greenlets

    def setup_raiden_nodes_with_sufficient_user_deposit_balances(
        self,
        pool: Pool,
        userdeposit_proxy: UserDeposit,
        node_addresses: Set[ChecksumAddress],
        mint_greenlets: Set[Greenlet],
    ) -> Set[Greenlet]:
        """Makes sure every Raiden node's account has enough tokens in the
        user deposit contract.

        For these transfers to work, the approve and mint transacations have to
        be mined and confirmed. This is necessary because otherwise the gas
        estimation of the deposits fail.
        """
        msg = "udc is not enabled, this function should not be called"
        assert is_udc_enabled(self.definition.settings.services.udc), msg

        minimum_effective_deposit = self.definition.settings.services.udc.token.balance_per_node
        maximum_funding = self.definition.settings.services.udc.token.max_funding

        log.debug("Depositing utility tokens for the nodes")
        greenlets: Set[Greenlet] = set()
        for address in node_addresses:
            g = pool.spawn(
                userdeposit_maybe_deposit,
                userdeposit_proxy=userdeposit_proxy,
                mint_greenlets=mint_greenlets,
                target_address=to_canonical_address(address),
                minimum_effective_deposit=minimum_effective_deposit,
                maximum_funding=maximum_funding,
            )
            greenlets.add(g)

        return greenlets

    def setup_token_contract_for_token_network(self, proxy_manager: ProxyManager) -> CustomToken:
        """Ensure there is a deployed token contract and return a `CustomToken`
        proxy to it. This token will be used for the scenario's token network.

        This will either:

        - Use the token from the address provided in the scenario
          configuration.
        - Use a previously deployed token, with the details loaded from the
          disk.
        - Deploy a new token if neither of the above options is used.
        """
        token_definition = self.definition.token
        reuse_token_from_file = token_definition.can_reuse_token

        if token_definition.address:
            token_address = to_canonical_address(token_definition.address)
        elif reuse_token_from_file:
            token_details = load_token_configuration_from_file(token_definition.token_file)
            token_address = to_canonical_address(token_details["address"])
        else:
            contract_data = proxy_manager.contract_manager.get_contract(CONTRACT_CUSTOM_TOKEN)
            contract, receipt = self.client.deploy_single_contract(
                contract_name=CONTRACT_CUSTOM_TOKEN,
                contract=contract_data,
                constructor_parameters=(
                    ORCHESTRATION_MAXIMUM_BALANCE,
                    token_definition.decimals,
                    token_definition.name,
                    token_definition.symbol,
                ),
            )
            token_address = to_canonical_address(contract.address)

            if token_definition.should_reuse_token:
                details = TokenDetails(
                    {
                        "name": token_definition.name,
                        "address": to_checksum_address(token_address),
                        "block": receipt["blockNumber"],
                    }
                )
                save_token_configuration_to_file(token_definition.token_file, details)

        return proxy_manager.custom_token(TokenAddress(token_address), "latest")

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

    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