use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GovernanceSection {
pub current_mhr: String,
pub epoch: u64,
#[serde(default)]
pub transitions: Vec<GovernanceTransition>,
pub policy: GovernancePolicy,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GovernanceTransition {
#[serde(skip_serializing_if = "Option::is_none")]
pub from: Option<String>,
pub to: String,
pub epoch: u64,
pub proof: GovernanceTransitionProof,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GovernanceTransitionProof {
#[serde(rename = "type")]
pub proof_type: String,
pub mhr_threshold_signature: String,
pub timestamp: String,
}
pub const ENR_GOVERNANCE_PROOF_TYPE: &str = "EnrGovernanceTransitionProof2025";
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GovernancePolicy {
pub transition_requires: String,
pub signing_key_rotation_requires: String,
#[serde(default = "default_max_transitions")]
pub max_transitions_per_day: u32,
#[serde(default = "default_timelock")]
pub timelock_hours: u32,
}
fn default_max_transitions() -> u32 {
1
}
fn default_timelock() -> u32 {
24
}
impl GovernancePolicy {
pub fn default_policy() -> Self {
Self {
transition_requires: "mhr_threshold".to_string(),
signing_key_rotation_requires: "mhr_threshold".to_string(),
max_transitions_per_day: default_max_transitions(),
timelock_hours: default_timelock(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn governance_section_round_trip() {
let section = GovernanceSection {
current_mhr: "did:oas:prod:mhr:council-alpha".to_string(),
epoch: 3,
transitions: vec![
GovernanceTransition {
from: None,
to: "did:oas:prod:mhr:council-alpha".to_string(),
epoch: 1,
proof: GovernanceTransitionProof {
proof_type: ENR_GOVERNANCE_PROOF_TYPE.to_string(),
mhr_threshold_signature: "sig-initial".to_string(),
timestamp: "2026-01-01T00:00:00Z".to_string(),
},
},
GovernanceTransition {
from: Some("did:oas:prod:mhr:council-alpha".to_string()),
to: "did:oas:prod:mhr:council-beta".to_string(),
epoch: 2,
proof: GovernanceTransitionProof {
proof_type: ENR_GOVERNANCE_PROOF_TYPE.to_string(),
mhr_threshold_signature: "sig-transition-2".to_string(),
timestamp: "2026-02-01T00:00:00Z".to_string(),
},
},
],
policy: GovernancePolicy::default_policy(),
};
let json = serde_json::to_string_pretty(§ion);
assert!(json.is_ok());
let json = json.unwrap_or_default();
let parsed: Result<GovernanceSection, _> = serde_json::from_str(&json);
assert!(parsed.is_ok());
let parsed = parsed.unwrap_or_else(|_| section.clone());
assert_eq!(parsed.epoch, 3);
assert_eq!(parsed.transitions.len(), 2);
}
#[test]
fn governance_policy_defaults() {
let policy = GovernancePolicy::default_policy();
assert_eq!(policy.max_transitions_per_day, 1);
assert_eq!(policy.timelock_hours, 24);
assert_eq!(policy.transition_requires, "mhr_threshold");
}
#[test]
fn governance_proof_type_constant() {
assert_eq!(
ENR_GOVERNANCE_PROOF_TYPE,
"EnrGovernanceTransitionProof2025"
);
}
#[test]
fn empty_transitions_deserializes() {
let json = r#"{
"currentMhr": "did:oas:test:mhr:council",
"epoch": 1,
"policy": {
"transitionRequires": "mhr_threshold",
"signingKeyRotationRequires": "mhr_threshold"
}
}"#;
let section: Result<GovernanceSection, _> = serde_json::from_str(json);
assert!(section.is_ok());
let section = section.unwrap_or_else(|_| GovernanceSection {
current_mhr: String::new(),
epoch: 0,
transitions: vec![],
policy: GovernancePolicy::default_policy(),
});
assert!(section.transitions.is_empty());
assert_eq!(section.policy.max_transitions_per_day, 1);
assert_eq!(section.policy.timelock_hours, 24);
}
}