"""Offline hash demonstration using the archived Litecoin.watch regtest study. Save as transaction-id-checks.py and run: python transaction-id-checks.py /path/to/batch-results.json Input: https://litecoin.watch/uploads/research/practical-20260930/batching/batch-results.json No network calls, wallet operations, broadcasts, signature checks or file writes. Altered cases are unvalidated data, NOT valid alternate payments. Only ordinary transparent SegWit serialization is supported, not MWEB. """ import hashlib,json,sys def doublehash(data):return hashlib.sha256(hashlib.sha256(data).digest()).digest()[::-1].hex() def parse(raw): b=bytes.fromhex(raw);p=4 assert b[p:p+2]==bytes([0,1]);p+=2 def vi(): nonlocal p first=b[p];p+=1 if first<253:return first n={253:2,254:4,255:8}[first];v=int.from_bytes(b[p:p+n],"little");p+=n;return v start=p;ni=vi() for _ in range(ni):p+=36;n=vi();p+=n+4 no=vi();first_output_value=p for _ in range(no):p+=8;n=vi();p+=n witness_start=p;first_item=None for _ in range(ni): count=vi() for j in range(count): n=vi() if first_item is None and n>8:first_item=p+n//2 p+=n locktime=p;p+=4;assert p==len(b) stripped=b[:4]+b[start:witness_start]+b[locktime:] return dict(txid=doublehash(stripped),wtxid=doublehash(b),bytes=len(b),base_bytes=len(stripped),weight=len(stripped)*3+len(b),first_witness_mutation_byte=first_item,first_output_value_byte=first_output_value) def calculate(archive): candidates=archive["candidates"] for c in candidates: p=parse(c["raw_hex"]);assert p["txid"]==c["txid"];assert p["weight"]==c["weight"] c=candidates[0];original=parse(c["raw_hex"]);b=bytearray.fromhex(c["raw_hex"]) b[original["first_witness_mutation_byte"]]^=1 witness=parse(b.hex()) b=bytearray.fromhex(c["raw_hex"]);b[original["first_output_value_byte"]]^=1 output=parse(b.hex()) assert witness["txid"]==original["txid"] and witness["wtxid"]!=original["wtxid"] assert output["txid"]!=original["txid"] and output["wtxid"]!=original["wtxid"] return dict(scope="ARCHIVED_REGTEST_BASE_PLUS_UNVALIDATED_OFFLINE_MUTATIONS_NOT_BROADCAST",archive_txid_weight_crosschecks=len(candidates),cases=[dict(case="archived_original",validation="Core local policy verdict in original archive only",**original),dict(case="witness_byte_changed",validation="NOT_VALIDATED_DO_NOT_BROADCAST",**witness),dict(case="output_value_byte_changed",validation="NOT_VALIDATED_DO_NOT_BROADCAST",**output)]) if __name__=="__main__": with open(sys.argv[1],encoding="utf-8") as f:archive=json.load(f) print(json.dumps(calculate(archive),indent=2))