src/pqc_reasoning_ledger/proof.py
2.0 KB · 63 lines · python Raw
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