Skip to content

Commit 761e0bb

Browse files
authored
Merge pull request #1068 from sumanjeet0012/fix/kademlia-dht-bootstrap-fallback
Implement fallback mechanism in Kademlia DHT for peer lookup.
2 parents 193bfbf + 3d77b26 commit 761e0bb

File tree

3 files changed

+177
-5
lines changed

3 files changed

+177
-5
lines changed

libp2p/kad_dht/peer_routing.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
logger = logging.getLogger("kademlia-example.peer_routing")
4444

4545
MAX_PEER_LOOKUP_ROUNDS = 20 # Maximum number of rounds in peer lookup
46+
MIN_PEERS_THRESHOLD = 5 # Minimum peers threshold for fallback to connected peers
4647

4748

4849
class PeerRouting(IPeerRouting):
@@ -158,6 +159,9 @@ async def find_closest_peers_network(
158159
Find the closest peers to a target key in the entire network.
159160
160161
Performs an iterative lookup by querying peers for their closest peers.
162+
If the routing table has fewer peers than MIN_PEERS_THRESHOLD, it falls
163+
back to using connected peers first, then peers from the peerstore if
164+
needed, to gather up to 'count' initial query targets.
161165
162166
Returns
163167
-------
@@ -168,6 +172,47 @@ async def find_closest_peers_network(
168172
# Start with closest peers from our routing table
169173
closest_peers = self.routing_table.find_local_closest_peers(target_key, count)
170174
logger.debug("Local closest peers: %d found", len(closest_peers))
175+
176+
# Fallback to connected peers and peerstore if routing table has
177+
# insufficient peers
178+
if len(closest_peers) < MIN_PEERS_THRESHOLD:
179+
# First, try connected peers
180+
connected_peers = self.host.get_connected_peers()
181+
if connected_peers:
182+
logger.debug(
183+
"Routing table has insufficient peers (%d < %d), "
184+
"adding %d connected peers",
185+
len(closest_peers),
186+
MIN_PEERS_THRESHOLD,
187+
len(connected_peers),
188+
)
189+
closest_peers.extend(connected_peers)
190+
191+
# If still not enough, get peers from peerstore
192+
if len(closest_peers) < count:
193+
try:
194+
peerstore_peers = self.host.get_peerstore().peer_ids()
195+
# Filter out our own ID and already included peers
196+
local_id = self.host.get_id()
197+
existing_peers = set(closest_peers)
198+
new_peerstore_peers = [
199+
p
200+
for p in peerstore_peers
201+
if p != local_id and p not in existing_peers
202+
]
203+
if new_peerstore_peers:
204+
logger.debug(
205+
"Adding %d peers from peerstore", len(new_peerstore_peers)
206+
)
207+
closest_peers.extend(new_peerstore_peers)
208+
except Exception as e:
209+
logger.debug(f"Failed to get peers from peerstore: {e}")
210+
211+
# Deduplicate and sort by distance, keeping closest peers
212+
closest_peers = sort_peer_ids_by_distance(
213+
target_key, list(dict.fromkeys(closest_peers))
214+
)[:count]
215+
171216
queried_peers: set[ID] = set()
172217
rounds = 0
173218

newsfragments/905.feature.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added fallback mechanism in Kademlia DHT to use connected peers and peerstore when routing table has insufficient peers.

tests/core/kad_dht/test_unit_peer_routing.py

Lines changed: 131 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from libp2p.kad_dht.peer_routing import (
3131
ALPHA,
3232
MAX_PEER_LOOKUP_ROUNDS,
33+
MIN_PEERS_THRESHOLD,
3334
PROTOCOL_ID,
3435
PeerRouting,
3536
)
@@ -145,18 +146,134 @@ async def test_find_peer_not_found(self, peer_routing, mock_host):
145146
assert result is None
146147

147148
@pytest.mark.trio
148-
async def test_find_closest_peers_network_empty_start(self, peer_routing):
149-
"""Test network search with no local peers."""
149+
async def test_find_closest_peers_network_empty_start(
150+
self, peer_routing, mock_host
151+
):
152+
"""Test network search with no local peers and no fallback peers."""
150153
target_key = b"target_key"
151154

152155
# Mock routing table to return empty list
153156
with patch.object(
154157
peer_routing.routing_table, "find_local_closest_peers", return_value=[]
155158
):
159+
# Mock no connected peers and empty peerstore
160+
mock_host.get_connected_peers.return_value = []
161+
mock_host.get_peerstore().peer_ids.return_value = []
156162
result = await peer_routing.find_closest_peers_network(target_key)
157163

158164
assert result == []
159165

166+
@pytest.mark.trio
167+
async def test_find_closest_peers_network_fallback_to_connected_peers(
168+
self, peer_routing, mock_host
169+
):
170+
"""
171+
Test that network search falls back to connected peers when routing
172+
table has insufficient peers.
173+
"""
174+
target_key = b"target_key"
175+
176+
# Create few local peers (less than MIN_PEERS_THRESHOLD)
177+
local_peers = [create_valid_peer_id(f"local{i}") for i in range(2)]
178+
179+
# Create connected peers
180+
connected_peers = [create_valid_peer_id(f"connected{i}") for i in range(3)]
181+
182+
# Mock routing table to return insufficient peers
183+
with patch.object(
184+
peer_routing.routing_table,
185+
"find_local_closest_peers",
186+
return_value=local_peers,
187+
):
188+
# Mock host to return connected peers
189+
mock_host.get_connected_peers.return_value = connected_peers
190+
# Mock peerstore to return empty (connected peers should be enough)
191+
mock_host.get_peerstore().peer_ids.return_value = []
192+
193+
# Mock _query_peer_for_closest to return empty results
194+
with patch.object(peer_routing, "_query_peer_for_closest", return_value=[]):
195+
result = await peer_routing.find_closest_peers_network(
196+
target_key, count=10
197+
)
198+
199+
# Should include both local and connected peers
200+
assert len(result) > 0
201+
# Verify host.get_connected_peers was called
202+
mock_host.get_connected_peers.assert_called()
203+
204+
@pytest.mark.trio
205+
async def test_find_closest_peers_network_fallback_to_peerstore(
206+
self, peer_routing, mock_host
207+
):
208+
"""
209+
Test that network search falls back to peerstore when connected peers
210+
are insufficient.
211+
"""
212+
target_key = b"target_key"
213+
214+
# Create few local peers (less than MIN_PEERS_THRESHOLD)
215+
local_peers = [create_valid_peer_id(f"local{i}") for i in range(2)]
216+
217+
# Create few connected peers (not enough to reach count)
218+
connected_peers = [create_valid_peer_id(f"connected{i}") for i in range(2)]
219+
220+
# Create peerstore peers
221+
peerstore_peers = [create_valid_peer_id(f"peerstore{i}") for i in range(5)]
222+
223+
# Mock routing table to return insufficient peers
224+
with patch.object(
225+
peer_routing.routing_table,
226+
"find_local_closest_peers",
227+
return_value=local_peers,
228+
):
229+
# Mock host to return connected peers
230+
mock_host.get_connected_peers.return_value = connected_peers
231+
# Mock peerstore to return additional peers
232+
mock_host.get_peerstore().peer_ids.return_value = peerstore_peers
233+
234+
# Mock _query_peer_for_closest to return empty results
235+
with patch.object(peer_routing, "_query_peer_for_closest", return_value=[]):
236+
result = await peer_routing.find_closest_peers_network(
237+
target_key, count=20
238+
)
239+
240+
# Should include peers from all sources
241+
assert len(result) > 0
242+
# Verify peerstore.peer_ids was called
243+
mock_host.get_peerstore().peer_ids.assert_called()
244+
245+
@pytest.mark.trio
246+
async def test_find_closest_peers_network_no_fallback_when_sufficient_peers(
247+
self, peer_routing, mock_host
248+
):
249+
"""
250+
Test that fallback is not triggered when routing table has
251+
sufficient peers.
252+
"""
253+
target_key = b"target_key"
254+
255+
# Create enough local peers (>= MIN_PEERS_THRESHOLD)
256+
local_peers = [
257+
create_valid_peer_id(f"local{i}") for i in range(MIN_PEERS_THRESHOLD + 1)
258+
]
259+
260+
# Mock routing table to return sufficient peers
261+
with patch.object(
262+
peer_routing.routing_table,
263+
"find_local_closest_peers",
264+
return_value=local_peers,
265+
):
266+
# Mock _query_peer_for_closest to return empty results
267+
with patch.object(peer_routing, "_query_peer_for_closest", return_value=[]):
268+
result = await peer_routing.find_closest_peers_network(
269+
target_key, count=10
270+
)
271+
272+
# Should have peers from routing table
273+
assert len(result) > 0
274+
# Verify host.get_connected_peers was NOT called
275+
mock_host.get_connected_peers.assert_not_called()
276+
160277
@pytest.mark.trio
161278
async def test_find_closest_peers_network_with_peers(self, peer_routing, mock_host):
162279
"""Test network search with some initial peers."""
@@ -171,7 +288,10 @@ async def test_find_closest_peers_network_with_peers(self, peer_routing, mock_ho
171288
"find_local_closest_peers",
172289
return_value=initial_peers,
173290
):
174-
# Mock _query_peer_for_closest to return empty results (no new peers found)
291+
# Mock get_connected_peers and peerstore to return empty
292+
mock_host.get_connected_peers.return_value = []
293+
mock_host.get_peerstore().peer_ids.return_value = []
294+
# Mock _query_peer_for_closest to return empty results
175295
with patch.object(peer_routing, "_query_peer_for_closest", return_value=[]):
176296
result = await peer_routing.find_closest_peers_network(
177297
target_key, count=5
@@ -182,7 +302,7 @@ async def test_find_closest_peers_network_with_peers(self, peer_routing, mock_ho
182302
assert all(peer in initial_peers for peer in result)
183303

184304
@pytest.mark.trio
185-
async def test_find_closest_peers_convergence(self, peer_routing):
305+
async def test_find_closest_peers_convergence(self, peer_routing, mock_host):
186306
"""Test that network search converges properly."""
187307
target_key = b"target_key"
188308

@@ -195,6 +315,9 @@ async def test_find_closest_peers_convergence(self, peer_routing):
195315
"find_local_closest_peers",
196316
return_value=initial_peers,
197317
):
318+
# Mock get_connected_peers and peerstore to return empty
319+
mock_host.get_connected_peers.return_value = []
320+
mock_host.get_peerstore().peer_ids.return_value = []
198321
with patch.object(peer_routing, "_query_peer_for_closest", return_value=[]):
199322
with patch(
200323
"libp2p.kad_dht.peer_routing.sort_peer_ids_by_distance",
@@ -430,7 +553,7 @@ def test_constants(self):
430553
assert PROTOCOL_ID == "/ipfs/kad/1.0.0"
431554

432555
@pytest.mark.trio
433-
async def test_edge_case_max_rounds_reached(self, peer_routing):
556+
async def test_edge_case_max_rounds_reached(self, peer_routing, mock_host):
434557
"""Test that lookup stops after maximum rounds."""
435558
target_key = b"target_key"
436559
initial_peers = [create_valid_peer_id("peer1")]
@@ -444,6 +567,9 @@ def mock_query_side_effect(peer, key):
444567
"find_local_closest_peers",
445568
return_value=initial_peers,
446569
):
570+
# Mock get_connected_peers and peerstore to return empty
571+
mock_host.get_connected_peers.return_value = []
572+
mock_host.get_peerstore().peer_ids.return_value = []
447573
with patch.object(
448574
peer_routing,
449575
"_query_peer_for_closest",

0 commit comments

Comments
 (0)