#!/usr/bin/env python3 # # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution # Copyright (C)2013-2021 The MMGen Project # # This program is free software: you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free Software # Foundation, either version 3 of the License, or (at your option) any later # version. # # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more # details. # # You should have received a copy of the GNU General Public License along with # this program. If not, see . """ mmgen-blocks-info: Display information about a block or range of blocks """ import time,re from collections import namedtuple from mmgen.common import * from decimal import Decimal opts_data = { 'sets': [('raw_miner_info',True,'miner_info',True)], 'text': { 'desc': 'Display information about a range of blocks', 'usage': '[opts] +||', 'options': """ -h, --help Print this help message --, --longhelp Print help message for long options (common options) -H, --hashes Display only block numbers and hashes -m, --miner-info Display miner info in coinbase transaction -M, --raw-miner-info Display miner info in uninterpreted form -o, --fields= Display the specified fields -s, --summary Print the summary only -S, --no-summary Don't print the summary If no block number is specified, the current block is assumed """ } } class local_vars: pass class BlocksInfo: first = None last = None nblocks = None sum_fs = '{:<15} {}\n' def __init__(self): self.get_block_range() self.post_init() def get_block_range(self): if not cmd_args: first = last = c.blockcount else: arg = cmd_args[0] if arg.startswith('+') and is_int(arg[1:]): last = c.blockcount first = last - int(arg[1:]) + 1 elif is_int(arg): first = last = int(arg) else: try: first,last = [int(ep) for ep in arg.split('-')] except: opts.usage() if first > last: die(2,f'{first}-{last}: invalid block range') if last > c.blockcount: die(2,f'Requested block number ({last}) greater than current block height') self.first = first self.last = last self.nblocks = last - first + 1 def post_init(self): pass async def run(self): for height in range(self.first,self.last+1): await self.process_block(height,await c.call('getblockhash',height)) return # WIP heights = range(self.first,self.last+1) hashes = await c.gathered_call('getblockhash',[(height,) for height in heights]) print(hashes) header = await c.gathered_call('getblockheader',[(H,) for H in hashes]) pdie(header) def print_header(self): pass async def print_summary(self): if not opt.summary: Msg('') Msg_r( self.sum_fs.format('Current height:', c.blockcount) + self.sum_fs.format('Range:', f'{self.first}-{self.last} ({self.nblocks} blocks)') ) class BlocksInfoOverview(BlocksInfo): total_bytes = 0 total_weight = 0 t_start = None t_prev = None t_cur = None bf = namedtuple('block_info_fields',['hdr1','hdr2','fs','bs_key','varname','deps','key']) # bs=getblockstats(), bh=getblockheader() # If 'bs_key' is set, it's included in self.bs_keys instead of 'key' fields = { 'block': bf('', 'Block', '{:<6}', None, 'height',[], None), 'hash': bf('', 'Hash', '{:<64}', None, 'H', [], None), 'date': bf('', 'Date', '{:<19}', None, 'df', ['bs'],None), 'interval': bf('Solve','Time ', '{:>6}', None, 'if', ['bs'],None), 'size': bf('', 'Size', '{:>7}', None, 'bs', [], 'total_size'), 'weight': bf('', 'Weight', '{:>7}', None, 'bs', [], 'total_weight'), 'utxo_inc': bf(' UTXO',' Incr', '{:>5}', None, 'bs', [], 'utxo_increase'), 'fee10': bf('10%', 'Fee', '{:>3}', 'feerate_percentiles','fp', ['bs'],0), 'fee25': bf('25%', 'Fee', '{:>3}', 'feerate_percentiles','fp', ['bs'],1), 'fee50': bf('50%', 'Fee', '{:>3}', 'feerate_percentiles','fp', ['bs'],2), 'fee75': bf('75%', 'Fee', '{:>3}', 'feerate_percentiles','fp', ['bs'],3), 'fee90': bf('90%', 'Fee', '{:>3}', 'feerate_percentiles','fp', ['bs'],4), 'fee_avg': bf('Avg', 'Fee', '{:>3}', None, 'bs', [], 'avgfeerate'), 'fee_min': bf('Min', 'Fee', '{:>3}', None, 'bs', [], 'minfeerate'), 'totalfee': bf('', 'Total Fee','{:>10}', 'totalfee', 'tf', ['bs'],None), 'outputs': bf('Out-', 'puts', '{:>5}', None, 'bs', [], 'outs'), 'inputs': bf('In- ', 'puts', '{:>5}', None, 'bs', [], 'ins'), 'version': bf('', 'Version', '{:8}', None, 'bh', [], 'versionHex'), 'nTx': bf('', 'nTx ', '{:>5}', None, 'bh', [], 'nTx'), 'subsidy': bf('', 'Subsidy', '{:7}', 'subsidy', 'su', ['bs'], None), } dfl_fields = ['block','date','interval','subsidy','totalfee','size','weight','fee50','fee25','fee10','version'] funcs = { 'df': lambda self,loc: time.strftime('%Y-%m-%d %X',time.gmtime(self.t_cur)), 'if': lambda self,loc: ( '-{:02}:{:02}'.format(abs(loc.t_diff)//60,abs(loc.t_diff)%60) if loc.t_diff < 0 else ' {:02}:{:02}'.format(loc.t_diff//60,loc.t_diff%60) ), 'tf': lambda self,loc: '{:.8f}'.format(loc.bs["totalfee"] * Decimal('0.00000001')), 'bh': lambda self,loc: c.call('getblockheader',loc.H), 'fp': lambda self,loc: loc.bs['feerate_percentiles'], 'su': lambda self,loc: str(loc.bs['subsidy'] * Decimal('0.00000001')).rstrip('0'), } def __init__(self): super().__init__() if opt.fields: fnames = opt.fields.split(',') for n in fnames: if n not in self.fields: die(1,f'{n!r}: unrecognized field') else: fnames = self.dfl_fields self.fvals = list(self.fields[k] for k in fnames if k in self.fields) self.fs = ' '.join( v.fs for v in self.fvals ) hdr1 = [v.hdr1 for v in self.fvals] hdr2 = [v.hdr2 for v in self.fvals] self.deps = set(' '.join(v.varname + ' ' + ' '.join(v.deps) for v in self.fvals).split()) self.bs_keys = [(v.bs_key or v.key) for v in self.fvals if v.bs_key or v.varname == 'bs'] self.bs_keys.extend(['total_size','time']) self.ufuncs = {v.varname:self.funcs[v.varname] for v in self.fvals if v.varname in self.funcs} if opt.miner_info: self.fs += ' {}' hdr1.append(' ') hdr2.append('Miner') self.miner_pats = [re.compile(pat) for pat in ( rb'[\xe3\xe4\xe5][\^/](.*?)\xfa', rb'([a-zA-Z0-9&. -]+/Mined by [a-zA-Z0-9. ]+)', rb'\x08/(.*Mined by [a-zA-Z0-9. ]+)', rb'Mined by ([a-zA-Z0-9. ]+)', rb'[/^]([a-zA-Z0-9&. /-]{5,})', rb'[/^]([a-zA-Z0-9&. /-]+)/', )] else: self.miner_pats = None if not opt.summary: Msg(self.fs.format(*hdr1)) Msg(self.fs.format(*hdr2)) async def get_miner_string(self,H): tx0 = (await c.call('getblock',H))['tx'][0] bd = await c.call('getrawtransaction',tx0,1) if type(bd) == tuple: return '---' else: cb = bytes.fromhex(bd['vin'][0]['coinbase']) if opt.raw_miner_info: return repr(cb) else: for pat in self.miner_pats: m = pat.search(cb) if m: return ''.join(chr(b) for b in m[1] if 31 < b < 127).strip('^').strip('/').replace('/',' ') async def process_block(self,height,H): loc = local_vars() loc.H = H loc.height = height if 'bs' in self.deps: loc.bs = await c.call('getblockstats',H,self.bs_keys) #pdie(loc.bs) self.total_bytes += loc.bs['total_size'] self.total_weight += loc.bs['total_weight'] self.t_cur = loc.bs['time'] if self.t_prev == None: if height == 0: b_prev = loc.bs else: bh = await c.call('getblockhash',height-1) b_prev = await c.call('getblockstats',bh) self.t_start = self.t_prev = b_prev['time'] loc.t_diff = self.t_cur - self.t_prev self.t_prev = self.t_cur if opt.summary: return for varname,func in self.ufuncs.items(): ret = func(self,loc) if type(ret).__name__ == 'coroutine': ret = await ret setattr(loc,varname,ret) if opt.miner_info: miner_info = await self.get_miner_string(H) def gen(): for v in self.fvals: if v.key is None: yield getattr(loc,v.varname) else: yield getattr(loc,v.varname)[v.key] if opt.miner_info: yield miner_info Msg(self.fs.format(*gen())) async def print_summary(self): if 'bs' in self.deps: await super().print_summary() if self.nblocks > 1: elapsed = self.t_cur - self.t_start ac = int(elapsed / self.nblocks) rate = (self.total_bytes / 10000) / (elapsed / 36) Msg_r ( self.sum_fs.format('Avg size: ', f'{self.total_bytes//self.nblocks} bytes') + self.sum_fs.format('Avg weight: ', f'{self.total_weight//self.nblocks} bytes') + self.sum_fs.format('MB/hr:', f'{rate:0.4f}') + self.sum_fs.format('Avg conf time:', f'{ac//60}:{ac%60:02}') ) class BlocksInfoHashes(BlocksInfo): def print_header(self): Msg('{:<7} {}'.format('BLOCK','HASH')) async def run(self): heights = range(self.first,self.last+1) hashes = await c.gathered_call('getblockhash',[(height,) for height in heights]) Msg('\n'.join('{:<7} {}'.format(height,H) for height,H in zip(heights,hashes))) cmd_args = opts.init(opts_data) if len(cmd_args) not in (0,1): opts.usage() async def main(): from mmgen.protocol import init_proto_from_opts proto = init_proto_from_opts() from mmgen.rpc import rpc_init global c c = await rpc_init(proto) if opt.hashes: m = BlocksInfoHashes() else: m = BlocksInfoOverview() m.print_header() await m.run() await m.print_summary() run_session(main())