Skip to content

feat: Implemented Bootstrap module in py-libp2p #711

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 24 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
12ad2dc
Added bootstrap module
sumanjeet0012 Jun 29, 2025
befb2d3
added newsfragments
sumanjeet0012 Jun 29, 2025
36be4c3
fix: ensure newline at end of file in Bootstrap peer discovery module…
sumanjeet0012 Jun 29, 2025
ddbd190
docs: added bootstrap docs in doctree
sumanjeet0012 Jun 29, 2025
b5ec1bd
Merge branch 'main' into feature/bootstrap
seetadev Jun 30, 2025
ec20ca8
remove unnecessary files from .gitignore
sumanjeet0012 Jun 30, 2025
69a2cb0
remove obsolete test script and add comprehensive validation tests fo…
sumanjeet0012 Jun 30, 2025
cbb1e26
refactor fixed some lint issues
sumanjeet0012 Jun 30, 2025
16be6fa
Merge branch 'main' into feature/bootstrap
sumanjeet0012 Jul 2, 2025
dcb199a
Merge branch 'main' into feature/bootstrap
seetadev Jul 3, 2025
80c686d
Merge branch 'main' into feature/bootstrap
seetadev Jul 7, 2025
2965b4e
DNS resolution working
sumanjeet0012 Jul 8, 2025
a26fd95
Merge branch 'feature/bootstrap' of https://github.com/sumanjeet0012/…
sumanjeet0012 Jul 8, 2025
198208a
validate and filter bootstrap addresses during discovery initialization
sumanjeet0012 Jul 9, 2025
2dfee68
Refactor bootstrap discovery to use async methods and update bootstra…
sumanjeet0012 Jul 10, 2025
9669a92
Fix formatting and linting issues
sumanjeet0012 Jul 10, 2025
21db1c3
Merge branch 'main' into feature/bootstrap
sumanjeet0012 Jul 10, 2025
d03bdd7
Merge branch 'main' into feature/bootstrap
seetadev Jul 12, 2025
9e76940
Refactor logging configuration to reduce verbosity and improve peer …
sumanjeet0012 Jul 13, 2025
2c1e504
Merge branch 'feature/bootstrap' of https://github.com/sumanjeet0012/…
sumanjeet0012 Jul 13, 2025
9f38d48
Fix valid bootstrap address in test case
sumanjeet0012 Jul 13, 2025
ab94e77
Merge branch 'main' into feature/bootstrap
seetadev Jul 15, 2025
41b1ecb
Merge branch 'main' into feature/bootstrap
seetadev Jul 16, 2025
c277cce
Merge branch 'main' into feature/bootstrap
seetadev Jul 20, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions docs/libp2p.discovery.bootstrap.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
libp2p.discovery.bootstrap package
==================================

Submodules
----------

Module contents
---------------

.. automodule:: libp2p.discovery.bootstrap
:members:
:undoc-members:
:show-inheritance:
1 change: 1 addition & 0 deletions docs/libp2p.discovery.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Subpackages
.. toctree::
:maxdepth: 4

libp2p.discovery.bootstrap
libp2p.discovery.events
libp2p.discovery.mdns

Expand Down
136 changes: 136 additions & 0 deletions examples/bootstrap/bootstrap.py
Original file line number Diff line number Diff line change
@@ -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()
6 changes: 4 additions & 2 deletions libp2p/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,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,
negotiate_timeout: int = DEFAULT_NEGOTIATE_TIMEOUT,
) -> IHost:
"""
Expand All @@ -264,6 +265,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(
Expand All @@ -276,7 +278,7 @@ def new_host(
)

if disc_opt is not None:
return RoutedHost(swarm, disc_opt, enable_mDNS)
return BasicHost(network=swarm,enable_mDNS=enable_mDNS , negotitate_timeout=negotiate_timeout)
return RoutedHost(swarm, disc_opt, enable_mDNS, bootstrap)
return BasicHost(network=swarm,enable_mDNS=enable_mDNS , bootstrap=bootstrap, negotitate_timeout=negotiate_timeout)

__version__ = __version("libp2p")
5 changes: 5 additions & 0 deletions libp2p/discovery/bootstrap/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""Bootstrap peer discovery module for py-libp2p."""

from .bootstrap import BootstrapDiscovery

__all__ = ["BootstrapDiscovery"]
91 changes: 91 additions & 0 deletions libp2p/discovery/bootstrap/bootstrap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
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]
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

# 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)
51 changes: 51 additions & 0 deletions libp2p/discovery/bootstrap/utils.py
Original file line number Diff line number Diff line change
@@ -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
9 changes: 9 additions & 0 deletions libp2p/host/basic_host.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
StreamHandlerFn,
TProtocol,
)
from libp2p.discovery.bootstrap.bootstrap import BootstrapDiscovery
from libp2p.discovery.mdns.mdns import MDNSDiscovery
from libp2p.host.defaults import (
get_default_protocols,
Expand Down Expand Up @@ -92,6 +93,7 @@ def __init__(
self,
network: INetworkService,
enable_mDNS: bool = False,
bootstrap: list[str] | None = None,
default_protocols: Optional["OrderedDict[TProtocol, StreamHandlerFn]"] = None,
negotitate_timeout: int = DEFAULT_NEGOTIATE_TIMEOUT,
) -> None:
Expand All @@ -105,6 +107,8 @@ def __init__(
self.multiselect_client = MultiselectClient()
if enable_mDNS:
self.mDNS = MDNSDiscovery(network)
if bootstrap:
self.bootstrap = BootstrapDiscovery(network, bootstrap)

def get_id(self) -> ID:
"""
Expand Down Expand Up @@ -172,11 +176,16 @@ async def _run() -> AsyncIterator[None]:
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")
await 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()

Expand Down
8 changes: 6 additions & 2 deletions libp2p/host/routed_host.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions newsfragments/711.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added `Bootstrap` peer discovery module that allows nodes to connect to predefined bootstrap peers for network discovery.
Loading