示例#1
0
    def check_configuration_settings(self, config):
        """
        Performs sanity checks on configuration parameters.
        """
        # collect state variables from the smart contract
        call = config.audit_contract.functions.getAuditTimeoutInBlocks()
        contract_audit_timeout_in_blocks = mk_read_only_call(config, call)
        call = config.audit_contract.functions.getMaxAssignedRequests()
        contract_max_assigned_requests = mk_read_only_call(config, call)

        # start_n_blocks_in_the_past should never exceed the submission timeout
        ConfigUtils.raise_err(
            cond=config.start_n_blocks_in_the_past > config.submission_timeout_limit_blocks,
            msg="start_n_blocks_in_the_past {0} should never exceed the "
                "submission timeout {1}".format(
                config.start_n_blocks_in_the_past,
                config.submission_timeout_limit_blocks)
            )

        # the submission timeout limit should not exceed the audit timeout limit
        ConfigUtils.raise_err(
            cond=config.submission_timeout_limit_blocks > contract_audit_timeout_in_blocks,
            msg="the submission timeout {0} limit should not exceed the audit "
                "timeout limit {1} set in the contract".format(
                config.submission_timeout_limit_blocks,
                contract_audit_timeout_in_blocks)
            )

        # the analyzer timeouts should never exceed the audit timeout (converted to seconds)
        for analyzer in config.analyzers:
            analyzer_timeout = analyzer.wrapper.timeout_sec
            ConfigUtils.raise_err(
                analyzer_timeout > contract_audit_timeout_in_blocks * ConfigUtils.__APPROXIMATE_BLOCK_LENGTH_IN_SECONDS)

        # max assigned requests should never exceed the limit specified in the contract
        ConfigUtils.raise_err(config.max_assigned_requests > contract_max_assigned_requests)

        # default gas price should never exceed max gas price, if set
        if config.max_gas_price_wei > 0:
            ConfigUtils.raise_err(config.default_gas_price_wei > config.max_gas_price_wei)

        # the gas price strategy can be either dynamic or static
        ConfigUtils.raise_err(config.gas_price_strategy not in ['dynamic', 'static'])

        # the n-blocks confirmation amount should never exceed the audit timeout in blocks
        ConfigUtils.raise_err(config.n_blocks_confirmation > ConfigUtils.__AUDIT_TIMEOUT_IN_BLOCKS)

        # the n-blocks confirmation amount should not be negative
        ConfigUtils.raise_err(config.n_blocks_confirmation < 0)
 def __get_next_police_assignment(self):
     """
     Gets the next police assignment tuple.
     """
     return mk_read_only_call(
         self.config,
         self.config.audit_contract.functions.getNextPoliceAssignment())
示例#3
0
 def setUp(self):
     self.config = fetch_config(inject_contract=True)
     self.thread = MonitorSubmissionThread(self.config)
     self.evt_pool_manager = self.thread.config.event_pool_manager
     self.evt_pool_manager.set_evt_status_to_error = MagicMock()
     self.evt_pool_manager.set_evt_status_to_be_submitted = MagicMock()
     self.evt_pool_manager.set_evt_status_to_done = MagicMock()
     self.timeout_limit_blocks = mk_read_only_call(
         self.config,
         self.config.audit_contract.functions.getAuditTimeoutInBlocks())
    def __get_min_stake_qsp(self):
        """
        Gets the minimum staking (in QSP) required to perform an audit.
        """
        min_stake = mk_read_only_call(
            self.config,
            self.config.audit_contract.functions.getMinAuditStake())

        # Puts the result (wei-QSP) back to QSP
        return min_stake / (10**18)
示例#5
0
 def check_and_update_min_price(self):
     """
     Checks that the minimum price in the audit node's configuration matches the smart contract
     and updates it if it differs. This is a blocking function.
     """
     contract_price = mk_read_only_call(
         self.config,
         self.config.audit_contract.functions.getMinAuditPrice(
             self.config.account))
     min_price_in_mini_qsp = self.config.min_price_in_qsp * (10**18)
     if min_price_in_mini_qsp != contract_price:
         self.update_min_price()
    def __get_report_in_blockchain(self, request_id):
        """
        Gets a compressed report already stored in the blockchain.
        """
        compressed_report_bytes = mk_read_only_call(
            self.config,
            self.config.audit_contract.functions.getReport(request_id))
        if compressed_report_bytes is None or len(
                compressed_report_bytes) == 0:
            return None

        return compressed_report_bytes.hex()
    def __has_available_rewards(self):
        """
        Checks if any unclaimed rewards are available for the node.
        """
        available_rewards = False
        try:
            available_rewards = mk_read_only_call(
                self.config,
                self.config.audit_contract.functions.hasAvailableRewards())
        except Exception as err:
            raise err

        return available_rewards
示例#8
0
    def get_stake_required(config):
        """
        Returns the minimum required stake.
        """
        stake_required = 0
        try:
            stake_required = mk_read_only_call(
                config, config.audit_contract.functions.getMinAuditStake())
        except Exception as err:
            config.logger.debug(
                "Failed to check the minimum required stake: {0}".format(err))

        return stake_required
示例#9
0
    def __process_submissions(self):
        """
        Checks all events in state SB for timeout and sets the ones that timed out to state ER.
        """
        # Checks for a potential timeouts
        timeout_limit_blocks = mk_read_only_call(
            self.config,
            self.config.audit_contract.functions.getAuditTimeoutInBlocks())

        self.config.event_pool_manager.process_submission_events(
            self.__monitor_submission_timeout,
            timeout_limit_blocks,
        )
示例#10
0
    def has_enough_stake(config):
        """
        Returns true if the node has enough stake and false otherwise.
        """
        has_stake = False
        try:
            has_stake = mk_read_only_call(
                config,
                config.audit_contract.functions.hasEnoughStake(config.account))
        except Exception as err:
            config.logger.debug(
                "Failed to check if node has enough stake: {0}".format(err))

        return has_stake
示例#11
0
    def is_police_officer(config):
        """
        Verifies whether the node is a police node.
        """
        is_police = False
        try:
            is_police = mk_read_only_call(
                config,
                config.audit_contract.functions.isPoliceNode(config.account))
        except Exception as err:
            config.logger.debug(
                "Failed to check if node is a police officer: {0}".format(err))
            config.logger.debug("Assuming the node is not a police officer.")

        return is_police
示例#12
0
    def __check_and_update_min_price(self):
        """
        Checks that the minimum price in the audit node's configuration matches the smart contract
        and updates it if it differs. This is a blocking function.
        """
        contract_price = mk_read_only_call(
            self.config,
            self.config.audit_contract.functions.getMinAuditPrice(
                self.config.account))

        contract_price_lower_cap = mk_read_only_call(
            self.config,
            self.config.audit_contract.functions.getMinAuditPriceLowerCap())

        min_price_in_mini_qsp = self.config.min_price_in_qsp * (10**18)
        if min_price_in_mini_qsp < contract_price_lower_cap:
            error_msg = "The provided min price {0} QSP must be equal to or higher than the floor of {1} QSP".format(
                self.config.min_price_in_qsp,
                contract_price_lower_cap / (10**18))
            self.logger.exception(error_msg)
            raise Exception(error_msg)

        if min_price_in_mini_qsp != contract_price:
            self.__update_min_price()
示例#13
0
    def get_current_stake(config):
        """
        Returns the amount of stake needed.
        """
        staked = 0
        try:
            staked = mk_read_only_call(
                config,
                config.audit_contract.functions.totalStakedFor(config.account))
        except Exception as err:
            config.logger.debug(
                "Failed to check the how much stake is missing: {0}".format(
                    err))

        return staked
示例#14
0
    def __monitor_submission_timeout(self, evt, timeout_limit_blocks):
        """
        Sets the event to state ER if the timeout window passed.
        """
        try:
            submission_attempts = evt['submission_attempts']

            is_finished = mk_read_only_call(
                self.config,
                self.config.audit_contract.functions.isAuditFinished(
                    evt['request_id']))

            current_block = self.config.web3_client.eth.blockNumber
            if is_finished and evt != {}:

                submission_block = evt['submission_block_nbr']
                if (current_block -
                        submission_block) < self.config.n_blocks_confirmation:
                    # Not yet confirmed. Wait...
                    self.logger.debug(
                        "Waiting on report submission for {0}".format(
                            evt['request_id']),
                        requestId=evt['request_id'])
                    return

                # (Else) Submission is finished and final. Move its status to done.
                evt['status_info'] = 'Report successfully submitted'
                self.config.event_pool_manager.set_evt_status_to_done(evt)
                self.logger.debug(
                    "Report successfully submitted for event: {0}".format(
                        str(evt)),
                    requestId=evt['request_id'])
            else:
                assigned_block = evt['assigned_block_nbr']

                # Checks if current timepoint still falls within the
                # audit window. If so, retry provided the number of
                # maximum attempts is not excedded.
                if current_block < (assigned_block + timeout_limit_blocks):
                    # Retries exceeds the number of allowed attempts. Then, mark the
                    # event as error.
                    if (submission_attempts + 1
                        ) > MonitorSubmissionThread.MAX_SUBMISSION_ATTEMPTS:
                        error_msg = "Submitting audit {0} timed-out after {1} attempts. "
                        error_msg += "The event was created in block {2}. The timeout limit is {3} blocks. "
                        error_msg += "The current block is {4}."
                        error_msg = error_msg.format(evt['request_id'],
                                                     submission_attempts,
                                                     evt['assigned_block_nbr'],
                                                     timeout_limit_blocks,
                                                     current_block)
                        self.logger.debug(error_msg,
                                          requestId=evt['request_id'])
                        evt['status_info'] = "Reached maximum number of submission attempts ({0})".format(
                            MonitorSubmissionThread.MAX_SUBMISSION_ATTEMPTS)
                        self.config.event_pool_manager.set_evt_status_to_error(
                            evt)
                    else:
                        # A retry is still possible. Make it happen.
                        evt['status_info'] = "Attempting to resubmit report {0} (retry = {1})".format(
                            evt['request_id'], (submission_attempts + 1))
                        self.logger.debug(evt['status_info'],
                                          requestId=evt['request_id'])

                        # Resets the transaction hash from the previous
                        # submission attempt
                        evt['tx_hash'] = None
                        self.config.event_pool_manager.set_evt_status_to_be_submitted(
                            evt)
                else:
                    evt['status_info'] = "Submission of audit report outside completion window ({0} blocks)".format(
                        timeout_limit_blocks)
                    self.logger.debug(evt['status_info'],
                                      requestId=evt['request_id'])
                    self.config.event_pool_manager.set_evt_status_to_error(evt)

        except KeyError as error:
            evt['status_info'] = "KeyError when monitoring submission and timeout: {0}".format(
                str(error))
            self.logger.exception(evt['status_info'],
                                  requestId=evt.get('request_id', -1))

            # Non-recoverable exception. If a field is missing, it is a bug
            # elsewhere!!! The field will not magically be given a value out
            # of nowhere...
            self.config.event_pool_manager.set_evt_status_to_error(evt)

        except Exception as error:
            # TODO How to inform the network of a submission timeout?
            error_msg = "Unexpected error when monitoring submission and timeout: {0}. "
            error_msg += "Audit event is {1}"
            error_msg = error_msg.format(error, evt)
            self.logger.exception(error_msg, requestId=evt['request_id'])
            evt['status_info'] = error_msg

            # Potentially recoverable if number of resubmission is not exceeded.
            if (submission_attempts +
                    1) > MonitorSubmissionThread.MAX_SUBMISSION_ATTEMPTS:
                self.config.event_pool_manager.set_evt_status_to_error(evt)
            else:
                self.config.event_pool_manager.set_evt_status_to_be_submitted(
                    evt)
    def __poll_audit_request(self, current_block):
        """
        Checks first an audit is assignable; then, bids to get an audit request.
        If successful, save the event in the database to move it along the audit pipeline.
        """
        if self.__is_police_officer() and \
            not self.config.enable_police_audit_polling:
            return

        try:
            most_recent_audit = mk_read_only_call(
                self.config,
                self.config.audit_contract.functions.myMostRecentAssignedAudit(
                ))

            request_id = most_recent_audit[0]
            audit_assignment_block_number = most_recent_audit[4]

            # Check if the most recent audit has been confirmed for N blocks. A consequence of this
            # is that the audit node will not call getNextAuditRequest again while a previous call
            # is not confirmed. If alternative approaches are developed, they should carefully
            # consider possibly adverse interactions between myMostRecentAssignedAudit and
            # waiting for confirmation.
            if audit_assignment_block_number != 0 and \
                audit_assignment_block_number + self.config.n_blocks_confirmation > current_block:
                # Check again when the next block is mined
                return

            # Checks if a previous bid was won. If so, it saves the event to the
            # database for processing by other threads and continues bidding
            # upon an available request

            new_assigned_request = (
                request_id !=
                0) and not self.config.event_pool_manager.is_request_processed(
                    request_id=request_id)

            if new_assigned_request:
                # New request id in (bid won). Persists the event in the database
                self.__add_evt_to_db(
                    request_id=request_id,
                    requestor=most_recent_audit[1],
                    uri=most_recent_audit[2],
                    price=most_recent_audit[3],
                    assigned_block_nbr=audit_assignment_block_number)

            any_request_available = mk_read_only_call(
                self.config,
                self.config.audit_contract.functions.anyRequestAvailable())

            if any_request_available == self.__AVAILABLE_AUDIT_UNDERSTAKED:
                raise NotEnoughStake(
                    "Missing funds. To audit contracts, nodes must stake at "
                    "least {0} QSP".format(self.__get_min_stake_qsp()))

            if any_request_available == self.__AVAILABLE_AUDIT_STATE_READY:
                pending_requests_count = mk_read_only_call(
                    self.config,
                    self.config.audit_contract.functions.assignedRequestCount(
                        self.config.account))

                if pending_requests_count >= self.config.max_assigned_requests:
                    self.logger.error(
                        "Skip bidding as node is currently processing {0} requests in "
                        "audit contract {1}".format(
                            str(pending_requests_count),
                            self.config.audit_contract_address))
                    return

                self.logger.debug(
                    "There is request available to bid on in contract {0}.".
                    format(self.config.audit_contract_address))

                # At this point, the node is ready to bid. As such,
                # it tries to get the next audit request
                self.__get_next_audit_request()
            else:
                self.logger.debug(
                    "No request available as the contract {0} returned {1}.".
                    format(self.config.audit_contract_address,
                           str(any_request_available)))

        except NotEnoughStake as error:
            self.logger.error("Cannot poll for audit request: {0}".format(
                str(error)))

        except DeduplicationException as error:
            self.logger.debug(
                "Error when attempting to perform an audit request: {0}".
                format(str(error)))
        except TransactionNotConfirmedException as error:
            error_msg = "A transaction occurred, but was then uncled and never recovered. {0}"
            self.logger.debug(error_msg.format(str(error)))
        except Exception as error:
            self.logger.exception(str(error))