"""Offline Electrum-LTC 4.2.2.1 recovery experiment, synthetic wallets only. Usage: python electrum-recovery.py /path/to/electrum-cli trust-results-private.json output.json The upstream signed Windows executable is used directly. Every call includes --offline, a fresh data directory and explicit wallet path. Never supply a real recovery phrase. The account-key bridge tests xprv/zprv import, NOT Electrum's GUI BIP39 wizard. """ from pathlib import Path import subprocess,json,hashlib,tempfile,datetime,sys B=Path(__file__).parent;exe=Path(sys.argv[1]);trust=json.loads(Path(sys.argv[2]).read_text());out=Path(sys.argv[3]) root=Path(tempfile.mkdtemp(prefix='lw-synthetic-electrum-'));checks=[];evidence={} password='public-test-wallet-file-password' def run(args,wallet=None,expect_error=False): cmd=[str(exe),'--offline','--dir',str(root/'config'),*(['--wallet',str(root/wallet)] if wallet else []),*args] p=subprocess.run(cmd,capture_output=True,text=True,timeout=35,creationflags=getattr(subprocess,'CREATE_NO_WINDOW',0)) if expect_error:return p.returncode!=0,(p.stderr+p.stdout).strip() if p.returncode:raise RuntimeError('Electrum command failed: '+args[0]+': '+p.stderr+p.stdout) try:return json.loads(p.stdout) except json.JSONDecodeError:return p.stdout.strip() def check(id,value,detail): if not value:raise AssertionError(id) checks.append({'id':id,'passed':True,'detail':detail}) def restore(text,name,passphrase=''): return run(['restore',text,'--passphrase',passphrase,'--password',password,'--encrypt_file','false'],name) def addresses(name,change=False):return run(['listaddresses','--change' if change else '--receiving'],name) def sign(name,address):return run(['signmessage',address,'Litecoin.watch synthetic recovery proof','--password',password],name) check('release-version',run(['version'])=='4.2.2.1','Signed project Windows executable, running offline.') fixture=B/'electrum-public-fixture.json' if fixture.exists():seed=json.loads(fixture.read_text())['mnemonic'] else: seed=run(['make_seed','--seed_type','segwit']);fixture.write_text(json.dumps({'warning':'Public synthetic test phrase. Never use or fund.','mnemonic':seed},indent=2)) restore(seed,'native-original');restore(seed,'native-restored') recv=addresses('native-original');change=addresses('native-original',True) check('native-seed-receive',recv==addresses('native-restored'),'Receiving-address list matches after a fresh native Electrum seed restore.') check('native-seed-change',change==addresses('native-restored',True),'Change-address list matches after a fresh native Electrum seed restore.') signature=sign('native-restored',recv[0]);check('native-restored-signing',run(['verifymessage',recv[0],signature,'Litecoin.watch synthetic recovery proof']) is True,'Restored private-key wallet signs a message that the executable verifies. No on-chain spend is claimed.') restore(seed,'native-extension','public-extension') check('native-extension-changes-address',addresses('native-extension')[0]!=recv[0],'Same Electrum words with a seed extension produce another first address.') mpk=run(['getmpk'],'native-original');restore(mpk,'native-watch-only') check('watch-only-addresses',addresses('native-watch-only')==recv,'A master-public-key restore reproduces the receiving addresses.') failed,reason=run(['signmessage',recv[0],'Litecoin.watch synthetic recovery proof','--password',password],'native-watch-only',True) watch_data=json.loads((root/'native-watch-only').read_text()) watch_keystore=watch_data['keystore'] check('watch-only-cannot-sign',failed and not watch_keystore.get('xprv') and watch_keystore.get('xpub')==mpk,'Master-public-key-only wallet has no account private key and produced no signature. The CLI returned a generic NoneType error, not a friendly watch-only message. Address matching alone does not prove spending capability.') evidence['native']={'mnemonic':seed,'receivingAddresses':recv,'changeAddresses':change,'signature':signature,'watchOnlyHasAccountPrivateKey':bool(watch_keystore.get('xprv')),'watchOnlyRejection':reason[:500]} for bridge in trust['bridges']: name='bip39-key-'+bridge['kind'];restore(bridge['xprv'],name);got=addresses(name);got_change=addresses(name,True) rows=[r for r in trust['rows'] if r['passphrase']=='' and r['kind']==bridge['kind'] and r['path'].startswith(bridge['accountPath']+'/')] compared=[] for r in rows: branch,index=map(int,r['path'].split('/')[-2:]);pool=got_change if branch else got if index>=len(pool):continue check('bridge-'+bridge['kind']+'-'+str(branch)+'-'+str(index),pool[index]==r['address'],'Trust Wallet Core BIP39-derived account private key imported into Electrum-LTC; same path and script type produce the same address.') compared.append({'path':r['path'],'expectedAddress':r['address'],'electrumAddress':pool[index]}) signature=sign(name,got[0]);check('bridge-signing-'+bridge['kind'],run(['verifymessage',got[0],signature,'Litecoin.watch synthetic recovery proof']) is True,'Imported account private key signs a verified message; no chain history or mobile app tested.') evidence[bridge['kind']]={'accountPath':bridge['accountPath'],'compared':compared,'signature':signature,'receivingPrepared':len(got),'changePrepared':len(got_change)} failed,reason=run(['restore',trust['mnemonic'],'--password',password,'--encrypt_file','false'],'bip39-default-mode',True) check('bip39-not-native-cli-seed',failed,'Default Electrum CLI restore does not recognize this BIP39 phrase as a native Electrum seed. Its GUI BIP39 restore option was not exercised.') evidence['bip39DefaultModeRejection']=reason[:500] result={'schema':1,'testedAt':datetime.datetime.now(datetime.timezone.utc).isoformat(),'implementation':'Electrum-LTC Windows executable, offline CLI','version':'4.2.2.1','binarySha256':hashlib.sha256(exe.read_bytes()).hexdigest(),'platform':sys.platform,'scope':'Native seed restores, master-public-key watch-only restore, BIP39-derived account-key imports and message signing. No GUI BIP39 wizard, balance discovery, mainnet spend or mobile application tested.','checks':checks,'evidence':evidence} out.write_text(json.dumps(result,indent=2),encoding='utf-8');print(json.dumps({'passed':len(checks),'nativeReceive':len(recv),'nativeChange':len(change),'output':str(out)}))