Browse Source

modified: mmnode-blocks-info

The MMGen Project 4 years ago
parent
commit
b0cd68af35
1 changed files with 280 additions and 120 deletions
  1. 280 120
      mmnode-blocks-info

+ 280 - 120
mmnode-blocks-info

@@ -1,7 +1,7 @@
 #!/usr/bin/env python3
 #
 # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
-# Copyright (C)2013-2017 Philemon <mmgen-py@yandex.com>
+# 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
@@ -21,9 +21,12 @@ 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 or find a transaction within a range of blocks',
 		'usage':   '[opts] +<last n blocks>|<block num>|<block num range>',
@@ -32,9 +35,10 @@ opts_data = {
 --, --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
--n, --nya             Display NYA (New York Agreement) support
 -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
 -t, --transaction=t   Search for transaction 't' in specified block range
 
 If no block number is specified, the current block is assumed
@@ -42,128 +46,284 @@ If no block number is specified, the current block is assumed
 	}
 }
 
-cmd_args = opts.init(opts_data)
+class local_vars: pass
 
-if len(cmd_args) not in (0,1): opts.usage()
-if opt.raw_miner_info: opt.miner_info = True
-
-c = rpc_init()
-cur_blk = c.getblockcount()
-
-arg = cmd_args[0] if cmd_args else None
-
-if not arg:
-	first = last = cur_blk
-elif arg[0] == '+' and is_int(arg[1:]):
-	last = cur_blk
-	first = last - int(arg[1:]) + 1
-elif is_int(arg):
-	first = last = int(arg)
-else:
-	try:
-		a,b = arg.split('-')
-		assert is_int(a) and is_int(b)
-		first,last = int(a),int(b)
-	except:
-		opts.usage()
-
-if first > last:
-	die(2,'{}: invalid block range'.format(arg))
-if last > cur_blk:
-	die(2,'Requested block number ({}) greater than current block height'.format(last))
-
-if opt.summary:
-	pass
-elif opt.transaction:
-	if len(opt.transaction) != 64 or not is_hex_str(opt.transaction):
-		die(2,'{}: invalid transaction id'.format(opt.transaction))
-elif opt.hashes:
-	Msg('{:<7} {}'.format('BLOCK','HASH'))
-else:
-	if opt.miner_info:
-		fs='{:<6} {:<19}{:>9} {:>6} {:>7} {:8} {}'
-		Msg(fs.format('BLOCK','DATE','INTERVAL','CONFS','SIZE','VERSION','MINER'))
-	else:
-		fs='{:<6} {:<19}{:>9} {:>6} {:>7} {}'
-		Msg(fs.format('BLOCK','DATE','INTERVAL','CONFS','SIZE','VERSION'))
-
-miner_strings = {
-	'Bixin':'Bixin',
-	'AntPool':'AntPool',
-	'Bitfury':'Bitfury',
-	'BTCC':'BTCC',
-	'BTC.COM':'BTC.COM',
-	'BTPOOL':'BTPOOL',
-	'ViaBTC':'ViaBTC',
-	'slush':'Slush',
-	'BitMinter':'BitMinter',
-	'BW.COM':'BW.COM',
-	'gbminers':'GBMiners',
-	'BitClub Network':'BitClub Network',
-	'bitcoin.com':'bitcoin.com',
-	'KanoPool':'KanoPool',
-	'BTC.TOP':'BTC.TOP',
-}
+class BlocksInfo:
 
-total,prev_time = 0,None
-for i in range(first,last+1):
-	b = c.getblock(c.getblockhash(i))
-	total += b['size']
-	if opt.transaction:
-		if opt.transaction in b['tx']:
-			Msg('Requested transaction is in block {}'.format(i))
-			sys.exit(0)
-		msg('Checking block {}'.format(i))
-	else:
-		gmt = time.gmtime(b['mediantime'])
-		date = '{}-{:02}-{:02} {:02}:{:02}:{:02}'.format(*gmt[:6])
-		if prev_time == None:
-			b_prev = b if i == 0 else c.getblock(c.getblockhash(i-1))
-			first_time = prev_time = b_prev['mediantime']
-		et = b['mediantime'] - prev_time
-		prev_time = b['mediantime']
-		if opt.summary:
-			continue
-		if opt.hashes:
-			Msg('{:<7} {}'.format(i,b['hash']))
+	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:
-			d = (i,date,'{}:{:02}'.format(et//60,et%60),b['confirmations'],b['size'],b['versionHex'])
-			if opt.miner_info:
-				bd = c.getrawtransaction(b['tx'][0],1,on_fail='silent')
-				if type(bd) == tuple:
-					Msg(fs.format(*(d+('---',))))
+			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)
+		stats = await c.gathered_call('getblockheader',[(H,) for H in hashes])
+		pdie(stats)
+
+	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),
+		'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','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(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 (
+				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:
-					cb = unhexlify(bd['vin'][0]['coinbase'])
-					scb = re.sub(b'[^\w /-:,;.]',b'',cb)[1:]
-					if opt.raw_miner_info:
-						Msg(fs.format(*(d+(scb.decode(),))))
-					else:
-						for ms in miner_strings:
-							if ms.encode() in scb:
-								s = miner_strings[ms]
-								break
-						else:
-							try:    s = scb.split('ined by')[1].strip()
-							except: s = '??'
-						nya_str = (b'    ',b'NYA ')[b'NYA' in scb] if opt.nya else b''
-						Msg(fs.format(*(d+(nya_str.decode()+s,))))
+					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:
-				Msg(fs.format(*d))
+				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'))
 
-if opt.transaction:
-	from mmgen.rpc import rpc_error
-	if rpc_error(c.getmempoolentry(opt.transaction,on_fail='silent')):
-		Msg('\rTransaction not found in block range {}-{} or in mempool'.format(first,last))
+	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:
-		Msg('\rTransaction is in mempool')
-else:
-	blocks = last - first + 1
-	if blocks > 1:
-		fs2 = '{:<15} {}\n'
-		et = b['mediantime'] - first_time
-		ac = int(et / blocks)
-		if not opt.summary: Msg('')
-		Msg_r(  fs2.format('Range:','{}-{} ({} blocks)'.format(first,last,blocks)) +
-				fs2.format('Avg size:','{} bytes'.format(total//blocks)) +
-				fs2.format('MB/hr:','{:0.5f}'.format(float(total)*36/10000/et)) +
-				fs2.format('Avg conf time:','{}:{:02}'.format(ac//60,ac%60)))
+		m = BlocksInfoOverview()
+
+	m.print_header()
+	await m.run()
+	await m.print_summary()
+
+run_session(main())