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:
The MMGen Project 2021-03-16 16:25:48 +00:00
commit 780ff3647e
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2

View file

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