├── LICENSE
├── README.md
├── consensus.py
└── screenshots
├── Screenshot from 2022-04-27 06-18-53.jpg
├── Screenshot from 2022-04-27 08-40-32.jpg
├── Screenshot from 2022-04-27 08-53-01.jpg
├── Screenshot from 2022-04-27 09-05-23.jpg
├── Screenshot from 2022-04-27 09-08-25.jpg
├── Screenshot from 2022-04-27 09-14-03.png
├── Screenshot from 2022-04-27 10-36-59.png
├── Screenshot from 2022-05-13 03-42-04.png
├── Screenshot from 2022-05-13 03-51-03.png
└── Screenshot from 2022-05-13 04-44-48.png
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Northa
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # consensus
2 | A brief info of the state of the consensus
3 |
4 |
5 | Quick start:
6 |
7 | ```sh
8 | cd && git clone https://github.com/Northa/consensus.git && cd consensus
9 | python3 consensus.py
10 |
11 | ```
12 |
13 |
14 |
15 | Requirements:
16 |
17 | * Ubuntu 20.04
18 | * python3.8
19 | * pip3
20 | * For the correct work of the application you should configure RPC :26657 and REST :1317 endpoints.
21 | For example:
22 | ```REST = 'http://1.1.1.1:1317'```
23 | ```RPC = "http://1.1.1.1:26657"```
24 |
25 |
26 |
27 |
28 |
29 | Installing:
30 |
31 | #### Technically, the installation itself is cloning the repo and providing 2 variables
32 |
33 | ```sh
34 | $ cd && git clone https://github.com/Northa/consensus.git && cd consensus
35 | ```
36 |
37 | Next open consensus.py in editor and replace REST/RPC variables with an appropriate values.
38 | Example:
39 | ```REST = 'http://1.1.1.1:1317'```
40 | ```RPC = "http://1.1.1.1:26657"```
41 |
42 | Once configured you can run the app by following:
43 |
44 | ```$ python3 consensus.py ```
45 |
46 |
47 |
48 | Tested chains:
49 |
50 | - Evmos mainnet
51 | - Evmos testnet
52 | - Umee
53 | - Archway
54 | - Cosmic Horizon
55 | - Kyve
56 | - Kichain
57 | - Konstellation
58 | - Stargaze
59 | - AssetMantle
60 | - Pylons
61 | - Deweb
62 |
63 |
64 |
65 |
66 | 
67 |
--------------------------------------------------------------------------------
/consensus.py:
--------------------------------------------------------------------------------
1 | # Github https://github.com/Northa
2 |
3 | from urllib import request
4 | import math
5 | from sys import exit
6 | from json import loads
7 |
8 | ERR_MSG = f"\033[91m[ERR] API endpoint unreachable: api\n" \
9 | f"[ERR] Be sure you have enabled your API " \
10 | f"(you can enable this in your app.toml config file)\n" \
11 | f"Bugreports Discord: Yep++#9963\033[0m"
12 |
13 | # default ports
14 | REST = "http://127.0.0.1:1317"
15 | RPC = "http://127.0.0.1:26657"
16 |
17 |
18 | def handle_request(api: str, pattern: str):
19 | try:
20 |
21 | response = loads(request.urlopen(f"{api}/{pattern}").read())
22 | return response if response is not None else exit(ERR_MSG.replace('api', api))
23 |
24 | except Exception:
25 | exit(ERR_MSG.replace('api', api))
26 |
27 |
28 | def get_validator_votes():
29 |
30 | validator_votes = []
31 | votes = STATE['result']['round_state']['votes']
32 | height = STATE['result']['round_state']['height']
33 | step = STATE['result']['round_state']['step']
34 | chain = get_chain_id()
35 | for r_ound in votes:
36 | if float(r_ound['precommits_bit_array'].split('=')[-1].strip()) <= 0.66 and float(r_ound['precommits_bit_array'].split('=')[-1].strip()) > 0:
37 | print(F"\nChain-id: {chain}\n"
38 | f"Height: {height} Round: {r_ound['round']} "
39 | f"step: {step}\n"
40 | f"precommits_bit_array: {r_ound['precommits_bit_array'].split('} ')[-1]}")
41 |
42 | for precommit in r_ound['precommits']:
43 | try:
44 | validator_votes.append(precommit.split('@')[0].split(':')[1].split(' ')[0])
45 | except IndexError:
46 | validator_votes.append(precommit)
47 |
48 | elif float(r_ound['prevotes_bit_array'].split('=')[-1].strip()) <= 0.66 and float(r_ound['prevotes_bit_array'].split('=')[-1].strip()) > 0:
49 | print(F"\nChain-id: {chain}\nHeight: {height} Round: {r_ound['round']} step: {step}\nprevotes_bit_array: {r_ound['prevotes_bit_array'].split('} ')[-1]}")
50 |
51 | for precommit in r_ound['prevotes']:
52 | try:
53 | validator_votes.append(precommit.split('@')[0].split(':')[1].split(' ')[0])
54 | except IndexError:
55 | validator_votes.append(precommit)
56 |
57 | if len(validator_votes) == 0:
58 | commit_votes = STATE['result']['round_state']['last_commit']
59 | height = STATE['result']['round_state']['height']
60 |
61 | if float(commit_votes['votes_bit_array'].split('=')[-1].strip()) > 0.66 and float(commit_votes['votes_bit_array'].split('=')[-1].strip()) > 0:
62 | print(F"\nChain-id: {chain}\nHeight: {height} Round: {r_ound['round']} step: {step}\nvotes_bit_array: {commit_votes['votes_bit_array'].split('} ')[-1]}")
63 | for commit_vote in commit_votes['votes']:
64 |
65 | try:
66 | validator_votes.append(commit_vote.split('@')[0].split(':')[1].split(' ')[0])
67 | except IndexError:
68 | validator_votes.append(commit_vote)
69 |
70 | return validator_votes
71 |
72 |
73 | def get_validators():
74 | validators = []
75 | state_validators = STATE['result']['round_state']['validators']['validators']
76 | for val in state_validators:
77 | res = val['address'], val['voting_power'], val['pub_key']['value']
78 | validators.append(res)
79 | return validators
80 |
81 |
82 | def get_bonded():
83 | result = handle_request(REST, '/cosmos/staking/v1beta1/pool')['pool']
84 | return result
85 |
86 |
87 | def strip_emoji_non_ascii(moniker):
88 | # moniker = emoji.replace_emoji(moniker, replace='')
89 | moniker = "".join([letter for letter in moniker if letter.isascii()])
90 | return moniker[:15].strip().lstrip()
91 |
92 |
93 | def get_validators_rest():
94 | bonded_tokens = int(get_bonded()["bonded_tokens"])
95 | validator_dict = {}
96 | validators = handle_request(REST, '/staking/validators')["result"]
97 |
98 | for validator in validators:
99 | validator_vp = int(int(validator["tokens"]))
100 | vp_percentage = round((100 / bonded_tokens) * validator_vp, 3)
101 | moniker = validator["description"]["moniker"][:15].strip()
102 | moniker = strip_emoji_non_ascii(moniker)
103 | validator_dict[validator["consensus_pubkey"]["value"]] = {
104 | "moniker": moniker,
105 | "address": validator["operator_address"],
106 | "status": validator["status"],
107 | "voting_power": validator_vp,
108 | "voting_power_perc": f"{vp_percentage}%"}
109 |
110 | return validator_dict, len(validators)
111 |
112 |
113 | def merge_info():
114 | print('Bugreports discord: Yep++#9963')
115 | votes = get_validator_votes()
116 | validators = get_validators()
117 | votes_and_vals = list(zip(votes, validators))
118 | validator_rest, total_validators = get_validators_rest()
119 | final_list = []
120 |
121 | for k, v in votes_and_vals:
122 | if v[2] in validator_rest:
123 | validator_rest[v[2]]['voted'] = k
124 | final_list.append(validator_rest[v[2]])
125 |
126 | return final_list, total_validators
127 |
128 |
129 | def list_columns(obj, cols=3, columnwise=True, gap=8):
130 | # thnx to https://stackoverflow.com/a/36085705
131 |
132 | sobj = [str(item) for item in obj]
133 | if cols > len(sobj): cols = len(sobj)
134 | max_len = max([len(item) for item in sobj])
135 | if columnwise: cols = int(math.ceil(float(len(sobj)) / float(cols)))
136 | plist = [sobj[i: i+cols] for i in range(0, len(sobj), cols)]
137 | if columnwise:
138 | if not len(plist[-1]) == cols:
139 | plist[-1].extend(['']*(len(sobj) - len(plist[-1])))
140 | plist = zip(*plist)
141 | printer = '\n'.join([
142 | ''.join([c.ljust(max_len + gap) for c in p])
143 | for p in plist])
144 | return printer
145 |
146 |
147 | def get_chain_id():
148 | response = handle_request(REST, 'node_info')
149 | chain_id = response['node_info']['network']
150 | return chain_id
151 |
152 |
153 | def colorize_output(validators):
154 | result = []
155 | for num, val in enumerate(validators):
156 | vp_perc = f"{val['voting_power_perc']:<7}"
157 | moniker = val['moniker']
158 |
159 | if val['voted'] != 'nil-Vote':
160 | stat = f"\033[92m{num+1:<3} {'ONLINE':<8} \033[0m"
161 | result.append(f"{stat} {moniker:<18} {vp_perc}")
162 |
163 | else:
164 | stat = f"\033[91m{num+1:<3} {'OFFLINE':<8} \033[0m"
165 | result.append(f"{stat} {moniker:<18} {vp_perc}")
166 |
167 | return result
168 |
169 |
170 | def calculate_colums(result):
171 | if len(result) <= 30:
172 | return list_columns(result, cols=1)
173 | elif 30 < len(result) <= 100:
174 | return list_columns(result, cols=2)
175 | elif 100 < len(result) <= 150:
176 | return list_columns(result, cols=3)
177 | else:
178 | return list_columns(result, cols=4)
179 |
180 |
181 | def get_pubkey_by_valcons(valcons, height):
182 | response = handle_request(REST, f"/validatorsets/{height}")
183 | for validator in response['result']['validators']:
184 | if valcons in validator['address']:
185 | return validator['pub_key']['value']
186 |
187 |
188 | def get_moniker_by_pub_key(pub_key, height):
189 | response = handle_request(REST, f"cosmos/staking/v1beta1/historical_info/{height}")
190 | for validator in response['hist']['valset']:
191 |
192 | if pub_key in validator['consensus_pubkey']['key']:
193 | return strip_emoji_non_ascii(validator['description']['moniker'])
194 |
195 |
196 | def get_evidence(height):
197 | evidences = handle_request(REST, '/cosmos/evidence/v1beta1/evidence')
198 | for evidence in evidences['evidence']:
199 | if int(height) - int(evidence['height']) < 1000:
200 | pub_key = get_pubkey_by_valcons(evidence['consensus_address'], evidence['height']).strip()
201 | moniker = get_moniker_by_pub_key(pub_key, evidence['height'])
202 | # print(colored(f"Evidence: {moniker}\nHeight: {evidence['height']} {evidence['consensus_address']} power: {evidence['power']}\n", 'yellow'))
203 | print(f"\033[93mEvidence: {moniker}\nHeight: {evidence['height']} {evidence['consensus_address']} power: {evidence['power']}\033[0m\n")
204 |
205 |
206 | def main(STATE):
207 | validators, total_validators = merge_info()
208 |
209 | online_vals = 0
210 | for num, val in enumerate(validators):
211 | if val['voted'] != 'nil-Vote':
212 | online_vals += 1
213 |
214 | print(f"Online: {online_vals}/{total_validators}\n")
215 | get_evidence(STATE['result']['round_state']['height'])
216 | result = colorize_output(validators)
217 | print(calculate_colums(result))
218 |
219 |
220 | if __name__ == '__main__':
221 | STATE = handle_request(RPC, 'dump_consensus_state')
222 |
223 | exit(main(STATE))
224 |
--------------------------------------------------------------------------------
/screenshots/Screenshot from 2022-04-27 06-18-53.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NodesBlocks/node-consensus/5fc154c09dd93af01324cbed0a3551dbc5692588/screenshots/Screenshot from 2022-04-27 06-18-53.jpg
--------------------------------------------------------------------------------
/screenshots/Screenshot from 2022-04-27 08-40-32.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NodesBlocks/node-consensus/5fc154c09dd93af01324cbed0a3551dbc5692588/screenshots/Screenshot from 2022-04-27 08-40-32.jpg
--------------------------------------------------------------------------------
/screenshots/Screenshot from 2022-04-27 08-53-01.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NodesBlocks/node-consensus/5fc154c09dd93af01324cbed0a3551dbc5692588/screenshots/Screenshot from 2022-04-27 08-53-01.jpg
--------------------------------------------------------------------------------
/screenshots/Screenshot from 2022-04-27 09-05-23.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NodesBlocks/node-consensus/5fc154c09dd93af01324cbed0a3551dbc5692588/screenshots/Screenshot from 2022-04-27 09-05-23.jpg
--------------------------------------------------------------------------------
/screenshots/Screenshot from 2022-04-27 09-08-25.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NodesBlocks/node-consensus/5fc154c09dd93af01324cbed0a3551dbc5692588/screenshots/Screenshot from 2022-04-27 09-08-25.jpg
--------------------------------------------------------------------------------
/screenshots/Screenshot from 2022-04-27 09-14-03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NodesBlocks/node-consensus/5fc154c09dd93af01324cbed0a3551dbc5692588/screenshots/Screenshot from 2022-04-27 09-14-03.png
--------------------------------------------------------------------------------
/screenshots/Screenshot from 2022-04-27 10-36-59.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NodesBlocks/node-consensus/5fc154c09dd93af01324cbed0a3551dbc5692588/screenshots/Screenshot from 2022-04-27 10-36-59.png
--------------------------------------------------------------------------------
/screenshots/Screenshot from 2022-05-13 03-42-04.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NodesBlocks/node-consensus/5fc154c09dd93af01324cbed0a3551dbc5692588/screenshots/Screenshot from 2022-05-13 03-42-04.png
--------------------------------------------------------------------------------
/screenshots/Screenshot from 2022-05-13 03-51-03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NodesBlocks/node-consensus/5fc154c09dd93af01324cbed0a3551dbc5692588/screenshots/Screenshot from 2022-05-13 03-51-03.png
--------------------------------------------------------------------------------
/screenshots/Screenshot from 2022-05-13 04-44-48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NodesBlocks/node-consensus/5fc154c09dd93af01324cbed0a3551dbc5692588/screenshots/Screenshot from 2022-05-13 04-44-48.png
--------------------------------------------------------------------------------