mmnode-blocks-info 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  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. mmgen-blocks-info: Display information about a block or range of blocks
  20. """
  21. import time,re
  22. from collections import namedtuple
  23. from mmgen.common import *
  24. from decimal import Decimal
  25. opts_data = {
  26. 'sets': [('raw_miner_info',True,'miner_info',True)],
  27. 'text': {
  28. 'desc': 'Display information about a range of blocks',
  29. 'usage': '[opts] +<last n blocks>|<block num>|<block num range>',
  30. 'options': """
  31. -h, --help Print this help message
  32. --, --longhelp Print help message for long options (common options)
  33. -H, --hashes Display only block numbers and hashes
  34. -m, --miner-info Display miner info in coinbase transaction
  35. -M, --raw-miner-info Display miner info in uninterpreted form
  36. -n, --no-header Don't print the column header
  37. -o, --fields= Display the specified fields
  38. -s, --summary Print the summary only
  39. -S, --no-summary Don't print the summary
  40. """,
  41. 'notes': """
  42. If no block number is specified, the current block is assumed.
  43. In addition to information about the requested range of blocks, an estimate
  44. of the next difficulty adjustment is also displayed based on the average
  45. Block Discovery Interval from the beginning of the current 2016-block period
  46. to the chain tip.
  47. Requires --txindex for correct operation.
  48. """
  49. }
  50. }
  51. class local_vars: pass
  52. class BlocksInfo:
  53. first = None
  54. last = None
  55. nblocks = None
  56. def __init__(self):
  57. self.get_block_range()
  58. self.post_init()
  59. def get_block_range(self):
  60. if not cmd_args:
  61. first = last = c.blockcount
  62. else:
  63. arg = cmd_args[0]
  64. if arg.startswith('+') and is_int(arg[1:]):
  65. last = c.blockcount
  66. first = last - int(arg[1:]) + 1
  67. elif is_int(arg):
  68. first = last = int(arg)
  69. else:
  70. try:
  71. first,last = [int(ep) for ep in arg.split('-')]
  72. except:
  73. opts.usage()
  74. if first > last:
  75. die(2,f'{first}-{last}: invalid block range')
  76. if last > c.blockcount:
  77. die(2,f'Requested block number ({last}) greater than current block height')
  78. self.first = first
  79. self.last = last
  80. self.nblocks = last - first + 1
  81. def post_init(self): pass
  82. async def run(self):
  83. for height in range(self.first,self.last+1):
  84. await self.process_block(height,await c.call('getblockhash',height))
  85. return
  86. # WIP
  87. heights = range(self.first,self.last+1)
  88. hashes = await c.gathered_call('getblockhash',[(height,) for height in heights])
  89. print(hashes)
  90. header = await c.gathered_call('getblockheader',[(H,) for H in hashes])
  91. pdie(header)
  92. def print_header(self): pass
  93. async def print_summary(self):
  94. from mmgen.util import secs_to_hms
  95. if not opt.summary:
  96. Msg('')
  97. tip = c.blockcount
  98. rel = tip % 2016
  99. if rel:
  100. H1,H2,HA,HB = await c.gathered_call('getblockhash',[[self.first],[self.last],[tip-rel],[tip]])
  101. h1,h2,hA,hB = await c.gathered_call('getblockheader',[[H1],[H2],[HA],[HB]])
  102. bdi = (hB['time']-hA['time']) / rel
  103. adj_pct = ((600 / bdi) - 1) * 100
  104. Msg_r(fmt(f"""
  105. Current height: {tip}
  106. Next diff adjust: {tip-rel+2016} (in {2016-rel} blocks [{((2016-rel)*bdi)/86400:.2f} days])
  107. BDI (cur period): {bdi/60:.2f} min
  108. Est. diff adjust: {adj_pct:+.2f}%
  109. """))
  110. else:
  111. H1,H2 = await c.gathered_call('getblockhash',[[self.first],[self.last]])
  112. h1,h2 = await c.gathered_call('getblockheader',[[H1],[H2]])
  113. Msg_r(fmt(f"""
  114. Current height: {tip}
  115. Next diff adjust: {tip-rel+2016} (in {2016-rel} blocks)
  116. """))
  117. Msg('\nRange: {}-{} ({} blocks [{}])'.format(
  118. self.first,
  119. self.last,
  120. self.nblocks,
  121. secs_to_hms(h2['time'] - h1['time']) ))
  122. class BlocksInfoOverview(BlocksInfo):
  123. total_bytes = 0
  124. total_weight = 0
  125. t_start = None
  126. t_prev = None
  127. t_cur = None
  128. bf = namedtuple('block_info_fields',['hdr1','hdr2','fs','bs_key','varname','deps','key'])
  129. # bs=getblockstats(), bh=getblockheader()
  130. # If 'bs_key' is set, it's included in self.bs_keys instead of 'key'
  131. fields = {
  132. 'block': bf('', 'Block', '{:<6}', None, 'height',[], None),
  133. 'hash': bf('', 'Hash', '{:<64}', None, 'H', [], None),
  134. 'date': bf('', 'Date', '{:<19}', None, 'df', ['bs'],None),
  135. 'interval': bf('Solve','Time ', '{:>6}', None, 'if', ['bs'],None),
  136. 'size': bf('', 'Size', '{:>7}', None, 'bs', [], 'total_size'),
  137. 'weight': bf('', 'Weight', '{:>7}', None, 'bs', [], 'total_weight'),
  138. 'utxo_inc': bf(' UTXO',' Incr', '{:>5}', None, 'bs', [], 'utxo_increase'),
  139. 'fee10': bf('10%', 'Fee', '{:>3}', 'feerate_percentiles','fp', ['bs'],0),
  140. 'fee25': bf('25%', 'Fee', '{:>3}', 'feerate_percentiles','fp', ['bs'],1),
  141. 'fee50': bf('50%', 'Fee', '{:>3}', 'feerate_percentiles','fp', ['bs'],2),
  142. 'fee75': bf('75%', 'Fee', '{:>3}', 'feerate_percentiles','fp', ['bs'],3),
  143. 'fee90': bf('90%', 'Fee', '{:>3}', 'feerate_percentiles','fp', ['bs'],4),
  144. 'fee_avg': bf('Avg', 'Fee', '{:>3}', None, 'bs', [], 'avgfeerate'),
  145. 'fee_min': bf('Min', 'Fee', '{:>3}', None, 'bs', [], 'minfeerate'),
  146. 'totalfee': bf('', 'Total Fee','{:>10}', 'totalfee', 'tf', ['bs'],None),
  147. 'outputs': bf('Out-', 'puts', '{:>5}', None, 'bs', [], 'outs'),
  148. 'inputs': bf('In- ', 'puts', '{:>5}', None, 'bs', [], 'ins'),
  149. 'version': bf('', 'Version', '{:8}', None, 'bh', [], 'versionHex'),
  150. 'nTx': bf('', 'nTx ', '{:>5}', None, 'bh', [], 'nTx'),
  151. 'subsidy': bf('', 'Subsidy', '{:7}', 'subsidy', 'su', ['bs'], None),
  152. }
  153. dfl_fields = ['block','date','interval','subsidy','totalfee','size','weight','fee50','fee25','fee10','version']
  154. funcs = {
  155. 'df': lambda self,loc: time.strftime('%Y-%m-%d %X',time.gmtime(self.t_cur)),
  156. 'if': lambda self,loc: (
  157. '-{:02}:{:02}'.format(abs(loc.t_diff)//60,abs(loc.t_diff)%60) if loc.t_diff < 0 else
  158. ' {:02}:{:02}'.format(loc.t_diff//60,loc.t_diff%60) ),
  159. 'tf': lambda self,loc: '{:.8f}'.format(loc.bs["totalfee"] * Decimal('0.00000001')),
  160. 'bh': lambda self,loc: c.call('getblockheader',loc.H),
  161. 'fp': lambda self,loc: loc.bs['feerate_percentiles'],
  162. 'su': lambda self,loc: str(loc.bs['subsidy'] * Decimal('0.00000001')).rstrip('0'),
  163. }
  164. def __init__(self):
  165. super().__init__()
  166. if opt.fields:
  167. fnames = opt.fields.split(',')
  168. for n in fnames:
  169. if n not in self.fields:
  170. die(1,f'{n!r}: unrecognized field')
  171. else:
  172. fnames = self.dfl_fields
  173. self.fvals = list(self.fields[k] for k in fnames if k in self.fields)
  174. self.fs = ' '.join( v.fs for v in self.fvals )
  175. hdr1 = [v.hdr1 for v in self.fvals]
  176. hdr2 = [v.hdr2 for v in self.fvals]
  177. self.deps = set(' '.join(v.varname + ' ' + ' '.join(v.deps) for v in self.fvals).split())
  178. self.bs_keys = [(v.bs_key or v.key) for v in self.fvals if v.bs_key or v.varname == 'bs']
  179. self.bs_keys.extend(['total_size','time'])
  180. self.ufuncs = {v.varname:self.funcs[v.varname] for v in self.fvals if v.varname in self.funcs}
  181. if opt.miner_info:
  182. self.fs += ' {}'
  183. hdr1.append(' ')
  184. hdr2.append('Miner')
  185. self.miner_pats = [re.compile(pat) for pat in (
  186. rb'[\xe3\xe4\xe5][\^/](.*?)\xfa',
  187. rb'([a-zA-Z0-9&. -]+/Mined by [a-zA-Z0-9. ]+)',
  188. rb'\x08/(.*Mined by [a-zA-Z0-9. ]+)',
  189. rb'Mined by ([a-zA-Z0-9. ]+)',
  190. rb'[/^]([a-zA-Z0-9&. /-]{5,})',
  191. rb'[/^]([a-zA-Z0-9&. /-]+)/',
  192. )]
  193. else:
  194. self.miner_pats = None
  195. if not (opt.summary or opt.no_header):
  196. Msg(self.fs.format(*hdr1))
  197. Msg(self.fs.format(*hdr2))
  198. async def get_miner_string(self,H):
  199. tx0 = (await c.call('getblock',H))['tx'][0]
  200. bd = await c.call('getrawtransaction',tx0,1)
  201. if type(bd) == tuple:
  202. return '---'
  203. else:
  204. cb = bytes.fromhex(bd['vin'][0]['coinbase'])
  205. if opt.raw_miner_info:
  206. return repr(cb)
  207. else:
  208. for pat in self.miner_pats:
  209. m = pat.search(cb)
  210. if m:
  211. return ''.join(chr(b) for b in m[1] if 31 < b < 127).strip('^').strip('/').replace('/',' ')
  212. async def process_block(self,height,H):
  213. loc = local_vars()
  214. loc.H = H
  215. loc.height = height
  216. if 'bs' in self.deps:
  217. loc.bs = await c.call('getblockstats',H,self.bs_keys)
  218. #pdie(loc.bs)
  219. self.total_bytes += loc.bs['total_size']
  220. self.total_weight += loc.bs['total_weight']
  221. self.t_cur = loc.bs['time']
  222. if self.t_prev == None:
  223. if height == 0:
  224. b_prev = loc.bs
  225. else:
  226. bh = await c.call('getblockhash',height-1)
  227. b_prev = await c.call('getblockstats',bh)
  228. self.t_start = self.t_prev = b_prev['time']
  229. loc.t_diff = self.t_cur - self.t_prev
  230. self.t_prev = self.t_cur
  231. if opt.summary:
  232. return
  233. for varname,func in self.ufuncs.items():
  234. ret = func(self,loc)
  235. if type(ret).__name__ == 'coroutine':
  236. ret = await ret
  237. setattr(loc,varname,ret)
  238. if opt.miner_info:
  239. miner_info = await self.get_miner_string(H)
  240. def gen():
  241. for v in self.fvals:
  242. if v.key is None:
  243. yield getattr(loc,v.varname)
  244. else:
  245. yield getattr(loc,v.varname)[v.key]
  246. if opt.miner_info:
  247. yield miner_info
  248. Msg(self.fs.format(*gen()))
  249. async def print_summary(self):
  250. if 'bs' in self.deps:
  251. await super().print_summary()
  252. if self.nblocks > 1:
  253. elapsed = self.t_cur - self.t_start
  254. ac = int(elapsed / self.nblocks)
  255. rate = (self.total_bytes / 10000) / (elapsed / 36)
  256. Msg_r (fmt(f"""
  257. Avg size: {self.total_bytes//self.nblocks} bytes
  258. Avg weight: {self.total_weight//self.nblocks} bytes
  259. MB/hr: {rate:0.4f}
  260. Avg BDI: {ac/60:.2f} min
  261. """))
  262. class BlocksInfoHashes(BlocksInfo):
  263. def print_header(self):
  264. Msg('{:<7} {}'.format('BLOCK','HASH'))
  265. async def run(self):
  266. heights = range(self.first,self.last+1)
  267. hashes = await c.gathered_call('getblockhash',[(height,) for height in heights])
  268. Msg('\n'.join('{:<7} {}'.format(height,H) for height,H in zip(heights,hashes)))
  269. cmd_args = opts.init(opts_data)
  270. if len(cmd_args) not in (0,1):
  271. opts.usage()
  272. async def main():
  273. from mmgen.protocol import init_proto_from_opts
  274. proto = init_proto_from_opts()
  275. from mmgen.rpc import rpc_init
  276. global c
  277. c = await rpc_init(proto)
  278. if opt.hashes:
  279. m = BlocksInfoHashes()
  280. else:
  281. m = BlocksInfoOverview()
  282. if not opt.no_header:
  283. m.print_header()
  284. await m.run()
  285. if not opt.no_summary:
  286. await m.print_summary()
  287. run_session(main())