class PathfindingService(gevent.Greenlet): # pylint: disable=too-many-instance-attributes def __init__( # pylint: disable=too-many-arguments self, web3: Web3, contracts: Dict[str, Contract], private_key: str, db_filename: str, sync_start_block: BlockNumber = BlockNumber(0), required_confirmations: int = 8, poll_interval: float = 10, ): super().__init__() self.web3 = web3 self.registry_address = contracts[ CONTRACT_TOKEN_NETWORK_REGISTRY].address self.user_deposit_contract = contracts[CONTRACT_USER_DEPOSIT] self.chain_id = ChainID(int(web3.net.version)) self.address = private_key_to_address(private_key) self._required_confirmations = required_confirmations self._poll_interval = poll_interval self._is_running = gevent.event.Event() log.info("PFS payment address", address=self.address) self.blockchain_state = BlockchainState( latest_known_block=BlockNumber(0), token_network_registry_address=self.registry_address, chain_id=self.chain_id, ) self.database = PFSDatabase( filename=db_filename, pfs_address=self.address, sync_start_block=sync_start_block, token_network_registry_address=self.registry_address, chain_id=self.chain_id, user_deposit_contract_address=self.user_deposit_contract.address, allow_create=True, ) self.matrix_listener = MatrixListener( private_key=private_key, chain_id=self.chain_id, service_room_suffix=PATH_FINDING_BROADCASTING_ROOM, message_received_callback=self.handle_message, address_reachability_changed_callback=self. handle_reachability_change, ) self.address_to_reachability: Dict[Address, AddressReachability] = dict() self.token_networks = self._load_token_networks() def _load_token_networks(self) -> Dict[TokenNetworkAddress, TokenNetwork]: network_for_address = { n.address: n for n in self.database.get_token_networks() } channel_views = self.database.get_channel_views() for cv in channel_views: network_for_address[cv.token_network_address].add_channel_view(cv) # Register channel participants for presence tracking self.matrix_listener.follow_address_presence(cv.participant1) self.matrix_listener.follow_address_presence(cv.participant2) return network_for_address def _run(self) -> None: # pylint: disable=method-hidden register_error_handler() try: self.matrix_listener.start() except ConnectionError as exc: log.critical("Could not connect to broadcasting system.", exc=exc) sys.exit(1) log.info( "Listening to token network registry", registry_address=self.registry_address, start_block=self.database.get_latest_known_block(), ) while not self._is_running.is_set(): last_confirmed_block = self.web3.eth.blockNumber - self._required_confirmations max_query_interval_end_block = ( self.database.get_latest_known_block() + MAX_FILTER_INTERVAL) # Limit the max number of blocks that is processed per iteration last_block = min(last_confirmed_block, max_query_interval_end_block) self._process_new_blocks(last_block) try: gevent.sleep(self._poll_interval) except KeyboardInterrupt: log.info("Shutting down") sys.exit(0) def _process_new_blocks(self, last_block: BlockNumber) -> None: self.blockchain_state.latest_known_block = self.database.get_latest_known_block( ) self.blockchain_state.token_network_addresses = list( self.token_networks.keys()) _, events = get_blockchain_events( web3=self.web3, contract_manager=CONTRACT_MANAGER, chain_state=self.blockchain_state, to_block=last_block, ) for event in events: self.handle_event(event) def stop(self) -> None: self.matrix_listener.stop() self._is_running.set() self.matrix_listener.join() def follows_token_network( self, token_network_address: TokenNetworkAddress) -> bool: """ Checks if a token network is followed by the pathfinding service. """ return token_network_address in self.token_networks.keys() def handle_reachability_change(self, address: Address, reachability: AddressReachability) -> None: self.address_to_reachability[address] = reachability def get_token_network( self, token_network_address: TokenNetworkAddress ) -> Optional[TokenNetwork]: """ Returns the `TokenNetwork` for the given address or `None` for unknown networks. """ return self.token_networks.get(token_network_address) def handle_event(self, event: Event) -> None: if isinstance(event, ReceiveTokenNetworkCreatedEvent): self.handle_token_network_created(event) elif isinstance(event, ReceiveChannelOpenedEvent): self.handle_channel_opened(event) elif isinstance(event, ReceiveChannelNewDepositEvent): self.handle_channel_new_deposit(event) elif isinstance(event, ReceiveChannelClosedEvent): self.handle_channel_closed(event) elif isinstance(event, UpdatedHeadBlockEvent): self.database.update_lastest_known_block(event.head_block_number) else: log.debug("Unhandled event", evt=event) def handle_token_network_created( self, event: ReceiveTokenNetworkCreatedEvent) -> None: network_address = TokenNetworkAddress(event.token_network_address) if not self.follows_token_network(network_address): log.info("Found new token network", event_=event) self.token_networks[network_address] = TokenNetwork( network_address) self.database.upsert_token_network(network_address) def handle_channel_opened(self, event: ReceiveChannelOpenedEvent) -> None: token_network = self.get_token_network(event.token_network_address) if token_network is None: return log.info("Received ChannelOpened event", event_=event) self.matrix_listener.follow_address_presence(event.participant1, refresh=True) self.matrix_listener.follow_address_presence(event.participant2, refresh=True) channel_views = token_network.handle_channel_opened_event( channel_identifier=event.channel_identifier, participant1=event.participant1, participant2=event.participant2, settle_timeout=event.settle_timeout, ) for cv in channel_views: self.database.upsert_channel_view(cv) # Handle messages for this channel which where received before ChannelOpened with self.database.conn: for message in self.database.pop_waiting_messages( token_network_address=token_network.address, channel_id=event.channel_identifier): self.handle_message(message) def handle_channel_new_deposit( self, event: ReceiveChannelNewDepositEvent) -> None: token_network = self.get_token_network(event.token_network_address) if token_network is None: return log.info("Received ChannelNewDeposit event", event_=event) channel_view = token_network.handle_channel_new_deposit_event( channel_identifier=event.channel_identifier, receiver=event.participant_address, total_deposit=event.total_deposit, ) if channel_view: self.database.upsert_channel_view(channel_view) def handle_channel_closed(self, event: ReceiveChannelClosedEvent) -> None: token_network = self.get_token_network(event.token_network_address) if token_network is None: return log.info("Received ChannelClosed event", event_=event) token_network.handle_channel_closed_event( channel_identifier=event.channel_identifier) self.database.delete_channel_views(event.channel_identifier) def handle_message(self, message: Message) -> None: try: if isinstance(message, PFSCapacityUpdate): changed_cvs = self.on_capacity_update(message) elif isinstance(message, PFSFeeUpdate): changed_cvs = self.on_fee_update(message) else: log.debug("Ignoring message", message=message) for cv in changed_cvs: self.database.upsert_channel_view(cv) except DeferMessage as ex: self.defer_message_until_channel_is_open(ex.deferred_message) except InvalidGlobalMessage as ex: log.info(str(ex), **asdict(message)) def defer_message_until_channel_is_open(self, message: DeferableMessage) -> None: log.debug( "Received message for unknown channel, defer until ChannelOpened is confirmed", channel_id=message.canonical_identifier.channel_identifier, message=message, ) self.database.insert_waiting_message(message) def on_fee_update(self, message: PFSFeeUpdate) -> List[ChannelView]: if message.sender != message.updating_participant: raise InvalidPFSFeeUpdate( "Invalid sender recovered from signature in PFSFeeUpdate") token_network = self.get_token_network( message.canonical_identifier.token_network_address) if not token_network: return [] log.debug("Received Fee Update", message=message) if (message.canonical_identifier.channel_identifier not in token_network.channel_id_to_addresses): raise DeferMessage(message) return token_network.handle_channel_fee_update(message) def _validate_pfs_capacity_update( self, message: PFSCapacityUpdate) -> TokenNetwork: token_network_address = TokenNetworkAddress( message.canonical_identifier.token_network_address) # check if chain_id matches if message.canonical_identifier.chain_identifier != self.chain_id: raise InvalidCapacityUpdate( "Received Capacity Update with unknown chain identifier") # check if token network exists token_network = self.get_token_network(token_network_address) if token_network is None: raise InvalidCapacityUpdate( "Received Capacity Update with unknown token network") # check values < max int 256 if message.updating_capacity > UINT256_MAX: raise InvalidCapacityUpdate( "Received Capacity Update with impossible updating_capacity") if message.other_capacity > UINT256_MAX: raise InvalidCapacityUpdate( "Received Capacity Update with impossible other_capacity") # check signature of Capacity Update if message.sender != message.updating_participant: raise InvalidCapacityUpdate("Capacity Update not signed correctly") # check if channel exists channel_identifier = message.canonical_identifier.channel_identifier if channel_identifier not in token_network.channel_id_to_addresses: raise DeferMessage(message) # check if participants fit to channel id participants = token_network.channel_id_to_addresses[ channel_identifier] if message.updating_participant not in participants: raise InvalidCapacityUpdate( "Sender of Capacity Update does not match the internal channel" ) if message.other_participant not in participants: raise InvalidCapacityUpdate( "Other Participant of Capacity Update does not match the internal channel" ) return token_network def on_capacity_update(self, message: PFSCapacityUpdate) -> List[ChannelView]: token_network = self._validate_pfs_capacity_update(message) log.debug("Received Capacity Update", message=message) self.database.upsert_capacity_update(message) # Follow presence for the channel participants self.matrix_listener.follow_address_presence( message.updating_participant, refresh=True) self.matrix_listener.follow_address_presence(message.other_participant, refresh=True) updating_capacity_partner, other_capacity_partner = self.database.get_capacity_updates( updating_participant=message.other_participant, token_network_address=TokenNetworkAddress( message.canonical_identifier.token_network_address), channel_id=message.canonical_identifier.channel_identifier, ) return token_network.handle_channel_balance_update_message( message=message, updating_capacity_partner=updating_capacity_partner, other_capacity_partner=other_capacity_partner, )
class RequestCollector(gevent.Greenlet): def __init__(self, private_key: str, state_db: SharedDatabase): super().__init__() self.private_key = private_key self.state_db = state_db state = self.state_db.load_state() self.chain_id = state.blockchain_state.chain_id self.matrix_listener = MatrixListener( private_key=private_key, chain_id=self.chain_id, service_room_suffix=MONITORING_BROADCASTING_ROOM, message_received_callback=self.handle_message, ) def listen_forever(self) -> None: self.matrix_listener.listen_forever() def _run(self) -> None: # pylint: disable=method-hidden register_error_handler() try: self.matrix_listener.start() except ConnectionError as exc: log.critical("Could not connect to broadcasting system.", exc=exc) sys.exit(1) def stop(self) -> None: self.matrix_listener.stop() self.matrix_listener.join() def handle_message(self, message: Message) -> None: if isinstance(message, RequestMonitoring): self.on_monitor_request(message) else: log.debug("Ignoring message", message=message) def on_monitor_request(self, request_monitoring: RequestMonitoring) -> None: assert isinstance(request_monitoring, RequestMonitoring) assert request_monitoring.non_closing_signature is not None assert request_monitoring.reward_proof_signature is not None # Convert Raiden's RequestMonitoring object to a MonitorRequest try: monitor_request = MonitorRequest( channel_identifier=request_monitoring.balance_proof. channel_identifier, token_network_address=TokenNetworkAddress( request_monitoring.balance_proof.token_network_address), chain_id=request_monitoring.balance_proof.chain_id, balance_hash=encode_hex( request_monitoring.balance_proof.balance_hash), nonce=request_monitoring.balance_proof.nonce, additional_hash=encode_hex( request_monitoring.balance_proof.additional_hash), closing_signature=request_monitoring.balance_proof.signature, non_closing_signature=request_monitoring.non_closing_signature, reward_amount=request_monitoring.reward_amount, # FIXME: not sure why the Signature call is necessary reward_proof_signature=Signature(request_monitoring.signature), ) except InvalidSignature: log.info("Ignore MR with invalid signature", monitor_request=request_monitoring) return # Validate MR if monitor_request.chain_id != self.chain_id: log.debug("Bad chain_id", monitor_request=monitor_request, expected=self.chain_id) return # Check that received MR is newer by comparing nonces old_mr = self.state_db.get_monitor_request( token_network_address=monitor_request.token_network_address, channel_id=monitor_request.channel_identifier, non_closing_signer=monitor_request.non_closing_signer, ) if old_mr and old_mr.nonce >= monitor_request.nonce: log.debug( "New MR does not have a newer nonce.", token_network_address=monitor_request.token_network_address, channel_identifier=monitor_request.channel_identifier, received_nonce=monitor_request.nonce, known_nonce=old_mr.nonce, ) return log.info( "Received new MR", token_network_address=monitor_request.token_network_address, channel_identifier=monitor_request.channel_identifier, nonce=monitor_request.nonce, signer=monitor_request.signer, non_closing_signer=monitor_request.non_closing_signer, reward_signer=monitor_request.reward_proof_signer, reward_amount=monitor_request.reward_amount, ) with self.state_db.conn: self.state_db.upsert_monitor_request(monitor_request)
class PathfindingService(gevent.Greenlet): def __init__( self, web3: Web3, contract_manager: ContractManager, registry_address: Address, private_key: str, db_filename: str, user_deposit_contract_address: Address, sync_start_block: int = 0, required_confirmations: int = 8, poll_interval: int = 10, service_fee: int = 0, ): """ Creates a new pathfinding service Args: contract_manager: A contract manager token_network_listener: A blockchain listener object token_network_registry_listener: A blockchain listener object for the network registry chain_id: The id of the chain the PFS runs on """ super().__init__() self.web3 = web3 self.contract_manager = contract_manager self.registry_address = registry_address self.sync_start_block = sync_start_block self.required_confirmations = required_confirmations self.poll_interval = poll_interval self.chain_id = int(web3.net.version) self.private_key = private_key self.address = private_key_to_address(private_key) self.service_fee = service_fee self.is_running = gevent.event.Event() self.token_networks: Dict[Address, TokenNetwork] = {} self.token_network_listeners: List[BlockchainListener] = [] self.database = PFSDatabase( filename=db_filename, pfs_address=self.address, ) self.user_deposit_contract = web3.eth.contract( abi=self.contract_manager.get_contract_abi( CONTRACT_USER_DEPOSIT, ), address=user_deposit_contract_address, ) log.info( 'Starting TokenNetworkRegistry Listener', required_confirmations=self.required_confirmations, ) self.token_network_registry_listener = BlockchainListener( web3=web3, contract_manager=self.contract_manager, contract_name=CONTRACT_TOKEN_NETWORK_REGISTRY, contract_address=self.registry_address, required_confirmations=self.required_confirmations, poll_interval=self.poll_interval, sync_start_block=self.sync_start_block, ) log.info( 'Listening to token network registry', registry_address=registry_address, start_block=sync_start_block, ) self._setup_token_networks() try: self.matrix_listener = MatrixListener( private_key=private_key, chain_id=self.chain_id, callback=self.handle_message, service_room_suffix=PATH_FINDING_BROADCASTING_ROOM, ) except ConnectionError as e: log.critical( 'Could not connect to broadcasting system.', exc=e, ) sys.exit(1) def _setup_token_networks(self): self.token_network_registry_listener.add_confirmed_listener( create_registry_event_topics(self.contract_manager), self.handle_token_network_created, ) def _run(self): register_error_handler(error_handler) self.matrix_listener.start() self.token_network_registry_listener.start() self.is_running.wait() def stop(self): self.token_network_registry_listener.stop() for task in self.token_network_listeners: task.stop() self.matrix_listener.stop() self.is_running.set() self.matrix_listener.join() def follows_token_network(self, token_network_address: Address) -> bool: """ Checks if a token network is followed by the pathfinding service. """ assert is_checksum_address(token_network_address) return token_network_address in self.token_networks.keys() def _get_token_network(self, token_network_address: Address) -> Optional[TokenNetwork]: """ Returns the `TokenNetwork` for the given address or `None` for unknown networks. """ assert is_checksum_address(token_network_address) if not self.follows_token_network(token_network_address): return None else: return self.token_networks[token_network_address] def handle_channel_event(self, event: Dict): event_name = event['event'] if event_name == ChannelEvent.OPENED: self.handle_channel_opened(event) elif event_name == ChannelEvent.DEPOSIT: self.handle_channel_new_deposit(event) elif event_name == ChannelEvent.CLOSED: self.handle_channel_closed(event) else: log.debug('Unhandled event', evt=event) def handle_channel_opened(self, event: Dict): token_network = self._get_token_network(event['address']) if token_network is None: return channel_identifier = event['args']['channel_identifier'] participant1 = event['args']['participant1'] participant2 = event['args']['participant2'] settle_timeout = event['args']['settle_timeout'] log.info( 'Received ChannelOpened event', token_network_address=token_network.address, channel_identifier=channel_identifier, participant1=participant1, participant2=participant2, settle_timeout=settle_timeout, ) token_network.handle_channel_opened_event( channel_identifier, participant1, participant2, settle_timeout, ) def handle_channel_new_deposit(self, event: Dict): token_network = self._get_token_network(event['address']) if token_network is None: return channel_identifier = event['args']['channel_identifier'] participant_address = event['args']['participant'] total_deposit = event['args']['total_deposit'] log.info( 'Received ChannelNewDeposit event', token_network_address=token_network.address, channel_identifier=channel_identifier, participant=participant_address, total_deposit=total_deposit, ) token_network.handle_channel_new_deposit_event( channel_identifier, participant_address, total_deposit, ) def handle_channel_closed(self, event: Dict): token_network = self._get_token_network(event['address']) if token_network is None: return channel_identifier = event['args']['channel_identifier'] log.info( 'Received ChannelClosed event', token_network_address=token_network.address, channel_identifier=channel_identifier, ) token_network.handle_channel_closed_event(channel_identifier) def handle_token_network_created(self, event): token_network_address = event['args']['token_network_address'] token_address = event['args']['token_address'] event_block_number = event['blockNumber'] assert is_checksum_address(token_network_address) assert is_checksum_address(token_address) if not self.follows_token_network(token_network_address): log.info( 'Found new token network', token_address=token_address, token_network_address=token_network_address, ) self.create_token_network_for_address( token_network_address, token_address, event_block_number, ) def create_token_network_for_address( self, token_network_address: Address, token_address: Address, block_number: int = 0, ): token_network = TokenNetwork(token_network_address, token_address) self.token_networks[token_network_address] = token_network log.debug('Creating token network model', token_network_address=token_network_address) token_network_listener = BlockchainListener( web3=self.web3, contract_manager=self.contract_manager, contract_address=token_network_address, contract_name=CONTRACT_TOKEN_NETWORK, required_confirmations=self.required_confirmations, poll_interval=self.poll_interval, sync_start_block=block_number, ) # subscribe to event notifications from blockchain listener token_network_listener.add_confirmed_listener( create_channel_event_topics(), self.handle_channel_event, ) token_network_listener.start() self.token_network_listeners.append(token_network_listener) def handle_message(self, message: SignedMessage): if isinstance(message, UpdatePFS): try: self.on_pfs_update(message) except InvalidCapacityUpdate as x: log.info( str(x), chain_id=message.canonical_identifier.chain_identifier, token_network_address=message.canonical_identifier.token_network_address, channel_identifier=message.canonical_identifier.channel_identifier, updating_capacity=message.updating_capacity, other_capacity=message.updating_capacity, ) else: log.info('Ignoring unknown message type') def on_pfs_update(self, message: UpdatePFS): token_network_address = to_checksum_address( message.canonical_identifier.token_network_address, ) log.info( 'Received Capacity Update', token_network_address=token_network_address, channel_identifier=message.canonical_identifier.channel_identifier, ) assert is_checksum_address(message.updating_participant) assert is_checksum_address(message.other_participant) # check if chain_id matches if message.canonical_identifier.chain_identifier != self.chain_id: raise InvalidCapacityUpdate('Received Capacity Update with unknown chain identifier') # check if token network exists token_network = self._get_token_network(token_network_address) if token_network is None: raise InvalidCapacityUpdate('Received Capacity Update with unknown token network') # check if channel exists channel_identifier = message.canonical_identifier.channel_identifier if channel_identifier not in token_network.channel_id_to_addresses: raise InvalidCapacityUpdate( 'Received Capacity Update with unknown channel identifier in token network', ) # TODO: check signature of message # check values < max int 256 if message.updating_capacity > UINT256_MAX: raise InvalidCapacityUpdate( 'Received Capacity Update with impossible updating_capacity', ) if message.other_capacity > UINT256_MAX: raise InvalidCapacityUpdate( 'Received Capacity Update with impossible other_capacity', ) # check if participants fit to channel id participants = token_network.channel_id_to_addresses[channel_identifier] if message.updating_participant not in participants: raise InvalidCapacityUpdate( 'Sender of Capacity Update does not match the internal channel', ) if message.other_participant not in participants: raise InvalidCapacityUpdate( 'Other Participant of Capacity Update does not match the internal channel', ) # check if nonce is higher than current nonce view_to_partner, view_from_partner = token_network.get_channel_views_for_partner( channel_identifier=channel_identifier, updating_participant=message.updating_participant, other_participant=message.other_participant, ) valid_nonces = ( message.updating_nonce <= view_to_partner.update_nonce and message.other_nonce <= view_from_partner.update_nonce ) if valid_nonces: raise InvalidCapacityUpdate('Capacity Update already received') token_network.handle_channel_balance_update_message( channel_identifier=message.canonical_identifier.channel_identifier, updating_participant=message.updating_participant, other_participant=message.other_participant, updating_nonce=message.updating_nonce, other_nonce=message.other_nonce, updating_capacity=message.updating_capacity, other_capacity=message.other_capacity, reveal_timeout=message.reveal_timeout, )
class RequestCollector(gevent.Greenlet): def __init__(self, private_key: str, state_db: SharedDatabase, matrix_servers: List[str] = None): super().__init__() self.private_key = private_key self.state_db = state_db state = self.state_db.load_state() self.chain_id = state.blockchain_state.chain_id self.matrix_listener = MatrixListener( private_key=private_key, chain_id=self.chain_id, service_room_suffix=MONITORING_BROADCASTING_ROOM, message_received_callback=self.handle_message, servers=matrix_servers, ) def listen_forever(self) -> None: self.matrix_listener.listen_forever() def _run(self) -> None: # pylint: disable=method-hidden register_error_handler() try: self.matrix_listener.start() self.matrix_listener.startup_finished.wait( timeout=MATRIX_START_TIMEOUT) except (Timeout, ConnectionError) as exc: log.critical("Could not connect to broadcasting system.", exc=exc) sys.exit(1) def stop(self) -> None: self.matrix_listener.stop() self.matrix_listener.join() def handle_message(self, message: Message) -> None: try: if isinstance(message, RequestMonitoring): self.on_monitor_request(message) else: log.debug("Ignoring message", message=message) # add more advanced exception catching except AssertionError as ex: log.error("Error while handling message", message=message, _exc=ex) def on_monitor_request(self, request_monitoring: RequestMonitoring) -> None: assert isinstance(request_monitoring, RequestMonitoring) assert request_monitoring.non_closing_signature is not None assert request_monitoring.reward_proof_signature is not None # Convert Raiden's RequestMonitoring object to a MonitorRequest try: monitor_request = MonitorRequest( channel_identifier=request_monitoring.balance_proof. channel_identifier, token_network_address=TokenNetworkAddress( request_monitoring.balance_proof.token_network_address), chain_id=request_monitoring.balance_proof.chain_id, balance_hash=encode_hex( request_monitoring.balance_proof.balance_hash), nonce=request_monitoring.balance_proof.nonce, additional_hash=encode_hex( request_monitoring.balance_proof.additional_hash), closing_signature=request_monitoring.balance_proof.signature, non_closing_signature=request_monitoring.non_closing_signature, reward_amount=request_monitoring.reward_amount, non_closing_participant=request_monitoring. non_closing_participant, reward_proof_signature=request_monitoring.signature, msc_address=request_monitoring. monitoring_service_contract_address, ) except InvalidSignature: log.info("Ignore MR with invalid signature", monitor_request=request_monitoring) return # Validate MR if monitor_request.chain_id != self.chain_id: log.debug("Bad chain_id", monitor_request=monitor_request, expected=self.chain_id) return if monitor_request.non_closing_signer != monitor_request.non_closing_participant: log.info("MR not signed by non_closing_participant", monitor_request=monitor_request) return if monitor_request.non_closing_signer != monitor_request.reward_proof_signer: log.debug("The two MR signatures don't match", monitor_request=monitor_request) return # Ignore MRs for channels that are already closed for a while. # We need to do this to prevent clients from wasting the MS' gas by # updating the BP after the MS has already called `monitor`, see # https://github.com/raiden-network/raiden-services/issues/504. close_age = self.state_db.channel_close_age( token_network_address=monitor_request.token_network_address, channel_id=monitor_request.channel_identifier, ) # This is x blocks after that event is already confirmed, so that should be plenty! if close_age is not None and close_age >= CHANNEL_CLOSE_MARGIN: log.warning( "Ignore MR for long closed channel", monitor_request=monitor_request, close_age=close_age, ) return # Check that received MR is newer by comparing nonces old_mr = self.state_db.get_monitor_request( token_network_address=monitor_request.token_network_address, channel_id=monitor_request.channel_identifier, non_closing_signer=monitor_request.non_closing_signer, ) if old_mr and old_mr.nonce >= monitor_request.nonce: log.debug( "New MR does not have a newer nonce.", token_network_address=monitor_request.token_network_address, channel_identifier=monitor_request.channel_identifier, received_nonce=monitor_request.nonce, known_nonce=old_mr.nonce, ) return log.info( "Received new MR", token_network_address=monitor_request.token_network_address, channel_identifier=monitor_request.channel_identifier, nonce=monitor_request.nonce, signer=monitor_request.signer, non_closing_signer=monitor_request.non_closing_signer, reward_signer=monitor_request.reward_proof_signer, reward_amount=monitor_request.reward_amount, ) self.state_db.upsert_monitor_request(monitor_request)
class PathfindingService(gevent.Greenlet): def __init__( self, web3: Web3, contracts: Dict[str, Contract], private_key: str, db_filename: str, sync_start_block: BlockNumber = BlockNumber(0), required_confirmations: int = 8, poll_interval: float = 10, service_fee: int = 0, ): super().__init__() self.web3 = web3 self.registry_address = contracts[CONTRACT_TOKEN_NETWORK_REGISTRY].address self.sync_start_block = sync_start_block self.required_confirmations = required_confirmations self.poll_interval = poll_interval self.chain_id = ChainID(int(web3.net.version)) self.private_key = private_key self.address = private_key_to_address(private_key) self.service_fee = service_fee self.is_running = gevent.event.Event() self.token_networks: Dict[TokenNetworkAddress, TokenNetwork] = {} self.database = PFSDatabase(filename=db_filename, pfs_address=self.address) self.user_deposit_contract = contracts[CONTRACT_USER_DEPOSIT] self.last_known_block = 0 self.blockchain_state = BlockchainState( chain_id=self.chain_id, token_network_registry_address=self.registry_address, monitor_contract_address=Address(''), # FIXME latest_known_block=self.sync_start_block, token_network_addresses=[], ) log.info( 'Listening to token network registry', registry_address=self.registry_address, start_block=sync_start_block, ) try: self.matrix_listener = MatrixListener( private_key=private_key, chain_id=self.chain_id, callback=self.handle_message, service_room_suffix=PATH_FINDING_BROADCASTING_ROOM, ) except ConnectionError as e: log.critical('Could not connect to broadcasting system.', exc=e) sys.exit(1) def _run(self) -> None: # pylint: disable=method-hidden register_error_handler(error_handler) self.matrix_listener.start() while not self.is_running.is_set(): last_confirmed_block = self.web3.eth.blockNumber - self.required_confirmations max_query_interval_end_block = ( self.blockchain_state.latest_known_block + MAX_FILTER_INTERVAL ) # Limit the max number of blocks that is processed per iteration last_block = min(last_confirmed_block, max_query_interval_end_block) self._process_new_blocks(last_block) try: gevent.sleep(self.poll_interval) except KeyboardInterrupt: log.info('Shutting down') sys.exit(0) def _process_new_blocks(self, last_block: BlockNumber) -> None: self.last_known_block = last_block # BCL return a new state and events related to channel lifecycle new_chain_state, events = get_blockchain_events( web3=self.web3, contract_manager=CONTRACT_MANAGER, chain_state=self.blockchain_state, to_block=last_block, query_ms=False, ) # If a new token network was found we need to write it to the DB, otherwise # the constraints for new channels will not be constrained. But only update # the network addresses here, all else is done later. token_networks_changed = ( self.blockchain_state.token_network_addresses != new_chain_state.token_network_addresses ) if token_networks_changed: self.blockchain_state.token_network_addresses = new_chain_state.token_network_addresses # self.context.db.update_state(self.context.ms_state) # Now set the updated chain state to the context, will be stored later self.blockchain_state = new_chain_state for event in events: self.handle_channel_event(event) self.blockchain_state.latest_known_block = last_block def stop(self) -> None: self.matrix_listener.stop() self.is_running.set() self.matrix_listener.join() def follows_token_network(self, token_network_address: TokenNetworkAddress) -> bool: """ Checks if a token network is followed by the pathfinding service. """ return token_network_address in self.token_networks.keys() def get_token_network( self, token_network_address: TokenNetworkAddress ) -> Optional[TokenNetwork]: """ Returns the `TokenNetwork` for the given address or `None` for unknown networks. """ return self.token_networks.get(token_network_address) def handle_channel_event(self, event: Event) -> None: if isinstance(event, ReceiveTokenNetworkCreatedEvent): self.handle_token_network_created(event) elif isinstance(event, ReceiveChannelOpenedEvent): self.handle_channel_opened(event) elif isinstance(event, ReceiveChannelNewDepositEvent): self.handle_channel_new_deposit(event) elif isinstance(event, ReceiveChannelClosedEvent): self.handle_channel_closed(event) else: log.debug('Unhandled event', evt=event) def handle_token_network_created(self, event: ReceiveTokenNetworkCreatedEvent) -> None: network_address = TokenNetworkAddress(event.token_network_address) if not self.follows_token_network(network_address): log.info('Found new token network', **asdict(event)) self.token_networks[network_address] = TokenNetwork(network_address) def handle_channel_opened(self, event: ReceiveChannelOpenedEvent) -> None: token_network = self.get_token_network(event.token_network_address) if token_network is None: return log.info('Received ChannelOpened event', **asdict(event)) token_network.handle_channel_opened_event( channel_identifier=event.channel_identifier, participant1=event.participant1, participant2=event.participant2, settle_timeout=event.settle_timeout, ) def handle_channel_new_deposit(self, event: ReceiveChannelNewDepositEvent) -> None: token_network = self.get_token_network(event.token_network_address) if token_network is None: return log.info('Received ChannelNewDeposit event', **asdict(event)) token_network.handle_channel_new_deposit_event( channel_identifier=event.channel_identifier, receiver=event.participant_address, total_deposit=event.total_deposit, ) def handle_channel_closed(self, event: ReceiveChannelClosedEvent) -> None: token_network = self.get_token_network(event.token_network_address) if token_network is None: return log.info('Received ChannelClosed event', **asdict(event)) token_network.handle_channel_closed_event(channel_identifier=event.channel_identifier) def handle_message(self, message: SignedMessage) -> None: if isinstance(message, UpdatePFS): try: self.on_pfs_update(message) except InvalidCapacityUpdate as x: log.info(str(x), **message.to_dict()) else: log.info('Ignoring unknown message type') def on_pfs_update(self, message: UpdatePFS) -> None: token_network_address = to_checksum_address( message.canonical_identifier.token_network_address ) updating_participant = to_checksum_address(message.updating_participant) other_participant = to_checksum_address(message.other_participant) # check if chain_id matches if message.canonical_identifier.chain_identifier != self.chain_id: raise InvalidCapacityUpdate('Received Capacity Update with unknown chain identifier') # check if token network exists token_network = self.get_token_network(token_network_address) if token_network is None: raise InvalidCapacityUpdate('Received Capacity Update with unknown token network') # check if channel exists channel_identifier = message.canonical_identifier.channel_identifier if channel_identifier not in token_network.channel_id_to_addresses: raise InvalidCapacityUpdate( 'Received Capacity Update with unknown channel identifier in token network' ) # check values < max int 256 if message.updating_capacity > UINT256_MAX: raise InvalidCapacityUpdate( 'Received Capacity Update with impossible updating_capacity' ) if message.other_capacity > UINT256_MAX: raise InvalidCapacityUpdate('Received Capacity Update with impossible other_capacity') # check if participants fit to channel id participants = token_network.channel_id_to_addresses[channel_identifier] if updating_participant not in participants: raise InvalidCapacityUpdate( 'Sender of Capacity Update does not match the internal channel' ) if other_participant not in participants: raise InvalidCapacityUpdate( 'Other Participant of Capacity Update does not match the internal channel' ) # check signature of Capacity Update signer = recover_signer_from_capacity_update(message) if signer != updating_participant: raise InvalidCapacityUpdate('Capacity Update not signed correctly') # check if nonce is higher than current nonce view_to_partner, view_from_partner = token_network.get_channel_views_for_partner( channel_identifier=channel_identifier, updating_participant=updating_participant, other_participant=other_participant, ) is_nonce_pair_known = ( message.updating_nonce <= view_to_partner.update_nonce and message.other_nonce <= view_from_partner.update_nonce ) if is_nonce_pair_known: raise InvalidCapacityUpdate('Capacity Update already received') log.info('Received Capacity Update', **message.to_dict()) token_network.handle_channel_balance_update_message( channel_identifier=message.canonical_identifier.channel_identifier, updating_participant=updating_participant, other_participant=other_participant, updating_nonce=message.updating_nonce, other_nonce=message.other_nonce, updating_capacity=message.updating_capacity, other_capacity=message.other_capacity, reveal_timeout=message.reveal_timeout, mediation_fee=message.mediation_fee, )
class PathfindingService(gevent.Greenlet): # pylint: disable=too-many-instance-attributes def __init__( # pylint: disable=too-many-arguments self, web3: Web3, contracts: Dict[str, Contract], private_key: str, db_filename: str, sync_start_block: BlockNumber = BlockNumber(0), required_confirmations: int = 8, poll_interval: float = 10, ): super().__init__() self.web3 = web3 self.registry_address = contracts[CONTRACT_TOKEN_NETWORK_REGISTRY].address self.user_deposit_contract = contracts[CONTRACT_USER_DEPOSIT] self.chain_id = ChainID(int(web3.net.version)) self.address = private_key_to_address(private_key) self._required_confirmations = required_confirmations self._poll_interval = poll_interval self._is_running = gevent.event.Event() self.database = PFSDatabase( filename=db_filename, pfs_address=self.address, sync_start_block=sync_start_block, token_network_registry_address=self.registry_address, chain_id=self.chain_id, user_deposit_contract_address=self.user_deposit_contract.address, allow_create=True, ) self.token_networks = self._load_token_networks() try: self.matrix_listener = MatrixListener( private_key=private_key, chain_id=self.chain_id, callback=self.handle_message, service_room_suffix=PATH_FINDING_BROADCASTING_ROOM, ) except ConnectionError as exc: log.critical("Could not connect to broadcasting system.", exc=exc) sys.exit(1) def _load_token_networks(self) -> Dict[TokenNetworkAddress, TokenNetwork]: network_for_address = {n.address: n for n in self.database.get_token_networks()} channel_views = self.database.get_channel_views() for cv in channel_views: network_for_address[cv.token_network_address].add_channel_view(cv) return network_for_address def _run(self) -> None: # pylint: disable=method-hidden register_error_handler() self.matrix_listener.start() log.info( "Listening to token network registry", registry_address=self.registry_address, start_block=self.database.get_latest_known_block(), ) while not self._is_running.is_set(): last_confirmed_block = self.web3.eth.blockNumber - self._required_confirmations max_query_interval_end_block = ( self.database.get_latest_known_block() + MAX_FILTER_INTERVAL ) # Limit the max number of blocks that is processed per iteration last_block = min(last_confirmed_block, max_query_interval_end_block) self._process_new_blocks(last_block) try: gevent.sleep(self._poll_interval) except KeyboardInterrupt: log.info("Shutting down") sys.exit(0) def _process_new_blocks(self, last_block: BlockNumber) -> None: _, events = get_blockchain_events( web3=self.web3, contract_manager=CONTRACT_MANAGER, chain_state=BlockchainState( latest_known_block=self.database.get_latest_known_block(), token_network_addresses=list(self.token_networks.keys()), token_network_registry_address=self.registry_address, monitor_contract_address=Address(""), # FIXME chain_id=self.chain_id, ), to_block=last_block, query_ms=False, ) for event in events: self.handle_event(event) def stop(self) -> None: self.matrix_listener.stop() self._is_running.set() self.matrix_listener.join() def follows_token_network(self, token_network_address: TokenNetworkAddress) -> bool: """ Checks if a token network is followed by the pathfinding service. """ return token_network_address in self.token_networks.keys() def get_token_network( self, token_network_address: TokenNetworkAddress ) -> Optional[TokenNetwork]: """ Returns the `TokenNetwork` for the given address or `None` for unknown networks. """ return self.token_networks.get(token_network_address) def handle_event(self, event: Event) -> None: if isinstance(event, ReceiveTokenNetworkCreatedEvent): self.handle_token_network_created(event) elif isinstance(event, ReceiveChannelOpenedEvent): self.handle_channel_opened(event) elif isinstance(event, ReceiveChannelNewDepositEvent): self.handle_channel_new_deposit(event) elif isinstance(event, ReceiveChannelClosedEvent): self.handle_channel_closed(event) elif isinstance(event, UpdatedHeadBlockEvent): self.database.update_lastest_known_block(event.head_block_number) else: log.debug("Unhandled event", evt=event) def handle_token_network_created(self, event: ReceiveTokenNetworkCreatedEvent) -> None: network_address = TokenNetworkAddress(event.token_network_address) if not self.follows_token_network(network_address): log.info("Found new token network", **asdict(event)) self.token_networks[network_address] = TokenNetwork(network_address) self.database.upsert_token_network(network_address) def handle_channel_opened(self, event: ReceiveChannelOpenedEvent) -> None: token_network = self.get_token_network(event.token_network_address) if token_network is None: return log.info("Received ChannelOpened event", **asdict(event)) channel_views = token_network.handle_channel_opened_event( channel_identifier=event.channel_identifier, participant1=event.participant1, participant2=event.participant2, settle_timeout=event.settle_timeout, ) for cv in channel_views: self.database.upsert_channel_view(cv) def handle_channel_new_deposit(self, event: ReceiveChannelNewDepositEvent) -> None: token_network = self.get_token_network(event.token_network_address) if token_network is None: return log.info("Received ChannelNewDeposit event", **asdict(event)) channel_view = token_network.handle_channel_new_deposit_event( channel_identifier=event.channel_identifier, receiver=event.participant_address, total_deposit=event.total_deposit, ) if channel_view: self.database.upsert_channel_view(channel_view) def handle_channel_closed(self, event: ReceiveChannelClosedEvent) -> None: token_network = self.get_token_network(event.token_network_address) if token_network is None: return log.info("Received ChannelClosed event", **asdict(event)) token_network.handle_channel_closed_event(channel_identifier=event.channel_identifier) self.database.delete_channel_views(event.channel_identifier) def handle_message(self, message: SignedMessage) -> None: if isinstance(message, UpdatePFS): try: self.on_pfs_update(message) except InvalidCapacityUpdate as x: log.info(str(x), **message.to_dict()) else: log.info("Ignoring unknown message type") def _validate_pfs_update(self, message: UpdatePFS) -> TokenNetwork: token_network_address = to_checksum_address( message.canonical_identifier.token_network_address ) updating_participant = to_checksum_address(message.updating_participant) other_participant = to_checksum_address(message.other_participant) # check if chain_id matches if message.canonical_identifier.chain_identifier != self.chain_id: raise InvalidCapacityUpdate("Received Capacity Update with unknown chain identifier") # check if token network exists token_network = self.get_token_network(token_network_address) if token_network is None: raise InvalidCapacityUpdate("Received Capacity Update with unknown token network") # check if channel exists channel_identifier = message.canonical_identifier.channel_identifier if channel_identifier not in token_network.channel_id_to_addresses: raise InvalidCapacityUpdate( "Received Capacity Update with unknown channel identifier in token network" ) # check values < max int 256 if message.updating_capacity > UINT256_MAX: raise InvalidCapacityUpdate( "Received Capacity Update with impossible updating_capacity" ) if message.other_capacity > UINT256_MAX: raise InvalidCapacityUpdate("Received Capacity Update with impossible other_capacity") # check if participants fit to channel id participants = token_network.channel_id_to_addresses[channel_identifier] if updating_participant not in participants: raise InvalidCapacityUpdate( "Sender of Capacity Update does not match the internal channel" ) if other_participant not in participants: raise InvalidCapacityUpdate( "Other Participant of Capacity Update does not match the internal channel" ) # check signature of Capacity Update signer = to_checksum_address(message.sender) # recover address from signature if not is_same_address(signer, updating_participant): raise InvalidCapacityUpdate("Capacity Update not signed correctly") # check if nonce is higher than current nonce view_to_partner, view_from_partner = token_network.get_channel_views_for_partner( channel_identifier=channel_identifier, updating_participant=updating_participant, other_participant=other_participant, ) is_nonce_pair_known = ( message.updating_nonce <= view_to_partner.update_nonce and message.other_nonce <= view_from_partner.update_nonce ) if is_nonce_pair_known: raise InvalidCapacityUpdate("Capacity Update already received") return token_network def on_pfs_update(self, message: UpdatePFS) -> None: token_network = self._validate_pfs_update(message) log.info("Received Capacity Update", **message.to_dict()) token_network.handle_channel_balance_update_message(message)
class RequestCollector(gevent.Greenlet): def __init__( self, private_key: str, state_db: SharedDatabase, ): super().__init__() self.private_key = private_key self.state_db = state_db state = self.state_db.load_state() try: self.matrix_listener = MatrixListener( private_key=private_key, chain_id=state.blockchain_state.chain_id, callback=self.handle_message, service_room_suffix=MONITORING_BROADCASTING_ROOM, ) except ConnectionError as e: log.critical( 'Could not connect to broadcasting system.', exc=e, ) sys.exit(1) def listen_forever(self): self.matrix_listener.listen_forever() def _run(self): register_error_handler(error_handler) self.matrix_listener.start() def stop(self): self.matrix_listener.stop() self.matrix_listener.join() def handle_message(self, message: SignedMessage): if isinstance(message, RequestMonitoring): self.on_monitor_request(message) else: log.info('Ignoring unknown message type') def on_monitor_request( self, request_monitoring: RequestMonitoring, ): assert isinstance(request_monitoring, RequestMonitoring) # Convert Raiden's RequestMonitoring object to a MonitorRequest try: monitor_request = MonitorRequest( channel_identifier=request_monitoring.balance_proof. channel_identifier, token_network_address=to_checksum_address( request_monitoring.balance_proof.token_network_address, ), chain_id=request_monitoring.balance_proof.chain_id, balance_hash=encode_hex( request_monitoring.balance_proof.balance_hash), nonce=request_monitoring.balance_proof.nonce, additional_hash=encode_hex( request_monitoring.balance_proof.additional_hash), closing_signature=encode_hex( request_monitoring.balance_proof.signature), non_closing_signature=encode_hex( request_monitoring.non_closing_signature), reward_amount=request_monitoring.reward_amount, reward_proof_signature=encode_hex( request_monitoring.signature), ) except InvalidSignature: log.info( 'Ignore MR with invalid signature', monitor_request=request_monitoring, ) return # Check that received MR is newer by comparing nonces old_mr = self.state_db.get_monitor_request( token_network_address=monitor_request.token_network_address, channel_id=monitor_request.channel_identifier, non_closing_signer=monitor_request.non_closing_signer, ) if old_mr and old_mr.nonce >= monitor_request.nonce: log.debug( 'New MR does not have a newer nonce.', token_network_address=monitor_request.token_network_address, channel_identifier=monitor_request.channel_identifier, received_nonce=monitor_request.nonce, known_nonce=old_mr.nonce, ) return log.info( 'Received new MR', token_network_address=monitor_request.token_network_address, channel_identifier=monitor_request.channel_identifier, nonce=monitor_request.nonce, signer=monitor_request.signer, non_closing_signer=monitor_request.non_closing_signer, reward_signer=monitor_request.reward_proof_signer, reward_amount=monitor_request.reward_amount, ) with self.state_db.conn: self.state_db.upsert_monitor_request(monitor_request)