Skip to content

Commit a334a93

Browse files
authored
Merge pull request #3631 from lbryio/bootstrap_node
Add all peers when running as a bootstrap node
2 parents 01cd95f + e3ee389 commit a334a93

File tree

8 files changed

+165
-164
lines changed

8 files changed

+165
-164
lines changed

lbry/conf.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -624,6 +624,10 @@ class Config(CLIConfig):
624624
"will increase. This setting is used by seed nodes, you probably don't want to change it during normal "
625625
"use.", 2
626626
)
627+
is_bootstrap_node = Toggle(
628+
"When running as a bootstrap node, disable all logic related to balancing the routing table, so we can "
629+
"add as many peers as possible and better help first-runs.", False
630+
)
627631

628632
# protocol timeouts
629633
download_timeout = Float("Cumulative timeout for a stream to begin downloading before giving up", 30.0)

lbry/dht/node.py

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,12 @@ class Node:
3030
)
3131
def __init__(self, loop: asyncio.AbstractEventLoop, peer_manager: 'PeerManager', node_id: bytes, udp_port: int,
3232
internal_udp_port: int, peer_port: int, external_ip: str, rpc_timeout: float = constants.RPC_TIMEOUT,
33-
split_buckets_under_index: int = constants.SPLIT_BUCKETS_UNDER_INDEX,
33+
split_buckets_under_index: int = constants.SPLIT_BUCKETS_UNDER_INDEX, is_bootstrap_node: bool = False,
3434
storage: typing.Optional['SQLiteStorage'] = None):
3535
self.loop = loop
3636
self.internal_udp_port = internal_udp_port
3737
self.protocol = KademliaProtocol(loop, peer_manager, node_id, external_ip, udp_port, peer_port, rpc_timeout,
38-
split_buckets_under_index)
38+
split_buckets_under_index, is_bootstrap_node)
3939
self.listening_port: asyncio.DatagramTransport = None
4040
self.joined = asyncio.Event(loop=self.loop)
4141
self._join_task: asyncio.Task = None
@@ -70,13 +70,6 @@ async def refresh_node(self, force_once=False):
7070

7171
# get ids falling in the midpoint of each bucket that hasn't been recently updated
7272
node_ids = self.protocol.routing_table.get_refresh_list(0, True)
73-
# if we have 3 or fewer populated buckets get two random ids in the range of each to try and
74-
# populate/split the buckets further
75-
buckets_with_contacts = self.protocol.routing_table.buckets_with_contacts()
76-
if buckets_with_contacts <= 3:
77-
for i in range(buckets_with_contacts):
78-
node_ids.append(self.protocol.routing_table.random_id_in_bucket_range(i))
79-
node_ids.append(self.protocol.routing_table.random_id_in_bucket_range(i))
8073

8174
if self.protocol.routing_table.get_peers():
8275
# if we have node ids to look up, perform the iterative search until we have k results
@@ -203,15 +196,13 @@ def start(self, interface: str, known_node_urls: typing.Optional[typing.List[typ
203196

204197
def get_iterative_node_finder(self, key: bytes, shortlist: typing.Optional[typing.List['KademliaPeer']] = None,
205198
max_results: int = constants.K) -> IterativeNodeFinder:
206-
207-
return IterativeNodeFinder(self.loop, self.protocol.peer_manager, self.protocol.routing_table, self.protocol,
208-
key, max_results, None, shortlist)
199+
shortlist = shortlist or self.protocol.routing_table.find_close_peers(key)
200+
return IterativeNodeFinder(self.loop, self.protocol, key, max_results, shortlist)
209201

210202
def get_iterative_value_finder(self, key: bytes, shortlist: typing.Optional[typing.List['KademliaPeer']] = None,
211203
max_results: int = -1) -> IterativeValueFinder:
212-
213-
return IterativeValueFinder(self.loop, self.protocol.peer_manager, self.protocol.routing_table, self.protocol,
214-
key, max_results, None, shortlist)
204+
shortlist = shortlist or self.protocol.routing_table.find_close_peers(key)
205+
return IterativeValueFinder(self.loop, self.protocol, key, max_results, shortlist)
215206

216207
async def peer_search(self, node_id: bytes, count=constants.K, max_results=constants.K * 2,
217208
shortlist: typing.Optional[typing.List['KademliaPeer']] = None

lbry/dht/protocol/iterative_find.py

Lines changed: 10 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
from lbry.dht.serialization.datagram import PAGE_KEY
1313

1414
if TYPE_CHECKING:
15-
from lbry.dht.protocol.routing_table import TreeRoutingTable
1615
from lbry.dht.protocol.protocol import KademliaProtocol
1716
from lbry.dht.peer import PeerManager, KademliaPeer
1817

@@ -57,37 +56,19 @@ def get_close_triples(self) -> typing.List[typing.Tuple[bytes, str, int]]:
5756
return [(node_id, address.decode(), port) for node_id, address, port in self.close_triples]
5857

5958

60-
def get_shortlist(routing_table: 'TreeRoutingTable', key: bytes,
61-
shortlist: typing.Optional[typing.List['KademliaPeer']]) -> typing.List['KademliaPeer']:
62-
"""
63-
If not provided, initialize the shortlist of peers to probe to the (up to) k closest peers in the routing table
64-
65-
:param routing_table: a TreeRoutingTable
66-
:param key: a 48 byte hash
67-
:param shortlist: optional manually provided shortlist, this is done during bootstrapping when there are no
68-
peers in the routing table. During bootstrap the shortlist is set to be the seed nodes.
69-
"""
70-
if len(key) != constants.HASH_LENGTH:
71-
raise ValueError("invalid key length: %i" % len(key))
72-
return shortlist or routing_table.find_close_peers(key)
73-
74-
7559
class IterativeFinder(AsyncIterator):
76-
def __init__(self, loop: asyncio.AbstractEventLoop, peer_manager: 'PeerManager',
77-
routing_table: 'TreeRoutingTable', protocol: 'KademliaProtocol', key: bytes,
60+
def __init__(self, loop: asyncio.AbstractEventLoop,
61+
protocol: 'KademliaProtocol', key: bytes,
7862
max_results: typing.Optional[int] = constants.K,
79-
exclude: typing.Optional[typing.List[typing.Tuple[str, int]]] = None,
8063
shortlist: typing.Optional[typing.List['KademliaPeer']] = None):
8164
if len(key) != constants.HASH_LENGTH:
8265
raise ValueError("invalid key length: %i" % len(key))
8366
self.loop = loop
84-
self.peer_manager = peer_manager
85-
self.routing_table = routing_table
67+
self.peer_manager = protocol.peer_manager
8668
self.protocol = protocol
8769

8870
self.key = key
8971
self.max_results = max(constants.K, max_results)
90-
self.exclude = exclude or []
9172

9273
self.active: typing.Dict['KademliaPeer', int] = OrderedDict() # peer: distance, sorted
9374
self.contacted: typing.Set['KademliaPeer'] = set()
@@ -99,7 +80,7 @@ def __init__(self, loop: asyncio.AbstractEventLoop, peer_manager: 'PeerManager',
9980
self.iteration_count = 0
10081
self.running = False
10182
self.tasks: typing.List[asyncio.Task] = []
102-
for peer in get_shortlist(routing_table, key, shortlist):
83+
for peer in shortlist:
10384
if peer.node_id:
10485
self._add_active(peer, force=True)
10586
else:
@@ -198,8 +179,6 @@ def _search_round(self):
198179
if index > (constants.K + len(self.running_probes)):
199180
break
200181
origin_address = (peer.address, peer.udp_port)
201-
if origin_address in self.exclude:
202-
continue
203182
if peer.node_id == self.protocol.node_id:
204183
continue
205184
if origin_address == (self.protocol.external_ip, self.protocol.udp_port):
@@ -277,13 +256,11 @@ async def aclose(self):
277256
type(self).__name__, id(self), self.key.hex()[:8])
278257

279258
class IterativeNodeFinder(IterativeFinder):
280-
def __init__(self, loop: asyncio.AbstractEventLoop, peer_manager: 'PeerManager',
281-
routing_table: 'TreeRoutingTable', protocol: 'KademliaProtocol', key: bytes,
259+
def __init__(self, loop: asyncio.AbstractEventLoop,
260+
protocol: 'KademliaProtocol', key: bytes,
282261
max_results: typing.Optional[int] = constants.K,
283-
exclude: typing.Optional[typing.List[typing.Tuple[str, int]]] = None,
284262
shortlist: typing.Optional[typing.List['KademliaPeer']] = None):
285-
super().__init__(loop, peer_manager, routing_table, protocol, key, max_results, exclude,
286-
shortlist)
263+
super().__init__(loop, protocol, key, max_results, shortlist)
287264
self.yielded_peers: typing.Set['KademliaPeer'] = set()
288265

289266
async def send_probe(self, peer: 'KademliaPeer') -> FindNodeResponse:
@@ -319,13 +296,11 @@ def check_result_ready(self, response: FindNodeResponse):
319296

320297

321298
class IterativeValueFinder(IterativeFinder):
322-
def __init__(self, loop: asyncio.AbstractEventLoop, peer_manager: 'PeerManager',
323-
routing_table: 'TreeRoutingTable', protocol: 'KademliaProtocol', key: bytes,
299+
def __init__(self, loop: asyncio.AbstractEventLoop,
300+
protocol: 'KademliaProtocol', key: bytes,
324301
max_results: typing.Optional[int] = constants.K,
325-
exclude: typing.Optional[typing.List[typing.Tuple[str, int]]] = None,
326302
shortlist: typing.Optional[typing.List['KademliaPeer']] = None):
327-
super().__init__(loop, peer_manager, routing_table, protocol, key, max_results, exclude,
328-
shortlist)
303+
super().__init__(loop, protocol, key, max_results, shortlist)
329304
self.blob_peers: typing.Set['KademliaPeer'] = set()
330305
# this tracks the index of the most recent page we requested from each peer
331306
self.peer_pages: typing.DefaultDict['KademliaPeer', int] = defaultdict(int)

lbry/dht/protocol/protocol.py

Lines changed: 14 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,10 @@ def __init__(self, loop: asyncio.AbstractEventLoop, protocol: 'KademliaProtocol'
218218
def running(self):
219219
return self._running
220220

221+
@property
222+
def busy(self):
223+
return self._running and (any(self._running_pings) or any(self._pending_contacts))
224+
221225
def enqueue_maybe_ping(self, *peers: 'KademliaPeer', delay: typing.Optional[float] = None):
222226
delay = delay if delay is not None else self._default_delay
223227
now = self._loop.time()
@@ -229,7 +233,7 @@ def maybe_ping(self, peer: 'KademliaPeer'):
229233
async def ping_task():
230234
try:
231235
if self._protocol.peer_manager.peer_is_good(peer):
232-
if peer not in self._protocol.routing_table.get_peers():
236+
if not self._protocol.routing_table.get_peer(peer.node_id):
233237
self._protocol.add_peer(peer)
234238
return
235239
await self._protocol.get_rpc_peer(peer).ping()
@@ -294,7 +298,7 @@ class KademliaProtocol(DatagramProtocol):
294298

295299
def __init__(self, loop: asyncio.AbstractEventLoop, peer_manager: 'PeerManager', node_id: bytes, external_ip: str,
296300
udp_port: int, peer_port: int, rpc_timeout: float = constants.RPC_TIMEOUT,
297-
split_buckets_under_index: int = constants.SPLIT_BUCKETS_UNDER_INDEX):
301+
split_buckets_under_index: int = constants.SPLIT_BUCKETS_UNDER_INDEX, is_boostrap_node: bool = False):
298302
self.peer_manager = peer_manager
299303
self.loop = loop
300304
self.node_id = node_id
@@ -309,7 +313,8 @@ def __init__(self, loop: asyncio.AbstractEventLoop, peer_manager: 'PeerManager',
309313
self.transport: DatagramTransport = None
310314
self.old_token_secret = constants.generate_id()
311315
self.token_secret = constants.generate_id()
312-
self.routing_table = TreeRoutingTable(self.loop, self.peer_manager, self.node_id, split_buckets_under_index)
316+
self.routing_table = TreeRoutingTable(
317+
self.loop, self.peer_manager, self.node_id, split_buckets_under_index, is_bootstrap_node=is_boostrap_node)
313318
self.data_store = DictDataStore(self.loop, self.peer_manager)
314319
self.ping_queue = PingQueue(self.loop, self)
315320
self.node_rpc = KademliaRPC(self, self.loop, self.peer_port)
@@ -356,72 +361,10 @@ def _migrate_incoming_rpc_args(peer: 'KademliaPeer', method: bytes, *args) -> ty
356361
return args, {}
357362

358363
async def _add_peer(self, peer: 'KademliaPeer'):
359-
if not peer.node_id:
360-
log.warning("Tried adding a peer with no node id!")
361-
return False
362-
for my_peer in self.routing_table.get_peers():
363-
if (my_peer.address, my_peer.udp_port) == (peer.address, peer.udp_port) and my_peer.node_id != peer.node_id:
364-
self.routing_table.remove_peer(my_peer)
365-
self.routing_table.join_buckets()
366-
bucket_index = self.routing_table.kbucket_index(peer.node_id)
367-
if self.routing_table.buckets[bucket_index].add_peer(peer):
368-
return True
369-
370-
# The bucket is full; see if it can be split (by checking if its range includes the host node's node_id)
371-
if self.routing_table.should_split(bucket_index, peer.node_id):
372-
self.routing_table.split_bucket(bucket_index)
373-
# Retry the insertion attempt
374-
result = await self._add_peer(peer)
375-
self.routing_table.join_buckets()
376-
return result
377-
else:
378-
# We can't split the k-bucket
379-
#
380-
# The 13 page kademlia paper specifies that the least recently contacted node in the bucket
381-
# shall be pinged. If it fails to reply it is replaced with the new contact. If the ping is successful
382-
# the new contact is ignored and not added to the bucket (sections 2.2 and 2.4).
383-
#
384-
# A reasonable extension to this is BEP 0005, which extends the above:
385-
#
386-
# Not all nodes that we learn about are equal. Some are "good" and some are not.
387-
# Many nodes using the DHT are able to send queries and receive responses,
388-
# but are not able to respond to queries from other nodes. It is important that
389-
# each node's routing table must contain only known good nodes. A good node is
390-
# a node has responded to one of our queries within the last 15 minutes. A node
391-
# is also good if it has ever responded to one of our queries and has sent us a
392-
# query within the last 15 minutes. After 15 minutes of inactivity, a node becomes
393-
# questionable. Nodes become bad when they fail to respond to multiple queries
394-
# in a row. Nodes that we know are good are given priority over nodes with unknown status.
395-
#
396-
# When there are bad or questionable nodes in the bucket, the least recent is selected for
397-
# potential replacement (BEP 0005). When all nodes in the bucket are fresh, the head (least recent)
398-
# contact is selected as described in section 2.2 of the kademlia paper. In both cases the new contact
399-
# is ignored if the pinged node replies.
400-
401-
not_good_contacts = self.routing_table.buckets[bucket_index].get_bad_or_unknown_peers()
402-
not_recently_replied = []
403-
for my_peer in not_good_contacts:
404-
last_replied = self.peer_manager.get_last_replied(my_peer.address, my_peer.udp_port)
405-
if not last_replied or last_replied + 60 < self.loop.time():
406-
not_recently_replied.append(my_peer)
407-
if not_recently_replied:
408-
to_replace = not_recently_replied[0]
409-
else:
410-
to_replace = self.routing_table.buckets[bucket_index].peers[0]
411-
last_replied = self.peer_manager.get_last_replied(to_replace.address, to_replace.udp_port)
412-
if last_replied and last_replied + 60 > self.loop.time():
413-
return False
414-
log.debug("pinging %s:%s", to_replace.address, to_replace.udp_port)
415-
try:
416-
to_replace_rpc = self.get_rpc_peer(to_replace)
417-
await to_replace_rpc.ping()
418-
return False
419-
except asyncio.TimeoutError:
420-
log.debug("Replacing dead contact in bucket %i: %s:%i with %s:%i ", bucket_index,
421-
to_replace.address, to_replace.udp_port, peer.address, peer.udp_port)
422-
if to_replace in self.routing_table.buckets[bucket_index]:
423-
self.routing_table.buckets[bucket_index].remove_peer(to_replace)
424-
return await self._add_peer(peer)
364+
async def probe(some_peer: 'KademliaPeer'):
365+
rpc_peer = self.get_rpc_peer(some_peer)
366+
await rpc_peer.ping()
367+
return await self.routing_table.add_peer(peer, probe)
425368

426369
def add_peer(self, peer: 'KademliaPeer'):
427370
if peer.node_id == self.node_id:
@@ -439,7 +382,6 @@ async def routing_table_task(self):
439382
async with self._split_lock:
440383
peer = self._to_remove.pop()
441384
self.routing_table.remove_peer(peer)
442-
self.routing_table.join_buckets()
443385
while self._to_add:
444386
async with self._split_lock:
445387
await self._add_peer(self._to_add.pop())
@@ -482,9 +424,8 @@ def handle_request_datagram(self, address: typing.Tuple[str, int], request_datag
482424
# This is an RPC method request
483425
self.received_request_metric.labels(method=request_datagram.method).inc()
484426
self.peer_manager.report_last_requested(address[0], address[1])
485-
try:
486-
peer = self.routing_table.get_peer(request_datagram.node_id)
487-
except IndexError:
427+
peer = self.routing_table.get_peer(request_datagram.node_id)
428+
if not peer:
488429
try:
489430
peer = make_kademlia_peer(request_datagram.node_id, address[0], address[1])
490431
except ValueError as err:

0 commit comments

Comments
 (0)