"""Verify the frozen duplicate-payment evidence offline; Python3 standard library. Usage: python verify.py.txt [package-directory] Does not start Core, contact any server or write to the package directory. The transparent transaction decoder is intentionally limited to this fixture. It is not a consensus verifier or a tool for authorizing a real payment. """ from decimal import Decimal from pathlib import Path import csv import hashlib import io import json import sys CHECKS = 0 def check(name, condition): global CHECKS CHECKS += 1 if not condition: raise AssertionError(name) def atoms(value): number = Decimal(str(value)) * 100000000 check("LTC amount is an exact whole litoshi", number == number.to_integral_value()) return int(number) def decode(raw_hex): raw = bytes.fromhex(raw_hex) if len(raw) > 1000000: raise ValueError("Fixture exceeds decoder byte budget") offset = 0 def take(count): nonlocal offset if count < 0 or offset + count > len(raw): raise ValueError("Truncated transaction") part = raw[offset:offset + count] offset += count return part def compact(): first = take(1)[0] if first < 253: return first size = {253: 2, 254: 4, 255: 8}[first] value = int.from_bytes(take(size), "little") if value < {253: 253, 254: 65536, 255: 4294967296}[first]: raise ValueError("Noncanonical compact size") if value > 1000000: raise ValueError("Compact-size fixture budget exceeded") return value version = take(4) witness = raw[offset:offset + 1] == b"\x00" if witness and take(2) != b"\x00\x01": raise ValueError("Only ordinary transparent witness serialization is supported") inputs_start = offset count = compact() if count < 1 or count > 1000: raise ValueError("Input count outside fixture budget") inputs = [] for _ in range(count): txid, vout = take(32)[::-1].hex(), int.from_bytes(take(4), "little") take(compact()) take(4) inputs.append({"txid": txid, "vout": vout}) inputs_bytes = raw[inputs_start:offset] outputs_start = offset output_count = compact() if output_count < 1 or output_count > 1000: raise ValueError("Output count outside fixture budget") outputs = [] for _ in range(output_count): outputs.append({"atoms": int.from_bytes(take(8), "little"), "script_hex": take(compact()).hex()}) outputs_bytes = raw[outputs_start:offset] if witness: for _ in range(count): stack_count = compact() if stack_count > 1000: raise ValueError("Witness stack outside fixture budget") for _ in range(stack_count): take(compact()) locktime = take(4) if offset != len(raw): raise ValueError("Unexpected extension or trailing bytes") stripped = version + inputs_bytes + outputs_bytes + locktime txid = hashlib.sha256(hashlib.sha256(stripped).digest()).digest()[::-1].hex() return {"txid": txid, "inputs": inputs, "outputs": outputs, "raw_sha256": hashlib.sha256(raw).hexdigest()} def main(folder): checksums = folder / "SHA256SUMS.txt" if checksums.is_file(): for line in checksums.read_text(encoding="utf-8").splitlines(): if not line.strip(): continue expected, name = line.split(" ", 1) target = (folder / name).resolve() check("Manifest stays within package", target.is_relative_to(folder.resolve())) check("SHA256 " + name, hashlib.sha256(target.read_bytes()).hexdigest() == expected) else: raise FileNotFoundError("SHA256SUMS.txt is required") results = json.loads((folder / "results.json").read_text(encoding="utf-8")) transactions = json.loads((folder / "transactions.json").read_text(encoding="utf-8")) check("Results transaction copy matches independent file", results["transactions"] == transactions) check("Recorded Core release", results["core_version"] == "/LitecoinCore:0.21.5.8/") check("Only regtest", results["network"] == "regtest") check("Two distinct payments", len(transactions) == 2 and len({row["txid"] for row in transactions}) == 2) total_fee = 0 identities = set() for payment in transactions: parsed = decode(payment["raw_hex"]) check("Recomputed txid " + payment["payment_id"], parsed["txid"] == payment["txid"]) check("Raw bytes checksum " + payment["payment_id"], parsed["raw_sha256"] == payment["raw_sha256"]) target = parsed["outputs"][payment["recipient_vout"]] check("Recipient script independently decoded", target["script_hex"] == payment["recipient_script_hex"]) check("Recipient native witness script", target["script_hex"].startswith("0014") and len(target["script_hex"]) == 44) check("Intended recipient amount0.125LTC", target["atoms"] == atoms(payment["recipient_value_ltc"]) == 12500000) check("All outputs agree with independent decoder", [row["atoms"] for row in parsed["outputs"]] == [atoms(value) for value in payment["all_outputs_ltc"]]) check("Recorded input identities match raw", parsed["inputs"] == [{"txid": row["txid"], "vout": row["vout"]} for row in payment["inputs"]]) input_atoms = 0 for row in payment["inputs"]: previous = decode(row["previous_raw_hex"]) check("Previous transaction hash independently derived", previous["txid"] == row["txid"]) value = previous["outputs"][row["vout"]]["atoms"] check("Prevout value derived from raw bytes", value == atoms(row["value_ltc"])) input_atoms += value fee = input_atoms - sum(row["atoms"] for row in parsed["outputs"]) check("Fee derived from all input/output amounts", fee == atoms(payment["fee_ltc"]) == 1410) check("Same local invoice comment", payment["sender_comment"] == results["invoice_comment"]) check("Observed confirmations positive", payment["final_confirmations"] >= 1) total_fee += fee identities.add((payment["txid"], payment["recipient_vout"])) check("Two independent recipient output identities", len(identities) == 2) check("Final summed fee2820litoshi", total_fee == atoms(results["summary"]["total_miner_fees_ltc"]) == 2820) check("Final recipient0.25LTC", atoms(results["summary"]["recipient_total_ltc"]) == 25000000) first_id = transactions[0]["txid"] mempool, confirmed = results["rebroadcasts"] check("Observed mempool rebroadcast returns original hash", mempool["response"] == first_id) check("Observed confirmed rebroadcast error preserved", confirmed["response"]["kind"] == "rpc_error" and confirmed["response"]["code"] == -27 and "Transaction already in block chain" in confirmed["response"]["message"]) stages = results["stages"] expected = [(0, 0, 0, 0), (12500000, 0, 1, 1), (12500000, 0, 1, 1), (12500000, 12500000, 1, 0), (12500000, 12500000, 1, 0), (25000000, 12500000, 2, 1), (25000000, 25000000, 2, 0)] check("Seven declared stages", len(stages) == len(expected)) csv_rows = list(csv.DictReader(io.StringIO((folder / "stages.csv").read_text(encoding="utf-8")))) check("CSV stage count", len(csv_rows) == len(stages)) for stage, csv_row, state in zip(stages, csv_rows, expected): all_received, confirmed_received, output_count, mempool_count = state check("Stage total " + stage["stage"], atoms(stage["recipient_received_minconf0_ltc"]) == all_received) check("Confirmed-only total " + stage["stage"], atoms(stage["recipient_received_minconf1_ltc"]) == confirmed_received) check("Stage output count " + stage["stage"], len(stage["recipient_unspent_outputs"]) == output_count) check("Stage mempool count " + stage["stage"], len(stage["mempool_txids"]) == mempool_count) check("Zero peers at every stage", stage["peer_count"] == 0) check("CSV totals and identities match JSON", csv_row["stage"] == stage["stage"] and atoms(csv_row["recipient_received_minconf0_ltc"]) == all_received and int(csv_row["recipient_output_count"]) == output_count) check("Final receipt identities match decoded transactions", {(row["txid"], row["vout"]) for row in stages[-1]["recipient_unspent_outputs"]} == identities) check("All experiment assertions passed", all(row["passed"] is True for row in results["assertions"]) and len(results["assertions"]) == results["assertions_passed"] == 38) check("Own node exited cleanly and temporary directory removed", results["cleanup"] == {"own_node_stopped": True, "fresh_directory_removed": True, "exit_code": 0}) calls = json.loads((folder / "rpc-trace.json").read_text(encoding="utf-8"))["calls"] send_calls = [row for row in calls if row["method"] == "sendtoaddress"] raw_calls = [row for row in calls if row["method"] == "sendrawtransaction"] check("Exactly two wallet Send operations", len(send_calls) == 2) check("Same address, amount and comment in both Send calls", send_calls[0]["params"] == send_calls[1]["params"]) check("No fee subtraction", send_calls[0]["params"][4] == "false") check("Exactly two rebroadcast calls use identical original bytes", len(raw_calls) == 2 and all(row["params"][0] == transactions[0]["raw_hex"] for row in raw_calls)) forbidden = {"dumpprivkey", "dumpwallet", "backupwallet", "gethdkeys", "listdescriptors", "getaddressinfo", "importprivkey", "importwallet"} check("No export or import of private wallet material", not forbidden.intersection(row["method"] for row in calls)) print(json.dumps({"success": True, "checks_passed": CHECKS, "recipient_litoshi": 25000000, "miner_fee_litoshi": total_fee, "distinct_outputs": len(identities), "mode": "offline verification of frozen regtest evidence"})) if __name__ == "__main__": main(Path(sys.argv[1]).resolve() if len(sys.argv) > 1 else Path(__file__).resolve().parent)