Skip to content

Commit c749086

Browse files
committed
remove external binary deps on doublezero and solana
1 parent 1f7f9a5 commit c749086

File tree

8 files changed

+217
-134
lines changed

8 files changed

+217
-134
lines changed

README.md

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,12 @@ This will not trigger on minor DZ packet loss, only substantial failures in the
1515

1616
## Installation
1717

18-
Edit the `config.py` file to configure the parameters to your liking.
18+
You can run the script from an unpriviledged user account or as root.
19+
Sudo access to the `nft` and `ip` commands should be granted to use this as an
20+
unpriviledged user.
1921

20-
It is recommended that you run this script from the sol user account (assuming it also has
21-
access to the `doublezero` command line). Sudo access to the `nft` command
22-
should be granted to use this as an unpriviledged user. Running this in tmux/zellij
22+
Edit the `config.py` file to configure the parameters to your liking.
23+
Running this in tmux/zellij and monitoring the output
2324
is a viable way to test that the parameters are chosen correctly.
2425

2526
For permanent install it is recommended to have a systemd service configured to
@@ -29,15 +30,8 @@ A systemd unit `doublezero_monitor.service` is provided, install as appropriate
2930
sudo cp doublezero_monitor.service /etc/systemd/system/
3031
```
3132

32-
Keep in mind that when running as system service, the script will still need access to both `solana`
33-
and `doublezero` binaries to perform its function. To check, log in as a root user and verify
34-
that both commands can still be executed.
33+
Keep in mind that when running as system service, the script will run as root.
3534

36-
In addition, you should make the DZ config available to the root user as follows:
37-
```bash
38-
mkdir -p /root/.config/doublezero/
39-
ln -s /home/sol/.config/doublezero/cli /root/.config/doublezero/cli
40-
```
4135

4236
## For IBRL mode
4337

config.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,19 @@
1-
21
# Tunable parameters
32

4-
# Which cluster to connect to (fed to solana CLI)
5-
CLUSTER="testnet" # "mainnet-beta"
3+
# Which cluster to connect to (this is used to pick the correct RPC endpoint)
4+
CLUSTER = "testnet" # "mainnet-beta"
65

76
# how much stake do we need to observe to consider connection "good"
87
STAKE_THRESHOLD: float = 0.9
98
# How long do we accumulate packets before checking the counters
10-
PASSIVE_MONITORING_INTERVAL_SECONDS: float = 5.0
9+
PASSIVE_MONITORING_INTERVAL_SECONDS: float = 7.0
1110
# How long do we wait before consindering connection dead
12-
GRACE_PERIOD_SEC: float = 10.0
11+
GRACE_PERIOD_SEC: float = 14.0
1312
# Interval between refreshes of gossip tables via RPC
1413
NODE_REFRESH_INTERVAL_SECONDS: float = 60.0
1514

1615
# Path to the admin RPC socket of the validator
17-
ADMIN_RPC_PATH="/home/sol/ledger/admin.rpc"
16+
ADMIN_RPC_PATH = "/home/sol/ledger/admin.rpc"
1817

1918
# Parameters you should probably not tune
2019

doublezero.py

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,25 @@
11
import asyncio
22
import ipaddress
33

4+
45
async def doublezero_is_active() -> bool:
5-
CMD = "doublezero status".split(" ")
6+
CMD = "ip link show doublezero0 up".split(" ")
67
proc = await asyncio.create_subprocess_exec(
7-
*CMD,
8-
stdout=asyncio.subprocess.DEVNULL,
9-
stderr=asyncio.subprocess.DEVNULL
8+
*CMD, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.DEVNULL
109
)
11-
return await proc.wait() == 0
10+
out, _ = await proc.communicate()
11+
return b"UP" in out
12+
1213

13-
async def get_doublezero_routes()->set[ipaddress.IPv4Address]:
14+
async def get_doublezero_routes() -> set[ipaddress.IPv4Address]:
1415
CMD = "ip route show table main".split(" ")
1516
proc = await asyncio.create_subprocess_exec(
16-
*CMD,
17-
stdout=asyncio.subprocess.PIPE,
18-
stderr=asyncio.subprocess.DEVNULL
19-
)
17+
*CMD, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.DEVNULL
18+
)
2019
output, _ = await proc.communicate()
21-
reachable:set[ipaddress.IPv4Address] = set()
20+
reachable: set[ipaddress.IPv4Address] = set()
2221
for line in output.decode().splitlines():
23-
if 'dev doublezero0 proto bgp' in line:
24-
line = line.split(' ')[0]
22+
if "dev doublezero0 proto bgp" in line:
23+
line = line.split(" ")[0]
2524
reachable.add(ipaddress.IPv4Address(line))
2625
return reachable

helpers.py

Lines changed: 73 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,55 +4,96 @@
44
from traceback import print_exc
55
import json
66
import os
7+
from typing import Any
78
from config import *
9+
from urllib import request
10+
811
# Set sudo command to blank if in systemd (since then we are root)
912
SUDO = "" if os.geteuid() == 0 else "sudo "
1013

14+
1115
def nft_add_table():
12-
cmd=f"{SUDO}nft add table inet {NFT_TABLE}"
13-
subprocess.check_call(cmd.split(" "))
14-
cmd=f"{SUDO}nft add chain inet {NFT_TABLE} " + r" input { type filter hook input priority 0 \; }"
15-
subprocess.check_call(cmd, shell=True)
16+
cmd = f"{SUDO}nft add table inet {NFT_TABLE}"
17+
_ = subprocess.check_call(cmd.split(" "))
18+
cmd = (
19+
f"{SUDO}nft add chain inet {NFT_TABLE} "
20+
+ r" input { type filter hook input priority 0 \; }"
21+
)
22+
_ = subprocess.check_call(cmd, shell=True)
23+
1624

1725
def nft_drop_table():
18-
cmd=f"{SUDO}nft delete table inet {NFT_TABLE}"
19-
subprocess.call(cmd.split(" "))
26+
cmd = f"{SUDO}nft delete table inet {NFT_TABLE}"
27+
_ = subprocess.call(cmd.split(" "))
28+
2029

21-
def get_nft_counters()->dict[ipaddress.IPv4Address, int]:
22-
cmd=f"{SUDO}nft -j list chain inet {NFT_TABLE} input"
23-
counters:dict[ipaddress.IPv4Address, int] = {}
30+
def get_nft_counters() -> dict[ipaddress.IPv4Address, int]:
31+
cmd = f"{SUDO}nft -j list chain inet {NFT_TABLE} input"
32+
counters: dict[ipaddress.IPv4Address, int] = {}
2433
try:
2534
(_status, output) = subprocess.getstatusoutput(cmd)
2635
x = json.loads(output)
27-
for row in x['nftables']:
28-
if 'rule' not in row:
36+
for row in x["nftables"]:
37+
if "rule" not in row:
2938
continue
30-
expr = row['rule']['expr']
31-
source = ipaddress.IPv4Address(expr[0]['match']['right'])
32-
counters[source] = expr[1]['counter']['packets']
39+
expr = row["rule"]["expr"]
40+
source = ipaddress.IPv4Address(expr[0]["match"]["right"])
41+
counters[source] = expr[1]["counter"]["packets"]
3342
except:
3443
print_exc()
3544
finally:
3645
return counters
3746

38-
def nft_add_counter(ip:ipaddress.IPv4Address):
47+
48+
def nft_add_counter(ip: ipaddress.IPv4Address):
3949
cmd = f"{SUDO} nft add rule inet {NFT_TABLE} input ip saddr {ip} counter"
4050
(_status, _output) = subprocess.getstatusoutput(cmd)
4151

42-
async def get_staked_nodes() ->dict[str, int]:
43-
cmd = f"-u{CLUSTER} validators --output json"
44-
proc = await asyncio.create_subprocess_exec("solana", *cmd.split(" "), stdout=asyncio.subprocess.PIPE,
45-
stderr=asyncio.subprocess.PIPE)
46-
stdout, _stderr = await proc.communicate()
47-
output = json.loads(stdout)
48-
return {v['identityPubkey']:v['activatedStake'] for v in output["validators"] if v['activatedStake'] > MIN_STAKE_TO_CARE and not v['delinquent']}
49-
50-
async def get_contact_infos()->dict[str, ipaddress.IPv4Address]:
51-
cmd = f"-u{CLUSTER} gossip --output json"
52-
proc = await asyncio.create_subprocess_exec("solana", *cmd.split(" "), stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
53-
stdout, _stderr = await proc.communicate()
54-
output = json.loads(stdout)
55-
return {v['identityPubkey']:ipaddress.IPv4Address( v['ipAddress']) for v in output if 'tpuPort' in v}
56-
57-
def kill_dz_interface()->None:
58-
subprocess.call(f"{SUDO}ip link set doublezero0 down", shell=True)
52+
53+
async def get_staked_nodes() -> dict[str, int]:
54+
output = await get_from_RPC("getVoteAccounts")
55+
return {
56+
v["nodePubkey"]: v["activatedStake"]
57+
for v in output["current"]
58+
if v["activatedStake"] > MIN_STAKE_TO_CARE
59+
}
60+
61+
62+
async def get_contact_infos() -> dict[str, ipaddress.IPv4Address]:
63+
output = await get_from_RPC("getClusterNodes")
64+
return {
65+
v["pubkey"]: ipaddress.IPv4Address(v["tpuQuic"].split(":")[0])
66+
for v in output
67+
if v.get("tpuQuic") is not None
68+
}
69+
70+
71+
def kill_dz_interface() -> None:
72+
_ = subprocess.call(f"{SUDO}ip link set doublezero0 down", shell=True)
73+
74+
75+
async def get_from_RPC(method: str) -> Any:
76+
url = f"https://api.{CLUSTER}.solana.com"
77+
headers = {"Content-Type": "application/json"}
78+
data = json.dumps(
79+
{
80+
"jsonrpc": "2.0",
81+
"id": 1,
82+
"method": method,
83+
"params": [],
84+
}
85+
).encode()
86+
87+
loop = asyncio.get_event_loop()
88+
89+
def fetch() -> dict[str, Any]:
90+
req = request.Request(url, data=data, headers=headers, method="POST")
91+
with request.urlopen(req) as resp:
92+
return json.load(resp)
93+
94+
try:
95+
result = await loop.run_in_executor(None, fetch)
96+
return result["result"]
97+
except Exception as e:
98+
print(f"Could not fetch data from RPC, error {e}")
99+
return []

0 commit comments

Comments
 (0)