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
This commit is contained in:
parent
11d9ddf680
commit
780ff3647e
1 changed files with 210 additions and 90 deletions
|
|
@ -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] [<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 '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())
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue