|
@@ -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())
|