mmgen-node-tools/mmnode-blocks-info

329 lines
11 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
2020-05-11 15:11:54 +00:00
# Copyright (C)2013-2020 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/>.
"""
mmgen-blocks-info: Display information about a block or range of blocks
"""
import time,re
2020-05-11 15:11:54 +00:00
from collections import namedtuple
from mmgen.common import *
2020-05-11 15:11:54 +00:00
from decimal import Decimal
2019-05-29 12:06:08 +00:00
opts_data = {
2020-05-11 15:11:54 +00:00
'sets': [('raw_miner_info',True,'miner_info',True)],
2019-05-29 12:06:08 +00:00
'text': {
'desc': 'Display information about or find a transaction within a range of blocks',
'usage': '[opts] +<last n blocks>|<block num>|<block num range>',
'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
2020-05-11 15:11:54 +00:00
-o, --fields= Display the specified fields
-s, --summary Print the summary only
2020-05-11 15:11:54 +00:00
-S, --no-summary Don't print the summary
-t, --transaction=t Search for transaction 't' in specified block range
If no block number is specified, the current block is assumed
"""
2019-05-29 12:06:08 +00:00
}
}
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
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:
2020-05-11 15:11:54 +00:00
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)
2020-05-11 15:11:54 +00:00
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
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 ', '{:>5}', 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),
2020-05-11 15:11:54 +00:00
'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']
2020-05-11 15:11:54 +00:00
funcs = {
'df': lambda self,loc: time.strftime('%Y-%m-%d %X',time.gmtime(self.t_cur)),
'if': lambda self,loc: '{: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(',')
2020-05-11 15:11:54 +00:00
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 (
r'([a-zA-Z0-9. ]+/Mined by [a-zA-Z0-9. ]+)',
r'Mined by ([a-zA-Z0-9. ]+)',
r'/([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:
ret = '---'
else:
cb = bytes.fromhex(bd['vin'][0]['coinbase'])
ret = ''.join(chr(b) for b in cb if 31 < b < 128)
if not opt.raw_miner_info:
for pat in self.miner_pats:
m = pat.search(ret)
if m:
ret = m[1]
break
return ret
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.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:
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 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('MB/hr:', f'{rate:0.4f}') +
self.sum_fs.format('Avg conf time:', f'{ac//60}:{ac%60:02}')
)
class BlocksInfoTxFind(BlocksInfo):
found_tx = False
def post_init(self):
if len(opt.transaction) != 64 or not is_hex_str(opt.transaction):
die(2,f'{opt.transaction}: invalid transaction id')
async def process_block(self,height,H):
if opt.transaction in (await c.call('getblock',H))['tx']:
Msg('\rRequested transaction is in block {} ({} confirmations)'.format(height,c.blockcount-height+1))
return True
msg_r('\rChecking block {} '.format(height))
async def print_summary(self):
if self.found_tx:
try:
await c.call('getmempoolentry',opt.transaction) # ,on_fail='silent')):
except:
Msg('\rTransaction not found in block range {}-{} or in mempool'.format(self.first,self.last))
else:
2020-05-11 15:11:54 +00:00
Msg('\rTransaction is in mempool')
async def run(self):
for height in range(self.first,self.last+1):
H = await c.call('getblockhash',height)
if await self.process_block(height,H): # returns True when finished
break
else:
self.found_tx = True
class BlocksInfoHashes(BlocksInfo):
def print_header(self):
Msg('{:<7} {}'.format('BLOCK','HASH'))
2020-05-11 15:11:54 +00:00
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.rpc import rpc_init
global c
c = await rpc_init()
if opt.transaction:
m = BlocksInfoTxFind()
elif opt.hashes:
m = BlocksInfoHashes()
else:
2020-05-11 15:11:54 +00:00
m = BlocksInfoOverview()
m.print_header()
await m.run()
await m.print_summary()
run_session(main())