Added bootstrap module

This commit is contained in:
sumanjeet0012@gmail.com
2025-06-29 16:10:27 +05:30
parent 92c9ba7e46
commit 12ad2dcdf4
12 changed files with 419 additions and 4 deletions

4
.gitignore vendored
View File

@ -178,3 +178,7 @@ env.bak/
#lockfiles
uv.lock
poetry.lock
bootstrap_instructions.txt
.gitignore
README.md

View File

@ -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()

View File

@ -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")

View File

@ -0,0 +1,5 @@
"""Bootstrap peer discovery module for py-libp2p."""
from .bootstrap import BootstrapDiscovery
__all__ = ["BootstrapDiscovery"]

View File

@ -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}")

View 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

View File

@ -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()

View File

@ -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:

View File

@ -0,0 +1 @@
"""Discovery tests for py-libp2p."""

View File

@ -0,0 +1 @@
"""Bootstrap discovery tests for py-libp2p."""

View File

@ -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)

View File

@ -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"
)