2020-12-22 18:03:48 +00:00
|
|
|
# SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
# Copyright (c) 2020, The Monero Project.
|
|
|
|
# Copyright (c) 2020, dsc@xmr.pm
|
|
|
|
|
|
|
|
import json
|
|
|
|
from typing import List
|
|
|
|
|
|
|
|
import settings
|
|
|
|
from fapi.utils import httpget, popularity_contest
|
|
|
|
from fapi.tasks import FeatherTask
|
|
|
|
|
|
|
|
|
|
|
|
class RPCNodeCheckTask(FeatherTask):
|
|
|
|
def __init__(self, interval: int = 60):
|
|
|
|
super(RPCNodeCheckTask, self).__init__(interval)
|
|
|
|
|
|
|
|
self._cache_key = "rpc_nodes"
|
|
|
|
self._cache_expiry = None
|
|
|
|
|
|
|
|
self._websocket_cmd = "nodes"
|
|
|
|
|
|
|
|
self._http_timeout = 5
|
|
|
|
self._http_timeout_onion = 10
|
|
|
|
|
|
|
|
async def task(self) -> List[dict]:
|
|
|
|
"""Check RPC nodes status"""
|
2020-12-29 20:33:32 +00:00
|
|
|
from fapi.factory import app, cache
|
2020-12-22 18:03:48 +00:00
|
|
|
|
|
|
|
try:
|
|
|
|
heights = json.loads(await cache.get("blockheights"))
|
|
|
|
except:
|
|
|
|
heights = {}
|
|
|
|
|
2020-12-29 20:33:32 +00:00
|
|
|
rpc_nodes = await self.cache_json_get("nodes")
|
|
|
|
|
2020-12-22 18:03:48 +00:00
|
|
|
nodes = []
|
|
|
|
for network_type_coin, _ in rpc_nodes.items():
|
|
|
|
data = []
|
|
|
|
|
|
|
|
for network_type, _nodes in _.items():
|
|
|
|
for node in _nodes:
|
|
|
|
try:
|
|
|
|
blob = await self.node_check(node, network_type=network_type)
|
|
|
|
data.append(blob)
|
|
|
|
except Exception as ex:
|
|
|
|
app.logger.warning(f"node {node} not reachable; {ex}")
|
|
|
|
data.append(self._bad_node(**{
|
|
|
|
"address": node,
|
|
|
|
"nettype": network_type_coin,
|
|
|
|
"type": network_type,
|
|
|
|
"height": 0
|
|
|
|
}))
|
|
|
|
|
|
|
|
# not neccesary for stagenet/testnet nodes to be validated
|
|
|
|
if network_type_coin != "mainnet":
|
|
|
|
nodes += data
|
|
|
|
continue
|
|
|
|
|
|
|
|
if not data:
|
|
|
|
continue
|
|
|
|
|
|
|
|
# Filter out nodes affected by < v0.17.1.3 sybil attack
|
2020-12-22 19:53:47 +00:00
|
|
|
data = list(map(lambda _node: _node if _node['target_height'] <= _node['height']
|
|
|
|
else self._bad_node(**_node), data))
|
2020-12-22 18:03:48 +00:00
|
|
|
|
|
|
|
allowed_offset = 3
|
|
|
|
valid_heights = []
|
2020-12-22 19:53:47 +00:00
|
|
|
# current_blockheight = heights.get(network_type_coin, 0)
|
|
|
|
|
|
|
|
# popularity contest
|
|
|
|
common_height = popularity_contest([z['height'] for z in data])
|
2020-12-29 23:30:25 +00:00
|
|
|
valid_heights = range(common_height + allowed_offset, common_height - allowed_offset, -1)
|
2020-12-22 19:53:47 +00:00
|
|
|
|
|
|
|
data = list(map(lambda _node: _node if _node['height'] in valid_heights
|
|
|
|
else self._bad_node(**_node), data))
|
2020-12-22 18:03:48 +00:00
|
|
|
nodes += data
|
|
|
|
return nodes
|
|
|
|
|
|
|
|
async def node_check(self, node, network_type: str) -> dict:
|
|
|
|
"""Call /get_info on the RPC, return JSON"""
|
|
|
|
opts = {
|
|
|
|
"timeout": self._http_timeout,
|
|
|
|
"json": True
|
|
|
|
}
|
|
|
|
|
|
|
|
if network_type == "tor":
|
|
|
|
opts["socks5"] = settings.TOR_SOCKS_PROXY
|
|
|
|
opts["timeout"] = self._http_timeout_onion
|
|
|
|
|
|
|
|
blob = await httpget(f"http://{node}/get_info", **opts)
|
|
|
|
for expect in ["nettype", "height", "target_height"]:
|
|
|
|
if expect not in blob:
|
|
|
|
raise Exception(f"Invalid JSON response from RPC; expected key '{expect}'")
|
|
|
|
|
|
|
|
height = int(blob.get("height", 0))
|
|
|
|
target_height = int(blob.get("target_height", 0))
|
|
|
|
|
|
|
|
return {
|
|
|
|
"address": node,
|
|
|
|
"height": height,
|
|
|
|
"target_height": target_height,
|
|
|
|
"online": True,
|
|
|
|
"nettype": blob["nettype"],
|
|
|
|
"type": network_type
|
|
|
|
}
|
|
|
|
|
|
|
|
def _bad_node(self, **kwargs):
|
|
|
|
return {
|
|
|
|
"address": kwargs['address'],
|
|
|
|
"height": kwargs['height'],
|
|
|
|
"target_height": 0,
|
|
|
|
"online": False,
|
|
|
|
"nettype": kwargs['nettype'],
|
|
|
|
"type": kwargs['type']
|
|
|
|
}
|