Skip to content
Snippets Groups Projects
Commit 3ecdbece authored by Miguel Montes's avatar Miguel Montes
Browse files

Commit inicial de bfa.py

parent f5c97fb4
No related branches found
No related tags found
No related merge requests found
# `bfa.py`
Script en Python que reporta distintos datos sobre la BFA
Este _script_ requiere Python 3 y la biblioteca web3 (que puede instalarse con `pip`).
Para funcionar requiere conocer el directorio donde está el _socket_ `geth.ipc` (asume que es `${BFANETWORKDIR}/node/geth.ipc` o, en su defecto `~/bfa/network/node/geth.ipc`), y el puerto RPC (asume que es el 8545). Ambos valores pueden especificarse en la línea de comandos.
También puede usarse como biblioteca. La clase BFA tiene un conjunto de métodos que permiten obtener información de un nodo `geth`.
```
bfa@bootnode:\~$ PS1="bfa@bootnode:~$ "
bfa@bootnode:~$ PATH=.:$PATH
bfa@bootnode:~$ bfa.py
usage: bfa.py [-h] {signers,sealers,votes,proposals,block} ...
bfa.py: error: the following arguments are required: command
bfa@bootnode:~$ bfa.py --help
usage: bfa.py [-h] {signers,sealers,votes,proposals,block} ...
Proporciona información acerca de la Blockchain Federal Argentina
positional arguments:
{signers,sealers,votes,proposals,block}
Consulta a realizar
signers (sealers) Información sobre selladores
votes (proposals) Indica el estado de una votación
block Información sobre un bloque
optional arguments:
-h, --help show this help message and exit
bfa@bootnode:~$
```
El comando `signers` (o `sealers`) permite obtener información sobre los selladores (similar al script `sealer_status`.
```
bfa@bootnode:~$ bfa.py signers
0x19fe7b9b3a1bebde77c5374c8e13c623e3d1b5b2
0x2388d2cdb2cd6e7722b4af39c3bb406dd31f560e
0x2feb6a8876bd9e2116b47834b977506a08ea77bd
0x342e1d075d820ed3f9d9a05967ec4055ab23fa1e
0x39170a1ce03729d141dfaf8077c08b72c9cfdd0c
0x401d7a8432caa1025d5f093276cc6ec957b87c00
0x46991ada2a2544468eb3673524641bf293f23ccc
0x609043ebde4a06bd28a1de238848e8f82cca9c23
0x91c055c6478bd0ad6d19bcb58f5e7ca7b04e67f1
0x998c2651db6f76ca568c0071667d265bcc1b1e98
0x9b3ac6719b02ec7bb4820ae178d31c0bbda3a4e0
0xc0310a7b3b25f49b11b901a667208a3eda8d7ceb
bfa@bootnode:~$ bfa.py signers --help
usage: bfa.py signers [-h] [--ipc-path IPC_PATH] [--rpc-port RPC_PORT]
[--json] [--status] [--use-block-number]
[--blocks BLOCKS]
optional arguments:
-h, --help show this help message and exit
--ipc-path IPC_PATH Path del archivo geth.ipc. Default:
/home/bfa/bfa/network/node/geth.ipc
--rpc-port RPC_PORT Puerto RPC. Default: 8545
--json Produce salida en formato JSON
--status Indica cuantos bloques han pasado desde último sellado
por cada sellador (-1 si no está activo)
--use-block-number Indica el número de bloque en lugar de los bloques
transcurridos
--blocks BLOCKS Especifica cuantos bloques buscar hacia atrás (default:
5 *número_de_selladores)
bfa@bootnode:~$ bfa.py signers --status
0x19fe7b9b3a1bebde77c5374c8e13c623e3d1b5b2: 7
0x2388d2cdb2cd6e7722b4af39c3bb406dd31f560e: -1
0x2feb6a8876bd9e2116b47834b977506a08ea77bd: 8
0x342e1d075d820ed3f9d9a05967ec4055ab23fa1e: 4
0x39170a1ce03729d141dfaf8077c08b72c9cfdd0c: 3
0x401d7a8432caa1025d5f093276cc6ec957b87c00: -1
0x46991ada2a2544468eb3673524641bf293f23ccc: 1
0x609043ebde4a06bd28a1de238848e8f82cca9c23: 0
0x91c055c6478bd0ad6d19bcb58f5e7ca7b04e67f1: 2
0x998c2651db6f76ca568c0071667d265bcc1b1e98: 6
0x9b3ac6719b02ec7bb4820ae178d31c0bbda3a4e0: 5
0xc0310a7b3b25f49b11b901a667208a3eda8d7ceb: 12
bfa@bootnode:~$
```
Se puede pedir que la salida sea en formato JSON:
```
bfa@bootnode:~$ bfa.py signers --status --json
{
"0x19fe7b9b3a1bebde77c5374c8e13c623e3d1b5b2": 3,
"0x2388d2cdb2cd6e7722b4af39c3bb406dd31f560e": -1,
"0x2feb6a8876bd9e2116b47834b977506a08ea77bd": 9,
"0x342e1d075d820ed3f9d9a05967ec4055ab23fa1e": 10,
"0x39170a1ce03729d141dfaf8077c08b72c9cfdd0c": 0,
"0x401d7a8432caa1025d5f093276cc6ec957b87c00": -1,
"0x46991ada2a2544468eb3673524641bf293f23ccc": 4,
"0x609043ebde4a06bd28a1de238848e8f82cca9c23": 6,
"0x91c055c6478bd0ad6d19bcb58f5e7ca7b04e67f1": 7,
"0x998c2651db6f76ca568c0071667d265bcc1b1e98": 1,
"0x9b3ac6719b02ec7bb4820ae178d31c0bbda3a4e0": 5,
"0xc0310a7b3b25f49b11b901a667208a3eda8d7ceb": 2
}
bfa@bootnode:~$
```
El comando `votes` (o `proposals`) brinda información sobre el estado de una votación (es similar al script `vote_status`:
```
bfa@bootnode:~$ bfa.py votes
Bloque: 538426
Propuestas en curso: 0
bfa@bootnode:~$
```
Se puede pedir el estado en un bloque determinado:
```
bfa@bootnode:~$ bfa.py votes --block-number 530000
Bloque: 530000
Propuestas en curso: 2
Propuesta: 0x2388d2cdb2cd6e7722b4af39c3bb406dd31f560e
0x19fe7b9b3a1bebde77c5374c8e13c623e3d1b5b2: None
0x2feb6a8876bd9e2116b47834b977506a08ea77bd: True
0x342e1d075d820ed3f9d9a05967ec4055ab23fa1e: None
0x39170a1ce03729d141dfaf8077c08b72c9cfdd0c: True
0x46991ada2a2544468eb3673524641bf293f23ccc: True
0x609043ebde4a06bd28a1de238848e8f82cca9c23: True
0x91c055c6478bd0ad6d19bcb58f5e7ca7b04e67f1: None
0x998c2651db6f76ca568c0071667d265bcc1b1e98: None
0x9b3ac6719b02ec7bb4820ae178d31c0bbda3a4e0: True
0xc0310a7b3b25f49b11b901a667208a3eda8d7ceb: None
A favor: 5, en contra: 0, no votaron: 5
Propuesta: 0x401d7a8432caa1025d5f093276cc6ec957b87c00
0x19fe7b9b3a1bebde77c5374c8e13c623e3d1b5b2: None
0x2feb6a8876bd9e2116b47834b977506a08ea77bd: True
0x342e1d075d820ed3f9d9a05967ec4055ab23fa1e: None
0x39170a1ce03729d141dfaf8077c08b72c9cfdd0c: True
0x46991ada2a2544468eb3673524641bf293f23ccc: True
0x609043ebde4a06bd28a1de238848e8f82cca9c23: True
0x91c055c6478bd0ad6d19bcb58f5e7ca7b04e67f1: None
0x998c2651db6f76ca568c0071667d265bcc1b1e98: None
0x9b3ac6719b02ec7bb4820ae178d31c0bbda3a4e0: True
0xc0310a7b3b25f49b11b901a667208a3eda8d7ceb: None
A favor: 5, en contra: 0, no votaron: 5
bfa@bootnode:~$
```
O, en formato JSON:
```
bfa@bootnode:~$ bfa.py votes --block-number 530000 --json
{
"block": 530000,
"proposals": [
"0x2388d2cdb2cd6e7722b4af39c3bb406dd31f560e",
"0x401d7a8432caa1025d5f093276cc6ec957b87c00"
],
"tally": {
"0x2388d2cdb2cd6e7722b4af39c3bb406dd31f560e": {
"None": 5,
"True": 5
},
"0x401d7a8432caa1025d5f093276cc6ec957b87c00": {
"None": 5,
"True": 5
}
},
"votes": {
"0x2388d2cdb2cd6e7722b4af39c3bb406dd31f560e": {
"0x19fe7b9b3a1bebde77c5374c8e13c623e3d1b5b2": null,
"0x2feb6a8876bd9e2116b47834b977506a08ea77bd": true,
"0x342e1d075d820ed3f9d9a05967ec4055ab23fa1e": null,
"0x39170a1ce03729d141dfaf8077c08b72c9cfdd0c": true,
"0x46991ada2a2544468eb3673524641bf293f23ccc": true,
"0x609043ebde4a06bd28a1de238848e8f82cca9c23": true,
"0x91c055c6478bd0ad6d19bcb58f5e7ca7b04e67f1": null,
"0x998c2651db6f76ca568c0071667d265bcc1b1e98": null,
"0x9b3ac6719b02ec7bb4820ae178d31c0bbda3a4e0": true,
"0xc0310a7b3b25f49b11b901a667208a3eda8d7ceb": null
},
"0x401d7a8432caa1025d5f093276cc6ec957b87c00": {
"0x19fe7b9b3a1bebde77c5374c8e13c623e3d1b5b2": null,
"0x2feb6a8876bd9e2116b47834b977506a08ea77bd": true,
"0x342e1d075d820ed3f9d9a05967ec4055ab23fa1e": null,
"0x39170a1ce03729d141dfaf8077c08b72c9cfdd0c": true,
"0x46991ada2a2544468eb3673524641bf293f23ccc": true,
"0x609043ebde4a06bd28a1de238848e8f82cca9c23": true,
"0x91c055c6478bd0ad6d19bcb58f5e7ca7b04e67f1": null,
"0x998c2651db6f76ca568c0071667d265bcc1b1e98": null,
"0x9b3ac6719b02ec7bb4820ae178d31c0bbda3a4e0": true,
"0xc0310a7b3b25f49b11b901a667208a3eda8d7ceb": null
}
}
}
bfa@bootnode:~$
```
El comando `block` permite ver un bloque, y adicionalmente, identificar el sellador de ese bloque:
```
bfa@bootnode:~$ bfa.py block --sealer
usage: bfa.py [-h] {signers,sealers,votes,proposals,block} ...
bfa.py: error: unrecognized arguments: --sealer
bfa@bootnode:~$ bfa.py block --signer
difficulty: 0x1
extraData: 0x393938633236353164623666373663613536382e303035303536396633313166
gasLimit: 8000000
gasUsed: 0
hash: 0xacd7427e4db75c65cd63a41defce7b6284797093d85cc3ba22e50519faee267b
logsBloom: 0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
miner: 0x0000000000000000000000000000000000000000
mixHash: 0x0000000000000000000000000000000000000000000000000000000000000000
nonce: 0x0000000000000000
number: 0x8389c
parentHash: 0xe6196028a81e9fae119b04d270b5b2f5c327d189d86358da9b8f29f40cb3d643
receiptsRoot: 0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421
sha3Uncles: 0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347
signer: 0x998c2651db6f76ca568c0071667d265bcc1b1e98
size: 0x261
stateRoot: 0x343e9a5c6763047eb7c99aaab5bd3078bdcfda16f076fe5916eb2ea45f57715b
timestamp: 0x5bd7aefc
totalDifficulty: 0xd8bf3
transactions: []
transactionsRoot: 0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421
uncles: []
bfa@bootnode:~$
```
#!/usr/bin/env python3
import requests
from web3 import Web3, IPCProvider
from web3.middleware import geth_poa_middleware
from itertools import groupby
import json
from os import environ
default_ipc_path = "{}/node/geth.ipc".format(environ.get('BFANETWORKDIR', "~/bfa/network"))
default_rpc_port = 8545
default_rpc_host = "localhost"
MAX_BLOCKS = 1 << 32
def int_to_bytes(x, byte_length = 0):
length = byte_length if byte_length else (x.bit_length() + 7) // 8
return x.to_bytes(length, 'big')
def hex_to_bytes(x):
try:
if x[:2] == '0x':
x = x[2:]
if len(x) % 2:
x = '0'+x
return bytes.fromhex(x)
except:
raise BFAException("invalid hex string: '{}'".format(x))
def rlp_encode(input):
if isinstance(input,bytes):
if len(input) == 1 and input[0] < 0x80: return input
else: return encode_length(len(input), 0x80) + input
elif isinstance(input,int):
return rlp_encode(int_to_bytes(input))
elif isinstance(input,str):
return rlp_encode(hex_to_bytes(input))
elif isinstance(input,list):
output = b''
for item in input: output += rlp_encode(item)
return encode_length(len(output), 0xc0) + output
else:
raise BFAException("cannot encode type '{}'".format(type(input)))
def encode_length(length,offset):
if length < 56:
return bytes([length + offset])
elif length < 256**8:
byte_length = int_to_bytes(length)
return bytes([len(byte_length) + offset + 55]) + byte_length
else:
raise BFAException("input too long")
def convert_response(response, ignore_keys = set()):
if type(response) == str:
if len(response) <= 10 and response[:2] == '0x':
return int(response,16)
else:
return response
if type(response) == list:
return [convert_response(x) for x in response]
if type(response) == dict:
return { key:convert_response(value) for key, value in response.items() if key not in ignore_keys }
return response
def convert_param(param):
if type(param) == int:
return "0x%x" % param
else:
return param
def convert_params(params):
return [convert_param(param) for param in params]
class BFAException(Exception):
pass
class BFA:
api = {
'getSigners': 'clique_getSigners',
'blockNumber': 'eth_blockNumber',
'getSnapshot': 'clique_getSnapshot',
'getSnapshotAtHash': 'clique_getSnapshotAtHash'
}
def __init__(self,
rpc_port = default_rpc_port,
rpc_host = default_rpc_host,
ipc_path = default_ipc_path):
self.rpc_port = rpc_port
self.rpc_host = rpc_host
try:
self.web3 = Web3(Web3.IPCProvider(ipc_path))
self.web3.middleware_stack.inject(geth_poa_middleware, layer=0)
self.web3.eth.blockNumber # raise exception if ipc_path is wrong
except:
raise BFAException("cannot connect via IPC ({})".format(ipc_path))
def rpc_call(self,method,params):
payload = {"jsonrpc":"2.0", "method": method,"params": convert_params(params),"id":1}
headers = {'Content-type':'application/json'}
try:
response = requests.post(
"http://{}:{}".format(self.rpc_host,self.rpc_port),
json=payload,
headers=headers).json()
except:
raise BFAException("RPC connecting with {}:{}".format(self.rpc_host,self.rpc_port))
if 'result' in response:
return response['result']
raise BFAException(response['error']['message'])
def getBlock(self, number = 'latest', full = False):
if isinstance(number,int):
latest = self.web3.eth.blockNumber
number = min(latest, max(0, number))
number = "0x%x" % number
return self.rpc_call('eth_getBlockByNumber',[number, full])
def getVotes(self, block = 0):
if block:
snapshot = self.getSnapshotAtHash(self.getBlockHash(block))
else:
snapshot = self.getSnapshot()
response = {}
proposals = sorted(list(snapshot['tally']))
votes = {}
for vote in snapshot['votes']:
address = vote['address']
if address not in votes:
votes[address] = {signer:None for signer in snapshot['signers']}
votes[address][vote['signer']] = vote['authorize']
tally = {}
for proposal,vot in votes.items():
tally[proposal] = {k:len(list(v)) for k,v in groupby(sorted(map(str,vot.values())))}
return {'block':snapshot['number'], 'votes': votes, 'proposals': proposals, 'tally':tally }
def getVanity(self, block = 'latest'):
return '0x'+ bytes.fromhex(self.getBlock(block)['extraData'][2:40]).decode('ascii')
def getSignature(self, block = 'latest', format="hex"):
if type(block) == int or block == 'latest':
block = self.getBlock(block)
signature = block['extraData'][-130:]
if format == "hex":
return signature
signature = bytes.fromhex(signature)
if format == "bytes":
return signature
if format == "rvs":
return (signature[:32], signature[32:64], signature[64])
raise BFAException("Unknown format: {}. Valid options are 'hex', 'bytes' and 'rvs'".format(format))
def encodeHeader(self, block = 'latest'):
fields = ['parentHash', 'sha3Uncles', 'miner', 'stateRoot', 'transactionsRoot', 'receiptsRoot', 'logsBloom', 'difficulty', 'number', 'gasLimit', 'gasUsed', 'timestamp', 'extraData', 'mixHash', 'nonce' ]
if type(block) == int or block == 'latest':
block = self.getBlock(block)
try:
# Convert fields that are uint64 in geth
block['gasLimit'] = int(block['gasLimit'],16)
block['gasUsed'] = int(block['gasUsed'],16)
# Remove signature from extraData
block['extraData'] = block['extraData'][:-130]
return rlp_encode([block[field] for field in fields])
except Exception as e:
raise BFAException("cannot encode block: {}".format(e))
def getBlockHash(self, block = 'latest'):
return self.getBlock(block)['hash']
def getHeaderHash(self, block = 'latest'):
return self.web3.sha3(self.encodeHeader(block))
def getBlockSigner(self, block = 'latest'):
if isinstance(block, int) or block == 'latest':
block = self.getBlock(block)
signature = self.getSignature(block)
return self.web3.eth.account.recoverHash(self.getHeaderHash(block), signature = signature).lower()
def lastSeen(self, n = 0, use_block_number = False):
signers = { signer:-1 for signer in self.getSigners()}
last = self.web3.eth.blockNumber
not_seen = len(signers)
if n == 0:
n = 5 * not_seen # 5 cycles
for i in range(n):
if not_seen == 0: break
block = last - i
address = self.getBlockSigner(block)
if signers[address] == -1:
signers[address] = block if use_block_number else i
not_seen -= 1
return signers
def __getattr__(self, name):
if name not in self.api:
raise BFAException("Unknown method: '{}'".format(name))
def method(*args):
return self.rpc_call(self.api[name],list(args))
return method
if __name__ == '__main__':
import argparse
from sys import stderr
def output_json(output):
print(json.dumps(output, indent=4, sort_keys = True))
def output_dict(output):
for key, value in sorted(output.items()):
print("{}: {}".format(key,value))
def signers(args,bfa):
output = bfa.lastSeen(args.blocks, args.use_block_number) if args.status else bfa.getSigners()
if args.json:
output_json(output)
elif args.status:
for signer, block in sorted(output.items()):
print("{}: {:2d}".format(signer,block))
else:
for signer in sorted(output):
print(signer)
def votes(args,bfa):
output = bfa.getVotes(args.block_number)
if args.json:
output_json(output)
else:
print("Bloque:", output['block'])
proposals = output['proposals']
print("Propuestas en curso:", len(proposals))
for proposal in proposals:
print("Propuesta:", proposal)
for signer, vote in sorted(output['votes'][proposal].items()):
print("\t{}: {}".format(signer,vote))
tally = output['tally'][proposal]
print("A favor: {}, en contra: {}, no votaron: {}\n".format(
tally.get("True",0),
tally.get("False",0),
tally.get("None",0)))
def getBlock(args,bfa):
block = bfa.getBlock(args.block_number)
if args.signer and args.block_number > 0:
block['signer'] = bfa.getBlockSigner(block)
if args.decimal:
block = convert_response(block, ignore_keys=set('nonce'))
if args.json:
output_json(block)
else:
output_dict(block)
def positive(n):
ret = int(n)
if ret < 0:
raise argparse.ArgumentTypeError("Argument should be positive")
return ret
parser = argparse.ArgumentParser(description=
"""Proporciona información acerca de la Blockchain Federal Argentina""")
parent_parser = argparse.ArgumentParser(add_help=False)
parent_parser.add_argument("--ipc-path", help ="Path del archivo geth.ipc. Default: {}".format(default_ipc_path), default = default_ipc_path)
parent_parser.add_argument("--rpc-port", help ="Puerto RPC. Default: {}".format(default_rpc_port),type=int, default = default_rpc_port)
parent_parser.add_argument("--json", help="Produce salida en formato JSON", action="store_true")
subparsers = parser.add_subparsers(help="Consulta a realizar",dest="command")
subparsers.required = True
parser_signers = subparsers.add_parser('signers', aliases=['sealers'], help='Información sobre selladores', parents=[parent_parser])
parser_signers.add_argument("--status", help="Indica cuantos bloques han pasado desde último sellado por cada sellador (-1 si no está activo)", action="store_true")
parser_signers.add_argument("--use-block-number", help="Indica el número de bloque en lugar de los bloques transcurridos", action = "store_true")
parser_signers.add_argument("--blocks", help ="Especifica cuantos bloques buscar hacia atrás (default: 5 *número_de_selladores)", type=positive, default = 0)
parser_signers.set_defaults(func=signers)
parser_votes = subparsers.add_parser('votes', aliases = ['proposals'], help="Indica el estado de una votación", parents=[parent_parser])
parser_votes.add_argument("--block-number", help = "Número de bloque en el cual se quiere conocer el estado de la votación (default: último)", type = positive, default = MAX_BLOCKS)
parser_votes.set_defaults(func=votes)
parser_block = subparsers.add_parser('block', help= "Información sobre un bloque", parents=[parent_parser])
parser_block.add_argument("--block-number", help= "Número de bloque (default: último)", type = positive, default = MAX_BLOCKS)
parser_block.add_argument("--decimal", help="Expresa los valores numéricos en decimal", action="store_true")
parser_block.add_argument("--signer", help="Agrega un campo con el sellador del bloque", action="store_true")
parser_block.set_defaults(func=getBlock)
args = parser.parse_args()
try:
bfa = BFA(ipc_path = args.ipc_path, rpc_port = args.rpc_port)
args.func(args,bfa)
except BFAException as e:
print(e, file=stderr)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment