"""Four ordinary regtest sends: wallet-local comments/labels versus serialized transactions. Python3 standard library; pass two verified Core0.21.5.8 binaries and a NEW output directory. No existing node, configuration, wallet, keys, seed or RPC credential is accepted. """ from datetime import datetime,timezone from decimal import Decimal from pathlib import Path import csv,hashlib,json,shutil,socket,subprocess,sys,tempfile,time PINS={"litecoind":"63fb440cdc5e2e7d46144b8e3a47dab4725fa9c056a77b8b12b11854504f4838", "litecoin-cli":"d185011cc0dbb5d2dd20611e9dc060dbb0a5d9be31a03f57da5dbd3dad4c3769"} def utc():return datetime.now(timezone.utc).isoformat() def serial(value): if isinstance(value,Decimal):return format(value,"f") if isinstance(value,dict):return {k:serial(v)for k,v in value.items()} if isinstance(value,list):return [serial(v)for v in value] return value def write(path,value):path.write_text(json.dumps(serial(value),indent=2,ensure_ascii=False)+"\n",encoding="utf-8") def names(info):return [v.get("name")if isinstance(v,dict)else v for v in info.get("labels",[])] def main(daemon,cli,output): output=output.resolve() if output.exists():raise ValueError("Choose a new output directory; existing output is never overwritten.") output.mkdir(parents=True) began=utc();start=time.monotonic();trace=[];assertions=[];lifecycle=[];cases=[];proc=None;log=None;root=None def check(name,value,detail=None): assertions.append({"name":name,"passed":bool(value),"details":serial(detail)}) if not value:raise AssertionError(name) versions={};hashes={} for name,binary in [("litecoind",daemon),("litecoin-cli",cli)]: hashes[name]=hashlib.sha256(binary.read_bytes()).hexdigest() check(name+" matches pinned binary",hashes[name]==PINS[name]) result=subprocess.run([str(binary),"-version"],capture_output=True,encoding="utf-8",timeout=8) check(name+" version command succeeds",result.returncode==0) versions[name]=result.stdout.splitlines()[0] check(name+" version0.21.5.8","0.21.5.8"in versions[name]) root=Path(tempfile.mkdtemp(prefix="node-",dir=output.parent)).resolve() data=root/"datadir";data.mkdir();reservation=socket.socket();reservation.bind(("127.0.0.1",0));port=reservation.getsockname()[1] flags=["-regtest","-connect=0","-listen=0","-dnsseed=0","-dns=0","-discover=0","-listenonion=0","-upnp=0", "-onlynet=ipv4","-dbcache=32","-par=1","-fallbackfee=0.00001","-acceptnonstdtxn=0", "-datadir="+str(data),"-rpcport="+str(port),"-rpcbind=127.0.0.1","-rpcallowip=127.0.0.1"] base=["-regtest","-datadir="+str(data),"-rpcport="+str(port)] def rpc(method,*args,wallet=None,wait=False): if time.monotonic()-start>75 and method!="stop":raise TimeoutError("75second experiment budget exhausted.") command=[str(cli),*base] if wallet:command.append("-rpcwallet="+wallet) if wait:command.append("-rpcwait") params=[json.dumps(v,separators=(",",":"),ensure_ascii=False)if isinstance(v,(list,dict,bool))else str(v)for v in args] result=subprocess.run([*command,method,*params],capture_output=True,encoding="utf-8",timeout=20 if wait else 8) entry={"time_utc":utc(),"wallet":wallet,"method":method,"params":params} if result.returncode: entry["error"]=result.stderr.strip();trace.append(entry);raise RuntimeError(method+": "+entry["error"]) try:value=json.loads(result.stdout,parse_float=Decimal)if result.stdout.strip()else None except json.JSONDecodeError:value=result.stdout.strip() if method=="getaddressinfo": entry["result"]={k:serial(value[k])for k in ["address","scriptPubKey","ismine","iswitness","witness_version","labels"]if k in value} entry["result_scope"]="Selected public fields only; no HD metadata is published." else:entry["result"]=serial(value) trace.append(entry);return value def launch(restart=False): nonlocal proc,log if not restart:reservation.close() log=(root/"process.log").open("ab") proc=subprocess.Popen([str(daemon),*flags,*(["-wallet=sender","-wallet=recipient"]if restart else [])],stdout=log,stderr=log) rpc("getblockchaininfo",wait=True) lifecycle.append({"time_utc":utc(),"event":"restarted"if restart else"started","pid":proc.pid}) check("Node is regtest after"+("restart"if restart else"first start"),rpc("getblockchaininfo")["chain"]=="regtest") check("No peers after"+("restart"if restart else"first start"),rpc("getconnectioncount")==0) def stop(): nonlocal log if proc and proc.poll()is None: rpc("stop");proc.wait(timeout=8) lifecycle.append({"time_utc":utc(),"event":"stopped","pid":proc.pid,"exit_code":proc.returncode}) if log:log.close();log=None def note_state(wallet,txid): result=rpc("gettransaction",txid,wallet=wallet) return result,{"comment_present":"comment"in result,"comment":result.get("comment"),"to_present":"to"in result,"to":result.get("to")} try: launch() rpc("createwallet","sender");rpc("createwallet","recipient") for method in ["sendtoaddress","gettransaction","setlabel"]: (output/("rpc-help-"+method+".txt")).write_text(rpc("help",method)+"\n",encoding="utf-8") mining=rpc("getnewaddress","","bech32",wallet="sender") rpc("generatetoaddress",101,mining) check("Initial height101",rpc("getblockcount")==101) inputs=[ {"id":"N01","kind":"empty control","amount":"0.01375","comment":"","comment_to":""}, {"id":"N02","kind":"ASCII invoice markers","amount":"0.02125","comment":"LW-NOTE-DEMO-2048","comment_to":"LW-TO-DEMO-ACCOUNTS"}, {"id":"N03","kind":"UTF8 markers","amount":"0.0325","comment":"LW-NOTE: café • 支払済み ✓","comment_to":"LW-TO: Łódź • 店舗 ✓"}, {"id":"N04","kind":"long repeated markers","amount":"0.0475","comment":"LW-NOTE-ONLY|"*64,"comment_to":"LW-TO-NOT-ON-CHAIN|"*32} ] for item in inputs: address=rpc("getnewaddress","","bech32",wallet="recipient") address_info=rpc("getaddressinfo",address,wallet="recipient") check(item["id"]+"ordinary native SegWit recipient",address_info["ismine"]is True and address_info["iswitness"]is True and address_info["witness_version"]==0) txid=rpc("sendtoaddress",address,item["amount"],item["comment"],item["comment_to"],wallet="sender") rpc("generatetoaddress",1,mining) sent,sm=note_state("sender",txid);received,rm=note_state("recipient",txid) raw=bytes.fromhex(sent["hex"]);decoded=rpc("decoderawtransaction",sent["hex"]) check(item["id"]+"both wallets return same published bytes",sent["hex"]==received["hex"]) check(item["id"]+"decoded txid matches",decoded["txid"]==txid) check(item["id"]+"receiver has no sender comment",not rm["comment_present"]) check(item["id"]+"receiver has no sender comment_to",not rm["to_present"]) for field,key in [("comment","comment"),("comment_to","to")]: check(item["id"]+"sender retains"+field,sent.get(key,"")==item[field]) matches=[v for v in decoded["vout"]if v["scriptPubKey"]["hex"]==address_info["scriptPubKey"]] check(item["id"]+"one exact recipient output",len(matches)==1 and matches[0]["value"]==Decimal(item["amount"])) check(item["id"]+"ordinary outputs have no OP_RETURN",all(v["scriptPubKey"]["type"]!="nulldata"for v in decoded["vout"])) searches={} for field in ["comment","comment_to"]: text=item[field].encode("utf-8") searches[field]={"utf8_bytes":len(text),"full_string_found":text in raw if text else None, "prefix24_found":text[:24]in raw if text else None} if text: check(item["id"]+"raw bytes omit full"+field,text not in raw) check(item["id"]+"raw bytes omit distinctive prefix"+field,text[:24]not in raw) record={**item,"recipient_address":address,"recipient_script":address_info["scriptPubKey"],"txid":txid, "sender_metadata_before_restart":sm,"recipient_metadata_before_restart":rm,"searches":searches, "raw_hex":sent["hex"],"decoded":decoded,"recipient_vout":matches[0]["n"],"observed_height":rpc("getblockcount")} cases.append(record) target=cases[1];slabel="LOCAL-SENDER-CONTACT-2048";rlabel="LOCAL-RECIPIENT-ORDER-2048" rpc("setlabel",target["recipient_address"],slabel,wallet="sender") rpc("setlabel",target["recipient_address"],rlabel,wallet="recipient") label_sender=rpc("getaddressinfo",target["recipient_address"],wallet="sender") label_receiver=rpc("getaddressinfo",target["recipient_address"],wallet="recipient") check("Each wallet has its own address label",names(label_sender)==[slabel]and names(label_receiver)==[rlabel]) s_after,sm_after=note_state("sender",target["txid"]);r_after,rm_after=note_state("recipient",target["txid"]) check("Address relabeling leaves published transaction byte-for-byte unchanged",s_after["hex"]==r_after["hex"]==target["raw_hex"]) check("Relabeling leaves sender note unchanged",sm_after==target["sender_metadata_before_restart"]) check("Relabeling does not deliver sender note",rm_after==target["recipient_metadata_before_restart"]) labels={"case_id":target["id"],"txid":target["txid"],"sender_label":slabel,"recipient_label":rlabel, "hex_before":target["raw_hex"],"hex_after":s_after["hex"],"sender_metadata_after":sm_after,"recipient_metadata_after":rm_after} check("Height105 before restart",rpc("getblockcount")==105) check("Recipient exact total before restart",rpc("getbalance",wallet="recipient")==Decimal("0.115")) stop();check("First process cleanly exited",proc.returncode==0) launch(restart=True);check("Both fresh wallets loaded after restart",set(rpc("listwallets"))=={"sender","recipient"}) for item in cases: sent,sm=note_state("sender",item["txid"]);received,rm=note_state("recipient",item["txid"]) item["sender_metadata_after_restart"]=sm;item["recipient_metadata_after_restart"]=rm item["sender_confirmations_after_restart"]=sent["confirmations"];item["recipient_confirmations_after_restart"]=received["confirmations"] check(item["id"]+"sender metadata survives restart",sm==item["sender_metadata_before_restart"]) check(item["id"]+"recipient still lacks sender metadata",rm==item["recipient_metadata_before_restart"]) check(item["id"]+"raw transaction survives restart unchanged",sent["hex"]==received["hex"]==item["raw_hex"]) expected=105-item["observed_height"]+1 check(item["id"]+"confirmation depth unchanged",sent["confirmations"]==received["confirmations"]==expected) check(item["id"]+"recipient exact received amount",rpc("getreceivedbyaddress",item["recipient_address"],1,wallet="recipient")==Decimal(item["amount"])) check("Receiver address label persists locally",names(rpc("getaddressinfo",target["recipient_address"],wallet="recipient"))==[rlabel]) check("Sender contact label persists separately",names(rpc("getaddressinfo",target["recipient_address"],wallet="sender"))==[slabel]) balance=rpc("getbalance",wallet="recipient");check("Exact total0.115LTC after restart",balance==Decimal("0.115")) check("No peer connections at completion",rpc("getconnectioncount")==0) stop();check("Restarted process cleanly exited",proc.returncode==0) result={"schema":1,"version":"1.0.0","started_at_utc":began,"completed_at_utc":utc(), "platform":sys.platform,"core_versions":versions,"binary_sha256":hashes,"network":"regtest", "node_options":flags[:13],"rpc_bound":"127.0.0.1","rpc_port":port, "scope":"One new isolated node; two independent fresh wallet databases. Four ordinary transparent native-SegWit sends, per-wallet comment/label inspection and one daemon restart. Not two networked nodes or a complete mobile/GUI wallet study.", "cases":cases,"labels":labels,"recipient_total_ltc":balance,"peer_lifecycle":lifecycle, "assertions_passed":len(assertions),"assertions":assertions} write(output/"results.json",result);write(output/"rpc-trace.json",{"scope":"Generated test wallets only; no private-key/seed/cookie calls; address-info result field subset stated per call.","calls":trace}) with(output/"cases.csv").open("w",encoding="utf-8",newline="")as h: cols=["case_id","kind","amount_ltc","comment_chars","comment_utf8_bytes","comment_to_chars","comment_to_utf8_bytes","sender_note_preserved","receiver_comment_present","receiver_to_present","comment_in_raw","comment_to_in_raw","vsize","confirmations"] writer=csv.DictWriter(h,fieldnames=cols);writer.writeheader() for item in cases: writer.writerow(dict(zip(cols,[item["id"],item["kind"],item["amount"],len(item["comment"]),len(item["comment"].encode()),len(item["comment_to"]),len(item["comment_to"].encode()),item["sender_metadata_before_restart"]==item["sender_metadata_after_restart"],item["recipient_metadata_after_restart"]["comment_present"],item["recipient_metadata_after_restart"]["to_present"],item["searches"]["comment"]["full_string_found"],item["searches"]["comment_to"]["full_string_found"],item["decoded"]["vsize"],item["recipient_confirmations_after_restart"]]))) print(json.dumps({"ok":True,"assertions":len(assertions),"elapsed_seconds":round(time.monotonic()-start,3),"output_files":["results.json","rpc-trace.json","cases.csv","rpc-help-sendtoaddress.txt","rpc-help-gettransaction.txt","rpc-help-setlabel.txt"]})) except BaseException: write(output/"failed-assertions.json",assertions);write(output/"failed-rpc-trace.json",{"calls":trace});raise finally: reservation.close() if proc and proc.poll()is None: try:stop() except Exception: proc.terminate() try:proc.wait(timeout=5) except subprocess.TimeoutExpired:proc.kill();proc.wait(timeout=5) if log:log.close() if root and root.is_dir(): if not(root.is_relative_to(output.parent.resolve())and root.name.startswith("node-")):raise RuntimeError("Unsafe disposable cleanup path") if proc and proc.poll()is None:raise RuntimeError("Refusing cleanup while own process remains running") shutil.rmtree(root) if __name__=="__main__": if len(sys.argv)!=4:raise SystemExit("Usage: python reproduce.py.txt /path/litecoind /path/litecoin-cli NEW-output-directory") main(Path(sys.argv[1]).resolve(),Path(sys.argv[2]).resolve(),Path(sys.argv[3]))