Exemplo n.º 1
0
    def __init__(
        self,
        local_private_key: keys.PrivateKey,
        listen_on: Endpoint,
        enr_db: ENRDatabaseAPI,
        events: EventsAPI = None,
        message_type_registry: MessageTypeRegistry = v51_registry,
    ) -> None:
        self._local_private_key = local_private_key

        self.listen_on = listen_on
        self._listening = trio.Event()

        self.enr_manager = ENRManager(
            private_key=local_private_key,
            enr_db=enr_db,
        )
        self.enr_db = enr_db
        self._registry = message_type_registry

        # Datagrams
        (
            self._outbound_datagram_send_channel,
            self._outbound_datagram_receive_channel,
        ) = trio.open_memory_channel[OutboundDatagram](256)
        (
            self._inbound_datagram_send_channel,
            self._inbound_datagram_receive_channel,
        ) = trio.open_memory_channel[InboundDatagram](256)

        # EnvelopePair
        (
            self._outbound_envelope_send_channel,
            self._outbound_envelope_receive_channel,
        ) = trio.open_memory_channel[OutboundEnvelope](256)
        (
            self._inbound_envelope_send_channel,
            self._inbound_envelope_receive_channel,
        ) = trio.open_memory_channel[InboundEnvelope](256)

        # Messages
        (
            self._outbound_message_send_channel,
            self._outbound_message_receive_channel,
        ) = trio.open_memory_channel[AnyOutboundMessage](256)
        (
            self._inbound_message_send_channel,
            self._inbound_message_receive_channel,
        ) = trio.open_memory_channel[AnyInboundMessage](256)

        if events is None:
            events = Events()
        self.events = events

        self.pool = Pool(
            local_private_key=self._local_private_key,
            local_node_id=self.enr_manager.enr.node_id,
            enr_db=self.enr_db,
            outbound_envelope_send_channel=self.
            _outbound_envelope_send_channel,
            inbound_message_send_channel=self._inbound_message_send_channel,
            message_type_registry=self._registry,
            events=self.events,
        )

        self.dispatcher = Dispatcher(
            self._inbound_envelope_receive_channel,
            self._inbound_message_receive_channel,
            self.pool,
            self.enr_db,
            self._registry,
            self.events,
        )
        self.envelope_decoder = EnvelopeEncoder(
            self._outbound_envelope_receive_channel,
            self._outbound_datagram_send_channel,
        )
        self.envelope_encoder = EnvelopeDecoder(
            self._inbound_datagram_receive_channel,
            self._inbound_envelope_send_channel,
            self.enr_manager.enr.node_id,
        )

        self._ready = trio.Event()
Exemplo n.º 2
0
    async def dispatcher_pair(
            self, node_a: NodeAPI, node_b: NodeAPI
    ) -> AsyncIterator[Tuple[DispatcherAPI, DispatcherAPI]]:
        if node_a.node_id < node_b.node_id:
            left = node_a
            right = node_b
        elif node_b.node_id < node_a.node_id:
            left = node_b
            right = node_a
        else:
            raise Exception("Cannot pair with self")

        key = (left.node_id, right.node_id)
        if key in self._running_dispatchers:
            raise Exception("Already running dispatchers for: "
                            f"{humanize_node_id(left.node_id)} <-> "
                            f"{humanize_node_id(right.node_id)}")

        self.logger.info("setting up dispatcher pair: %s <> %s", left, right)

        async with AsyncExitStack() as stack:
            left_pool, left_channels = self._get_or_create_pool_for_node(left)

            if left.node_id in self._managed_dispatchers:
                self.logger.info("dispatcher already present for %s", left)
                left_managed_dispatcher = self._managed_dispatchers[
                    left.node_id]
            else:
                self.logger.info("setting up new dispatcher for %s", left)
                (
                    left_inbound_envelope_send_channel,
                    left_inbound_envelope_receive_channel,
                ) = trio.open_memory_channel[InboundEnvelope](256)

                left_dispatcher = Dispatcher(
                    left_inbound_envelope_receive_channel,
                    left_channels.inbound_message_receive_channel,
                    left_pool,
                    left.enr_db,
                    events=left.events,
                )
                left_managed_dispatcher = ManagedDispatcher(
                    dispatcher=left_dispatcher,
                    send_channel=left_inbound_envelope_send_channel,
                )
                self._managed_dispatchers[
                    left.node_id] = left_managed_dispatcher
                await stack.enter_async_context(
                    background_trio_service(left_dispatcher))

            right_pool, right_channels = self._get_or_create_pool_for_node(
                right)

            if right.node_id in self._managed_dispatchers:
                self.logger.info("dispatcher already present for %s", right)
                right_managed_dispatcher = self._managed_dispatchers[
                    right.node_id]
            else:
                self.logger.info("setting up new dispatcher for %s", right)
                (
                    right_inbound_envelope_send_channel,
                    right_inbound_envelope_receive_channel,
                ) = trio.open_memory_channel[InboundEnvelope](256)

                right_dispatcher = Dispatcher(
                    right_inbound_envelope_receive_channel,
                    right_channels.inbound_message_receive_channel,
                    right_pool,
                    right.enr_db,
                    events=right.events,
                )
                right_managed_dispatcher = ManagedDispatcher(
                    dispatcher=right_dispatcher,
                    send_channel=right_inbound_envelope_send_channel,
                )
                self._managed_dispatchers[
                    right.node_id] = right_managed_dispatcher
                await stack.enter_async_context(
                    background_trio_service(right_dispatcher))

            if left is node_a:
                dispatchers = (
                    left_managed_dispatcher.dispatcher,
                    right_managed_dispatcher.dispatcher,
                )
            elif left is node_b:
                dispatchers = (
                    right_managed_dispatcher.dispatcher,
                    left_managed_dispatcher.dispatcher,
                )
            else:
                raise Exception("Invariant")

            async with staple(
                    left,
                    left_channels.outbound_envelope_receive_channel,
                    right_managed_dispatcher.send_channel.clone(
                    ),  # type: ignore
            ):
                async with staple(
                        right,
                        right_channels.outbound_envelope_receive_channel,
                        left_managed_dispatcher.send_channel.clone(
                        ),  # type: ignore
                ):
                    self._running_dispatchers.add(key)
                    try:
                        yield dispatchers
                    finally:
                        self._running_dispatchers.remove(key)
Exemplo n.º 3
0
class Client(Service, ClientAPI):
    logger = logging.getLogger("ddht.Client")

    def __init__(
        self,
        local_private_key: keys.PrivateKey,
        listen_on: Endpoint,
        enr_db: ENRDatabaseAPI,
        events: EventsAPI = None,
        message_type_registry: MessageTypeRegistry = v51_registry,
    ) -> None:
        self._local_private_key = local_private_key

        self.listen_on = listen_on
        self._listening = trio.Event()

        self.enr_manager = ENRManager(
            private_key=local_private_key,
            enr_db=enr_db,
        )
        self.enr_db = enr_db
        self._registry = message_type_registry

        # Datagrams
        (
            self._outbound_datagram_send_channel,
            self._outbound_datagram_receive_channel,
        ) = trio.open_memory_channel[OutboundDatagram](256)
        (
            self._inbound_datagram_send_channel,
            self._inbound_datagram_receive_channel,
        ) = trio.open_memory_channel[InboundDatagram](256)

        # EnvelopePair
        (
            self._outbound_envelope_send_channel,
            self._outbound_envelope_receive_channel,
        ) = trio.open_memory_channel[OutboundEnvelope](256)
        (
            self._inbound_envelope_send_channel,
            self._inbound_envelope_receive_channel,
        ) = trio.open_memory_channel[InboundEnvelope](256)

        # Messages
        (
            self._outbound_message_send_channel,
            self._outbound_message_receive_channel,
        ) = trio.open_memory_channel[AnyOutboundMessage](256)
        (
            self._inbound_message_send_channel,
            self._inbound_message_receive_channel,
        ) = trio.open_memory_channel[AnyInboundMessage](256)

        if events is None:
            events = Events()
        self.events = events

        self.pool = Pool(
            local_private_key=self._local_private_key,
            local_node_id=self.enr_manager.enr.node_id,
            enr_db=self.enr_db,
            outbound_envelope_send_channel=self.
            _outbound_envelope_send_channel,
            inbound_message_send_channel=self._inbound_message_send_channel,
            message_type_registry=self._registry,
            events=self.events,
        )

        self.dispatcher = Dispatcher(
            self._inbound_envelope_receive_channel,
            self._inbound_message_receive_channel,
            self.pool,
            self.enr_db,
            self._registry,
            self.events,
        )
        self.envelope_decoder = EnvelopeEncoder(
            self._outbound_envelope_receive_channel,
            self._outbound_datagram_send_channel,
        )
        self.envelope_encoder = EnvelopeDecoder(
            self._inbound_datagram_receive_channel,
            self._inbound_envelope_send_channel,
            self.enr_manager.enr.node_id,
        )

        self._ready = trio.Event()

    @property
    def local_node_id(self) -> NodeID:
        return self.pool.local_node_id

    async def run(self) -> None:
        self.manager.run_daemon_child_service(self.dispatcher)

        self.manager.run_daemon_child_service(self.envelope_decoder)
        self.manager.run_daemon_child_service(self.envelope_encoder)

        self.manager.run_daemon_task(self._do_listen, self.listen_on)

        await self.manager.wait_finished()

    async def wait_listening(self) -> None:
        await self._listening.wait()

    async def _do_listen(self, listen_on: Endpoint) -> None:
        sock = trio.socket.socket(
            family=trio.socket.AF_INET,
            type=trio.socket.SOCK_DGRAM,
        )
        ip_address, port = listen_on
        await sock.bind((socket.inet_ntoa(ip_address), port))

        self._listening.set()
        await self.events.listening.trigger(listen_on)

        self.logger.debug("Network connection listening on %s", listen_on)

        # TODO: the datagram services need to use the `EventsAPI`
        datagram_sender = DatagramSender(
            self._outbound_datagram_receive_channel,
            sock)  # type: ignore  # noqa: E501
        self.manager.run_daemon_child_service(datagram_sender)

        datagram_receiver = DatagramReceiver(
            sock,
            self._inbound_datagram_send_channel)  # type: ignore  # noqa: E501
        self.manager.run_daemon_child_service(datagram_receiver)

        await self.manager.wait_finished()

    #
    # Message API
    #
    @contextmanager
    def _get_request_id(self,
                        node_id: NodeID,
                        request_id: Optional[int] = None) -> Iterator[int]:
        request_id_context: ContextManager[int]

        if request_id is None:
            request_id_context = self.dispatcher.reserve_request_id(node_id)
        else:
            request_id_context = nullcontext(request_id)

        with request_id_context as message_request_id:
            yield message_request_id

    async def send_ping(
        self,
        endpoint: Endpoint,
        node_id: NodeID,
        *,
        request_id: Optional[int] = None,
    ) -> int:
        with self._get_request_id(node_id, request_id) as message_request_id:
            message = AnyOutboundMessage(
                PingMessage(message_request_id,
                            self.enr_manager.enr.sequence_number),
                endpoint,
                node_id,
            )
            await self.dispatcher.send_message(message)

        return message_request_id

    async def send_pong(
        self,
        endpoint: Endpoint,
        node_id: NodeID,
        *,
        request_id: int,
    ) -> None:
        message = AnyOutboundMessage(
            PongMessage(
                request_id,
                self.enr_manager.enr.sequence_number,
                endpoint.ip_address,
                endpoint.port,
            ),
            endpoint,
            node_id,
        )
        await self.dispatcher.send_message(message)

    async def send_find_nodes(
        self,
        endpoint: Endpoint,
        node_id: NodeID,
        *,
        distances: Collection[int],
        request_id: Optional[int] = None,
    ) -> int:
        with self._get_request_id(node_id, request_id) as message_request_id:
            message = AnyOutboundMessage(
                FindNodeMessage(message_request_id, tuple(distances)),
                endpoint,
                node_id,
            )
            await self.dispatcher.send_message(message)

        return message_request_id

    async def send_found_nodes(
        self,
        endpoint: Endpoint,
        node_id: NodeID,
        *,
        enrs: Sequence[ENRAPI],
        request_id: int,
    ) -> int:
        enr_batches = partition_enrs(
            enrs, max_payload_size=FOUND_NODES_MAX_PAYLOAD_SIZE)
        num_batches = len(enr_batches)
        for batch in enr_batches:
            message = AnyOutboundMessage(
                FoundNodesMessage(
                    request_id,
                    num_batches,
                    batch,
                ),
                endpoint,
                node_id,
            )
            await self.dispatcher.send_message(message)

        return num_batches

    async def send_talk_request(
        self,
        endpoint: Endpoint,
        node_id: NodeID,
        *,
        protocol: bytes,
        request: bytes,
        request_id: Optional[int] = None,
    ) -> int:
        with self._get_request_id(node_id, request_id) as message_request_id:
            message = AnyOutboundMessage(
                TalkRequestMessage(message_request_id, protocol, request),
                endpoint,
                node_id,
            )
            await self.dispatcher.send_message(message)

        return message_request_id

    async def send_talk_response(
        self,
        endpoint: Endpoint,
        node_id: NodeID,
        *,
        response: bytes,
        request_id: int,
    ) -> None:
        message = AnyOutboundMessage(
            TalkResponseMessage(
                request_id,
                response,
            ),
            endpoint,
            node_id,
        )
        await self.dispatcher.send_message(message)

    async def send_register_topic(
        self,
        endpoint: Endpoint,
        node_id: NodeID,
        *,
        topic: bytes,
        enr: ENRAPI,
        ticket: bytes = b"",
        request_id: Optional[int] = None,
    ) -> int:
        with self._get_request_id(node_id, request_id) as message_request_id:
            message = AnyOutboundMessage(
                RegisterTopicMessage(message_request_id, topic, enr, ticket),
                endpoint,
                node_id,
            )
            await self.dispatcher.send_message(message)

        return message_request_id

    async def send_ticket(
        self,
        endpoint: Endpoint,
        node_id: NodeID,
        *,
        ticket: bytes,
        wait_time: int,
        request_id: int,
    ) -> None:
        message = AnyOutboundMessage(
            TicketMessage(request_id, ticket, wait_time),
            endpoint,
            node_id,
        )
        await self.dispatcher.send_message(message)

    async def send_registration_confirmation(
        self,
        endpoint: Endpoint,
        node_id: NodeID,
        *,
        topic: bytes,
        request_id: int,
    ) -> None:
        message = AnyOutboundMessage(
            RegistrationConfirmationMessage(request_id, topic),
            endpoint,
            node_id,
        )
        await self.dispatcher.send_message(message)

    async def send_topic_query(
        self,
        endpoint: Endpoint,
        node_id: NodeID,
        *,
        topic: bytes,
        request_id: Optional[int] = None,
    ) -> int:
        with self._get_request_id(node_id, request_id) as message_request_id:
            message = AnyOutboundMessage(
                TopicQueryMessage(message_request_id, topic),
                endpoint,
                node_id,
            )
            await self.dispatcher.send_message(message)
        return message_request_id

    #
    # Request/Response API
    #
    async def ping(self, endpoint: Endpoint,
                   node_id: NodeID) -> InboundMessage[PongMessage]:
        with self._get_request_id(node_id) as request_id:
            request = AnyOutboundMessage(
                PingMessage(request_id, self.enr_manager.enr.sequence_number),
                endpoint,
                node_id,
            )
            async with self.dispatcher.subscribe_request(
                    request, PongMessage) as subscription:
                with trio.fail_after(REQUEST_RESPONSE_TIMEOUT):
                    return await subscription.receive()

    async def find_nodes(
        self,
        endpoint: Endpoint,
        node_id: NodeID,
        distances: Collection[int],
    ) -> Tuple[InboundMessage[FoundNodesMessage], ...]:
        with self._get_request_id(node_id) as request_id:
            request = AnyOutboundMessage(
                FindNodeMessage(request_id, tuple(distances)),
                endpoint,
                node_id,
            )
            async with self.dispatcher.subscribe_request(
                    request, FoundNodesMessage) as subscription:
                with trio.fail_after(REQUEST_RESPONSE_TIMEOUT):
                    head_response = await subscription.receive()
                    total = head_response.message.total
                    responses: Tuple[InboundMessage[FoundNodesMessage], ...]
                    if total == 1:
                        responses = (head_response, )
                    elif total > 1:
                        tail_responses: List[
                            InboundMessage[FoundNodesMessage]] = []
                        for _ in range(total - 1):
                            tail_responses.append(await subscription.receive())
                        responses = (head_response, ) + tuple(tail_responses)
                    else:
                        # TODO: this code path needs to be excercised and
                        # probably replaced with some sort of
                        # `SessionTerminated` exception.
                        raise Exception("Invalid `total` counter in response")

                # Validate that all responses are indeed at one of the
                # specified distances.
                for response in responses:
                    for enr in response.message.enrs:
                        if enr.node_id == node_id:
                            if 0 not in distances:
                                raise ValidationError(
                                    f"Invalid response: distance=0  expected={distances}"
                                )
                        else:
                            distance = compute_log_distance(
                                enr.node_id, node_id)
                            if distance not in distances:
                                raise ValidationError(
                                    f"Invalid response: distance={distance}  expected={distances}"
                                )

                return responses

    async def talk(self, endpoint: Endpoint, node_id: NodeID, protocol: bytes,
                   request: bytes) -> InboundMessage[TalkResponseMessage]:
        raise NotImplementedError

    async def register_topic(
        self,
        endpoint: Endpoint,
        node_id: NodeID,
        topic: bytes,
        ticket: Optional[bytes] = None,
    ) -> Tuple[InboundMessage[TicketMessage],
               Optional[InboundMessage[RegistrationConfirmationMessage]], ]:
        raise NotImplementedError

    async def topic_query(self, endpoint: Endpoint, node_id: NodeID,
                          topic: bytes) -> InboundMessage[FoundNodesMessage]:
        raise NotImplementedError
Exemplo n.º 4
0
class Client(Service, ClientAPI):
    logger = logging.getLogger("ddht.Client")

    def __init__(
        self,
        local_private_key: keys.PrivateKey,
        listen_on: Endpoint,
        enr_db: QueryableENRDatabaseAPI,
        session_cache_size: int,
        events: EventsAPI = None,
        message_type_registry: MessageTypeRegistry = v51_registry,
    ) -> None:
        self.local_private_key = local_private_key

        self.listen_on = listen_on
        self._listening = trio.Event()

        self.enr_manager = ENRManager(
            private_key=local_private_key,
            enr_db=enr_db,
        )
        self.enr_db = enr_db
        self._registry = message_type_registry

        self.request_tracker = RequestTracker()

        # Datagrams
        (
            self._outbound_datagram_send_channel,
            self._outbound_datagram_receive_channel,
        ) = trio.open_memory_channel[OutboundDatagram](256)
        (
            self._inbound_datagram_send_channel,
            self._inbound_datagram_receive_channel,
        ) = trio.open_memory_channel[InboundDatagram](256)

        # EnvelopePair
        (
            self._outbound_envelope_send_channel,
            self._outbound_envelope_receive_channel,
        ) = trio.open_memory_channel[OutboundEnvelope](256)
        (
            self._inbound_envelope_send_channel,
            self._inbound_envelope_receive_channel,
        ) = trio.open_memory_channel[InboundEnvelope](256)

        # Messages
        (
            self._outbound_message_send_channel,
            self._outbound_message_receive_channel,
        ) = trio.open_memory_channel[AnyOutboundMessage](256)
        (
            self._inbound_message_send_channel,
            self._inbound_message_receive_channel,
        ) = trio.open_memory_channel[AnyInboundMessage](256)

        if events is None:
            events = Events()
        self.events = events

        self.pool = Pool(
            local_private_key=self.local_private_key,
            local_node_id=self.enr_manager.enr.node_id,
            enr_db=self.enr_db,
            outbound_envelope_send_channel=self.
            _outbound_envelope_send_channel,
            inbound_message_send_channel=self._inbound_message_send_channel,
            session_cache_size=session_cache_size,
            message_type_registry=self._registry,
            events=self.events,
        )

        self.dispatcher = Dispatcher(
            self._inbound_envelope_receive_channel,
            self._inbound_message_receive_channel,
            self.pool,
            self.enr_db,
            self.events,
        )
        self.envelope_decoder = EnvelopeEncoder(
            self._outbound_envelope_receive_channel,
            self._outbound_datagram_send_channel,
        )
        self.envelope_encoder = EnvelopeDecoder(
            self._inbound_datagram_receive_channel,
            self._inbound_envelope_send_channel,
            self.enr_manager.enr.node_id,
        )

        self._ready = trio.Event()

    @property
    def local_node_id(self) -> NodeID:
        return self.pool.local_node_id

    async def run(self) -> None:
        self.manager.run_daemon_task(
            self._run_envelope_and_dispatcher_services)
        self.manager.run_daemon_task(self._do_listen, self.listen_on)

        await self.manager.wait_finished()

    async def _run_envelope_and_dispatcher_services(self) -> None:
        """
        Ensure that in the task hierarchy the envelope encode will be shut down
        *after* the dispatcher.

        run()
          |
          ---EnvelopeEncoder
                |
                ---EnvelopeDecoder
                      |
                      ---Dispatcher
        """
        async with background_trio_service(self.envelope_encoder):
            async with background_trio_service(self.envelope_decoder):
                async with background_trio_service(self.dispatcher):
                    await self.manager.wait_finished()

    async def wait_listening(self) -> None:
        await self._listening.wait()

    async def _do_listen(self, listen_on: Endpoint) -> None:
        sock = trio.socket.socket(
            family=trio.socket.AF_INET,
            type=trio.socket.SOCK_DGRAM,
        )
        ip_address, port = listen_on
        await sock.bind((socket.inet_ntoa(ip_address), port))

        self._listening.set()
        await self.events.listening.trigger(listen_on)

        self.logger.debug("Network connection listening on %s", listen_on)

        # TODO: the datagram services need to use the `EventsAPI`
        datagram_sender = DatagramSender(
            self._outbound_datagram_receive_channel,
            sock)  # type: ignore  # noqa: E501
        self.manager.run_daemon_child_service(datagram_sender)

        datagram_receiver = DatagramReceiver(
            sock,
            self._inbound_datagram_send_channel)  # type: ignore  # noqa: E501
        self.manager.run_daemon_child_service(datagram_receiver)

        await self.manager.wait_finished()

    #
    # Message API
    #
    async def send_ping(
        self,
        node_id: NodeID,
        endpoint: Endpoint,
        *,
        enr_seq: Optional[int] = None,
        request_id: Optional[bytes] = None,
    ) -> bytes:
        if enr_seq is None:
            enr_seq = self.enr_manager.enr.sequence_number

        with self.request_tracker.reserve_request_id(
                node_id, request_id) as message_request_id:
            message = AnyOutboundMessage(
                PingMessage(message_request_id, enr_seq),
                endpoint,
                node_id,
            )
            await self.dispatcher.send_message(message)

        return message_request_id

    async def send_pong(
        self,
        node_id: NodeID,
        endpoint: Endpoint,
        *,
        enr_seq: Optional[int] = None,
        request_id: bytes,
    ) -> None:
        if enr_seq is None:
            enr_seq = self.enr_manager.enr.sequence_number

        message = AnyOutboundMessage(
            PongMessage(
                request_id,
                enr_seq,
                endpoint.ip_address,
                endpoint.port,
            ),
            endpoint,
            node_id,
        )
        await self.dispatcher.send_message(message)

    async def send_find_nodes(
        self,
        node_id: NodeID,
        endpoint: Endpoint,
        *,
        distances: Collection[int],
        request_id: Optional[bytes] = None,
    ) -> bytes:
        with self.request_tracker.reserve_request_id(
                node_id, request_id) as message_request_id:
            message = AnyOutboundMessage(
                FindNodeMessage(message_request_id, tuple(distances)),
                endpoint,
                node_id,
            )
            await self.dispatcher.send_message(message)

        return message_request_id

    async def send_found_nodes(
        self,
        node_id: NodeID,
        endpoint: Endpoint,
        *,
        enrs: Sequence[ENRAPI],
        request_id: bytes,
    ) -> int:
        enr_batches = partition_enrs(
            enrs, max_payload_size=FOUND_NODES_MAX_PAYLOAD_SIZE)
        num_batches = len(enr_batches)
        for batch in enr_batches:
            message = AnyOutboundMessage(
                FoundNodesMessage(
                    request_id,
                    num_batches,
                    batch,
                ),
                endpoint,
                node_id,
            )
            await self.dispatcher.send_message(message)

        return num_batches

    async def send_talk_request(
        self,
        node_id: NodeID,
        endpoint: Endpoint,
        *,
        protocol: bytes,
        payload: bytes,
        request_id: Optional[bytes] = None,
    ) -> bytes:
        with self.request_tracker.reserve_request_id(
                node_id, request_id) as message_request_id:
            message = AnyOutboundMessage(
                TalkRequestMessage(message_request_id, protocol, payload),
                endpoint,
                node_id,
            )
            await self.dispatcher.send_message(message)

        return message_request_id

    async def send_talk_response(
        self,
        node_id: NodeID,
        endpoint: Endpoint,
        *,
        payload: bytes,
        request_id: bytes,
    ) -> None:
        message = AnyOutboundMessage(
            TalkResponseMessage(request_id, payload),
            endpoint,
            node_id,
        )
        await self.dispatcher.send_message(message)

    async def send_register_topic(
        self,
        node_id: NodeID,
        endpoint: Endpoint,
        *,
        topic: bytes,
        enr: ENRAPI,
        ticket: bytes = b"",
        request_id: Optional[bytes] = None,
    ) -> bytes:
        with self.request_tracker.reserve_request_id(
                node_id, request_id) as message_request_id:
            message = AnyOutboundMessage(
                RegisterTopicMessage(message_request_id, topic, enr, ticket),
                endpoint,
                node_id,
            )
            await self.dispatcher.send_message(message)

        return message_request_id

    async def send_ticket(
        self,
        node_id: NodeID,
        endpoint: Endpoint,
        *,
        ticket: bytes,
        wait_time: int,
        request_id: bytes,
    ) -> None:
        message = AnyOutboundMessage(
            TicketMessage(request_id, ticket, wait_time),
            endpoint,
            node_id,
        )
        await self.dispatcher.send_message(message)

    async def send_registration_confirmation(
        self,
        node_id: NodeID,
        endpoint: Endpoint,
        *,
        topic: bytes,
        request_id: bytes,
    ) -> None:
        message = AnyOutboundMessage(
            RegistrationConfirmationMessage(request_id, topic),
            endpoint,
            node_id,
        )
        await self.dispatcher.send_message(message)

    async def send_topic_query(
        self,
        node_id: NodeID,
        endpoint: Endpoint,
        *,
        topic: bytes,
        request_id: Optional[bytes] = None,
    ) -> bytes:
        with self.request_tracker.reserve_request_id(
                node_id, request_id) as message_request_id:
            message = AnyOutboundMessage(
                TopicQueryMessage(message_request_id, topic),
                endpoint,
                node_id,
            )
            await self.dispatcher.send_message(message)
        return message_request_id

    #
    # Request/Response API
    #
    async def ping(
        self,
        node_id: NodeID,
        endpoint: Endpoint,
        *,
        request_id: Optional[bytes] = None,
    ) -> InboundMessage[PongMessage]:
        with self.request_tracker.reserve_request_id(node_id,
                                                     request_id) as request_id:
            request = AnyOutboundMessage(
                PingMessage(request_id, self.enr_manager.enr.sequence_number),
                endpoint,
                node_id,
            )
            async with self.dispatcher.subscribe_request(
                    request, PongMessage) as subscription:
                return await subscription.receive()

    async def find_nodes(
        self,
        node_id: NodeID,
        endpoint: Endpoint,
        distances: Collection[int],
        *,
        request_id: Optional[bytes] = None,
    ) -> Tuple[InboundMessage[FoundNodesMessage], ...]:
        with self.request_tracker.reserve_request_id(node_id,
                                                     request_id) as request_id:
            request = AnyOutboundMessage(
                FindNodeMessage(request_id, tuple(distances)),
                endpoint,
                node_id,
            )
            async with self.dispatcher.subscribe_request(
                    request, FoundNodesMessage) as subscription:
                head_response = await subscription.receive()
                total = head_response.message.total
                responses: Tuple[InboundMessage[FoundNodesMessage], ...]
                if total == 1:
                    responses = (head_response, )
                elif total > 1:
                    tail_responses: List[
                        InboundMessage[FoundNodesMessage]] = []
                    for _ in range(total - 1):
                        tail_responses.append(await subscription.receive())
                    responses = (head_response, ) + tuple(tail_responses)
                else:
                    raise ValidationError(
                        f"Invalid `total` counter in response: total={total}")

                return responses

    def stream_find_nodes(
        self,
        node_id: NodeID,
        endpoint: Endpoint,
        distances: Collection[int],
        *,
        request_id: Optional[bytes] = None,
    ) -> AsyncContextManager[trio.abc.ReceiveChannel[
            InboundMessage[FoundNodesMessage]]]:
        return common_client_stream_find_nodes(self,
                                               node_id,
                                               endpoint,
                                               distances,
                                               request_id=request_id)

    async def talk(
        self,
        node_id: NodeID,
        endpoint: Endpoint,
        protocol: bytes,
        payload: bytes,
        *,
        request_id: Optional[bytes] = None,
    ) -> InboundMessage[TalkResponseMessage]:
        with self.request_tracker.reserve_request_id(node_id,
                                                     request_id) as request_id:
            request = AnyOutboundMessage(
                TalkRequestMessage(request_id, protocol, payload),
                endpoint,
                node_id,
            )
            async with self.dispatcher.subscribe_request(
                    request, TalkResponseMessage) as subscription:
                return await subscription.receive()

    async def register_topic(
        self,
        node_id: NodeID,
        endpoint: Endpoint,
        topic: bytes,
        ticket: Optional[bytes] = None,
        *,
        request_id: Optional[bytes] = None,
    ) -> Tuple[InboundMessage[TicketMessage],
               Optional[InboundMessage[RegistrationConfirmationMessage]], ]:
        raise NotImplementedError

    async def topic_query(
        self,
        node_id: NodeID,
        endpoint: Endpoint,
        topic: bytes,
        *,
        request_id: Optional[bytes] = None,
    ) -> InboundMessage[FoundNodesMessage]:
        raise NotImplementedError