"""Recalculate frozen chain observations. Python 3.10+, standard library only.""" from pathlib import Path import json, hashlib, statistics, math, collections, argparse def analyze(base): base=Path(base) manifest=json.loads((base/'input-sha256.json').read_text()) for name,digest in manifest.items(): if Path(name).name!=name or hashlib.sha256((base/name).read_bytes()).hexdigest()!=digest: raise ValueError('Input checksum mismatch: '+name) h=json.loads((base/'early-headers.json').read_text()) b=json.loads((base/'recent-blocks.json').read_text()) t=json.loads((base/'recent-transactions.json').read_text()) o=json.loads((base/'early-output-status.json').read_text()) assert [x['height'] for x in h]==list(range(1001)) assert all(q['previousblockhash']==p['hash'] for p,q in zip(h,h[1:])) assert len(b)==50 and all(q['height']==p['height']+1 and q['previousblockhash']==p['hash'] for p,q in zip(b,b[1:])) assert len({x['txid'] for x in t})==len(t) assert all(sum(x['output_litoshis'])==x['output_total_litoshis'] for x in t) assert sum(x['nTx'] for x in b)==len(t) assert all(x['hogaddr_output']==('witness_mweb_hogaddr' in x['output_types']) for x in t) intervals=[q['time']-p['time'] for p,q in zip(h[3:-1],h[4:])] rank=lambda rows: sorted(rows,key=lambda x:(-x['output_total_litoshis'],x['txid']))[:10] ordinary=[x for x in t if not x['coinbase'] and not x['hogaddr_output']] naive=rank([x for x in t if not x['coinbase']]) def q(values,f): values=sorted(values);pos=(len(values)-1)*f;lo=math.floor(pos);hi=math.ceil(pos) return round(values[lo]+(values[hi]-values[lo])*(pos-lo),6) mw=[] for block in b: candidates=[x for x in t if x['height']==block['height'] and x['hogaddr_output']] assert len(candidates)==1 tx=candidates[0] balance=sum(value for value,kind in zip(tx['output_litoshis'],tx['output_types']) if kind=='witness_mweb_hogaddr') mw.append({'height':block['height'],'block_hash':block['hash'],'header_time':block['time'],'hogex_txid':tx['txid'],'hogaddr_litoshis':balance,'hogex_total_output_litoshis':tx['output_total_litoshis'],'kernels':block['mweb_num_kernels'],'inputs':block['mweb_inputs'],'outputs':block['mweb_outputs']}) outputs=o['rows'] assert len({(x['coinbase_txid'],x['vout']) for x in outputs})==len(outputs) assert all(x['vout']==0 and x['height']!=0 for x in outputs) assert all(not x['unspent'] or (x['coinbase_flag'] and x['observed_bestblock']==o['bestblock']) for x in outputs) return {'early_headers':{'count':len(h),'first_height3_time':h[3]['time'],'height1000_time':h[-1]['time'],'height3_to1000_seconds':h[-1]['time']-h[3]['time'],'intervals':len(intervals),'negative':sum(v<0 for v in intervals),'zero':sum(v==0 for v in intervals),'median_signed_interval_seconds':q(intervals,.5),'p90_signed_interval_seconds':q(intervals,.9),'transaction_counts':dict(collections.Counter(x['nTx'] for x in h))}, 'early_outputs':{'checked':len(outputs),'unspent_output_zero':sum(x['unspent'] for x in outputs),'not_in_utxo_view':sum(not x['unspent'] for x in outputs),'unspent_litoshis':sum(round(x['value_ltc']*100000000) for x in outputs if x['unspent']),'cutoff_height':o['height'],'cutoff_hash':o['bestblock'],'excluded_multiple_tx_blocks':o['multi_transaction_blocks_excluded']}, 'recent':{'height_range':[b[0]['height'],b[-1]['height']],'transactions':len(t),'coinbase':sum(x['coinbase'] for x in t),'hogaddr_rows':sum(x['hogaddr_output'] for x in t),'ordinary_rows':len(ordinary),'naive_top10_hogaddr_count':sum(x['hogaddr_output'] for x in naive),'ordinary_top10':rank(ordinary),'naive_top10':naive,'first_header_time':b[0]['time'],'last_header_time':b[-1]['time']}, 'mweb':{'series':mw,'first_hogaddr_litoshis':mw[0]['hogaddr_litoshis'],'last_hogaddr_litoshis':mw[-1]['hogaddr_litoshis'],'change_litoshis':mw[-1]['hogaddr_litoshis']-mw[0]['hogaddr_litoshis'],'kernel_sum':sum(x['kernels'] or 0 for x in mw),'blocks_with_kernels':sum(bool(x['kernels']) for x in mw)}} if __name__=='__main__': ap=argparse.ArgumentParser();ap.add_argument('directory',nargs='?',default='.');args=ap.parse_args() print(json.dumps(analyze(args.directory),indent=2))