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()
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())
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()
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()
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
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 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()
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
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()
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)
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)
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
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, )
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)
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
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()
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(), )
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()
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)
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)