src/pqc_ai_governance/vote.py
2.8 KB · 104 lines · python Raw
1 """Votes and signed votes."""
2
3 from __future__ import annotations
4
5 import json
6 import uuid
7 from dataclasses import asdict, dataclass
8 from datetime import datetime, timezone
9 from enum import Enum
10 from typing import Any
11
12
13 class VoteDecision(str, Enum):
14 APPROVE = "approve"
15 REJECT = "reject"
16 ABSTAIN = "abstain"
17
18
19 @dataclass
20 class Vote:
21 """A node's vote on a proposal (unsigned)."""
22
23 vote_id: str
24 proposal_id: str
25 proposal_hash: str # binds the vote to a specific proposal hash
26 voter_did: str
27 decision: VoteDecision
28 rationale: str = ""
29 cast_at: str = ""
30
31 @classmethod
32 def create(
33 cls,
34 proposal_id: str,
35 proposal_hash: str,
36 voter_did: str,
37 decision: VoteDecision,
38 rationale: str = "",
39 ) -> Vote:
40 return cls(
41 vote_id=f"urn:pqc-gov-vote:{uuid.uuid4().hex}",
42 proposal_id=proposal_id,
43 proposal_hash=proposal_hash,
44 voter_did=voter_did,
45 decision=decision,
46 rationale=rationale,
47 cast_at=datetime.now(timezone.utc).isoformat(),
48 )
49
50 def canonical_bytes(self) -> bytes:
51 payload = {
52 "vote_id": self.vote_id,
53 "proposal_id": self.proposal_id,
54 "proposal_hash": self.proposal_hash,
55 "voter_did": self.voter_did,
56 "decision": self.decision.value,
57 "rationale": self.rationale,
58 "cast_at": self.cast_at,
59 }
60 return json.dumps(
61 payload, sort_keys=True, separators=(",", ":"), ensure_ascii=False
62 ).encode("utf-8")
63
64 def to_dict(self) -> dict[str, Any]:
65 d = asdict(self)
66 d["decision"] = self.decision.value
67 return d
68
69
70 @dataclass
71 class SignedVote:
72 """A Vote + ML-DSA signature envelope."""
73
74 vote: Vote
75 algorithm: str
76 signature: str # hex
77 public_key: str # hex
78
79 def to_dict(self) -> dict[str, Any]:
80 return {
81 "vote": self.vote.to_dict(),
82 "algorithm": self.algorithm,
83 "signature": self.signature,
84 "public_key": self.public_key,
85 }
86
87 @classmethod
88 def from_dict(cls, data: dict[str, Any]) -> SignedVote:
89 v = data["vote"]
90 return cls(
91 vote=Vote(
92 vote_id=v["vote_id"],
93 proposal_id=v["proposal_id"],
94 proposal_hash=v["proposal_hash"],
95 voter_did=v["voter_did"],
96 decision=VoteDecision(v["decision"]),
97 rationale=v.get("rationale", ""),
98 cast_at=v.get("cast_at", ""),
99 ),
100 algorithm=data["algorithm"],
101 signature=data["signature"],
102 public_key=data["public_key"],
103 )
104