diff --git a/examples/identify_push/identify_push_demo.py b/examples/identify_push/identify_push_demo.py index d6c84950..41030d9b 100644 --- a/examples/identify_push/identify_push_demo.py +++ b/examples/identify_push/identify_push_demo.py @@ -35,10 +35,10 @@ from libp2p.peer.peerinfo import ( ) # Configure logging -logging.basicConfig( - level=logging.DEBUG, - format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", -) +# logging.basicConfig( +# level=logging.DEBUG, +# format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", +# ) logger = logging.getLogger(__name__) @@ -119,3 +119,9 @@ async def main() -> None: if __name__ == "__main__": trio.run(main) + + +# Non-async entry point for console script +def run_main(): + """Non-async entry point for the console script.""" + trio.run(main) diff --git a/examples/identify_push/identify_push_listener_dialer.py b/examples/identify_push/identify_push_listener_dialer.py index 895bcc4a..16be6fba 100644 --- a/examples/identify_push/identify_push_listener_dialer.py +++ b/examples/identify_push/identify_push_listener_dialer.py @@ -23,11 +23,17 @@ import logging import sys import multiaddr +from multiaddr import ( + Multiaddr, +) import trio from libp2p import ( new_host, ) +from libp2p.abc import ( + INetStream, +) from libp2p.crypto.secp256k1 import ( create_new_key_pair, ) @@ -35,6 +41,9 @@ from libp2p.identity.identify import ( identify_handler_for, ) from libp2p.identity.identify import ID as ID_IDENTIFY +from libp2p.identity.identify.pb.identify_pb2 import ( + Identify, +) from libp2p.identity.identify_push import ( identify_push_handler_for, push_identify_to_peer, @@ -45,16 +54,88 @@ from libp2p.peer.peerinfo import ( ) # Configure logging -logging.basicConfig( - level=logging.DEBUG, - format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", -) +# logging.basicConfig( +# level=logging.DEBUG, +# format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", +# ) logger = logging.getLogger("libp2p.identity.identify-push-example") # Default port configuration DEFAULT_PORT = 8888 +def custom_identify_push_handler_for(host): + """ + Create a custom handler for the identify/push protocol that logs and prints + the identity information received from the dialer. + """ + + async def handle_identify_push(stream: INetStream) -> None: + peer_id = stream.muxed_conn.peer_id + + try: + # Read the identify message from the stream + data = await stream.read() + identify_msg = Identify() + identify_msg.ParseFromString(data) + + # Log and print the identify information + logger.info("Received identify/push from peer %s", peer_id) + print(f"\n==== Received identify/push from peer {peer_id} ====") + + if identify_msg.HasField("protocol_version"): + logger.info(" Protocol Version: %s", identify_msg.protocol_version) + print(f" Protocol Version: {identify_msg.protocol_version}") + + if identify_msg.HasField("agent_version"): + logger.info(" Agent Version: %s", identify_msg.agent_version) + print(f" Agent Version: {identify_msg.agent_version}") + + if identify_msg.HasField("public_key"): + logger.info( + " Public Key: %s", identify_msg.public_key.hex()[:16] + "..." + ) + print(f" Public Key: {identify_msg.public_key.hex()[:16]}...") + + if identify_msg.listen_addrs: + addrs = [Multiaddr(addr) for addr in identify_msg.listen_addrs] + logger.info(" Listen Addresses: %s", addrs) + print(" Listen Addresses:") + for addr in addrs: + print(f" - {addr}") + + if identify_msg.HasField("observed_addr") and identify_msg.observed_addr: + observed_addr = Multiaddr(identify_msg.observed_addr) + logger.info(" Observed Address: %s", observed_addr) + print(f" Observed Address: {observed_addr}") + + if identify_msg.protocols: + logger.info(" Protocols: %s", identify_msg.protocols) + print(" Protocols:") + for protocol in identify_msg.protocols: + print(f" - {protocol}") + + # Update the peerstore with the new information as usual + peerstore = host.get_peerstore() + from libp2p.identity.identify_push.identify_push import ( + _update_peerstore_from_identify, + ) + + await _update_peerstore_from_identify(peerstore, peer_id, identify_msg) + + logger.info("Successfully processed identify/push from peer %s", peer_id) + print(f"\nSuccessfully processed identify/push from peer {peer_id}") + + except Exception as e: + logger.error("Error processing identify/push from %s: %s", peer_id, e) + print(f"\nError processing identify/push from {peer_id}: {e}") + finally: + # Close the stream after processing + await stream.close() + + return handle_identify_push + + async def run_listener(port: int) -> None: """Run a host in listener mode.""" print(f"\n==== Starting Identify-Push Listener on port {port} ====\n") @@ -67,7 +148,7 @@ async def run_listener(port: int) -> None: # Set up the identify and identify/push handlers host.set_stream_handler(ID_IDENTIFY, identify_handler_for(host)) - host.set_stream_handler(ID_IDENTIFY_PUSH, identify_push_handler_for(host)) + host.set_stream_handler(ID_IDENTIFY_PUSH, custom_identify_push_handler_for(host)) # Start listening listen_addr = multiaddr.Multiaddr(f"/ip4/0.0.0.0/tcp/{port}") @@ -84,7 +165,7 @@ async def run_listener(port: int) -> None: print(f"Peer ID: {host.get_id().pretty()}") print("\nRun dialer with command:") - print(f"python identify_push_listener_dialer.py -d {addr}") + print(f"identify-push-listener-dialer-demo -d {addr}") print("\nWaiting for incoming connections... (Ctrl+C to exit)") # Keep running until interrupted diff --git a/setup.py b/setup.py index 9196f8aa..144d3063 100644 --- a/setup.py +++ b/setup.py @@ -108,6 +108,8 @@ setup( "echo-demo=examples.echo.echo:main", "ping-demo=examples.ping.ping:main", "identify-demo=examples.identify.identify:main", + "identify-push-demo=examples.identify_push.identify_push_demo:run_main", + "identify-push-listener-dialer-demo=examples.identify_push.identify_push_listener_dialer:main", "pubsub-demo=examples.pubsub.pubsub:main", ], }, diff --git a/tests/core/identity/identify_push/test_identify_push.py b/tests/core/identity/identify_push/test_identify_push.py index ec9ef2a0..1e935c13 100644 --- a/tests/core/identity/identify_push/test_identify_push.py +++ b/tests/core/identity/identify_push/test_identify_push.py @@ -14,6 +14,7 @@ from libp2p.identity.identify_push.identify_push import ( _update_peerstore_from_identify, identify_push_handler_for, push_identify_to_peer, + push_identify_to_peers, ) from tests.utils.factories import ( host_pair_factory, @@ -24,6 +25,16 @@ logger = logging.getLogger("libp2p.identity.identify-push-test") @pytest.mark.trio async def test_identify_push_protocol(security_protocol): + """ + Test the basic functionality of the identify/push protocol. + + This test verifies that when host_a pushes identify information to host_b: + 1. The information is correctly received and processed + 2. The peerstore of host_b is updated with host_a's peer ID + 3. The peerstore contains all of host_a's addresses + 4. The peerstore contains all of host_a's supported protocols + 5. The public key in the peerstore matches host_a's public key + """ async with host_pair_factory(security_protocol=security_protocol) as ( host_a, host_b, @@ -72,6 +83,17 @@ async def test_identify_push_protocol(security_protocol): @pytest.mark.trio async def test_identify_push_handler(security_protocol): + """ + Test the identify_push_handler_for function specifically. + + This test focuses on verifying that the handler function correctly: + 1. Receives the identify message + 2. Updates the peerstore with the peer information + 3. Processes all fields of the identify message (addresses, protocols, public key) + + Note: This test is similar to test_identify_push_protocol but specifically + focuses on the handler function's behavior. + """ async with host_pair_factory(security_protocol=security_protocol) as ( host_a, host_b, @@ -120,6 +142,16 @@ async def test_identify_push_handler(security_protocol): @pytest.mark.trio async def test_identify_push_to_peers(security_protocol): + """ + Test the push_identify_to_peers function to broadcast identity to multiple peers. + + This test verifies that: + 1. Host_a can push identify information to multiple peers simultaneously + 2. The identify information is correctly received by all connected peers + 3. Both host_b and host_c have their peerstores updated with host_a's information + + This tests the broadcasting capability of the identify/push protocol. + """ # Create three hosts async with host_pair_factory(security_protocol=security_protocol) as ( host_a, @@ -178,8 +210,116 @@ async def test_identify_push_to_peers(security_protocol): assert peer_id_a in peerstore_c.peer_ids() +@pytest.mark.trio +async def test_push_identify_to_peers_with_explicit_params(security_protocol): + """ + Test the push_identify_to_peers function with explicit parameters. + + This test verifies that: + 1. The function correctly handles an explicitly provided set of peer IDs + 2. The function correctly uses the provided observed_multiaddr + 3. The identify information is only pushed to the specified peers + 4. The observed address is correctly included in the identify message + + This test ensures all parameters of push_identify_to_peers are properly tested. + """ + import multiaddr + + from libp2p import ( + new_host, + ) + from libp2p.crypto.secp256k1 import ( + create_new_key_pair, + ) + from libp2p.peer.peerinfo import ( + info_from_p2p_addr, + ) + + # Create four hosts to thoroughly test selective pushing + async with host_pair_factory(security_protocol=security_protocol) as ( + host_a, + host_b, + ): + # Create two additional hosts + key_pair_c = create_new_key_pair() + host_c = new_host(key_pair=key_pair_c) + + key_pair_d = create_new_key_pair() + host_d = new_host(key_pair=key_pair_d) + + # Set up the identify/push handlers for all hosts + host_b.set_stream_handler(ID_PUSH, identify_push_handler_for(host_b)) + host_c.set_stream_handler(ID_PUSH, identify_push_handler_for(host_c)) + host_d.set_stream_handler(ID_PUSH, identify_push_handler_for(host_d)) + + # Start listening on random ports + listen_addr_c = multiaddr.Multiaddr("/ip4/127.0.0.1/tcp/0") + listen_addr_d = multiaddr.Multiaddr("/ip4/127.0.0.1/tcp/0") + + async with host_c.run([listen_addr_c]), host_d.run([listen_addr_d]): + # Connect all hosts to host_a + await host_c.connect(info_from_p2p_addr(host_a.get_addrs()[0])) + await host_d.connect(info_from_p2p_addr(host_a.get_addrs()[0])) + + # Create a specific observed multiaddr for the test + observed_addr = multiaddr.Multiaddr("/ip4/192.0.2.1/tcp/1234") + + # Only push to hosts B and C (not D) + selected_peers = {host_b.get_id(), host_c.get_id()} + + # Push identify information from host_a to selected peers with observed addr + await push_identify_to_peers( + host=host_a, peer_ids=selected_peers, observed_multiaddr=observed_addr + ) + + # Wait a bit for the push to complete + await trio.sleep(0.1) + + # Check that host_b's and host_c's peerstores have been updated + peerstore_b = host_b.get_peerstore() + peerstore_c = host_c.get_peerstore() + peerstore_d = host_d.get_peerstore() + peer_id_a = host_a.get_id() + + # Hosts B and C should have peer_id_a in their peerstores + assert peer_id_a in peerstore_b.peer_ids() + assert peer_id_a in peerstore_c.peer_ids() + + # Host D should NOT have peer_id_a in its peerstore from the push + # (it may still have it from the connection) + # So we check for the observed address instead, which would only be + # present from a push + + # Hosts B and C should have the observed address in their peerstores + addrs_b = [str(addr) for addr in peerstore_b.addrs(peer_id_a)] + addrs_c = [str(addr) for addr in peerstore_c.addrs(peer_id_a)] + + assert str(observed_addr) in addrs_b + assert str(observed_addr) in addrs_c + + # If host D has addresses for peer_id_a, the observed address + # should not be there + if peer_id_a in peerstore_d.peer_ids(): + addrs_d = [str(addr) for addr in peerstore_d.addrs(peer_id_a)] + assert str(observed_addr) not in addrs_d + + @pytest.mark.trio async def test_update_peerstore_from_identify(security_protocol): + """ + Test the _update_peerstore_from_identify function directly. + + This test verifies that the internal function responsible for updating + the peerstore from an identify message works correctly: + 1. It properly updates the peerstore with all fields from the identify message + 2. The peer ID is added to the peerstore + 3. All addresses are correctly stored + 4. All protocols are correctly stored + 5. The public key is correctly stored + + This tests the low-level peerstore update mechanism used by + the identify/push protocol. + """ async with host_pair_factory(security_protocol=security_protocol) as ( host_a, host_b, @@ -225,6 +365,19 @@ async def test_update_peerstore_from_identify(security_protocol): @pytest.mark.trio async def test_partial_update_peerstore_from_identify(security_protocol): + """ + Test partial updates of the peerstore using the identify/push protocol. + + This test verifies that: + 1. A partial identify message (containing only some fields) correctly updates + the peerstore without affecting other existing information + 2. New protocols are added to the existing set in the peerstore + 3. The original protocols, addresses, and public key remain intact + 4. The update is additive rather than replacing all existing data + + This tests the ability of the identify/push protocol to handle incremental + or partial updates to peer information. + """ async with host_pair_factory(security_protocol=security_protocol) as ( host_a, host_b,