123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389 |
- #!/usr/bin/env python3
- #
- # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
- # Copyright (C)2013-2021 The MMGen Project <mmgen@tuta.io>
- #
- # 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 <http://www.gnu.org/licenses/>.
- """
- mmnode-blocks-info: Display information about a block or range of blocks
- """
- import re
- from collections import namedtuple
- from time import strftime,gmtime
- from mmgen.common import *
- from mmgen.util import secs_to_hms
- from decimal import Decimal
- class local_vars: pass
- class BlocksInfo:
- total_bytes = 0
- total_weight = 0
- bf = namedtuple('block_info_fields',['hdr1','hdr2','fs','bs_key','varname','deps','key'])
- # bs=getblockstats(), bh=getblockheader()
- # 'getblockstats' raises exception on Genesis Block!
- # 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', [], None),
- 'interval': bf('Solve','Time ', '{:>7}', None, 'td', [], 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('Sub-', 'sidy', '{:5}', 'subsidy', 'su', ['bs'], None),
- 'difficulty':bf('Diffi-','culty', '{:8}', None, 'di', [], None),
- }
- dfl_fields = ['block','date','interval','subsidy','totalfee','size','weight','fee50','fee25','fee10','version']
- fixed_fields = [
- 'block', # until ≈ 09/01/2028 (block 1000000)
- 'hash',
- 'date',
- 'size', # until ≈ 6x block size increase
- 'weight', # until ≈ 2.5x block size increase
- 'version',
- 'subsidy', # until ≈ 01/04/2028 (increases by 1 digit per halving until 9th halving [max 10 digits])
- 'difficulty', # until 1.00e+100 (i.e. never)
- ]
- # column width adjustment data:
- fs_lsqueeze = ['interval','totalfee','inputs','outputs','nTx']
- fs_rsqueeze = []
- fs_groups = [
- ('fee10','fee25','fee50','fee75','fee90','fee_avg','fee_min'),
- ]
- funcs = {
- 'df': lambda self,loc: strftime('%Y-%m-%d %X',gmtime(self.t_cur)),
- 'td': lambda self,loc: (
- '-{:02}:{:02}'.format(abs(self.t_diff)//60,abs(self.t_diff)%60) if self.t_diff < 0 else
- ' {:02}:{:02}'.format(self.t_diff//60,self.t_diff%60) ),
- 'tf': lambda self,loc: '{:.8f}'.format(loc.bs["totalfee"] * Decimal('0.00000001')),
- 'fp': lambda self,loc: loc.bs['feerate_percentiles'],
- 'su': lambda self,loc: str(loc.bs['subsidy'] * Decimal('0.00000001')).rstrip('0').rstrip('.'),
- 'di': lambda self,loc: '{:.2e}'.format(loc.bh['difficulty']),
- }
- def __init__(self):
- def get_fields():
- if opt.fields:
- ufields = opt.fields.lstrip('+').split(',')
- for field in ufields:
- if field not in self.fields:
- die(1,f'{field!r}: unrecognized field')
- return self.dfl_fields + ufields if opt.fields[0] == '+' else ufields
- else:
- return self.dfl_fields
- def gen_fs(fnames):
- for i in range(len(fnames)):
- name = fnames[i]
- ls = (' ','')[name in self.fs_lsqueeze]
- rs = (' ','')[name in self.fs_rsqueeze]
- if i:
- for group in self.fs_groups:
- if name in group and fnames[i-1] in group:
- ls = ''
- break
- yield ls + self.fields[name].fs + rs
- self.get_block_range(cmd_args)
- fnames = get_fields()
- self.fvals = list(self.fields[name] for name in fnames)
- self.fs = ''.join(gen_fs(fnames)).strip()
- 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','total_weight'])
- 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 += ' {}'
- 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&. #/-]+)[/\xfa]',
- rb'[/^]([a-zA-Z0-9&. #/-]{5,})',
- rb'[/^]([_a-zA-Z0-9&. #/-]+)/',
- )]
- else:
- self.miner_pats = None
- def get_block_range(self,args):
- if not args:
- first = last = c.blockcount
- else:
- arg = args[0]
- from_current = arg[0] == '-'
- if arg[0] == '-':
- arg = arg[1:]
- ps = arg.split('+')
- if len(ps) == 2 and is_int(ps[1]):
- if not ps[0] and not from_current:
- last = c.blockcount
- first = last - int(arg[1:]) + 1
- elif is_int(ps[0]):
- first = (c.blockcount - int(ps[0])) if from_current else int(ps[0])
- last = first + int(ps[1]) - 1
- else:
- opts.usage()
- elif is_int(arg):
- first = last = (c.blockcount - int(arg)) if from_current else int(arg)
- else:
- try:
- assert not from_current
- 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
- async def run(self):
- heights = range(self.first,self.last+1)
- hashes = await c.gathered_call('getblockhash',[(height,) for height in heights])
- hdrs = await c.gathered_call('getblockheader',[(H,) for H in hashes])
- self.last_hdr = hdrs[-1]
- self.t_start = hdrs[0]['time']
- self.t_cur = (
- self.t_start if heights[0] == 0 else
- (await c.call('getblockheader',await c.call('getblockhash',heights[0]-1)))['time']
- )
- for height in heights:
- await self.process_block(height,hashes.pop(0),hdrs.pop(0))
- async def process_block(self,height,H,hdr):
- loc = local_vars()
- loc.height = height
- loc.H = H
- loc.bh = hdr
- self.t_diff = hdr['time'] - self.t_cur
- self.t_cur = hdr['time']
- if 'bs' in self.deps:
- loc.bs = await c.call('getblockstats',H,self.bs_keys)
- self.total_bytes += loc.bs['total_size']
- self.total_weight += loc.bs['total_weight']
- if opt.summary:
- return
- for varname,func in self.ufuncs.items():
- setattr(loc,varname,func(self,loc))
- 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 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('/',' ')
- def print_header(self):
- hdr1 = [v.hdr1 for v in self.fvals]
- hdr2 = [v.hdr2 for v in self.fvals]
- if opt.miner_info:
- hdr1.append(' ')
- hdr2.append('Miner')
- if ''.join(hdr1).replace(' ',''):
- Msg(self.fs.format(*hdr1))
- Msg(self.fs.format(*hdr2))
- async def print_summary(self):
- tip = c.blockcount
- if self.last == tip:
- cur_diff_disp = f'Cur difficulty: {self.last_hdr["difficulty"]:.2e}'
- rel = tip % 2016
- if rel:
- rel_hdr = await c.call('getblockheader',await c.call('getblockhash',tip-rel))
- bdi = (self.last_hdr['time']-rel_hdr['time']) / rel
- adj_pct = ((600 / bdi) - 1) * 100
- Msg(fmt(f"""
- Current height: {tip}
- Next diff adjust: {tip-rel+2016} (in {2016-rel} blocks [{((2016-rel)*bdi)/86400:.2f} days])
- BDI (cur period): {bdi/60:.2f} min
- {cur_diff_disp}
- Est. diff adjust: {adj_pct:+.2f}%
- """))
- else:
- Msg(fmt(f"""
- Current height: {tip}
- {cur_diff_disp}
- Next diff adjust: {tip-rel+2016} (in {2016-rel} blocks)
- """))
- nblocks = self.last - self.first + 1
- Msg('Range: {}-{} ({} blocks [{}])'.format(
- self.first,
- self.last,
- nblocks,
- secs_to_hms(self.t_cur - self.t_start) ))
- if 'bs' in self.deps and nblocks > 1:
- elapsed = self.t_cur - self.t_start
- ac = int(elapsed / nblocks)
- rate = (self.total_bytes / 10000) / (elapsed / 36)
- Msg_r(fmt(f"""
- Avg size: {self.total_bytes//nblocks} bytes
- Avg weight: {self.total_weight//nblocks} bytes
- MB/hr: {rate:0.4f}
- Avg BDI: {ac/60:.2f} min
- """))
- opts_data = {
- 'sets': [
- ('raw_miner_info', True, 'miner_info', True),
- ('summary', True, 'raw_miner_info', False),
- ('summary', True, 'miner_info', False),
- ('hashes', True, 'fields', 'block,hash'),
- ('hashes', True, 'no_summary', True),
- ],
- 'text': {
- 'desc': 'Display information about a block or range of blocks',
- 'usage': '[opts] [<block num>|-<N blocks>]+<N blocks>|<block num>[-<block num>]',
- '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
- -n, --no-header Don’t print the column header
- -o, --fields= Display the specified fields (comma-separated list)
- See AVAILABLE FIELDS below. If the first character
- is '+', fields are appended to the defaults.
- -s, --summary Print the summary only
- -S, --no-summary Don’t print the summary
- """,
- 'notes': """
- If no block number is specified, the current block is assumed.
- If the requested range ends at the current chain tip, an estimate of the next
- difficulty adjustment is also displayed. The estimate is based on the average
- Block Discovery Interval from the beginning of the current 2016-block period.
- AVAILABLE FIELDS: {f}
- EXAMPLES:
- # Display default info for current block:
- {p}
- # Display default info for blocks 1-200
- {p} 1-200
- # Display default info for 20 blocks beginning from block 600000
- {p} 600000+20
- # Display default info for 12 blocks beginning 100 blocks from chain tip
- {p} -- -100+12
- # Display info for block 152817, adding miner field:
- {p} --miner-info 152817
- # Display info for last 10 blocks, adding 'inputs' and 'nTx' fields:
- {p} -o +inputs,nTx +10
- # Display 'block', 'date', 'version' and 'hash' fields for blocks 0-10:
- # Note: these are the only supported fields for the Genesis Block
- {p} -o block,date,version,hash 0-10
- This program requires a txindex-enabled daemon for correct operation.
- """.format(
- f = fmt_list(BlocksInfo.fields,fmt='bare'),
- p = g.prog_name )
- }
- }
- 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)
- m = BlocksInfo()
- if not (opt.summary or opt.no_header):
- m.print_header()
- await m.run()
- if not opt.no_summary:
- if not opt.summary:
- Msg('')
- await m.print_summary()
- run_session(main())
|