Skip to main content

oas_document/
governance.rs

1// OAS Spec §8 — ENR Governance
2//! Governance types for Entity Name Record (ENR) entities.
3//!
4//! An ENR holds a stable Ed25519 signing key governed by a rotating MHR.
5//! The governance section tracks which MHR currently controls the ENR,
6//! the epoch counter, and the full transition history.
7
8use serde::{Deserialize, Serialize};
9
10/// Governance section attached to ENR identity documents.
11///
12/// Tracks the governing MHR, monotonic epoch counter, transition history,
13/// and governance policy constraints.
14///
15/// # Invariants
16///
17/// - `epoch` MUST strictly increase (no gaps, no rewinds).
18/// - `current_mhr` MUST reference a valid, non-revoked MHR DID.
19/// - Each transition's `from` MUST match the previous transition's `to`.
20///
21/// # Examples
22///
23/// ```
24/// use oas_document::governance::GovernanceSection;
25///
26/// let json = r#"{
27///   "currentMhr": "did:oas:prod:mhr:council-alpha",
28///   "epoch": 1,
29///   "transitions": [],
30///   "policy": {
31///     "transitionRequires": "mhr_threshold",
32///     "signingKeyRotationRequires": "mhr_threshold",
33///     "maxTransitionsPerDay": 1,
34///     "timelockHours": 24
35///   }
36/// }"#;
37/// let gov: GovernanceSection = serde_json::from_str(json).unwrap();
38/// assert_eq!(gov.epoch, 1);
39/// ```
40#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
41#[serde(rename_all = "camelCase")]
42pub struct GovernanceSection {
43    /// DID of the MHR currently governing this ENR.
44    pub current_mhr: String,
45
46    /// Monotonically increasing epoch counter.
47    /// Incremented on every governance transition.
48    pub epoch: u64,
49
50    /// Complete history of governance transitions.
51    #[serde(default)]
52    pub transitions: Vec<GovernanceTransition>,
53
54    /// Governance policy constraints.
55    pub policy: GovernancePolicy,
56}
57
58/// A single governance transition record.
59///
60/// Records the handoff from one MHR to another, including the
61/// cryptographic proof (FROST threshold signature from the outgoing MHR).
62#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
63#[serde(rename_all = "camelCase")]
64pub struct GovernanceTransition {
65    /// DID of the outgoing MHR (None for the initial governance assignment).
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub from: Option<String>,
68
69    /// DID of the incoming MHR.
70    pub to: String,
71
72    /// Epoch at which this transition occurred.
73    pub epoch: u64,
74
75    /// Cryptographic proof of the transition.
76    pub proof: GovernanceTransitionProof,
77}
78
79/// Proof that a governance transition was authorized.
80///
81/// Contains a FROST threshold signature from the outgoing MHR members,
82/// proving that a quorum approved the handoff.
83#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
84#[serde(rename_all = "camelCase")]
85pub struct GovernanceTransitionProof {
86    /// Proof type identifier.
87    #[serde(rename = "type")]
88    pub proof_type: String,
89
90    /// FROST threshold signature from the outgoing MHR (base64url-encoded).
91    pub mhr_threshold_signature: String,
92
93    /// ISO 8601 timestamp of when the proof was created.
94    pub timestamp: String,
95}
96
97/// The canonical proof type string for ENR governance transitions.
98pub const ENR_GOVERNANCE_PROOF_TYPE: &str = "EnrGovernanceTransitionProof2025";
99
100/// Policy constraints governing how ENR transitions may occur.
101#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
102#[serde(rename_all = "camelCase")]
103pub struct GovernancePolicy {
104    /// What is required to transition governance (e.g., `"mhr_threshold"`).
105    pub transition_requires: String,
106
107    /// What is required to rotate the ENR signing key (e.g., `"mhr_threshold"`).
108    pub signing_key_rotation_requires: String,
109
110    /// Maximum number of governance transitions allowed per 24-hour period.
111    #[serde(default = "default_max_transitions")]
112    pub max_transitions_per_day: u32,
113
114    /// Mandatory delay (in hours) between initiating and confirming a transition.
115    #[serde(default = "default_timelock")]
116    pub timelock_hours: u32,
117}
118
119fn default_max_transitions() -> u32 {
120    1
121}
122
123fn default_timelock() -> u32 {
124    24
125}
126
127impl GovernancePolicy {
128    /// Creates a default governance policy with standard constraints.
129    pub fn default_policy() -> Self {
130        Self {
131            transition_requires: "mhr_threshold".to_string(),
132            signing_key_rotation_requires: "mhr_threshold".to_string(),
133            max_transitions_per_day: default_max_transitions(),
134            timelock_hours: default_timelock(),
135        }
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[test]
144    fn governance_section_round_trip() {
145        let section = GovernanceSection {
146            current_mhr: "did:oas:prod:mhr:council-alpha".to_string(),
147            epoch: 3,
148            transitions: vec![
149                GovernanceTransition {
150                    from: None,
151                    to: "did:oas:prod:mhr:council-alpha".to_string(),
152                    epoch: 1,
153                    proof: GovernanceTransitionProof {
154                        proof_type: ENR_GOVERNANCE_PROOF_TYPE.to_string(),
155                        mhr_threshold_signature: "sig-initial".to_string(),
156                        timestamp: "2026-01-01T00:00:00Z".to_string(),
157                    },
158                },
159                GovernanceTransition {
160                    from: Some("did:oas:prod:mhr:council-alpha".to_string()),
161                    to: "did:oas:prod:mhr:council-beta".to_string(),
162                    epoch: 2,
163                    proof: GovernanceTransitionProof {
164                        proof_type: ENR_GOVERNANCE_PROOF_TYPE.to_string(),
165                        mhr_threshold_signature: "sig-transition-2".to_string(),
166                        timestamp: "2026-02-01T00:00:00Z".to_string(),
167                    },
168                },
169            ],
170            policy: GovernancePolicy::default_policy(),
171        };
172
173        let json = serde_json::to_string_pretty(&section);
174        assert!(json.is_ok());
175        let json = json.unwrap_or_default();
176
177        let parsed: Result<GovernanceSection, _> = serde_json::from_str(&json);
178        assert!(parsed.is_ok());
179        let parsed = parsed.unwrap_or_else(|_| section.clone());
180        assert_eq!(parsed.epoch, 3);
181        assert_eq!(parsed.transitions.len(), 2);
182    }
183
184    #[test]
185    fn governance_policy_defaults() {
186        let policy = GovernancePolicy::default_policy();
187        assert_eq!(policy.max_transitions_per_day, 1);
188        assert_eq!(policy.timelock_hours, 24);
189        assert_eq!(policy.transition_requires, "mhr_threshold");
190    }
191
192    #[test]
193    fn governance_proof_type_constant() {
194        assert_eq!(
195            ENR_GOVERNANCE_PROOF_TYPE,
196            "EnrGovernanceTransitionProof2025"
197        );
198    }
199
200    #[test]
201    fn empty_transitions_deserializes() {
202        let json = r#"{
203            "currentMhr": "did:oas:test:mhr:council",
204            "epoch": 1,
205            "policy": {
206                "transitionRequires": "mhr_threshold",
207                "signingKeyRotationRequires": "mhr_threshold"
208            }
209        }"#;
210        let section: Result<GovernanceSection, _> = serde_json::from_str(json);
211        assert!(section.is_ok());
212        let section = section.unwrap_or_else(|_| GovernanceSection {
213            current_mhr: String::new(),
214            epoch: 0,
215            transitions: vec![],
216            policy: GovernancePolicy::default_policy(),
217        });
218        assert!(section.transitions.is_empty());
219        assert_eq!(section.policy.max_transitions_per_day, 1);
220        assert_eq!(section.policy.timelock_hours, 24);
221    }
222}