├── 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 | ![Example](https://github.com/Northa/consensus/blob/main/screenshots/Screenshot%20from%202022-04-27%2008-53-01.jpg?raw=true "EX") 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 --------------------------------------------------------------------------------