|
| 1 | +"""Strict migration helpers for historical OProver certification metadata.""" |
| 2 | +from __future__ import annotations |
| 3 | + |
| 4 | +import hashlib |
| 5 | +import json |
| 6 | +import os |
| 7 | +import re |
| 8 | +import time |
| 9 | +from pathlib import Path |
| 10 | +from typing import Mapping |
| 11 | + |
| 12 | + |
| 13 | +STRICT = "STRICTLY_INDEPENDENTLY_VERIFIED" |
| 14 | +SHORTCUT = "INVALID_PROOF_SHORTCUT" |
| 15 | +INCOMPLETE = "LEGACY_INCOMPLETE" |
| 16 | +RECOMPILE_FAILED = "RECOMPILE_FAILED" |
| 17 | +MISMATCH = "HASH/ENV_MISMATCH" |
| 18 | +REVOKED = "CERTIFICATION_REVOKED" |
| 19 | + |
| 20 | +_HOLE = re.compile(r"\b(?:sorry|admit|sorryAx)\b|\bby\?", re.IGNORECASE) |
| 21 | +_WARNING = re.compile(r"(?mi)^.*\bwarning:") |
| 22 | + |
| 23 | + |
| 24 | +def _canonical(value: object) -> bytes: |
| 25 | + return json.dumps( |
| 26 | + value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), |
| 27 | + ).encode() |
| 28 | + |
| 29 | + |
| 30 | +def _sha256(value: str | bytes) -> str: |
| 31 | + if isinstance(value, str): |
| 32 | + value = value.encode() |
| 33 | + return hashlib.sha256(value).hexdigest() |
| 34 | + |
| 35 | + |
| 36 | +def artifact_integrity(payload: Mapping[str, object]) -> bool: |
| 37 | + """Validate all self-contained hashes in a retained proof artifact.""" |
| 38 | + proof = str(payload.get("proof_body", "")) |
| 39 | + identity = { |
| 40 | + key: value for key, value in payload.items() |
| 41 | + if key not in {"artifact_hash", "created_at"} |
| 42 | + } |
| 43 | + return ( |
| 44 | + bool(payload.get("artifact_hash")) |
| 45 | + and _sha256(_canonical(identity)) == payload.get("artifact_hash") |
| 46 | + and _sha256(proof) == payload.get("proof_body_hash") |
| 47 | + and _sha256(proof) == payload.get("candidate_hash") |
| 48 | + and _sha256(str(payload.get("lean_output", ""))) |
| 49 | + == payload.get("lean_output_hash") |
| 50 | + and len(proof.encode()) == payload.get("proof_byte_length") |
| 51 | + ) |
| 52 | + |
| 53 | + |
| 54 | +def classify_candidate( |
| 55 | + candidate_hash: str, |
| 56 | + artifacts: Mapping[str, Mapping[str, object]], |
| 57 | + recompiles: Mapping[str, bool] | None = None, |
| 58 | +) -> str: |
| 59 | + """Classify one historically verified candidate under strict policy.""" |
| 60 | + artifact = artifacts.get(candidate_hash) |
| 61 | + if artifact is None: |
| 62 | + return INCOMPLETE |
| 63 | + if not artifact_integrity(artifact): |
| 64 | + return MISMATCH |
| 65 | + proof = str(artifact.get("proof_body", "")) |
| 66 | + if _HOLE.search(proof): |
| 67 | + return SHORTCUT |
| 68 | + if _WARNING.search(str(artifact.get("lean_output", ""))): |
| 69 | + return RECOMPILE_FAILED |
| 70 | + if recompiles is None or candidate_hash not in recompiles: |
| 71 | + return INCOMPLETE |
| 72 | + return STRICT if recompiles[candidate_hash] else RECOMPILE_FAILED |
| 73 | + |
| 74 | + |
| 75 | +def _contains_verified_status(value: object) -> bool: |
| 76 | + if isinstance(value, dict): |
| 77 | + if ( |
| 78 | + value.get("status") == "INDEPENDENTLY_VERIFIED" |
| 79 | + or value.get("proof_status") == "INDEPENDENTLY_VERIFIED" |
| 80 | + ): |
| 81 | + return True |
| 82 | + return any(_contains_verified_status(child) for child in value.values()) |
| 83 | + if isinstance(value, list): |
| 84 | + return any(_contains_verified_status(child) for child in value) |
| 85 | + return False |
| 86 | + |
| 87 | + |
| 88 | +def _candidate_fields(value: object) -> set[str]: |
| 89 | + found: set[str] = set() |
| 90 | + if isinstance(value, dict): |
| 91 | + for key in ("verified_candidate_hash", "selected_candidate_hash"): |
| 92 | + if value.get(key): |
| 93 | + found.add(str(value[key])) |
| 94 | + for child in value.values(): |
| 95 | + found.update(_candidate_fields(child)) |
| 96 | + elif isinstance(value, list): |
| 97 | + for child in value: |
| 98 | + found.update(_candidate_fields(child)) |
| 99 | + return found |
| 100 | + |
| 101 | + |
| 102 | +def collect_verified_candidate_hashes(value: object) -> set[str]: |
| 103 | + """Collect hashes in records containing a historical verified status.""" |
| 104 | + if not _contains_verified_status(value): |
| 105 | + return set() |
| 106 | + return _candidate_fields(value) |
| 107 | + |
| 108 | + |
| 109 | +def revoke_document( |
| 110 | + value: object, |
| 111 | + outcomes: Mapping[str, str], |
| 112 | + *, |
| 113 | + report_hash: str, |
| 114 | + inherited_outcome: str = "", |
| 115 | +) -> tuple[object, int]: |
| 116 | + """Revoke every invalid status in one JSON document.""" |
| 117 | + count = 0 |
| 118 | + if isinstance(value, dict): |
| 119 | + candidate = ( |
| 120 | + value.get("verified_candidate_hash") |
| 121 | + or value.get("selected_candidate_hash") |
| 122 | + ) |
| 123 | + outcome = outcomes.get(str(candidate), "") if candidate else "" |
| 124 | + if not outcome: |
| 125 | + descendant_candidates = collect_verified_candidate_hashes(value) |
| 126 | + descendant_outcomes = { |
| 127 | + outcomes[candidate] |
| 128 | + for candidate in descendant_candidates |
| 129 | + if candidate in outcomes and outcomes[candidate] != STRICT |
| 130 | + } |
| 131 | + if len(descendant_outcomes) == 1: |
| 132 | + outcome = descendant_outcomes.pop() |
| 133 | + if not outcome: |
| 134 | + outcome = inherited_outcome |
| 135 | + if ( |
| 136 | + value.get("status") == "INDEPENDENTLY_VERIFIED" |
| 137 | + and outcome |
| 138 | + and outcome != STRICT |
| 139 | + ): |
| 140 | + value["revoked_status"] = value["status"] |
| 141 | + value["status"] = REVOKED |
| 142 | + value["strict_audit_outcome"] = outcome |
| 143 | + value["strict_audit_report_hash"] = report_hash |
| 144 | + count += 1 |
| 145 | + if ( |
| 146 | + value.get("proof_status") == "INDEPENDENTLY_VERIFIED" |
| 147 | + and outcome |
| 148 | + and outcome != STRICT |
| 149 | + ): |
| 150 | + value["revoked_proof_status"] = value["proof_status"] |
| 151 | + value["proof_status"] = REVOKED |
| 152 | + value["strict_audit_outcome"] = outcome |
| 153 | + value["strict_audit_report_hash"] = report_hash |
| 154 | + count += 1 |
| 155 | + if outcome and outcome != STRICT: |
| 156 | + if value.get("integration_status") == "VERIFIED_PROOF_ARTIFACT": |
| 157 | + value["integration_status"] = REVOKED |
| 158 | + if value.get("classification") == "VERIFIED_SUPPORTING_LEMMA": |
| 159 | + value["classification"] = REVOKED |
| 160 | + for key, child in tuple(value.items()): |
| 161 | + value[key], child_count = revoke_document( |
| 162 | + child, outcomes, report_hash=report_hash, |
| 163 | + inherited_outcome=outcome, |
| 164 | + ) |
| 165 | + count += child_count |
| 166 | + elif isinstance(value, list): |
| 167 | + for index, child in enumerate(value): |
| 168 | + value[index], child_count = revoke_document( |
| 169 | + child, outcomes, report_hash=report_hash, |
| 170 | + inherited_outcome=inherited_outcome, |
| 171 | + ) |
| 172 | + count += child_count |
| 173 | + return value, count |
| 174 | + |
| 175 | + |
| 176 | +def commit_revocations( |
| 177 | + documents: Mapping[Path, object], |
| 178 | + outcomes: Mapping[str, str], |
| 179 | + *, |
| 180 | + report_hash: str, |
| 181 | + journal_path: Path, |
| 182 | +) -> int: |
| 183 | + """Stage, journal, and atomically replace each affected JSON file.""" |
| 184 | + staged: list[tuple[Path, Path]] = [] |
| 185 | + revoked = 0 |
| 186 | + for path, original in documents.items(): |
| 187 | + updated, count = revoke_document( |
| 188 | + original, outcomes, report_hash=report_hash, |
| 189 | + ) |
| 190 | + if not count: |
| 191 | + continue |
| 192 | + temporary = path.with_name(f".{path.name}.{report_hash}.audit.tmp") |
| 193 | + temporary.write_bytes(_canonical(updated) + b"\n") |
| 194 | + with temporary.open("rb") as handle: |
| 195 | + os.fsync(handle.fileno()) |
| 196 | + staged.append((path, temporary)) |
| 197 | + revoked += count |
| 198 | + journal = { |
| 199 | + "schema_version": 1, |
| 200 | + "report_hash": report_hash, |
| 201 | + "state": "PREPARED", |
| 202 | + "created_at": time.time(), |
| 203 | + "files": [str(path) for path, _ in staged], |
| 204 | + "revoked_status_count": revoked, |
| 205 | + } |
| 206 | + journal_path.parent.mkdir(parents=True, exist_ok=True) |
| 207 | + journal_path.write_bytes(_canonical(journal) + b"\n") |
| 208 | + for path, temporary in staged: |
| 209 | + os.replace(temporary, path) |
| 210 | + journal["state"] = "COMMITTED" |
| 211 | + journal_path.write_bytes(_canonical(journal) + b"\n") |
| 212 | + return revoked |
0 commit comments