def __init__( # pylint: disable=too-many-arguments self, web3: Web3, contracts: Dict[str, Contract], private_key: PrivateKey, db_filename: str, sync_start_block: BlockNumber, required_confirmations: BlockTimeout, poll_interval: float, matrix_servers: Optional[List[str]] = None, ): super().__init__() self.web3 = web3 self.registry_address = contracts[ CONTRACT_TOKEN_NETWORK_REGISTRY].address self.user_deposit_contract = contracts[CONTRACT_USER_DEPOSIT] self.service_token_address = self.user_deposit_contract.functions.token( ).call() self.chain_id = ChainID(web3.eth.chainId) 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.database = PFSDatabase( filename=db_filename, pfs_address=self.address, sync_start_block=sync_start_block, token_network_registry_address=to_canonical_address( self.registry_address), chain_id=self.chain_id, user_deposit_contract_address=to_canonical_address( self.user_deposit_contract.address), allow_create=True, ) self.blockchain_state = BlockchainState( latest_committed_block=self.database.get_latest_committed_block(), token_network_registry_address=to_canonical_address( self.registry_address), chain_id=self.chain_id, ) self.matrix_listener = MatrixListener( private_key=private_key, chain_id=self.chain_id, device_id=DeviceIDs.PFS, message_received_callback=self.handle_message, servers=matrix_servers, ) self.token_networks = self._load_token_networks() self.updated = gevent.event.Event( ) # set whenever blocks are processed self.startup_finished = gevent.event.AsyncResult() self._init_metrics()
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, matrix_servers: List[str] = None, ): 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_commited_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, servers=matrix_servers, ) self.address_to_reachability: Dict[Address, AddressReachability] = dict() self.token_networks = self._load_token_networks()
def main( private_key: str, state_db: str, web3: Web3, contracts: Dict[str, Contract], start_block: BlockNumber, rdn_per_eth: float, expires_within: BlockNumber, ) -> None: pfs_address = private_key_to_address(private_key) chain_id = ChainID(web3.eth.chainId) database = PFSDatabase(filename=state_db, chain_id=chain_id, pfs_address=pfs_address, sync_start_block=start_block) claim_cost_rdn = calc_claim_cost_rdn(web3, rdn_per_eth) ious = list( get_claimable_ious( database, expires_after=web3.eth.blockNumber, expires_before=BlockNumber(web3.eth.blockNumber + expires_within), claim_cost_rdn=claim_cost_rdn, )) print(f"Found {len(ious)} claimable IOUs") _, failures = claim_ious(ious, claim_cost_rdn, contracts[CONTRACT_ONE_TO_N], web3, database) if failures: sys.exit(1)
def main( private_key: str, state_db: str, web3: Web3, contracts: Dict[str, Contract], start_block: BlockNumber, rdn_per_eth: float, expires_within: BlockNumber, ) -> None: pfs_address = private_key_to_address(private_key) chain_id = ChainID(int(web3.net.version)) database = PFSDatabase(filename=state_db, chain_id=chain_id, pfs_address=pfs_address) one_to_n_contract = contracts[CONTRACT_ONE_TO_N] claim_cost_eth = 90897 claim_cost_rdn = TokenAmount(int(claim_cost_eth / rdn_per_eth)) ious = list( get_claimable_ious( database, expires_before=web3.eth.blockNumber + expires_within, claim_cost_rdn=claim_cost_rdn, )) print(f"Found {len(ious)} claimable IOUs") _, failures = claim_ious(ious, claim_cost_rdn, one_to_n_contract, web3, database) if failures: sys.exit(1)
def get_claimable_ious( database: PFSDatabase, expires_before: BlockNumber, claim_cost_rdn: TokenAmount ) -> Iterable[IOU]: ious = database.get_ious( claimed=False, expires_before=expires_before, amount_at_least=claim_cost_rdn ) return ious
def claim_ious( ious: Iterable[IOU], claim_cost_rdn: TokenAmount, one_to_n_contract: Contract, web3: Web3, database: PFSDatabase, ) -> Tuple[int, int]: unchecked_txs = [] skipped = 0 for iou in ious: claim = one_to_n_contract.functions.claim( sender=iou.sender, receiver=iou.receiver, amount=iou.amount, expiration_block=iou.expiration_block, one_to_n_address=iou.one_to_n_address, signature=iou.signature, ) transferrable = claim.call() if transferrable < claim_cost_rdn: print("Not enough user deposit to claim profitably for", iou) skipped += 1 continue tx_hash = claim.transact() unchecked_txs.append((tx_hash, iou)) failures = 0 while unchecked_txs: for tx_hash, iou in unchecked_txs: try: receipt = web3.eth.getTransactionReceipt(tx_hash) except TransactionNotFound: continue unchecked_txs.remove((tx_hash, iou)) if receipt["status"] == 1: print(f"Successfully claimed {iou}.") iou.claimed = True database.upsert_iou(iou) else: print(f"Claiming {iou} failed!") failures += 1 return skipped, failures
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 get_claimable_ious( database: PFSDatabase, claimable_until_after: Timestamp, claimable_until_before: Timestamp, claim_cost_rdn: TokenAmount, ) -> Iterable[IOU]: return database.get_ious( claimed=False, claimable_until_after=claimable_until_after, claimable_until_before=claimable_until_before, amount_at_least=claim_cost_rdn, )
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)
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 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: PrivateKey, db_filename: str, sync_start_block: BlockNumber, required_confirmations: BlockTimeout, poll_interval: float, matrix_servers: Optional[List[str]] = None, ): super().__init__() self.web3 = web3 self.registry_address = contracts[ CONTRACT_TOKEN_NETWORK_REGISTRY].address self.user_deposit_contract = contracts[CONTRACT_USER_DEPOSIT] self.service_token_address = self.user_deposit_contract.functions.token( ).call() self.chain_id = ChainID(web3.eth.chainId) 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.database = PFSDatabase( filename=db_filename, pfs_address=self.address, sync_start_block=sync_start_block, token_network_registry_address=to_canonical_address( self.registry_address), chain_id=self.chain_id, user_deposit_contract_address=to_canonical_address( self.user_deposit_contract.address), allow_create=True, ) self.blockchain_state = BlockchainState( latest_committed_block=self.database.get_latest_committed_block(), token_network_registry_address=to_canonical_address( self.registry_address), chain_id=self.chain_id, ) self.matrix_listener = MatrixListener( private_key=private_key, chain_id=self.chain_id, device_id=DeviceIDs.PFS, message_received_callback=self.handle_message, servers=matrix_servers, ) self.token_networks = self._load_token_networks() self.updated = gevent.event.Event( ) # set whenever blocks are processed self.startup_finished = gevent.event.AsyncResult() self._init_metrics() def _init_metrics(self) -> None: def _get_number_of_claimed_ious() -> float: return float(self.database.get_nof_claimed_ious()) def _get_total_amount_of_claimed_ious() -> float: return float(sum(iou.amount for iou in self._iter_claimed_ious())) metrics.get_metrics_for_label( metrics.IOU_CLAIMS, metrics.IouStatus.SUCCESSFUL).set_function( _get_number_of_claimed_ious) metrics.get_metrics_for_label( metrics.IOU_CLAIMS_TOKEN, metrics.IouStatus.SUCCESSFUL).set_function( _get_total_amount_of_claimed_ious) def _iter_claimed_ious(self) -> Iterator[IOU]: return self.database.get_ious(claimed=True) def _load_token_networks(self) -> Dict[TokenNetworkAddress, TokenNetwork]: network_for_address = { n.address: n for n in self.database.get_token_networks() } for channel in self.database.get_channels(): 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 try: self.matrix_listener.start() except (Timeout, ConnectionError) as exc: log.critical("Could not connect to broadcasting system.", exc=exc) sys.exit(1) self.matrix_listener.link(self.startup_finished) try: self.matrix_listener.startup_finished.get( timeout=MATRIX_START_TIMEOUT) except Timeout: raise Exception("MatrixListener did not start in time.") self.startup_finished.set() log.info( "Listening to token network registry", registry_address=self.registry_address, start_block=self.database.get_latest_committed_block(), ) while not self._is_running.is_set(): self._process_new_blocks( BlockNumber(self.web3.eth.blockNumber - self.required_confirmations)) # Let tests waiting for this event know that we're done with processing self.updated.set() self.updated.clear() # Sleep, then collect errors from greenlets gevent.sleep(self._poll_interval) gevent.joinall({self.matrix_listener}, timeout=0, raise_error=True) def _process_new_blocks(self, latest_confirmed_block: BlockNumber) -> None: start = time.monotonic() db_block = self.database.get_latest_committed_block() assert db_block == self.blockchain_state.latest_committed_block, ( f"Unexpected `latest_committed_block` in db: " f"was {db_block}, expected {self.blockchain_state.latest_committed_block}. " f"Is the db accidentally shared by two PFSes?") events = get_blockchain_events_adaptive( web3=self.web3, blockchain_state=self.blockchain_state, token_network_addresses=list(self.token_networks.keys()), latest_confirmed_block=latest_confirmed_block, ) if events is None: return before_process = time.monotonic() for event in events: self.handle_event(event) gevent.idle() # Allow answering requests in between events if events: log.info( "Processed events", getting=round(before_process - start, 2), processing=round(time.monotonic() - before_process, 2), total_duration=round(time.monotonic() - start, 2), event_counts=collections.Counter(e.__class__.__name__ for e in events), ) def stop(self) -> None: self.matrix_listener.kill() 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: with sentry_sdk.configure_scope() as scope: with metrics.collect_event_metrics(event): scope.set_extra("event", event) if isinstance(event, ReceiveTokenNetworkCreatedEvent): self.handle_token_network_created(event) elif isinstance(event, ReceiveChannelOpenedEvent): self.handle_channel_opened(event) elif isinstance(event, ReceiveChannelClosedEvent): self.handle_channel_closed(event) elif isinstance(event, UpdatedHeadBlockEvent): # TODO: Store blockhash here as well self.blockchain_state.latest_committed_block = event.head_block_number self.database.update_lastest_committed_block( event.head_block_number) else: log.debug("Unhandled event", evt=event) def handle_token_network_created( self, event: ReceiveTokenNetworkCreatedEvent) -> None: network_address = 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) channel = token_network.handle_channel_opened_event( channel_identifier=event.channel_identifier, participant1=event.participant1, participant2=event.participant2, settle_timeout=event.settle_timeout, ) self.database.upsert_channel(channel) # 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): log.debug("Processing deferred message", message=message) self.handle_message(message) 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) channel_deleted = self.database.delete_channel( event.token_network_address, event.channel_identifier) if channel_deleted: token_network.handle_channel_closed_event(event.channel_identifier) else: log.error( "Received ChannelClosed event for unknown channel", token_network_address=event.token_network_address, channel_identifier=event.channel_identifier, ) metrics.get_metrics_for_label(metrics.ERRORS_LOGGED, metrics.ErrorCategory.STATE).inc() def handle_message(self, message: Message) -> None: with sentry_sdk.configure_scope() as scope: scope.set_extra("message", message) try: with metrics.collect_message_metrics(message): if isinstance(message, PFSCapacityUpdate): changed_channel: Optional[ Channel] = self.on_capacity_update(message) elif isinstance(message, PFSFeeUpdate): changed_channel = self.on_fee_update(message) else: log.debug("Ignoring message", unknown_message=message) return if changed_channel: self.database.upsert_channel(changed_channel) 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 _validate_pfs_fee_update(self, message: PFSFeeUpdate) -> TokenNetwork: # check if chain_id matches if message.canonical_identifier.chain_identifier != self.chain_id: raise InvalidFeeUpdate( "Received Fee Update with unknown chain identifier") # check if token network exists token_network = self.get_token_network( message.canonical_identifier.token_network_address) if token_network is None: raise InvalidFeeUpdate( "Received Fee Update with unknown token network") # check signature of Capacity Update if message.sender != message.updating_participant: raise InvalidFeeUpdate("Fee 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 InvalidFeeUpdate( "Sender of Fee Update does not match the internal channel") # check that timestamp has no timezone if message.timestamp.tzinfo is not None: raise InvalidFeeUpdate( "Timestamp of Fee Update should not contain timezone") return token_network def on_fee_update(self, message: PFSFeeUpdate) -> Optional[Channel]: token_network = self._validate_pfs_fee_update(message) log.debug("Received Fee Update", message=message) return token_network.handle_channel_fee_update(message) def _validate_pfs_capacity_update( self, message: PFSCapacityUpdate) -> TokenNetwork: # 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( message.canonical_identifier.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) -> Channel: token_network = self._validate_pfs_capacity_update(message) log.debug("Received Capacity Update", message=message) self.database.upsert_capacity_update(message) 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, )
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)
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)