mirror of
https://github.com/varun-r-mallya/py-libp2p.git
synced 2026-02-11 07:30:55 +00:00
Merge branch 'main' into todo/handletimeout
This commit is contained in:
13
docs/libp2p.discovery.bootstrap.rst
Normal file
13
docs/libp2p.discovery.bootstrap.rst
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
libp2p.discovery.bootstrap package
|
||||||
|
==================================
|
||||||
|
|
||||||
|
Submodules
|
||||||
|
----------
|
||||||
|
|
||||||
|
Module contents
|
||||||
|
---------------
|
||||||
|
|
||||||
|
.. automodule:: libp2p.discovery.bootstrap
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
@ -7,6 +7,7 @@ Subpackages
|
|||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 4
|
:maxdepth: 4
|
||||||
|
|
||||||
|
libp2p.discovery.bootstrap
|
||||||
libp2p.discovery.events
|
libp2p.discovery.events
|
||||||
libp2p.discovery.mdns
|
libp2p.discovery.mdns
|
||||||
|
|
||||||
|
|||||||
136
examples/bootstrap/bootstrap.py
Normal file
136
examples/bootstrap/bootstrap.py
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
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)
|
||||||
|
|
||||||
|
# Configure root logger to only show warnings and above to reduce noise
|
||||||
|
# This prevents verbose DEBUG messages from multiaddr, DNS, etc.
|
||||||
|
logging.getLogger().setLevel(logging.WARNING)
|
||||||
|
|
||||||
|
# Specifically silence noisy libraries
|
||||||
|
logging.getLogger("multiaddr").setLevel(logging.WARNING)
|
||||||
|
logging.getLogger("root").setLevel(logging.WARNING)
|
||||||
|
|
||||||
|
|
||||||
|
def on_peer_discovery(peer_info: PeerInfo) -> None:
|
||||||
|
"""Handler for peer discovery events."""
|
||||||
|
logger.info(f"🔍 Discovered peer: {peer_info.peer_id}")
|
||||||
|
logger.debug(f" Addresses: {[str(addr) for addr in peer_info.addrs]}")
|
||||||
|
|
||||||
|
|
||||||
|
# Example bootstrap peers
|
||||||
|
BOOTSTRAP_PEERS = [
|
||||||
|
"/dnsaddr/github.com/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN",
|
||||||
|
"/dnsaddr/cloudflare.com/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN",
|
||||||
|
"/dnsaddr/google.com/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN",
|
||||||
|
"/dnsaddr/bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN",
|
||||||
|
"/dnsaddr/bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb",
|
||||||
|
"/ip4/104.131.131.82/tcp/4001/p2p/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ",
|
||||||
|
"/ip6/2604:a880:1:20::203:d001/tcp/4001/p2p/QmSoLPppuBtQSGwKDZT2M73ULpjvfd3aZ6ha4oFGL1KrGM",
|
||||||
|
"/ip4/128.199.219.111/tcp/4001/p2p/QmSoLV4Bbm51jM9C4gDYZQ9Cy3U6aXMJDAbzgu2fzaDs64",
|
||||||
|
"/ip4/104.236.76.40/tcp/4001/p2p/QmSoLV4Bbm51jM9C4gDYZQ9Cy3U6aXMJDAbzgu2fzaDs64",
|
||||||
|
"/ip4/178.62.158.247/tcp/4001/p2p/QmSoLer265NRgSp2LA3dPaeykiS1J6DifTC88f5uVQKNAd",
|
||||||
|
"/ip6/2604:a880:1:20::203:d001/tcp/4001/p2p/QmSoLPppuBtQSGwKDZT2M73ULpjvfd3aZ6ha4oFGL1KrGM",
|
||||||
|
"/ip6/2400:6180:0:d0::151:6001/tcp/4001/p2p/QmSoLSafTMBsPKadTEgaXctDQVcqN88CNLHXMkTNwMKPnu",
|
||||||
|
"/ip6/2a03:b0c0:0:1010::23:1001/tcp/4001/p2p/QmSoLueR4xBeUbY9WZ9xGUUxunbKWcrNFTDAadQJmocnWm",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
@ -251,6 +251,7 @@ def new_host(
|
|||||||
muxer_preference: Literal["YAMUX", "MPLEX"] | None = None,
|
muxer_preference: Literal["YAMUX", "MPLEX"] | None = None,
|
||||||
listen_addrs: Sequence[multiaddr.Multiaddr] | None = None,
|
listen_addrs: Sequence[multiaddr.Multiaddr] | None = None,
|
||||||
enable_mDNS: bool = False,
|
enable_mDNS: bool = False,
|
||||||
|
bootstrap: list[str] | None = None,
|
||||||
negotiate_timeout: int = DEFAULT_NEGOTIATE_TIMEOUT,
|
negotiate_timeout: int = DEFAULT_NEGOTIATE_TIMEOUT,
|
||||||
) -> IHost:
|
) -> IHost:
|
||||||
"""
|
"""
|
||||||
@ -264,6 +265,7 @@ def new_host(
|
|||||||
:param muxer_preference: optional explicit muxer preference
|
:param muxer_preference: optional explicit muxer preference
|
||||||
:param listen_addrs: optional list of multiaddrs to listen on
|
:param listen_addrs: optional list of multiaddrs to listen on
|
||||||
:param enable_mDNS: whether to enable mDNS discovery
|
:param enable_mDNS: whether to enable mDNS discovery
|
||||||
|
:param bootstrap: optional list of bootstrap peer addresses as strings
|
||||||
:return: return a host instance
|
:return: return a host instance
|
||||||
"""
|
"""
|
||||||
swarm = new_swarm(
|
swarm = new_swarm(
|
||||||
@ -276,7 +278,7 @@ def new_host(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if disc_opt is not None:
|
if disc_opt is not None:
|
||||||
return RoutedHost(swarm, disc_opt, enable_mDNS)
|
return RoutedHost(swarm, disc_opt, enable_mDNS, bootstrap)
|
||||||
return BasicHost(network=swarm,enable_mDNS=enable_mDNS , negotitate_timeout=negotiate_timeout)
|
return BasicHost(network=swarm,enable_mDNS=enable_mDNS , bootstrap=bootstrap, negotitate_timeout=negotiate_timeout)
|
||||||
|
|
||||||
__version__ = __version("libp2p")
|
__version__ = __version("libp2p")
|
||||||
|
|||||||
5
libp2p/discovery/bootstrap/__init__.py
Normal file
5
libp2p/discovery/bootstrap/__init__.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
"""Bootstrap peer discovery module for py-libp2p."""
|
||||||
|
|
||||||
|
from .bootstrap import BootstrapDiscovery
|
||||||
|
|
||||||
|
__all__ = ["BootstrapDiscovery"]
|
||||||
94
libp2p/discovery/bootstrap/bootstrap.py
Normal file
94
libp2p/discovery/bootstrap/bootstrap.py
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from multiaddr import Multiaddr
|
||||||
|
from multiaddr.resolvers import DNSResolver
|
||||||
|
|
||||||
|
from libp2p.abc import ID, INetworkService, PeerInfo
|
||||||
|
from libp2p.discovery.bootstrap.utils import validate_bootstrap_addresses
|
||||||
|
from libp2p.discovery.events.peerDiscovery import peerDiscovery
|
||||||
|
from libp2p.peer.peerinfo import info_from_p2p_addr
|
||||||
|
|
||||||
|
logger = logging.getLogger("libp2p.discovery.bootstrap")
|
||||||
|
resolver = DNSResolver()
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
async 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"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Validate and filter bootstrap addresses
|
||||||
|
self.bootstrap_addrs = validate_bootstrap_addresses(self.bootstrap_addrs)
|
||||||
|
|
||||||
|
for addr_str in self.bootstrap_addrs:
|
||||||
|
try:
|
||||||
|
await self._process_bootstrap_addr(addr_str)
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(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()
|
||||||
|
|
||||||
|
async def _process_bootstrap_addr(self, addr_str: str) -> None:
|
||||||
|
"""Convert string address to PeerInfo and add to peerstore."""
|
||||||
|
try:
|
||||||
|
multiaddr = Multiaddr(addr_str)
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Invalid multiaddr format '{addr_str}': {e}")
|
||||||
|
return
|
||||||
|
if self.is_dns_addr(multiaddr):
|
||||||
|
resolved_addrs = await resolver.resolve(multiaddr)
|
||||||
|
peer_id_str = multiaddr.get_peer_id()
|
||||||
|
if peer_id_str is None:
|
||||||
|
logger.warning(f"Missing peer ID in DNS address: {addr_str}")
|
||||||
|
return
|
||||||
|
peer_id = ID.from_base58(peer_id_str)
|
||||||
|
addrs = [addr for addr in resolved_addrs]
|
||||||
|
if not addrs:
|
||||||
|
logger.warning(f"No addresses resolved for DNS address: {addr_str}")
|
||||||
|
return
|
||||||
|
peer_info = PeerInfo(peer_id, addrs)
|
||||||
|
self.add_addr(peer_info)
|
||||||
|
else:
|
||||||
|
self.add_addr(info_from_p2p_addr(multiaddr))
|
||||||
|
|
||||||
|
def is_dns_addr(self, addr: Multiaddr) -> bool:
|
||||||
|
"""Check if the address is a DNS address."""
|
||||||
|
return any(protocol.name == "dnsaddr" for protocol in addr.protocols())
|
||||||
|
|
||||||
|
def add_addr(self, peer_info: PeerInfo) -> None:
|
||||||
|
"""Add a peer to the peerstore and emit discovery event."""
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# Always add addresses to peerstore (allows multiple addresses for same peer)
|
||||||
|
self.peerstore.add_addrs(peer_info.peer_id, peer_info.addrs, 10)
|
||||||
|
|
||||||
|
# Only emit discovery event if this is the first time we see this peer
|
||||||
|
peer_id_str = str(peer_info.peer_id)
|
||||||
|
if peer_id_str not in self.discovered_peers:
|
||||||
|
# Track discovered peer
|
||||||
|
self.discovered_peers.add(peer_id_str)
|
||||||
|
# Emit peer discovery event
|
||||||
|
peerDiscovery.emit_peer_discovered(peer_info)
|
||||||
|
logger.debug(f"Peer discovered: {peer_info.peer_id}")
|
||||||
|
else:
|
||||||
|
logger.debug(f"Additional addresses added for peer: {peer_info.peer_id}")
|
||||||
51
libp2p/discovery/bootstrap/utils.py
Normal file
51
libp2p/discovery/bootstrap/utils.py
Normal file
@ -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
|
||||||
@ -29,6 +29,7 @@ from libp2p.custom_types import (
|
|||||||
StreamHandlerFn,
|
StreamHandlerFn,
|
||||||
TProtocol,
|
TProtocol,
|
||||||
)
|
)
|
||||||
|
from libp2p.discovery.bootstrap.bootstrap import BootstrapDiscovery
|
||||||
from libp2p.discovery.mdns.mdns import MDNSDiscovery
|
from libp2p.discovery.mdns.mdns import MDNSDiscovery
|
||||||
from libp2p.host.defaults import (
|
from libp2p.host.defaults import (
|
||||||
get_default_protocols,
|
get_default_protocols,
|
||||||
@ -92,6 +93,7 @@ class BasicHost(IHost):
|
|||||||
self,
|
self,
|
||||||
network: INetworkService,
|
network: INetworkService,
|
||||||
enable_mDNS: bool = False,
|
enable_mDNS: bool = False,
|
||||||
|
bootstrap: list[str] | None = None,
|
||||||
default_protocols: Optional["OrderedDict[TProtocol, StreamHandlerFn]"] = None,
|
default_protocols: Optional["OrderedDict[TProtocol, StreamHandlerFn]"] = None,
|
||||||
negotitate_timeout: int = DEFAULT_NEGOTIATE_TIMEOUT,
|
negotitate_timeout: int = DEFAULT_NEGOTIATE_TIMEOUT,
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -105,6 +107,8 @@ class BasicHost(IHost):
|
|||||||
self.multiselect_client = MultiselectClient()
|
self.multiselect_client = MultiselectClient()
|
||||||
if enable_mDNS:
|
if enable_mDNS:
|
||||||
self.mDNS = MDNSDiscovery(network)
|
self.mDNS = MDNSDiscovery(network)
|
||||||
|
if bootstrap:
|
||||||
|
self.bootstrap = BootstrapDiscovery(network, bootstrap)
|
||||||
|
|
||||||
def get_id(self) -> ID:
|
def get_id(self) -> ID:
|
||||||
"""
|
"""
|
||||||
@ -172,11 +176,16 @@ class BasicHost(IHost):
|
|||||||
if hasattr(self, "mDNS") and self.mDNS is not None:
|
if hasattr(self, "mDNS") and self.mDNS is not None:
|
||||||
logger.debug("Starting mDNS Discovery")
|
logger.debug("Starting mDNS Discovery")
|
||||||
self.mDNS.start()
|
self.mDNS.start()
|
||||||
|
if hasattr(self, "bootstrap") and self.bootstrap is not None:
|
||||||
|
logger.debug("Starting Bootstrap Discovery")
|
||||||
|
await self.bootstrap.start()
|
||||||
try:
|
try:
|
||||||
yield
|
yield
|
||||||
finally:
|
finally:
|
||||||
if hasattr(self, "mDNS") and self.mDNS is not None:
|
if hasattr(self, "mDNS") and self.mDNS is not None:
|
||||||
self.mDNS.stop()
|
self.mDNS.stop()
|
||||||
|
if hasattr(self, "bootstrap") and self.bootstrap is not None:
|
||||||
|
self.bootstrap.stop()
|
||||||
|
|
||||||
return _run()
|
return _run()
|
||||||
|
|
||||||
|
|||||||
@ -19,9 +19,13 @@ class RoutedHost(BasicHost):
|
|||||||
_router: IPeerRouting
|
_router: IPeerRouting
|
||||||
|
|
||||||
def __init__(
|
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
|
self._router = router
|
||||||
|
|
||||||
async def connect(self, peer_info: PeerInfo) -> None:
|
async def connect(self, peer_info: PeerInfo) -> None:
|
||||||
|
|||||||
1
newsfragments/711.feature.rst
Normal file
1
newsfragments/711.feature.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
Added `Bootstrap` peer discovery module that allows nodes to connect to predefined bootstrap peers for network discovery.
|
||||||
@ -0,0 +1 @@
|
|||||||
|
"""Discovery tests for py-libp2p."""
|
||||||
|
|||||||
1
tests/discovery/bootstrap/__init__.py
Normal file
1
tests/discovery/bootstrap/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""Bootstrap discovery tests for py-libp2p."""
|
||||||
52
tests/discovery/bootstrap/test_integration.py
Normal file
52
tests/discovery/bootstrap/test_integration.py
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test the full bootstrap discovery integration
|
||||||
|
"""
|
||||||
|
|
||||||
|
import secrets
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from libp2p import new_host
|
||||||
|
from libp2p.crypto.secp256k1 import create_new_key_pair
|
||||||
|
from libp2p.host.basic_host import BasicHost
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.trio
|
||||||
|
async def test_bootstrap_integration():
|
||||||
|
"""Test bootstrap integration with new_host"""
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
# Create host with bootstrap
|
||||||
|
host = new_host(key_pair=key_pair, bootstrap=bootstrap_addrs)
|
||||||
|
|
||||||
|
# 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"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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"
|
||||||
|
)
|
||||||
39
tests/discovery/bootstrap/test_utils.py
Normal file
39
tests/discovery/bootstrap/test_utils.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test bootstrap address validation
|
||||||
|
"""
|
||||||
|
|
||||||
|
from libp2p.discovery.bootstrap.utils import (
|
||||||
|
parse_bootstrap_peer_info,
|
||||||
|
validate_bootstrap_addresses,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_addresses():
|
||||||
|
"""Test validation with a mix of valid and invalid addresses in one list."""
|
||||||
|
addresses = [
|
||||||
|
# Valid - using proper peer IDs
|
||||||
|
"/ip4/104.131.131.82/tcp/4001/p2p/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ",
|
||||||
|
"/ip4/104.236.179.241/tcp/4001/p2p/QmSoLPppuBtQSGwKDZT2M73ULpjvfd3aZ6ha4oFGL1KrGM",
|
||||||
|
# Invalid
|
||||||
|
"invalid-address",
|
||||||
|
"/ip4/192.168.1.1/tcp/4001", # Missing p2p part
|
||||||
|
"", # Empty
|
||||||
|
"/ip4/127.0.0.1/tcp/4001/p2p/InvalidPeerID", # Bad peer ID
|
||||||
|
]
|
||||||
|
valid_expected = [
|
||||||
|
"/ip4/104.131.131.82/tcp/4001/p2p/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ",
|
||||||
|
"/ip4/104.236.179.241/tcp/4001/p2p/QmSoLPppuBtQSGwKDZT2M73ULpjvfd3aZ6ha4oFGL1KrGM",
|
||||||
|
]
|
||||||
|
validated = validate_bootstrap_addresses(addresses)
|
||||||
|
assert validated == valid_expected, (
|
||||||
|
f"Expected only valid addresses, got: {validated}"
|
||||||
|
)
|
||||||
|
for addr in addresses:
|
||||||
|
peer_info = parse_bootstrap_peer_info(addr)
|
||||||
|
if addr in valid_expected:
|
||||||
|
assert peer_info is not None and peer_info.peer_id is not None, (
|
||||||
|
f"Should parse valid address: {addr}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
assert peer_info is None, f"Should not parse invalid address: {addr}"
|
||||||
Reference in New Issue
Block a user