"""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'(?' for row in priv)) check(name+' private signing params redacted',all(row['params'][1]==[''] 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']=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))