Example #1
0
async def test_simple_two_nodes():
    node_a = await new_node(transport_opt=[str(LISTEN_MADDR)])
    node_b = await new_node(transport_opt=[str(LISTEN_MADDR)])

    await node_a.get_network().listen(LISTEN_MADDR)
    await node_b.get_network().listen(LISTEN_MADDR)

    supported_protocols = [FLOODSUB_PROTOCOL_ID]
    topic = "my_topic"
    data = b"some data"

    floodsub_a = FloodSub(supported_protocols)
    pubsub_a = Pubsub(node_a, floodsub_a, ID(b"a" * 32))
    floodsub_b = FloodSub(supported_protocols)
    pubsub_b = Pubsub(node_b, floodsub_b, ID(b"b" * 32))

    await connect(node_a, node_b)
    await asyncio.sleep(0.25)

    sub_b = await pubsub_b.subscribe(topic)
    # Sleep to let a know of b's subscription
    await asyncio.sleep(0.25)

    await pubsub_a.publish(topic, data)

    res_b = await sub_b.get()

    # Check that the msg received by node_b is the same
    # as the message sent by node_a
    assert ID(res_b.from_id) == node_a.get_id()
    assert res_b.data == data
    assert res_b.topicIDs == [topic]

    # Success, terminate pending tasks.
    await cleanup()
Example #2
0
File: node.py Project: onyb/trinity
    def __init__(self,
                 key_pair: KeyPair,
                 listen_ip: str,
                 listen_port: int,
                 chain: BaseBeaconChain,
                 event_bus: EndpointAPI,
                 security_protocol_ops: Dict[TProtocol,
                                             BaseSecureTransport] = None,
                 muxer_protocol_ops: Dict[TProtocol, IMuxedConn] = None,
                 gossipsub_params: Optional[GossipsubParams] = None,
                 cancel_token: CancelToken = None,
                 bootstrap_nodes: Tuple[Multiaddr, ...] = (),
                 preferred_nodes: Tuple[Multiaddr, ...] = (),
                 subnets: Optional[Set[SubnetId]] = None) -> None:
        super().__init__(cancel_token)
        self.listen_ip = listen_ip
        self.listen_port = listen_port
        self.key_pair = key_pair
        self.bootstrap_nodes = bootstrap_nodes
        self.preferred_nodes = preferred_nodes
        self.subnets = subnets if subnets is not None else set()
        # TODO: Add key and peer_id to the peerstore
        if security_protocol_ops is None:
            security_protocol_ops = {SecIOID: SecIOTransport(key_pair)}
        if muxer_protocol_ops is None:
            muxer_protocol_ops = {MPLEX_PROTOCOL_ID: Mplex}
        network: INetwork = initialize_default_swarm(
            key_pair=key_pair,
            transport_opt=[self.listen_maddr],
            muxer_opt=muxer_protocol_ops,
            sec_opt=security_protocol_ops,
            peerstore_opt=None,  # let the function initialize it
        )
        self.host = BasicHost(network=network)

        if gossipsub_params is None:
            gossipsub_params = GossipsubParams()
        gossipsub_router = GossipSub(
            protocols=[GOSSIPSUB_PROTOCOL_ID],
            degree=gossipsub_params.DEGREE,
            degree_low=gossipsub_params.DEGREE_LOW,
            degree_high=gossipsub_params.DEGREE_HIGH,
            time_to_live=gossipsub_params.FANOUT_TTL,
            gossip_window=gossipsub_params.GOSSIP_WINDOW,
            gossip_history=gossipsub_params.GOSSIP_HISTORY,
            heartbeat_interval=gossipsub_params.HEARTBEAT_INTERVAL,
        )
        self.pubsub = Pubsub(
            host=self.host,
            router=gossipsub_router,
            my_id=self.peer_id,
        )

        self.chain = chain
        self._event_bus = event_bus

        self.handshaked_peers = PeerPool()

        self.run_task(self.start())
Example #3
0
async def test_lru_cache_two_nodes(monkeypatch):
    # two nodes with cache_size of 4
    # `node_a` send the following messages to node_b
    message_indices = [1, 1, 2, 1, 3, 1, 4, 1, 5, 1]
    # `node_b` should only receive the following
    expected_received_indices = [1, 2, 3, 4, 5, 1]

    node_a = await new_node(transport_opt=[str(LISTEN_MADDR)])
    node_b = await new_node(transport_opt=[str(LISTEN_MADDR)])

    await node_a.get_network().listen(LISTEN_MADDR)
    await node_b.get_network().listen(LISTEN_MADDR)

    supported_protocols = SUPPORTED_PROTOCOLS
    topic = "my_topic"

    # Mock `get_msg_id` to make us easier to manipulate `msg_id` by `data`.
    def get_msg_id(msg):
        # Originally it is `(msg.seqno, msg.from_id)`
        return (msg.data, msg.from_id)
    import libp2p.pubsub.pubsub
    monkeypatch.setattr(libp2p.pubsub.pubsub, "get_msg_id", get_msg_id)

    # Initialize Pubsub with a cache_size of 4
    cache_size = 4
    floodsub_a = FloodSub(supported_protocols)
    pubsub_a = Pubsub(node_a, floodsub_a, ID(b"a" * 32), cache_size)

    floodsub_b = FloodSub(supported_protocols)
    pubsub_b = Pubsub(node_b, floodsub_b, ID(b"b" * 32), cache_size)

    await connect(node_a, node_b)
    await asyncio.sleep(0.25)

    sub_b = await pubsub_b.subscribe(topic)
    await asyncio.sleep(0.25)

    def _make_testing_data(i: int) -> bytes:
        num_int_bytes = 4
        if i >= 2**(num_int_bytes * 8):
            raise ValueError("integer is too large to be serialized")
        return b"data" + i.to_bytes(num_int_bytes, "big")

    for index in message_indices:
        await pubsub_a.publish(topic, _make_testing_data(index))
    await asyncio.sleep(0.25)

    for index in expected_received_indices:
        res_b = await sub_b.get()
        assert res_b.data == _make_testing_data(index)
    assert sub_b.empty()

    # Success, terminate pending tasks.
    await cleanup()
Example #4
0
async def test_simple_three_nodes():
    # Want to pass message from A -> B -> C
    node_a = await new_node(transport_opt=["/ip4/127.0.0.1/tcp/0"])
    node_b = await new_node(transport_opt=["/ip4/127.0.0.1/tcp/0"])
    node_c = await new_node(transport_opt=["/ip4/127.0.0.1/tcp/0"])

    await node_a.get_network().listen(multiaddr.Multiaddr("/ip4/127.0.0.1/tcp/0"))
    await node_b.get_network().listen(multiaddr.Multiaddr("/ip4/127.0.0.1/tcp/0"))
    await node_c.get_network().listen(multiaddr.Multiaddr("/ip4/127.0.0.1/tcp/0"))

    supported_protocols = ["/floodsub/1.0.0"]

    floodsub_a = FloodSub(supported_protocols)
    pubsub_a = Pubsub(node_a, floodsub_a, "a")
    floodsub_b = FloodSub(supported_protocols)
    pubsub_b = Pubsub(node_b, floodsub_b, "b")
    floodsub_c = FloodSub(supported_protocols)
    pubsub_c = Pubsub(node_c, floodsub_c, "c")

    await connect(node_a, node_b)
    await connect(node_b, node_c)

    await asyncio.sleep(0.25)
    qb = await pubsub_b.subscribe("my_topic")
    qc = await pubsub_c.subscribe("my_topic")
    await asyncio.sleep(0.25)

    node_a_id = str(node_a.get_id())

    msg = MessageTalk(node_a_id, node_a_id, ["my_topic"], "some data", generate_message_id())

    await floodsub_a.publish(node_a.get_id(), msg.to_str())

    await asyncio.sleep(0.25)
    res_b = await qb.get()
    res_c = await qc.get()

    # Check that the msg received by node_b is the same
    # as the message sent by node_a
    assert res_b == msg.to_str()

    # res_c should match original msg but with b as sender
    node_b_id = str(node_b.get_id())
    msg.from_id = node_b_id

    assert res_c == msg.to_str()

    # Success, terminate pending tasks.
    await cleanup()
Example #5
0
 async def add_node(node_id: str) -> None:
     node = await new_node(transport_opt=[str(LISTEN_MADDR)])
     await node.get_network().listen(LISTEN_MADDR)
     node_map[node_id] = node
     pubsub_router = router_factory(protocols=obj["supported_protocols"])
     pubsub = Pubsub(node, pubsub_router, ID(node_id.encode()))
     pubsub_map[node_id] = pubsub
Example #6
0
 def __init__(self, fork_digest_provider: ForkDigestProvider,
              host: IHost) -> None:
     self._fork_digest_provider = fork_digest_provider
     self._host = host
     gossipsub_router = GossipSub(
         protocols=[GOSSIPSUB_PROTOCOL_ID],
         degree=self.DEGREE,
         degree_low=self.DEGREE_LOW,
         degree_high=self.DEGREE_HIGH,
         time_to_live=self.FANOUT_TTL,
         gossip_window=self.GOSSIP_WINDOW,
         gossip_history=self.GOSSIP_HISTORY,
         heartbeat_interval=self.HEARTBEAT_INTERVAL,
     )
     self.gossipsub = gossipsub_router
     self.pubsub = Pubsub(
         host=self._host,
         router=gossipsub_router,
         msg_id_constructor=get_content_addressed_msg_id,
     )
Example #7
0
async def test_simple_two_nodes():
    node_a = await new_node(transport_opt=["/ip4/127.0.0.1/tcp/0"])
    node_b = await new_node(transport_opt=["/ip4/127.0.0.1/tcp/0"])

    await node_a.get_network().listen(
        multiaddr.Multiaddr("/ip4/127.0.0.1/tcp/0"))
    await node_b.get_network().listen(
        multiaddr.Multiaddr("/ip4/127.0.0.1/tcp/0"))

    supported_protocols = ["/floodsub/1.0.0"]

    floodsub_a = FloodSub(supported_protocols)
    pubsub_a = Pubsub(node_a, floodsub_a, "a")
    floodsub_b = FloodSub(supported_protocols)
    pubsub_b = Pubsub(node_b, floodsub_b, "b")

    await connect(node_a, node_b)

    await asyncio.sleep(0.25)
    qb = await pubsub_b.subscribe("my_topic")

    await asyncio.sleep(0.25)

    node_a_id = str(node_a.get_id())

    next_msg_id_func = message_id_generator(0)
    msg = generate_RPC_packet(node_a_id, ["my_topic"], "some data",
                              next_msg_id_func())
    await floodsub_a.publish(node_a_id, msg.SerializeToString())
    await asyncio.sleep(0.25)

    res_b = await qb.get()

    # Check that the msg received by node_b is the same
    # as the message sent by node_a
    assert res_b.SerializeToString() == msg.publish[0].SerializeToString()

    # Success, terminate pending tasks.
    await cleanup()
Example #8
0
def create_pubsub_and_gossipsub_instances(libp2p_hosts, supported_protocols, degree, degree_low, \
    degree_high, time_to_live, gossip_window, gossip_history, heartbeat_interval):
    pubsubs = []
    gossipsubs = []
    for node in libp2p_hosts:
        gossipsub = GossipSub(supported_protocols, degree, degree_low,
                              degree_high, time_to_live, gossip_window,
                              gossip_history, heartbeat_interval)
        pubsub = Pubsub(node, gossipsub, "a")
        pubsubs.append(pubsub)
        gossipsubs.append(gossipsub)

    return pubsubs, gossipsubs
Example #9
0
async def test_init():
    node = await new_node(transport_opt=["/ip4/127.1/tcp/0"])

    await node.get_network().listen(multiaddr.Multiaddr("/ip4/127.1/tcp/0"))

    supported_protocols = ["/gossipsub/1.0.0"]

    gossipsub = GossipSub(supported_protocols, 3, 2, 4, 30)
    pubsub = Pubsub(node, gossipsub, "a")

    # Did it work?
    assert gossipsub and pubsub

    await cleanup()
Example #10
0
async def initialize_host(key, host='0.0.0.0', port=4025, listen=True, protocol_active=True):
    from .peers import publish_host, monitor_hosts
    from .protocol import AlephProtocol
    from .jobs import reconnect_p2p_job, tidy_http_peers_job

    assert key, "Host cannot be initialized without a key"

    tasks: List[Coroutine]

    priv = import_key(key)
    private_key = RSAPrivateKey(priv)
    public_key = private_key.get_public_key()
    keypair = KeyPair(private_key, public_key)
        
    transport_opt = f"/ip4/{host}/tcp/{port}"
    host = await new_node(transport_opt=[transport_opt],
                          key_pair=keypair)
    protocol = None
    # gossip = gossipsub.GossipSub([GOSSIPSUB_PROTOCOL_ID], 10, 9, 11, 30)
    # psub = Pubsub(host, gossip, host.get_id())
    flood = floodsub.FloodSub([FLOODSUB_PROTOCOL_ID, GOSSIPSUB_PROTOCOL_ID])
    psub = Pubsub(host, flood, host.get_id())
    if protocol_active:
        protocol = AlephProtocol(host)
    tasks = [
        reconnect_p2p_job(),
        tidy_http_peers_job(),
    ]
    if listen:
        from aleph.web import app
        
        await host.get_network().listen(multiaddr.Multiaddr(transport_opt))
        LOGGER.info("Listening on " + f'{transport_opt}/p2p/{host.get_id()}')
        ip = await get_IP()
        public_address = f'/ip4/{ip}/tcp/{port}/p2p/{host.get_id()}'
        http_port = app['config'].p2p.http_port.value
        public_http_address = f'http://{ip}:{http_port}'
        LOGGER.info("Probable public on " + public_address)
        # TODO: set correct interests and args here
        tasks += [
            publish_host(public_address, psub, peer_type="P2P"),
            publish_host(public_http_address, psub, peer_type="HTTP"),
            monitor_hosts(psub),
        ]

        # Enable message exchange using libp2p
        # host.set_stream_handler(PROTOCOL_ID, stream_handler)
        
    return (host, psub, protocol, tasks)
Example #11
0
async def initialize_host(host='0.0.0.0',
                          port=4025,
                          key=None,
                          listen=True,
                          protocol_active=True):
    from .peers import publish_host, monitor_hosts
    from .protocol import PROTOCOL_ID, AlephProtocol
    from .jobs import reconnect_p2p_job, tidy_http_peers_job
    if key is None:
        keypair = generate_keypair(print_info=listen)
    else:
        priv = import_key(key)
        private_key = RSAPrivateKey(priv)
        public_key = private_key.get_public_key()
        keypair = KeyPair(private_key, public_key)

    transport_opt = f"/ip4/{host}/tcp/{port}"
    host = await new_node(transport_opt=[transport_opt], key_pair=keypair)
    protocol = None
    # gossip = gossipsub.GossipSub([GOSSIPSUB_PROTOCOL_ID], 10, 9, 11, 30)
    # psub = Pubsub(host, gossip, host.get_id())
    flood = floodsub.FloodSub([FLOODSUB_PROTOCOL_ID, GOSSIPSUB_PROTOCOL_ID])
    psub = Pubsub(host, flood, host.get_id())
    if protocol_active:
        protocol = AlephProtocol(host)
    asyncio.create_task(reconnect_p2p_job())
    asyncio.create_task(tidy_http_peers_job())
    if listen:
        from aleph.web import app

        await host.get_network().listen(multiaddr.Multiaddr(transport_opt))
        LOGGER.info("Listening on " + f'{transport_opt}/p2p/{host.get_id()}')
        ip = await get_IP()
        public_address = f'/ip4/{ip}/tcp/{port}/p2p/{host.get_id()}'
        http_port = app['config'].p2p.http_port.value
        public_http_address = f'http://{ip}:{http_port}'
        LOGGER.info("Probable public on " + public_address)
        # TODO: set correct interests and args here
        asyncio.create_task(publish_host(public_address, psub,
                                         peer_type="P2P"))
        asyncio.create_task(
            publish_host(public_http_address, psub, peer_type="HTTP"))
        asyncio.create_task(monitor_hosts(psub))
        # host.set_stream_handler(PROTOCOL_ID, stream_handler)

    return (host, psub, protocol)
Example #12
0
    async def create(cls):
        """
        Create a new DummyAccountNode and attach a libp2p node, a floodsub, and a pubsub
        instance to this new node

        We use create as this serves as a factory function and allows us
        to use async await, unlike the init function
        """
        self = DummyAccountNode()

        libp2p_node = await new_node(transport_opt=["/ip4/127.0.0.1/tcp/0"])
        await libp2p_node.get_network().listen(multiaddr.Multiaddr("/ip4/127.0.0.1/tcp/0"))

        self.libp2p_node = libp2p_node

        self.floodsub = FloodSub(SUPPORTED_PUBSUB_PROTOCOLS)
        self.pubsub = Pubsub(self.libp2p_node, self.floodsub, "a")
        return self
Example #13
0
    def __init__(self,
                 privkey: datatypes.PrivateKey,
                 listen_ip: str,
                 listen_port: int,
                 security_protocol_ops: Dict[str, ISecureTransport],
                 muxer_protocol_ids: Tuple[str, ...],
                 gossipsub_params: Optional[GossipsubParams] = None,
                 cancel_token: CancelToken = None,
                 bootstrap_nodes: Tuple[Multiaddr, ...] = None,
                 preferred_nodes: Tuple[Multiaddr, ...] = None) -> None:
        super().__init__(cancel_token)
        self.listen_ip = listen_ip
        self.listen_port = listen_port
        self.privkey = privkey
        self.bootstrap_nodes = bootstrap_nodes
        self.preferred_nodes = preferred_nodes
        # TODO: Add key and peer_id to the peerstore
        network: INetwork = initialize_default_swarm(
            id_opt=peer_id_from_pubkey(self.privkey.public_key),
            transport_opt=[self.listen_maddr],
            muxer_opt=list(muxer_protocol_ids),
            sec_opt=security_protocol_ops,
            peerstore_opt=None,  # let the function initialize it
            disc_opt=None,  # no routing required here
        )
        self.host = BasicHost(network=network, router=None)

        if gossipsub_params is None:
            gossipsub_params = GossipsubParams()
        gossipsub_router = GossipSub(
            protocols=[GOSSIPSUB_PROTOCOL_ID],
            degree=gossipsub_params.DEGREE,
            degree_low=gossipsub_params.DEGREE_LOW,
            degree_high=gossipsub_params.DEGREE_HIGH,
            time_to_live=gossipsub_params.FANOUT_TTL,
            gossip_window=gossipsub_params.GOSSIP_WINDOW,
            gossip_history=gossipsub_params.GOSSIP_HISTORY,
            heartbeat_interval=gossipsub_params.HEARTBEAT_INTERVAL,
        )
        self.pubsub = Pubsub(
            host=self.host,
            router=gossipsub_router,
            my_id=self.peer_id,
        )
Example #14
0
class Node(BaseService):

    _is_started: bool = False

    key_pair: KeyPair
    listen_ip: str
    listen_port: int
    host: BasicHost
    pubsub: Pubsub
    bootstrap_nodes: Tuple[Multiaddr, ...]
    preferred_nodes: Tuple[Multiaddr, ...]
    chain: BaseBeaconChain

    handshaked_peers: PeerPool = None

    def __init__(
            self,
            key_pair: KeyPair,
            listen_ip: str,
            listen_port: int,
            chain: BaseBeaconChain,
            security_protocol_ops: Dict[TProtocol, BaseSecureTransport] = None,
            muxer_protocol_ops: Dict[TProtocol, IMuxedConn] = None,
            gossipsub_params: Optional[GossipsubParams] = None,
            cancel_token: CancelToken = None,
            bootstrap_nodes: Tuple[Multiaddr, ...] = (),
            preferred_nodes: Tuple[Multiaddr, ...] = ()) -> None:
        super().__init__(cancel_token)
        self.listen_ip = listen_ip
        self.listen_port = listen_port
        self.key_pair = key_pair
        self.bootstrap_nodes = bootstrap_nodes
        self.preferred_nodes = preferred_nodes
        # TODO: Add key and peer_id to the peerstore
        if security_protocol_ops is None:
            security_protocol_ops = {
                SecIOID: SecIOTransport(key_pair)
            }
        if muxer_protocol_ops is None:
            muxer_protocol_ops = {MPLEX_PROTOCOL_ID: Mplex}
        network: INetwork = initialize_default_swarm(
            key_pair=key_pair,
            transport_opt=[self.listen_maddr],
            muxer_opt=muxer_protocol_ops,
            sec_opt=security_protocol_ops,
            peerstore_opt=None,  # let the function initialize it
            disc_opt=None,  # no routing required here
        )
        self.host = BasicHost(network=network, router=None)

        if gossipsub_params is None:
            gossipsub_params = GossipsubParams()
        gossipsub_router = GossipSub(
            protocols=[GOSSIPSUB_PROTOCOL_ID],
            degree=gossipsub_params.DEGREE,
            degree_low=gossipsub_params.DEGREE_LOW,
            degree_high=gossipsub_params.DEGREE_HIGH,
            time_to_live=gossipsub_params.FANOUT_TTL,
            gossip_window=gossipsub_params.GOSSIP_WINDOW,
            gossip_history=gossipsub_params.GOSSIP_HISTORY,
            heartbeat_interval=gossipsub_params.HEARTBEAT_INTERVAL,
        )
        self.pubsub = Pubsub(
            host=self.host,
            router=gossipsub_router,
            my_id=self.peer_id,
        )

        self.chain = chain

        self.handshaked_peers = PeerPool()

        self.run_task(self.start())

    @property
    def is_started(self) -> bool:
        return self._is_started

    async def _run(self) -> None:
        self.logger.info("libp2p node %s is up", self.listen_maddr)
        await self.cancellation()

    async def start(self) -> None:
        # host
        self._register_rpc_handlers()
        # TODO: Register notifees
        await self.host.get_network().listen(self.listen_maddr)
        await self.connect_preferred_nodes()
        # TODO: Connect bootstrap nodes?

        # pubsub
        await self.pubsub.subscribe(PUBSUB_TOPIC_BEACON_BLOCK)
        await self.pubsub.subscribe(PUBSUB_TOPIC_BEACON_ATTESTATION)
        self._setup_topic_validators()

        self._is_started = True

    def _setup_topic_validators(self) -> None:
        self.pubsub.set_topic_validator(
            PUBSUB_TOPIC_BEACON_BLOCK,
            get_beacon_block_validator(self.chain),
            False,
        )
        self.pubsub.set_topic_validator(
            PUBSUB_TOPIC_BEACON_ATTESTATION,
            get_beacon_attestation_validator(self.chain),
            False,
        )

    async def dial_peer(self, ip: str, port: int, peer_id: ID) -> None:
        """
        Dial the peer ``peer_id`` through the IPv4 protocol
        """
        await self.host.connect(
            PeerInfo(
                peer_id=peer_id,
                addrs=[make_tcp_ip_maddr(ip, port)],
            )
        )
        try:
            # TODO: set a time limit on completing handshake
            await self.request_status(peer_id)
        except HandshakeFailure as e:
            self.logger.info("HandshakeFailure: %s", str(e))
            # TODO: handle it

    async def dial_peer_with_retries(self, ip: str, port: int, peer_id: ID) -> None:
        """
        Dial the peer ``peer_id`` through the IPv4 protocol
        """
        for i in range(DIAL_RETRY_COUNT):
            try:
                # exponential backoff...
                await asyncio.sleep(2**i + random.random())
                await self.dial_peer(ip, port, peer_id)
                return
            except ConnectionRefusedError:
                logger.debug(
                    "could not connect to peer %s at %s:%d;"
                    " retrying attempt %d of %d...",
                    peer_id,
                    ip,
                    port,
                    i,
                    DIAL_RETRY_COUNT,
                )
                continue
        raise ConnectionRefusedError

    async def dial_peer_maddr(self, maddr: Multiaddr) -> None:
        """
        Parse `maddr`, get the ip:port and PeerID, and call `dial_peer` with the parameters.
        """
        try:
            ip = maddr.value_for_protocol(protocols.P_IP4)
            port = maddr.value_for_protocol(protocols.P_TCP)
            peer_id = ID.from_base58(maddr.value_for_protocol(protocols.P_P2P))
            await self.dial_peer_with_retries(ip=ip, port=port, peer_id=peer_id)
        except Exception:
            traceback.print_exc()
            raise

    async def connect_preferred_nodes(self) -> None:
        results = await asyncio.gather(
            *(self.dial_peer_maddr(node_maddr)
              for node_maddr in self.preferred_nodes),
            return_exceptions=True,
        )
        for result in results:
            if isinstance(result, Exception):
                logger.warning("could not connect to %s", result)

    async def disconnect_peer(self, peer_id: ID) -> None:
        if peer_id in self.handshaked_peers:
            self.logger.debug("Disconnect from %s", peer_id)
            self.handshaked_peers.remove(peer_id)
            await self.host.disconnect(peer_id)
        else:
            self.logger.debug("Already disconnected from %s", peer_id)

    async def broadcast_beacon_block(self, block: BaseBeaconBlock) -> None:
        await self._broadcast_data(PUBSUB_TOPIC_BEACON_BLOCK, ssz.encode(block))

    async def broadcast_attestation(self, attestation: Attestation) -> None:
        await self._broadcast_data(PUBSUB_TOPIC_BEACON_ATTESTATION, ssz.encode(attestation))

    async def _broadcast_data(self, topic: str, data: bytes) -> None:
        await self.pubsub.publish(topic, data)

    @property
    def peer_id(self) -> ID:
        return self.host.get_id()

    @property
    def listen_maddr(self) -> Multiaddr:
        return make_tcp_ip_maddr(self.listen_ip, self.listen_port)

    @property
    def listen_maddr_with_peer_id(self) -> Multiaddr:
        return self.listen_maddr.encapsulate(Multiaddr(f"/p2p/{self.peer_id.to_base58()}"))

    @property
    def peer_store(self) -> PeerStore:
        return self.host.get_network().peerstore

    async def close(self) -> None:
        # FIXME: Add `tear_down` to `Swarm` in the upstream
        network = self.host.get_network()
        for listener in network.listeners.values():
            listener.server.close()
            await listener.server.wait_closed()
        # TODO: Add `close` in `Pubsub`

    def _register_rpc_handlers(self) -> None:
        self.host.set_stream_handler(REQ_RESP_STATUS_SSZ, self._handle_status)
        self.host.set_stream_handler(REQ_RESP_GOODBYE_SSZ, self._handle_goodbye)
        self.host.set_stream_handler(
            REQ_RESP_BEACON_BLOCKS_BY_RANGE_SSZ,
            self._handle_beacon_blocks_by_range,
        )
        self.host.set_stream_handler(
            REQ_RESP_BEACON_BLOCKS_BY_ROOT_SSZ,
            self._handle_beacon_blocks_by_root,
        )

    #
    # RPC Handlers
    #

    async def new_stream(self, peer_id: ID, protocol: TProtocol) -> INetStream:
        return await self.host.new_stream(peer_id, [protocol])

    @asynccontextmanager
    async def new_handshake_interaction(self, stream: INetStream) -> AsyncIterator[Interaction]:
        try:
            async with Interaction(stream) as interaction:
                peer_id = interaction.peer_id
                yield interaction
        except MessageIOFailure as error:
            await self.disconnect_peer(peer_id)
            raise HandshakeFailure() from error
        except PeerRespondedAnError as error:
            await stream.reset()
            await self.disconnect_peer(peer_id)
            raise HandshakeFailure() from error
        except IrrelevantNetwork as error:
            await stream.reset()
            asyncio.ensure_future(
                self.say_goodbye(peer_id, GoodbyeReasonCode.IRRELEVANT_NETWORK)
            )
            raise HandshakeFailure from error

    @asynccontextmanager
    async def post_handshake_handler_interaction(
        self,
        stream: INetStream
    ) -> AsyncIterator[Interaction]:
        try:
            async with Interaction(stream) as interaction:
                yield interaction
        except WriteMessageFailure as error:
            self.logger.debug("WriteMessageFailure %s", error)
            return
        except ReadMessageFailure as error:
            self.logger.debug("ReadMessageFailure %s", error)
            return
        except UnhandshakedPeer:
            await stream.reset()
            return

    @asynccontextmanager
    async def my_request_interaction(self, stream: INetStream) -> AsyncIterator[Interaction]:
        try:
            async with Interaction(stream) as interaction:
                yield interaction
        except (MessageIOFailure, UnhandshakedPeer, PeerRespondedAnError) as error:
            raise RequestFailure(str(error)) from error

    # TODO: Handle the reputation of peers. Deduct their scores and even disconnect when they
    #   behave.

    # TODO: Register notifee to the `Network` to
    #   - Record peers' joining time.
    #   - Disconnect peers when they fail to join in a certain amount of time.

    def _add_peer_from_status(self, peer_id: ID, status: Status) -> None:
        peer = Peer.from_status(self, peer_id, status)
        self.handshaked_peers.add(peer)
        self.logger.debug(
            "Handshake from %s is finished. Added to the `handshake_peers`",
            peer_id,
        )

    async def _handle_status(self, stream: INetStream) -> None:
        # TODO: Find out when we should respond the `ResponseCode`
        #   other than `ResponseCode.SUCCESS`.

        async with self.new_handshake_interaction(stream) as interaction:
            peer_id = interaction.peer_id
            peer_status = await interaction.read_request(Status)
            self.logger.info("Received Status from %s  %s", str(peer_id), peer_status)
            await validate_peer_status(self.chain, peer_status)

            my_status = get_my_status(self.chain)
            await interaction.write_response(my_status)

            self._add_peer_from_status(peer_id, peer_status)

            # Check if we are behind the peer
            compare_chain_tip_and_finalized_epoch(self.chain, peer_status)

    async def request_status(self, peer_id: ID) -> None:
        self.logger.info("Initiate handshake with %s", str(peer_id))

        stream = await self.new_stream(peer_id, REQ_RESP_STATUS_SSZ)
        async with self.new_handshake_interaction(stream) as interaction:
            my_status = get_my_status(self.chain)
            await interaction.write_request(my_status)
            peer_status = await interaction.read_response(Status)

            await validate_peer_status(self.chain, peer_status)

            self._add_peer_from_status(peer_id, peer_status)

            # Check if we are behind the peer
            compare_chain_tip_and_finalized_epoch(self.chain, peer_status)

    async def _handle_goodbye(self, stream: INetStream) -> None:
        async with Interaction(stream) as interaction:
            peer_id = interaction.peer_id
            try:
                await interaction.read_request(Goodbye)
            except ReadMessageFailure:
                pass
            await self.disconnect_peer(peer_id)

    async def say_goodbye(self, peer_id: ID, reason: GoodbyeReasonCode) -> None:
        stream = await self.new_stream(peer_id, REQ_RESP_GOODBYE_SSZ)
        async with Interaction(stream) as interaction:
            goodbye = Goodbye(reason)
            try:
                await interaction.write_request(goodbye)
            except WriteMessageFailure:
                pass
            await self.disconnect_peer(peer_id)

    def _check_peer_handshaked(self, peer_id: ID) -> None:
        if peer_id not in self.handshaked_peers:
            raise UnhandshakedPeer(peer_id)

    async def _handle_beacon_blocks_by_range(self, stream: INetStream) -> None:
        # TODO: Should it be a successful response if peer is requesting
        # blocks on a fork we don't have data for?

        async with self.post_handshake_handler_interaction(stream) as interaction:
            peer_id = interaction.peer_id
            self._check_peer_handshaked(peer_id)

            request = await interaction.read_request(BeaconBlocksByRangeRequest)
            try:
                blocks = get_requested_beacon_blocks(self.chain, request)
            except InvalidRequest as error:
                error_message = str(error)[:128]
                await interaction.write_error_response(error_message, ResponseCode.INVALID_REQUEST)
            else:
                await interaction.write_chunk_response(blocks)

    async def request_beacon_blocks_by_range(
        self,
        peer_id: ID,
        head_block_root: SigningRoot,
        start_slot: Slot,
        count: int,
        step: int,
    ) -> Tuple[BaseBeaconBlock, ...]:
        stream = await self.new_stream(peer_id, REQ_RESP_BEACON_BLOCKS_BY_RANGE_SSZ)
        async with self.my_request_interaction(stream) as interaction:
            self._check_peer_handshaked(peer_id)
            request = BeaconBlocksByRangeRequest(
                head_block_root=head_block_root,
                start_slot=start_slot,
                count=count,
                step=step,
            )
            await interaction.write_request(request)
            blocks = tuple([
                block async for block in
                interaction.read_chunk_response(BeaconBlock, count)
            ])

            return blocks

    async def _handle_beacon_blocks_by_root(self, stream: INetStream) -> None:
        async with self.post_handshake_handler_interaction(stream) as interaction:
            peer_id = interaction.peer_id
            self._check_peer_handshaked(peer_id)
            request = await interaction.read_request(BeaconBlocksByRootRequest)
            blocks = get_beacon_blocks_by_root(self.chain, request)

            await interaction.write_chunk_response(blocks)

    async def request_beacon_blocks_by_root(
            self,
            peer_id: ID,
            block_roots: Sequence[SigningRoot]) -> Tuple[BaseBeaconBlock, ...]:
        stream = await self.new_stream(peer_id, REQ_RESP_BEACON_BLOCKS_BY_ROOT_SSZ)
        async with self.my_request_interaction(stream) as interaction:
            self._check_peer_handshaked(peer_id)
            request = BeaconBlocksByRootRequest(block_roots=block_roots)
            await interaction.write_request(request)
            blocks = tuple([
                block async for block in
                interaction.read_chunk_response(BeaconBlock, len(block_roots))
            ])

            return blocks
 async def create(cls):
     host = await new_node()
     floodsub = FloodSub(SUPPORTED_PUBSUB_PROTOCOLS)
     pubsub = Pubsub(host, floodsub, "test")
     return cls(host, pubsub)
Example #16
0
class Node(BaseService):

    _is_started: bool = False

    key_pair: KeyPair
    listen_ip: str
    listen_port: int
    host: BasicHost
    pubsub: Pubsub
    bootstrap_nodes: Optional[Tuple[Multiaddr, ...]]
    preferred_nodes: Optional[Tuple[Multiaddr, ...]]
    chain: BaseBeaconChain

    handshaked_peers: PeerPool = None

    def __init__(
            self,
            key_pair: KeyPair,
            listen_ip: str,
            listen_port: int,
            chain: BaseBeaconChain,
            security_protocol_ops: Dict[TProtocol, BaseSecureTransport] = None,
            muxer_protocol_ops: Dict[TProtocol, IMuxedConn] = None,
            gossipsub_params: Optional[GossipsubParams] = None,
            cancel_token: CancelToken = None,
            bootstrap_nodes: Tuple[Multiaddr, ...] = None,
            preferred_nodes: Tuple[Multiaddr, ...] = None) -> None:
        super().__init__(cancel_token)
        self.listen_ip = listen_ip
        self.listen_port = listen_port
        self.key_pair = key_pair
        self.bootstrap_nodes = bootstrap_nodes
        self.preferred_nodes = preferred_nodes
        # TODO: Add key and peer_id to the peerstore
        if security_protocol_ops is None:
            security_protocol_ops = {
                PLAINTEXT_PROTOCOL_ID: InsecureTransport(key_pair)
            }
        if muxer_protocol_ops is None:
            muxer_protocol_ops = {MPLEX_PROTOCOL_ID: Mplex}
        network: INetwork = initialize_default_swarm(
            key_pair=key_pair,
            transport_opt=[self.listen_maddr],
            muxer_opt=muxer_protocol_ops,
            sec_opt=security_protocol_ops,
            peerstore_opt=None,  # let the function initialize it
            disc_opt=None,  # no routing required here
        )
        self.host = BasicHost(network=network, router=None)

        if gossipsub_params is None:
            gossipsub_params = GossipsubParams()
        gossipsub_router = GossipSub(
            protocols=[GOSSIPSUB_PROTOCOL_ID],
            degree=gossipsub_params.DEGREE,
            degree_low=gossipsub_params.DEGREE_LOW,
            degree_high=gossipsub_params.DEGREE_HIGH,
            time_to_live=gossipsub_params.FANOUT_TTL,
            gossip_window=gossipsub_params.GOSSIP_WINDOW,
            gossip_history=gossipsub_params.GOSSIP_HISTORY,
            heartbeat_interval=gossipsub_params.HEARTBEAT_INTERVAL,
        )
        self.pubsub = Pubsub(
            host=self.host,
            router=gossipsub_router,
            my_id=self.peer_id,
        )

        self.chain = chain

        self.handshaked_peers = PeerPool()

        self.run_task(self.start())

    @property
    def is_started(self) -> bool:
        return self._is_started

    async def _run(self) -> None:
        self.logger.info("libp2p node %s is up", self.listen_maddr)
        await self.cancellation()

    async def start(self) -> None:
        # host
        self._register_rpc_handlers()
        # TODO: Register notifees
        await self.host.get_network().listen(self.listen_maddr)
        await self.connect_preferred_nodes()
        # TODO: Connect bootstrap nodes?

        # pubsub
        await self.pubsub.subscribe(PUBSUB_TOPIC_BEACON_BLOCK)
        await self.pubsub.subscribe(PUBSUB_TOPIC_BEACON_ATTESTATION)
        self._setup_topic_validators()

        self._is_started = True

    def _setup_topic_validators(self) -> None:
        self.pubsub.set_topic_validator(
            PUBSUB_TOPIC_BEACON_BLOCK,
            get_beacon_block_validator(self.chain),
            False,
        )
        self.pubsub.set_topic_validator(
            PUBSUB_TOPIC_BEACON_ATTESTATION,
            get_beacon_attestation_validator(self.chain),
            False,
        )

    async def dial_peer(self, ip: str, port: int, peer_id: ID) -> None:
        """
        Dial the peer ``peer_id`` through the IPv4 protocol
        """
        await self.host.connect(
            PeerInfo(
                peer_id=peer_id,
                addrs=[make_tcp_ip_maddr(ip, port)],
            )
        )

    async def dial_peer_maddr(self, maddr: Multiaddr) -> None:
        """
        Parse `maddr`, get the ip:port and PeerID, and call `dial_peer` with the parameters.
        """
        ip = maddr.value_for_protocol(protocols.P_IP4)
        port = maddr.value_for_protocol(protocols.P_TCP)
        peer_id = ID.from_base58(maddr.value_for_protocol(protocols.P_P2P))
        await self.dial_peer(ip=ip, port=port, peer_id=peer_id)

    async def connect_preferred_nodes(self) -> None:
        if self.preferred_nodes is None or len(self.preferred_nodes) == 0:
            return
        await asyncio.wait([
            self.dial_peer_maddr(node_maddr)
            for node_maddr in self.preferred_nodes
        ])

    async def disconnect_peer(self, peer_id: ID) -> None:
        if peer_id in self.handshaked_peers:
            self.logger.debug("Disconnect from %s", peer_id)
            self.handshaked_peers.remove(peer_id)
            await self.host.disconnect(peer_id)
        else:
            self.logger.debug("Already disconnected from %s", peer_id)

    async def broadcast_beacon_block(self, block: BaseBeaconBlock) -> None:
        await self._broadcast_data(PUBSUB_TOPIC_BEACON_BLOCK, ssz.encode(block))

    async def broadcast_attestation(self, attestation: Attestation) -> None:
        await self._broadcast_data(PUBSUB_TOPIC_BEACON_ATTESTATION, ssz.encode(attestation))

    async def _broadcast_data(self, topic: str, data: bytes) -> None:
        await self.pubsub.publish(topic, data)

    @property
    def peer_id(self) -> ID:
        return self.host.get_id()

    @property
    def listen_maddr(self) -> Multiaddr:
        return make_tcp_ip_maddr(self.listen_ip, self.listen_port)

    @property
    def listen_maddr_with_peer_id(self) -> Multiaddr:
        return self.listen_maddr.encapsulate(Multiaddr(f"/p2p/{self.peer_id.to_base58()}"))

    @property
    def peer_store(self) -> PeerStore:
        return self.host.get_network().peerstore

    async def close(self) -> None:
        # FIXME: Add `tear_down` to `Swarm` in the upstream
        network = self.host.get_network()
        for listener in network.listeners.values():
            listener.server.close()
            await listener.server.wait_closed()
        # TODO: Add `close` in `Pubsub`

    def _register_rpc_handlers(self) -> None:
        self.host.set_stream_handler(REQ_RESP_HELLO_SSZ, self._handle_hello)
        self.host.set_stream_handler(REQ_RESP_GOODBYE_SSZ, self._handle_goodbye)
        self.host.set_stream_handler(REQ_RESP_BEACON_BLOCKS_SSZ, self._handle_beacon_blocks)
        self.host.set_stream_handler(
            REQ_RESP_RECENT_BEACON_BLOCKS_SSZ,
            self._handle_recent_beacon_blocks,
        )

    #
    # RPC Handlers
    #

    # TODO: Add a wrapper or decorator to handle the exceptions in handlers,
    #   to close the streams safely. Probably starting from: if the function
    #   returns successfully, then close the stream. Otherwise, reset the stream.

    # TODO: Handle the reputation of peers. Deduct their scores and even disconnect when they
    #   behave.

    # TODO: Register notifee to the `Network` to
    #   - Record peers' joining time.
    #   - Disconnect peers when they fail to join in a certain amount of time.

    async def _validate_hello_req(self, hello_other_side: HelloRequest) -> None:
        state_machine = self.chain.get_state_machine()
        state = self.chain.get_head_state()
        config = state_machine.config
        if hello_other_side.fork_version != state.fork.current_version:
            raise ValidationError(
                "`fork_version` mismatches: "
                f"hello_other_side.fork_version={hello_other_side.fork_version}, "
                f"state.fork.current_version={state.fork.current_version}"
            )

        # Can not validate the checkpoint with `finalized_epoch` higher than ours
        if hello_other_side.finalized_epoch > state.finalized_checkpoint.epoch:
            return

        # Get the finalized root at `hello_other_side.finalized_epoch`
        # Edge case where nothing is finalized yet
        if (
            hello_other_side.finalized_epoch == 0 and
            hello_other_side.finalized_root == ZERO_SIGNING_ROOT
        ):
            return

        finalized_epoch_start_slot = compute_start_slot_of_epoch(
            hello_other_side.finalized_epoch,
            config.SLOTS_PER_EPOCH,
        )
        finalized_root = self.chain.get_canonical_block_root(
            finalized_epoch_start_slot)

        if hello_other_side.finalized_root != finalized_root:
            raise ValidationError(
                "`finalized_root` mismatches: "
                f"hello_other_side.finalized_root={hello_other_side.finalized_root}, "
                f"hello_other_side.finalized_epoch={hello_other_side.finalized_epoch}, "
                f"our `finalized_root` at the same `finalized_epoch`={finalized_root}"
            )

    def _make_hello_packet(self) -> HelloRequest:
        state = self.chain.get_head_state()
        head = self.chain.get_canonical_head()
        finalized_checkpoint = state.finalized_checkpoint
        return HelloRequest(
            fork_version=state.fork.current_version,
            finalized_root=finalized_checkpoint.root,
            finalized_epoch=finalized_checkpoint.epoch,
            head_root=head.hash_tree_root,
            head_slot=head.slot,
        )

    def _compare_chain_tip_and_finalized_epoch(self,
                                               peer_finalized_epoch: Epoch,
                                               peer_head_slot: Slot) -> None:
        checkpoint = self.chain.get_head_state().finalized_checkpoint
        head_block = self.chain.get_canonical_head()
        peer_has_higher_finalized_epoch = peer_finalized_epoch > checkpoint.epoch
        peer_has_equal_finalized_epoch = peer_finalized_epoch == checkpoint.epoch
        peer_has_higher_head_slot = peer_head_slot > head_block.slot
        if (
            peer_has_higher_finalized_epoch or
            (peer_has_equal_finalized_epoch and peer_has_higher_head_slot)
        ):
            # TODO: kickoff syncing process with this peer
            self.logger.debug("Peer's chain is ahead of us, start syncing with the peer.")
            pass

    async def _handle_hello(self, stream: INetStream) -> None:
        # TODO: Find out when we should respond the `ResponseCode`
        #   other than `ResponseCode.SUCCESS`.

        peer_id = stream.mplex_conn.peer_id

        self.logger.debug("Waiting for hello from the other side")
        try:
            hello_other_side = await read_req(stream, HelloRequest)
            has_error = False
        except (ReadMessageFailure, MplexStreamEOF, MplexStreamReset) as error:
            has_error = True
            if isinstance(error, ReadMessageFailure):
                await stream.reset()
            elif isinstance(error, MplexStreamEOF):
                await stream.close()
        finally:
            if has_error:
                await self.disconnect_peer(peer_id)
                return
        self.logger.debug("Received the hello message %s", hello_other_side)

        try:
            await self._validate_hello_req(hello_other_side)
        except ValidationError as error:
            self.logger.info(
                "Handshake failed: hello message %s is invalid: %s",
                hello_other_side,
                str(error)
            )
            await stream.reset()
            await self.say_goodbye(peer_id, GoodbyeReasonCode.IRRELEVANT_NETWORK)
            await self.disconnect_peer(peer_id)
            return

        hello_mine = self._make_hello_packet()

        self.logger.debug("Sending our hello message %s", hello_mine)
        try:
            await write_resp(stream, hello_mine, ResponseCode.SUCCESS)
            has_error = False
        except (WriteMessageFailure, MplexStreamEOF, MplexStreamReset) as error:
            has_error = True
            if isinstance(error, WriteMessageFailure):
                await stream.reset()
            elif isinstance(error, MplexStreamEOF):
                await stream.close()
        finally:
            if has_error:
                self.logger.info(
                    "Handshake failed: failed to write message %s",
                    hello_mine,
                )
                await self.disconnect_peer(peer_id)
                return

        if peer_id not in self.handshaked_peers:
            peer = Peer.from_hello_request(self, peer_id, hello_other_side)
            self.handshaked_peers.add(peer)
            self.logger.debug(
                "Handshake from %s is finished. Added to the `handshake_peers`",
                peer_id,
            )

        # Check if we are behind the peer
        self._compare_chain_tip_and_finalized_epoch(
            hello_other_side.finalized_epoch,
            hello_other_side.head_slot,
        )

        await stream.close()

    async def say_hello(self, peer_id: ID) -> None:
        hello_mine = self._make_hello_packet()

        self.logger.debug(
            "Opening new stream to peer=%s with protocols=%s",
            peer_id,
            [REQ_RESP_HELLO_SSZ],
        )
        stream = await self.host.new_stream(peer_id, [REQ_RESP_HELLO_SSZ])
        self.logger.debug("Sending our hello message %s", hello_mine)
        try:
            await write_req(stream, hello_mine)
            has_error = False
        except (WriteMessageFailure, MplexStreamEOF, MplexStreamReset) as error:
            has_error = True
            if isinstance(error, WriteMessageFailure):
                await stream.reset()
            elif isinstance(error, MplexStreamEOF):
                await stream.close()
        finally:
            if has_error:
                await self.disconnect_peer(peer_id)
                error_msg = f"fail to write request={hello_mine}"
                self.logger.info("Handshake failed: %s", error_msg)
                raise HandshakeFailure(error_msg)

        self.logger.debug("Waiting for hello from the other side")
        try:
            resp_code, hello_other_side = await read_resp(stream, HelloRequest)
            has_error = False
        except (ReadMessageFailure, MplexStreamEOF, MplexStreamReset) as error:
            has_error = True
            if isinstance(error, ReadMessageFailure):
                await stream.reset()
            elif isinstance(error, MplexStreamEOF):
                await stream.close()
        finally:
            if has_error:
                await self.disconnect_peer(peer_id)
                self.logger.info("Handshake failed: fail to read the response")
                raise HandshakeFailure("fail to read the response")

        self.logger.debug(
            "Received the hello message %s, resp_code=%s",
            hello_other_side,
            resp_code,
        )

        # TODO: Handle the case when `resp_code` is not success.
        if resp_code != ResponseCode.SUCCESS:
            # TODO: Do something according to the `ResponseCode`
            error_msg = (
                "resp_code != ResponseCode.SUCCESS, "
                f"resp_code={resp_code}, error_msg={hello_other_side}"
            )
            self.logger.info("Handshake failed: %s", error_msg)
            await stream.reset()
            await self.disconnect_peer(peer_id)
            raise HandshakeFailure(error_msg)

        hello_other_side = cast(HelloRequest, hello_other_side)
        try:
            await self._validate_hello_req(hello_other_side)
        except ValidationError as error:
            error_msg = f"hello message {hello_other_side} is invalid: {str(error)}"
            self.logger.info(
                "Handshake failed: %s. Disconnecting %s",
                error_msg,
                peer_id,
            )
            await stream.reset()
            await self.say_goodbye(peer_id, GoodbyeReasonCode.IRRELEVANT_NETWORK)
            await self.disconnect_peer(peer_id)
            raise HandshakeFailure(error_msg) from error

        if peer_id not in self.handshaked_peers:
            peer = Peer.from_hello_request(self, peer_id, hello_other_side)
            self.handshaked_peers.add(peer)
            self.logger.debug(
                "Handshake to peer=%s is finished. Added to the `handshake_peers`",
                peer_id,
            )

        # Check if we are behind the peer
        self._compare_chain_tip_and_finalized_epoch(
            hello_other_side.finalized_epoch,
            hello_other_side.head_slot,
        )

        await stream.close()

    async def _handle_goodbye(self, stream: INetStream) -> None:
        peer_id = stream.mplex_conn.peer_id
        self.logger.debug("Waiting for goodbye from %s", peer_id)
        try:
            goodbye = await read_req(stream, Goodbye)
            has_error = False
        except (ReadMessageFailure, MplexStreamEOF, MplexStreamReset) as error:
            has_error = True
            if isinstance(error, ReadMessageFailure):
                await stream.reset()
            elif isinstance(error, MplexStreamEOF):
                await stream.close()

        self.logger.debug("Received the goodbye message %s", goodbye)

        if not has_error:
            await stream.close()
        await self.disconnect_peer(peer_id)

    async def say_goodbye(self, peer_id: ID, reason: GoodbyeReasonCode) -> None:
        goodbye = Goodbye(reason)
        self.logger.debug(
            "Opening new stream to peer=%s with protocols=%s",
            peer_id,
            [REQ_RESP_GOODBYE_SSZ],
        )
        stream = await self.host.new_stream(peer_id, [REQ_RESP_GOODBYE_SSZ])
        self.logger.debug("Sending our goodbye message %s", goodbye)
        try:
            await write_req(stream, goodbye)
            has_error = False
        except (WriteMessageFailure, MplexStreamEOF, MplexStreamReset) as error:
            has_error = True
            if isinstance(error, WriteMessageFailure):
                await stream.reset()
            elif isinstance(error, MplexStreamEOF):
                await stream.close()

        if not has_error:
            await stream.close()
        await self.disconnect_peer(peer_id)

    @to_tuple
    def _get_blocks_from_canonical_chain_by_slot(
        self,
        slot_of_requested_blocks: Sequence[Slot],
    ) -> Iterable[BaseBeaconBlock]:
        # If peer's head block is on our canonical chain,
        # start getting the requested blocks by slots.
        for slot in slot_of_requested_blocks:
            try:
                block = self.chain.get_canonical_block_by_slot(slot)
            except BlockNotFound:
                pass
            else:
                yield block

    @to_tuple
    def _get_blocks_from_fork_chain_by_root(
        self,
        start_slot: Slot,
        peer_head_block: BaseBeaconBlock,
        slot_of_requested_blocks: Sequence[Slot],
    ) -> Iterable[BaseBeaconBlock]:
        # Peer's head block is on a fork chain,
        # start getting the requested blocks by
        # traversing the history from the head.

        # `slot_of_requested_blocks` starts with earliest slot
        # and end with most recent slot, so we start traversing
        # from the most recent slot.
        cur_index = len(slot_of_requested_blocks) - 1
        block = peer_head_block
        if block.slot == slot_of_requested_blocks[cur_index]:
            yield block
            cur_index -= 1
        while block.slot > start_slot and cur_index >= 0:
            try:
                block = self.chain.get_block_by_root(block.parent_root)
            except (BlockNotFound, ValidationError):
                # This should not happen as we only persist block if its
                # ancestors are also in the database.
                break
            else:
                while block.slot < slot_of_requested_blocks[cur_index]:
                    if cur_index > 0:
                        cur_index -= 1
                    else:
                        break
                if block.slot == slot_of_requested_blocks[cur_index]:
                    yield block

    def _validate_start_slot(self, start_slot: Slot) -> None:
        config = self.chain.get_state_machine().config
        state = self.chain.get_head_state()
        finalized_epoch_start_slot = compute_start_slot_of_epoch(
            epoch=state.finalized_checkpoint.epoch,
            slots_per_epoch=config.SLOTS_PER_EPOCH,
        )
        if start_slot < finalized_epoch_start_slot:
            raise ValidationError(
                f"`start_slot`({start_slot}) lower than our"
                f" latest finalized slot({finalized_epoch_start_slot})"
            )

    def _get_requested_beacon_blocks(
        self,
        beacon_blocks_request: BeaconBlocksRequest,
        requested_head_block: BaseBeaconBlock,
    ) -> Tuple[BaseBeaconBlock, ...]:
        slot_of_requested_blocks = tuple(
            beacon_blocks_request.start_slot + i * beacon_blocks_request.step
            for i in range(beacon_blocks_request.count)
        )
        self.logger.info("slot_of_requested_blocks: %s", slot_of_requested_blocks)
        slot_of_requested_blocks = tuple(
            filter(lambda slot: slot <= requested_head_block.slot, slot_of_requested_blocks)
        )

        if len(slot_of_requested_blocks) == 0:
            return tuple()

        # We have the peer's head block in our database,
        # next check if the head block is on our canonical chain.
        try:
            canonical_block_at_slot = self.chain.get_canonical_block_by_slot(
                requested_head_block.slot
            )
            block_match = canonical_block_at_slot == requested_head_block
        except BlockNotFound:
            self.logger.debug(
                (
                    "The requested head block is not on our canonical chain  "
                    "requested_head_block: %s  canonical_block_at_slot: %s"
                ),
                requested_head_block,
                canonical_block_at_slot,
            )
            block_match = False
        finally:
            if block_match:
                # Peer's head block is on our canonical chain
                return self._get_blocks_from_canonical_chain_by_slot(
                    slot_of_requested_blocks
                )
            else:
                # Peer's head block is not on our canonical chain
                # Validate `start_slot` is greater than our latest finalized slot
                self._validate_start_slot(beacon_blocks_request.start_slot)
                return self._get_blocks_from_fork_chain_by_root(
                    beacon_blocks_request.start_slot,
                    requested_head_block,
                    slot_of_requested_blocks,
                )

    async def _handle_beacon_blocks(self, stream: INetStream) -> None:
        peer_id = stream.mplex_conn.peer_id
        if peer_id not in self.handshaked_peers:
            self.logger.info(
                "Processing beacon blocks request failed: not handshaked with peer=%s yet",
                peer_id,
            )
            await stream.reset()
            return

        self.logger.debug("Waiting for beacon blocks request from the other side")
        try:
            beacon_blocks_request = await read_req(stream, BeaconBlocksRequest)
            has_error = False
        except (ReadMessageFailure, MplexStreamEOF, MplexStreamReset) as error:
            has_error = True
            if isinstance(error, ReadMessageFailure):
                await stream.reset()
            elif isinstance(error, MplexStreamEOF):
                await stream.close()
        finally:
            if has_error:
                return
        self.logger.debug("Received the beacon blocks request message %s", beacon_blocks_request)

        try:
            requested_head_block = self.chain.get_block_by_hash_tree_root(
                beacon_blocks_request.head_block_root
            )
        except (BlockNotFound, ValidationError) as error:
            self.logger.info("Sending empty blocks, reason: %s", error)
            # We don't have the chain data peer is requesting
            requested_beacon_blocks: Tuple[BaseBeaconBlock, ...] = tuple()
        else:
            # Check if slot of specified head block is greater than specified start slot
            if requested_head_block.slot < beacon_blocks_request.start_slot:
                reason = (
                    f"Invalid request: head block slot({requested_head_block.slot})"
                    f" lower than `start_slot`({beacon_blocks_request.start_slot})"
                )
                try:
                    await write_resp(stream, reason, ResponseCode.INVALID_REQUEST)
                    has_error = False
                except (WriteMessageFailure, MplexStreamEOF, MplexStreamReset) as error:
                    has_error = True
                    if isinstance(error, WriteMessageFailure):
                        await stream.reset()
                    elif isinstance(error, MplexStreamEOF):
                        await stream.close()
                finally:
                    if has_error:
                        self.logger.info(
                            "Processing beacon blocks request failed: failed to write message %s",
                            reason,
                        )
                        return
                await stream.close()
                return
            else:
                try:
                    requested_beacon_blocks = self._get_requested_beacon_blocks(
                        beacon_blocks_request, requested_head_block
                    )
                except ValidationError as val_error:
                    reason = "Invalid request: " + str(val_error)
                    try:
                        await write_resp(stream, reason, ResponseCode.INVALID_REQUEST)
                        has_error = False
                    except (WriteMessageFailure, MplexStreamEOF, MplexStreamReset) as error:
                        has_error = True
                        if isinstance(error, WriteMessageFailure):
                            await stream.reset()
                        elif isinstance(error, MplexStreamEOF):
                            await stream.close()
                    finally:
                        if has_error:
                            self.logger.info(
                                "Processing beacon blocks request failed: "
                                "failed to write message %s",
                                reason,
                            )
                            return
                    await stream.close()
                    return
        # TODO: Should it be a successful response if peer is requesting
        # blocks on a fork we don't have data for?
        beacon_blocks_response = BeaconBlocksResponse(blocks=requested_beacon_blocks)
        self.logger.debug("Sending beacon blocks response %s", beacon_blocks_response)
        try:
            await write_resp(stream, beacon_blocks_response, ResponseCode.SUCCESS)
            has_error = False
        except (WriteMessageFailure, MplexStreamEOF, MplexStreamReset) as error:
            has_error = True
            if isinstance(error, WriteMessageFailure):
                await stream.reset()
            elif isinstance(error, MplexStreamEOF):
                await stream.close()
        finally:
            if has_error:
                self.logger.info(
                    "Processing beacon blocks request failed: failed to write message %s",
                    beacon_blocks_response,
                )
                return

        self.logger.debug(
            "Processing beacon blocks request from %s is finished",
            peer_id,
        )
        await stream.close()

    async def request_beacon_blocks(self,
                                    peer_id: ID,
                                    head_block_root: HashTreeRoot,
                                    start_slot: Slot,
                                    count: int,
                                    step: int) -> Tuple[BaseBeaconBlock, ...]:
        if peer_id not in self.handshaked_peers:
            error_msg = f"not handshaked with peer={peer_id} yet"
            self.logger.info("Request beacon block failed: %s", error_msg)
            raise RequestFailure(error_msg)

        beacon_blocks_request = BeaconBlocksRequest(
            head_block_root=head_block_root,
            start_slot=start_slot,
            count=count,
            step=step,
        )

        self.logger.debug(
            "Opening new stream to peer=%s with protocols=%s",
            peer_id,
            [REQ_RESP_BEACON_BLOCKS_SSZ],
        )
        stream = await self.host.new_stream(peer_id, [REQ_RESP_BEACON_BLOCKS_SSZ])
        self.logger.debug("Sending beacon blocks request %s", beacon_blocks_request)
        try:
            await write_req(stream, beacon_blocks_request)
            has_error = False
        except (WriteMessageFailure, MplexStreamEOF, MplexStreamReset) as error:
            has_error = True
            if isinstance(error, WriteMessageFailure):
                await stream.reset()
            elif isinstance(error, MplexStreamEOF):
                await stream.close()
        finally:
            if has_error:
                error_msg = f"fail to write request={beacon_blocks_request}"
                self.logger.info("Request beacon blocks failed: %s", error_msg)
                raise RequestFailure(error_msg)

        self.logger.debug("Waiting for beacon blocks response")
        try:
            resp_code, beacon_blocks_response = await read_resp(stream, BeaconBlocksResponse)
            has_error = False
        except (ReadMessageFailure, MplexStreamEOF, MplexStreamReset) as error:
            has_error = True
            if isinstance(error, ReadMessageFailure):
                await stream.reset()
            elif isinstance(error, MplexStreamEOF):
                await stream.close()
        finally:
            if has_error:
                self.logger.info("Request beacon blocks failed: fail to read the response")
                raise RequestFailure("fail to read the response")

        self.logger.debug(
            "Received beacon blocks response %s, resp_code=%s",
            beacon_blocks_response,
            resp_code,
        )

        if resp_code != ResponseCode.SUCCESS:
            error_msg = (
                "resp_code != ResponseCode.SUCCESS, "
                f"resp_code={resp_code}, error_msg={beacon_blocks_response}"
            )
            self.logger.info("Request beacon blocks failed: %s", error_msg)
            await stream.reset()
            raise RequestFailure(error_msg)

        await stream.close()

        beacon_blocks_response = cast(BeaconBlocksResponse, beacon_blocks_response)
        return beacon_blocks_response.blocks

    async def _handle_recent_beacon_blocks(self, stream: INetStream) -> None:
        peer_id = stream.mplex_conn.peer_id
        if peer_id not in self.handshaked_peers:
            self.logger.info(
                "Processing recent beacon blocks request failed: not handshaked with peer=%s yet",
                peer_id,
            )
            await stream.reset()
            return

        self.logger.debug("Waiting for recent beacon blocks request from the other side")
        try:
            recent_beacon_blocks_request = await read_req(stream, RecentBeaconBlocksRequest)
            has_error = False
        except (ReadMessageFailure, MplexStreamEOF, MplexStreamReset) as error:
            has_error = True
            if isinstance(error, ReadMessageFailure):
                await stream.reset()
            elif isinstance(error, MplexStreamEOF):
                await stream.close()
        finally:
            if has_error:
                return
        self.logger.debug(
            "Received the recent beacon blocks request message %s",
            recent_beacon_blocks_request,
        )

        recent_beacon_blocks = []
        for block_root in recent_beacon_blocks_request.block_roots:
            try:
                block = self.chain.get_block_by_hash_tree_root(block_root)
            except (BlockNotFound, ValidationError):
                pass
            else:
                recent_beacon_blocks.append(block)

        recent_beacon_blocks_response = RecentBeaconBlocksResponse(blocks=recent_beacon_blocks)
        self.logger.debug("Sending recent beacon blocks response %s", recent_beacon_blocks_response)
        try:
            await write_resp(stream, recent_beacon_blocks_response, ResponseCode.SUCCESS)
            has_error = False
        except (WriteMessageFailure, MplexStreamEOF, MplexStreamReset) as error:
            has_error = True
            if isinstance(error, WriteMessageFailure):
                await stream.reset()
            elif isinstance(error, MplexStreamEOF):
                await stream.close()
        finally:
            if has_error:
                self.logger.info(
                    "Processing recent beacon blocks request failed: failed to write message %s",
                    recent_beacon_blocks_response,
                )
                return

        self.logger.debug(
            "Processing recent beacon blocks request from %s is finished",
            peer_id,
        )
        await stream.close()

    async def request_recent_beacon_blocks(
            self,
            peer_id: ID,
            block_roots: Sequence[HashTreeRoot]) -> Tuple[BaseBeaconBlock, ...]:
        if peer_id not in self.handshaked_peers:
            error_msg = f"not handshaked with peer={peer_id} yet"
            self.logger.info("Request recent beacon block failed: %s", error_msg)
            raise RequestFailure(error_msg)

        recent_beacon_blocks_request = RecentBeaconBlocksRequest(block_roots=block_roots)

        self.logger.debug(
            "Opening new stream to peer=%s with protocols=%s",
            peer_id,
            [REQ_RESP_RECENT_BEACON_BLOCKS_SSZ],
        )
        stream = await self.host.new_stream(peer_id, [REQ_RESP_RECENT_BEACON_BLOCKS_SSZ])
        self.logger.debug("Sending recent beacon blocks request %s", recent_beacon_blocks_request)
        try:
            await write_req(stream, recent_beacon_blocks_request)
            has_error = False
        except (WriteMessageFailure, MplexStreamEOF, MplexStreamReset) as error:
            has_error = True
            if isinstance(error, WriteMessageFailure):
                await stream.reset()
            elif isinstance(error, MplexStreamEOF):
                await stream.close()
        finally:
            if has_error:
                error_msg = f"fail to write request={recent_beacon_blocks_request}"
                self.logger.info("Request recent beacon blocks failed: %s", error_msg)
                raise RequestFailure(error_msg)

        self.logger.debug("Waiting for recent beacon blocks response")
        try:
            resp_code, recent_beacon_blocks_response = await read_resp(
                stream,
                RecentBeaconBlocksResponse,
            )
            has_error = False
        except (ReadMessageFailure, MplexStreamEOF, MplexStreamReset) as error:
            has_error = True
            if isinstance(error, ReadMessageFailure):
                await stream.reset()
            elif isinstance(error, MplexStreamEOF):
                await stream.close()
        finally:
            if has_error:
                self.logger.info("Request recent beacon blocks failed: fail to read the response")
                raise RequestFailure("fail to read the response")

        self.logger.debug(
            "Received recent beacon blocks response %s, resp_code=%s",
            recent_beacon_blocks_response,
            resp_code,
        )

        if resp_code != ResponseCode.SUCCESS:
            error_msg = (
                "resp_code != ResponseCode.SUCCESS, "
                f"resp_code={resp_code}, error_msg={recent_beacon_blocks_response}"
            )
            self.logger.info("Request recent beacon blocks failed: %s", error_msg)
            await stream.reset()
            raise RequestFailure(error_msg)

        await stream.close()

        recent_beacon_blocks_response = cast(
            RecentBeaconBlocksResponse,
            recent_beacon_blocks_response,
        )
        return recent_beacon_blocks_response.blocks
Example #17
0
async def test_lru_cache_two_nodes():
    # two nodes with cache_size of 4
    # node_a send the following messages to node_b
    # [1, 1, 2, 1, 3, 1, 4, 1, 5, 1]
    # node_b should only receive the following
    # [1, 2, 3, 4, 5, 1]
    node_a = await new_node(transport_opt=["/ip4/127.0.0.1/tcp/0"])
    node_b = await new_node(transport_opt=["/ip4/127.0.0.1/tcp/0"])

    await node_a.get_network().listen(
        multiaddr.Multiaddr("/ip4/127.0.0.1/tcp/0"))
    await node_b.get_network().listen(
        multiaddr.Multiaddr("/ip4/127.0.0.1/tcp/0"))

    supported_protocols = ["/floodsub/1.0.0"]

    # initialize PubSub with a cache_size of 4
    floodsub_a = FloodSub(supported_protocols)
    pubsub_a = Pubsub(node_a, floodsub_a, "a", 4)
    floodsub_b = FloodSub(supported_protocols)
    pubsub_b = Pubsub(node_b, floodsub_b, "b", 4)

    await connect(node_a, node_b)

    await asyncio.sleep(0.25)
    qb = await pubsub_b.subscribe("my_topic")

    await asyncio.sleep(0.25)

    node_a_id = str(node_a.get_id())

    # initialize message_id_generator
    # store first message
    next_msg_id_func = message_id_generator(0)
    first_message = generate_RPC_packet(node_a_id, ["my_topic"], "some data 1",
                                        next_msg_id_func())

    await floodsub_a.publish(node_a_id, first_message.SerializeToString())
    await asyncio.sleep(0.25)
    print(first_message)

    messages = [first_message]
    # for the next 5 messages
    for i in range(2, 6):
        # write first message
        await floodsub_a.publish(node_a_id, first_message.SerializeToString())
        await asyncio.sleep(0.25)

        # generate and write next message
        msg = generate_RPC_packet(node_a_id, ["my_topic"],
                                  "some data " + str(i), next_msg_id_func())
        messages.append(msg)

        await floodsub_a.publish(node_a_id, msg.SerializeToString())
        await asyncio.sleep(0.25)

    # write first message again
    await floodsub_a.publish(node_a_id, first_message.SerializeToString())
    await asyncio.sleep(0.25)

    # check the first five messages in queue
    # should only see 1 first_message
    for i in range(5):
        # Check that the msg received by node_b is the same
        # as the message sent by node_a
        res_b = await qb.get()
        assert res_b.SerializeToString(
        ) == messages[i].publish[0].SerializeToString()

    # the 6th message should be first_message
    res_b = await qb.get()
    assert res_b.SerializeToString(
    ) == first_message.publish[0].SerializeToString()
    assert qb.empty()

    # Success, terminate pending tasks.
    await cleanup()
Example #18
0
File: node.py Project: onyb/trinity
class Node(BaseService):

    _is_started: bool = False

    key_pair: KeyPair
    listen_ip: str
    listen_port: int
    host: BasicHost
    pubsub: Pubsub
    bootstrap_nodes: Tuple[Multiaddr, ...]
    preferred_nodes: Tuple[Multiaddr, ...]
    chain: BaseBeaconChain
    subnets: Set[SubnetId]
    _event_bus: EndpointAPI

    handshaked_peers: PeerPool = None

    def __init__(self,
                 key_pair: KeyPair,
                 listen_ip: str,
                 listen_port: int,
                 chain: BaseBeaconChain,
                 event_bus: EndpointAPI,
                 security_protocol_ops: Dict[TProtocol,
                                             BaseSecureTransport] = None,
                 muxer_protocol_ops: Dict[TProtocol, IMuxedConn] = None,
                 gossipsub_params: Optional[GossipsubParams] = None,
                 cancel_token: CancelToken = None,
                 bootstrap_nodes: Tuple[Multiaddr, ...] = (),
                 preferred_nodes: Tuple[Multiaddr, ...] = (),
                 subnets: Optional[Set[SubnetId]] = None) -> None:
        super().__init__(cancel_token)
        self.listen_ip = listen_ip
        self.listen_port = listen_port
        self.key_pair = key_pair
        self.bootstrap_nodes = bootstrap_nodes
        self.preferred_nodes = preferred_nodes
        self.subnets = subnets if subnets is not None else set()
        # TODO: Add key and peer_id to the peerstore
        if security_protocol_ops is None:
            security_protocol_ops = {SecIOID: SecIOTransport(key_pair)}
        if muxer_protocol_ops is None:
            muxer_protocol_ops = {MPLEX_PROTOCOL_ID: Mplex}
        network: INetwork = initialize_default_swarm(
            key_pair=key_pair,
            transport_opt=[self.listen_maddr],
            muxer_opt=muxer_protocol_ops,
            sec_opt=security_protocol_ops,
            peerstore_opt=None,  # let the function initialize it
        )
        self.host = BasicHost(network=network)

        if gossipsub_params is None:
            gossipsub_params = GossipsubParams()
        gossipsub_router = GossipSub(
            protocols=[GOSSIPSUB_PROTOCOL_ID],
            degree=gossipsub_params.DEGREE,
            degree_low=gossipsub_params.DEGREE_LOW,
            degree_high=gossipsub_params.DEGREE_HIGH,
            time_to_live=gossipsub_params.FANOUT_TTL,
            gossip_window=gossipsub_params.GOSSIP_WINDOW,
            gossip_history=gossipsub_params.GOSSIP_HISTORY,
            heartbeat_interval=gossipsub_params.HEARTBEAT_INTERVAL,
        )
        self.pubsub = Pubsub(
            host=self.host,
            router=gossipsub_router,
            my_id=self.peer_id,
        )

        self.chain = chain
        self._event_bus = event_bus

        self.handshaked_peers = PeerPool()

        self.run_task(self.start())

    @property
    def is_started(self) -> bool:
        return self._is_started

    async def _run(self) -> None:
        self.logger.info("libp2p node %s is up", self.listen_maddr)
        self.run_daemon_task(self.update_status())

        # Metrics and HTTP APIs
        self.run_daemon_task(self.handle_libp2p_peers_requests())
        self.run_daemon_task(self.handle_libp2p_peer_id_requests())

        await self.cancellation()

    async def start(self) -> None:
        # host
        self._register_rpc_handlers()
        # TODO: Register notifees
        is_listening = await self.host.get_network().listen(self.listen_maddr)
        if not is_listening:
            self.logger.error("Fail to listen on maddr: %s", self.listen_maddr)
            raise NodeStartError(
                f"Fail to listen on maddr: {self.listen_maddr}")
        self.logger.warning("Node listening: %s",
                            self.listen_maddr_with_peer_id)
        await self.connect_preferred_nodes()
        # TODO: Connect bootstrap nodes?

        # Pubsub
        # Global channel
        await self.pubsub.subscribe(PUBSUB_TOPIC_BEACON_BLOCK)
        await self.pubsub.subscribe(PUBSUB_TOPIC_BEACON_ATTESTATION)
        await self.pubsub.subscribe(PUBSUB_TOPIC_BEACON_AGGREGATE_AND_PROOF)
        # Attestation subnets
        for subnet_id in self.subnets:
            topic = PUBSUB_TOPIC_COMMITTEE_BEACON_ATTESTATION.substitute(
                subnet_id=str(subnet_id))
            await self.pubsub.subscribe(topic)

        self._setup_topic_validators()

        self._is_started = True

    def _setup_topic_validators(self) -> None:
        # Global channel
        self.pubsub.set_topic_validator(
            PUBSUB_TOPIC_BEACON_BLOCK,
            get_beacon_block_validator(self.chain),
            False,
        )
        self.pubsub.set_topic_validator(
            PUBSUB_TOPIC_BEACON_ATTESTATION,
            get_beacon_attestation_validator(self.chain),
            False,
        )
        # Attestation subnets
        for subnet_id in self.subnets:
            self.pubsub.set_topic_validator(
                PUBSUB_TOPIC_COMMITTEE_BEACON_ATTESTATION.substitute(
                    subnet_id=str(subnet_id)),
                get_committee_index_beacon_attestation_validator(
                    self.chain, subnet_id),
                False,
            )

        self.pubsub.set_topic_validator(
            PUBSUB_TOPIC_BEACON_AGGREGATE_AND_PROOF,
            get_beacon_aggregate_and_proof_validator(self.chain),
            False,
        )

    async def dial_peer_maddr(self, maddr: Multiaddr, peer_id: ID) -> None:
        """
        Dial the peer with given multi-address
        """
        self.logger.debug("Dialing peer_id: %s, maddr: %s", peer_id, maddr)
        try:
            await self.host.connect(PeerInfo(
                peer_id=peer_id,
                addrs=[maddr],
            ))
        except SwarmException as e:
            self.logger.debug("Fail to dial peer_id: %s, maddr: %s, error: %s",
                              peer_id, maddr, e)
            raise DialPeerError from e

        try:
            # TODO: set a time limit on completing handshake
            await self.request_status(peer_id)
        except HandshakeFailure as e:
            self.logger.debug("Fail to handshake with peer_id: %s, error: %s",
                              peer_id, e)
            raise DialPeerError from e
        self.logger.debug("Successfully connect to peer_id %s maddr %s",
                          peer_id, maddr)

    async def dial_peer_maddr_with_retries(self, maddr: Multiaddr) -> None:
        """
        Dial the peer with given multi-address repeatedly for `DIAL_RETRY_COUNT` times
        """
        try:
            p2p_id = maddr.value_for_protocol(protocols.P_P2P)
        except (BinaryParseError, ProtocolLookupError) as error:
            self.logger.debug("Invalid maddr: %s, error: %s", maddr, error)
            raise DialPeerError from error
        peer_id = ID.from_base58(p2p_id)

        for i in range(DIAL_RETRY_COUNT):
            try:
                # exponential backoff...
                await asyncio.sleep(2**i + random.random())
                await self.dial_peer_maddr(maddr, peer_id)
                return
            except DialPeerError:
                self.logger.debug(
                    "Could not dial peer: %s, maddr: %s retrying attempt %d of %d...",
                    peer_id,
                    maddr,
                    i,
                    DIAL_RETRY_COUNT,
                )
                continue
        raise DialPeerError

    async def connect_preferred_nodes(self) -> None:
        results = await asyncio.gather(
            *(self.dial_peer_maddr_with_retries(node_maddr)
              for node_maddr in self.preferred_nodes),
            return_exceptions=True,
        )
        for result, node_maddr in zip(results, self.preferred_nodes):
            if isinstance(result, Exception):
                logger.warning("Could not connect to preferred node at %s",
                               node_maddr)

    async def disconnect_peer(self, peer_id: ID) -> None:
        if peer_id in self.handshaked_peers:
            self.logger.debug("Disconnect from %s", peer_id)
            self.handshaked_peers.remove(peer_id)
            await self.host.disconnect(peer_id)
        else:
            self.logger.debug("Already disconnected from %s", peer_id)

    async def broadcast_beacon_block(self,
                                     block: BaseSignedBeaconBlock) -> None:
        await self._broadcast_data(PUBSUB_TOPIC_BEACON_BLOCK,
                                   ssz.encode(block))

    async def broadcast_attestation(self, attestation: Attestation) -> None:
        await self._broadcast_data(PUBSUB_TOPIC_BEACON_ATTESTATION,
                                   ssz.encode(attestation))

    async def broadcast_attestation_to_subnet(self, attestation: Attestation,
                                              subnet_id: SubnetId) -> None:
        await self._broadcast_data(
            PUBSUB_TOPIC_COMMITTEE_BEACON_ATTESTATION.substitute(
                subnet_id=str(subnet_id)), ssz.encode(attestation))

    async def broadcast_beacon_aggregate_and_proof(
            self, aggregate_and_proof: AggregateAndProof) -> None:
        await self._broadcast_data(
            PUBSUB_TOPIC_BEACON_AGGREGATE_AND_PROOF,
            ssz.encode(aggregate_and_proof),
        )

    async def _broadcast_data(self, topic: str, data: bytes) -> None:
        await self.pubsub.publish(topic, data)

    @property
    def peer_id(self) -> ID:
        return self.host.get_id()

    @property
    def listen_maddr(self) -> Multiaddr:
        return make_tcp_ip_maddr(self.listen_ip, self.listen_port)

    @property
    def listen_maddr_with_peer_id(self) -> Multiaddr:
        return self.listen_maddr.encapsulate(
            Multiaddr(f"/p2p/{self.peer_id.to_base58()}"))

    @property
    def peer_store(self) -> PeerStore:
        return self.host.get_network().peerstore

    async def close(self) -> None:
        # FIXME: Add `tear_down` to `Swarm` in the upstream
        network = self.host.get_network()
        for listener in network.listeners.values():
            listener.server.close()
            await listener.server.wait_closed()
        # TODO: Add `close` in `Pubsub`

    def _register_rpc_handlers(self) -> None:
        self.host.set_stream_handler(REQ_RESP_STATUS_SSZ, self._handle_status)
        self.host.set_stream_handler(REQ_RESP_GOODBYE_SSZ,
                                     self._handle_goodbye)
        self.host.set_stream_handler(
            REQ_RESP_BEACON_BLOCKS_BY_RANGE_SSZ,
            self._handle_beacon_blocks_by_range,
        )
        self.host.set_stream_handler(
            REQ_RESP_BEACON_BLOCKS_BY_ROOT_SSZ,
            self._handle_beacon_blocks_by_root,
        )

    #
    # RPC Handlers
    #

    async def new_stream(self, peer_id: ID, protocol: TProtocol) -> INetStream:
        return await self.host.new_stream(peer_id, [protocol])

    @asynccontextmanager
    async def new_handshake_interaction(
            self, stream: INetStream) -> AsyncIterator[Interaction]:
        try:
            async with Interaction(stream) as interaction:
                peer_id = interaction.peer_id
                yield interaction
        except MessageIOFailure as error:
            await self.disconnect_peer(peer_id)
            raise HandshakeFailure() from error
        except PeerRespondedAnError as error:
            await stream.reset()
            await self.disconnect_peer(peer_id)
            raise HandshakeFailure() from error
        except IrrelevantNetwork as error:
            await stream.reset()
            asyncio.ensure_future(
                self.say_goodbye(peer_id,
                                 GoodbyeReasonCode.IRRELEVANT_NETWORK))
            raise HandshakeFailure from error

    @asynccontextmanager
    async def post_handshake_handler_interaction(
            self, stream: INetStream) -> AsyncIterator[Interaction]:
        try:
            async with Interaction(stream) as interaction:
                yield interaction
        except WriteMessageFailure as error:
            self.logger.debug("WriteMessageFailure %s", error)
            return
        except ReadMessageFailure as error:
            self.logger.debug("ReadMessageFailure %s", error)
            return
        except UnhandshakedPeer:
            await stream.reset()
            return

    @asynccontextmanager
    async def my_request_interaction(
            self, stream: INetStream) -> AsyncIterator[Interaction]:
        try:
            async with Interaction(stream) as interaction:
                yield interaction
        except (MessageIOFailure, UnhandshakedPeer,
                PeerRespondedAnError) as error:
            raise RequestFailure(str(error)) from error

    # TODO: Handle the reputation of peers. Deduct their scores and even disconnect when they
    #   behave.

    # TODO: Register notifee to the `Network` to
    #   - Record peers' joining time.
    #   - Disconnect peers when they fail to join in a certain amount of time.

    def _add_peer_from_status(self, peer_id: ID, status: Status) -> None:
        peer = Peer.from_status(self, peer_id, status)
        self.handshaked_peers.add(peer)
        self.logger.debug(
            "Handshake from %s is finished. Added to the `handshake_peers`",
            peer_id,
        )

    async def _handle_status(self, stream: INetStream) -> None:
        # TODO: Find out when we should respond the `ResponseCode`
        #   other than `ResponseCode.SUCCESS`.

        async with self.new_handshake_interaction(stream) as interaction:
            peer_id = interaction.peer_id
            peer_status = await interaction.read_request(Status)
            self.logger.info("Received Status from %s  %s", str(peer_id),
                             peer_status)
            await validate_peer_status(self.chain, peer_status)

            my_status = get_my_status(self.chain)
            await interaction.write_response(my_status)

            self._add_peer_from_status(peer_id, peer_status)

            if peer_is_ahead(self.chain, peer_status):
                logger.debug(
                    "Peer's chain is ahead of us, start syncing with the peer(%s)",
                    str(peer_id),
                )
                await self._event_bus.broadcast(SyncRequest())

    async def request_status(self, peer_id: ID) -> None:
        self.logger.info("Initiate handshake with %s", str(peer_id))

        try:
            stream = await self.new_stream(peer_id, REQ_RESP_STATUS_SSZ)
        except StreamFailure as error:
            self.logger.debug("Fail to open stream to %s", str(peer_id))
            raise HandshakeFailure from error
        async with self.new_handshake_interaction(stream) as interaction:
            my_status = get_my_status(self.chain)
            await interaction.write_request(my_status)
            peer_status = await interaction.read_response(Status)

            await validate_peer_status(self.chain, peer_status)

            self._add_peer_from_status(peer_id, peer_status)

            if peer_is_ahead(self.chain, peer_status):
                logger.debug(
                    "Peer's chain is ahead of us, start syncing with the peer(%s)",
                    str(peer_id),
                )
                await self._event_bus.broadcast(SyncRequest())

    async def _handle_goodbye(self, stream: INetStream) -> None:
        async with Interaction(stream) as interaction:
            peer_id = interaction.peer_id
            try:
                await interaction.read_request(Goodbye)
            except ReadMessageFailure:
                pass
            await self.disconnect_peer(peer_id)

    async def say_goodbye(self, peer_id: ID,
                          reason: GoodbyeReasonCode) -> None:
        try:
            stream = await self.new_stream(peer_id, REQ_RESP_GOODBYE_SSZ)
        except StreamFailure:
            self.logger.debug("Fail to open stream to %s", str(peer_id))
        else:
            async with Interaction(stream) as interaction:
                goodbye = Goodbye.create(reason)
                try:
                    await interaction.write_request(goodbye)
                except WriteMessageFailure:
                    pass
        finally:
            await self.disconnect_peer(peer_id)

    def _check_peer_handshaked(self, peer_id: ID) -> None:
        if peer_id not in self.handshaked_peers:
            raise UnhandshakedPeer(peer_id)

    async def _handle_beacon_blocks_by_range(self, stream: INetStream) -> None:
        # TODO: Should it be a successful response if peer is requesting
        # blocks on a fork we don't have data for?

        async with self.post_handshake_handler_interaction(
                stream) as interaction:
            peer_id = interaction.peer_id
            self._check_peer_handshaked(peer_id)

            request = await interaction.read_request(BeaconBlocksByRangeRequest
                                                     )
            try:
                blocks = get_requested_beacon_blocks(self.chain, request)
            except InvalidRequest as error:
                error_message = str(error)[:128]
                await interaction.write_error_response(
                    error_message, ResponseCode.INVALID_REQUEST)
            else:
                await interaction.write_chunk_response(blocks)

    async def request_beacon_blocks_by_range(
        self,
        peer_id: ID,
        head_block_root: Root,
        start_slot: Slot,
        count: int,
        step: int,
    ) -> Tuple[BaseSignedBeaconBlock, ...]:
        try:
            stream = await self.new_stream(
                peer_id, REQ_RESP_BEACON_BLOCKS_BY_RANGE_SSZ)
        except StreamFailure as error:
            self.logger.debug("Fail to open stream to %s", str(peer_id))
            raise RequestFailure(str(error)) from error
        async with self.my_request_interaction(stream) as interaction:
            self._check_peer_handshaked(peer_id)
            request = BeaconBlocksByRangeRequest.create(
                head_block_root=head_block_root,
                start_slot=start_slot,
                count=count,
                step=step,
            )
            await interaction.write_request(request)
            blocks = tuple([
                block async for block in interaction.read_chunk_response(
                    SignedBeaconBlock, count)
            ])

            return blocks

    async def _handle_beacon_blocks_by_root(self, stream: INetStream) -> None:
        async with self.post_handshake_handler_interaction(
                stream) as interaction:
            peer_id = interaction.peer_id
            self._check_peer_handshaked(peer_id)
            request = await interaction.read_request(BeaconBlocksByRootRequest)
            blocks = get_beacon_blocks_by_root(self.chain, request)

            await interaction.write_chunk_response(blocks)

    async def request_beacon_blocks_by_root(
            self, peer_id: ID,
            block_roots: Sequence[Root]) -> Tuple[BaseSignedBeaconBlock, ...]:
        try:
            stream = await self.new_stream(peer_id,
                                           REQ_RESP_BEACON_BLOCKS_BY_ROOT_SSZ)
        except StreamFailure as error:
            self.logger.debug("Fail to open stream to %s", str(peer_id))
            raise RequestFailure(str(error)) from error
        async with self.my_request_interaction(stream) as interaction:
            self._check_peer_handshaked(peer_id)
            request = BeaconBlocksByRootRequest.create(block_roots=block_roots)
            await interaction.write_request(request)
            blocks = tuple([
                block async for block in interaction.read_chunk_response(
                    SignedBeaconBlock, len(block_roots))
            ])

            return blocks

    async def update_status(self) -> None:
        while True:
            for peer_id in self.handshaked_peers.peer_ids:
                asyncio.ensure_future(self.request_status(peer_id))
            await asyncio.sleep(NEXT_UPDATE_INTERVAL)

    #
    # Metrics and APIs
    #
    async def handle_libp2p_peers_requests(self) -> None:
        async for req in self.wait_iter(
                self._event_bus.stream(Libp2pPeersRequest)):
            peers = tuple(self.handshaked_peers.peer_ids)
            await self._event_bus.broadcast(
                Libp2pPeersResponse(peers),
                req.broadcast_config(),
            )

    async def handle_libp2p_peer_id_requests(self) -> None:
        async for req in self.wait_iter(
                self._event_bus.stream(Libp2pPeerIDRequest)):
            await self._event_bus.broadcast(
                Libp2pPeerIDResponse(self.peer_id),
                req.broadcast_config(),
            )
Example #19
0
async def perform_test_from_obj(obj):
    """
    Perform a floodsub test from a test obj.
    test obj are composed as follows:
    
    {
        "supported_protocols": ["supported/protocol/1.0.0",...],
        "adj_list": {
            "node1": ["neighbor1_of_node1", "neighbor2_of_node1", ...],
            "node2": ["neighbor1_of_node2", "neighbor2_of_node2", ...],
            ...
        },
        "topic_map": {
            "topic1": ["node1_subscribed_to_topic1", "node2_subscribed_to_topic1", ...]
        },
        "messages": [
            {
                "topics": ["topic1_for_message", "topic2_for_message", ...],
                "data": "some contents of the message (newlines are not supported)",
                "node_id": "message sender node id"
            },
            ...
        ]
    }
    NOTE: In adj_list, for any neighbors A and B, only list B as a neighbor of A
    or B as a neighbor of A once. Do NOT list both A: ["B"] and B:["A"] as the behavior
    is undefined (even if it may work)
    """

    # Step 1) Create graph
    adj_list = obj["adj_list"]
    node_map = {}
    floodsub_map = {}
    pubsub_map = {}

    supported_protocols = obj["supported_protocols"]

    tasks_connect = []
    for start_node_id in adj_list:
        # Create node if node does not yet exist
        if start_node_id not in node_map:
            node = await new_node(transport_opt=["/ip4/127.0.0.1/tcp/0"])
            await node.get_network().listen(
                multiaddr.Multiaddr("/ip4/127.0.0.1/tcp/0"))

            node_map[start_node_id] = node

            floodsub = FloodSub(supported_protocols)
            floodsub_map[start_node_id] = floodsub
            pubsub = Pubsub(node, floodsub, start_node_id)
            pubsub_map[start_node_id] = pubsub

        # For each neighbor of start_node, create if does not yet exist,
        # then connect start_node to neighbor
        for neighbor_id in adj_list[start_node_id]:
            # Create neighbor if neighbor does not yet exist
            if neighbor_id not in node_map:
                neighbor_node = await new_node(
                    transport_opt=["/ip4/127.0.0.1/tcp/0"])
                await neighbor_node.get_network().listen(
                    multiaddr.Multiaddr("/ip4/127.0.0.1/tcp/0"))

                node_map[neighbor_id] = neighbor_node

                floodsub = FloodSub(supported_protocols)
                floodsub_map[neighbor_id] = floodsub
                pubsub = Pubsub(neighbor_node, floodsub, neighbor_id)
                pubsub_map[neighbor_id] = pubsub

            # Connect node and neighbor
            # await connect(node_map[start_node_id], node_map[neighbor_id])
            tasks_connect.append(
                asyncio.ensure_future(
                    connect(node_map[start_node_id], node_map[neighbor_id])))
    tasks_connect.append(asyncio.sleep(2))
    await asyncio.gather(*tasks_connect)

    # Allow time for graph creation before continuing
    # await asyncio.sleep(0.25)

    # Step 2) Subscribe to topics
    queues_map = {}
    topic_map = obj["topic_map"]

    tasks_topic = []
    tasks_topic_data = []
    for topic in topic_map:
        for node_id in topic_map[topic]:
            """
            # Subscribe node to topic
            q = await pubsub_map[node_id].subscribe(topic)

            # Create topic-queue map for node_id if one does not yet exist
            if node_id not in queues_map:
                queues_map[node_id] = {}

            # Store queue in topic-queue map for node
            queues_map[node_id][topic] = q
            """
            tasks_topic.append(
                asyncio.ensure_future(pubsub_map[node_id].subscribe(topic)))
            tasks_topic_data.append((node_id, topic))
    tasks_topic.append(asyncio.sleep(2))

    # Gather is like Promise.all
    responses = await asyncio.gather(*tasks_topic, return_exceptions=True)
    for i in range(len(responses) - 1):
        q = responses[i]
        node_id, topic = tasks_topic_data[i]
        if node_id not in queues_map:
            queues_map[node_id] = {}

        # Store queue in topic-queue map for node
        queues_map[node_id][topic] = q

    # Allow time for subscribing before continuing
    # await asyncio.sleep(0.01)

    # Step 3) Publish messages
    topics_in_msgs_ordered = []
    messages = obj["messages"]
    tasks_publish = []
    next_msg_id_func = message_id_generator(0)

    for msg in messages:
        topics = msg["topics"]

        data = msg["data"]
        node_id = msg["node_id"]

        # Get actual id for sender node (not the id from the test obj)
        actual_node_id = str(node_map[node_id].get_id())

        # Create correctly formatted message
        msg_talk = generate_RPC_packet(actual_node_id, topics, data,
                                       next_msg_id_func())

        # Publish message
        # await floodsub_map[node_id].publish(actual_node_id, msg_talk.to_str())
        tasks_publish.append(asyncio.ensure_future(floodsub_map[node_id].publish(\
            actual_node_id, msg_talk.SerializeToString())))

        # For each topic in topics, add topic, msg_talk tuple to ordered test list
        # TODO: Update message sender to be correct message sender before
        # adding msg_talk to this list
        for topic in topics:
            topics_in_msgs_ordered.append((topic, msg_talk))

    # Allow time for publishing before continuing
    # await asyncio.sleep(0.4)
    tasks_publish.append(asyncio.sleep(2))
    await asyncio.gather(*tasks_publish)

    # Step 4) Check that all messages were received correctly.
    # TODO: Check message sender too
    for i in range(len(topics_in_msgs_ordered)):
        topic, actual_msg = topics_in_msgs_ordered[i]

        # Look at each node in each topic
        for node_id in topic_map[topic]:
            # Get message from subscription queue
            msg_on_node_str = await queues_map[node_id][topic].get()
            assert actual_msg.publish[0].SerializeToString(
            ) == msg_on_node_str.SerializeToString()

    # Success, terminate pending tasks.
    await cleanup()
Example #20
0
async def initialize_host(key,
                          host="0.0.0.0",
                          port=4025,
                          listen=True,
                          protocol_active=True
                          ) -> Tuple[BasicHost, Pubsub, Any, List]:
    from .protocol import AlephProtocol
    from .jobs import reconnect_p2p_job, tidy_http_peers_job

    assert key, "Host cannot be initialized without a key"

    tasks: List[Coroutine]

    priv = import_key(key)
    private_key = RSAPrivateKey(priv)
    public_key = private_key.get_public_key()
    keypair = KeyPair(private_key, public_key)

    transport_opt = f"/ip4/{host}/tcp/{port}"
    host: BasicHost = await new_node(transport_opt=[transport_opt],
                                     key_pair=keypair)
    protocol = None
    # gossip = gossipsub.GossipSub([GOSSIPSUB_PROTOCOL_ID], 10, 9, 11, 30)
    # psub = Pubsub(host, gossip, host.get_id())
    flood = floodsub.FloodSub([FLOODSUB_PROTOCOL_ID, GOSSIPSUB_PROTOCOL_ID])
    psub = Pubsub(host, flood, host.get_id())
    if protocol_active:
        protocol = AlephProtocol(host)
    tasks = [
        reconnect_p2p_job(),
        tidy_http_peers_job(),
    ]
    if listen:
        from aleph.web import app

        await host.get_network().listen(multiaddr.Multiaddr(transport_opt))
        LOGGER.info("Listening on " + f"{transport_opt}/p2p/{host.get_id()}")
        ip = await get_IP()
        public_address = f"/ip4/{ip}/tcp/{port}/p2p/{host.get_id()}"
        http_port = app["config"].p2p.http_port.value
        public_adresses.append(public_address)

        public_http_address = f"http://{ip}:{http_port}"

        LOGGER.info("Probable public on " + public_address)
        # TODO: set correct interests and args here
        tasks += [
            publish_host(
                public_address,
                psub,
                peer_type="P2P",
                use_ipfs=app["config"].ipfs.enabled.value,
            ),
            publish_host(
                public_http_address,
                psub,
                peer_type="HTTP",
                use_ipfs=app["config"].ipfs.enabled.value,
            ),
            monitor_hosts_p2p(psub),
        ]

        if app["config"].ipfs.enabled.value:
            tasks.append(monitor_hosts_ipfs(app["config"]))
            try:
                public_ipfs_address = await get_public_address()
                tasks.append(
                    publish_host(public_ipfs_address,
                                 psub,
                                 peer_type="IPFS",
                                 use_ipfs=True))
            except Exception:
                LOGGER.exception("Can't publish public IPFS address")

        # Enable message exchange using libp2p
        # host.set_stream_handler(PROTOCOL_ID, stream_handler)

    return (host, psub, protocol, tasks)
Example #21
0
class Gossiper:
    GOSSIP_MAX_SIZE: int = 2**20
    MAXIMUM_GOSSIP_CLOCK_DISPARITY = 0.5  # seconds

    # `D` (topic stable mesh target count)
    DEGREE: int = 6
    # `D_low` (topic stable mesh low watermark)
    DEGREE_LOW: int = 4
    # `D_high` (topic stable mesh high watermark)
    DEGREE_HIGH: int = 12
    # `D_lazy` (gossip target)
    DEGREE_LAZY: int = 6
    # `fanout_ttl` (ttl for fanout maps for topics we are not subscribed to
    #   but have published to seconds).
    FANOUT_TTL: int = 60
    # `gossip_advertise` (number of windows to gossip about).
    GOSSIP_WINDOW: int = 3
    # `gossip_history` (number of heartbeat intervals to retain message IDs).
    GOSSIP_HISTORY: int = 5
    # `heartbeat_interval` (frequency of heartbeat, seconds).
    HEARTBEAT_INTERVAL: int = 1  # seconds

    ATTESTATION_SUBNET_COUNT = 64
    ATTESTATION_PROPAGATION_SLOT_RANGE = 32

    def __init__(self, fork_digest_provider: ForkDigestProvider,
                 host: IHost) -> None:
        self._fork_digest_provider = fork_digest_provider
        self._host = host
        gossipsub_router = GossipSub(
            protocols=[GOSSIPSUB_PROTOCOL_ID],
            degree=self.DEGREE,
            degree_low=self.DEGREE_LOW,
            degree_high=self.DEGREE_HIGH,
            time_to_live=self.FANOUT_TTL,
            gossip_window=self.GOSSIP_WINDOW,
            gossip_history=self.GOSSIP_HISTORY,
            heartbeat_interval=self.HEARTBEAT_INTERVAL,
        )
        self.gossipsub = gossipsub_router
        self.pubsub = Pubsub(
            host=self._host,
            router=gossipsub_router,
            msg_id_constructor=get_content_addressed_msg_id,
        )

    async def _subscribe_to_gossip_topic(
            self, topic: str, validator: GossipValidator) -> ISubscriptionAPI:
        self.pubsub.set_topic_validator(topic, validator, False)
        return await self.pubsub.subscribe(topic)

    def _gossip_topic_id_for(self, topic: PubSubTopic) -> str:
        return f"/eth2/{self._fork_digest_provider().hex()}/{topic.value}/ssz_snappy"

    async def subscribe_gossip_channels(self) -> None:
        # TODO handle concurrently
        self._block_gossip = await self._subscribe_to_gossip_topic(
            self._gossip_topic_id_for(PubSubTopic.beacon_block),
            validator=_validate_beacon_block_gossip(self),
        )
        self._attestation_gossip = await self._subscribe_to_gossip_topic(
            self._gossip_topic_id_for(PubSubTopic.attestation),
            validator=_validate_attestation_gossip(self),
        )

    async def unsubscribe_gossip_channels(self) -> None:
        # TODO handle concurrently
        await self.pubsub.unsubscribe(
            self._gossip_topic_id_for(PubSubTopic.beacon_block))
        await self.pubsub.unsubscribe(
            self._gossip_topic_id_for(PubSubTopic.attestation))

    async def stream_block_gossip(self) -> AsyncIterable[SignedBeaconBlock]:
        async with self._block_gossip:
            async for block_message in self._block_gossip:
                if block_message.from_id == self.pubsub.my_id:
                    # FIXME: this check should happen inside `py-libp2p`
                    continue
                # TODO: validate block further at the p2p layer?
                block_data = block_message.data
                yield _deserialize_gossip(block_data, SignedBeaconBlock)

    async def broadcast_block(self, block: SignedBeaconBlock) -> None:
        gossip = _serialize_gossip(block)
        await self.pubsub.publish(
            self._gossip_topic_id_for(PubSubTopic.beacon_block), gossip)