"""Verify frozen public PSBT/regtest evidence offline. Python3.10+ stdlib.
No node/network/wallet and no writes. Educational bounded verifier, not consensus.
"""
from pathlib import Path
from decimal import Decimal
import csv,hashlib,importlib.machinery,importlib.util,io,json,sys
sys.dont_write_bytecode=True
BASE_PUBLIC={'results.json','candidates.json','stages.csv','cases.csv','rpc-trace.json','rpc-help.json','cleanup.json','reproduce.py.txt','fixture-codec.py.txt','verify.py.txt','README.txt','sources.json','psbt-howto.md.txt','source-excerpts.txt','payment-review-checklist.txt','LICENSE.txt'}
OPTIONAL_PUBLIC={'artwork-provenance.json','COPYING.txt','psbt-lifecycle.svg','approval-vs-availability.svg'}
P=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
N=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
G=(0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798,0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8)
CHECKS=0
def check(name,ok):
 global CHECKS
 CHECKS+=1
 if not ok:raise AssertionError(name)
def add(a,b):
 if a is None:return b
 if b is None:return a
 if a[0]==b[0] and (a[1]+b[1])%P==0:return None
 slope=(3*a[0]**2*pow(2*a[1],-1,P))%P if a==b else ((b[1]-a[1])*pow(b[0]-a[0],-1,P))%P
 x=(slope*slope-a[0]-b[0])%P;return x,(slope*(a[0]-x)-a[1])%P
def multiply(n,point):
 out=None
 while n:
  if n&1:out=add(out,point)
  point=add(point,point);n>>=1
 return out
def verify_ecdsa(pub,sig,digest):
 try:
  if len(pub)!=33 or pub[0] not in (2,3) or sig[-1:]!=b'\x01':return False
  x=int.from_bytes(pub[1:],'big')
  if x>=P:return False
  y=pow((x*x*x+7)%P,(P+1)//4,P)
  if (y*y-x*x*x-7)%P:return False
  if y%2!=pub[0]%2:y=P-y
  der=sig[:-1]
  if len(der)<8 or der[0]!=0x30 or der[1]!=len(der)-2 or der[2]!=2:return False
  size=der[3];rb=der[4:4+size];at=4+size
  if at+2>len(der) or der[at]!=2:return False
  sb=der[at+2:]
  if der[at+1]!=len(sb) or not rb or not sb:return False
  for b in (rb,sb):
   if b[0]&128 or (len(b)>1 and b[0]==0 and not b[1]&128):return False
  r,s=int.from_bytes(rb,'big'),int.from_bytes(sb,'big')
  if not 0<r<N or not 0<s<=N//2:return False
  inv=pow(s,-1,N);point=add(multiply(int.from_bytes(digest,'big')*inv%N,G),multiply(r*inv%N,(x,y)))
  return point is not None and point[0]%N==r
 except (ValueError,IndexError,ZeroDivisionError):return False
def atoms(v):
 n=Decimal(str(v))*100000000
 if n!=n.to_integral_value():raise ValueError('Non-integer litoshi')
 return int(n)
def main(folder):
 manifest=(folder/'SHA256SUMS.txt').read_text(encoding='utf-8');check('Bounded manifest',len(manifest)<65536);seen=set()
 for line in manifest.splitlines():
  sha,name=line.split('  ',1);check('Explicit public names only',name in BASE_PUBLIC|OPTIONAL_PUBLIC and name not in seen);seen.add(name)
  path=(folder/name).resolve();check('Bounded local artifact',path.parent==folder.resolve() and path.stat().st_size<=8000000)
  check('SHA256 '+name,hashlib.sha256(path.read_bytes()).hexdigest()==sha)
 check('All required public files present',BASE_PUBLIC<=seen)
 loader=importlib.machinery.SourceFileLoader('codec_verifier',str(folder/'fixture-codec.py.txt'));codec=importlib.util.module_from_spec(importlib.util.spec_from_loader(loader.name,loader));loader.exec_module(codec)
 r=json.loads((folder/'results.json').read_text(encoding='utf-8'));candidates=json.loads((folder/'candidates.json').read_text(encoding='utf-8'))
 check('Candidate copy matches evidence',candidates==r['candidates']);check('Only pinned Core regtest',r['network']=='regtest' and r['core_version']=='/LitecoinCore:0.21.5.8/')
 check('Declared ALL scope',r['sighash']=='ALL')
 prevouts={}
 for u in r['funding_outputs']:
  tx=codec.transaction(u['raw_hex']);check('Funding txid independently hashed',codec.details(tx)['txid']==u['txid']);out=tx['outputs'][u['vout']]
  check('Funding value from actual raw bytes',out['atoms']==atoms(u['amount']));prevouts[(u['txid'],u['vout'])]=out
 check('Two distinct fixture prevouts',len(prevouts)==2)
 verified={};parsed={}
 for c in candidates:
  tx=codec.transaction(c['raw_hex']);parsed[c['candidate']]=tx;details=codec.details(tx)
  for k in ['txid','wtxid','size','weight','vsize']:check(c['candidate']+' '+k,details[k]==c[k])
  check('One ordinary native-SegWit input',len(tx['inputs'])==1 and tx['inputs'][0]['script']==b'' and len(tx['inputs'][0]['witness'])==2)
  inp=tx['inputs'][0];key=(inp['outpoint'][:32][::-1].hex(),int.from_bytes(inp['outpoint'][32:],'little'));check('Recorded input identity',key==(c['input_txid'],c['input_vout']))
  old=prevouts[key];check('Native P2WPKH prevout',old['script'][:2]==b'\x00\x14' and len(old['script'])==22)
  signature,pub=inp['witness'];kh=hashlib.new('ripemd160',hashlib.sha256(pub).digest()).digest();check('Witness public key matches prevout',kh==old['script'][2:])
  check('Every candidate conserves exact1000litoshi fee',old['atoms']-sum(x['atoms'] for x in tx['outputs'])==c['fee_litoshi']==1000)
  check('Output amount vector independently parsed',[x['atoms'] for x in tx['outputs']]==c['output_values_litoshi'])
  verified[c['candidate']]=verify_ecdsa(pub,signature,codec.bip143(tx,0,old['atoms'],kh))
 check('Signatures true/false at declared scope',verified=={'approved_payment':True,'changed_recipient_original_signature':False,'changed_amount_original_signature':False,'held_payment':True,'confirmed_self_spend_of_held_input':True})
 main_tx=parsed['approved_payment'];check('Approved recipient exact0.25LTC',main_tx['outputs'][0]['atoms']==25000000 and main_tx['outputs'][0]['script'].hex()==r['recipient_script_hex'])
 for name in ['changed_recipient_original_signature','changed_amount_original_signature']:
  check('Mutation preserves original input and signature',parsed[name]['inputs']==main_tx['inputs'])
 check('Recipient-only mutation exact',parsed['changed_recipient_original_signature']['outputs'][0]['script'].hex()==r['alternate_recipient_script_hex'] and [x['atoms'] for x in parsed['changed_recipient_original_signature']['outputs']]==[x['atoms'] for x in main_tx['outputs']])
 check('Amount mutation exact one-litoshi transfer',[x['atoms'] for x in parsed['changed_amount_original_signature']['outputs']]==[main_tx['outputs'][0]['atoms']+1,main_tx['outputs'][1]['atoms']-1])
 check('Held and confirmed self-spend use same input',parsed['held_payment']['inputs'][0]['outpoint']==parsed['confirmed_self_spend_of_held_input']['inputs'][0]['outpoint'])
 for name,p in r['psbts'].items():
  prepared=codec.psbt(p['prepared']);signed=codec.psbt(p['signed'])
  check(name+' unsigned transaction unchanged by signing',codec.serialize(prepared['tx'])==codec.serialize(signed['tx']))
  check(name+' unsigned has no final witness',b'\x08' not in prepared['inputs'][0]);check(name+' signed has final witness',b'\x08' in signed['inputs'][0])
  check(name+' prepared carries public derivation metadata',any(k[0]==6 for k in prepared['inputs'][0]))
  if name=='payment':
   check('Extracted raw has same unsigned transaction',codec.serialize(signed['tx'])==codec.serialize(main_tx,False))
   check('Prepared witness txid existed before signing',p['prepared_decoded']['tx']['txid']==codec.details(main_tx)['txid'])
 stages=r['stages'];csv_stages=list(csv.DictReader(io.StringIO((folder/'stages.csv').read_text(encoding='utf-8'))))
 check('Eight observed stages',len(stages)==len(csv_stages)==8)
 for i,(s,cs) in enumerate(zip(stages,csv_stages)):
  check('Zero peers each stage',s['peers']==0);check('Stage CSV identity',s['stage']==cs['stage'] and len(s['mempool'])==int(cs['mempool_count']))
  check('Stage CSV values exact',int(cs['height'])==s['height'] and int(cs['peers'])==s['peers'] and all(atoms(cs[k])==atoms(s[k]) for k in ['recipient_received_0_ltc','recipient_received_1_ltc']) and all(cs[k]==str(s[k]) for k in ['selected_input_chain_unspent','selected_input_with_mempool_unspent']))
  if i<5:check('No submission before explicit send',s['height']==102 and s['mempool']==[] and s['sender_balances']==stages[0]['sender_balances'] and atoms(s['recipient_received_0_ltc'])==0 and s['selected_input_with_mempool_unspent'])
 check('Only explicit submission populates mempool',len(stages[5]['mempool'])==1 and atoms(stages[5]['recipient_received_0_ltc'])==25000000 and atoms(stages[5]['recipient_received_1_ltc'])==0)
 check('After confirmation recipient0.25LTC',stages[6]['height']==103 and atoms(stages[6]['recipient_received_1_ltc'])==25000000)
 check('Final held input absent and no additional receipt',stages[7]['height']==104 and not stages[7]['selected_input_chain_unspent'] and stages[7]['mempool']==[] and atoms(stages[7]['recipient_received_1_ltc'])==25000000)
 cases=r['cases'];csv_cases=list(csv.DictReader(io.StringIO((folder/'cases.csv').read_text(encoding='utf-8'))));check('Six policy cases',len(cases)==len(csv_cases)==6)
 check('Actual policy outcomes',[c['allowed'] for c in cases]==[True,False,False,True,True,False]);check('Same held bytes before/after spending',cases[3]['raw_hex']==cases[5]['raw_hex']);check('Missing inputs distinct from bad signature',cases[5]['reject_reason']=='missing-inputs' and all('CHECK(MULTI)SIG' in cases[i]['reject_reason'] for i in [1,2]))
 for c,cs in zip(cases,csv_cases):check('Case CSV exact',cs['case_id']==c['case_id'] and cs['allowed']==str(c['allowed']) and cs['reject_reason']==c['reject_reason'])
 calls=json.loads((folder/'rpc-trace.json').read_text(encoding='utf-8'))['calls'];sends=[c for c in calls if c['method']=='sendrawtransaction'];check('Exactly three explicit submission attempts',len(sends)==3)
 check('Submission outcomes are two successes/one rejection',[c['return_code']==0 for c in sends]==[True,True,False]);check('Six dry-run policy calls',sum(c['method']=='testmempoolaccept' for c in calls)==6)
 check('No private material export or real-wallet import',not {'dumpprivkey','dumpwallet','backupwallet','listdescriptors','importprivkey','importwallet','signrawtransactionwithkey'}.intersection(c['method'] for c in calls))
 check('Final complete flag remains signature completeness only',r['summary']['held_after_spend_complete'] is True and any(c['method']=='finalizepsbt' and c.get('result',{}).get('complete') is True for c in calls[calls.index(sends[-1])+1:]))
 check('All43 experiment assertions passed',r['assertions_passed']==len(r['assertions'])==43 and all(x['passed'] is True for x in r['assertions']))
 check('Recorded successful cleanup',r['cleanup']==json.loads((folder/'cleanup.json').read_text())=={'own_node_stopped':True,'fresh_directory_removed':True,'exit_code':0})
 check('Network config bounded',all(x in r['node_options'] for x in ['-listen=0','-connect=0','-rpcbind=127.0.0.1','-rpcallowip=127.0.0.1','-dbcache=32','-par=1']))
 print(json.dumps({'success':True,'checks_passed':CHECKS,'cryptographic_candidates':len(verified),'policy_cases':6,'recorded_assertions':43,'scope':'Offline public-data signature/serialization and recorded-evidence checks; no current UTXO or consensus validation.'}))
if __name__=='__main__':main(Path(sys.argv[1]).resolve() if len(sys.argv)>1 else Path(__file__).resolve().parent)
