diff --git a/mmnode-blocks-info b/mmnode-blocks-info index fbbb7bb..4f1f3f4 100755 --- a/mmnode-blocks-info +++ b/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) + + 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] + + # 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 = range(self.first,self.last+1) + 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]) - 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'] - ) + 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 - for height in heights: - await self.process_block(height,hashes.pop(0),hdrs.pop(0)) + if not self.block_list: + await init(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] [|-]+|[-]', + '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 'block', 'date', 'version' and 'hash' fields for blocks 0-10: - {p} -o block,date,version,hash 0-10 + # Display every difficulty adjustment from Genesis Block to chain tip: + {p} -o +difficulty 0-cur+2016 + + # 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())