@gabrielhicks/solv 5.6.5 → 5.6.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/monitoring/scripts/common.py +63 -0
- package/dist/cli/monitoring/scripts/measurement_validator_info.py +353 -0
- package/dist/cli/monitoring/scripts/output_validator_measurements.py +6 -0
- package/dist/cli/monitoring/scripts/request_utils.py +76 -0
- package/dist/cli/monitoring/scripts/solana_rpc.py +202 -0
- package/dist/index.js +160 -160
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from pprint import pprint
|
|
2
|
+
import json
|
|
3
|
+
import numpy
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ValidatorConfig:
|
|
8
|
+
def __init__(self,
|
|
9
|
+
validator_name: str,
|
|
10
|
+
secrets_path: str,
|
|
11
|
+
local_rpc_address: str,
|
|
12
|
+
remote_rpc_address: str,
|
|
13
|
+
cluster_environment: str,
|
|
14
|
+
debug_mode: bool):
|
|
15
|
+
self.validator_name = validator_name
|
|
16
|
+
self.secrets_path = secrets_path
|
|
17
|
+
self.local_rpc_address = local_rpc_address
|
|
18
|
+
self.remote_rpc_address = remote_rpc_address
|
|
19
|
+
self.cluster_environment = cluster_environment
|
|
20
|
+
self.debug_mode = debug_mode
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class JsonEncoder(json.JSONEncoder):
|
|
24
|
+
""" Special json encoder for numpy types """
|
|
25
|
+
def default(self, obj):
|
|
26
|
+
if isinstance(obj, (numpy.int_, numpy.intc, numpy.intp, numpy.int8,
|
|
27
|
+
numpy.int16, numpy.int32, numpy.int64, numpy.uint8,
|
|
28
|
+
numpy.uint16, numpy.uint32, numpy.uint64)):
|
|
29
|
+
return int(obj)
|
|
30
|
+
elif isinstance(obj, (numpy.float_, numpy.float16, numpy.float32,
|
|
31
|
+
numpy.float64)):
|
|
32
|
+
return float(obj)
|
|
33
|
+
elif isinstance(obj, (numpy.ndarray,)):
|
|
34
|
+
return obj.tolist()
|
|
35
|
+
return json.JSONEncoder.default(self, obj)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def debug(config: ValidatorConfig, data):
|
|
39
|
+
if config.debug_mode:
|
|
40
|
+
pprint(data)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def print_json(data):
|
|
44
|
+
print(json.dumps(data, cls=JsonEncoder))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def measurement_from_fields(name, data, tags, config, legacy_tags=None):
|
|
48
|
+
if legacy_tags is None:
|
|
49
|
+
legacy_tags = {}
|
|
50
|
+
data.update({"cluster_environment": config.cluster_environment})
|
|
51
|
+
measurement = {
|
|
52
|
+
"measurement": name,
|
|
53
|
+
"time": round(time.time() * 1000),
|
|
54
|
+
"monitoring_version": "3.2.0",
|
|
55
|
+
"cluster_environment": config.cluster_environment,
|
|
56
|
+
"fields": data,
|
|
57
|
+
"tags": tags
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
measurement.update(legacy_tags)
|
|
61
|
+
|
|
62
|
+
return measurement
|
|
63
|
+
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import solana_rpc as rpc
|
|
3
|
+
from common import debug
|
|
4
|
+
from common import ValidatorConfig
|
|
5
|
+
import statistics
|
|
6
|
+
import numpy as np
|
|
7
|
+
from common import measurement_from_fields
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_metrics_from_vote_account_item(item):
|
|
11
|
+
return {
|
|
12
|
+
'epoch_number': item['epochCredits'][-1][0],
|
|
13
|
+
'credits_epoch': item['epochCredits'][-1][1],
|
|
14
|
+
'credits_previous_epoch': item['epochCredits'][-1][2],
|
|
15
|
+
'activated_stake': item['activatedStake'],
|
|
16
|
+
'credits_epoch_delta': item['epochCredits'][-1][1] - item['epochCredits'][-1][2],
|
|
17
|
+
'commission': item['commission']
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def find_item_in_vote_accounts_section(identity_account_pubkey, section_parent, section_name):
|
|
22
|
+
if section_name in section_parent:
|
|
23
|
+
section = section_parent[section_name]
|
|
24
|
+
for item in section:
|
|
25
|
+
if item['nodePubkey'] == identity_account_pubkey:
|
|
26
|
+
return get_metrics_from_vote_account_item(item)
|
|
27
|
+
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_vote_account_metrics(vote_accounts_data, identity_account_pubkey):
|
|
32
|
+
"""
|
|
33
|
+
get vote metrics from vote account
|
|
34
|
+
:return:
|
|
35
|
+
voting_status: 0 if validator not found in voting accounts
|
|
36
|
+
voting_status: 1 if validator is current
|
|
37
|
+
voting_status: 2 if validator is delinquent
|
|
38
|
+
|
|
39
|
+
"""
|
|
40
|
+
result = find_item_in_vote_accounts_section(identity_account_pubkey, vote_accounts_data, 'current')
|
|
41
|
+
if result is not None:
|
|
42
|
+
result.update({'voting_status': 1})
|
|
43
|
+
else:
|
|
44
|
+
result = find_item_in_vote_accounts_section(identity_account_pubkey, vote_accounts_data, 'delinquent')
|
|
45
|
+
if result is not None:
|
|
46
|
+
result.update({'voting_status': 2})
|
|
47
|
+
else:
|
|
48
|
+
result = {'voting_status': 0}
|
|
49
|
+
return result
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def get_leader_schedule_metrics(leader_schedule_data, identity_account_pubkey):
|
|
53
|
+
"""
|
|
54
|
+
get metrics about leader slots
|
|
55
|
+
"""
|
|
56
|
+
if identity_account_pubkey in leader_schedule_data:
|
|
57
|
+
return {"leader_slots_this_epoch": len(leader_schedule_data[identity_account_pubkey])}
|
|
58
|
+
else:
|
|
59
|
+
return {"leader_slots_this_epoch": 0}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def get_block_production_metrics(block_production_data, identity_account_pubkey):
|
|
63
|
+
try:
|
|
64
|
+
item = block_production_data['value']['byIdentity'][identity_account_pubkey]
|
|
65
|
+
return {
|
|
66
|
+
"slots_done": item[0],
|
|
67
|
+
"slots_skipped": item[0] - item[1],
|
|
68
|
+
"blocks_produced": item[1]
|
|
69
|
+
|
|
70
|
+
}
|
|
71
|
+
except:
|
|
72
|
+
return {"slots_done": 0, "slots_skipped": 0, "blocks_produced": 0}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def get_block_production_cli_metrics(block_production_data_cli, identity_account_pubkey: str):
|
|
76
|
+
if 'leaders' in block_production_data_cli:
|
|
77
|
+
leaders = block_production_data_cli['leaders']
|
|
78
|
+
skip_rate = []
|
|
79
|
+
my_skip_rate = 0
|
|
80
|
+
for leader in leaders:
|
|
81
|
+
leader_slots = leader.get('leaderSlots', 0)
|
|
82
|
+
if leader_slots > 0:
|
|
83
|
+
current_skip_rate = leader.get('skippedSlots', 0) / leader_slots
|
|
84
|
+
skip_rate.append(current_skip_rate)
|
|
85
|
+
if leader['identityPubkey'] == identity_account_pubkey:
|
|
86
|
+
my_skip_rate = current_skip_rate
|
|
87
|
+
|
|
88
|
+
result = {
|
|
89
|
+
'leader_skip_rate': my_skip_rate,
|
|
90
|
+
'cluster_min_leader_skip_rate': min(skip_rate),
|
|
91
|
+
'cluster_max_leader_skip_rate': max(skip_rate),
|
|
92
|
+
'cluster_mean_leader_skip_rate': statistics.mean(skip_rate),
|
|
93
|
+
'cluster_median_leader_skip_rate': statistics.median(skip_rate),
|
|
94
|
+
}
|
|
95
|
+
else:
|
|
96
|
+
result = {}
|
|
97
|
+
|
|
98
|
+
return result
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def get_performance_metrics(performance_sample_data, epoch_info_data, leader_schedule_by_identity):
|
|
102
|
+
if len(performance_sample_data) > 0:
|
|
103
|
+
sample = performance_sample_data[0]
|
|
104
|
+
if sample['numSlots'] > 0:
|
|
105
|
+
mid_slot_time = sample['samplePeriodSecs'] / sample['numSlots']
|
|
106
|
+
else:
|
|
107
|
+
mid_slot_time = 0
|
|
108
|
+
current_slot_index = epoch_info_data['slotIndex']
|
|
109
|
+
remaining_time = (epoch_info_data["slotsInEpoch"] - current_slot_index) * mid_slot_time
|
|
110
|
+
epoch_end_time = round(time.time()) + remaining_time
|
|
111
|
+
time_until_next_slot = -1
|
|
112
|
+
if leader_schedule_by_identity is not None:
|
|
113
|
+
for slot in leader_schedule_by_identity:
|
|
114
|
+
if current_slot_index < slot:
|
|
115
|
+
next_slot = slot
|
|
116
|
+
time_until_next_slot = (next_slot - current_slot_index) * mid_slot_time
|
|
117
|
+
break
|
|
118
|
+
else:
|
|
119
|
+
time_until_next_slot = None
|
|
120
|
+
|
|
121
|
+
result = {
|
|
122
|
+
"epoch_endtime": epoch_end_time,
|
|
123
|
+
"epoch_remaining_sec": remaining_time
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if time_until_next_slot is not None:
|
|
127
|
+
result.update({"time_until_next_slot": time_until_next_slot})
|
|
128
|
+
else:
|
|
129
|
+
result = {}
|
|
130
|
+
|
|
131
|
+
return result
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def get_balance_metric(balance_data, key: str):
|
|
135
|
+
if 'value' in balance_data:
|
|
136
|
+
result = {key: balance_data['value']}
|
|
137
|
+
else:
|
|
138
|
+
result = {}
|
|
139
|
+
|
|
140
|
+
return result
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def get_solana_version_metric(solana_version_data):
|
|
144
|
+
if solana_version_data is not None:
|
|
145
|
+
if 'solana-core' in solana_version_data:
|
|
146
|
+
return {'solana_version': solana_version_data['solana-core']}
|
|
147
|
+
|
|
148
|
+
return {}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def get_validators_metric(validators, identity_account_pubkey):
|
|
152
|
+
if validators is not None:
|
|
153
|
+
epoch_credits_l = []
|
|
154
|
+
last_vote_l = []
|
|
155
|
+
root_slot_l = []
|
|
156
|
+
current_last_vote = -1
|
|
157
|
+
current_root_slot = -1
|
|
158
|
+
|
|
159
|
+
for v in validators:
|
|
160
|
+
if not v['delinquent']:
|
|
161
|
+
epoch_credits_l.append(v['epochCredits'])
|
|
162
|
+
last_vote_l.append(v['lastVote'])
|
|
163
|
+
root_slot_l.append(v['rootSlot'])
|
|
164
|
+
if identity_account_pubkey == v['identityPubkey']:
|
|
165
|
+
current_last_vote = v['lastVote']
|
|
166
|
+
current_root_slot = v['rootSlot']
|
|
167
|
+
|
|
168
|
+
epoch_credits = np.array(epoch_credits_l, dtype=np.int32)
|
|
169
|
+
last_vote = np.array(last_vote_l, dtype=np.int32)
|
|
170
|
+
root_slot = np.array(root_slot_l, dtype=np.int32)
|
|
171
|
+
|
|
172
|
+
last_vote = last_vote[last_vote > 0]
|
|
173
|
+
root_slot = root_slot[root_slot > 0]
|
|
174
|
+
|
|
175
|
+
cluster_max_last_vote = np.amax(last_vote)
|
|
176
|
+
cluster_min_last_vote = np.amin(last_vote)
|
|
177
|
+
cluster_mean_last_vote = abs((last_vote - cluster_max_last_vote).mean())
|
|
178
|
+
cluster_median_last_vote = abs(np.median(last_vote - cluster_max_last_vote))
|
|
179
|
+
|
|
180
|
+
cluster_max_root_slot = np.amax(root_slot)
|
|
181
|
+
cluster_min_root_slot = np.amin(root_slot)
|
|
182
|
+
cluster_mean_root_slot = abs((root_slot - cluster_max_root_slot).mean())
|
|
183
|
+
cluster_median_root_slot = abs(np.median(root_slot - cluster_max_root_slot))
|
|
184
|
+
|
|
185
|
+
result = {
|
|
186
|
+
'cluster_mean_epoch_credits': epoch_credits.mean(),
|
|
187
|
+
'cluster_min_epoch_credits': np.amin(epoch_credits),
|
|
188
|
+
'cluster_max_epoch_credits': np.amax(epoch_credits),
|
|
189
|
+
'cluster_median_epoch_credits': np.median(epoch_credits),
|
|
190
|
+
|
|
191
|
+
'cluster_max_last_vote': cluster_max_last_vote,
|
|
192
|
+
'cluster_min_last_vote_v2': cluster_min_last_vote,
|
|
193
|
+
'cluster_mean_last_vote_v2': cluster_mean_last_vote,
|
|
194
|
+
'cluster_median_last_vote': cluster_median_last_vote,
|
|
195
|
+
'current_last_vote': current_last_vote,
|
|
196
|
+
|
|
197
|
+
'cluster_max_root_slot': cluster_max_root_slot,
|
|
198
|
+
'cluster_min_root_slot_v2': cluster_min_root_slot,
|
|
199
|
+
'cluster_mean_root_slot_v2': cluster_mean_root_slot,
|
|
200
|
+
'cluster_median_root_slot': cluster_median_root_slot,
|
|
201
|
+
'current_root_slot': current_root_slot
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
else:
|
|
205
|
+
result = {}
|
|
206
|
+
|
|
207
|
+
return result
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def get_current_stake_metric(stake_data):
|
|
211
|
+
active = 0
|
|
212
|
+
activating = 0
|
|
213
|
+
deactivating = 0
|
|
214
|
+
active_cnt = 0
|
|
215
|
+
activating_cnt = 0
|
|
216
|
+
deactivating_cnt = 0
|
|
217
|
+
for item in stake_data:
|
|
218
|
+
if 'activeStake' in item:
|
|
219
|
+
active = active + item.get('activeStake', 0)
|
|
220
|
+
active_cnt = active_cnt + 1
|
|
221
|
+
if 'activatingStake' in item:
|
|
222
|
+
activating = activating + item.get('activatingStake', 0)
|
|
223
|
+
activating_cnt = activating_cnt + 1
|
|
224
|
+
if 'deactivatingStake' in item:
|
|
225
|
+
deactivating = deactivating + item.get('deactivatingStake', 0)
|
|
226
|
+
deactivating_cnt = deactivating_cnt + 1
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
'active_stake': active,
|
|
230
|
+
'activating_stake': activating,
|
|
231
|
+
'deactivating_stake': deactivating,
|
|
232
|
+
'stake_holders': len(stake_data),
|
|
233
|
+
'active_cnt': active_cnt,
|
|
234
|
+
'activating_cnt': activating_cnt,
|
|
235
|
+
'deactivating_cnt': deactivating_cnt
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def load_data(config: ValidatorConfig):
|
|
240
|
+
identity_account_pubkey = rpc.load_identity_account_pubkey(config)
|
|
241
|
+
vote_account_pubkey = rpc.load_vote_account_pubkey(config)
|
|
242
|
+
|
|
243
|
+
epoch_info_data = rpc.load_epoch_info(config)
|
|
244
|
+
block_production_cli = rpc.load_block_production_cli(config)
|
|
245
|
+
performance_sample_data = rpc.load_recent_performance_sample(config)
|
|
246
|
+
solana_version_data = rpc.load_solana_version(config)
|
|
247
|
+
validators_data = rpc.load_solana_validators(config)
|
|
248
|
+
|
|
249
|
+
default = []
|
|
250
|
+
|
|
251
|
+
identity_account_balance_data = default
|
|
252
|
+
leader_schedule_data = default
|
|
253
|
+
block_production_data = default
|
|
254
|
+
|
|
255
|
+
vote_account_balance_data = default
|
|
256
|
+
vote_accounts_data = default
|
|
257
|
+
stakes_data = default
|
|
258
|
+
|
|
259
|
+
if identity_account_pubkey is not None:
|
|
260
|
+
identity_account_balance_data = rpc.load_identity_account_balance(config, identity_account_pubkey)
|
|
261
|
+
leader_schedule_data = rpc.load_leader_schedule(config, identity_account_pubkey)
|
|
262
|
+
block_production_data = rpc.load_block_production(config, identity_account_pubkey)
|
|
263
|
+
|
|
264
|
+
if vote_account_pubkey is not None:
|
|
265
|
+
vote_account_balance_data = rpc.load_vote_account_balance(config, vote_account_pubkey)
|
|
266
|
+
vote_accounts_data = rpc.load_vote_accounts(config, vote_account_pubkey)
|
|
267
|
+
stakes_data = rpc.load_stakes(config, vote_account_pubkey)
|
|
268
|
+
|
|
269
|
+
result = {
|
|
270
|
+
'identity_account_pubkey': identity_account_pubkey,
|
|
271
|
+
'vote_account_pubkey': vote_account_pubkey,
|
|
272
|
+
'identity_account_balance': identity_account_balance_data,
|
|
273
|
+
'vote_account_balance': vote_account_balance_data,
|
|
274
|
+
'epoch_info': epoch_info_data,
|
|
275
|
+
'leader_schedule': leader_schedule_data,
|
|
276
|
+
'block_production': block_production_data,
|
|
277
|
+
'load_block_production_cli': block_production_cli,
|
|
278
|
+
'vote_accounts': vote_accounts_data,
|
|
279
|
+
'performance_sample': performance_sample_data,
|
|
280
|
+
'solana_version_data': solana_version_data,
|
|
281
|
+
'stakes_data': stakes_data,
|
|
282
|
+
'validators_data': validators_data,
|
|
283
|
+
'cpu_model': rpc.load_cpu_model(config)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
debug(config, str(result))
|
|
287
|
+
|
|
288
|
+
return result
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def calculate_influx_fields(data):
|
|
292
|
+
if data is None:
|
|
293
|
+
result = {"validator_status": 0}
|
|
294
|
+
else:
|
|
295
|
+
identity_account_pubkey = data['identity_account_pubkey']
|
|
296
|
+
|
|
297
|
+
vote_account_metrics = get_vote_account_metrics(data['vote_accounts'], identity_account_pubkey)
|
|
298
|
+
leader_schedule_metrics = get_leader_schedule_metrics(data['leader_schedule'], identity_account_pubkey)
|
|
299
|
+
epoch_metrics = data['epoch_info']
|
|
300
|
+
block_production_metrics = get_block_production_metrics(data['block_production'], identity_account_pubkey)
|
|
301
|
+
if identity_account_pubkey in data['leader_schedule']:
|
|
302
|
+
leader_schedule_by_identity = data['leader_schedule'][identity_account_pubkey]
|
|
303
|
+
else:
|
|
304
|
+
leader_schedule_by_identity = None
|
|
305
|
+
|
|
306
|
+
performance_metrics = get_performance_metrics(
|
|
307
|
+
data['performance_sample'], epoch_metrics, leader_schedule_by_identity)
|
|
308
|
+
|
|
309
|
+
result = {"validator_status": 1}
|
|
310
|
+
result.update(vote_account_metrics)
|
|
311
|
+
result.update(leader_schedule_metrics)
|
|
312
|
+
result.update(epoch_metrics)
|
|
313
|
+
result.update(block_production_metrics)
|
|
314
|
+
result.update(performance_metrics)
|
|
315
|
+
result.update(get_balance_metric(data['identity_account_balance'], 'identity_account_balance'))
|
|
316
|
+
result.update(get_balance_metric(data['vote_account_balance'], 'vote_account_balance'))
|
|
317
|
+
result.update(get_current_stake_metric(data['stakes_data']))
|
|
318
|
+
result.update(get_validators_metric(data['validators_data'], identity_account_pubkey))
|
|
319
|
+
result.update(get_block_production_cli_metrics(data['load_block_production_cli'], identity_account_pubkey))
|
|
320
|
+
result.update({"cpu_model": data['cpu_model']})
|
|
321
|
+
|
|
322
|
+
return result
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def calculate_output_data(config: ValidatorConfig):
|
|
326
|
+
data = load_data(config)
|
|
327
|
+
|
|
328
|
+
tags = {
|
|
329
|
+
"validator_identity_pubkey": data['identity_account_pubkey'],
|
|
330
|
+
"validator_vote_pubkey": data['vote_account_pubkey'],
|
|
331
|
+
"validator_name": config.validator_name,
|
|
332
|
+
"cluster_environment": config.cluster_environment
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
legacy_tags = {
|
|
336
|
+
"validator_identity_pubkey": data['identity_account_pubkey'],
|
|
337
|
+
"validator_vote_pubkey": data['vote_account_pubkey'],
|
|
338
|
+
"validator_name": config.validator_name,
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
measurement = measurement_from_fields(
|
|
342
|
+
"validators_info",
|
|
343
|
+
calculate_influx_fields(data),
|
|
344
|
+
tags,
|
|
345
|
+
config,
|
|
346
|
+
legacy_tags
|
|
347
|
+
)
|
|
348
|
+
measurement.update({"cpu_model": data['cpu_model']})
|
|
349
|
+
if data is not None and 'solana_version_data' in data:
|
|
350
|
+
measurement.update(get_solana_version_metric(data['solana_version_data']))
|
|
351
|
+
|
|
352
|
+
return measurement
|
|
353
|
+
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from common import ValidatorConfig
|
|
2
|
+
import subprocess
|
|
3
|
+
import requests
|
|
4
|
+
import json
|
|
5
|
+
from common import debug
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def execute_cmd_str(config: ValidatorConfig, cmd: str, convert_to_json: bool, default=None):
|
|
9
|
+
"""
|
|
10
|
+
executes shell command and return string result
|
|
11
|
+
:param default:
|
|
12
|
+
:param config:
|
|
13
|
+
:param convert_to_json:
|
|
14
|
+
:param cmd: shell command
|
|
15
|
+
:return: returns string result or None
|
|
16
|
+
"""
|
|
17
|
+
try:
|
|
18
|
+
debug(config, cmd)
|
|
19
|
+
result: str = subprocess.check_output(cmd, shell=True, stderr=subprocess.DEVNULL, timeout=10).decode().strip()
|
|
20
|
+
|
|
21
|
+
if convert_to_json:
|
|
22
|
+
result = json.loads(result)
|
|
23
|
+
|
|
24
|
+
debug(config, result)
|
|
25
|
+
|
|
26
|
+
return result
|
|
27
|
+
except:
|
|
28
|
+
return default
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def rpc_call(config: ValidatorConfig, address: str, method: str, params, error_result, except_result):
|
|
32
|
+
"""
|
|
33
|
+
calls solana rpc (https://docs.solana.com/developing/clients/jsonrpc-api)
|
|
34
|
+
and returns result or default
|
|
35
|
+
:param config:
|
|
36
|
+
:param except_result:
|
|
37
|
+
:param error_result:
|
|
38
|
+
:param address: local or remote rpc server address
|
|
39
|
+
:param method: rpc method
|
|
40
|
+
:param params: rpc call parameters
|
|
41
|
+
:return: result or default
|
|
42
|
+
"""
|
|
43
|
+
try:
|
|
44
|
+
json_request = {
|
|
45
|
+
"jsonrpc": "2.0",
|
|
46
|
+
"id": 1,
|
|
47
|
+
"method": method,
|
|
48
|
+
"params": params
|
|
49
|
+
}
|
|
50
|
+
debug(config, json_request)
|
|
51
|
+
debug(config, address)
|
|
52
|
+
|
|
53
|
+
json_response = requests.post(address, json=json_request).json()
|
|
54
|
+
if 'result' not in json_response:
|
|
55
|
+
result = error_result
|
|
56
|
+
else:
|
|
57
|
+
result = json_response['result']
|
|
58
|
+
except:
|
|
59
|
+
result = except_result
|
|
60
|
+
|
|
61
|
+
debug(config, result)
|
|
62
|
+
|
|
63
|
+
return result
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def smart_rpc_call(config: ValidatorConfig, method: str, params, default_result):
|
|
67
|
+
"""
|
|
68
|
+
tries to call local rpc, if it fails tries to call remote rpc
|
|
69
|
+
"""
|
|
70
|
+
result = rpc_call(config, config.local_rpc_address, method, params, None, None)
|
|
71
|
+
|
|
72
|
+
if result is None:
|
|
73
|
+
result = rpc_call(config, config.remote_rpc_address, method, params, default_result, default_result)
|
|
74
|
+
|
|
75
|
+
return result
|
|
76
|
+
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
from common import ValidatorConfig
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from common import debug
|
|
4
|
+
from request_utils import execute_cmd_str, smart_rpc_call, rpc_call
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def load_identity_account_pubkey(config: ValidatorConfig) -> Optional[str]:
|
|
8
|
+
"""
|
|
9
|
+
loads validator identity account pubkey
|
|
10
|
+
:param config: Validator Configuration
|
|
11
|
+
:return: returns validator identity pubkey or None
|
|
12
|
+
"""
|
|
13
|
+
identity_cmd = f'solana address -u localhost --keypair ' + config.secrets_path + '/validator-keypair.json'
|
|
14
|
+
debug(config, identity_cmd)
|
|
15
|
+
return execute_cmd_str(config, identity_cmd, convert_to_json=False)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def load_vote_account_pubkey(config: ValidatorConfig) -> Optional[str]:
|
|
19
|
+
"""
|
|
20
|
+
loads vote account pubkey
|
|
21
|
+
:param config: Validator Configuration
|
|
22
|
+
:return: returns vote account pubkey or None
|
|
23
|
+
"""
|
|
24
|
+
vote_pubkey_cmd = f'solana address -u localhost --keypair ' + config.secrets_path + '/vote-account-keypair.json'
|
|
25
|
+
debug(config, vote_pubkey_cmd)
|
|
26
|
+
return execute_cmd_str(config, vote_pubkey_cmd, convert_to_json=False)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def load_vote_account_balance(config: ValidatorConfig, vote_account_pubkey: str):
|
|
30
|
+
"""
|
|
31
|
+
loads vote account balance
|
|
32
|
+
https://docs.solana.com/developing/clients/jsonrpc-api#getbalance
|
|
33
|
+
"""
|
|
34
|
+
return smart_rpc_call(config, "getBalance", [vote_account_pubkey], {})
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def load_identity_account_balance(config: ValidatorConfig, identity_account_pubkey: str):
|
|
38
|
+
"""
|
|
39
|
+
loads identity account balance
|
|
40
|
+
https://docs.solana.com/developing/clients/jsonrpc-api#getbalance
|
|
41
|
+
"""
|
|
42
|
+
return smart_rpc_call(config, "getBalance", [identity_account_pubkey], {})
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def load_epoch_info(config: ValidatorConfig):
|
|
46
|
+
"""
|
|
47
|
+
loads epoch info
|
|
48
|
+
https://docs.solana.com/developing/clients/jsonrpc-api#getepochinfo
|
|
49
|
+
"""
|
|
50
|
+
return smart_rpc_call(config, "getEpochInfo", [], {})
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def load_leader_schedule(config: ValidatorConfig, identity_account_pubkey: str):
|
|
54
|
+
"""
|
|
55
|
+
loads leader schedule
|
|
56
|
+
https://docs.solana.com/developing/clients/jsonrpc-api#getleaderschedule
|
|
57
|
+
"""
|
|
58
|
+
params = [
|
|
59
|
+
None,
|
|
60
|
+
{
|
|
61
|
+
'identity': identity_account_pubkey
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
return smart_rpc_call(config, "getLeaderSchedule", params, {})
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def load_block_production(config: ValidatorConfig, identity_account_pubkey: str):
|
|
68
|
+
"""
|
|
69
|
+
loads block production
|
|
70
|
+
https://docs.solana.com/developing/clients/jsonrpc-api#getblockproduction
|
|
71
|
+
"""
|
|
72
|
+
params = [
|
|
73
|
+
{
|
|
74
|
+
'identity': identity_account_pubkey
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
return smart_rpc_call(config, "getBlockProduction", params, {})
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def load_block_production_cli(config: ValidatorConfig):
|
|
81
|
+
cmd = f'solana block-production -u l --output json-compact'
|
|
82
|
+
return execute_cmd_str(config, cmd, convert_to_json=True, default={})
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def load_vote_accounts(config: ValidatorConfig, vote_account_pubkey: str):
|
|
86
|
+
"""
|
|
87
|
+
loads block production
|
|
88
|
+
https://docs.solana.com/developing/clients/jsonrpc-api#getvoteaccounts
|
|
89
|
+
"""
|
|
90
|
+
params = [
|
|
91
|
+
{
|
|
92
|
+
'votePubkey': vote_account_pubkey
|
|
93
|
+
}
|
|
94
|
+
]
|
|
95
|
+
return smart_rpc_call(config, "getVoteAccounts", params, {})
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def load_recent_performance_sample(config: ValidatorConfig):
|
|
99
|
+
"""
|
|
100
|
+
loads recent performance sample
|
|
101
|
+
https://docs.solana.com/developing/clients/jsonrpc-api#getrecentperformancesamples
|
|
102
|
+
"""
|
|
103
|
+
params = [1]
|
|
104
|
+
return rpc_call(config, config.remote_rpc_address, "getRecentPerformanceSamples", params, [], [])
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def load_solana_version(config: ValidatorConfig):
|
|
108
|
+
"""
|
|
109
|
+
loads solana version
|
|
110
|
+
https://docs.solana.com/developing/clients/jsonrpc-api#getversion
|
|
111
|
+
"""
|
|
112
|
+
return rpc_call(config, config.local_rpc_address, "getVersion", [], [], [])
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def load_stake_account_rewards(config: ValidatorConfig, stake_account):
|
|
116
|
+
cmd = f'solana stake-account ' + stake_account + ' --num-rewards-epochs=1 --with-rewards --output json-compact'
|
|
117
|
+
return execute_cmd_str(config, cmd, convert_to_json=True)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def load_solana_validators(config: ValidatorConfig):
|
|
121
|
+
cmd = f'solana validators -ul --output json-compact'
|
|
122
|
+
data = execute_cmd_str(config, cmd, convert_to_json=True)
|
|
123
|
+
|
|
124
|
+
if (data is not None) and ('validators' in data):
|
|
125
|
+
return data['validators']
|
|
126
|
+
else:
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def load_stakes(config: ValidatorConfig, vote_account):
|
|
131
|
+
cmd = f'solana stakes ' + vote_account + ' --output json-compact'
|
|
132
|
+
return execute_cmd_str(config, cmd, convert_to_json=True, default=[])
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def load_block_time(config: ValidatorConfig, block):
|
|
136
|
+
"""
|
|
137
|
+
loads solana version
|
|
138
|
+
https://docs.solana.com/developing/clients/jsonrpc-api#getblocktime
|
|
139
|
+
"""
|
|
140
|
+
params = [block]
|
|
141
|
+
return rpc_call(config, config.local_rpc_address, "getBlockTime", params, None, None)
|
|
142
|
+
|
|
143
|
+
# cmd = f'solana block-time -u l ' + str(block) + ' --output json-compact'
|
|
144
|
+
# return execute_cmd_str(cmd, convert_to_json=True)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def try_to_load_current_block_info(config: ValidatorConfig):
|
|
148
|
+
epoch_info_data = load_epoch_info(config)
|
|
149
|
+
|
|
150
|
+
if epoch_info_data is not None:
|
|
151
|
+
slot_index = epoch_info_data['slotIndex']
|
|
152
|
+
absolute_slot = epoch_info_data['absoluteSlot']
|
|
153
|
+
|
|
154
|
+
block_time_data = load_block_time(config, absolute_slot)
|
|
155
|
+
|
|
156
|
+
if block_time_data is not None:
|
|
157
|
+
return {
|
|
158
|
+
'slot_index': slot_index,
|
|
159
|
+
'absolute_block': absolute_slot,
|
|
160
|
+
'block_time': block_time_data['timestamp']
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def load_current_block_info(config: ValidatorConfig):
|
|
167
|
+
result = None
|
|
168
|
+
max_tries = 10
|
|
169
|
+
current_try = 0
|
|
170
|
+
while result is None and current_try < max_tries:
|
|
171
|
+
result = try_to_load_current_block_info(config)
|
|
172
|
+
current_try = current_try + 1
|
|
173
|
+
|
|
174
|
+
return result
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def load_cpu_model(config: ValidatorConfig):
|
|
178
|
+
cmd = 'cat /proc/cpuinfo | grep name| uniq'
|
|
179
|
+
cpu_info = execute_cmd_str(config, cmd, False).split(":")
|
|
180
|
+
cpu_model = cpu_info[1].strip()
|
|
181
|
+
|
|
182
|
+
if cpu_model is not None:
|
|
183
|
+
return cpu_model
|
|
184
|
+
else:
|
|
185
|
+
return 'Unknown'
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def load_solana_validators_full(config: ValidatorConfig):
|
|
189
|
+
cmd = f'solana validators -ul --output json-compact'
|
|
190
|
+
return execute_cmd_str(config, cmd, convert_to_json=True)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def load_solana_validators_info(config: ValidatorConfig):
|
|
194
|
+
cmd = f'solana validator-info get --url ' + config.remote_rpc_address + ' --output json-compact'
|
|
195
|
+
data = execute_cmd_str(config, cmd, convert_to_json=True)
|
|
196
|
+
return data
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def load_solana_gossip(config: ValidatorConfig):
|
|
200
|
+
cmd = f'solana gossip -ul --output json-compact'
|
|
201
|
+
return execute_cmd_str(config, cmd, convert_to_json=True)
|
|
202
|
+
|