"""Synthetic address fixtures checked by isolated Core main/regtest parsers. No wallets, peers, broadcasts, deposits or mainnet chain observations. Run: python address-checks.py /absolute/path/to/litecoind /new/output/directory All generated addresses are fixtures. NEVER SEND FUNDS TO THEM. """ from pathlib import Path import base64,csv,datetime,hashlib,json,socket,subprocess,sys,tempfile,time,urllib.request,urllib.error ALPHABET='123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' CHARSET='qpzry9x8gf2tvdw0s3jn54khce6mua7l' def base58(version,payload): raw=bytes([version])+payload;raw+=hashlib.sha256(hashlib.sha256(raw).digest()).digest()[:4] num=int.from_bytes(raw,'big');encoded='' while num:num,rem=divmod(num,58);encoded=ALPHABET[rem]+encoded return '1'*(len(raw)-len(raw.lstrip(b'\0')))+encoded def bech32(hrp,program): values=[0];acc=bits=0 for byte in program: acc=(acc<<8)|byte;bits+=8 while bits>=5:bits-=5;values.append((acc>>bits)&31) if bits:values.append((acc<<(5-bits))&31) expanded=[ord(c)>>5 for c in hrp]+[0]+[ord(c)&31 for c in hrp] chk=1 for value in expanded+values+[0]*6: top=chk>>25;chk=((chk&0x1ffffff)<<5)^value for index,g in enumerate([0x3b6a57b2,0x26508e6d,0x1ea119fa,0x3d4233dd,0x2a1462b3]): if (top>>index)&1:chk^=g chk^=1;checksum=[(chk>>5*(5-i))&31 for i in range(6)] return hrp+'1'+''.join(CHARSET[v] for v in values+checksum) def fixtures(): a=hashlib.sha256(b'Litecoin.watch synthetic address fixture A 2026-10-03').digest()[:20] b=hashlib.sha256(b'Litecoin.watch synthetic address fixture B 2026-10-03').digest()[:20] l=base58(48,a);native=bech32('ltc',a) rows=[('ltc_legacy_L','LTC mainnet P2PKH',l,True,False),('ltc_script_M','LTC mainnet modern P2SH',base58(50,a),True,False), ('shared_script_3','Historical P2SH version 5',base58(5,a),True,False),('bitcoin_legacy_1','Bitcoin mainnet P2PKH version 0',base58(0,a),False,False), ('shared_test_legacy','Test-network P2PKH version 111',base58(111,a),False,True),('shared_test_script','Test-network P2SH version 196',base58(196,a),False,True), ('ltc_test_script','Litecoin test P2SH version 58',base58(58,a),False,True),('ltc_native','LTC mainnet witness v0',native,True,False), ('bitcoin_native','Bitcoin witness-v0 bc HRP',bech32('bc',a),False,False),('ltc_test_native','Litecoin testnet tltc HRP',bech32('tltc',a),False,False), ('ltc_regtest_native','Litecoin regtest rltc HRP',bech32('rltc',a),False,True),('other_valid_ltc','Different valid LTC witness-v0 payload',bech32('ltc',b),True,False), ('uppercase_native','All-uppercase LTC Bech32',native.upper(),True,False),('mixed_case_native','Mixed-case LTC Bech32',native[:3].upper()+native[3:],False,False), ('base58_typo','One checksum-character substitution',l[:-1]+('1' if l[-1]!='1' else '2'),False,False), ('bech32_typo','One Bech32 checksum-character substitution',native[:-1]+('q' if native[-1]!='q' else 'p'),False,False), ('evm_style','EVM-style 0x string', '0x'+a.hex(),False,False),('payment_uri','URI rather than bare address','litecoin:'+native+'?amount=1',False,False)] return [{'id':i,'description':d,'address':address,'expected_main':main,'expected_regtest':reg} for i,d,address,main,reg in rows] class ParserNode: def __init__(self,daemon,root,chain): self.root=root/chain;self.root.mkdir();self.chain=chain with socket.socket() as s:s.bind(('127.0.0.1',0));self.port=s.getsockname()[1] self.cookie=self.root/('regtest/.cookie' if chain=='regtest' else '.cookie') args=[str(daemon),'-chain='+chain,'-datadir='+str(self.root),'-rpcport='+str(self.port),'-rpcbind=127.0.0.1', '-listen=0','-connect=0','-dnsseed=0','-discover=0','-upnp=0','-disablewallet=1','-dbcache=32','-printtoconsole=0'] self.process=subprocess.Popen(args,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,creationflags=getattr(subprocess,'CREATE_NO_WINDOW',0)) try: for _ in range(120): try:self.rpc('getblockchaininfo');break except Exception: if self.process.poll() is not None:raise RuntimeError('Parser node exited') time.sleep(.25) else:raise RuntimeError('Parser node startup timeout') except BaseException: self.process.terminate();self.process.wait(timeout=30);raise def rpc(self,method,*params): cookie=self.cookie.read_text().strip() req=urllib.request.Request('http://127.0.0.1:'+str(self.port)+'/',data=json.dumps({'jsonrpc':'1.0','id':'address-check','method':method,'params':params}).encode(), headers={'Authorization':'Basic '+base64.b64encode(cookie.encode()).decode(),'Content-Type':'application/json'}) try: with urllib.request.urlopen(req,timeout=15) as r:obj=json.load(r) except urllib.error.HTTPError as err:obj=json.loads(err.read()) if obj['error']:raise RuntimeError(method+': '+str(obj['error'])) return obj['result'] def stop(self): try:self.rpc('stop') except Exception: if self.process.poll() is None:self.process.terminate() self.process.wait(timeout=30) def main(): daemon=Path(sys.argv[1]).resolve();out=Path(sys.argv[2]).resolve();out.mkdir(parents=True,exist_ok=True) nodes=[];checks=[];results=[];metadata={} def check(name,value): assert value,name;checks.append({'name':name,'passed':True}) with tempfile.TemporaryDirectory(prefix='lw-address-parser-') as tmp: root=Path(tmp).resolve();check('Fresh temporary test root',root.parent==Path(tempfile.gettempdir()).resolve()) try: for chain in ['main','regtest']: n=ParserNode(daemon,root,chain);nodes.append(n);info=n.rpc('getblockchaininfo');version=n.rpc('getnetworkinfo')['subversion'] peers=n.rpc('getconnectioncount');check(chain+' network mode',info['chain']==chain) check(chain+' no peers or synchronization',peers==0 and info['blocks']==0) check(chain+' pinned Core version','0.21.5.8' in version) metadata[chain]={'chain':chain,'height':info['blocks'],'peers':peers,'version':version,'wallet_disabled':True} for fixture in fixtures(): observed=n.rpc('validateaddress',fixture['address']);check(chain+'/'+fixture['id'],observed['isvalid']==fixture['expected_'+chain]) results.append({'network_mode':chain,**fixture,'observed':observed}) main={r['id']:r['observed'] for r in results if r['network_mode']=='main'} check('Historical 3 and modern M encode same output script',main['shared_script_3']['scriptPubKey']==main['ltc_script_M']['scriptPubKey']) check('Uppercase Bech32 encodes same script',main['ltc_native']['scriptPubKey']==main['uppercase_native']['scriptPubKey']) check('Two distinct LTC destinations both pass',main['ltc_native']['isvalid'] and main['other_valid_ltc']['isvalid'] and main['ltc_native']['scriptPubKey']!=main['other_valid_ltc']['scriptPubKey']) result={'schema':1,'release':'2026-10-03-v1','tested_at':datetime.datetime.now(datetime.timezone.utc).isoformat(), 'binary_sha256':hashlib.sha256(daemon.read_bytes()).hexdigest(),'nodes':metadata,'fixture_count':18,'rpc_checks':36, 'scope':'Core address parsing on disconnected mainnet-mode and regtest nodes, both at height zero, wallets disabled. Synthetic hash payloads; no ownership, service-credit, recovery, wallet-GUI, MWEB or transfer test. NEVER SEND FUNDS TO ANY FIXTURE.', 'results':results,'assertions':checks} (out/'address-results.json').write_text(json.dumps(result,indent=2)+'\n',encoding='utf-8') with (out/'address-checks.csv').open('w',newline='',encoding='utf-8') as f: w=csv.writer(f);w.writerow(['network_mode','fixture_id','description','valid','script_hex','fixture_address_do_not_send']) for r in results:w.writerow([r['network_mode'],r['id'],r['description'],r['observed']['isvalid'],r['observed'].get('scriptPubKey',''),r['address']]) print(json.dumps({'fixtures':18,'rpc_checks':36,'assertions':len(checks),'nodes':metadata})) finally: for n in reversed(nodes):n.stop() if __name__=='__main__':main()