class UDPTransport(object): """ Node communication using the UDP protocol. """ def __init__(self, host, port, socket=None, protocol=None, throttle_policy=DummyPolicy()): self.protocol = protocol if socket is not None: self.server = DatagramServer(socket, handle=self.receive) else: self.server = DatagramServer((host, port), handle=self.receive) self.host = self.server.server_host self.port = self.server.server_port self.throttle_policy = throttle_policy def receive(self, data, host_port): # pylint: disable=unused-argument self.protocol.receive(data) # enable debugging using the DummyNetwork callbacks DummyTransport.track_recv(self.protocol.raiden, host_port, data) def send(self, sender, host_port, bytes_): """ Send `bytes_` to `host_port`. Args: sender (address): The address of the running node. host_port (Tuple[(str, int)]): Tuple with the host name and port number. bytes_ (bytes): The bytes that are going to be sent through the wire. """ sleep_timeout = self.throttle_policy.consume(1) # Don't sleep if timeout is zero, otherwise a context-switch is done # and the message is delayed, increasing it's latency if sleep_timeout: gevent.sleep(sleep_timeout) if not hasattr(self.server, 'socket'): raise RuntimeError('trying to send a message on a closed server') self.server.sendto(bytes_, host_port) # enable debugging using the DummyNetwork callbacks DummyTransport.network.track_send(sender, host_port, bytes_) def stop(self): self.server.stop() def stop_accepting(self): self.server.stop_accepting() def start(self): assert not self.server.started # server.stop() clears the handle, since this may be a restart the # handle must always be set self.server.set_handle(self.receive) self.server.start()
class UDPTransport: """ Node communication using the UDP protocol. """ def __init__( self, host, port, socket=None, protocol=None, throttle_policy=DummyPolicy()): self.protocol = protocol if socket is not None: self.server = DatagramServer(socket, handle=self.receive) else: self.server = DatagramServer((host, port), handle=self.receive) self.host = self.server.server_host self.port = self.server.server_port self.throttle_policy = throttle_policy def receive(self, data, host_port): # pylint: disable=unused-argument try: self.protocol.receive(data) except InvalidProtocolMessage as e: if log.isEnabledFor(logging.WARNING): log.warning("Can't decode: {} (data={}, len={})".format(str(e), data, len(data))) return except RaidenShuttingDown: # For a clean shutdown return # enable debugging using the DummyNetwork callbacks DummyTransport.track_recv(self.protocol.raiden, host_port, data) def send(self, sender, host_port, bytes_): """ Send `bytes_` to `host_port`. Args: sender (address): The address of the running node. host_port (Tuple[(str, int)]): Tuple with the host name and port number. bytes_ (bytes): The bytes that are going to be sent through the wire. """ sleep_timeout = self.throttle_policy.consume(1) # Don't sleep if timeout is zero, otherwise a context-switch is done # and the message is delayed, increasing it's latency if sleep_timeout: gevent.sleep(sleep_timeout) if not hasattr(self.server, 'socket'): raise RuntimeError('trying to send a message on a closed server') self.server.sendto(bytes_, host_port) # enable debugging using the DummyNetwork callbacks DummyTransport.network.track_send(sender, host_port, bytes_) def stop(self): self.server.stop() # Calling `.close()` on a gevent socket doesn't actually close the underlying os socket # so we do that ourselves here. # See: https://github.com/gevent/gevent/blob/master/src/gevent/_socket2.py#L208 # and: https://groups.google.com/forum/#!msg/gevent/Ro8lRra3nH0/ZENgEXrr6M0J try: self.server._socket.close() except socket.error: pass def stop_accepting(self): self.server.stop_accepting() def start(self): assert not self.server.started # server.stop() clears the handle, since this may be a restart the # handle must always be set self.server.set_handle(self.receive) self.server.start()
class UDPTransport: UDP_MAX_MESSAGE_SIZE = 1200 def __init__(self, discovery, udpsocket, throttle_policy, config): # these values are initialized by the start method self.queueids_to_queues: typing.Dict self.raiden: RaidenService self.discovery = discovery self.config = config self.retry_interval = config['retry_interval'] self.retries_before_backoff = config['retries_before_backoff'] self.nat_keepalive_retries = config['nat_keepalive_retries'] self.nat_keepalive_timeout = config['nat_keepalive_timeout'] self.nat_invitation_timeout = config['nat_invitation_timeout'] self.event_stop = Event() self.greenlets = list() self.addresses_events = dict() self.messageids_to_asyncresults = dict() # Maps the addresses to a dict with the latest nonce (using a dict # because python integers are immutable) self.nodeaddresses_to_nonces = dict() cache = cachetools.TTLCache( maxsize=50, ttl=CACHE_TTL, ) cache_wrapper = cachetools.cached(cache=cache) self.get_host_port = cache_wrapper(discovery.get) self.throttle_policy = throttle_policy self.server = DatagramServer(udpsocket, handle=self._receive) def start( self, raiden: RaidenService, queueids_to_queues: typing.List[SendMessageEvent], ): self.raiden = raiden self.queueids_to_queues = dict() # server.stop() clears the handle. Since this may be a restart the # handle must always be set self.server.set_handle(self._receive) for (recipient, queue_name), queue in queueids_to_queues.items(): encoded_queue = list() for sendevent in queue: message = message_from_sendevent(sendevent, raiden.address) raiden.sign(message) encoded = message.encode() encoded_queue.append((encoded, sendevent.message_identifier)) self.init_queue_for(recipient, queue_name, encoded_queue) self.server.start() def stop_and_wait(self): # Stop handling incoming packets, but don't close the socket. The # socket can only be safely closed after all outgoing tasks are stopped self.server.stop_accepting() # Stop processing the outgoing queues self.event_stop.set() gevent.wait(self.greenlets) # All outgoing tasks are stopped. Now it's safe to close the socket. At # this point there might be some incoming message being processed, # keeping the socket open is not useful for these. self.server.stop() # Calling `.close()` on a gevent socket doesn't actually close the underlying os socket # so we do that ourselves here. # See: https://github.com/gevent/gevent/blob/master/src/gevent/_socket2.py#L208 # and: https://groups.google.com/forum/#!msg/gevent/Ro8lRra3nH0/ZENgEXrr6M0J try: self.server._socket.close() # pylint: disable=protected-access except socket.error: pass # Set all the pending results to False for async_result in self.messageids_to_asyncresults.values(): async_result.set(False) def get_health_events(self, recipient): """ Starts a healthcheck task for `recipient` and returns a HealthEvents with locks to react on its current state. """ if recipient not in self.addresses_events: self.start_health_check(recipient) return self.addresses_events[recipient] def start_health_check(self, recipient): """ Starts a task for healthchecking `recipient` if there is not one yet. """ if recipient not in self.addresses_events: ping_nonce = self.nodeaddresses_to_nonces.setdefault( recipient, {'nonce': 0}, # HACK: Allows the task to mutate the object ) events = healthcheck.HealthEvents( event_healthy=Event(), event_unhealthy=Event(), ) self.addresses_events[recipient] = events greenlet_healthcheck = gevent.spawn( healthcheck.healthcheck, self, recipient, self.event_stop, events.event_healthy, events.event_unhealthy, self.nat_keepalive_retries, self.nat_keepalive_timeout, self.nat_invitation_timeout, ping_nonce, ) greenlet_healthcheck.name = f'Healthcheck for {pex(recipient)}' self.greenlets.append(greenlet_healthcheck) def init_queue_for( self, recipient: typing.Address, queue_name: bytes, items: typing.List[QueueItem_T], ) -> Queue_T: """ Create the queue identified by the pair `(recipient, queue_name)` and initialize it with `items`. """ queueid = (recipient, queue_name) queue = self.queueids_to_queues.get(queueid) assert queue is None queue = NotifyingQueue(items=items) self.queueids_to_queues[queueid] = queue events = self.get_health_events(recipient) greenlet_queue = gevent.spawn( single_queue_send, self, recipient, queue, self.event_stop, events.event_healthy, events.event_unhealthy, self.retries_before_backoff, self.retry_interval, self.retry_interval * 10, ) if queue_name == b'global': greenlet_queue.name = f'Queue for {pex(recipient)} - global' else: greenlet_queue.name = f'Queue for {pex(recipient)} - {pex(queue_name)}' self.greenlets.append(greenlet_queue) log.debug( 'new queue created for', node=pex(self.raiden.address), token=pex(queue_name), to=pex(recipient), ) return queue def get_queue_for( self, recipient: typing.Address, queue_name: bytes, ) -> Queue_T: """ Return the queue identified by the pair `(recipient, queue_name)`. If the queue doesn't exist it will be instantiated. """ queueid = (recipient, queue_name) queue = self.queueids_to_queues.get(queueid) if queue is None: items = () queue = self.init_queue_for(recipient, queue_name, items) return queue def send_async( self, recipient: typing.Address, queue_name: bytes, message: 'Message', ): """ Send a new ordered message to recipient. Messages that use the same `queue_name` are ordered. """ if not is_binary_address(recipient): raise ValueError('Invalid address {}'.format(pex(recipient))) # These are not protocol messages, but transport specific messages if isinstance(message, (Delivered, Ping, Pong)): raise ValueError('Do not use send for {} messages'.format(message.__class__.__name__)) messagedata = message.encode() if len(messagedata) > self.UDP_MAX_MESSAGE_SIZE: raise ValueError( 'message size exceeds the maximum {}'.format(self.UDP_MAX_MESSAGE_SIZE), ) # message identifiers must be unique message_id = message.message_identifier # ignore duplicates if message_id not in self.messageids_to_asyncresults: self.messageids_to_asyncresults[message_id] = AsyncResult() queue = self.get_queue_for(recipient, queue_name) queue.put((messagedata, message_id)) log.debug( 'MESSAGE QUEUED', node=pex(self.raiden.address), queue_name=queue_name, to=pex(recipient), message=message, ) def maybe_send(self, recipient: typing.Address, message: Message): """ Send message to recipient if the transport is running. """ if not is_binary_address(recipient): raise InvalidAddress('Invalid address {}'.format(pex(recipient))) messagedata = message.encode() host_port = self.get_host_port(recipient) self.maybe_sendraw(host_port, messagedata) def maybe_sendraw_with_result( self, recipient: typing.Address, messagedata: bytes, message_id: typing.MessageID, ) -> AsyncResult: """ Send message to recipient if the transport is running. Returns: An AsyncResult that will be set once the message is delivered. As long as the message has not been acknowledged with a Delivered message the function will return the same AsyncResult. """ async_result = self.messageids_to_asyncresults.get(message_id) if async_result is None: async_result = AsyncResult() self.messageids_to_asyncresults[message_id] = async_result host_port = self.get_host_port(recipient) self.maybe_sendraw(host_port, messagedata) return async_result def maybe_sendraw(self, host_port: typing.Tuple[int, int], messagedata: bytes): """ Send message to recipient if the transport is running. """ # Don't sleep if timeout is zero, otherwise a context-switch is done # and the message is delayed, increasing it's latency sleep_timeout = self.throttle_policy.consume(1) if sleep_timeout: gevent.sleep(sleep_timeout) # Check the udp socket is still available before trying to send the # message. There must be *no context-switches after this test*. if hasattr(self.server, 'socket'): self.server.sendto( messagedata, host_port, ) def _receive(self, data, host_port): # pylint: disable=unused-argument try: self.receive(data) except RaidenShuttingDown: # For a clean shutdown return def receive(self, messagedata: bytes): """ Handle an UDP packet. """ # pylint: disable=unidiomatic-typecheck if len(messagedata) > self.UDP_MAX_MESSAGE_SIZE: log.error( 'INVALID MESSAGE: Packet larger than maximum size', node=pex(self.raiden.address), message=hexlify(messagedata), length=len(messagedata), ) return message = decode(messagedata) if type(message) == Pong: self.receive_pong(message) elif type(message) == Ping: self.receive_ping(message) elif type(message) == Delivered: self.receive_delivered(message) elif message is not None: self.receive_message(message) else: log.error( 'INVALID MESSAGE: Unknown cmdid', node=pex(self.raiden.address), message=hexlify(messagedata), ) def receive_message(self, message: Message): """ Handle a Raiden protocol message. The protocol requires durability of the messages. The UDP transport relies on the node's WAL for durability. The message will be converted to a state change, saved to the WAL, and *processed* before the durability is confirmed, which is a stronger property than what is required of any transport. """ # pylint: disable=unidiomatic-typecheck if on_message(self.raiden, message): # Sending Delivered after the message is decoded and *processed* # gives a stronger guarantee than what is required from a # transport. # # Alternatives are, from weakest to strongest options: # - Just save it on disk and asynchronously process the messages # - Decode it, save to the WAL, and asynchronously process the # state change # - Decode it, save to the WAL, and process it (the current # implementation) delivered_message = Delivered(message.message_identifier) self.raiden.sign(delivered_message) self.maybe_send( message.sender, delivered_message, ) def receive_delivered(self, delivered: Delivered): """ Handle a Delivered message. The Delivered message is how the UDP transport guarantees persistence by the partner node. The message itself is not part of the raiden protocol, but it's required by this transport to provide the required properties. """ processed = ReceiveDelivered(delivered.delivered_message_identifier) self.raiden.handle_state_change(processed) message_id = delivered.delivered_message_identifier async_result = self.raiden.transport.messageids_to_asyncresults.get(message_id) # clear the async result, otherwise we have a memory leak if async_result is not None: del self.messageids_to_asyncresults[message_id] async_result.set() # Pings and Pongs are used to check the health status of another node. They # are /not/ part of the raiden protocol, only part of the UDP transport, # therefore these messages are not forwarded to the message handler. def receive_ping(self, ping: Ping): """ Handle a Ping message by answering with a Pong. """ log.debug( 'PING RECEIVED', node=pex(self.raiden.address), message_id=ping.nonce, message=ping, sender=pex(ping.sender), ) pong = Pong(ping.nonce) self.raiden.sign(pong) try: self.maybe_send(ping.sender, pong) except (InvalidAddress, UnknownAddress) as e: log.debug("Couldn't send the `Delivered` message", e=e) def receive_pong(self, pong: Pong): """ Handles a Pong message. """ message_id = ('ping', pong.nonce, pong.sender) async_result = self.messageids_to_asyncresults.get(message_id) if async_result is not None: log.debug( 'PONG RECEIVED', node=pex(self.raiden.address), sender=pex(pong.sender), message_id=pong.nonce, ) async_result.set(True) def get_ping(self, nonce: int) -> Ping: """ Returns a signed Ping message. Note: Ping messages don't have an enforced ordering, so a Ping message with a higher nonce may be acknowledged first. """ message = Ping(nonce) self.raiden.sign(message) message_data = message.encode() return message_data def set_node_network_state(self, node_address: typing.Address, node_state): state_change = ActionChangeNodeNetworkState(node_address, node_state) self.raiden.handle_state_change(state_change)
class UDPTransport(Runnable): UDP_MAX_MESSAGE_SIZE = 1200 def __init__(self, discovery, udpsocket, throttle_policy, config): super().__init__() # these values are initialized by the start method self.queueids_to_queues: typing.Dict self.raiden: RaidenService self.discovery = discovery self.config = config self.retry_interval = config['retry_interval'] self.retries_before_backoff = config['retries_before_backoff'] self.nat_keepalive_retries = config['nat_keepalive_retries'] self.nat_keepalive_timeout = config['nat_keepalive_timeout'] self.nat_invitation_timeout = config['nat_invitation_timeout'] self.event_stop = Event() self.event_stop.set() self.greenlets = list() self.addresses_events = dict() self.messageids_to_asyncresults = dict() # Maps the addresses to a dict with the latest nonce (using a dict # because python integers are immutable) self.nodeaddresses_to_nonces = dict() cache = cachetools.TTLCache( maxsize=50, ttl=CACHE_TTL, ) cache_wrapper = cachetools.cached(cache=cache) self.get_host_port = cache_wrapper(discovery.get) self.throttle_policy = throttle_policy self.server = DatagramServer(udpsocket, handle=self.receive) def start( self, raiden: RaidenService, message_handler: MessageHandler, ): if not self.event_stop.ready(): raise RuntimeError('UDPTransport started while running') self.event_stop.clear() self.raiden = raiden self.message_handler = message_handler self.queueids_to_queues = dict() # server.stop() clears the handle. Since this may be a restart the # handle must always be set self.server.set_handle(self.receive) self.server.start() super().start() def _run(self): """ Runnable main method, perform wait on long-running subtasks """ try: self.event_stop.wait() except gevent.GreenletExit: # killed without exception self.event_stop.set() gevent.killall(self.greenlets) # kill children raise # re-raise to keep killed status except Exception: self.stop() # ensure cleanup and wait on subtasks raise def stop(self): if self.event_stop.ready(): return # double call, happens on normal stop, ignore self.event_stop.set() # Stop handling incoming packets, but don't close the socket. The # socket can only be safely closed after all outgoing tasks are stopped self.server.stop_accepting() # Stop processing the outgoing queues gevent.wait(self.greenlets) # All outgoing tasks are stopped. Now it's safe to close the socket. At # this point there might be some incoming message being processed, # keeping the socket open is not useful for these. self.server.stop() # Calling `.close()` on a gevent socket doesn't actually close the underlying os socket # so we do that ourselves here. # See: https://github.com/gevent/gevent/blob/master/src/gevent/_socket2.py#L208 # and: https://groups.google.com/forum/#!msg/gevent/Ro8lRra3nH0/ZENgEXrr6M0J try: self.server._socket.close() # pylint: disable=protected-access except socket.error: pass # Set all the pending results to False for async_result in self.messageids_to_asyncresults.values(): async_result.set(False) def get_health_events(self, recipient): """ Starts a healthcheck task for `recipient` and returns a HealthEvents with locks to react on its current state. """ if recipient not in self.addresses_events: self.start_health_check(recipient) return self.addresses_events[recipient] def start_health_check(self, recipient): """ Starts a task for healthchecking `recipient` if there is not one yet. """ if recipient not in self.addresses_events: ping_nonce = self.nodeaddresses_to_nonces.setdefault( recipient, {'nonce': 0}, # HACK: Allows the task to mutate the object ) events = healthcheck.HealthEvents( event_healthy=Event(), event_unhealthy=Event(), ) self.addresses_events[recipient] = events greenlet_healthcheck = gevent.spawn( healthcheck.healthcheck, self, recipient, self.event_stop, events.event_healthy, events.event_unhealthy, self.nat_keepalive_retries, self.nat_keepalive_timeout, self.nat_invitation_timeout, ping_nonce, ) greenlet_healthcheck.name = f'Healthcheck for {pex(recipient)}' greenlet_healthcheck.link_exception(self.on_error) self.greenlets.append(greenlet_healthcheck) def init_queue_for( self, queue_identifier: QueueIdentifier, items: typing.List[QueueItem_T], ) -> Queue_T: """ Create the queue identified by the queue_identifier and initialize it with `items`. """ recipient = queue_identifier.recipient queue = self.queueids_to_queues.get(queue_identifier) assert queue is None queue = NotifyingQueue(items=items) self.queueids_to_queues[queue_identifier] = queue events = self.get_health_events(recipient) greenlet_queue = gevent.spawn( single_queue_send, self, recipient, queue, queue_identifier, self.event_stop, events.event_healthy, events.event_unhealthy, self.retries_before_backoff, self.retry_interval, self.retry_interval * 10, ) if queue_identifier.channel_identifier == CHANNEL_IDENTIFIER_GLOBAL_QUEUE: greenlet_queue.name = f'Queue for {pex(recipient)} - global' else: greenlet_queue.name = ( f'Queue for {pex(recipient)} - {queue_identifier.channel_identifier}' ) greenlet_queue.link_exception(self.on_error) self.greenlets.append(greenlet_queue) log.debug( 'new queue created for', node=pex(self.raiden.address), queue_identifier=queue_identifier, items_qty=len(items), ) return queue def get_queue_for( self, queue_identifier: QueueIdentifier, ) -> Queue_T: """ Return the queue identified by the given queue identifier. If the queue doesn't exist it will be instantiated. """ queue = self.queueids_to_queues.get(queue_identifier) if queue is None: items = () queue = self.init_queue_for(queue_identifier, items) return queue def send_async( self, queue_identifier: QueueIdentifier, message: 'Message', ): """ Send a new ordered message to recipient. Messages that use the same `queue_identifier` are ordered. """ recipient = queue_identifier.recipient if not is_binary_address(recipient): raise ValueError('Invalid address {}'.format(pex(recipient))) # These are not protocol messages, but transport specific messages if isinstance(message, (Delivered, Ping, Pong)): raise ValueError('Do not use send for {} messages'.format( message.__class__.__name__)) messagedata = message.encode() if len(messagedata) > self.UDP_MAX_MESSAGE_SIZE: raise ValueError( 'message size exceeds the maximum {}'.format( self.UDP_MAX_MESSAGE_SIZE), ) # message identifiers must be unique message_id = message.message_identifier # ignore duplicates if message_id not in self.messageids_to_asyncresults: self.messageids_to_asyncresults[message_id] = AsyncResult() queue = self.get_queue_for(queue_identifier) queue.put((messagedata, message_id)) assert queue.is_set() log.debug( 'Message queued', node=pex(self.raiden.address), queue_identifier=queue_identifier, queue_size=len(queue), message=message, ) def maybe_send(self, recipient: typing.Address, message: Message): """ Send message to recipient if the transport is running. """ if not is_binary_address(recipient): raise InvalidAddress('Invalid address {}'.format(pex(recipient))) messagedata = message.encode() host_port = self.get_host_port(recipient) self.maybe_sendraw(host_port, messagedata) def maybe_sendraw_with_result( self, recipient: typing.Address, messagedata: bytes, message_id: typing.MessageID, ) -> AsyncResult: """ Send message to recipient if the transport is running. Returns: An AsyncResult that will be set once the message is delivered. As long as the message has not been acknowledged with a Delivered message the function will return the same AsyncResult. """ async_result = self.messageids_to_asyncresults.get(message_id) if async_result is None: async_result = AsyncResult() self.messageids_to_asyncresults[message_id] = async_result host_port = self.get_host_port(recipient) self.maybe_sendraw(host_port, messagedata) return async_result def maybe_sendraw(self, host_port: typing.Tuple[int, int], messagedata: bytes): """ Send message to recipient if the transport is running. """ # Don't sleep if timeout is zero, otherwise a context-switch is done # and the message is delayed, increasing it's latency sleep_timeout = self.throttle_policy.consume(1) if sleep_timeout: gevent.sleep(sleep_timeout) # Check the udp socket is still available before trying to send the # message. There must be *no context-switches after this test*. if hasattr(self.server, 'socket'): self.server.sendto( messagedata, host_port, ) def receive( self, messagedata: bytes, host_port: typing.Tuple[str, int], # pylint: disable=unused-argument ) -> bool: """ Handle an UDP packet. """ # pylint: disable=unidiomatic-typecheck if len(messagedata) > self.UDP_MAX_MESSAGE_SIZE: log.warning( 'Invalid message: Packet larger than maximum size', node=pex(self.raiden.address), message=hexlify(messagedata), length=len(messagedata), ) return False try: message = decode(messagedata) except InvalidProtocolMessage as e: log.warning( 'Invalid protocol message', error=str(e), node=pex(self.raiden.address), message=hexlify(messagedata), ) return False if type(message) == Pong: self.receive_pong(message) elif type(message) == Ping: self.receive_ping(message) elif type(message) == Delivered: self.receive_delivered(message) elif message is not None: self.receive_message(message) else: log.warning( 'Invalid message: Unknown cmdid', node=pex(self.raiden.address), message=hexlify(messagedata), ) return False return True def receive_message(self, message: Message): """ Handle a Raiden protocol message. The protocol requires durability of the messages. The UDP transport relies on the node's WAL for durability. The message will be converted to a state change, saved to the WAL, and *processed* before the durability is confirmed, which is a stronger property than what is required of any transport. """ self.message_handler.on_message(self.raiden, message) # Sending Delivered after the message is decoded and *processed* # gives a stronger guarantee than what is required from a # transport. # # Alternatives are, from weakest to strongest options: # - Just save it on disk and asynchronously process the messages # - Decode it, save to the WAL, and asynchronously process the # state change # - Decode it, save to the WAL, and process it (the current # implementation) delivered_message = Delivered(message.message_identifier) self.raiden.sign(delivered_message) self.maybe_send( message.sender, delivered_message, ) def receive_delivered(self, delivered: Delivered): """ Handle a Delivered message. The Delivered message is how the UDP transport guarantees persistence by the partner node. The message itself is not part of the raiden protocol, but it's required by this transport to provide the required properties. """ self.message_handler.on_message(self.raiden, delivered) message_id = delivered.delivered_message_identifier async_result = self.raiden.transport.messageids_to_asyncresults.get( message_id) # clear the async result, otherwise we have a memory leak if async_result is not None: del self.messageids_to_asyncresults[message_id] async_result.set() else: log.warn( 'Unknown delivered message received', message_id=message_id, ) # Pings and Pongs are used to check the health status of another node. They # are /not/ part of the raiden protocol, only part of the UDP transport, # therefore these messages are not forwarded to the message handler. def receive_ping(self, ping: Ping): """ Handle a Ping message by answering with a Pong. """ log_healthcheck.debug( 'Ping received', node=pex(self.raiden.address), message_id=ping.nonce, message=ping, sender=pex(ping.sender), ) pong = Pong(ping.nonce) self.raiden.sign(pong) try: self.maybe_send(ping.sender, pong) except (InvalidAddress, UnknownAddress) as e: log.debug("Couldn't send the `Delivered` message", e=e) def receive_pong(self, pong: Pong): """ Handles a Pong message. """ message_id = ('ping', pong.nonce, pong.sender) async_result = self.messageids_to_asyncresults.get(message_id) if async_result is not None: log_healthcheck.debug( 'Pong received', node=pex(self.raiden.address), sender=pex(pong.sender), message_id=pong.nonce, ) async_result.set(True) else: log_healthcheck.warn( 'Unknown pong received', message_id=message_id, ) def get_ping(self, nonce: int) -> Ping: """ Returns a signed Ping message. Note: Ping messages don't have an enforced ordering, so a Ping message with a higher nonce may be acknowledged first. """ message = Ping( nonce=nonce, current_protocol_version=constants.PROTOCOL_VERSION, ) self.raiden.sign(message) message_data = message.encode() return message_data def set_node_network_state(self, node_address: typing.Address, node_state): state_change = ActionChangeNodeNetworkState(node_address, node_state) self.raiden.handle_state_change(state_change)
class UDPTransport: """ Node communication using the UDP protocol. """ def __init__( self, host, port, socket=None, protocol=None, throttle_policy=DummyPolicy()): self.protocol = protocol if socket is not None: self.server = DatagramServer(socket, handle=self.receive) else: self.server = DatagramServer((host, port), handle=self.receive) self.host = self.server.server_host self.port = self.server.server_port self.throttle_policy = throttle_policy def receive(self, data, host_port): # pylint: disable=unused-argument try: self.protocol.receive(data) except InvalidProtocolMessage as e: log.warning("Can't decode: {} (data={}, len={})".format(str(e), data, len(data))) return except RaidenShuttingDown: # For a clean shutdown return # enable debugging using the DummyNetwork callbacks DummyTransport.track_recv(self.protocol.raiden, host_port, data) def send(self, sender, host_port, bytes_): """ Send `bytes_` to `host_port`. Args: sender (address): The address of the running node. host_port (Tuple[(str, int)]): Tuple with the host name and port number. bytes_ (bytes): The bytes that are going to be sent through the wire. """ sleep_timeout = self.throttle_policy.consume(1) # Don't sleep if timeout is zero, otherwise a context-switch is done # and the message is delayed, increasing it's latency if sleep_timeout: gevent.sleep(sleep_timeout) if not hasattr(self.server, 'socket'): raise RuntimeError('trying to send a message on a closed server') self.server.sendto(bytes_, host_port) # enable debugging using the DummyNetwork callbacks DummyTransport.network.track_send(sender, host_port, bytes_) def stop(self): self.server.stop() # Calling `.close()` on a gevent socket doesn't actually close the underlying os socket # so we do that ourselves here. # See: https://github.com/gevent/gevent/blob/master/src/gevent/_socket2.py#L208 # and: https://groups.google.com/forum/#!msg/gevent/Ro8lRra3nH0/ZENgEXrr6M0J try: self.server._socket.close() except socket.error: pass def stop_accepting(self): self.server.stop_accepting() def start(self): assert not self.server.started # server.stop() clears the handle, since this may be a restart the # handle must always be set self.server.set_handle(self.receive) self.server.start()
class UDPTransport: def __init__(self, discovery, udpsocket, throttle_policy, config): # these values are initialized by the start method self.queueids_to_queues: typing.Dict self.raiden: 'RaidenService' self.discovery = discovery self.config = config self.retry_interval = config['retry_interval'] self.retries_before_backoff = config['retries_before_backoff'] self.nat_keepalive_retries = config['nat_keepalive_retries'] self.nat_keepalive_timeout = config['nat_keepalive_timeout'] self.nat_invitation_timeout = config['nat_invitation_timeout'] self.event_stop = Event() self.greenlets = list() self.addresses_events = dict() # Maps the message_id to a SentMessageState self.messageids_to_asyncresults = dict() # Maps the addresses to a dict with the latest nonce (using a dict # because python integers are immutable) self.nodeaddresses_to_nonces = dict() cache = cachetools.TTLCache( maxsize=50, ttl=CACHE_TTL, ) cache_wrapper = cachetools.cached(cache=cache) self.get_host_port = cache_wrapper(discovery.get) self.throttle_policy = throttle_policy self.server = DatagramServer(udpsocket, handle=self._receive) def start(self, raiden, queueids_to_queues): self.raiden = raiden self.queueids_to_queues = dict() # server.stop() clears the handle. Since this may be a restart the # handle must always be set self.server.set_handle(self._receive) for (recipient, queue_name), queue in queueids_to_queues.items(): queue_copy = list(queue) self.init_queue_for(recipient, queue_name, queue_copy) self.server.start() def stop_and_wait(self): # Stop handling incoming packets, but don't close the socket. The # socket can only be safely closed after all outgoing tasks are stopped self.server.stop_accepting() # Stop processing the outgoing queues self.event_stop.set() gevent.wait(self.greenlets) # All outgoing tasks are stopped. Now it's safe to close the socket. At # this point there might be some incoming message being processed, # keeping the socket open is not useful for these. self.server.stop() # Calling `.close()` on a gevent socket doesn't actually close the underlying os socket # so we do that ourselves here. # See: https://github.com/gevent/gevent/blob/master/src/gevent/_socket2.py#L208 # and: https://groups.google.com/forum/#!msg/gevent/Ro8lRra3nH0/ZENgEXrr6M0J try: self.server._socket.close() # pylint: disable=protected-access except socket.error: pass # Set all the pending results to False for async_result in self.messageids_to_asyncresults.values(): async_result.set(False) def get_health_events(self, recipient): """ Starts a healthcheck taks for `recipient` and returns a HealthEvents with locks to react on its current state. """ if recipient not in self.addresses_events: self.start_health_check(recipient) return self.addresses_events[recipient] def start_health_check(self, recipient): """ Starts a task for healthchecking `recipient` if there is not one yet. """ if recipient not in self.addresses_events: ping_nonce = self.nodeaddresses_to_nonces.setdefault( recipient, {'nonce': 0}, # HACK: Allows the task to mutate the object ) events = HealthEvents( event_healthy=Event(), event_unhealthy=Event(), ) self.addresses_events[recipient] = events self.greenlets.append( gevent.spawn( healthcheck, self, recipient, self.event_stop, events.event_healthy, events.event_unhealthy, self.nat_keepalive_retries, self.nat_keepalive_timeout, self.nat_invitation_timeout, ping_nonce, )) def init_queue_for(self, recipient, queue_name, items): """ Create the queue identified by the pair `(recipient, queue_name)` and initialize it with `items`. """ queueid = (recipient, queue_name) queue = self.queueids_to_queues.get(queueid) assert queue is None queue = NotifyingQueue(items=items) self.queueids_to_queues[queueid] = queue events = self.get_health_events(recipient) self.greenlets.append( gevent.spawn( single_queue_send, self, recipient, queue, self.event_stop, events.event_healthy, events.event_unhealthy, self.retries_before_backoff, self.retry_interval, self.retry_interval * 10, )) if log.isEnabledFor(logging.DEBUG): log.debug( 'new queue created for', node=pex(self.raiden.address), token=pex(queue_name), to=pex(recipient), ) return queue def get_queue_for(self, recipient, queue_name): """ Return the queue identified by the pair `(recipient, queue_name)`. If the queue doesn't exist it will be instantiated. """ queueid = (recipient, queue_name) queue = self.queueids_to_queues.get(queueid) if queue is None: items = () queue = self.init_queue_for(recipient, queue_name, items) return queue def send_async(self, queue_name, recipient, message): """ Send a new ordered message to recipient. Messages that use the same `queue_name` are ordered. """ if not isaddress(recipient): raise ValueError('Invalid address {}'.format(pex(recipient))) # These are not protocol messages, but transport specific messages if isinstance(message, (Delivered, Ping, Pong)): raise ValueError('Do not use send for {} messages'.format( message.__class__.__name__)) messagedata = message.encode() if len(messagedata) > UDP_MAX_MESSAGE_SIZE: raise ValueError('message size exceeds the maximum {}'.format( UDP_MAX_MESSAGE_SIZE)) # message identifiers must be unique message_id = message.message_identifier # ignore duplicates if message_id not in self.messageids_to_asyncresults: self.messageids_to_asyncresults[message_id] = AsyncResult() queue = self.get_queue_for(recipient, queue_name) queue.put((messagedata, message_id)) if log.isEnabledFor(logging.DEBUG): log.debug( 'MESSAGE QUEUED', node=pex(self.raiden.address), queue_name=queue_name, to=pex(recipient), message=message, ) def maybe_send(self, recipient, message): """ Send message to recipient if the transport is running. """ if not isaddress(recipient): raise InvalidAddress('Invalid address {}'.format(pex(recipient))) messagedata = message.encode() host_port = self.get_host_port(recipient) self.maybe_sendraw(host_port, messagedata) def maybe_sendraw_with_result(self, recipient, messagedata, message_id): """ Send message to recipient if the transport is running. Returns: An AsyncResult that will be set once the message is delivered. As long as the message has not been acknowledged with a Delivered message the function will return the same AsyncResult. """ async_result = self.messageids_to_asyncresults.get(message_id) if async_result is None: async_result = AsyncResult() self.messageids_to_asyncresults[message_id] = async_result host_port = self.get_host_port(recipient) self.maybe_sendraw(host_port, messagedata) return async_result def maybe_sendraw(self, host_port, messagedata): """ Send message to recipient if the transport is running. """ # Don't sleep if timeout is zero, otherwise a context-switch is done # and the message is delayed, increasing it's latency sleep_timeout = self.throttle_policy.consume(1) if sleep_timeout: gevent.sleep(sleep_timeout) # Check the udp socket is still available before trying to send the # message. There must be *no context-switches after this test*. if hasattr(self.server, 'socket'): self.server.sendto( messagedata, host_port, ) def _receive(self, data, host_port): # pylint: disable=unused-argument try: self.receive(data) except RaidenShuttingDown: # For a clean shutdown return def receive(self, messagedata): """ Handle an UDP packet. """ # pylint: disable=unidiomatic-typecheck if len(messagedata) > UDP_MAX_MESSAGE_SIZE: log.error( 'INVALID MESSAGE: Packet larger than maximum size', node=pex(self.raiden.address), message=hexlify(messagedata), length=len(messagedata), ) return message = decode(messagedata) if type(message) == Pong: self.receive_pong(message) elif type(message) == Ping: self.receive_ping(message) elif type(message) == Delivered: self.receive_delivered(message) elif message is not None: self.receive_message(message) elif log.isEnabledFor(logging.ERROR): log.error( 'INVALID MESSAGE: Unknown cmdid', node=pex(self.raiden.address), message=hexlify(messagedata), ) def receive_message(self, message): """ Handle a Raiden protocol message. The protocol requires durability of the messages. The UDP transport relies on the node's WAL for durability. The message will be converted to a state change, saved to the WAL, and *processed* before the durability is confirmed, which is a stronger property than what is required of any transport. """ # pylint: disable=unidiomatic-typecheck if on_udp_message(self.raiden, message): # Sending Delivered after the message is decoded and *processed* # gives a stronger guarantee than what is required from a # transport. # # Alternatives are, from weakest to strongest options: # - Just save it on disk and asynchronously process the messages # - Decode it, save to the WAL, and asynchronously process the # state change # - Decode it, save to the WAL, and process it (the current # implementation) delivered_message = Delivered(message.message_identifier) self.raiden.sign(delivered_message) self.maybe_send( message.sender, delivered_message, ) def receive_delivered(self, delivered: Delivered): """ Handle a Delivered message. The Delivered message is how the UDP transport guarantees persistence by the partner node. The message itself is not part of the raiden protocol, but it's required by this transport to provide the required properties. """ processed = ReceiveDelivered(delivered.delivered_message_identifier) self.raiden.handle_state_change(processed) message_id = delivered.delivered_message_identifier async_result = self.raiden.protocol.messageids_to_asyncresults.get( message_id) # clear the async result, otherwise we have a memory leak if async_result is not None: del self.messageids_to_asyncresults[message_id] async_result.set() # Pings and Pongs are used to check the health status of another node. They # are /not/ part of the raiden protocol, only part of the UDP transport, # therefore these messages are not forwarded to the message handler. def receive_ping(self, ping): """ Handle a Ping message by answering with a Pong. """ if ping_log.isEnabledFor(logging.DEBUG): ping_log.debug( 'PING RECEIVED', node=pex(self.raiden.address), message_id=ping.nonce, message=ping, sender=pex(ping.sender), ) pong = Pong(ping.nonce) self.raiden.sign(pong) try: self.maybe_send(ping.sender, pong) except (InvalidAddress, UnknownAddress) as e: log.debug("Couldn't send the `Delivered` message", e=e) def receive_pong(self, pong): """ Handles a Pong message. """ message_id = ('ping', pong.nonce, pong.sender) async_result = self.messageids_to_asyncresults.get(message_id) if async_result is not None: if log.isEnabledFor(logging.DEBUG): log.debug( 'PONG RECEIVED', node=pex(self.raiden.address), message_id=pong.nonce, ) async_result.set(True) def get_ping(self, nonce): """ Returns a signed Ping message. Note: Ping messages don't have an enforced ordering, so a Ping message with a higher nonce may be acknowledged first. """ message = Ping(nonce) self.raiden.sign(message) message_data = message.encode() return message_data def set_node_network_state(self, node_address, node_state): state_change = ActionChangeNodeNetworkState(node_address, node_state) self.raiden.handle_state_change(state_change)
class PeerManager(gevent.Greenlet): default_config = dict(p2p=dict(bootstrap_nodes=[], min_peers=5, max_peers=10, listen_port=30303, listen_host='0.0.0.0', timeout = 1.0, # tbd discovery_delay = 0.1), # tbd log_disconnects=False, node=dict(privkey='')) def __init__(self, configs=None): print('Initializing peerManager....') super(PeerManager,self).__init__() self.is_stopped = False self.configs = configs if configs else self.default_config self.peers = [] self.address = (self.configs['p2p']['listen_host'], self.configs['p2p']['listen_port']) self.server = Server(self.address, handle=self._new_conn) # make sure privkey is given in config self.configs['node']['id'] = crypto.priv2addr(self.configs['node']['privkey']) # needs further investigation self.upnp = None self.errors = PeerErrors() if self.configs['log_disconnects'] else None def start(self): print('Starting peerManager...') super(PeerManager,self).start() self.server.set_handle(self._new_conn) self.server.start() gevent.spawn_later(0.001, self.bootstrap, self.configs['p2p']['bootstrap_nodes']) # delays tbd gevent.spawn_later(1, self.discovery) # delays tbd def stop(self): print('Stopping peerManager..') self.server.stop() for peer in self.peers: peer.stop() super(PeerManager,self).stop() def _new_conn(self, data, addr): pubkey = str(data, 'ascii') self.connect(addr, pubkey) # TODO!! def discovery(self): gevent.sleep(self.configs['p2p']['discovery_delay']) # yield to other processes while not self.is_stopped: try: num_peers = len(self.peers) # check if all peers are valid and working min_peers = self.configs['p2p']['min_peers'] if num_peers < min_peers: print('Min #peers: %i; Current #peers: %i' %(min_peers,num_peers)) # connect to random peers? # connect to nearest neighbors? def bootstrap(self, bootstrap_nodes=[]): for node in bootstrap_nodes: addr, pubkey = node #undecided format. temp: node = (addr, pubkey) try: self.connect(addr, pubkey) except socket.error: print('bootstrap failed at peer address:', addr) def connect(self, address, pubkey): """ gevent.socket.create_connection(address, timeout=Timeout, source_address=None) Connect to address (a 2-tuple (host, port)) and return the socket object. """ print('Connecting to: ', address) try: connection = create_connection(address, timeout=self.configs['p2p']['timeout'], source_address=self.address) except socket.timeout: #self.errors.add(address, 'connection timeout') print('Connection timeout at address:', address) return False except socket.error as e: #self.errors.add(address, 'connection error') print('Connection error at address:', address) print(e) return False # successful connection self.start_peer(connection, address, pubkey) return True def start_peer(self, connection, address, pubkey): pubID = crypto.pub2addr(pubkey) print('Starting new peer:', pubID) peer = Peer(self, connection, address, pubID) peer.link(peer_die) self.peers.append(peer) peer.start() return peer # do we need this? yes, we do, and we also need a send() for a specific peer # change: use peermanager.broadcast() for broadcasts; use peer.send() for direct transports def broadcast(self, *args, num_peers=None, excluded=[]): valid_peers = [p for p in self.peers if p not in excluded] num_peers = num_peers if num_peers else len(valid_peers) for peer in random.sample(valid_peers, min(num_peers, len(valid_peers))): peer.protocol.send(*args) peer.safe_to_read.wait()