diff --git a/.gitignore b/.gitignore index e46cc8aa..85c04467 100644 --- a/.gitignore +++ b/.gitignore @@ -178,3 +178,7 @@ env.bak/ #lockfiles uv.lock poetry.lock + +bootstrap_instructions.txt +.gitignore +README.md diff --git a/examples/bootstrap/bootstrap.py b/examples/bootstrap/bootstrap.py new file mode 100644 index 00000000..415b7a81 --- /dev/null +++ b/examples/bootstrap/bootstrap.py @@ -0,0 +1,121 @@ +import argparse +import logging +import secrets + +import multiaddr +import trio + +from libp2p import new_host +from libp2p.abc import PeerInfo +from libp2p.crypto.secp256k1 import create_new_key_pair +from libp2p.discovery.events.peerDiscovery import peerDiscovery + +# Configure logging +logger = logging.getLogger("libp2p.discovery.bootstrap") +logger.setLevel(logging.INFO) +handler = logging.StreamHandler() +handler.setFormatter( + logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") +) +logger.addHandler(handler) + +# Set root logger to DEBUG to capture all logs +logging.getLogger().setLevel(logging.DEBUG) + + +def on_peer_discovery(peer_info: PeerInfo) -> None: + """Handler for peer discovery events.""" + logger.info(f"๐Ÿ” Discovered peer: {peer_info.peer_id}") + logger.info(f" Addresses: {[str(addr) for addr in peer_info.addrs]}") + + +# Example bootstrap peers (you can replace with real bootstrap nodes) +BOOTSTRAP_PEERS = [ + # IPFS bootstrap nodes (examples - replace with actual working nodes) + "/ip4/104.131.131.82/tcp/4001/p2p/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SznbYGzPwp8qDrq", + "/ip6/2604:a880:1:20::203:d001/tcp/4001/p2p/QmSoLPppuBtQSGwKDZT2M73ULpjvfd3aZ6ha4oFGL1KrGM", +] + + +async def run(port: int, bootstrap_addrs: list[str]) -> None: + """Run the bootstrap discovery example.""" + # Generate key pair + secret = secrets.token_bytes(32) + key_pair = create_new_key_pair(secret) + + # Create listen address + listen_addr = multiaddr.Multiaddr(f"/ip4/0.0.0.0/tcp/{port}") + + # Register peer discovery handler + peerDiscovery.register_peer_discovered_handler(on_peer_discovery) + + logger.info("๐Ÿš€ Starting Bootstrap Discovery Example") + logger.info(f"๐Ÿ“ Listening on: {listen_addr}") + logger.info(f"๐ŸŒ Bootstrap peers: {len(bootstrap_addrs)}") + + print("\n" + "=" * 60) + print("Bootstrap Discovery Example") + print("=" * 60) + print("This example demonstrates connecting to bootstrap peers.") + print("Watch the logs for peer discovery events!") + print("Press Ctrl+C to exit.") + print("=" * 60) + + # Create and run host with bootstrap discovery + host = new_host(key_pair=key_pair, bootstrap=bootstrap_addrs) + + try: + async with host.run(listen_addrs=[listen_addr]): + # Keep running and log peer discovery events + await trio.sleep_forever() + except KeyboardInterrupt: + logger.info("๐Ÿ‘‹ Shutting down...") + + +def main() -> None: + """Main entry point.""" + description = """ + Bootstrap Discovery Example for py-libp2p + + This example demonstrates how to use bootstrap peers for peer discovery. + Bootstrap peers are predefined peers that help new nodes join the network. + + Usage: + python bootstrap.py -p 8000 + python bootstrap.py -p 8001 --custom-bootstrap \\ + "/ip4/127.0.0.1/tcp/8000/p2p/QmYourPeerID" + """ + + parser = argparse.ArgumentParser( + description=description, formatter_class=argparse.RawDescriptionHelpFormatter + ) + parser.add_argument( + "-p", "--port", default=0, type=int, help="Port to listen on (default: random)" + ) + parser.add_argument( + "--custom-bootstrap", + nargs="*", + help="Custom bootstrap addresses (space-separated)", + ) + parser.add_argument( + "-v", "--verbose", action="store_true", help="Enable verbose output" + ) + + args = parser.parse_args() + + if args.verbose: + logger.setLevel(logging.DEBUG) + + # Use custom bootstrap addresses if provided, otherwise use defaults + bootstrap_addrs = ( + args.custom_bootstrap if args.custom_bootstrap else BOOTSTRAP_PEERS + ) + + try: + trio.run(run, args.port, bootstrap_addrs) + except KeyboardInterrupt: + logger.info("Exiting...") + + +if __name__ == "__main__": + main() diff --git a/libp2p/__init__.py b/libp2p/__init__.py index fa7ebefd..744099b7 100644 --- a/libp2p/__init__.py +++ b/libp2p/__init__.py @@ -249,6 +249,7 @@ def new_host( muxer_preference: Literal["YAMUX", "MPLEX"] | None = None, listen_addrs: Sequence[multiaddr.Multiaddr] | None = None, enable_mDNS: bool = False, + bootstrap: list[str] | None = None, ) -> IHost: """ Create a new libp2p host based on the given parameters. @@ -261,6 +262,7 @@ def new_host( :param muxer_preference: optional explicit muxer preference :param listen_addrs: optional list of multiaddrs to listen on :param enable_mDNS: whether to enable mDNS discovery + :param bootstrap: optional list of bootstrap peer addresses as strings :return: return a host instance """ swarm = new_swarm( @@ -273,7 +275,7 @@ def new_host( ) if disc_opt is not None: - return RoutedHost(swarm, disc_opt, enable_mDNS) - return BasicHost(swarm, enable_mDNS) + return RoutedHost(swarm, disc_opt, enable_mDNS, bootstrap) + return BasicHost(swarm, enable_mDNS, bootstrap) __version__ = __version("libp2p") diff --git a/libp2p/discovery/bootstrap/__init__.py b/libp2p/discovery/bootstrap/__init__.py new file mode 100644 index 00000000..bad6ff74 --- /dev/null +++ b/libp2p/discovery/bootstrap/__init__.py @@ -0,0 +1,5 @@ +"""Bootstrap peer discovery module for py-libp2p.""" + +from .bootstrap import BootstrapDiscovery + +__all__ = ["BootstrapDiscovery"] diff --git a/libp2p/discovery/bootstrap/bootstrap.py b/libp2p/discovery/bootstrap/bootstrap.py new file mode 100644 index 00000000..49e070e5 --- /dev/null +++ b/libp2p/discovery/bootstrap/bootstrap.py @@ -0,0 +1,69 @@ +import logging + +from multiaddr import Multiaddr + +from libp2p.abc import INetworkService +from libp2p.discovery.events.peerDiscovery import peerDiscovery +from libp2p.peer.peerinfo import info_from_p2p_addr + +logger = logging.getLogger("libp2p.discovery.bootstrap") + + +class BootstrapDiscovery: + """ + Bootstrap-based peer discovery for py-libp2p. + Connects to predefined bootstrap peers and adds them to peerstore. + """ + + def __init__(self, swarm: INetworkService, bootstrap_addrs: list[str]): + self.swarm = swarm + self.peerstore = swarm.peerstore + self.bootstrap_addrs = bootstrap_addrs or [] + self.discovered_peers: set[str] = set() + + def start(self) -> None: + """Process bootstrap addresses and emit peer discovery events.""" + logger.debug( + f"Starting bootstrap discovery with " + f"{len(self.bootstrap_addrs)} bootstrap addresses" + ) + + for addr_str in self.bootstrap_addrs: + try: + self._process_bootstrap_addr(addr_str) + except Exception as e: + logger.warning(f"Failed to process bootstrap address {addr_str}: {e}") + + def stop(self) -> None: + """Clean up bootstrap discovery resources.""" + logger.debug("Stopping bootstrap discovery") + self.discovered_peers.clear() + + def _process_bootstrap_addr(self, addr_str: str) -> None: + """Convert string address to PeerInfo and add to peerstore.""" + # Convert string to Multiaddr + multiaddr = Multiaddr(addr_str) + + # Extract peer info from multiaddr + peer_info = info_from_p2p_addr(multiaddr) + + # Skip if it's our own peer + if peer_info.peer_id == self.swarm.get_peer_id(): + logger.debug(f"Skipping own peer ID: {peer_info.peer_id}") + return + + # Skip if already discovered + if str(peer_info.peer_id) in self.discovered_peers: + logger.debug(f"Peer already discovered: {peer_info.peer_id}") + return + + # Add to peerstore with TTL (using same pattern as mDNS) + self.peerstore.add_addrs(peer_info.peer_id, peer_info.addrs, 10) + + # Track discovered peer + self.discovered_peers.add(str(peer_info.peer_id)) + + # Emit peer discovery event + peerDiscovery.emit_peer_discovered(peer_info) + + logger.info(f"Discovered bootstrap peer: {peer_info.peer_id}") diff --git a/libp2p/discovery/bootstrap/utils.py b/libp2p/discovery/bootstrap/utils.py new file mode 100644 index 00000000..c88dfd87 --- /dev/null +++ b/libp2p/discovery/bootstrap/utils.py @@ -0,0 +1,51 @@ +"""Utility functions for bootstrap discovery.""" + +import logging + +from multiaddr import Multiaddr + +from libp2p.peer.peerinfo import InvalidAddrError, PeerInfo, info_from_p2p_addr + +logger = logging.getLogger("libp2p.discovery.bootstrap.utils") + + +def validate_bootstrap_addresses(addrs: list[str]) -> list[str]: + """ + Validate and filter bootstrap addresses. + + :param addrs: List of bootstrap address strings + :return: List of valid bootstrap addresses + """ + valid_addrs = [] + + for addr_str in addrs: + try: + # Try to parse as multiaddr + multiaddr = Multiaddr(addr_str) + + # Try to extract peer info (this validates the p2p component) + info_from_p2p_addr(multiaddr) + + valid_addrs.append(addr_str) + logger.debug(f"Valid bootstrap address: {addr_str}") + + except (InvalidAddrError, ValueError, Exception) as e: + logger.warning(f"Invalid bootstrap address '{addr_str}': {e}") + continue + + return valid_addrs + + +def parse_bootstrap_peer_info(addr_str: str) -> PeerInfo | None: + """ + Parse bootstrap address string into PeerInfo. + + :param addr_str: Bootstrap address string + :return: PeerInfo object or None if parsing fails + """ + try: + multiaddr = Multiaddr(addr_str) + return info_from_p2p_addr(multiaddr) + except Exception as e: + logger.error(f"Failed to parse bootstrap address '{addr_str}': {e}") + return None diff --git a/libp2p/host/basic_host.py b/libp2p/host/basic_host.py index 798186cf..ff17effd 100644 --- a/libp2p/host/basic_host.py +++ b/libp2p/host/basic_host.py @@ -29,6 +29,7 @@ from libp2p.custom_types import ( StreamHandlerFn, TProtocol, ) +from libp2p.discovery.bootstrap.bootstrap import BootstrapDiscovery from libp2p.discovery.mdns.mdns import MDNSDiscovery from libp2p.host.defaults import ( get_default_protocols, @@ -91,6 +92,7 @@ class BasicHost(IHost): self, network: INetworkService, enable_mDNS: bool = False, + bootstrap: list[str] | None = None, default_protocols: Optional["OrderedDict[TProtocol, StreamHandlerFn]"] = None, ) -> None: self._network = network @@ -102,6 +104,8 @@ class BasicHost(IHost): self.multiselect_client = MultiselectClient() if enable_mDNS: self.mDNS = MDNSDiscovery(network) + if bootstrap: + self.bootstrap = BootstrapDiscovery(network, bootstrap) def get_id(self) -> ID: """ @@ -169,11 +173,16 @@ class BasicHost(IHost): if hasattr(self, "mDNS") and self.mDNS is not None: logger.debug("Starting mDNS Discovery") self.mDNS.start() + if hasattr(self, "bootstrap") and self.bootstrap is not None: + logger.debug("Starting Bootstrap Discovery") + self.bootstrap.start() try: yield finally: if hasattr(self, "mDNS") and self.mDNS is not None: self.mDNS.stop() + if hasattr(self, "bootstrap") and self.bootstrap is not None: + self.bootstrap.stop() return _run() diff --git a/libp2p/host/routed_host.py b/libp2p/host/routed_host.py index 166a15ec..e103c9e5 100644 --- a/libp2p/host/routed_host.py +++ b/libp2p/host/routed_host.py @@ -19,9 +19,13 @@ class RoutedHost(BasicHost): _router: IPeerRouting def __init__( - self, network: INetworkService, router: IPeerRouting, enable_mDNS: bool = False + self, + network: INetworkService, + router: IPeerRouting, + enable_mDNS: bool = False, + bootstrap: list[str] | None = None, ): - super().__init__(network, enable_mDNS) + super().__init__(network, enable_mDNS, bootstrap) self._router = router async def connect(self, peer_info: PeerInfo) -> None: diff --git a/tests/discovery/__init__.py b/tests/discovery/__init__.py index e69de29b..297d7bd2 100644 --- a/tests/discovery/__init__.py +++ b/tests/discovery/__init__.py @@ -0,0 +1 @@ +"""Discovery tests for py-libp2p.""" diff --git a/tests/discovery/bootstrap/__init__.py b/tests/discovery/bootstrap/__init__.py new file mode 100644 index 00000000..4bb10e8a --- /dev/null +++ b/tests/discovery/bootstrap/__init__.py @@ -0,0 +1 @@ +"""Bootstrap discovery tests for py-libp2p.""" diff --git a/tests/discovery/bootstrap/test_bootstrap_script.py b/tests/discovery/bootstrap/test_bootstrap_script.py new file mode 100644 index 00000000..f22fa970 --- /dev/null +++ b/tests/discovery/bootstrap/test_bootstrap_script.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +""" +Simple test script to verify bootstrap functionality +""" + +import os +import sys + +# Add the parent directory to sys.path so we can import libp2p +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../../")) + +from libp2p.discovery.bootstrap.utils import ( + parse_bootstrap_peer_info, + validate_bootstrap_addresses, +) + + +def test_bootstrap_validation(): + """Test bootstrap address validation""" + print("๐Ÿงช Testing Bootstrap Address Validation") + + # Test addresses + test_addresses = [ + "/ip4/104.131.131.82/tcp/4001/p2p/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SznbYGzPwp8qDrq", + "/ip4/127.0.0.1/tcp/8000/p2p/QmTest123", # This might be invalid peer ID format + "invalid-address", + "/ip4/192.168.1.1/tcp/4001", # Missing p2p component + "/ip6/2604:a880:1:20::203:d001/tcp/4001/p2p/QmSoLPppuBtQSGwKDZT2M73ULpjvfd3aZ6ha4oFGL1KrGM", + ] + + print(f"๐Ÿ“‹ Testing {len(test_addresses)} addresses:") + for addr in test_addresses: + print(f" โ€ข {addr}") + + # Validate addresses + valid_addresses = validate_bootstrap_addresses(test_addresses) + + print(f"\nโœ… Valid addresses ({len(valid_addresses)}):") + for addr in valid_addresses: + print(f" โ€ข {addr}") + + # Try to parse peer info + peer_info = parse_bootstrap_peer_info(addr) + if peer_info: + print(f" โ†’ Peer ID: {peer_info.peer_id}") + print(f" โ†’ Addresses: {[str(a) for a in peer_info.addrs]}") + else: + print(" โ†’ Failed to parse peer info") + + return len(valid_addresses) > 0 + + +if __name__ == "__main__": + print("=" * 60) + print("Bootstrap Module Test") + print("=" * 60) + + try: + success = test_bootstrap_validation() + if success: + print("\n๐ŸŽ‰ Bootstrap module test completed successfully!") + else: + print("\nโŒ No valid bootstrap addresses found") + sys.exit(1) + except Exception as e: + print(f"\n๐Ÿ’ฅ Test failed with error: {e}") + import traceback + + traceback.print_exc() + sys.exit(1) diff --git a/tests/discovery/bootstrap/test_integration.py b/tests/discovery/bootstrap/test_integration.py new file mode 100644 index 00000000..dc77aab2 --- /dev/null +++ b/tests/discovery/bootstrap/test_integration.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +""" +Test the full bootstrap discovery integration +""" + +import logging +import secrets + +import pytest + +from libp2p import new_host +from libp2p.abc import PeerInfo +from libp2p.crypto.secp256k1 import create_new_key_pair +from libp2p.discovery.events.peerDiscovery import peerDiscovery +from libp2p.host.basic_host import BasicHost + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger("bootstrap_test") + + +def on_peer_discovery(peer_info: PeerInfo) -> None: + """Handler for peer discovery events.""" + logger.info(f"๐Ÿ” Discovered peer: {peer_info.peer_id}") + logger.info(f" Addresses: {[str(addr) for addr in peer_info.addrs]}") + + +@pytest.mark.trio +async def test_bootstrap_integration(): + """Test bootstrap integration with new_host""" + print("๐Ÿงช Testing Bootstrap Integration") + + # Test bootstrap addresses + bootstrap_addrs = [ + "/ip4/104.131.131.82/tcp/4001/p2p/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SznbYGzPwp8qDrq", + "/ip4/104.236.179.241/tcp/4001/p2p/QmSoLPppuBtQSGwKDZT2M73ULpjvfd3aZ6ha4oFGL1KrGM", + ] + + # Generate key pair + secret = secrets.token_bytes(32) + key_pair = create_new_key_pair(secret) + + # Register peer discovery handler + peerDiscovery.register_peer_discovered_handler(on_peer_discovery) + + print(f"๐ŸŒ Testing with {len(bootstrap_addrs)} bootstrap peers") + + # Create host with bootstrap + host = new_host(key_pair=key_pair, bootstrap=bootstrap_addrs) + + print("โœ… Successfully created host with bootstrap") + print(f"๐Ÿ“ Host peer ID: {host.get_id()}") + print("๐Ÿ”— Bootstrap discovery should process peers when host starts") + + # Verify bootstrap discovery is set up (cast to BasicHost for type checking) + assert isinstance(host, BasicHost), "Host should be a BasicHost instance" + assert hasattr(host, "bootstrap"), "Host should have bootstrap attribute" + assert host.bootstrap is not None, "Bootstrap discovery should be initialized" + assert len(host.bootstrap.bootstrap_addrs) == len(bootstrap_addrs), ( + "Bootstrap addresses should match" + ) + + print("๐ŸŽ‰ Bootstrap integration test completed successfully!") + + +def test_bootstrap_no_addresses(): + """Test that bootstrap is not initialized when no addresses provided""" + secret = secrets.token_bytes(32) + key_pair = create_new_key_pair(secret) + + # Create host without bootstrap + host = new_host(key_pair=key_pair) + + # Verify bootstrap is not initialized + assert isinstance(host, BasicHost) + assert not hasattr(host, "bootstrap") or host.bootstrap is None, ( + "Bootstrap should not be initialized when no addresses provided" + )