mmnode-blocks-info 11 KB

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