mmnode-blocks-info 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  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. bf = namedtuple('block_info_fields',['hdr1','hdr2','fs','bs_key','varname','deps','key'])
  32. # bs=getblockstats(), bh=getblockheader()
  33. # If 'bs_key' is set, it's included in self.bs_keys instead of 'key'
  34. fields = {
  35. 'block': bf('', 'Block', '{:<6}', None, 'height',[], None),
  36. 'hash': bf('', 'Hash', '{:<64}', None, 'H', [], None),
  37. 'date': bf('', 'Date', '{:<19}', None, 'df', [], None),
  38. 'interval': bf('Solve','Time ', '{:>8}', None, 'td', [], None),
  39. 'size': bf('', 'Size', '{:>7}', None, 'bs', [], 'total_size'),
  40. 'weight': bf('', 'Weight', '{:>7}', None, 'bs', [], 'total_weight'),
  41. 'utxo_inc': bf(' UTXO',' Incr', '{:>5}', None, 'bs', [], 'utxo_increase'),
  42. 'fee10': bf('10%', 'Fee', '{:>3}', 'feerate_percentiles','fp', ['bs'], 0),
  43. 'fee25': bf('25%', 'Fee', '{:>3}', 'feerate_percentiles','fp', ['bs'], 1),
  44. 'fee50': bf('50%', 'Fee', '{:>3}', 'feerate_percentiles','fp', ['bs'], 2),
  45. 'fee75': bf('75%', 'Fee', '{:>3}', 'feerate_percentiles','fp', ['bs'], 3),
  46. 'fee90': bf('90%', 'Fee', '{:>3}', 'feerate_percentiles','fp', ['bs'], 4),
  47. 'fee_avg': bf('Avg', 'Fee', '{:>3}', None, 'bs', [], 'avgfeerate'),
  48. 'fee_min': bf('Min', 'Fee', '{:>3}', None, 'bs', [], 'minfeerate'),
  49. 'fee_max': bf('Max', 'Fee', '{:>5}', None, 'bs', [], 'maxfeerate'),
  50. 'totalfee': bf('', 'Total Fee','{:>10}', 'totalfee', 'tf', ['bs'], None),
  51. 'outputs': bf('Out-', 'puts', '{:>5}', None, 'bs', [], 'outs'),
  52. 'inputs': bf('In- ', 'puts', '{:>5}', None, 'bs', [], 'ins'),
  53. 'version': bf('', 'Version', '{:8}', None, 'bh', [], 'versionHex'),
  54. 'nTx': bf('', ' nTx ', '{:>5}', None, 'bh', [], 'nTx'),
  55. 'subsidy': bf('Sub-', 'sidy', '{:5}', 'subsidy', 'su', ['bs'], None),
  56. 'difficulty':bf('Diffi-','culty', '{:8}', None, 'di', [], None),
  57. }
  58. dfl_fields = [
  59. 'block',
  60. 'date',
  61. 'interval',
  62. 'subsidy',
  63. 'totalfee',
  64. 'size',
  65. 'weight',
  66. 'fee50',
  67. 'fee25',
  68. 'fee10',
  69. 'fee_avg',
  70. 'fee_min',
  71. 'version',
  72. ]
  73. fixed_fields = [
  74. 'block', # until ≈ 09/01/2028 (block 1000000)
  75. 'hash',
  76. 'date',
  77. 'size', # until ≈ 6x block size increase
  78. 'weight', # until ≈ 2.5x block size increase
  79. 'version',
  80. 'subsidy', # until ≈ 01/04/2028 (increases by 1 digit per halving until 9th halving [max 10 digits])
  81. 'difficulty', # until 1.00e+100 (i.e. never)
  82. ]
  83. # column width adjustment data:
  84. fs_lsqueeze = ['totalfee','inputs','outputs','nTx']
  85. fs_rsqueeze = []
  86. fs_groups = [
  87. ('fee10','fee25','fee50','fee75','fee90','fee_avg','fee_min','fee_max'),
  88. ]
  89. fs_lsqueeze2 = ['interval']
  90. funcs = {
  91. 'df': lambda self,loc: strftime('%Y-%m-%d %X',gmtime(self.t_cur)),
  92. 'td': lambda self,loc: (
  93. '-{:02}:{:02}'.format(abs(self.t_diff)//60,abs(self.t_diff)%60) if self.t_diff < 0 else
  94. ' {:02}:{:02}'.format(self.t_diff//60,self.t_diff%60) ),
  95. 'tf': lambda self,loc: '{:.8f}'.format(loc.bs["totalfee"] * Decimal('0.00000001')),
  96. 'fp': lambda self,loc: loc.bs['feerate_percentiles'],
  97. 'su': lambda self,loc: str(loc.bs['subsidy'] * Decimal('0.00000001')).rstrip('0').rstrip('.'),
  98. 'di': lambda self,loc: '{:.2e}'.format(loc.bh['difficulty']),
  99. }
  100. def __init__(self):
  101. def get_fields():
  102. if opt.fields:
  103. ufields = opt.fields.lstrip('+').split(',')
  104. for field in ufields:
  105. if field not in self.fields:
  106. die(1,f'{field!r}: unrecognized field')
  107. return self.dfl_fields + ufields if opt.fields[0] == '+' else ufields
  108. else:
  109. return self.dfl_fields
  110. def gen_fs(fnames):
  111. for i in range(len(fnames)):
  112. name = fnames[i]
  113. ls = (' ','')[name in self.fs_lsqueeze + self.fs_lsqueeze2]
  114. rs = (' ','')[name in self.fs_rsqueeze]
  115. if i < len(fnames) - 1 and fnames[i+1] in self.fs_lsqueeze2:
  116. rs = ''
  117. if i:
  118. for group in self.fs_groups:
  119. if name in group and fnames[i-1] in group:
  120. ls = ''
  121. break
  122. yield ls + self.fields[name].fs + rs
  123. self.get_block_range(cmd_args)
  124. fnames = get_fields()
  125. self.fvals = list(self.fields[name] for name in fnames)
  126. self.fs = ''.join(gen_fs(fnames)).strip()
  127. self.deps = set(' '.join(v.varname + ' ' + ' '.join(v.deps) for v in self.fvals).split())
  128. self.bs_keys = [(v.bs_key or v.key) for v in self.fvals if v.bs_key or v.varname == 'bs']
  129. self.bs_keys.extend(['total_size','total_weight'])
  130. self.ufuncs = {v.varname:self.funcs[v.varname] for v in self.fvals if v.varname in self.funcs}
  131. if opt.miner_info:
  132. self.fs += ' {}'
  133. self.miner_pats = [re.compile(pat) for pat in (
  134. rb'[\xe3\xe4\xe5][\^/](.*?)\xfa',
  135. rb'([a-zA-Z0-9&. -]+/Mined by [a-zA-Z0-9. ]+)',
  136. rb'\x08/(.*Mined by [a-zA-Z0-9. ]+)',
  137. rb'Mined by ([a-zA-Z0-9. ]+)',
  138. rb'[`]([_a-zA-Z0-9&. #/-]+)[/\xfa]',
  139. rb'[/^]([a-zA-Z0-9&. #/-]{5,})',
  140. rb'[/^]([_a-zA-Z0-9&. #/-]+)/',
  141. )]
  142. else:
  143. self.miner_pats = None
  144. def get_block_range(self,args):
  145. if not args:
  146. first = last = c.blockcount
  147. else:
  148. arg = args[0]
  149. from_current = arg[0] == '-'
  150. if arg[0] == '-':
  151. arg = arg[1:]
  152. ps = arg.split('+')
  153. if len(ps) == 2 and is_int(ps[1]):
  154. if not ps[0] and not from_current:
  155. last = c.blockcount
  156. first = last - int(arg[1:]) + 1
  157. elif is_int(ps[0]):
  158. first = (c.blockcount - int(ps[0])) if from_current else int(ps[0])
  159. last = first + int(ps[1]) - 1
  160. else:
  161. opts.usage()
  162. elif is_int(arg):
  163. first = last = (c.blockcount - int(arg)) if from_current else int(arg)
  164. else:
  165. try:
  166. assert not from_current
  167. first,last = [int(ep) for ep in arg.split('-')]
  168. except:
  169. opts.usage()
  170. if first > last:
  171. die(2,f'{first}-{last}: invalid block range')
  172. if last > c.blockcount:
  173. die(2,f'Requested block number ({last}) greater than current block height')
  174. self.first = first
  175. self.last = last
  176. async def run(self):
  177. heights = range(self.first,self.last+1)
  178. hashes = await c.gathered_call('getblockhash',[(height,) for height in heights])
  179. hdrs = await c.gathered_call('getblockheader',[(H,) for H in hashes])
  180. self.last_hdr = hdrs[-1]
  181. self.t_start = hdrs[0]['time']
  182. self.t_cur = (
  183. self.t_start if heights[0] == 0 else
  184. (await c.call('getblockheader',await c.call('getblockhash',heights[0]-1)))['time']
  185. )
  186. for height in heights:
  187. await self.process_block(height,hashes.pop(0),hdrs.pop(0))
  188. async def process_block(self,height,H,hdr):
  189. loc = local_vars()
  190. loc.height = height
  191. loc.H = H
  192. loc.bh = hdr
  193. self.t_diff = hdr['time'] - self.t_cur
  194. self.t_cur = hdr['time']
  195. if 'bs' in self.deps:
  196. loc.bs = genesis_stats if height == 0 else await c.call('getblockstats',H,self.bs_keys)
  197. self.total_bytes += loc.bs['total_size']
  198. self.total_weight += loc.bs['total_weight']
  199. if opt.summary:
  200. return
  201. for varname,func in self.ufuncs.items():
  202. setattr(loc,varname,func(self,loc))
  203. if opt.miner_info:
  204. miner_info = '-' if height == 0 else await self.get_miner_string(H)
  205. def gen():
  206. for v in self.fvals:
  207. if v.key is None:
  208. yield getattr(loc,v.varname)
  209. else:
  210. yield getattr(loc,v.varname)[v.key]
  211. if opt.miner_info:
  212. yield miner_info
  213. Msg(self.fs.format(*gen()))
  214. async def get_miner_string(self,H):
  215. tx0 = (await c.call('getblock',H))['tx'][0]
  216. bd = await c.call('getrawtransaction',tx0,1)
  217. if type(bd) == tuple:
  218. return '---'
  219. else:
  220. cb = bytes.fromhex(bd['vin'][0]['coinbase'])
  221. if opt.raw_miner_info:
  222. return repr(cb)
  223. else:
  224. for pat in self.miner_pats:
  225. m = pat.search(cb)
  226. if m:
  227. return ''.join(chr(b) for b in m[1] if 31 < b < 127).strip('^').strip('/').replace('/',' ')
  228. def print_header(self):
  229. hdr1 = [v.hdr1 for v in self.fvals]
  230. hdr2 = [v.hdr2 for v in self.fvals]
  231. if opt.miner_info:
  232. hdr1.append(' ')
  233. hdr2.append('Miner')
  234. if ''.join(hdr1).replace(' ',''):
  235. Msg(self.fs.format(*hdr1))
  236. Msg(self.fs.format(*hdr2))
  237. async def print_summary(self):
  238. tip = c.blockcount
  239. if self.last == tip:
  240. cur_diff_disp = f'Cur difficulty: {self.last_hdr["difficulty"]:.2e}'
  241. rel = tip % 2016
  242. if rel:
  243. rel_hdr = await c.call('getblockheader',await c.call('getblockhash',tip-rel))
  244. bdi = (self.last_hdr['time']-rel_hdr['time']) / rel
  245. adj_pct = ((600 / bdi) - 1) * 100
  246. Msg(fmt(f"""
  247. Current height: {tip}
  248. Next diff adjust: {tip-rel+2016} (in {2016-rel} blocks [{((2016-rel)*bdi)/86400:.2f} days])
  249. BDI (cur period): {bdi/60:.2f} min
  250. {cur_diff_disp}
  251. Est. diff adjust: {adj_pct:+.2f}%
  252. """))
  253. else:
  254. Msg(fmt(f"""
  255. Current height: {tip}
  256. {cur_diff_disp}
  257. Next diff adjust: {tip-rel+2016} (in {2016-rel} blocks)
  258. """))
  259. nblocks = self.last - self.first + 1
  260. Msg('Range: {}-{} ({} blocks [{}])'.format(
  261. self.first,
  262. self.last,
  263. nblocks,
  264. secs_to_hms(self.t_cur - self.t_start) ))
  265. if 'bs' in self.deps and nblocks > 1:
  266. elapsed = self.t_cur - self.t_start
  267. ac = int(elapsed / nblocks)
  268. rate = (self.total_bytes / 10000) / (elapsed / 36)
  269. Msg_r(fmt(f"""
  270. Avg size: {self.total_bytes//nblocks} bytes
  271. Avg weight: {self.total_weight//nblocks} bytes
  272. MB/hr: {rate:0.4f}
  273. Avg BDI: {ac/60:.2f} min
  274. """))
  275. opts_data = {
  276. 'sets': [
  277. ('raw_miner_info', True, 'miner_info', True),
  278. ('summary', True, 'raw_miner_info', False),
  279. ('summary', True, 'miner_info', False),
  280. ('hashes', True, 'fields', 'block,hash'),
  281. ('hashes', True, 'no_summary', True),
  282. ],
  283. 'text': {
  284. 'desc': 'Display information about a block or range of blocks',
  285. 'usage': '[opts] [<block num>|-<N blocks>]+<N blocks>|<block num>[-<block num>]',
  286. 'options': """
  287. -h, --help Print this help message
  288. --, --longhelp Print help message for long options (common options)
  289. -H, --hashes Display only block numbers and hashes
  290. -m, --miner-info Display miner info in coinbase transaction
  291. -M, --raw-miner-info Display miner info in uninterpreted form
  292. -n, --no-header Don’t print the column header
  293. -o, --fields= Display the specified fields (comma-separated list)
  294. See AVAILABLE FIELDS below. If the first character
  295. is '+', fields are appended to the defaults.
  296. -s, --summary Print the summary only
  297. -S, --no-summary Don’t print the summary
  298. """,
  299. 'notes': """
  300. If no block number is specified, the current block is assumed.
  301. If the requested range ends at the current chain tip, an estimate of the next
  302. difficulty adjustment is also displayed. The estimate is based on the average
  303. Block Discovery Interval from the beginning of the current 2016-block period.
  304. All fee fields except for 'totalfee' are in satoshis per virtual byte.
  305. AVAILABLE FIELDS: {f}
  306. EXAMPLES:
  307. # Display default info for current block:
  308. {p}
  309. # Display default info for blocks 1-200
  310. {p} 1-200
  311. # Display default info for 20 blocks beginning from block 600000
  312. {p} 600000+20
  313. # Display default info for 12 blocks beginning 100 blocks from chain tip
  314. {p} -- -100+12
  315. # Display info for block 152817, adding miner field:
  316. {p} --miner-info 152817
  317. # Display info for last 10 blocks, adding 'inputs' and 'nTx' fields:
  318. {p} -o +inputs,nTx +10
  319. # Display 'block', 'date', 'version' and 'hash' fields for blocks 0-10:
  320. {p} -o block,date,version,hash 0-10
  321. This program requires a txindex-enabled daemon for correct operation.
  322. """.format(
  323. f = fmt_list(BlocksInfo.fields,fmt='bare'),
  324. p = g.prog_name )
  325. }
  326. }
  327. cmd_args = opts.init(opts_data)
  328. if len(cmd_args) not in (0,1):
  329. opts.usage()
  330. # 'getblockstats' RPC raises exception on Genesis Block, so provide our own stats:
  331. genesis_stats = {
  332. 'avgfee': 0,
  333. 'avgfeerate': 0,
  334. 'avgtxsize': 0,
  335. 'feerate_percentiles': [ 0, 0, 0, 0, 0 ],
  336. 'height': 0,
  337. 'ins': 0,
  338. 'maxfee': 0,
  339. 'maxfeerate': 0,
  340. 'maxtxsize': 0,
  341. 'medianfee': 0,
  342. 'mediantxsize': 0,
  343. 'minfee': 0,
  344. 'minfeerate': 0,
  345. 'mintxsize': 0,
  346. 'outs': 1,
  347. 'subsidy': 5000000000,
  348. 'swtotal_size': 0,
  349. 'swtotal_weight': 0,
  350. 'swtxs': 0,
  351. 'total_out': 0,
  352. 'total_size': 0,
  353. 'total_weight': 0,
  354. 'totalfee': 0,
  355. 'txs': 1,
  356. 'utxo_increase': 1,
  357. 'utxo_size_inc': 117
  358. }
  359. async def main():
  360. from mmgen.protocol import init_proto_from_opts
  361. proto = init_proto_from_opts()
  362. from mmgen.rpc import rpc_init
  363. global c
  364. c = await rpc_init(proto)
  365. m = BlocksInfo()
  366. if not (opt.summary or opt.no_header):
  367. m.print_header()
  368. await m.run()
  369. if not opt.no_summary:
  370. if not opt.summary:
  371. Msg('')
  372. await m.print_summary()
  373. run_session(main())