mmnode-blocks-info 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553
  1. #!/usr/bin/env python3
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C)2013-2021 The MMGen Project <mmgen@tuta.io>
  5. #
  6. # This program is free software: you can redistribute it and/or modify it under
  7. # the terms of the GNU General Public License as published by the Free Software
  8. # Foundation, either version 3 of the License, or (at your option) any later
  9. # version.
  10. #
  11. # This program is distributed in the hope that it will be useful, but WITHOUT
  12. # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  13. # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
  14. # details.
  15. #
  16. # You should have received a copy of the GNU General Public License along with
  17. # this program. If not, see <http://www.gnu.org/licenses/>.
  18. """
  19. mmnode-blocks-info: Display information about a block or range of blocks
  20. """
  21. import re
  22. from collections import namedtuple
  23. from time import strftime,gmtime
  24. from mmgen.common import *
  25. from mmgen.util import secs_to_hms
  26. from decimal import Decimal
  27. class local_vars: pass
  28. class BlocksInfo:
  29. total_bytes = 0
  30. total_weight = 0
  31. total_solve_time = 0
  32. step = None
  33. bf = namedtuple('block_info_fields',['hdr1','hdr2','fs','bs_key','varname','deps','key'])
  34. # bs=getblockstats(), bh=getblockheader()
  35. # If 'bs_key' is set, it's included in self.bs_keys instead of 'key'
  36. fields = {
  37. 'block': bf('', 'Block', '{:<6}', None, 'height',[], None),
  38. 'hash': bf('', 'Hash', '{:<64}', None, 'H', [], None),
  39. 'date': bf('', 'Date', '{:<19}', None, 'df', [], None),
  40. 'interval': bf('Solve','Time ', '{:>8}', None, 'td', [], None),
  41. 'size': bf('', 'Size', '{:>7}', None, 'bs', [], 'total_size'),
  42. 'weight': bf('', 'Weight', '{:>7}', None, 'bs', [], 'total_weight'),
  43. 'utxo_inc': bf(' UTXO',' Incr', '{:>5}', None, 'bs', [], 'utxo_increase'),
  44. 'fee10': bf('10%', 'Fee', '{:>3}', 'feerate_percentiles','fp', ['bs'], 0),
  45. 'fee25': bf('25%', 'Fee', '{:>3}', 'feerate_percentiles','fp', ['bs'], 1),
  46. 'fee50': bf('50%', 'Fee', '{:>3}', 'feerate_percentiles','fp', ['bs'], 2),
  47. 'fee75': bf('75%', 'Fee', '{:>3}', 'feerate_percentiles','fp', ['bs'], 3),
  48. 'fee90': bf('90%', 'Fee', '{:>3}', 'feerate_percentiles','fp', ['bs'], 4),
  49. 'fee_avg': bf('Avg', 'Fee', '{:>3}', None, 'bs', [], 'avgfeerate'),
  50. 'fee_min': bf('Min', 'Fee', '{:>3}', None, 'bs', [], 'minfeerate'),
  51. 'fee_max': bf('Max', 'Fee', '{:>5}', None, 'bs', [], 'maxfeerate'),
  52. 'totalfee': bf('', 'Total Fee','{:>10}', 'totalfee', 'tf', ['bs'], None),
  53. 'outputs': bf('Out-', 'puts', '{:>5}', None, 'bs', [], 'outs'),
  54. 'inputs': bf('In- ', 'puts', '{:>5}', None, 'bs', [], 'ins'),
  55. 'version': bf('', 'Version', '{:8}', None, 'bh', [], 'versionHex'),
  56. 'nTx': bf('', ' nTx ', '{:>5}', None, 'bh', [], 'nTx'),
  57. 'subsidy': bf('Sub-', 'sidy', '{:5}', 'subsidy', 'su', ['bs'], None),
  58. 'difficulty':bf('Diffi-','culty', '{:8}', None, 'di', [], None),
  59. }
  60. dfl_fields = [
  61. 'block',
  62. 'date',
  63. 'interval',
  64. 'subsidy',
  65. 'totalfee',
  66. 'size',
  67. 'weight',
  68. 'fee50',
  69. 'fee25',
  70. 'fee10',
  71. 'fee_avg',
  72. 'fee_min',
  73. 'version',
  74. ]
  75. fixed_fields = [
  76. 'block', # until ≈ 09/01/2028 (block 1000000)
  77. 'hash',
  78. 'date',
  79. 'size', # until ≈ 6x block size increase
  80. 'weight', # until ≈ 2.5x block size increase
  81. 'version',
  82. 'subsidy', # until ≈ 01/04/2028 (increases by 1 digit per halving until 9th halving [max 10 digits])
  83. 'difficulty', # until 1.00e+100 (i.e. never)
  84. ]
  85. # column width adjustment data:
  86. fs_lsqueeze = ['totalfee','inputs','outputs','nTx']
  87. fs_rsqueeze = []
  88. fs_groups = [
  89. ('fee10','fee25','fee50','fee75','fee90','fee_avg','fee_min','fee_max'),
  90. ]
  91. fs_lsqueeze2 = ['interval']
  92. funcs = {
  93. 'df': lambda self,loc: strftime('%Y-%m-%d %X',gmtime(self.t_cur)),
  94. 'td': lambda self,loc: (
  95. '-{:02}:{:02}'.format(abs(self.t_diff)//60,abs(self.t_diff)%60) if self.t_diff < 0 else
  96. ' {:02}:{:02}'.format(self.t_diff//60,self.t_diff%60) ),
  97. 'tf': lambda self,loc: '{:.8f}'.format(loc.bs["totalfee"] * Decimal('0.00000001')),
  98. 'fp': lambda self,loc: loc.bs['feerate_percentiles'],
  99. 'su': lambda self,loc: str(loc.bs['subsidy'] * Decimal('0.00000001')).rstrip('0').rstrip('.'),
  100. 'di': lambda self,loc: '{:.2e}'.format(loc.bh['difficulty']),
  101. }
  102. def __init__(self):
  103. def get_fields():
  104. if opt.fields:
  105. ufields = opt.fields.lstrip('+').split(',')
  106. for field in ufields:
  107. if field not in self.fields:
  108. die(1,f'{field!r}: unrecognized field')
  109. return self.dfl_fields + ufields if opt.fields[0] == '+' else ufields
  110. else:
  111. return self.dfl_fields
  112. def gen_fs(fnames):
  113. for i in range(len(fnames)):
  114. name = fnames[i]
  115. ls = (' ','')[name in self.fs_lsqueeze + self.fs_lsqueeze2]
  116. rs = (' ','')[name in self.fs_rsqueeze]
  117. if i < len(fnames) - 1 and fnames[i+1] in self.fs_lsqueeze2:
  118. rs = ''
  119. if i:
  120. for group in self.fs_groups:
  121. if name in group and fnames[i-1] in group:
  122. ls = ''
  123. break
  124. yield ls + self.fields[name].fs + rs
  125. self.block_list,self.first,self.last = self.parse_block_range(cmd_args)
  126. fnames = get_fields()
  127. self.fvals = list(self.fields[name] for name in fnames)
  128. self.fs = ''.join(gen_fs(fnames)).strip()
  129. self.deps = set(' '.join(v.varname + ' ' + ' '.join(v.deps) for v in self.fvals).split())
  130. self.bs_keys = [(v.bs_key or v.key) for v in self.fvals if v.bs_key or v.varname == 'bs']
  131. self.bs_keys.extend(['total_size','total_weight'])
  132. self.ufuncs = {v.varname:self.funcs[v.varname] for v in self.fvals if v.varname in self.funcs}
  133. if opt.miner_info:
  134. self.fs += ' {}'
  135. self.miner_pats = [re.compile(pat) for pat in (
  136. rb'[\xe3\xe4\xe5][\^/](.*?)\xfa',
  137. rb'([a-zA-Z0-9&. -]+/Mined by [a-zA-Z0-9. ]+)',
  138. rb'\x08/(.*Mined by [a-zA-Z0-9. ]+)',
  139. rb'Mined by ([a-zA-Z0-9. ]+)',
  140. rb'[`]([_a-zA-Z0-9&. #/-]+)[/\xfa]',
  141. rb'[/^]([a-zA-Z0-9&. #/-]{5,})',
  142. rb'[/^]([_a-zA-Z0-9&. #/-]+)/',
  143. )]
  144. else:
  145. self.miner_pats = None
  146. def parse_block_range(self,args):
  147. def conv_blkspec(arg):
  148. if arg == 'cur':
  149. return c.blockcount
  150. elif is_int(arg):
  151. if int(arg) < 0:
  152. die(1,f'{arg}: block number must be non-negative')
  153. elif int(arg) > c.blockcount:
  154. die(1,f'{arg}: requested block height greater than current chain tip!')
  155. else:
  156. return int(arg)
  157. else:
  158. die(1,f'{arg}: invalid block specifier')
  159. def check_nblocks(arg):
  160. if arg <= 0:
  161. die(1,'nBlocks must be a positive integer')
  162. if arg > c.blockcount:
  163. die(1, f"'{arg}': nBlocks must be less than current chain height")
  164. return arg
  165. def parse_rangespec(arg):
  166. class RangeParser:
  167. debug = True
  168. def __init__(self,arg):
  169. self.arg = self.orig_arg = arg
  170. def parse(self,target):
  171. ret = getattr(self,'parse_'+target)()
  172. if self.debug: print(f'arg after parse({target}): {self.arg}')
  173. return ret
  174. def finalize(self):
  175. if self.arg:
  176. die(1,f'{self.orig_arg!r}: invalid range specifier')
  177. def parse_from_tip(self):
  178. m = re.match(r'-([0-9]+)(.*)',self.arg)
  179. if m:
  180. res,self.arg = (m[1],m[2])
  181. return check_nblocks(int(res))
  182. def parse_abs_range(self):
  183. m = re.match(r'([^+-]+)(-([^+-]+)){0,1}(.*)',self.arg)
  184. if m:
  185. if self.debug: print(f'abs_range parse: first={m[1]}, last={m[3]}')
  186. self.arg = m[4]
  187. return (
  188. conv_blkspec(m[1]),
  189. conv_blkspec(m[3]) if m[3] else None
  190. )
  191. return (None,None)
  192. def parse_add(self):
  193. m = re.match(r'\+([0-9*]+)(.*)',self.arg)
  194. if m:
  195. res,self.arg = (m[1],m[2])
  196. if res.strip('*') != res:
  197. die(1,f"'+{res}': malformed nBlocks specifier")
  198. if len(res) > 30:
  199. die(1,f"'+{res}': overly long nBlocks specifier")
  200. return check_nblocks(eval(res)) # res is only digits plus '*', so eval safe
  201. range_data = namedtuple('parsed_range_data',['first','last','from_tip','nblocks','step'])
  202. p = RangeParser(arg)
  203. from_tip = p.parse('from_tip')
  204. first,last = (c.blockcount-from_tip,None) if from_tip else p.parse('abs_range')
  205. add1 = p.parse('add')
  206. add2 = p.parse('add')
  207. p.finalize()
  208. if add2 and last is not None:
  209. die(1,f'{arg!r}: invalid range specifier')
  210. nblocks,step = (add1,add2) if last is None else (None,add1)
  211. if p.debug: print(range_data(first,last,from_tip,nblocks,step))
  212. if nblocks:
  213. if first == None:
  214. first = c.blockcount - nblocks + 1
  215. last = first + nblocks - 1
  216. first = conv_blkspec(first)
  217. last = conv_blkspec(last or first)
  218. if p.debug: print(range_data(first,last,from_tip,nblocks,step))
  219. if first > last:
  220. die(1,f'{first}-{last}: invalid block range')
  221. return range_data(first,last,from_tip,nblocks,step)
  222. # return (block_list,first,last)
  223. if not args:
  224. return (None,c.blockcount,c.blockcount)
  225. elif len(args) == 1:
  226. r = parse_rangespec(args[0])
  227. return (
  228. list(range(r.first,r.last+1,r.step)) if r.step else None,
  229. r.first,
  230. r.last
  231. )
  232. else:
  233. return ([conv_blkspec(a) for a in args],None,None)
  234. async def run(self):
  235. heights = self.block_list or range(self.first,self.last+1)
  236. hashes = await c.gathered_call('getblockhash',[(height,) for height in heights])
  237. self.hdrs = await c.gathered_call('getblockheader',[(H,) for H in hashes])
  238. async def init(count):
  239. h0 = (
  240. self.hdrs[count] if heights[count] == 0 else
  241. await c.call('getblockheader',await c.call('getblockhash',heights[count]-1))
  242. )
  243. self.t_cur = h0['time']
  244. if count == 0:
  245. self.first_prev_hdr = h0
  246. if not self.block_list:
  247. await init(0)
  248. for n in range(len(heights)):
  249. if self.block_list:
  250. await init(n)
  251. await self.process_block(heights[n],hashes[n],self.hdrs[n])
  252. async def process_block(self,height,H,hdr):
  253. loc = local_vars()
  254. loc.height = height
  255. loc.H = H
  256. loc.bh = hdr
  257. self.t_diff = hdr['time'] - self.t_cur
  258. self.t_cur = hdr['time']
  259. self.total_solve_time += self.t_diff
  260. if 'bs' in self.deps:
  261. loc.bs = genesis_stats if height == 0 else await c.call('getblockstats',H,self.bs_keys)
  262. self.total_bytes += loc.bs['total_size']
  263. self.total_weight += loc.bs['total_weight']
  264. if opt.summary:
  265. return
  266. for varname,func in self.ufuncs.items():
  267. setattr(loc,varname,func(self,loc))
  268. if opt.miner_info:
  269. miner_info = '-' if height == 0 else await self.get_miner_string(H)
  270. def gen():
  271. for v in self.fvals:
  272. if v.key is None:
  273. yield getattr(loc,v.varname)
  274. else:
  275. yield getattr(loc,v.varname)[v.key]
  276. if opt.miner_info:
  277. yield miner_info
  278. Msg(self.fs.format(*gen()))
  279. async def get_miner_string(self,H):
  280. tx0 = (await c.call('getblock',H))['tx'][0]
  281. bd = await c.call('getrawtransaction',tx0,1)
  282. if type(bd) == tuple:
  283. return '---'
  284. else:
  285. cb = bytes.fromhex(bd['vin'][0]['coinbase'])
  286. if opt.raw_miner_info:
  287. return repr(cb)
  288. else:
  289. for pat in self.miner_pats:
  290. m = pat.search(cb)
  291. if m:
  292. return ''.join(chr(b) for b in m[1] if 31 < b < 127).strip('^').strip('/').replace('/',' ')
  293. def print_header(self):
  294. hdr1 = [v.hdr1 for v in self.fvals]
  295. hdr2 = [v.hdr2 for v in self.fvals]
  296. if opt.miner_info:
  297. hdr1.append(' ')
  298. hdr2.append('Miner')
  299. if ''.join(hdr1).replace(' ',''):
  300. Msg(self.fs.format(*hdr1))
  301. Msg(self.fs.format(*hdr2))
  302. async def print_range_stats(self):
  303. # These figures don’t include the Genesis Block:
  304. elapsed = self.hdrs[-1]['time'] - self.first_prev_hdr['time']
  305. nblocks = self.hdrs[-1]['height'] - self.first_prev_hdr['height']
  306. Msg('Range: {}-{} ({} blocks [{}])'.format(
  307. self.hdrs[0]['height'],
  308. self.hdrs[-1]['height'],
  309. self.hdrs[-1]['height'] - self.hdrs[0]['height'] + 1, # includes Genesis Block
  310. secs_to_hms(elapsed) ))
  311. if elapsed:
  312. avg_bdi = int(elapsed / nblocks)
  313. if 'bs' in self.deps:
  314. total_blocks = len(self.hdrs)
  315. rate = (self.total_bytes / 10000) / (self.total_solve_time / 36)
  316. Msg_r(fmt(f"""
  317. Avg size: {self.total_bytes//total_blocks} bytes
  318. Avg weight: {self.total_weight//total_blocks} bytes
  319. MB/hr: {rate:0.4f}
  320. """))
  321. Msg(f'Avg BDI: {avg_bdi/60:.2f} min')
  322. async def print_diff_stats(self):
  323. tip = c.blockcount
  324. # Only display stats if user-requested range ends with chain tip
  325. if self.last != tip:
  326. return
  327. cur_diff_disp = 'Cur difficulty: {:.2e}'.format(self.hdrs[-1]['difficulty'])
  328. rel = tip % 2016
  329. if rel:
  330. rel_hdr = await c.call('getblockheader',await c.call('getblockhash',tip-rel))
  331. tip_time = (
  332. self.hdrs[-1]['time'] if self.hdrs[-1]['height'] == tip else
  333. (await c.call('getblockheader',await c.call('getblockhash',tip)))['time']
  334. )
  335. tdiff = tip_time - rel_hdr['time']
  336. if tdiff: # if the 2 timestamps are equal (very unlikely), skip display to avoid div-by-zero error
  337. bdi = tdiff / rel
  338. adj_pct = ((600 / bdi) - 1) * 100
  339. Msg_r(fmt(f"""
  340. Current height: {tip}
  341. Next diff adjust: {tip-rel+2016} (in {2016-rel} blocks [{((2016-rel)*bdi)/86400:.2f} days])
  342. BDI (cur period): {bdi/60:.2f} min
  343. {cur_diff_disp}
  344. Est. diff adjust: {adj_pct:+.2f}%
  345. """))
  346. else:
  347. Msg_r(fmt(f"""
  348. Current height: {tip}
  349. {cur_diff_disp}
  350. Next diff adjust: {tip-rel+2016} (in {2016-rel} blocks)
  351. """))
  352. opts_data = {
  353. 'sets': [
  354. ('raw_miner_info', True, 'miner_info', True),
  355. ('summary', True, 'raw_miner_info', False),
  356. ('summary', True, 'miner_info', False),
  357. ('hashes', True, 'fields', 'block,hash'),
  358. ('hashes', True, 'no_summary', True),
  359. ],
  360. 'text': {
  361. 'desc': 'Display information about a block or range of blocks',
  362. 'usage': '[opts] blocknum [blocknum ...] | blocknum-blocknum[+step] | [blocknum|-nBlocks]+nBlocks[+step]',
  363. 'usage2': [
  364. '[opts] blocknum [blocknum ...]',
  365. '[opts] blocknum-blocknum[+step]',
  366. '[opts] [blocknum|-nBlocks]+nBlocks[+step]',
  367. ],
  368. 'options': """
  369. -h, --help Print this help message
  370. --, --longhelp Print help message for long options (common options)
  371. -D, --no-diff-stats Omit difficulty adjustment stats from summary
  372. -H, --hashes Display only block numbers and hashes
  373. -m, --miner-info Display miner info in coinbase transaction
  374. -M, --raw-miner-info Display miner info in uninterpreted form
  375. -n, --no-header Don’t print the column header
  376. -o, --fields= Display the specified fields (comma-separated list)
  377. See AVAILABLE FIELDS below. If the first character
  378. is '+', fields are appended to the defaults.
  379. -s, --summary Print the summary only
  380. -S, --no-summary Don’t print the summary
  381. """,
  382. 'notes': """
  383. If no block number is specified, the current block is assumed. The string
  384. 'cur' can be used in place of the current block number.
  385. If the requested range ends at the current chain tip, an estimate of the next
  386. difficulty adjustment is also displayed. The estimate is based on the average
  387. Block Discovery Interval from the beginning of the current 2016-block period.
  388. All fee fields except for 'totalfee' are in satoshis per virtual byte.
  389. AVAILABLE FIELDS: {f}
  390. EXAMPLES:
  391. # Display info for current block:
  392. {p}
  393. # Display info for the Genesis Block:
  394. {p} 0
  395. # Display info for the last 20 blocks:
  396. {p} +20
  397. # Display specified fields for blocks 165-190
  398. {p} -o block,date,size,inputs,nTx 165-190
  399. # Display info for 10 blocks beginning at block 600000:
  400. {p} 600000+10
  401. # Display info for every 5th block of 50-block range beginning at 1000
  402. # blocks from chain tip:
  403. {p} -- -1000+50+5
  404. # Display info for block 152817, adding miner field:
  405. {p} --miner-info 152817
  406. # Display specified fields for listed blocks:
  407. {p} -o block,date,hash 245798 170 624044
  408. # Display every difficulty adjustment from Genesis Block to chain tip:
  409. {p} -o +difficulty 0-cur+2016
  410. # Display roughly a block a day over the last two weeks. Note that
  411. # multiplication is allowed in the nBlocks spec:
  412. {p} +144*14+144
  413. This program requires a txindex-enabled daemon for correct operation.
  414. """.format(
  415. f = fmt_list(BlocksInfo.fields,fmt='bare'),
  416. p = g.prog_name )
  417. }
  418. }
  419. cmd_args = opts.init(opts_data)
  420. # 'getblockstats' RPC raises exception on Genesis Block, so provide our own stats:
  421. genesis_stats = {
  422. 'avgfee': 0,
  423. 'avgfeerate': 0,
  424. 'avgtxsize': 0,
  425. 'feerate_percentiles': [ 0, 0, 0, 0, 0 ],
  426. 'height': 0,
  427. 'ins': 0,
  428. 'maxfee': 0,
  429. 'maxfeerate': 0,
  430. 'maxtxsize': 0,
  431. 'medianfee': 0,
  432. 'mediantxsize': 0,
  433. 'minfee': 0,
  434. 'minfeerate': 0,
  435. 'mintxsize': 0,
  436. 'outs': 1,
  437. 'subsidy': 5000000000,
  438. 'swtotal_size': 0,
  439. 'swtotal_weight': 0,
  440. 'swtxs': 0,
  441. 'total_out': 0,
  442. 'total_size': 0,
  443. 'total_weight': 0,
  444. 'totalfee': 0,
  445. 'txs': 1,
  446. 'utxo_increase': 1,
  447. 'utxo_size_inc': 117
  448. }
  449. async def main():
  450. from mmgen.protocol import init_proto_from_opts
  451. proto = init_proto_from_opts()
  452. from mmgen.rpc import rpc_init
  453. global c
  454. c = await rpc_init(proto)
  455. m = BlocksInfo()
  456. if not (opt.summary or opt.no_header):
  457. m.print_header()
  458. await m.run()
  459. if m.last and not opt.no_summary:
  460. Msg('')
  461. await m.print_range_stats()
  462. if not opt.no_diff_stats:
  463. Msg('')
  464. await m.print_diff_stats()
  465. run_session(main())