Skip to content

Commit 12ad2dc

Browse files
committed
Added bootstrap module
1 parent 92c9ba7 commit 12ad2dc

File tree

12 files changed

+419
-4
lines changed

12 files changed

+419
-4
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,3 +178,7 @@ env.bak/
178178
#lockfiles
179179
uv.lock
180180
poetry.lock
181+
182+
bootstrap_instructions.txt
183+
.gitignore
184+
README.md

examples/bootstrap/bootstrap.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
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+
# Set root logger to DEBUG to capture all logs
23+
logging.getLogger().setLevel(logging.DEBUG)
24+
25+
26+
def on_peer_discovery(peer_info: PeerInfo) -> None:
27+
"""Handler for peer discovery events."""
28+
logger.info(f"🔍 Discovered peer: {peer_info.peer_id}")
29+
logger.info(f" Addresses: {[str(addr) for addr in peer_info.addrs]}")
30+
31+
32+
# Example bootstrap peers (you can replace with real bootstrap nodes)
33+
BOOTSTRAP_PEERS = [
34+
# IPFS bootstrap nodes (examples - replace with actual working nodes)
35+
"/ip4/104.131.131.82/tcp/4001/p2p/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SznbYGzPwp8qDrq",
36+
"/ip6/2604:a880:1:20::203:d001/tcp/4001/p2p/QmSoLPppuBtQSGwKDZT2M73ULpjvfd3aZ6ha4oFGL1KrGM",
37+
]
38+
39+
40+
async def run(port: int, bootstrap_addrs: list[str]) -> None:
41+
"""Run the bootstrap discovery example."""
42+
# Generate key pair
43+
secret = secrets.token_bytes(32)
44+
key_pair = create_new_key_pair(secret)
45+
46+
# Create listen address
47+
listen_addr = multiaddr.Multiaddr(f"/ip4/0.0.0.0/tcp/{port}")
48+
49+
# Register peer discovery handler
50+
peerDiscovery.register_peer_discovered_handler(on_peer_discovery)
51+
52+
logger.info("🚀 Starting Bootstrap Discovery Example")
53+
logger.info(f"📍 Listening on: {listen_addr}")
54+
logger.info(f"🌐 Bootstrap peers: {len(bootstrap_addrs)}")
55+
56+
print("\n" + "=" * 60)
57+
print("Bootstrap Discovery Example")
58+
print("=" * 60)
59+
print("This example demonstrates connecting to bootstrap peers.")
60+
print("Watch the logs for peer discovery events!")
61+
print("Press Ctrl+C to exit.")
62+
print("=" * 60)
63+
64+
# Create and run host with bootstrap discovery
65+
host = new_host(key_pair=key_pair, bootstrap=bootstrap_addrs)
66+
67+
try:
68+
async with host.run(listen_addrs=[listen_addr]):
69+
# Keep running and log peer discovery events
70+
await trio.sleep_forever()
71+
except KeyboardInterrupt:
72+
logger.info("👋 Shutting down...")
73+
74+
75+
def main() -> None:
76+
"""Main entry point."""
77+
description = """
78+
Bootstrap Discovery Example for py-libp2p
79+
80+
This example demonstrates how to use bootstrap peers for peer discovery.
81+
Bootstrap peers are predefined peers that help new nodes join the network.
82+
83+
Usage:
84+
python bootstrap.py -p 8000
85+
python bootstrap.py -p 8001 --custom-bootstrap \\
86+
"/ip4/127.0.0.1/tcp/8000/p2p/QmYourPeerID"
87+
"""
88+
89+
parser = argparse.ArgumentParser(
90+
description=description, formatter_class=argparse.RawDescriptionHelpFormatter
91+
)
92+
parser.add_argument(
93+
"-p", "--port", default=0, type=int, help="Port to listen on (default: random)"
94+
)
95+
parser.add_argument(
96+
"--custom-bootstrap",
97+
nargs="*",
98+
help="Custom bootstrap addresses (space-separated)",
99+
)
100+
parser.add_argument(
101+
"-v", "--verbose", action="store_true", help="Enable verbose output"
102+
)
103+
104+
args = parser.parse_args()
105+
106+
if args.verbose:
107+
logger.setLevel(logging.DEBUG)
108+
109+
# Use custom bootstrap addresses if provided, otherwise use defaults
110+
bootstrap_addrs = (
111+
args.custom_bootstrap if args.custom_bootstrap else BOOTSTRAP_PEERS
112+
)
113+
114+
try:
115+
trio.run(run, args.port, bootstrap_addrs)
116+
except KeyboardInterrupt:
117+
logger.info("Exiting...")
118+
119+
120+
if __name__ == "__main__":
121+
main()

libp2p/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,7 @@ def new_host(
249249
muxer_preference: Literal["YAMUX", "MPLEX"] | None = None,
250250
listen_addrs: Sequence[multiaddr.Multiaddr] | None = None,
251251
enable_mDNS: bool = False,
252+
bootstrap: list[str] | None = None,
252253
) -> IHost:
253254
"""
254255
Create a new libp2p host based on the given parameters.
@@ -261,6 +262,7 @@ def new_host(
261262
:param muxer_preference: optional explicit muxer preference
262263
:param listen_addrs: optional list of multiaddrs to listen on
263264
:param enable_mDNS: whether to enable mDNS discovery
265+
:param bootstrap: optional list of bootstrap peer addresses as strings
264266
:return: return a host instance
265267
"""
266268
swarm = new_swarm(
@@ -273,7 +275,7 @@ def new_host(
273275
)
274276

275277
if disc_opt is not None:
276-
return RoutedHost(swarm, disc_opt, enable_mDNS)
277-
return BasicHost(swarm, enable_mDNS)
278+
return RoutedHost(swarm, disc_opt, enable_mDNS, bootstrap)
279+
return BasicHost(swarm, enable_mDNS, bootstrap)
278280

279281
__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: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import logging
2+
3+
from multiaddr import Multiaddr
4+
5+
from libp2p.abc import INetworkService
6+
from libp2p.discovery.events.peerDiscovery import peerDiscovery
7+
from libp2p.peer.peerinfo import info_from_p2p_addr
8+
9+
logger = logging.getLogger("libp2p.discovery.bootstrap")
10+
11+
12+
class BootstrapDiscovery:
13+
"""
14+
Bootstrap-based peer discovery for py-libp2p.
15+
Connects to predefined bootstrap peers and adds them to peerstore.
16+
"""
17+
18+
def __init__(self, swarm: INetworkService, bootstrap_addrs: list[str]):
19+
self.swarm = swarm
20+
self.peerstore = swarm.peerstore
21+
self.bootstrap_addrs = bootstrap_addrs or []
22+
self.discovered_peers: set[str] = set()
23+
24+
def start(self) -> None:
25+
"""Process bootstrap addresses and emit peer discovery events."""
26+
logger.debug(
27+
f"Starting bootstrap discovery with "
28+
f"{len(self.bootstrap_addrs)} bootstrap addresses"
29+
)
30+
31+
for addr_str in self.bootstrap_addrs:
32+
try:
33+
self._process_bootstrap_addr(addr_str)
34+
except Exception as e:
35+
logger.warning(f"Failed to process bootstrap address {addr_str}: {e}")
36+
37+
def stop(self) -> None:
38+
"""Clean up bootstrap discovery resources."""
39+
logger.debug("Stopping bootstrap discovery")
40+
self.discovered_peers.clear()
41+
42+
def _process_bootstrap_addr(self, addr_str: str) -> None:
43+
"""Convert string address to PeerInfo and add to peerstore."""
44+
# Convert string to Multiaddr
45+
multiaddr = Multiaddr(addr_str)
46+
47+
# Extract peer info from multiaddr
48+
peer_info = info_from_p2p_addr(multiaddr)
49+
50+
# Skip if it's our own peer
51+
if peer_info.peer_id == self.swarm.get_peer_id():
52+
logger.debug(f"Skipping own peer ID: {peer_info.peer_id}")
53+
return
54+
55+
# Skip if already discovered
56+
if str(peer_info.peer_id) in self.discovered_peers:
57+
logger.debug(f"Peer already discovered: {peer_info.peer_id}")
58+
return
59+
60+
# Add to peerstore with TTL (using same pattern as mDNS)
61+
self.peerstore.add_addrs(peer_info.peer_id, peer_info.addrs, 10)
62+
63+
# Track discovered peer
64+
self.discovered_peers.add(str(peer_info.peer_id))
65+
66+
# Emit peer discovery event
67+
peerDiscovery.emit_peer_discovered(peer_info)
68+
69+
logger.info(f"Discovered bootstrap 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,
@@ -91,6 +92,7 @@ def __init__(
9192
self,
9293
network: INetworkService,
9394
enable_mDNS: bool = False,
95+
bootstrap: list[str] | None = None,
9496
default_protocols: Optional["OrderedDict[TProtocol, StreamHandlerFn]"] = None,
9597
) -> None:
9698
self._network = network
@@ -102,6 +104,8 @@ def __init__(
102104
self.multiselect_client = MultiselectClient()
103105
if enable_mDNS:
104106
self.mDNS = MDNSDiscovery(network)
107+
if bootstrap:
108+
self.bootstrap = BootstrapDiscovery(network, bootstrap)
105109

106110
def get_id(self) -> ID:
107111
"""
@@ -169,11 +173,16 @@ async def _run() -> AsyncIterator[None]:
169173
if hasattr(self, "mDNS") and self.mDNS is not None:
170174
logger.debug("Starting mDNS Discovery")
171175
self.mDNS.start()
176+
if hasattr(self, "bootstrap") and self.bootstrap is not None:
177+
logger.debug("Starting Bootstrap Discovery")
178+
self.bootstrap.start()
172179
try:
173180
yield
174181
finally:
175182
if hasattr(self, "mDNS") and self.mDNS is not None:
176183
self.mDNS.stop()
184+
if hasattr(self, "bootstrap") and self.bootstrap is not None:
185+
self.bootstrap.stop()
177186

178187
return _run()
179188

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:

tests/discovery/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Discovery tests for py-libp2p."""

tests/discovery/bootstrap/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Bootstrap discovery tests for py-libp2p."""

0 commit comments

Comments
 (0)