Skip to content

Commit a414e74

Browse files
committed
feat(p2P): Add new whitelist policy
Co-Author: LFRezende <lfsrsprofessional@gmail.com>
1 parent 529ffce commit a414e74

28 files changed

Lines changed: 574 additions & 206 deletions

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,6 @@ keys.json
3030

3131
# Pycharm
3232
.idea
33+
34+
# Vscode
35+
.vscode/

hathor/builder/builder.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
from hathor.nanocontracts.sorter.types import NCSorterCallable
4242
from hathor.p2p.manager import ConnectionsManager
4343
from hathor.p2p.peer import PrivatePeer
44+
from hathor.p2p.peers_whitelist import PeersWhitelist
4445
from hathor.pubsub import PubSubManager
4546
from hathor.reactor import ReactorProtocol as Reactor
4647
from hathor.storage import RocksDBStorage
@@ -190,6 +191,7 @@ def __init__(self) -> None:
190191
self._enable_ipv6: bool = False
191192
self._disable_ipv4: bool = False
192193

194+
self._peers_whitelist: PeersWhitelist | None = None
193195
self._nc_anti_mev: bool = True
194196

195197
self._nc_storage_factory: NCStorageFactory | None = None
@@ -348,6 +350,18 @@ def set_peer(self, peer: PrivatePeer) -> 'Builder':
348350
self._peer = peer
349351
return self
350352

353+
def set_url_whitelist(self, reactor: Reactor, url: str = '') -> 'Builder':
354+
"""Sets the peers whitelist to a URLPeersWhitelist."""
355+
self.check_if_can_modify()
356+
if not url:
357+
url = 'https://something.com'
358+
from hathor.p2p.peers_whitelist import URLPeersWhitelist
359+
url_peers_whitelist = URLPeersWhitelist(reactor, url, False)
360+
url_peers_whitelist.follow_wl()
361+
# We do not start the URLPeersWhitelist here, as it is started by the ConnectionsManager
362+
self._peers_whitelist = url_peers_whitelist
363+
return self
364+
351365
def _get_or_create_settings(self) -> HathorSettingsType:
352366
"""Return the HathorSettings instance set on this builder, or a new one if not set."""
353367
if self._settings is None:
@@ -474,7 +488,7 @@ def _get_or_create_p2p_manager(self) -> ConnectionsManager:
474488
my_peer=my_peer,
475489
pubsub=self._get_or_create_pubsub(),
476490
ssl=enable_ssl,
477-
whitelist_only=False,
491+
peers_whitelist=self._peers_whitelist,
478492
rng=self._rng,
479493
enable_ipv6=self._enable_ipv6,
480494
disable_ipv4=self._disable_ipv4,
@@ -791,6 +805,11 @@ def enable_event_queue(self) -> 'Builder':
791805
self._enable_event_queue = True
792806
return self
793807

808+
def set_whitelist(self, peers_wl: PeersWhitelist | None) -> 'Builder':
809+
self.check_if_can_modify()
810+
self._peers_whitelist = peers_wl
811+
return self
812+
794813
def set_tx_storage(self, tx_storage: TransactionStorage) -> 'Builder':
795814
self.check_if_can_modify()
796815
self._tx_storage = tx_storage

hathor/conf/mainnet.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
MULTISIG_VERSION_BYTE=b'\x64',
2424
NETWORK_NAME='mainnet',
2525
BOOTSTRAP_DNS=['mainnet.hathor.network'],
26-
ENABLE_PEER_WHITELIST=True,
2726
WHITELIST_URL='https://hathor-public-files.s3.amazonaws.com/whitelist_peer_ids',
2827
# Genesis stuff
2928
# output addr: HJB2yxxsHtudGGy3jmVeadwMfRi2zNCKKD

hathor/conf/mainnet.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ MULTISIG_VERSION_BYTE: x64
33
NETWORK_NAME: mainnet
44
BOOTSTRAP_DNS:
55
- mainnet.hathor.network
6-
ENABLE_PEER_WHITELIST: true
76
WHITELIST_URL: https://hathor-public-files.s3.amazonaws.com/whitelist_peer_ids
87

98
# Genesis stuff

hathor/conf/settings.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,6 @@ class HathorSettings(NamedTuple):
7272
# Initial bootstrap servers
7373
BOOTSTRAP_DNS: list[str] = []
7474

75-
# enable peer whitelist
76-
ENABLE_PEER_WHITELIST: bool = False
77-
7875
# weather to use the whitelist with sync-v2 peers, does not affect whether the whitelist is enabled or not, it will
7976
# always be enabled for sync-v1 if it is enabled
8077
USE_PEER_WHITELIST_ON_SYNC_V2: bool = True

hathor/manager.py

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@
5050
from hathor.nanocontracts.storage import NCBlockStorage, NCContractStorage
5151
from hathor.p2p.manager import ConnectionsManager
5252
from hathor.p2p.peer import PrivatePeer
53-
from hathor.p2p.peer_id import PeerId
5453
from hathor.pubsub import HathorEvents, PubSubManager
5554
from hathor.reactor import ReactorProtocol as Reactor
5655
from hathor.reward_lock import is_spent_reward_locked
@@ -232,9 +231,6 @@ def __init__(
232231
# Thread pool used to resolve pow when sending tokens
233232
self.pow_thread_pool = ThreadPool(minthreads=0, maxthreads=settings.MAX_POW_THREADS, name='Pow thread pool')
234233

235-
# List of whitelisted peers
236-
self.peers_whitelist: list[PeerId] = []
237-
238234
# List of capabilities of the peer
239235
if capabilities is not None:
240236
self.capabilities = capabilities
@@ -881,24 +877,6 @@ def on_new_tx(
881877
def has_sync_version_capability(self) -> bool:
882878
return self._settings.CAPABILITY_SYNC_VERSION in self.capabilities
883879

884-
def add_peer_to_whitelist(self, peer_id: PeerId) -> None:
885-
if not self._settings.ENABLE_PEER_WHITELIST:
886-
return
887-
888-
if peer_id in self.peers_whitelist:
889-
self.log.info('peer already in whitelist', peer_id=peer_id)
890-
else:
891-
self.peers_whitelist.append(peer_id)
892-
893-
def remove_peer_from_whitelist_and_disconnect(self, peer_id: PeerId) -> None:
894-
if not self._settings.ENABLE_PEER_WHITELIST:
895-
return
896-
897-
if peer_id in self.peers_whitelist:
898-
self.peers_whitelist.remove(peer_id)
899-
# disconnect from node
900-
self.connections.drop_connection_by_peer_id(peer_id)
901-
902880
def has_recent_activity(self) -> bool:
903881
current_timestamp = time.time()
904882
latest_blockchain_timestamp = self.tx_storage.latest_timestamp

hathor/p2p/manager.py

Lines changed: 57 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,12 @@
3232
from hathor.p2p.peer_endpoint import PeerAddress, PeerEndpoint
3333
from hathor.p2p.peer_id import PeerId
3434
from hathor.p2p.peer_storage import VerifiedPeerStorage
35+
from hathor.p2p.peers_whitelist import PeersWhitelist
3536
from hathor.p2p.protocol import HathorProtocol
3637
from hathor.p2p.rate_limiter import RateLimiter
3738
from hathor.p2p.states.ready import ReadyState
3839
from hathor.p2p.sync_factory import SyncAgentFactory
3940
from hathor.p2p.sync_version import SyncVersion
40-
from hathor.p2p.utils import parse_whitelist
4141
from hathor.pubsub import HathorEvents, PubSubManager
4242
from hathor.reactor import ReactorProtocol as Reactor
4343
from hathor.transaction import BaseTransaction
@@ -48,9 +48,6 @@
4848

4949
logger = get_logger()
5050

51-
# The timeout in seconds for the whitelist GET request
52-
WHITELIST_REQUEST_TIMEOUT = 45
53-
5451

5552
class _SyncRotateInfo(NamedTuple):
5653
candidates: list[PeerId]
@@ -85,10 +82,11 @@ class GlobalRateLimiter:
8582
new_connection_from_queue: deque[PeerId]
8683
connecting_peers: dict[IStreamClientEndpoint, _ConnectingPeer]
8784
handshaking_peers: set[HathorProtocol]
88-
whitelist_only: bool
8985
verified_peer_storage: VerifiedPeerStorage
9086
_sync_factories: dict[SyncVersion, SyncAgentFactory]
9187
_enabled_sync_versions: set[SyncVersion]
88+
p2p_whitelist: Optional[PeersWhitelist]
89+
only_whitelist: bool
9290

9391
rate_limiter: RateLimiter
9492

@@ -100,7 +98,7 @@ def __init__(
10098
pubsub: PubSubManager,
10199
ssl: bool,
102100
rng: Random,
103-
whitelist_only: bool,
101+
peers_whitelist: PeersWhitelist | None,
104102
enable_ipv6: bool,
105103
disable_ipv4: bool,
106104
) -> None:
@@ -187,17 +185,16 @@ def __init__(
187185
self.lc_connect.clock = self.reactor
188186
self.lc_connect_interval = 0.2 # seconds
189187

190-
# A timer to try to reconnect to the disconnect known peers.
191-
if self._settings.ENABLE_PEER_WHITELIST:
192-
self.wl_reconnect = LoopingCall(self.update_whitelist)
193-
self.wl_reconnect.clock = self.reactor
188+
# Whitelisted peers.
189+
self.peers_whitelist: PeersWhitelist | None = peers_whitelist
190+
191+
# One may chose whether to follow the whitelist or not. Alterable by sysctl.
192+
# This is a p2p_manager copy of the wl_object status "_following_wl".
193+
self.only_whitelist = self.peers_whitelist.following_wl() if self.peers_whitelist else False
194194

195195
# Pubsub object to publish events
196196
self.pubsub = pubsub
197197

198-
# Parameter to explicitly enable whitelist-only mode, when False it will still check the whitelist for sync-v1
199-
self.whitelist_only = whitelist_only
200-
201198
# Parameter to enable IPv6 connections
202199
self.enable_ipv6 = enable_ipv6
203200

@@ -303,29 +300,14 @@ def start(self) -> None:
303300
self.lc_reconnect.start(5, now=False)
304301
self.lc_sync_update.start(self.lc_sync_update_interval, now=False)
305302

306-
if self._settings.ENABLE_PEER_WHITELIST:
307-
self._start_whitelist_reconnect()
303+
if self.peers_whitelist:
304+
self.peers_whitelist.start(self.drop_connection_by_peer_id)
308305

309306
for description in self.listen_address_descriptions:
310307
self.listen(description)
311308

312309
self.do_discovery()
313310

314-
def _start_whitelist_reconnect(self) -> None:
315-
# The deferred returned by the LoopingCall start method
316-
# executes when the looping call stops running
317-
# https://docs.twistedmatrix.com/en/stable/api/twisted.internet.task.LoopingCall.html
318-
d = self.wl_reconnect.start(30)
319-
d.addErrback(self._handle_whitelist_reconnect_err)
320-
321-
def _handle_whitelist_reconnect_err(self, *args: Any, **kwargs: Any) -> None:
322-
""" This method will be called when an exception happens inside the whitelist update
323-
and ends up stopping the looping call.
324-
We log the error and start the looping call again.
325-
"""
326-
self.log.error('whitelist reconnect had an exception. Start looping call again.', args=args, kwargs=kwargs)
327-
self.reactor.callLater(30, self._start_whitelist_reconnect)
328-
329311
def _start_peer_connect_loop(self) -> None:
330312
# The deferred returned by the LoopingCall start method
331313
# executes when the looping call stops running
@@ -354,6 +336,9 @@ def stop(self) -> None:
354336
if self.lc_sync_update.running:
355337
self.lc_sync_update.stop()
356338

339+
if self.peers_whitelist:
340+
self.peers_whitelist.stop()
341+
357342
def _get_peers_count(self) -> PeerConnectionsMetrics:
358343
"""Get a dict containing the count of peers in each state"""
359344

@@ -421,9 +406,9 @@ def on_peer_connect(self, protocol: HathorProtocol) -> None:
421406
self.log.warn('reached maximum number of connections', max_connections=self.max_connections)
422407
protocol.disconnect(force=True)
423408
return
409+
424410
self.connections.add(protocol)
425411
self.handshaking_peers.add(protocol)
426-
427412
self.pubsub.publish(
428413
HathorEvents.NETWORK_PEER_CONNECTED,
429414
protocol=protocol,
@@ -602,47 +587,6 @@ def reconnect_to_all(self) -> None:
602587
for peer in list(self.verified_peer_storage.values()):
603588
self.connect_to_peer(peer, int(now))
604589

605-
def update_whitelist(self) -> Deferred[None]:
606-
from twisted.web.client import readBody
607-
from twisted.web.http_headers import Headers
608-
assert self._settings.WHITELIST_URL is not None
609-
self.log.info('update whitelist')
610-
d = self._http_agent.request(
611-
b'GET',
612-
self._settings.WHITELIST_URL.encode(),
613-
Headers({'User-Agent': ['hathor-core']}),
614-
None)
615-
d.addCallback(readBody)
616-
d.addTimeout(WHITELIST_REQUEST_TIMEOUT, self.reactor)
617-
d.addCallback(self._update_whitelist_cb)
618-
d.addErrback(self._update_whitelist_err)
619-
620-
return d
621-
622-
def _update_whitelist_err(self, *args: Any, **kwargs: Any) -> None:
623-
self.log.error('update whitelist failed', args=args, kwargs=kwargs)
624-
625-
def _update_whitelist_cb(self, body: bytes) -> None:
626-
assert self.manager is not None
627-
self.log.info('update whitelist got response')
628-
try:
629-
text = body.decode()
630-
new_whitelist = parse_whitelist(text)
631-
except Exception:
632-
self.log.exception('failed to parse whitelist')
633-
return
634-
current_whitelist = set(self.manager.peers_whitelist)
635-
peers_to_add = new_whitelist - current_whitelist
636-
if peers_to_add:
637-
self.log.info('add new peers to whitelist', peers=peers_to_add)
638-
peers_to_remove = current_whitelist - new_whitelist
639-
if peers_to_remove:
640-
self.log.info('remove peers peers from whitelist', peers=peers_to_remove)
641-
for peer_id in peers_to_add:
642-
self.manager.add_peer_to_whitelist(peer_id)
643-
for peer_id in peers_to_remove:
644-
self.manager.remove_peer_from_whitelist_and_disconnect(peer_id)
645-
646590
def connect_to_peer(self, peer: UnverifiedPeer | PublicPeer, now: int) -> None:
647591
""" Attempts to connect if it is not connected to the peer.
648592
"""
@@ -940,3 +884,44 @@ def reload_entrypoints_and_connections(self) -> None:
940884
self.log.warn('Killing all connections and resetting entrypoints...')
941885
self.disconnect_all_peers(force=True)
942886
self.my_peer.reload_entrypoints_from_source_file()
887+
888+
def whitelist_swap(self, wl_object: PeersWhitelist) -> None:
889+
"""
890+
Altering whitelist (URL/PATH) during full-node runtime.
891+
"""
892+
893+
if not wl_object:
894+
return
895+
896+
if self.peers_whitelist:
897+
self.peers_whitelist.stop()
898+
899+
# Sysctl may only update to another URL or Path, not None.
900+
self.log.warn("Swapping whitelists... ")
901+
self.peers_whitelist = wl_object
902+
903+
self.whitelist_toggle(wl_object.following_wl())
904+
905+
def whitelist_toggle(self, wl_toggle: bool) -> None:
906+
"""
907+
Called if whitelist is turned "on" or "off".
908+
Called via sysctl methods.
909+
"""
910+
911+
if not self.peers_whitelist:
912+
return
913+
914+
# Pass toggle option to p2p local copy.
915+
self.only_whitelist = wl_toggle
916+
self.peers_whitelist._following_wl = wl_toggle
917+
918+
if wl_toggle:
919+
self.log.warn('Whitelist ON: Node starts following it...')
920+
# All connections not in the whitelist are severed.
921+
for conn in self.connections:
922+
if conn.get_peer_id():
923+
if conn.get_peer_id() not in self.peers_whitelist._current:
924+
conn.disconnect(reason='Whitelist turned on', force=True)
925+
return
926+
927+
self.log.warn("Whitelist OFF - Node starts ignoring it...")

0 commit comments

Comments
 (0)