Browse Source

mmnode-blocks-info: support new rangespecs, rewrite range parser

- support arbitrary block lists
- support ranges with step
- support multiplication symbol in nBlocks specifier
- support 'cur' to designate current block
The MMGen Project 4 years ago
parent
commit
780ff3647e
1 changed files with 210 additions and 90 deletions
  1. 210 90
      mmnode-blocks-info

+ 210 - 90
mmnode-blocks-info

@@ -34,6 +34,8 @@ class BlocksInfo:
 
 	total_bytes = 0
 	total_weight = 0
+	total_solve_time = 0
+	step = None
 
 	bf = namedtuple('block_info_fields',['hdr1','hdr2','fs','bs_key','varname','deps','key'])
 	# bs=getblockstats(), bh=getblockheader()
@@ -132,7 +134,7 @@ class BlocksInfo:
 							break
 				yield ls + self.fields[name].fs + rs
 
-		self.get_block_range(cmd_args)
+		self.block_list,self.first,self.last = self.parse_block_range(cmd_args)
 
 		fnames     = get_fields()
 		self.fvals = list(self.fields[name] for name in fnames)
@@ -158,58 +160,141 @@ class BlocksInfo:
 		else:
 			self.miner_pats = None
 
-	def get_block_range(self,args):
+	def parse_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)
+		def conv_blkspec(arg):
+			if arg == 'cur':
+				return c.blockcount
+			elif is_int(arg) and int(arg) >= 0:
+				return int(arg)
 			else:
-				try:
-					assert not from_current
-					first,last = [int(ep) for ep in arg.split('-')]
-				except:
+				die(1,f'{arg}: invalid block specifier')
+
+		def parse_rangespec(arg):
+
+			class RangeParser:
+
+				def __init__(self,arg):
+					self.arg = arg
+
+				def parse(self,target):
+					ret = getattr(self,'parse_'+target)()
+					if debug: print(f'after parse({target}): {self.arg}')
+					return ret
+
+				def parse_from_tip(self):
+					m = re.match(r'-(\d+)(.*)',self.arg)
+					if m:
+						self.arg = m[2]
+						assert int(m[1]) > 0, 'block count cannot be zero'
+						return int(m[1])
+
+				def parse_range(self):
+					if self.arg and self.arg[0] == '-':
+						opts.usage()
+					else:
+						m = re.match(r'([^+-]+)(-([^+-]+))*(.*)',self.arg)
+						if m:
+							if debug: print(m.groups())
+							self.arg = m[4]
+							return (
+								conv_blkspec(m[1]),
+								conv_blkspec(m[3]) if m[3] else None
+							)
+					return (None,None)
+
+				def parse_add(self):
+					m = re.match(r'\+([0-9*]+)(.*)',self.arg)
+					if m:
+						self.arg = m[2]
+						assert m[1].strip('*') == m[1], f"'+{m[1]}': malformed nBlocks specifier"
+						assert len(m[1]) <= 30,         f"'+{m[1]}': overly long nBlocks specifier"
+						res = eval(m[1]) # m[1] is only digits plus '*', so safe
+						assert res > 0, "'+0' not allowed"
+						assert res <= c.blockcount, f"'+{m[1]}': nBlocks must be less than current chain height"
+						return res
+
+			debug = False
+			range_spec = namedtuple('parsed_range_spec',['first','last','from_tip','nblocks','step'])
+
+			p = RangeParser(arg)
+			# parsing order must be preserved!
+			from_tip   = p.parse('from_tip')
+			first,last = p.parse('range')
+			add1       = p.parse('add')
+			add2       = p.parse('add')
+
+			if p.arg or (from_tip and first):
+				opts.usage()
+
+			if last:
+				nblocks,step = (None,add1)
+				if add2:
 					opts.usage()
+			else:
+				nblocks,step = (add1,add2)
+
+			if debug: print(range_spec(first,last,from_tip,nblocks,step))
+
+			if from_tip:
+				first = c.blockcount - from_tip
+			if nblocks:
+				if not first:
+					first = c.blockcount - nblocks + 1
+				last = first + nblocks - 1
+			if not last:
+				last = first
+
+			if debug: print(range_spec(first,last,from_tip,nblocks,step))
 
 			if first > last:
-				die(2,f'{first}-{last}: invalid block range')
+				die(1,f'{first}-{last}: invalid block range')
 
 			if last > c.blockcount:
-				die(2,f'Requested block number ({last}) greater than current block height')
+				die(1,f'Requested block number {last} greater than current chain height')
 
-		self.first = first
-		self.last = last
+			block_list = list(range(first,last+1,step)) if step else None
+			return (block_list, first, last)
 
-	async def run(self):
+		def parse_blocklist(args):
+			for arg in args:
+				if arg != 'cur':
+					if not is_int(arg):
+						die(1,f'{arg!r}: invalid block number (not an integer)')
+					if int(arg) > c.blockcount:
+						die(1,f'Requested block number {arg} greater than current chain height')
+
+			return [conv_blkspec(a) for a in args]
 
-		heights = range(self.first,self.last+1)
+		# return (block_list,first,last)
+		if not args:
+			return (None,c.blockcount,c.blockcount)
+		elif len(args) == 1:
+			return parse_rangespec(args[0])
+		else:
+			return (parse_blocklist(args),None,None)
+
+	async def run(self):
+		heights = self.block_list or 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.hdrs = await c.gathered_call('getblockheader',[(H,) for H in hashes])
+
+		async def init(count):
+			h0 = (
+				self.hdrs[count] if heights[count] == 0 else
+				await c.call('getblockheader',await c.call('getblockhash',heights[count]-1))
+			)
+			self.t_cur = h0['time']
+			if count == 0:
+				self.first_prev_hdr = h0
 
-		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']
-		)
+		if not self.block_list:
+			await init(0)
 
-		for height in heights:
-			await self.process_block(height,hashes.pop(0),hdrs.pop(0))
+		for n in range(len(heights)):
+			if self.block_list:
+				await init(n)
+			await self.process_block(heights[n],hashes[n],self.hdrs[n])
 
 	async def process_block(self,height,H,hdr):
 		loc = local_vars()
@@ -219,6 +304,7 @@ class BlocksInfo:
 
 		self.t_diff = hdr['time'] - self.t_cur
 		self.t_cur  = hdr['time']
+		self.total_solve_time += self.t_diff
 
 		if 'bs' in self.deps:
 			loc.bs = genesis_stats if height == 0 else await c.call('getblockstats',H,self.bs_keys)
@@ -270,47 +356,63 @@ class BlocksInfo:
 			Msg(self.fs.format(*hdr1))
 		Msg(self.fs.format(*hdr2))
 
-	async def print_summary(self):
+	async def print_range_stats(self):
+
+		# These figures don’t include the Genesis Block:
+		elapsed = self.hdrs[-1]['time'] - self.first_prev_hdr['time']
+		nblocks = self.hdrs[-1]['height'] - self.first_prev_hdr['height']
+
+		Msg('Range:      {}-{} ({} blocks [{}])'.format(
+			self.hdrs[0]['height'],
+			self.hdrs[-1]['height'],
+			self.hdrs[-1]['height'] - self.hdrs[0]['height'] + 1, # includes Genesis Block
+			secs_to_hms(elapsed) ))
+
+		if elapsed:
+			avg_bdi = int(elapsed / nblocks)
+			if 'bs' in self.deps:
+				total_blocks = len(self.hdrs)
+				rate = (self.total_bytes / 10000) / (self.total_solve_time / 36)
+				Msg_r(fmt(f"""
+					Avg size:   {self.total_bytes//total_blocks} bytes
+					Avg weight: {self.total_weight//total_blocks} bytes
+					MB/hr:      {rate:0.4f}
+					"""))
+			Msg(f'Avg BDI:    {avg_bdi/60:.2f} min')
+
+	async def print_diff_stats(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
+		# Only display stats if user-requested range ends with chain tip
+		if self.last != tip:
+			return
+
+		cur_diff_disp = 'Cur difficulty:    {:.2e}'.format(self.hdrs[-1]['difficulty'])
+		rel = tip % 2016
+
+		if rel:
+			rel_hdr = await c.call('getblockheader',await c.call('getblockhash',tip-rel))
+			tip_time = (
+				self.hdrs[-1]['time'] if self.hdrs[-1]['height'] == tip else
+				(await c.call('getblockheader',await c.call('getblockhash',tip)))['time']
+			)
+			tdiff = tip_time - rel_hdr['time']
+			if tdiff: # if the 2 timestamps are equal (very unlikely), skip display to avoid div-by-zero error
+				bdi = tdiff / rel
 				adj_pct = ((600 / bdi) - 1) * 100
-				Msg(fmt(f"""
+				Msg_r(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)
+		else:
 			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
+				Current height:    {tip}
+				{cur_diff_disp}
+				Next diff adjust:  {tip-rel+2016} (in {2016-rel} blocks)
 				"""))
 
 opts_data = {
@@ -323,10 +425,16 @@ opts_data = {
 	],
 	'text': {
 		'desc':    'Display information about a block or range of blocks',
-		'usage':   '[opts] [<block num>|-<N blocks>]+<N blocks>|<block num>[-<block num>]',
+		'usage':   '[opts] blocknum [blocknum ...] | blocknum-blocknum[+step] | [blocknum|-nBlocks]+nBlocks[+step]',
+		'usage2': [
+			'[opts] blocknum [blocknum ...]',
+			'[opts] blocknum-blocknum[+step]',
+			'[opts] [blocknum|-nBlocks]+nBlocks[+step]',
+		],
 		'options': """
 -h, --help            Print this help message
 --, --longhelp        Print help message for long options (common options)
+-D, --no-diff-stats   Omit difficulty adjustment stats from summary
 -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
@@ -338,7 +446,8 @@ opts_data = {
 -S, --no-summary      Don’t print the summary
 """,
 	'notes': """
-If no block number is specified, the current block is assumed.
+If no block number is specified, the current block is assumed.  The string
+'cur' can be used in place of the current block number.
 
 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
@@ -350,26 +459,37 @@ AVAILABLE FIELDS: {f}
 
 EXAMPLES:
 
-    # Display default info for current block:
+    # Display info for current block:
     {p}
 
-    # Display default info for blocks 1-200
-    {p} 1-200
+    # Display info for the Genesis Block:
+    {p} 0
 
-    # Display default info for 20 blocks beginning from block 600000
-    {p} 600000+20
+    # Display info for the last 20 blocks:
+    {p} +20
 
-    # Display default info for 12 blocks beginning 100 blocks from chain tip
-    {p} -- -100+12
+    # Display specified fields for blocks 165-190
+    {p} -o block,date,size,inputs,nTx 165-190
+
+    # Display info for 10 blocks beginning at block 600000:
+    {p} 600000+10
+
+    # Display info for every 5th block of 50-block range beginning at 1000
+    # blocks from chain tip:
+    {p} -- -1000+50+5
 
     # 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 specified fields for listed blocks:
+    {p} -o block,date,hash 245798 170 624044
+
+    # Display every difficulty adjustment from Genesis Block to chain tip:
+    {p} -o +difficulty 0-cur+2016
 
-    # Display 'block', 'date', 'version' and 'hash' fields for blocks 0-10:
-    {p} -o block,date,version,hash 0-10
+    # Display roughly a block a day over the last two weeks.  Note that
+    # multiplication is allowed in the nBlocks spec:
+    {p} +144*14+144
 
 This program requires a txindex-enabled daemon for correct operation.
 """.format(
@@ -380,9 +500,6 @@ This program requires a txindex-enabled daemon for correct operation.
 
 cmd_args = opts.init(opts_data)
 
-if len(cmd_args) not in (0,1):
-	opts.usage()
-
 # 'getblockstats' RPC raises exception on Genesis Block, so provide our own stats:
 genesis_stats = {
 	'avgfee': 0,
@@ -429,9 +546,12 @@ async def main():
 
 	await m.run()
 
-	if not opt.no_summary:
-		if not opt.summary:
+	if m.last and not opt.no_summary:
+		Msg('')
+		await m.print_range_stats()
+
+		if not opt.no_diff_stats:
 			Msg('')
-		await m.print_summary()
+			await m.print_diff_stats()
 
 run_session(main())