mmgen-node-tools/mmnode-blocks-info

351 lines
12 KiB
Text
Raw Normal View History

2019-02-15 15:49:55 +00:00
#!/usr/bin/env python3
#
# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
2021-03-12 09:32:25 +00:00
# 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/>.
"""
2021-03-12 18:20:44 +00:00
mmnode-blocks-info: Display information about a block or range of blocks
"""
2021-03-13 17:03:06 +00:00
import re
2020-05-11 15:11:54 +00:00
from collections import namedtuple
2021-03-13 17:03:06 +00:00
from time import strftime,gmtime
from mmgen.common import *
2021-03-13 17:03:06 +00:00
from mmgen.util import secs_to_hms
2020-05-11 15:11:54 +00:00
from decimal import Decimal
2020-05-11 15:11:54 +00:00
class local_vars: pass
2020-05-11 15:11:54 +00:00
class BlocksInfo:
2020-05-11 15:11:54 +00:00
total_bytes = 0
total_weight = 0
2020-05-11 15:11:54 +00:00
bf = namedtuple('block_info_fields',['hdr1','hdr2','fs','bs_key','varname','deps','key'])
# bs=getblockstats(), bh=getblockheader()
2021-03-13 17:03:06 +00:00
# 'getblockstats' raises exception on Genesis Block!
2020-05-11 15:11:54 +00:00
# If 'bs_key' is set, it's included in self.bs_keys instead of 'key'
fields = {
2021-03-13 16:47:47 +00:00
'block': bf('', 'Block', '{:<6}', None, 'height',[], None),
'hash': bf('', 'Hash', '{:<64}', None, 'H', [], None),
2021-03-13 17:03:06 +00:00
'date': bf('', 'Date', '{:<19}', None, 'df', [], None),
'interval': bf('Solve','Time ', '{:>7}', None, 'td', [], None),
2021-03-13 16:47:47 +00:00
'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),
2020-05-11 15:11:54 +00:00
}
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'),
]
2020-05-11 15:11:54 +00:00
funcs = {
2021-03-13 17:03:06 +00:00
'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) ),
2020-05-11 15:11:54 +00:00
'tf': lambda self,loc: '{:.8f}'.format(loc.bs["totalfee"] * Decimal('0.00000001')),
'fp': lambda self,loc: loc.bs['feerate_percentiles'],
2021-03-13 17:03:06 +00:00
'su': lambda self,loc: str(loc.bs['subsidy'] * Decimal('0.00000001')).rstrip('0').rstrip('.'),
'di': lambda self,loc: '{:.2e}'.format(loc.bh['difficulty']),
2020-05-11 15:11:54 +00:00
}
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
2020-05-11 15:11:54 +00:00
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()
2020-05-11 15:11:54 +00:00
fnames = get_fields()
self.fvals = list(self.fields[name] for name in fnames)
self.fs = ''.join(gen_fs(fnames)).strip()
2020-05-11 15:11:54 +00:00
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']
2021-03-13 17:03:06 +00:00
self.bs_keys.extend(['total_size','total_weight'])
2020-05-11 15:11:54 +00:00
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&. /-]{5,})',
rb'[/^]([a-zA-Z0-9&. /-]+)/',
2020-05-11 15:11:54 +00:00
)]
else:
self.miner_pats = None
def get_block_range(self):
if not cmd_args:
first = last = c.blockcount
2020-05-11 15:11:54 +00:00
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
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]
2021-03-13 17:03:06 +00:00
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))
2020-05-11 15:11:54 +00:00
async def process_block(self,height,H,hdr):
2020-05-11 15:11:54 +00:00
loc = local_vars()
loc.height = height
loc.H = H
2021-03-13 17:03:06 +00:00
loc.bh = hdr
2021-03-13 17:03:06 +00:00
self.t_diff = hdr['time'] - self.t_cur
self.t_cur = hdr['time']
2020-05-11 15:11:54 +00:00
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']
2020-05-11 15:11:54 +00:00
if opt.summary:
return
for varname,func in self.ufuncs.items():
2021-03-13 17:03:06 +00:00
setattr(loc,varname,func(self,loc))
2020-05-11 15:11:54 +00:00
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:
2020-05-11 15:11:54 +00:00
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')
2021-03-13 17:03:06 +00:00
if ''.join(hdr1).replace(' ',''):
Msg(self.fs.format(*hdr1))
Msg(self.fs.format(*hdr2))
2020-05-11 15:11:54 +00:00
async def print_summary(self):
tip = c.blockcount
2021-03-13 17:03:06 +00:00
if self.last == tip:
cur_diff_disp = f'Cur difficulty: {self.last_hdr["difficulty"]:.2e}'
2021-03-13 17:03:06 +00:00
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
2021-03-13 17:03:06 +00:00
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}
2021-03-13 17:03:06 +00:00
Est. diff adjust: {adj_pct:+.2f}%
"""))
else:
Msg(fmt(f"""
Current height: {tip}
{cur_diff_disp}
2021-03-13 17:03:06 +00:00
Next diff adjust: {tip-rel+2016} (in {2016-rel} blocks)
"""))
2021-03-13 17:03:06 +00:00
nblocks = self.last - self.first + 1
Msg('Range: {}-{} ({} blocks [{}])'.format(
self.first,
self.last,
2021-03-13 17:03:06 +00:00
nblocks,
secs_to_hms(self.t_cur - self.t_start) ))
2021-03-13 17:03:06 +00:00
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
"""))
2020-05-11 15:11:54 +00:00
2021-03-13 16:49:05 +00:00
opts_data = {
'sets': [
2021-03-13 17:03:06 +00:00
('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),
2021-03-13 16:49:05 +00:00
],
'text': {
2021-03-13 17:03:06 +00:00
'desc': 'Display information about a block or range of blocks',
'usage': '[opts] +<last N blocks>|<block num>[-<block num>]',
2021-03-13 16:49:05 +00:00
'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
2021-03-13 17:03:06 +00:00
-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.
2021-03-13 16:49:05 +00:00
-s, --summary Print the summary only
2021-03-13 17:03:06 +00:00
-S, --no-summary Don’t print the summary
2021-03-13 16:49:05 +00:00
""",
'notes': """
If no block number is specified, the current block is assumed.
2021-03-13 17:03:06 +00:00
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.
2021-03-13 16:49:05 +00:00
2021-03-13 17:03:06 +00:00
AVAILABLE FIELDS: {}
This program requires a txindex-enabled daemon for correct operation.
""".format(fmt_list(BlocksInfo.fields,fmt='bare'))
2021-03-13 16:49:05 +00:00
}
}
2020-05-11 15:11:54 +00:00
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()
2020-05-11 15:11:54 +00:00
from mmgen.rpc import rpc_init
global c
c = await rpc_init(proto)
2020-05-11 15:11:54 +00:00
m = BlocksInfo()
2020-05-11 15:11:54 +00:00
if not (opt.summary or opt.no_header):
m.print_header()
2020-05-11 15:11:54 +00:00
await m.run()
if not opt.no_summary:
if not opt.summary:
Msg('')
await m.print_summary()
2020-05-11 15:11:54 +00:00
run_session(main())