Skip to content

Commit ceb9f7d

Browse files
authored
Merge branch 'main' into todo/handletimeout
2 parents eca5488 + 9b667bd commit ceb9f7d

File tree

14 files changed

+413
-4
lines changed

14 files changed

+413
-4
lines changed

docs/libp2p.discovery.bootstrap.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
libp2p.discovery.bootstrap package
2+
==================================
3+
4+
Submodules
5+
----------
6+
7+
Module contents
8+
---------------
9+
10+
.. automodule:: libp2p.discovery.bootstrap
11+
:members:
12+
:undoc-members:
13+
:show-inheritance:

docs/libp2p.discovery.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Subpackages
77
.. toctree::
88
:maxdepth: 4
99

10+
libp2p.discovery.bootstrap
1011
libp2p.discovery.events
1112
libp2p.discovery.mdns
1213

examples/bootstrap/bootstrap.py

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import argparse
2+
import logging
3+
import secrets
4+
5+
import multiaddr
6+
import trio
7+
8+
from libp2p import new_host
9+
from libp2p.abc import PeerInfo
10+
from libp2p.crypto.secp256k1 import create_new_key_pair
11+
from libp2p.discovery.events.peerDiscovery import peerDiscovery
12+
13+
# Configure logging
14+
logger = logging.getLogger("libp2p.discovery.bootstrap")
15+
logger.setLevel(logging.INFO)
16+
handler = logging.StreamHandler()
17+
handler.setFormatter(
18+
logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
19+
)
20+
logger.addHandler(handler)
21+
22+
# Configure root logger to only show warnings and above to reduce noise
23+
# This prevents verbose DEBUG messages from multiaddr, DNS, etc.
24+
logging.getLogger().setLevel(logging.WARNING)
25+
26+
# Specifically silence noisy libraries
27+
logging.getLogger("multiaddr").setLevel(logging.WARNING)
28+
logging.getLogger("root").setLevel(logging.WARNING)
29+
30+
31+
def on_peer_discovery(peer_info: PeerInfo) -> None:
32+
"""Handler for peer discovery events."""
33+
logger.info(f"🔍 Discovered peer: {peer_info.peer_id}")
34+
logger.debug(f" Addresses: {[str(addr) for addr in peer_info.addrs]}")
35+
36+
37+
# Example bootstrap peers
38+
BOOTSTRAP_PEERS = [
39+
"/dnsaddr/github.com/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN",
40+
"/dnsaddr/cloudflare.com/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN",
41+
"/dnsaddr/google.com/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN",
42+
"/dnsaddr/bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN",
43+
"/dnsaddr/bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb",
44+
"/ip4/104.131.131.82/tcp/4001/p2p/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ",
45+
"/ip6/2604:a880:1:20::203:d001/tcp/4001/p2p/QmSoLPppuBtQSGwKDZT2M73ULpjvfd3aZ6ha4oFGL1KrGM",
46+
"/ip4/128.199.219.111/tcp/4001/p2p/QmSoLV4Bbm51jM9C4gDYZQ9Cy3U6aXMJDAbzgu2fzaDs64",
47+
"/ip4/104.236.76.40/tcp/4001/p2p/QmSoLV4Bbm51jM9C4gDYZQ9Cy3U6aXMJDAbzgu2fzaDs64",
48+
"/ip4/178.62.158.247/tcp/4001/p2p/QmSoLer265NRgSp2LA3dPaeykiS1J6DifTC88f5uVQKNAd",
49+
"/ip6/2604:a880:1:20::203:d001/tcp/4001/p2p/QmSoLPppuBtQSGwKDZT2M73ULpjvfd3aZ6ha4oFGL1KrGM",
50+
"/ip6/2400:6180:0:d0::151:6001/tcp/4001/p2p/QmSoLSafTMBsPKadTEgaXctDQVcqN88CNLHXMkTNwMKPnu",
51+
"/ip6/2a03:b0c0:0:1010::23:1001/tcp/4001/p2p/QmSoLueR4xBeUbY9WZ9xGUUxunbKWcrNFTDAadQJmocnWm",
52+
]
53+
54+
55+
async def run(port: int, bootstrap_addrs: list[str]) -> None:
56+
"""Run the bootstrap discovery example."""
57+
# Generate key pair
58+
secret = secrets.token_bytes(32)
59+
key_pair = create_new_key_pair(secret)
60+
61+
# Create listen address
62+
listen_addr = multiaddr.Multiaddr(f"/ip4/0.0.0.0/tcp/{port}")
63+
64+
# Register peer discovery handler
65+
peerDiscovery.register_peer_discovered_handler(on_peer_discovery)
66+
67+
logger.info("🚀 Starting Bootstrap Discovery Example")
68+
logger.info(f"📍 Listening on: {listen_addr}")
69+
logger.info(f"🌐 Bootstrap peers: {len(bootstrap_addrs)}")
70+
71+
print("\n" + "=" * 60)
72+
print("Bootstrap Discovery Example")
73+
print("=" * 60)
74+
print("This example demonstrates connecting to bootstrap peers.")
75+
print("Watch the logs for peer discovery events!")
76+
print("Press Ctrl+C to exit.")
77+
print("=" * 60)
78+
79+
# Create and run host with bootstrap discovery
80+
host = new_host(key_pair=key_pair, bootstrap=bootstrap_addrs)
81+
82+
try:
83+
async with host.run(listen_addrs=[listen_addr]):
84+
# Keep running and log peer discovery events
85+
await trio.sleep_forever()
86+
except KeyboardInterrupt:
87+
logger.info("👋 Shutting down...")
88+
89+
90+
def main() -> None:
91+
"""Main entry point."""
92+
description = """
93+
Bootstrap Discovery Example for py-libp2p
94+
95+
This example demonstrates how to use bootstrap peers for peer discovery.
96+
Bootstrap peers are predefined peers that help new nodes join the network.
97+
98+
Usage:
99+
python bootstrap.py -p 8000
100+
python bootstrap.py -p 8001 --custom-bootstrap \\
101+
"/ip4/127.0.0.1/tcp/8000/p2p/QmYourPeerID"
102+
"""
103+
104+
parser = argparse.ArgumentParser(
105+
description=description, formatter_class=argparse.RawDescriptionHelpFormatter
106+
)
107+
parser.add_argument(
108+
"-p", "--port", default=0, type=int, help="Port to listen on (default: random)"
109+
)
110+
parser.add_argument(
111+
"--custom-bootstrap",
112+
nargs="*",
113+
help="Custom bootstrap addresses (space-separated)",
114+
)
115+
parser.add_argument(
116+
"-v", "--verbose", action="store_true", help="Enable verbose output"
117+
)
118+
119+
args = parser.parse_args()
120+
121+
if args.verbose:
122+
logger.setLevel(logging.DEBUG)
123+
124+
# Use custom bootstrap addresses if provided, otherwise use defaults
125+
bootstrap_addrs = (
126+
args.custom_bootstrap if args.custom_bootstrap else BOOTSTRAP_PEERS
127+
)
128+
129+
try:
130+
trio.run(run, args.port, bootstrap_addrs)
131+
except KeyboardInterrupt:
132+
logger.info("Exiting...")
133+
134+
135+
if __name__ == "__main__":
136+
main()

libp2p/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@ def new_host(
251251
muxer_preference: Literal["YAMUX", "MPLEX"] | None = None,
252252
listen_addrs: Sequence[multiaddr.Multiaddr] | None = None,
253253
enable_mDNS: bool = False,
254+
bootstrap: list[str] | None = None,
254255
negotiate_timeout: int = DEFAULT_NEGOTIATE_TIMEOUT,
255256
) -> IHost:
256257
"""
@@ -264,6 +265,7 @@ def new_host(
264265
:param muxer_preference: optional explicit muxer preference
265266
:param listen_addrs: optional list of multiaddrs to listen on
266267
:param enable_mDNS: whether to enable mDNS discovery
268+
:param bootstrap: optional list of bootstrap peer addresses as strings
267269
:return: return a host instance
268270
"""
269271
swarm = new_swarm(
@@ -276,7 +278,7 @@ def new_host(
276278
)
277279

278280
if disc_opt is not None:
279-
return RoutedHost(swarm, disc_opt, enable_mDNS)
280-
return BasicHost(network=swarm,enable_mDNS=enable_mDNS , negotitate_timeout=negotiate_timeout)
281+
return RoutedHost(swarm, disc_opt, enable_mDNS, bootstrap)
282+
return BasicHost(network=swarm,enable_mDNS=enable_mDNS , bootstrap=bootstrap, negotitate_timeout=negotiate_timeout)
281283

282284
__version__ = __version("libp2p")
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""Bootstrap peer discovery module for py-libp2p."""
2+
3+
from .bootstrap import BootstrapDiscovery
4+
5+
__all__ = ["BootstrapDiscovery"]
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import logging
2+
3+
from multiaddr import Multiaddr
4+
from multiaddr.resolvers import DNSResolver
5+
6+
from libp2p.abc import ID, INetworkService, PeerInfo
7+
from libp2p.discovery.bootstrap.utils import validate_bootstrap_addresses
8+
from libp2p.discovery.events.peerDiscovery import peerDiscovery
9+
from libp2p.peer.peerinfo import info_from_p2p_addr
10+
11+
logger = logging.getLogger("libp2p.discovery.bootstrap")
12+
resolver = DNSResolver()
13+
14+
15+
class BootstrapDiscovery:
16+
"""
17+
Bootstrap-based peer discovery for py-libp2p.
18+
Connects to predefined bootstrap peers and adds them to peerstore.
19+
"""
20+
21+
def __init__(self, swarm: INetworkService, bootstrap_addrs: list[str]):
22+
self.swarm = swarm
23+
self.peerstore = swarm.peerstore
24+
self.bootstrap_addrs = bootstrap_addrs or []
25+
self.discovered_peers: set[str] = set()
26+
27+
async def start(self) -> None:
28+
"""Process bootstrap addresses and emit peer discovery events."""
29+
logger.debug(
30+
f"Starting bootstrap discovery with "
31+
f"{len(self.bootstrap_addrs)} bootstrap addresses"
32+
)
33+
34+
# Validate and filter bootstrap addresses
35+
self.bootstrap_addrs = validate_bootstrap_addresses(self.bootstrap_addrs)
36+
37+
for addr_str in self.bootstrap_addrs:
38+
try:
39+
await self._process_bootstrap_addr(addr_str)
40+
except Exception as e:
41+
logger.debug(f"Failed to process bootstrap address {addr_str}: {e}")
42+
43+
def stop(self) -> None:
44+
"""Clean up bootstrap discovery resources."""
45+
logger.debug("Stopping bootstrap discovery")
46+
self.discovered_peers.clear()
47+
48+
async def _process_bootstrap_addr(self, addr_str: str) -> None:
49+
"""Convert string address to PeerInfo and add to peerstore."""
50+
try:
51+
multiaddr = Multiaddr(addr_str)
52+
except Exception as e:
53+
logger.debug(f"Invalid multiaddr format '{addr_str}': {e}")
54+
return
55+
if self.is_dns_addr(multiaddr):
56+
resolved_addrs = await resolver.resolve(multiaddr)
57+
peer_id_str = multiaddr.get_peer_id()
58+
if peer_id_str is None:
59+
logger.warning(f"Missing peer ID in DNS address: {addr_str}")
60+
return
61+
peer_id = ID.from_base58(peer_id_str)
62+
addrs = [addr for addr in resolved_addrs]
63+
if not addrs:
64+
logger.warning(f"No addresses resolved for DNS address: {addr_str}")
65+
return
66+
peer_info = PeerInfo(peer_id, addrs)
67+
self.add_addr(peer_info)
68+
else:
69+
self.add_addr(info_from_p2p_addr(multiaddr))
70+
71+
def is_dns_addr(self, addr: Multiaddr) -> bool:
72+
"""Check if the address is a DNS address."""
73+
return any(protocol.name == "dnsaddr" for protocol in addr.protocols())
74+
75+
def add_addr(self, peer_info: PeerInfo) -> None:
76+
"""Add a peer to the peerstore and emit discovery event."""
77+
# Skip if it's our own peer
78+
if peer_info.peer_id == self.swarm.get_peer_id():
79+
logger.debug(f"Skipping own peer ID: {peer_info.peer_id}")
80+
return
81+
82+
# Always add addresses to peerstore (allows multiple addresses for same peer)
83+
self.peerstore.add_addrs(peer_info.peer_id, peer_info.addrs, 10)
84+
85+
# Only emit discovery event if this is the first time we see this peer
86+
peer_id_str = str(peer_info.peer_id)
87+
if peer_id_str not in self.discovered_peers:
88+
# Track discovered peer
89+
self.discovered_peers.add(peer_id_str)
90+
# Emit peer discovery event
91+
peerDiscovery.emit_peer_discovered(peer_info)
92+
logger.debug(f"Peer discovered: {peer_info.peer_id}")
93+
else:
94+
logger.debug(f"Additional addresses added for peer: {peer_info.peer_id}")

libp2p/discovery/bootstrap/utils.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""Utility functions for bootstrap discovery."""
2+
3+
import logging
4+
5+
from multiaddr import Multiaddr
6+
7+
from libp2p.peer.peerinfo import InvalidAddrError, PeerInfo, info_from_p2p_addr
8+
9+
logger = logging.getLogger("libp2p.discovery.bootstrap.utils")
10+
11+
12+
def validate_bootstrap_addresses(addrs: list[str]) -> list[str]:
13+
"""
14+
Validate and filter bootstrap addresses.
15+
16+
:param addrs: List of bootstrap address strings
17+
:return: List of valid bootstrap addresses
18+
"""
19+
valid_addrs = []
20+
21+
for addr_str in addrs:
22+
try:
23+
# Try to parse as multiaddr
24+
multiaddr = Multiaddr(addr_str)
25+
26+
# Try to extract peer info (this validates the p2p component)
27+
info_from_p2p_addr(multiaddr)
28+
29+
valid_addrs.append(addr_str)
30+
logger.debug(f"Valid bootstrap address: {addr_str}")
31+
32+
except (InvalidAddrError, ValueError, Exception) as e:
33+
logger.warning(f"Invalid bootstrap address '{addr_str}': {e}")
34+
continue
35+
36+
return valid_addrs
37+
38+
39+
def parse_bootstrap_peer_info(addr_str: str) -> PeerInfo | None:
40+
"""
41+
Parse bootstrap address string into PeerInfo.
42+
43+
:param addr_str: Bootstrap address string
44+
:return: PeerInfo object or None if parsing fails
45+
"""
46+
try:
47+
multiaddr = Multiaddr(addr_str)
48+
return info_from_p2p_addr(multiaddr)
49+
except Exception as e:
50+
logger.error(f"Failed to parse bootstrap address '{addr_str}': {e}")
51+
return None

libp2p/host/basic_host.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
StreamHandlerFn,
3030
TProtocol,
3131
)
32+
from libp2p.discovery.bootstrap.bootstrap import BootstrapDiscovery
3233
from libp2p.discovery.mdns.mdns import MDNSDiscovery
3334
from libp2p.host.defaults import (
3435
get_default_protocols,
@@ -92,6 +93,7 @@ def __init__(
9293
self,
9394
network: INetworkService,
9495
enable_mDNS: bool = False,
96+
bootstrap: list[str] | None = None,
9597
default_protocols: Optional["OrderedDict[TProtocol, StreamHandlerFn]"] = None,
9698
negotitate_timeout: int = DEFAULT_NEGOTIATE_TIMEOUT,
9799
) -> None:
@@ -105,6 +107,8 @@ def __init__(
105107
self.multiselect_client = MultiselectClient()
106108
if enable_mDNS:
107109
self.mDNS = MDNSDiscovery(network)
110+
if bootstrap:
111+
self.bootstrap = BootstrapDiscovery(network, bootstrap)
108112

109113
def get_id(self) -> ID:
110114
"""
@@ -172,11 +176,16 @@ async def _run() -> AsyncIterator[None]:
172176
if hasattr(self, "mDNS") and self.mDNS is not None:
173177
logger.debug("Starting mDNS Discovery")
174178
self.mDNS.start()
179+
if hasattr(self, "bootstrap") and self.bootstrap is not None:
180+
logger.debug("Starting Bootstrap Discovery")
181+
await self.bootstrap.start()
175182
try:
176183
yield
177184
finally:
178185
if hasattr(self, "mDNS") and self.mDNS is not None:
179186
self.mDNS.stop()
187+
if hasattr(self, "bootstrap") and self.bootstrap is not None:
188+
self.bootstrap.stop()
180189

181190
return _run()
182191

libp2p/host/routed_host.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,13 @@ class RoutedHost(BasicHost):
1919
_router: IPeerRouting
2020

2121
def __init__(
22-
self, network: INetworkService, router: IPeerRouting, enable_mDNS: bool = False
22+
self,
23+
network: INetworkService,
24+
router: IPeerRouting,
25+
enable_mDNS: bool = False,
26+
bootstrap: list[str] | None = None,
2327
):
24-
super().__init__(network, enable_mDNS)
28+
super().__init__(network, enable_mDNS, bootstrap)
2529
self._router = router
2630

2731
async def connect(self, peer_info: PeerInfo) -> None:

newsfragments/711.feature.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added `Bootstrap` peer discovery module that allows nodes to connect to predefined bootstrap peers for network discovery.

0 commit comments

Comments
 (0)