src/pqc_reasoning_ledger/proof.py
| 1 | """StepInclusionProof + ReasoningProver - prove a specific step is in a sealed trace.""" |
| 2 | |
| 3 | from __future__ import annotations |
| 4 | |
| 5 | from dataclasses import asdict, dataclass |
| 6 | from typing import Any |
| 7 | |
| 8 | from pqc_reasoning_ledger.errors import StepNotFoundError |
| 9 | from pqc_reasoning_ledger.merkle import InclusionProof, build_proof, verify_inclusion |
| 10 | from pqc_reasoning_ledger.step import ReasoningStep |
| 11 | from pqc_reasoning_ledger.trace import SealedTrace |
| 12 | |
| 13 | |
| 14 | @dataclass |
| 15 | class StepInclusionProof: |
| 16 | """Inclusion proof for a single step in a sealed trace.""" |
| 17 | |
| 18 | step: ReasoningStep |
| 19 | proof: InclusionProof |
| 20 | trace_id: str |
| 21 | merkle_root: str |
| 22 | |
| 23 | def to_dict(self) -> dict[str, Any]: |
| 24 | return { |
| 25 | "step": self.step.to_dict(), |
| 26 | "proof": asdict(self.proof), |
| 27 | "trace_id": self.trace_id, |
| 28 | "merkle_root": self.merkle_root, |
| 29 | } |
| 30 | |
| 31 | |
| 32 | class ReasoningProver: |
| 33 | """Produce and verify inclusion proofs for steps in sealed traces.""" |
| 34 | |
| 35 | @staticmethod |
| 36 | def prove_step(sealed: SealedTrace, step_id: str) -> StepInclusionProof: |
| 37 | idx: int | None = None |
| 38 | for i, s in enumerate(sealed.steps): |
| 39 | if s.step_id == step_id: |
| 40 | idx = i |
| 41 | break |
| 42 | if idx is None: |
| 43 | raise StepNotFoundError( |
| 44 | f"no step with id {step_id} in trace {sealed.metadata.trace_id}" |
| 45 | ) |
| 46 | leaves = [s.step_hash for s in sealed.steps] |
| 47 | proof = build_proof(leaves, idx, sealed.merkle_root) |
| 48 | return StepInclusionProof( |
| 49 | step=sealed.steps[idx], |
| 50 | proof=proof, |
| 51 | trace_id=sealed.metadata.trace_id, |
| 52 | merkle_root=sealed.merkle_root, |
| 53 | ) |
| 54 | |
| 55 | @staticmethod |
| 56 | def verify_proof(proof: StepInclusionProof) -> bool: |
| 57 | expected_leaf = proof.step.step_hash |
| 58 | if expected_leaf != proof.proof.leaf_hash: |
| 59 | return False |
| 60 | if proof.proof.root != proof.merkle_root: |
| 61 | return False |
| 62 | return verify_inclusion(proof.proof) |
| 63 | |