"""Offline configuration-record verification; no Core, network, wallet or mainnet data.
python verify.py.txt .
Checks data contracts and SHA whitelist; negative controls mutate in-memory test copies only.
It is not an independent witness to execution, a disk benchmark or a signature check.
"""
from pathlib import Path
import copy,csv,hashlib,json,re,sys,zipfile
BASE={"results.json","rpc-trace.json","cases.csv","reproduce.py.txt","verify.py.txt","README.txt","sources.json","results.schema.json",
      "daemon-help.txt","rpc-help-getblockchaininfo.txt","rpc-help-pruneblockchain.txt","storage-planning-checklist.txt",
      "storage-planning-worksheet.csv","implementation-sources.json","implementation-sources.zip","COPYING.txt","cleanup.json"}
OPTIONAL={"artwork-provenance.json","source-index.json","source-snapshots.zip","core-source-snapshots.json","core-source-snapshots.zip","verification.json","public-files.json"}
def manifest_entries(text):
    entries={}
    for line in text.splitlines():
        match=re.fullmatch(r"([a-f0-9]{64})  ([A-Za-z0-9][A-Za-z0-9_.-]{0,127})",line)
        if not match:raise AssertionError("Unsafe SHA manifest syntax/path")
        digest,name=match.groups()
        if name not in BASE|OPTIONAL or name in entries:raise AssertionError("Unknown or duplicate SHA manifest entry")
        entries[name]=digest
    if not BASE<=entries.keys():raise AssertionError("Required SHA entries missing")
    return entries
def validate(data,rows,events):
    checks=0
    def check(value,why):
        nonlocal checks
        if not value:raise AssertionError(why)
        checks+=1
    check(data["schema"]==1,"known result schema")
    check(data["network"]=="regtest","explicit isolated network")
    check(len(data["cases"])==len(rows)==5,"five cases")
    check(data["assertions_passed"]==len(data["assertions"])==45,"recorded experiment assertion count")
    check(all(a["passed"]for a in data["assertions"]),"all recorded experiment assertions pass")
    check(data["elapsed_seconds"]<120,"bounded observed run")
    check(all(v in data["node_options"]for v in ["-connect=0","-listen=0","-dnsseed=0","-dns=0","-discover=0","-disablewallet=1","-dbcache=32","-par=1"]),"isolation options")
    for name in ["litecoind","litecoin-cli"]:check("0.21.5.8"in data["core_versions"][name],"pinned version")
    cases={c["id"]:c for c in data["cases"]}
    check(set(cases)=={"P01","P02","P03","P04","P05"},"case IDs")
    for row,c in zip(rows,data["cases"]):
        check(row["case_id"]==c["id"]and int(row["prune_argument"])==c["prune"]and int(row["txindex_argument"])==c["txindex"],"CSV exact inputs")
        check(row["startup"]==c["startup"]and int(row["exit_code"])==c["exit_code"],"CSV startup/exit")
        check(c["startup"]in["started","rejected"],"bounded startup state")
        if c["startup"]=="started":
            info=c["blockchain_info"];check(info["chain"]=="regtest","reported actual chain")
            check(info["blocks"]==info["headers"]==0,"genesis only, no block pruning observed")
            check(c["exit_code"]==0,"successful node clean exit")
            check(c["pruned"]is info["pruned"],"pruned flag from actualRPC")
            check(c["automatic_pruning"]==info.get("automatic_pruning"),"automatic flag from actualRPC")
            check(c["prune_target_size"]==info.get("prune_target_size"),"target from actualRPC")
            rpc=[e for e in events if e.get("case")==c["id"]and e.get("method")=="getblockchaininfo"];check(len(rpc)==1 and rpc[0]["result"]==info,"raw RPC matches case")
            peers=[e for e in events if e.get("case")==c["id"]and e.get("method")=="getconnectioncount"];check(len(peers)==1 and peers[0]["result"]==0,"recorded zero peers")
            meta=[e for e in events if e.get("case")==c["id"]and e.get("method")=="getpeerinfo"];check(len(meta)==1 and meta[0]["result"]==[],"recorded empty peer metadata")
        else:
            check(c["exit_code"]!=0 and c["blockchain_info"]is None,"rejection cannot pretend RPC success")
            check(all(c[k]is None for k in["pruned","automatic_pruning","prune_target_size"]),"rejected fields unavailable")
            check(row["block_height"]=="","no invented rejected chain height")
        for name in["pruned","automatic_pruning","prune_target_size"]:
            expected=""if c[name]is None else str(c[name]);check(row[name]==expected,"CSV preserves false versus not reported")
        check(row["diagnostic"]==" | ".join(c["diagnostic_lines"]),"CSV raw error")
    p0,p549,p550,p1,ptx=(cases[k]for k in ["P01","P02","P03","P04","P05"])
    check(p0["prune"]==0 and p0["pruned"]is False and p0["prune_target_size"]is None,"disabled pruning semantics")
    check(p549["prune"]==549 and p549["startup"]=="rejected"and any("minimum of 550 MiB"in line for line in p549["diagnostic_lines"]),"measured minimum rejection")
    check(p550["prune"]==550 and p550["pruned"]is True and p550["automatic_pruning"]is True,"automatic mode")
    check(p550["prune_target_size"]==550*(2**20)==576716800,"independent MiB arithmetic, not decimal MB")
    check(p1["prune"]==1 and p1["pruned"]is True and p1["automatic_pruning"]is False and p1["prune_target_size"]is None,"manual1 not one MiB cap")
    check(ptx["prune"]==550 and ptx["txindex"]==1 and ptx["startup"]=="rejected"and any("Prune mode is incompatible with -txindex"in line for line in ptx["diagnostic_lines"]),"measured txindex incompatibility")
    starts=[v for v in data["process_lifecycle"]if v["event"]=="started_process"];exits=[v for v in data["process_lifecycle"]if v["event"]=="exited"]
    check(len(starts)==len(exits)==5,"all five process exit records")
    for i,(start,end)in enumerate(zip(starts,exits)):
        check(start["case"]==end["case"]and start["pid"]==end["pid"]and start["time_utc"]<=end["time_utc"],"paired process lifecycle")
        if i+1<len(starts):check(end["time_utc"]<=starts[i+1]["time_utc"],"nodes ran sequentially")
    check(not any(e.get("method","").startswith(("send","generate","import","dump","wallet"))for e in events),"no wallet/funding/mining RPC")
    return checks
def main(folder):
    entries=manifest_entries((folder/"SHA256SUMS.txt").read_text(encoding="utf-8"));checks=0
    for name,digest in entries.items():
        path=folder/name
        if not path.is_file()or path.is_symlink()or hashlib.sha256(path.read_bytes()).hexdigest()!=digest:raise AssertionError("SHA mismatch or unsafe file: "+name)
        checks+=1
    data=json.loads((folder/"results.json").read_text(encoding="utf-8"));rows=list(csv.DictReader((folder/"cases.csv").open(encoding="utf-8")));events=json.loads((folder/"rpc-trace.json").read_text(encoding="utf-8"))["events"]
    checks+=validate(data,rows,events)
    sources=json.loads((folder/"implementation-sources.json").read_text(encoding="utf-8"))
    with zipfile.ZipFile(folder/"implementation-sources.zip")as archive:
        for record in sources["sources"]:
            if hashlib.sha256(archive.read(record["file"])).hexdigest()!=record["sha256"]:raise AssertionError("Frozen upstream source mismatch")
            checks+=1
    negatives=[]
    for name,mutate in [
      ("decimalMB_target",lambda d:d["cases"][2].__setitem__("prune_target_size",550000000)),
      ("false_success_for549",lambda d:d["cases"][1].__setitem__("exit_code",0)),
      ("manual_reported_automatic",lambda d:d["cases"][3].__setitem__("automatic_pruning",True)),
      ("mainnet_claim",lambda d:d["cases"][0]["blockchain_info"].__setitem__("chain","main")),
      ("block_history_claim",lambda d:d["cases"][2]["blockchain_info"].__setitem__("blocks",1000000))]:
        changed=copy.deepcopy(data);mutate(changed)
        try:validate(changed,rows,events)
        except (AssertionError,KeyError):negatives.append(name);checks+=1
        else:raise AssertionError("Negative control passed: "+name)
    for unsafe in ["../wallet.dat","wallet.dat","C:/wallet.dat"]:
        try:manifest_entries("0"*64+"  "+unsafe+"\n")
        except AssertionError:negatives.append("reject_manifest_"+unsafe);checks+=1
        else:raise AssertionError("Unsafe manifest accepted")
    print(json.dumps({"ok":True,"checks":checks,"negative_controls_rejected":negatives,"automatic_target_bytes":550*(2**20),"scope":"Offline consistency, source/manifest hashes and unit arithmetic; not an independent runtime witness, actual-pruning test or storage benchmark."}))
if __name__=="__main__":main(Path(sys.argv[1]if len(sys.argv)>1 else"."))
