diff --git a/docs/examples.identify_push.rst b/docs/examples.identify_push.rst index 202668e4..72981379 100644 --- a/docs/examples.identify_push.rst +++ b/docs/examples.identify_push.rst @@ -1,21 +1,79 @@ -examples.identify\_push package -=============================== +Identify Push Protocol Demo +=========================== -Submodules ----------- +This example demonstrates how to use the libp2p ``identify-push`` protocol, which allows nodes to proactively push their identity information to peers when it changes. -examples.identify\_push.identify\_push\_example module ------------------------------------------------------- +.. code-block:: console -.. automodule:: examples.identify_push.identify_push_example - :members: - :undoc-members: - :show-inheritance: + $ python -m pip install libp2p + Collecting libp2p + ... + Successfully installed libp2p-x.x.x + $ identify-push-demo + ==== Starting Identify-Push Example ==== -Module contents ---------------- + Host 1 listening on /ip4/127.0.0.1/tcp/xxxxx/p2p/QmAbCdEfGhIjKlMnOpQrStUvWxYz + Peer ID: QmAbCdEfGhIjKlMnOpQrStUvWxYz + Host 2 listening on /ip4/127.0.0.1/tcp/xxxxx/p2p/QmZyXwVuTaBcDeRsSkJpOpWrSt + Peer ID: QmZyXwVuTaBcDeRsSkJpOpWrSt -.. automodule:: examples.identify_push - :members: - :undoc-members: - :show-inheritance: + Connecting Host 2 to Host 1... + Host 2 successfully connected to Host 1 + + Host 1 pushing identify information to Host 2... + Identify push completed successfully! + + Example completed successfully! + +There is also a more interactive version of the example which runs as separate listener and dialer processes: + +.. code-block:: console + + $ identify-push-listener-dialer + + ==== Starting Identify-Push Listener on port 8888 ==== + + Listener host ready! + Listening on: /ip4/0.0.0.0/tcp/8888/p2p/QmUiN4R3fNrCoQugGgmmb3v35neMEjKFNrsbNGVDsRHWpM + Peer ID: QmUiN4R3fNrCoQugGgmmb3v35neMEjKFNrsbNGVDsRHWpM + + Run dialer with command: + identify-push-listener-dialer -d /ip4/0.0.0.0/tcp/8888/p2p/QmUiN4R3fNrCoQugGgmmb3v35neMEjKFNrsbNGVDsRHWpM + + Waiting for incoming connections... (Ctrl+C to exit) + +Copy the line that starts with ``identify-push-listener-dialer -d ...``, open a new terminal in the same +folder and paste it in: + +.. code-block:: console + + $ identify-push-listener-dialer -d /ip4/0.0.0.0/tcp/8888/p2p/QmUiN4R3fNrCoQugGgmmb3v35neMEjKFNrsbNGVDsRHWpM + + ==== Starting Identify-Push Dialer on port 8889 ==== + + Dialer host ready! + Listening on: /ip4/0.0.0.0/tcp/8889/p2p/QmZyXwVuTaBcDeRsSkJpOpWrSt + + Connecting to peer: QmUiN4R3fNrCoQugGgmmb3v35neMEjKFNrsbNGVDsRHWpM + Successfully connected to listener! + + Pushing identify information to listener... + Identify push completed successfully! + + Example completed successfully! + +The identify-push protocol enables libp2p nodes to proactively notify their peers when their metadata changes, such as supported protocols or listening addresses. This helps maintain an up-to-date view of the network without requiring regular polling. + +The full source code for these examples is below: + +Basic example: + +.. literalinclude:: ../examples/identify_push/identify_push_demo.py + :language: python + :linenos: + +Listener/Dialer example: + +.. literalinclude:: ../examples/identify_push/identify_push_listener_dialer.py + :language: python + :linenos: diff --git a/docs/examples.rst b/docs/examples.rst index 1658cba8..e2f0bdd4 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -6,6 +6,7 @@ Examples :caption: Examples: examples.identify + examples.identify_push examples.chat examples.echo examples.ping diff --git a/docs/libp2p.identity.rst b/docs/libp2p.identity.rst index ccd89182..d3bf0df8 100644 --- a/docs/libp2p.identity.rst +++ b/docs/libp2p.identity.rst @@ -8,6 +8,7 @@ Subpackages :maxdepth: 4 libp2p.identity.identify + libp2p.identity.identify_push Module contents --------------- diff --git a/examples/identify_push/identify_push_example.py b/examples/identify_push/identify_push_demo.py similarity index 60% rename from examples/identify_push/identify_push_example.py rename to examples/identify_push/identify_push_demo.py index 9666bde2..d6c84950 100644 --- a/examples/identify_push/identify_push_example.py +++ b/examples/identify_push/identify_push_demo.py @@ -43,6 +43,8 @@ logger = logging.getLogger(__name__) async def main() -> None: + print("\n==== Starting Identify-Push Example ====\n") + # Create key pairs for the two hosts key_pair_1 = create_new_key_pair() key_pair_2 = create_new_key_pair() @@ -70,27 +72,49 @@ async def main() -> None: async with host_1.run([listen_addr_1]), host_2.run([listen_addr_2]): # Get the addresses of both hosts addr_1 = host_1.get_addrs()[0] - logger.info("Host 1 listening on %s", addr_1) + logger.info(f"Host 1 listening on {addr_1}") + print(f"Host 1 listening on {addr_1}") + print(f"Peer ID: {host_1.get_id().pretty()}") addr_2 = host_2.get_addrs()[0] - logger.info("Host 2 listening on %s", addr_2) + logger.info(f"Host 2 listening on {addr_2}") + print(f"Host 2 listening on {addr_2}") + print(f"Peer ID: {host_2.get_id().pretty()}") + + print("\nConnecting Host 2 to Host 1...") # Connect host_2 to host_1 peer_info = info_from_p2p_addr(addr_1) await host_2.connect(peer_info) logger.info("Host 2 connected to Host 1") - - # Wait a bit for the connection to establish - await trio.sleep(1) + print("Host 2 successfully connected to Host 1") # Push identify information from host_1 to host_2 logger.info("Host 1 pushing identify information to Host 2") - await push_identify_to_peer(host_1, host_2.get_id()) + print("\nHost 1 pushing identify information to Host 2...") - # Wait a bit for the push to complete - await trio.sleep(1) + try: + # Call push_identify_to_peer which now returns a boolean + success = await push_identify_to_peer(host_1, host_2.get_id()) - logger.info("Example completed successfully") + if success: + logger.info("Identify push completed successfully") + print("Identify push completed successfully!") + + logger.info("Example completed successfully") + print("\nExample completed successfully!") + else: + logger.warning("Identify push didn't complete successfully") + print("\nWarning: Identify push didn't complete successfully") + + logger.warning("Example completed with warnings") + print("Example completed with warnings") + except Exception as e: + logger.error(f"Error during identify push: {str(e)}") + print(f"\nError during identify push: {str(e)}") + + logger.error("Example completed with errors") + print("Example completed with errors") if __name__ == "__main__": diff --git a/examples/identify_push/identify_push_listener_dialer.py b/examples/identify_push/identify_push_listener_dialer.py index 937bd136..895bcc4a 100644 --- a/examples/identify_push/identify_push_listener_dialer.py +++ b/examples/identify_push/identify_push_listener_dialer.py @@ -31,17 +31,15 @@ from libp2p import ( from libp2p.crypto.secp256k1 import ( create_new_key_pair, ) -from libp2p.custom_types import ( - TProtocol, -) from libp2p.identity.identify import ( identify_handler_for, ) +from libp2p.identity.identify import ID as ID_IDENTIFY from libp2p.identity.identify_push import ( - ID_PUSH, identify_push_handler_for, push_identify_to_peer, ) +from libp2p.identity.identify_push import ID_PUSH as ID_IDENTIFY_PUSH from libp2p.peer.peerinfo import ( info_from_p2p_addr, ) @@ -53,6 +51,9 @@ logging.basicConfig( ) logger = logging.getLogger("libp2p.identity.identify-push-example") +# Default port configuration +DEFAULT_PORT = 8888 + async def run_listener(port: int) -> None: """Run a host in listener mode.""" @@ -65,22 +66,23 @@ async def run_listener(port: int) -> None: host = new_host(key_pair=key_pair) # Set up the identify and identify/push handlers - host.set_stream_handler(TProtocol("/ipfs/id/1.0.0"), identify_handler_for(host)) - host.set_stream_handler(ID_PUSH, identify_push_handler_for(host)) + host.set_stream_handler(ID_IDENTIFY, identify_handler_for(host)) + host.set_stream_handler(ID_IDENTIFY_PUSH, identify_push_handler_for(host)) # Start listening listen_addr = multiaddr.Multiaddr(f"/ip4/0.0.0.0/tcp/{port}") async with host.run([listen_addr]): addr = host.get_addrs()[0] - logger.info("Listener host ready") - logger.info("Listening on: %s", addr) - logger.info("Peer ID: %s", host.get_id().pretty()) - - # Print user-friendly information + logger.info("Listener host ready!") print("Listener host ready!") + + logger.info(f"Listening on: {addr}") print(f"Listening on: {addr}") + + logger.info(f"Peer ID: {host.get_id().pretty()}") print(f"Peer ID: {host.get_id().pretty()}") + print("\nRun dialer with command:") print(f"python identify_push_listener_dialer.py -d {addr}") print("\nWaiting for incoming connections... (Ctrl+C to exit)") @@ -100,49 +102,60 @@ async def run_dialer(port: int, destination: str) -> None: host = new_host(key_pair=key_pair) # Set up the identify and identify/push handlers - host.set_stream_handler(TProtocol("/ipfs/id/1.0.0"), identify_handler_for(host)) - host.set_stream_handler(ID_PUSH, identify_push_handler_for(host)) + host.set_stream_handler(ID_IDENTIFY, identify_handler_for(host)) + host.set_stream_handler(ID_IDENTIFY_PUSH, identify_push_handler_for(host)) # Start listening on a different port listen_addr = multiaddr.Multiaddr(f"/ip4/0.0.0.0/tcp/{port}") async with host.run([listen_addr]): - logger.info("Dialer host ready") - logger.info("Listening on: %s", host.get_addrs()[0]) - + logger.info("Dialer host ready!") print("Dialer host ready!") + + logger.info(f"Listening on: {host.get_addrs()[0]}") print(f"Listening on: {host.get_addrs()[0]}") # Parse the destination multiaddress and connect to the listener maddr = multiaddr.Multiaddr(destination) peer_info = info_from_p2p_addr(maddr) - logger.info("Connecting to peer: %s", peer_info.peer_id) + logger.info(f"Connecting to peer: {peer_info.peer_id}") print(f"\nConnecting to peer: {peer_info.peer_id}") try: await host.connect(peer_info) - logger.info("Successfully connected to listener") + logger.info("Successfully connected to listener!") print("Successfully connected to listener!") - # Wait briefly for the connection to establish - await trio.sleep(1) - # Push identify information to the listener - logger.info("Pushing identify information to listener") + logger.info("Pushing identify information to listener...") print("\nPushing identify information to listener...") - await push_identify_to_peer(host, peer_info.peer_id) - logger.info("Identify push completed. Waiting a moment...") - print("Identify push completed successfully!") + try: + # Call push_identify_to_peer which returns a boolean + success = await push_identify_to_peer(host, peer_info.peer_id) - # Keep the connection open for a bit to observe what happens - print("Waiting a moment to keep connection open...") - await trio.sleep(5) + if success: + logger.info("Identify push completed successfully!") + print("Identify push completed successfully!") + + logger.info("Example completed successfully!") + print("\nExample completed successfully!") + else: + logger.warning("Identify push didn't complete successfully.") + print("\nWarning: Identify push didn't complete successfully.") + + logger.warning("Example completed with warnings.") + print("Example completed with warnings.") + except Exception as e: + logger.error(f"Error during identify push: {str(e)}") + print(f"\nError during identify push: {str(e)}") + + logger.error("Example completed with errors.") + print("Example completed with errors.") + # Continue execution despite the push error - logger.info("Example completed successfully") - print("\nExample completed successfully!") except Exception as e: - logger.error("Error during dialer operation: %s", str(e)) + logger.error(f"Error during dialer operation: {str(e)}") print(f"\nError during dialer operation: {str(e)}") raise @@ -156,7 +169,8 @@ def main() -> None: """ example = ( - "/ip4/127.0.0.1/tcp/8888/p2p/QmQn4SwGkDZkUEpBRBvTmheQycxAHJUNmVEnjA2v1qe8Q" + f"/ip4/127.0.0.1/tcp/{DEFAULT_PORT}/p2p/" + "QmQn4SwGkDZkUEpBRBvTmheQycxAHJUNmVEnjA2v1qe8Q" ) parser = argparse.ArgumentParser(description=description) @@ -164,7 +178,10 @@ def main() -> None: "-p", "--port", type=int, - help="port to listen on (default: 8888 for listener, 8889 for dialer)", + help=( + f"port to listen on (default: {DEFAULT_PORT} for listener, " + f"{DEFAULT_PORT + 1} for dialer)" + ), ) parser.add_argument( "-d", @@ -176,12 +193,12 @@ def main() -> None: try: if args.destination: - # Run in dialer mode with default port 8889 if not specified - port = args.port if args.port is not None else 8889 + # Run in dialer mode with default port DEFAULT_PORT + 1 if not specified + port = args.port if args.port is not None else DEFAULT_PORT + 1 trio.run(run_dialer, port, args.destination) else: - # Run in listener mode with default port 8888 if not specified - port = args.port if args.port is not None else 8888 + # Run in listener mode with default port DEFAULT_PORT if not specified + port = args.port if args.port is not None else DEFAULT_PORT trio.run(run_listener, port) except KeyboardInterrupt: print("\nInterrupted by user") diff --git a/libp2p/identity/identify_push/identify_push.py b/libp2p/identity/identify_push/identify_push.py index 2f57f666..1110b5e3 100644 --- a/libp2p/identity/identify_push/identify_push.py +++ b/libp2p/identity/identify_push/identify_push.py @@ -135,12 +135,18 @@ async def _update_peerstore_from_identify( async def push_identify_to_peer( host: IHost, peer_id: ID, observed_multiaddr: Optional[Multiaddr] = None -) -> None: +) -> bool: """ Push an identify message to a specific peer. This function opens a stream to the peer using the identify/push protocol, sends the identify message, and closes the stream. + + Returns + ------- + bool + True if the push was successful, False otherwise. + """ try: # Create a new stream to the peer using the identify/push protocol @@ -157,8 +163,10 @@ async def push_identify_to_peer( await stream.close() logger.debug("Successfully pushed identify to peer %s", peer_id) + return True except Exception as e: logger.error("Error pushing identify to peer %s: %s", peer_id, e) + return False async def push_identify_to_peers( diff --git a/newsfragments/552.feature.rst b/newsfragments/552.feature.rst new file mode 100644 index 00000000..168d2cd3 --- /dev/null +++ b/newsfragments/552.feature.rst @@ -0,0 +1 @@ +Added identify-push protocol implementation and examples to demonstrate how peers can proactively push their identity information to other peers when it changes. diff --git a/tests/core/examples/test_examples.py b/tests/core/examples/test_examples.py index d46e4208..a003233d 100644 --- a/tests/core/examples/test_examples.py +++ b/tests/core/examples/test_examples.py @@ -7,6 +7,11 @@ from libp2p.custom_types import ( from libp2p.host.exceptions import ( StreamFailure, ) +from libp2p.identity.identify_push import ( + ID_PUSH, + identify_push_handler_for, + push_identify_to_peer, +) from libp2p.peer.peerinfo import ( info_from_p2p_addr, ) @@ -246,6 +251,32 @@ async def pubsub_demo(host_a, host_b): assert b_received.is_set() +async def identify_push_demo(host_a, host_b): + # Set up the identify/push handlers + host_b.set_stream_handler(ID_PUSH, identify_push_handler_for(host_b)) + + # Push identify information from host_a to host_b + success = await push_identify_to_peer(host_a, host_b.get_id()) + assert success is True + + # Check that host_b's peerstore has been updated with host_a's information + peer_id = host_a.get_id() + peerstore = host_b.get_peerstore() + + # Check that the peer is in the peerstore + assert peer_id in peerstore.peer_ids() + + # Check that the protocols were updated + host_a_protocols = set(host_a.get_mux().get_protocols()) + peerstore_protocols = set(peerstore.get_protocols(peer_id)) + assert all(protocol in peerstore_protocols for protocol in host_a_protocols) + + # Check that the addresses were updated + host_a_addrs = set(host_a.get_addrs()) + peerstore_addrs = set(peerstore.addrs(peer_id)) + assert all(addr in peerstore_addrs for addr in host_a_addrs) + + @pytest.mark.parametrize( "test", [ @@ -257,6 +288,7 @@ async def pubsub_demo(host_a, host_b): echo_demo, ping_demo, pubsub_demo, + identify_push_demo, ], ) @pytest.mark.trio