From 604a447287fd5140c8a6e4a142bb78261129fd53 Mon Sep 17 00:00:00 2001 From: acul71 Date: Wed, 19 Feb 2025 03:43:50 +0100 Subject: [PATCH] feat: add identify protocol example --- docs/examples.identify.rst | 41 +++++ docs/examples.rst | 11 +- docs/libp2p.identity.identify.rst | 4 +- examples/identify/identify.py | 132 +++++++++++++++ libp2p/host/defaults.py | 4 +- .../identify/{protocol.py => identify.py} | 0 newsfragments/358.feature.rst | 1 + newsfragments/536.feature.rst | 1 + setup.py | 1 + tests/core/identity/identify/test_identify.py | 79 +++++++++ tests/core/identity/identify/test_protocol.py | 158 ------------------ 11 files changed, 265 insertions(+), 167 deletions(-) create mode 100644 docs/examples.identify.rst create mode 100644 examples/identify/identify.py rename libp2p/identity/identify/{protocol.py => identify.py} (100%) create mode 100644 newsfragments/358.feature.rst create mode 100644 newsfragments/536.feature.rst create mode 100644 tests/core/identity/identify/test_identify.py delete mode 100644 tests/core/identity/identify/test_protocol.py diff --git a/docs/examples.identify.rst b/docs/examples.identify.rst new file mode 100644 index 00000000..53981254 --- /dev/null +++ b/docs/examples.identify.rst @@ -0,0 +1,41 @@ +Identify Protocol Demo +====================== + +This example demonstrates how to use the libp2p ``identify`` protocol. + +.. code-block:: console + + $ python -m pip install libp2p + Collecting libp2p + ... + Successfully installed libp2p-x.x.x + $ python identify.py + First host listening. Run this from another console: + + python identify.py -p 8889 -d /ip4/0.0.0.0/tcp/8888/p2p/QmUiN4R3fNrCoQugGgmmb3v35neMEjKFNrsbNGVDsRHWpM + + Waiting for incoming identify request... + +Copy the line that starts with ``python identify.py -p 8889 ....``, open a new terminal in the same +folder and paste it in: + +.. code-block:: console + + $ python identify.py -p 8889 -d /ip4/0.0.0.0/tcp/8888/p2p/QmUiN4R3fNrCoQugGgmmb3v35neMEjKFNrsbNGVDsRHWpM + dialer (host_b) listening on /ip4/0.0.0.0/tcp/8889 + Second host connecting to peer: QmUiN4R3fNrCoQugGgmmb3v35neMEjKFNrsbNGVDsRHWpM + Starting identify protocol... + Identify response: + Public Key (Base64): CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDC6c/oNPP9X13NDQ3Xrlp3zOj+ErXIWb/A4JGwWchiDBwMhMslEX3ct8CqI0BqUYKuwdFjowqqopOJ3cS2MlqtGaiP6Dg9bvGqSDoD37BpNaRVNcebRxtB0nam9SQy3PYLbHAmz0vR4ToSiL9OLRORnGOxCtHBuR8ZZ5vS0JEni8eQMpNa7IuXwyStnuty/QjugOZudBNgYSr8+9gH722KTjput5IRL7BrpIdd4HNXGVRm4b9BjNowvHu404x3a/ifeNblpy/FbYyFJEW0looygKF7hpRHhRbRKIDZt2BqOfT1sFkbqsHE85oY859+VMzP61YELgvGwai2r7KcjkW/AgMBAAE= + Listen Addresses: ['/ip4/0.0.0.0/tcp/8888/p2p/QmUiN4R3fNrCoQugGgmmb3v35neMEjKFNrsbNGVDsRHWpM'] + Protocols: ['/ipfs/id/1.0.0', '/ipfs/ping/1.0.0'] + Observed Address: ['/ip4/127.0.0.1/tcp/38082'] + Protocol Version: ipfs/0.1.0 + Agent Version: py-libp2p/0.2.0 + + +The full source code for this example is below: + +.. literalinclude:: ../examples/identify/identify.py + :language: python + :linenos: diff --git a/docs/examples.rst b/docs/examples.rst index 1574d968..3593a814 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -1,10 +1,11 @@ Examples ======== -These are functional demonstrations of aspects of the library. - .. toctree:: + :maxdepth: 2 + :caption: Examples: - examples.chat - examples.echo - examples.ping + examples.identify + examples.chat + examples.echo + examples.ping diff --git a/docs/libp2p.identity.identify.rst b/docs/libp2p.identity.identify.rst index e6d892d0..2e619dec 100644 --- a/docs/libp2p.identity.identify.rst +++ b/docs/libp2p.identity.identify.rst @@ -12,10 +12,10 @@ Subpackages Submodules ---------- -libp2p.identity.identify.protocol module +libp2p.identity.identify.identify module ---------------------------------------- -.. automodule:: libp2p.identity.identify.protocol +.. automodule:: libp2p.identity.identify.identify :members: :undoc-members: :show-inheritance: diff --git a/examples/identify/identify.py b/examples/identify/identify.py new file mode 100644 index 00000000..0a9fde27 --- /dev/null +++ b/examples/identify/identify.py @@ -0,0 +1,132 @@ +import argparse +import base64 +import logging + +import multiaddr +import trio + +from libp2p import ( + new_host, +) +from libp2p.identity.identify.identify import ID as IDENTIFY_PROTOCOL_ID +from libp2p.identity.identify.pb.identify_pb2 import ( + Identify, +) +from libp2p.peer.peerinfo import ( + info_from_p2p_addr, +) + +logger = logging.getLogger("libp2p.identity.identify-example") + + +def decode_multiaddrs(raw_addrs): + """Convert raw listen addresses into human-readable multiaddresses.""" + decoded_addrs = [] + for addr in raw_addrs: + try: + decoded_addrs.append(str(multiaddr.Multiaddr(addr))) + except Exception as e: + decoded_addrs.append(f"Invalid Multiaddr ({addr}): {e}") + return decoded_addrs + + +def print_identify_response(identify_response): + """Pretty-print Identify response.""" + public_key_b64 = base64.b64encode(identify_response.public_key).decode("utf-8") + listen_addrs = decode_multiaddrs(identify_response.listen_addrs) + try: + observed_addr_decoded = decode_multiaddrs([identify_response.observed_addr]) + except Exception: + observed_addr_decoded = identify_response.observed_addr + print( + f"Identify response:\n" + f" Public Key (Base64): {public_key_b64}\n" + f" Listen Addresses: {listen_addrs}\n" + f" Protocols: {list(identify_response.protocols)}\n" + f" Observed Address: " + f"{observed_addr_decoded if identify_response.observed_addr else 'None'}\n" + f" Protocol Version: {identify_response.protocol_version}\n" + f" Agent Version: {identify_response.agent_version}" + ) + + +async def run(port: int, destination: str) -> None: + localhost_ip = "0.0.0.0" + + if not destination: + # Create first host (listener) + listen_addr = multiaddr.Multiaddr(f"/ip4/{localhost_ip}/tcp/{port}") + host_a = new_host() + + async with host_a.run(listen_addrs=[listen_addr]): + print( + "First host listening. Run this from another console:\n\n" + f"python identify.py -p {int(port) + 1} " + f"-d /ip4/{localhost_ip}/tcp/{port}/p2p/{host_a.get_id().pretty()}\n" + ) + print("Waiting for incoming identify request...") + await trio.sleep_forever() + + else: + # Create second host (dialer) + print(f"dialer (host_b) listening on /ip4/{localhost_ip}/tcp/{port}") + listen_addr = multiaddr.Multiaddr(f"/ip4/{localhost_ip}/tcp/{port}") + host_b = new_host() + + async with host_b.run(listen_addrs=[listen_addr]): + # Connect to the first host + maddr = multiaddr.Multiaddr(destination) + info = info_from_p2p_addr(maddr) + print(f"Second host connecting to peer: {info.peer_id}") + + await host_b.connect(info) + stream = await host_b.new_stream(info.peer_id, (IDENTIFY_PROTOCOL_ID,)) + + try: + print("Starting identify protocol...") + response = await stream.read() + await stream.close() + identify_msg = Identify() + identify_msg.ParseFromString(response) + print_identify_response(identify_msg) + except Exception as e: + print(f"Identify protocol error: {e}") + + return + + +def main() -> None: + description = """ + This program demonstrates the libp2p identify protocol. + First run 'python identify.py -p ' to start a listener. + Then run 'python identify.py -p -d ' + where is the multiaddress shown by the listener. + """ + + example_maddr = ( + "/ip4/127.0.0.1/tcp/8888/p2p/QmQn4SwGkDZkUEpBRBvTmheQycxAHJUNmVEnjA2v1qe8Q" + ) + + parser = argparse.ArgumentParser(description=description) + parser.add_argument( + "-p", "--port", default=8888, type=int, help="source port number" + ) + parser.add_argument( + "-d", + "--destination", + type=str, + help=f"destination multiaddr string, e.g. {example_maddr}", + ) + args = parser.parse_args() + + if not args.port: + raise RuntimeError("failed to determine local port") + + try: + trio.run(run, *(args.port, args.destination)) + except KeyboardInterrupt: + pass + + +if __name__ == "__main__": + main() diff --git a/libp2p/host/defaults.py b/libp2p/host/defaults.py index 93634233..eb454dc5 100644 --- a/libp2p/host/defaults.py +++ b/libp2p/host/defaults.py @@ -12,10 +12,10 @@ from libp2p.host.ping import ( handle_ping, ) from libp2p.host.ping import ID as PingID -from libp2p.identity.identify.protocol import ( +from libp2p.identity.identify.identify import ( identify_handler_for, ) -from libp2p.identity.identify.protocol import ID as IdentifyID +from libp2p.identity.identify.identify import ID as IdentifyID if TYPE_CHECKING: from libp2p.custom_types import ( diff --git a/libp2p/identity/identify/protocol.py b/libp2p/identity/identify/identify.py similarity index 100% rename from libp2p/identity/identify/protocol.py rename to libp2p/identity/identify/identify.py diff --git a/newsfragments/358.feature.rst b/newsfragments/358.feature.rst new file mode 100644 index 00000000..e17d467e --- /dev/null +++ b/newsfragments/358.feature.rst @@ -0,0 +1 @@ +Improved the implementation of the identify protocol and enhanced test coverage to ensure proper functionality and network layer address delegation. diff --git a/newsfragments/536.feature.rst b/newsfragments/536.feature.rst new file mode 100644 index 00000000..247de065 --- /dev/null +++ b/newsfragments/536.feature.rst @@ -0,0 +1 @@ +Added an example implementation of the identify protocol to demonstrate its usage and help users understand how to properly integrate it into their libp2p applications. diff --git a/setup.py b/setup.py index a17574e0..a6e24cfa 100644 --- a/setup.py +++ b/setup.py @@ -117,6 +117,7 @@ setup( "chat-demo=examples.chat.chat:main", "echo-demo=examples.echo.echo:main", "ping-demo=examples.ping.ping:main", + "identify-demo=examples.identify.identify:main", ], }, ) diff --git a/tests/core/identity/identify/test_identify.py b/tests/core/identity/identify/test_identify.py new file mode 100644 index 00000000..8dd58521 --- /dev/null +++ b/tests/core/identity/identify/test_identify.py @@ -0,0 +1,79 @@ +import logging + +import pytest +from multiaddr import ( + Multiaddr, +) + +from libp2p.identity.identify.identify import ( + AGENT_VERSION, + ID, + PROTOCOL_VERSION, + _mk_identify_protobuf, + _multiaddr_to_bytes, +) +from libp2p.identity.identify.pb.identify_pb2 import ( + Identify, +) +from tests.factories import ( + host_pair_factory, +) + +logger = logging.getLogger("libp2p.identity.identify-test") + + +@pytest.mark.trio +async def test_identify_protocol(security_protocol): + async with host_pair_factory(security_protocol=security_protocol) as ( + host_a, + host_b, + ): + # Here, host_b is the requester and host_a is the responder. + # observed_addr represent host_b’s address as observed by host_a + # (i.e., the address from which host_b’s request was received). + stream = await host_b.new_stream(host_a.get_id(), (ID,)) + response = await stream.read() + await stream.close() + + identify_response = Identify() + identify_response.ParseFromString(response) + + logger.debug("host_a: %s", host_a.get_addrs()) + logger.debug("host_b: %s", host_b.get_addrs()) + + # Check protocol version + assert identify_response.protocol_version == PROTOCOL_VERSION + + # Check agent version + assert identify_response.agent_version == AGENT_VERSION + + # Check public key + assert identify_response.public_key == host_a.get_public_key().serialize() + + # Check listen addresses + assert identify_response.listen_addrs == list( + map(_multiaddr_to_bytes, host_a.get_addrs()) + ) + + # Check observed address + # TODO: use decapsulateCode(protocols('p2p').code) + # when the Multiaddr class will implement it + host_b_addr = host_b.get_addrs()[0] + cleaned_addr = Multiaddr.join( + *( + host_b_addr.split()[:-1] + if str(host_b_addr.split()[-1]).startswith("/p2p/") + else host_b_addr.split() + ) + ) + + logger.debug("observed_addr: %s", Multiaddr(identify_response.observed_addr)) + logger.debug("host_b.get_addrs()[0]: %s", host_b.get_addrs()[0]) + logger.debug("cleaned_addr= %s", cleaned_addr) + assert identify_response.observed_addr == _multiaddr_to_bytes(cleaned_addr) + + # Check protocols + assert set(identify_response.protocols) == set(host_a.get_mux().get_protocols()) + + # sanity check + assert identify_response == _mk_identify_protobuf(host_a, cleaned_addr) diff --git a/tests/core/identity/identify/test_protocol.py b/tests/core/identity/identify/test_protocol.py deleted file mode 100644 index af87ab1a..00000000 --- a/tests/core/identity/identify/test_protocol.py +++ /dev/null @@ -1,158 +0,0 @@ -import logging - -import pytest -from multiaddr import ( - Multiaddr, -) - -from libp2p.identity.identify.pb.identify_pb2 import ( - Identify, -) -from libp2p.identity.identify.protocol import ( - AGENT_VERSION, - ID, - PROTOCOL_VERSION, - _mk_identify_protobuf, - _multiaddr_to_bytes, - _remote_address_to_multiaddr, -) -from tests.factories import ( - host_pair_factory, -) - - -def clean_multiaddr(addr: Multiaddr) -> Multiaddr: - """ - Clean a multiaddr by removing the '/p2p/' part if it exists. - - Args: - addr: The multiaddr to clean - - Returns: - The cleaned multiaddr - """ - return Multiaddr.join( - *( - addr.split()[:-1] - if str(addr.split()[-1]).startswith("/p2p/") - else addr.split() - ) - ) - - -# logger = logging.getLogger("libp2p.identity.identify-test") -logger = logging.getLogger(__name__) - - -@pytest.mark.trio -async def test_identify_protocol(security_protocol): - async with host_pair_factory(security_protocol=security_protocol) as ( - host_a, - host_b, - ): - # Here, host_b is the requester and host_a is the responder. - # observed_addr represent host_b's address as observed by host_a - # (i.e., the address from which host_b's request was received). - stream = await host_b.new_stream(host_a.get_id(), (ID,)) - response = await stream.read() - await stream.close() - - identify_response = Identify() - identify_response.ParseFromString(response) - - logger.debug("host_a: %s", host_a.get_addrs()) - logger.debug("host_b: %s", host_b.get_addrs()) - - # Check protocol version - assert identify_response.protocol_version == PROTOCOL_VERSION - - # Check agent version - assert identify_response.agent_version == AGENT_VERSION - - # Check public key - assert identify_response.public_key == host_a.get_public_key().serialize() - - # Check listen addresses - assert identify_response.listen_addrs == list( - map(_multiaddr_to_bytes, host_a.get_addrs()) - ) - - # Check observed address - # TODO: use decapsulateCode(protocols('p2p').code) - # when the Multiaddr class will implement it - host_b_addr = host_b.get_addrs()[0] - cleaned_addr = clean_multiaddr(host_b_addr) - - logger.debug("observed_addr: %s", Multiaddr(identify_response.observed_addr)) - logger.debug("host_b.get_addrs()[0]: %s", host_b.get_addrs()[0]) - logger.debug("cleaned_addr= %s", cleaned_addr) - assert identify_response.observed_addr == _multiaddr_to_bytes(cleaned_addr) - - # Check protocols - assert set(identify_response.protocols) == set(host_a.get_mux().get_protocols()) - - # sanity check - assert identify_response == _mk_identify_protobuf(host_a, cleaned_addr) - - -@pytest.mark.trio -async def test_complete_remote_address_delegation_chain(security_protocol): - async with host_pair_factory(security_protocol=security_protocol) as ( - host_a, - host_b, - ): - logger.debug( - "test_complete_remote_address_delegation_chain security_protocol: %s", - security_protocol, - ) - - # Create a stream from host_a to host_b - stream = await host_a.new_stream(host_b.get_id(), (ID,)) - - # Get references to all components in the delegation chain - mplex_stream = stream.muxed_stream - swarm_conn = host_a.get_network().connections[host_b.get_id()] - mplex = swarm_conn.muxed_conn - secure_session = mplex.secured_conn - raw_connection = secure_session.conn - trio_tcp_stream = raw_connection.stream - - # Get remote addresses at each layer - stream_addr = stream.get_remote_address() - muxed_stream_addr = stream.muxed_stream.get_remote_address() - mplex_addr = mplex_stream.muxed_conn.get_remote_address() - secure_session_addr = mplex.secured_conn.get_remote_address() - raw_connection_addr = secure_session.conn.get_remote_address() - trio_tcp_stream_addr = raw_connection.stream.get_remote_address() - socket_addr = trio_tcp_stream.stream.socket.getpeername() - - # Log all addresses - logger.debug("NetStream address: %s", stream_addr) - logger.debug("MplexStream address: %s", muxed_stream_addr) - logger.debug("Mplex address: %s", mplex_addr) - logger.debug("SecureSession address: %s", secure_session_addr) - logger.debug("RawConnection address: %s", raw_connection_addr) - logger.debug("TrioTCPStream address: %s", trio_tcp_stream_addr) - logger.debug("Socket address: %s", socket_addr) - - # Verify complete delegation chain - assert ( - stream_addr - == muxed_stream_addr - == mplex_addr - == secure_session_addr - == raw_connection_addr - == trio_tcp_stream_addr - == socket_addr - ) - - # Convert to multiaddr and verify it matches host_b's cleaned address - remote_address_multiaddr = _remote_address_to_multiaddr(stream_addr) - host_b_addr = clean_multiaddr(host_b.get_addrs()[0]) - - logger.debug("Remote address multiaddr: %s", remote_address_multiaddr) - logger.debug("Host B cleaned address: %s", host_b_addr) - - assert remote_address_multiaddr == host_b_addr - - await stream.close()