"""Validate published observations, not a replacement for rerunning Core.
Usage: python verify-controls.py timelock-results.json password-results.json core-controls.py
Requires only Python3. Assertions check signatures/control outcomes, boundaries,
raw RPC backing, fresh-run protocol, and deliberate trace redactions.
"""
from pathlib import Path
import sys,json,hashlib,re
checks=[]
def check(label,value):
 checks.append({'check':label,'passed':bool(value)})
 if not value:raise AssertionError(label)
def cases(result):return {c['case']:c for c in result['cases']}
def verify(t,p,source):
 h=hashlib.sha256(source).hexdigest()
 for name,r,count in [('timelock',t,13),('password',p,12)]:
  m=r['metadata'];check(name+' case count',len(r['cases'])==count==m['caseCount'])
  check(name+' unique case names',len(cases(r))==count)
  check(name+' pinned version and successful assertions',m['coreVersion']=='/LitecoinCore:0.21.5.8/' and m['allAssertionsPassed'])
  check(name+' regtest zero peers',m['chain']=='regtest' and m['externalPeers']==0)
  check(name+' final runner hash',m['runnerSHA256']==h)
  check(name+' daemon hash',m['daemonSHA256']=='63fb440cdc5e2e7d46144b8e3a47dab4725fa9c056a77b8b12b11854504f4838')
  check(name+' CLI hash',m['cliSHA256']=='d185011cc0dbb5d2dd20611e9dc060dbb0a5d9be31a03f57da5dbd3dad4c3769')
  raw=json.dumps(r)
  check(name+' no actual filesystem/user disclosure','/var/www/'not in raw and 'C:\\Users\\'not in raw)
  check(name+' disposable passphrases redacted','disposable-study-old-pass'not in raw and 'disposable-study-new-pass'not in raw)
  check(name+' no WIF-shaped exported strings',re.search(r'(?<![A-Za-z0-9])[c9][1-9A-HJ-NP-Za-km-z]{50,51}(?![A-Za-z0-9])',raw) is None)
  priv=[row for row in r['rawRPC'] if row['method']=='dumpprivkey']
  check(name+' dumpprivkey trace redacted',all(row['stdout']=='<disposable-private-key-redacted>' for row in priv))
  check(name+' private signing params redacted',all(row['params'][1]==['<disposable-private-key>'] for row in r['rawRPC'] if row['method']=='signrawtransactionwithkey'))
  # Every raw case verdict must correspond to an actual testmempoolaccept stdout.
  observed=[]
  for row in r['rawRPC']:
   if row['method']=='testmempoolaccept' and row['exitCode']==0:observed.extend(json.loads(row['stdout']))
  def recurse(obj):
   if isinstance(obj,dict):
    if 'allowed'in obj and 'txid'in obj:check(name+' recorded verdict '+obj['txid'][:8],obj in observed)
    for v in obj.values():recurse(v)
   elif isinstance(obj,list):
    for v in obj:recurse(v)
  recurse(r['cases'])
 T=cases(t);P=cases(p)
 f=T['ordinary-future-nonfinal'];final=T['ordinary-all-final-bypass'];alt=T['ordinary-same-input-alternate-now']
 check('ordinary variants use same input',f['outpoint']==final['outpoint']==alt['outpoint'])
 for c in [f,final,alt]:
  d=c['decodedTransaction'];check(c['case']+' actual raw lock/sequence',d['locktime']==c['lockTime'] and d['vin'][0]['sequence']==c['sequence'])
 check('future rejected all-final/alternative allowed',not f['mempool']['allowed'] and final['mempool']['allowed'] and alt['mempool']['allowed'])
 equal=T['ordinary-next-block-equals-lock'];greater=T['ordinary-next-block-greater-than-lock']
 check('strict next block boundary',equal['nextBlockHeight']==equal['lockTime'] and not equal['mempool']['allowed'] and greater['nextBlockHeight']==greater['lockTime']+1 and greater['mempool']['allowed'])
 check('equality rejected by block validation','bad-txns-nonfinal'in equal['blockValidation']['rpcError']['stderr'])
 early=T['ordinary-alternate-confirmed-before-future-lock']
 check('ordinary coin actually confirmed before future lock',early['confirmedAtHeight']<early['lockTime'] and early['confirmedBeforeFutureLock'] and early['transactionIncluded'] and not early['futureMempool']['allowed'] and early['alternateMempool']['allowed'])
 # A real getblock response contains the claimed earlier transaction and height.
 blocks=[json.loads(row['stdout']) for row in t['rawRPC'] if row['method']=='getblock' and row['exitCode']==0]
 check('earlier confirmation independently backed by raw block',any(b['height']==early['confirmedAtHeight'] and any(tx['txid']==early['alternateTxid'] for tx in b['tx']) for b in blocks))
 activations=[json.loads(row['stdout']) for row in t['rawRPC'] if row['method']=='getblockchaininfo' and row['exitCode']==0]
 check('regtest CLTV and CSV activated MWEB absent',any(b['blocks']>=1351 and b['softforks']['bip65']['active'] and b['softforks']['csv']['active'] and not b['softforks']['mweb']['active'] for b in activations))
 for label in ['cltv-zero-lock-cannot-bypass','cltv-correct-lock-but-premature','cltv-final-sequence-cannot-bypass']:
  c=T[label];check(label+' mempool rejects',c['mempool']['allowed'] is False)
  err=c['blockValidation']['rpcError'];check(label+' block rejects',err['method']=='generateblock' and err['exitCode']==25 and 'TestBlockValidity failed:'in err['stderr'])
 ripe=T['cltv-key-signed-ripe-block-accepted']
 check('CLTV ripe signature accepted actual block',ripe['mempool']['allowed'] and ripe['transactionIncluded'] and ripe['confirmedAtHeight']==ripe['requiredLockHeight']+1)
 check('CLTV ripe raw block has signed transaction',any(b['height']==ripe['confirmedAtHeight'] and any(tx['txid']==ripe['transactionTxid'] for tx in b['tx']) for b in blocks))
 stale=T['time-wall-clock-ahead-median-behind'];ready=T['time-chain-median-now-past-lock']
 check('node clock cannot replace MTP',stale['mockNodeTime']>stale['transactionLockTime']>stale['medianTimePast'] and not stale['mempool']['allowed'])
 check('MTP advancement enables same timed candidate',ready['medianTimePast']>ready['transactionLockTime'] and ready['mempool']['allowed'] and ready['mempool']['txid']==stale['mempool']['txid'])
 for label,expected in [('original-old-passphrase',False),('original-new-passphrase',True),('copied-new-passphrase',False),('copied-old-passphrase',True)]:
  c=P[label];check(label+' unlock matrix',c['unlockSucceeded']==expected)
  if not expected:check(label+' incorrect password RPC','error code: -14'in c['result']['rpcError']['stderr'])
  # Locate the relevant passphrase RPC and assert a preceding walletlock for this wallet.
  matches=[i for i,row in enumerate(p['rawRPC']) if row['method']=='walletpassphrase' and row['wallet']==c['wallet'] and (row['exitCode']==0)==expected]
  check(label+' checked from locked wallet',any(i>0 and p['rawRPC'][i-1]['method']=='walletlock' and p['rawRPC'][i-1]['wallet']==c['wallet'] for i in matches))
 setup=P['setup-encrypt-before-copy'];same=P['encrypted-snapshot-shares-funded-key'];change=P['passphrasechange-preserves-funded-key']
 check('snapshot contains same funded key and seed',same['bothLoaded'] and same['originalOwns'] and same['copiedOwns'] and same['samePublicKey'] and same['originalHDSeedID']==same['copiedHDSeedID'])
 check('password change does not rotate observed key',change['samePublicKey'] and change['hdseedidBefore']==change['hdseedidAfter'])
 check('initial encrypt status contradicted by measured seed ID',setup['hdseedidBefore']==setup['hdseedidAfter'] and 'new HD seed was generated'in setup['encryptionRPCText'])
 new=P['new-address-same-wallet'];check('copied keypool address observed control',new['copiedIsMine'] and new['copiedSignatureVerified'] and 'prefilled'in new['scope'])
 control=P['old-copy-still-signs-funded-coin'];check('copied old key signs accepted spend',control['signingComplete'] and control['mempool']['allowed'] and not control['copiedOwnsDestination'] and control['newWalletOwnsDestination'])
 transfer=P['confirmed-transfer-spends-old-outpoint'];check('transfer really spent old output',transfer['oldOutpoint'] is None and transfer['newConfirmations']>=1)
 spent=P['old-key-still-signs-spent-outpoint'];check('old key can sign but spent input blocks spend',spent['signingComplete'] and not spent['mempool']['allowed'] and spent['mempool']['reject-reason']=='missing-inputs')
 independent=P['new-independent-coin-needs-independent-key'];check('new independent key has exclusive tested control',not independent['copiedWalletSigning']['complete'] and independent['independentWalletSigning']['complete'] and not independent['copiedWalletMempool']['allowed'] and independent['independentWalletMempool']['allowed'] and not independent['copiedOwnsDestination'] and independent['independentOwnsDestination'])
 check('two studies used distinct initial wallets',f['outpoint']['txid']!=same['outpoint']['txid'])
 return {'protocol':'litecoin-watch-core-controls-v1','checks':len(checks),'allPassed':all(c['passed'] for c in checks),'runnerSHA256':h,'details':checks}
if __name__=='__main__':
 t,p,source=map(Path,sys.argv[1:4]);report=verify(json.loads(t.read_text()),json.loads(p.read_text()),source.read_bytes())
 print(json.dumps(report,indent=2))
