"""Dust boundary experiment: isolated Litecoin Core 0.21.5.8 regtest only. Usage: python dust-boundaries.py /path/to/litecoind /path/to/litecoin-cli results.json Creates a fresh temporary data directory, disables networking, and enables standard transaction policy explicitly. Never reads an existing wallet or sends real LTC. """ from pathlib import Path from decimal import Decimal import csv, datetime, hashlib, json, socket, subprocess, sys, tempfile, time TYPES = [('legacy', 'P2PKH', 34, 148), ('p2sh-segwit', 'P2SH-P2WPKH', 32, 148), ('bech32', 'P2WPKH', 31, 67)] def main(): daemon, cli, out = map(Path, sys.argv[1:4]) root = Path(tempfile.mkdtemp(prefix='lw-dust-regtest-')) with socket.socket() as sock: sock.bind(('127.0.0.1', 0)); port = sock.getsockname()[1] opts = ['-regtest', '-datadir='+str(root), '-rpcport='+str(port)] isolation = ['-listen=0', '-connect=0', '-dnsseed=0', '-discover=0', '-upnp=0', '-printtoconsole=0', '-acceptnonstdtxn=0'] process = None def rpc(method, *args, wallet=None): p = subprocess.run([str(cli), *opts, *(['-rpcwallet='+wallet] if wallet else []), method, *[json.dumps(a, separators=(',', ':')) if isinstance(a, (dict, list, bool)) else str(a) for a in args]], capture_output=True, text=True, timeout=60, creationflags=getattr(subprocess, 'CREATE_NO_WINDOW', 0)) if p.returncode: raise RuntimeError(method+': '+p.stderr) try: return json.loads(p.stdout) except json.JSONDecodeError: return p.stdout.strip() def start(extra): nonlocal process process = subprocess.Popen([str(daemon), *opts, *isolation, *extra], stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, creationflags=getattr(subprocess, 'CREATE_NO_WINDOW', 0)) for _ in range(100): try: v = rpc('getnetworkinfo')['subversion']; break except Exception: time.sleep(.3) else: raise RuntimeError('Node did not start') assert v == '/LitecoinCore:0.21.5.8/', v assert rpc('getblockchaininfo')['chain'] == 'regtest' assert rpc('getconnectioncount') == 0 return v def stop(): nonlocal process if process is None: return try: rpc('stop') except Exception: process.terminate() process.wait(timeout=30); process = None def amount(n): return format(Decimal(n)/100000000, '.8f') rows = [] try: version = start([]) rpc('createwallet', 'lab') miner = rpc('getnewaddress', '', 'bech32', wallet='lab') rpc('generatetoaddress', 101, miner) coin = max(rpc('listunspent', wallet='lab'), key=lambda v:v['confirmations']) input_litoshis = int(Decimal(str(coin['amount'])) * 100000000) change = rpc('getnewaddress', '', 'bech32', wallet='lab') addresses = {kind:rpc('getnewaddress', '', kind, wallet='lab') for kind, *_ in TYPES} def candidate(phase, rate, kind, label, output_bytes, spend_allowance, delta, fee): threshold = (output_bytes + spend_allowance) * rate // 1000 value = threshold + delta unsigned = rpc('createrawtransaction', [{'txid':coin['txid'], 'vout':coin['vout']}], {addresses[kind]:amount(value), change:amount(input_litoshis-value-fee)}) signed = rpc('signrawtransactionwithwallet', unsigned, wallet='lab') assert signed['complete'] decoded = rpc('decoderawtransaction', signed['hex']) target = next(o for o in decoded['vout'] if int(Decimal(str(o['value']))*100000000)==value) assert len(bytes.fromhex(target['scriptPubKey']['hex']))+9 == output_bytes verdict = rpc('testmempoolaccept', [signed['hex']])[0] expected = delta >= 0 assert verdict['allowed'] == expected, (phase, kind, value, verdict) if not expected: assert verdict['reject-reason'] == 'dust', verdict assert rpc('getrawmempool') == [] rows.append({'phase':phase, 'type':label, 'dust_relay_litoshis_per_kb':rate, 'policy_threshold_litoshis':threshold, 'target_litoshis':value, 'offset_litoshis':delta, 'target_ltc':amount(value), 'transaction_fee_litoshis':fee, 'transaction_vsize':decoded['vsize'], 'output_bytes':output_bytes, 'policy_spend_allowance':spend_allowance, 'script_pub_key':target['scriptPubKey']['hex'], 'txid':decoded['txid'], 'raw_hex':signed['hex'], 'verdict':verdict, 'expected_allowed':expected, 'passed':True}) for kind, label, size, spend in TYPES: for delta in [-1,0,1]: candidate('default', 30000, kind, label, size, spend, delta, 10000) candidate('default-higher-fee', 30000, kind, label, size, spend, -1, 100000) stop(); start(['-dustrelayfee=0.00060000']) if 'lab' not in rpc('listwallets'): rpc('loadwallet', 'lab') for kind, label, size, spend in TYPES: for delta in [-1,0,1]: candidate('double-dust-policy', 60000, kind, label, size, spend, delta, 10000) result = {'schema':1, 'release':'2026-09-27.1', 'tested_at':datetime.datetime.now(datetime.timezone.utc).isoformat(), 'version':version, 'platform':sys.platform, 'binary_sha256':hashlib.sha256(daemon.read_bytes()).hexdigest(), 'network':'regtest', 'isolation_flags':isolation, 'default_dust_relay_override':None, 'second_phase_flags':['-dustrelayfee=0.00060000'], 'scope':'Transparent P2PKH, nested P2WPKH in P2SH, and native P2WPKH outputs. Local mempool policy only; no mainnet propagation, mining, wallet payment construction, MWEB or economic-value measurement.', 'funding_input':{'txid':coin['txid'], 'vout':coin['vout'], 'value_litoshis':input_litoshis, 'confirmations':coin['confirmations']}, 'cases':rows, 'passed':len(rows)} out.parent.mkdir(parents=True,exist_ok=True) out.write_text(json.dumps(result,indent=2)+'\n',encoding='utf-8') with out.with_suffix('.csv').open('w',encoding='utf-8',newline='') as f: fields=['phase','type','dust_relay_litoshis_per_kb','policy_threshold_litoshis','target_litoshis','offset_litoshis','target_ltc','transaction_fee_litoshis','transaction_vsize','allowed','reject_reason'] writer=csv.DictWriter(f,fields);writer.writeheader() for row in rows: writer.writerow({**{k:row[k] for k in fields if k in row},'allowed':row['verdict']['allowed'],'reject_reason':row['verdict'].get('reject-reason','')}) print(json.dumps({'passed':len(rows),'version':version,'rows':[{k:r[k] for k in ['phase','type','target_litoshis','transaction_fee_litoshis','verdict']} for r in rows]})) finally: stop() if __name__ == '__main__': main()