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())
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)
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
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
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, )
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
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
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()
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
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))