You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
wowlet-backend/wowlet_backend/tasks/rpc_nodes.py

119 lines
3.9 KiB

# 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 wowlet_backend.utils import httpget, popularity_contest
from wowlet_backend.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"""
from wowlet_backend.factory import app, cache
try:
heights = json.loads(await cache.get("blockheights"))
except:
heights = {}
rpc_nodes = await self.cache_json_get("nodes")
if not rpc_nodes:
rpc_nodes = {}
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
}, reason="unreachable"))
# 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
# data = list(map(lambda _node: _node if _node['target_height'] <= _node['height']
# else self._bad_node(_node, reason="+2_attack"), data))
allowed_offset = 5
valid_heights = []
# current_blockheight = heights.get(network_type_coin, 0)
# popularity contest
common_height = popularity_contest([z['height'] for z in data if z['height'] != 0])
valid_heights = range(common_height + allowed_offset, common_height - allowed_offset, -1)
data = list(map(lambda _node: _node if _node['height'] in valid_heights
else self._bad_node(_node, reason="out_of_sync"), data))
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, node: dict, reason=""):
return {
"address": node['address'],
"height": node['height'],
"target_height": 0,
"online": False,
"nettype": node['nettype'],
"type": node['type'],
"reason": reason
}