BlocksInfo.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756
  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_node_tools.BlocksInfo: Display information about a block or range of blocks
  20. """
  21. import re, json
  22. from collections import namedtuple
  23. from time import strftime, gmtime
  24. from decimal import Decimal
  25. from mmgen.util import msg, Msg, Msg_r, die, suf, secs_to_ms, secs_to_dhms, is_int
  26. from mmgen.rpc.util import json_encoder
  27. class RangeParser:
  28. debug = False
  29. def __init__(self, caller, arg):
  30. self.caller = caller
  31. self.arg = self.orig_arg = arg
  32. def parse(self, target):
  33. ret = getattr(self, 'parse_'+target)()
  34. if self.debug:
  35. msg(f'arg after parse({target}): {self.arg}')
  36. return ret
  37. def finalize(self):
  38. if self.arg:
  39. die(1, f'{self.orig_arg!r}: invalid range specifier')
  40. def parse_from_tip(self):
  41. m = re.match(r'-([0-9]+)(.*)', self.arg)
  42. if m:
  43. res, self.arg = (m[1], m[2])
  44. return self.caller.check_nblocks(int(res))
  45. def parse_abs_range(self):
  46. m = re.match(r'([^+-]+)(-([^+-]+)){0,1}(.*)', self.arg)
  47. if m:
  48. if self.debug:
  49. msg(f'abs_range parse: first={m[1]}, last={m[3]}')
  50. self.arg = m[4]
  51. return (
  52. self.caller.conv_blkspec(m[1]),
  53. self.caller.conv_blkspec(m[3]) if m[3] else None)
  54. return (None, None)
  55. def parse_add(self):
  56. m = re.match(r'\+([0-9*]+)(.*)', self.arg)
  57. if m:
  58. res, self.arg = (m[1], m[2])
  59. if res.strip('*') != res:
  60. die(1, f"'+{res}': malformed nBlocks specifier")
  61. if len(res) > 30:
  62. die(1, f"'+{res}': overly long nBlocks specifier")
  63. return self.caller.check_nblocks(eval(res)) # res is only digits plus '*', so eval safe
  64. class BlocksInfo:
  65. total_bytes = 0
  66. total_weight = 0
  67. total_solve_time = 0
  68. header_printed = False
  69. bf = namedtuple('block_info_fields', ['fmt_func', 'src', 'fs', 'hdr1', 'hdr2', 'key1', 'key2'])
  70. # bh=getblockheader, bs=getblockstats, lo=local
  71. fields = {
  72. 'block': bf(None, 'bh', '{:<6}', '', 'Block', 'height', None),
  73. 'hash': bf(None, 'bh', '{:<64}', '', 'Hash', 'hash', None),
  74. 'date': bf('da', 'bh', '{:<19}', '', 'Date', 'time', None),
  75. 'interval': bf('td', 'lo', '{:>8}', 'Solve', 'Time ', 'interval', None),
  76. 'subsidy': bf('su', 'bs', '{:<5}', 'Sub-', 'sidy', 'subsidy', None),
  77. 'totalfee': bf('tf', 'bs', '{:>10}', '', 'Total Fee', 'totalfee', None),
  78. 'size': bf(None, 'bs', '{:>7}', '', 'Size', 'total_size', None),
  79. 'weight': bf(None, 'bs', '{:>7}', '', 'Weight', 'total_weight', None),
  80. 'fee90': bf('fe', 'bs', '{:>3}', '90%', 'Fee', 'feerate_percentiles', 4),
  81. 'fee75': bf('fe', 'bs', '{:>3}', '75%', 'Fee', 'feerate_percentiles', 3),
  82. 'fee50': bf('fe', 'bs', '{:>3}', '50%', 'Fee', 'feerate_percentiles', 2),
  83. 'fee25': bf('fe', 'bs', '{:>3}', '25%', 'Fee', 'feerate_percentiles', 1),
  84. 'fee10': bf('fe', 'bs', '{:>3}', '10%', 'Fee', 'feerate_percentiles', 0),
  85. 'fee_max': bf('fe', 'bs', '{:>5}', 'Max', 'Fee', 'maxfeerate', None),
  86. 'fee_avg': bf('fe', 'bs', '{:>3}', 'Avg', 'Fee', 'avgfeerate', None),
  87. 'fee_min': bf('fe', 'bs', '{:>3}', 'Min', 'Fee', 'minfeerate', None),
  88. 'nTx': bf(None, 'bh', '{:>5}', '', ' nTx ', 'nTx', None),
  89. 'inputs': bf(None, 'bs', '{:>5}', 'In- ', 'puts', 'ins', None),
  90. 'outputs': bf(None, 'bs', '{:>5}', 'Out-', 'puts', 'outs', None),
  91. 'utxo_inc': bf(None, 'bs', '{:>6}', ' UTXO', ' Incr', 'utxo_increase', None),
  92. 'version': bf(None, 'bh', '{:<8}', '', 'Version', 'versionHex', None),
  93. 'difficulty': bf('di', 'bh', '{:<8}', 'Diffi-','culty', 'difficulty', None),
  94. 'miner': bf(None, 'lo', '{:<5}', '', 'Miner', 'miner', None)}
  95. dfl_fields = (
  96. 'block',
  97. 'date',
  98. 'interval',
  99. 'subsidy',
  100. 'totalfee',
  101. 'size',
  102. 'weight',
  103. 'fee50',
  104. 'fee25',
  105. 'fee10',
  106. 'fee_avg',
  107. 'fee_min',
  108. 'version')
  109. fixed_fields = (
  110. 'block', # until ≈ 09/01/2028 (block 1000000)
  111. 'hash',
  112. 'date',
  113. 'size', # until ≈ 6x block size increase
  114. 'weight', # until ≈ 2.5x block size increase
  115. 'version',
  116. 'subsidy', # until ≈ 01/04/2028 (increases by 1 digit per halving until 9th halving [max 10 digits])
  117. 'difficulty') # until 1.00e+100 (i.e. never)
  118. # column width adjustment data:
  119. fs_lsqueeze = ('totalfee', 'inputs', 'outputs', 'nTx')
  120. fs_rsqueeze = ()
  121. fs_groups = (
  122. ('fee10', 'fee25', 'fee50', 'fee75', 'fee90', 'fee_avg', 'fee_min', 'fee_max'))
  123. fs_lsqueeze2 = ('interval',)
  124. all_stats = ['col_avg', 'range', 'avg', 'mini_avg', 'total', 'diff']
  125. dfl_stats = ['range', 'mini_avg', 'diff']
  126. noindent_stats = ['col_avg']
  127. avg_stats_skip = {'block', 'hash', 'date', 'version', 'miner'}
  128. range_data = namedtuple('parsed_range_data', ['first', 'last', 'from_tip', 'nblocks', 'step'])
  129. t_fmt = lambda self, t: f'{t/86400:.2f} days' if t > 172800 else f'{t/3600:.2f} hrs'
  130. @classmethod
  131. def parse_cslist(cls, uarg, full_set, dfl_set, desc):
  132. def make_list(m, func):
  133. groups_lc = [set(e.lower() for e in gi.split(',')) for gi in m.groups()]
  134. for group in groups_lc:
  135. for e in group:
  136. if e not in full_set_lc:
  137. die(1, f'{e!r}: unrecognized {desc}')
  138. # display elements in order:
  139. return [e for e in full_set if e.lower() in func(groups_lc)]
  140. full_set_lc = set(e.lower() for e in full_set)
  141. dfl_set_lc = set(e.lower() for e in dfl_set)
  142. cspat = r'(\w+(?:,\w+)*)'
  143. for pat, func in (
  144. (rf'{cspat}$', lambda g: g[0]),
  145. (rf'\+{cspat}$', lambda g: dfl_set_lc | g[0]),
  146. (rf'\-{cspat}$', lambda g: dfl_set_lc - g[0]),
  147. (rf'\+{cspat}\-{cspat}$', lambda g: (dfl_set_lc | g[0]) - g[1]),
  148. (rf'\-{cspat}\+{cspat}$', lambda g: (dfl_set_lc - g[0]) | g[1]),
  149. (rf'all\-{cspat}$', lambda g: full_set_lc - g[0])):
  150. m = re.match(pat, uarg, re.ASCII|re.IGNORECASE)
  151. if m:
  152. return make_list(m, func)
  153. else:
  154. die(1, f'{uarg}: invalid parameter')
  155. def __init__(self, cfg, cmd_args, rpc):
  156. def parse_cs_uarg(uarg, full_set, dfl_set, desc):
  157. return (
  158. full_set if uarg == 'all' else [] if uarg == 'none' else
  159. self.parse_cslist(uarg, full_set, dfl_set, desc))
  160. def get_fields():
  161. return parse_cs_uarg(self.cfg.fields, list(self.fields), self.dfl_fields, 'field')
  162. def get_stats():
  163. return parse_cs_uarg(self.cfg.stats.lower(), self.all_stats, self.dfl_stats, 'stat')
  164. def parse_cmd_args(): # => (block_list, first, last, step)
  165. match cmd_args:
  166. case [] | None:
  167. return (None, self.tip, self.tip, None)
  168. case [arg]:
  169. r = self.parse_rangespec(arg)
  170. return (
  171. list(range(r.first, r.last+1, r.step)) if r.step else None,
  172. r.first,
  173. r.last,
  174. r.step)
  175. case [*args]:
  176. return ([self.conv_blkspec(a) for a in args], None, None, None)
  177. self.cfg = cfg
  178. self.rpc = rpc
  179. self.tip = rpc.blockcount
  180. from_satoshi = self.rpc.proto.coin_amt.satoshi
  181. to_satoshi = 1 / from_satoshi
  182. self.block_list, self.first, self.last, self.step = parse_cmd_args()
  183. have_segwit = self.rpc.info('segwit_is_active')
  184. if not have_segwit:
  185. del self.fields['weight']
  186. self.dfl_fields = tuple(f for f in self.dfl_fields if f != 'weight')
  187. self.stats_deps = {
  188. 'avg': set(self.fields) - self.avg_stats_skip,
  189. 'col_avg': set(self.fields) - self.avg_stats_skip,
  190. 'mini_avg': {'interval', 'size'} | ({'weight'} if have_segwit else set()),
  191. 'total': {'interval', 'subsidy', 'totalfee', 'nTx', 'inputs', 'outputs', 'utxo_inc'},
  192. 'range': {},
  193. 'diff': {}}
  194. self.fmt_funcs = {
  195. 'da': lambda arg: strftime('%Y-%m-%d %X', gmtime(arg)),
  196. 'td': lambda arg: (
  197. '-{:02}:{:02}'.format(abs(arg)//60, abs(arg)%60) if arg < 0 else
  198. ' {:02}:{:02}'.format(arg//60, arg%60)),
  199. 'tf': lambda arg: '{:.8f}'.format(arg * from_satoshi),
  200. 'su': lambda arg: str(arg * from_satoshi).rstrip('0').rstrip('.'),
  201. 'fe': lambda arg: str(arg),
  202. 'di': lambda arg: '{:.2e}'.format(Decimal(arg))}
  203. if self.cfg.coin == 'BCH':
  204. self.fmt_funcs.update({
  205. 'su': lambda arg: str(arg).rstrip('0').rstrip('.'),
  206. 'fe': lambda arg: str(int(Decimal(arg) * to_satoshi)),
  207. 'tf': lambda arg: '{:.8f}'.format(Decimal(arg))})
  208. self.fnames = tuple(
  209. [f for f in self.fields if self.fields[f].src == 'bh' or f == 'interval']
  210. if self.cfg.header_info
  211. else get_fields() if self.cfg.fields
  212. else self.dfl_fields)
  213. if self.cfg.miner_info and 'miner' not in self.fnames:
  214. self.fnames += ('miner',)
  215. self.stats = get_stats() if self.cfg.stats else self.dfl_stats
  216. # Display diff stats by default only if user-requested range ends with chain tip
  217. if 'diff' in self.stats and not self.cfg.stats and self.last != self.tip:
  218. self.stats.remove('diff')
  219. if {'avg', 'col_avg'} <= set(self.stats) and self.cfg.stats_only:
  220. self.stats.remove('col_avg')
  221. if {'avg', 'mini_avg'} <= set(self.stats):
  222. self.stats.remove('mini_avg')
  223. if self.cfg.full_stats:
  224. add_fnames = {fname for sname in self.stats for fname in self.stats_deps[sname]}
  225. self.fnames = tuple(f for f in self.fields if f in {'block'} | set(self.fnames) | add_fnames)
  226. else:
  227. if 'col_avg' in self.stats and not self.fnames:
  228. self.stats.remove('col_avg')
  229. # self.fnames is now finalized
  230. self.fvals = [self.fields[name] for name in self.fnames]
  231. self.fs = ''.join(self.gen_fs(self.fnames)).strip()
  232. self.bs_keys = set(
  233. [v.key1 for v in self.fvals if v.src == 'bs'] +
  234. ['total_size'] +
  235. (['total_weight'] if have_segwit else []))
  236. if 'miner' in self.fnames:
  237. # capturing parens must contain only ASCII chars!
  238. self.miner_pats = [re.compile(pat) for pat in (
  239. rb'`/([_a-zA-Z0-9&. #/-]+)/',
  240. rb'[\xe3\xe4\xe5][\^/]([\x20-\x7e]+?)\xfa',
  241. rb'([a-zA-Z0-9&. -]+/Mined by [a-zA-Z0-9. ]+)',
  242. rb'\x08/(.*Mined by [a-zA-Z0-9. ]+)',
  243. rb'Mined by ([a-zA-Z0-9. ]+)',
  244. rb'[`]([_a-zA-Z0-9&. #/-]+)[/\xfa]',
  245. rb'([\x20-\x7e]{9,})',
  246. rb'[/^]([a-zA-Z0-9&. #/-]{5,})',
  247. rb'[/^]([_a-zA-Z0-9&. #/-]+)/',
  248. rb'^\x03...\W{0,5}([\\_a-zA-Z0-9&. #/-]+)[/\\]')]
  249. self.block_data = namedtuple('block_data', self.fnames)
  250. self.deps = {v.src for v in self.fvals}
  251. def gen_fs(self, fnames, fill=[], fill_char='-', add_name=False):
  252. for i in range(len(fnames)):
  253. name = fnames[i]
  254. ls = (' ', '')[name in self.fs_lsqueeze + self.fs_lsqueeze2]
  255. rs = (' ', '')[name in self.fs_rsqueeze]
  256. if i < len(fnames) - 1 and fnames[i+1] in self.fs_lsqueeze2:
  257. rs = ''
  258. if i:
  259. for group in self.fs_groups:
  260. if name in group and fnames[i-1] in group:
  261. ls = ''
  262. break
  263. repl = (name if add_name else '') + ':' + (fill_char if name in fill else '')
  264. yield (ls + self.fields[name].fs.replace(':', repl) + rs)
  265. def conv_blkspec(self, arg):
  266. match arg:
  267. case str() if arg.lower() == 'cur':
  268. return self.tip
  269. case x if is_int(x):
  270. match int(arg):
  271. case x if x < 0:
  272. die(1, f'{x}: block number must be non-negative')
  273. case x if x > self.tip:
  274. die(1, f'{x}: requested block height greater than current chain tip!')
  275. case x:
  276. return x
  277. case _:
  278. die(1, f'{arg}: invalid block specifier')
  279. def check_nblocks(self, arg):
  280. match arg:
  281. case x if x <= 0:
  282. die(1, 'nBlocks must be a positive integer')
  283. case x if x > self.tip:
  284. die(1, f'{arg}: nBlocks must be less than current chain height')
  285. case _:
  286. return arg
  287. def parse_rangespec(self, arg):
  288. p = RangeParser(self, arg)
  289. from_tip = p.parse('from_tip')
  290. first, last = (self.tip-from_tip, None) if from_tip else p.parse('abs_range')
  291. add1 = p.parse('add')
  292. add2 = p.parse('add')
  293. p.finalize()
  294. if add2 and last is not None:
  295. die(1, f'{arg!r}: invalid range specifier')
  296. nblocks, step = (add1, add2) if last is None else (None, add1)
  297. if p.debug: msg(repr(self.range_data(first, last, from_tip, nblocks, step)))
  298. if nblocks:
  299. if first is None:
  300. first = self.tip - nblocks + 1
  301. last = first + nblocks - 1
  302. first = self.conv_blkspec(first)
  303. last = self.conv_blkspec(last or first)
  304. if p.debug:
  305. msg(repr(self.range_data(first, last, from_tip, nblocks, step)))
  306. if first > last:
  307. die(1, f'{first}-{last}: invalid block range')
  308. return self.range_data(first, last, from_tip, nblocks, step)
  309. async def process_blocks(self):
  310. async def get_hdrs(heights):
  311. hashes = await c.gathered_call('getblockhash',[(height,) for height in heights])
  312. return await c.gathered_call('getblockheader',[(H,) for H in hashes])
  313. c = self.rpc
  314. heights = self.block_list or range(self.first, self.last+1)
  315. self.hdrs = await get_hdrs(heights)
  316. if self.block_list:
  317. self.prev_hdrs = await get_hdrs([(n-1 if n else 0) for n in self.block_list])
  318. self.first_prev_hdr = self.prev_hdrs[0]
  319. else:
  320. self.first_prev_hdr = (
  321. self.hdrs[0] if heights[0] == 0 else
  322. await c.call('getblockheader', await c.call('getblockhash', heights[0]-1)))
  323. self.t_cur = self.first_prev_hdr['time']
  324. self.res = []
  325. for n in range(len(heights)):
  326. if self.block_list:
  327. self.t_cur = self.prev_hdrs[n]['time']
  328. ret = await self.process_block(self.hdrs[n])
  329. self.res.append(ret)
  330. if self.fnames and not self.cfg.stats_only:
  331. self.output_block(ret, n)
  332. def output_block(self, data, n):
  333. def gen():
  334. for k, v in data._asdict().items():
  335. func = self.fields[k].fmt_func
  336. yield self.fmt_funcs[func](v) if func else v
  337. Msg(self.fs.format(*gen()))
  338. async def process_block(self, hdr):
  339. self.t_diff = hdr['time'] - self.t_cur
  340. self.t_cur = hdr['time']
  341. self.total_solve_time += self.t_diff
  342. blk_data = {
  343. 'bh': hdr,
  344. 'lo': {'interval': self.t_diff}}
  345. if 'bs' in self.deps:
  346. bs = (
  347. self.genesis_stats if hdr['height'] == 0 else
  348. await self.rpc.call('getblockstats', hdr['hash'], list(self.bs_keys)))
  349. self.total_bytes += bs['total_size']
  350. if 'total_weight' in bs:
  351. self.total_weight += bs['total_weight']
  352. blk_data['bs'] = bs
  353. if 'miner' in self.fnames:
  354. blk_data['lo']['miner'] = '-' if hdr['height'] == 0 else await self.get_miner_string(hdr['hash'])
  355. def gen():
  356. for v in self.fvals:
  357. yield (
  358. blk_data[v.src][v.key1] if v.key2 is None else
  359. blk_data[v.src][v.key1][v.key2])
  360. return self.block_data(*gen())
  361. async def get_miner_string(self, H):
  362. tx0 = (await self.rpc.call('getblock', H))['tx'][0]
  363. bd = await self.rpc.call('getrawtransaction', tx0, 1)
  364. if type(bd) == tuple:
  365. return '---'
  366. else:
  367. cb = bytes.fromhex(bd['vin'][0]['coinbase'])
  368. if self.cfg.raw_miner_info:
  369. return repr(cb)
  370. else:
  371. trmap_in = {
  372. '\\': ' ',
  373. '/': ' ',
  374. ',': ' '}
  375. trmap = {ord(a): b for a, b in trmap_in.items()}
  376. for pat in self.miner_pats:
  377. m = pat.search(cb)
  378. if m:
  379. return re.sub(r'\s+', ' ', m[1].decode().strip('^').translate(trmap).strip())
  380. return ''
  381. def print_header(self):
  382. Msg('\n'.join(self.gen_header()))
  383. self.header_printed = True
  384. def gen_header(self):
  385. hdr1 = [v.hdr1 for v in self.fvals]
  386. hdr2 = [v.hdr2 for v in self.fvals]
  387. if ''.join(hdr1):
  388. yield self.fs.format(*hdr1)
  389. yield self.fs.format(*hdr2)
  390. def process_stats(self, sname):
  391. method = getattr(self, f'create_{sname}_stats', None)
  392. return self.output_stats(method() if method else self.create_stats(sname), sname)
  393. def fmt_stat_item(self, fs, s):
  394. return fs.format(s) if type(fs) == str else fs(s)
  395. async def output_stats(self, res, sname):
  396. def gen(data):
  397. for d in data:
  398. match d:
  399. case [a, b]:
  400. yield (indent + a).format(**{k: self.fmt_stat_item(*v) for k, v in b.items()})
  401. case [a, _, b, c]:
  402. yield (indent + a).format(self.fmt_stat_item(b, c))
  403. case str():
  404. yield d
  405. case _:
  406. assert False, f'{d}: invalid stats data'
  407. foo, data = await res
  408. indent = '' if sname in self.noindent_stats else ' '
  409. Msg('\n'.join(gen(data)))
  410. async def create_range_stats(self):
  411. # These figures don’t include the Genesis Block:
  412. elapsed = self.hdrs[-1]['time'] - self.first_prev_hdr['time']
  413. nblocks = self.hdrs[-1]['height'] - self.first_prev_hdr['height']
  414. total_blks = len(self.hdrs)
  415. step_disp = f', nBlocks={total_blks}, step={self.step}' if self.step else ''
  416. def gen():
  417. yield 'Range Statistics:'
  418. yield (
  419. 'Range: {start}-{end} ({range} blocks [{elapsed}]%s)' % step_disp, {
  420. 'start': ('{}', self.hdrs[0]['height']),
  421. 'end': ('{}', self.hdrs[-1]['height']),
  422. 'range': ('{}', self.hdrs[-1]['height'] - self.hdrs[0]['height'] + 1),
  423. 'elapsed': (self.t_fmt, elapsed),
  424. 'nBlocks': ('{}', total_blks),
  425. 'step': ('{}', self.step)})
  426. if elapsed:
  427. yield ('Start: {}', 'start_date', self.fmt_funcs['da'], self.hdrs[0]['time'])
  428. yield ('End: {}', 'end_date', self.fmt_funcs['da'], self.hdrs[-1]['time'])
  429. yield ('Avg BDI: {} min', 'avg_bdi', '{:.2f}', elapsed / nblocks / 60)
  430. return ('range', gen())
  431. async def create_diff_stats(self):
  432. c = self.rpc
  433. rel = self.tip % self.rpc.proto.diff_adjust_interval
  434. tip_hdr = (
  435. self.hdrs[-1] if self.hdrs[-1]['height'] == self.tip else
  436. await c.call('getblockheader', await c.call('getblockhash', self.tip)))
  437. min_sample_blks = 432 # ≈3 days
  438. rel_hdr = await c.call('getblockheader', await c.call('getblockhash', self.tip-rel))
  439. if rel >= min_sample_blks:
  440. sample_blks = rel
  441. bdi = (tip_hdr['time'] - rel_hdr['time']) / rel
  442. else:
  443. sample_blks = min(min_sample_blks, self.tip)
  444. start_hdr = await c.call('getblockheader', await c.call('getblockhash', self.tip-sample_blks))
  445. diff_adj = Decimal(tip_hdr['difficulty']) / Decimal(start_hdr['difficulty'])
  446. time1 = rel_hdr['time'] - start_hdr['time']
  447. time2 = tip_hdr['time'] - rel_hdr['time']
  448. bdi = ((time1 * diff_adj) + time2) / sample_blks
  449. rem = self.rpc.proto.diff_adjust_interval - rel
  450. return ('difficulty', (
  451. 'Difficulty Statistics:',
  452. ('Current height: {}', 'chain_tip', '{}', self.tip),
  453. ('Next diff adjust: {next_diff_adjust} (in {blks_remaining} block%s [{time_remaining}])' % suf(rem),
  454. {
  455. 'next_diff_adjust': ('{}', self.tip + rem),
  456. 'blks_remaining': ('{}', rem),
  457. 'time_remaining': (self.t_fmt, rem * bdi)
  458. }
  459. ),
  460. ('Avg BDI: {avg_bdi} min (over {sample_blks}-block period)',
  461. {
  462. 'avg_bdi': ('{:.2f}', bdi/60),
  463. 'sample_blks': ('{}', sample_blks)
  464. }
  465. ),
  466. ('Cur difficulty: {}', 'cur_diff', '{:.2e}', Decimal(tip_hdr['difficulty'])),
  467. ('Est. diff adjust: {}%', 'est_diff_adjust_pct', '{:+.2f}', ((600 / bdi) - 1) * 100),
  468. ))
  469. def sum_field_avg(self, field):
  470. return self.sum_field_total(field) // len(self.res)
  471. def sum_field_total(self, field):
  472. if isinstance(getattr(self.res[0], field), str):
  473. return sum(Decimal(getattr(block, field)) for block in self.res)
  474. else:
  475. return sum(getattr(block, field) for block in self.res)
  476. async def create_col_avg_stats(self):
  477. def gen():
  478. for field in self.fnames:
  479. if field in self.avg_stats_skip:
  480. yield (field, ('{}', ''))
  481. else:
  482. ret = self.sum_field_avg(field)
  483. func = self.fields[field].fmt_func
  484. yield (field, ((self.fmt_funcs[func] if func else '{}'), ret))
  485. if not self.header_printed:
  486. self.print_header()
  487. fs = ''.join(self.gen_fs(self.fnames, fill=self.avg_stats_skip, add_name=True)).strip()
  488. return ('column_averages', ('Column averages:', (fs, dict(gen()))))
  489. def avg_stats_data(self, data, spec_conv, spec_val):
  490. coin = self.rpc.proto.coin
  491. return data(
  492. hdr = 'Averages for processed blocks:',
  493. func = self.sum_field_avg,
  494. spec_sufs = {'subsidy': f' {coin}', 'totalfee': f' {coin}'},
  495. spec_convs = {
  496. 'interval': spec_conv(0, lambda arg: secs_to_ms(arg)),
  497. 'utxo_inc': spec_conv(-1, '{:<+}'),
  498. 'mb_per_hour': spec_conv(0, '{}')},
  499. spec_vals = (
  500. spec_val(
  501. 'mb_per_hour', 'MB/hr', 'interval',
  502. lambda values: 'bs' in self.deps,
  503. lambda values: (
  504. '{:.4f}'.format((self.total_bytes / 10000) / (self.total_solve_time / 36))
  505. if self.total_solve_time else 'N/A')),
  506. ))
  507. mini_avg_stats_data = avg_stats_data
  508. def total_stats_data(self, data, spec_conv, spec_val):
  509. coin = self.rpc.proto.coin
  510. return data(
  511. hdr = 'Totals for processed blocks:',
  512. func = self.sum_field_total,
  513. spec_sufs = {'subsidy': f' {coin}', 'totalfee': f' {coin}', 'reward': f' {coin}'},
  514. spec_convs = {
  515. 'interval': spec_conv(0, lambda arg: secs_to_dhms(arg)),
  516. 'utxo_inc': spec_conv(-1, '{:<+}'),
  517. 'reward': spec_conv(0, self.fmt_funcs['tf'])},
  518. spec_vals = (
  519. spec_val(
  520. 'reward', 'Reward', 'totalfee',
  521. lambda values: {'subsidy', 'totalfee'} <= set(values),
  522. lambda values: values['subsidy'] + values['totalfee']),
  523. ))
  524. async def create_stats(self, sname):
  525. def convert_stats_hdr(field):
  526. v = self.fields[field]
  527. return '{} {}'.format(
  528. v.hdr1.strip(), v.hdr2.strip()).replace('- ', '') if v.hdr1 else v.hdr2.strip()
  529. d = getattr(self, f'{sname}_stats_data')(
  530. namedtuple('stats_data', ['hdr', 'func', 'spec_sufs', 'spec_convs', 'spec_vals']),
  531. namedtuple('spec_conv', ['width_adj', 'conv']),
  532. namedtuple('spec_val', ['name', 'lbl', 'insert_after', 'condition', 'code']))
  533. fnames = [n for n in self.fnames if n in self.stats_deps[sname]]
  534. lbls = {n: convert_stats_hdr(n) for n in fnames}
  535. values = {n: d.func(n) for n in fnames}
  536. col1_w = max((len(l) for l in lbls.values()), default=0) + 2
  537. print(d.spec_vals)
  538. for v in d.spec_vals:
  539. print(v)
  540. if v.condition(values):
  541. try: idx = fnames.index(v.insert_after) + 1
  542. except: idx = 0
  543. fnames.insert(idx, v.name)
  544. lbls[v.name] = v.lbl
  545. values[v.name] = v.code(values)
  546. def gen():
  547. for n, fname in enumerate(fnames):
  548. spec_conv = d.spec_convs.get(fname)
  549. yield (
  550. '{lbl:{wid}} {{}}{suf}'.format(
  551. lbl = lbls[fname] + ':',
  552. wid = col1_w + (spec_conv.width_adj if spec_conv else 0),
  553. suf = d.spec_sufs.get(fname) or ''),
  554. fname,
  555. spec_conv.conv if spec_conv else (
  556. (lambda x: self.fmt_funcs[x] if x else '{}')(self.fields[fname].fmt_func)),
  557. values[fname])
  558. return (sname, (d.hdr,) + tuple(gen()))
  559. def process_stats_pre(self, i):
  560. if (self.fnames and not self.cfg.stats_only) or i != 0:
  561. Msg('')
  562. def finalize_output(self): pass
  563. # 'getblockstats' RPC raises exception on Genesis Block, so provide our own stats:
  564. genesis_stats = {
  565. 'avgfee': 0,
  566. 'avgfeerate': 0,
  567. 'avgtxsize': 0,
  568. 'feerate_percentiles': [ 0, 0, 0, 0, 0 ],
  569. 'height': 0,
  570. 'ins': 0,
  571. 'maxfee': 0,
  572. 'maxfeerate': 0,
  573. 'maxtxsize': 0,
  574. 'medianfee': 0,
  575. 'mediantxsize': 0,
  576. 'minfee': 0,
  577. 'minfeerate': 0,
  578. 'mintxsize': 0,
  579. 'outs': 1,
  580. 'subsidy': 5000000000,
  581. 'swtotal_size': 0,
  582. 'swtotal_weight': 0,
  583. 'swtxs': 0,
  584. 'total_out': 0,
  585. 'total_size': 0,
  586. 'total_weight': 0,
  587. 'totalfee': 0,
  588. 'txs': 1,
  589. 'utxo_increase': 1,
  590. 'utxo_size_inc': 117}
  591. class JSONBlocksInfo(BlocksInfo):
  592. def __init__(self, cfg, cmd_args, rpc):
  593. super().__init__(cfg, cmd_args, rpc)
  594. if self.cfg.json_raw:
  595. self.output_block = self.output_block_raw
  596. self.fmt_stat_item = self.fmt_stat_item_raw
  597. Msg_r('{')
  598. async def process_blocks(self):
  599. Msg_r('"block_data": [')
  600. await super().process_blocks()
  601. Msg_r(']')
  602. def output_block_raw(self, data, n):
  603. Msg_r((', ', '')[n==0] + json.dumps(data._asdict(), cls=json_encoder))
  604. def output_block(self, data, n):
  605. def gen():
  606. for k, v in data._asdict().items():
  607. func = self.fields[k].fmt_func
  608. yield (k, (self.fmt_funcs[func](v) if func else v))
  609. Msg_r((', ', '')[n==0] + json.dumps(dict(gen()), cls=json_encoder))
  610. def print_header(self): pass
  611. def fmt_stat_item_raw(self, fs, s):
  612. return s
  613. async def output_stats(self, res, sname):
  614. def gen(data):
  615. for d in data:
  616. match d:
  617. case [_, a]:
  618. for k, v in a.items():
  619. yield (k, self.fmt_stat_item(*v))
  620. case [_, a, b, c]:
  621. yield (a, self.fmt_stat_item(b, c))
  622. case str():
  623. pass
  624. case _:
  625. assert False, f'{d}: invalid stats data'
  626. varname, data = await res
  627. Msg_r(', "{}_data": {}'.format(varname, json.dumps(dict(gen(data)), cls=json_encoder)))
  628. def process_stats_pre(self, i): pass
  629. def finalize_output(self):
  630. Msg('}')