oas-document 1.1.1

OAS Identity Document types and validation for the Open Agent Specification
Documentation
// OAS Spec §8 — ENR Governance
//! Governance types for Entity Name Record (ENR) entities.
//!
//! An ENR holds a stable Ed25519 signing key governed by a rotating MHR.
//! The governance section tracks which MHR currently controls the ENR,
//! the epoch counter, and the full transition history.

use serde::{Deserialize, Serialize};

/// Governance section attached to ENR identity documents.
///
/// Tracks the governing MHR, monotonic epoch counter, transition history,
/// and governance policy constraints.
///
/// # Invariants
///
/// - `epoch` MUST strictly increase (no gaps, no rewinds).
/// - `current_mhr` MUST reference a valid, non-revoked MHR DID.
/// - Each transition's `from` MUST match the previous transition's `to`.
///
/// # Examples
///
/// ```
/// use oas_document::governance::GovernanceSection;
///
/// let json = r#"{
///   "currentMhr": "did:oas:prod:mhr:council-alpha",
///   "epoch": 1,
///   "transitions": [],
///   "policy": {
///     "transitionRequires": "mhr_threshold",
///     "signingKeyRotationRequires": "mhr_threshold",
///     "maxTransitionsPerDay": 1,
///     "timelockHours": 24
///   }
/// }"#;
/// let gov: GovernanceSection = serde_json::from_str(json).unwrap();
/// assert_eq!(gov.epoch, 1);
/// ```
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GovernanceSection {
    /// DID of the MHR currently governing this ENR.
    pub current_mhr: String,

    /// Monotonically increasing epoch counter.
    /// Incremented on every governance transition.
    pub epoch: u64,

    /// Complete history of governance transitions.
    #[serde(default)]
    pub transitions: Vec<GovernanceTransition>,

    /// Governance policy constraints.
    pub policy: GovernancePolicy,
}

/// A single governance transition record.
///
/// Records the handoff from one MHR to another, including the
/// cryptographic proof (FROST threshold signature from the outgoing MHR).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GovernanceTransition {
    /// DID of the outgoing MHR (None for the initial governance assignment).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub from: Option<String>,

    /// DID of the incoming MHR.
    pub to: String,

    /// Epoch at which this transition occurred.
    pub epoch: u64,

    /// Cryptographic proof of the transition.
    pub proof: GovernanceTransitionProof,
}

/// Proof that a governance transition was authorized.
///
/// Contains a FROST threshold signature from the outgoing MHR members,
/// proving that a quorum approved the handoff.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GovernanceTransitionProof {
    /// Proof type identifier.
    #[serde(rename = "type")]
    pub proof_type: String,

    /// FROST threshold signature from the outgoing MHR (base64url-encoded).
    pub mhr_threshold_signature: String,

    /// ISO 8601 timestamp of when the proof was created.
    pub timestamp: String,
}

/// The canonical proof type string for ENR governance transitions.
pub const ENR_GOVERNANCE_PROOF_TYPE: &str = "EnrGovernanceTransitionProof2025";

/// Policy constraints governing how ENR transitions may occur.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GovernancePolicy {
    /// What is required to transition governance (e.g., `"mhr_threshold"`).
    pub transition_requires: String,

    /// What is required to rotate the ENR signing key (e.g., `"mhr_threshold"`).
    pub signing_key_rotation_requires: String,

    /// Maximum number of governance transitions allowed per 24-hour period.
    #[serde(default = "default_max_transitions")]
    pub max_transitions_per_day: u32,

    /// Mandatory delay (in hours) between initiating and confirming a transition.
    #[serde(default = "default_timelock")]
    pub timelock_hours: u32,
}

fn default_max_transitions() -> u32 {
    1
}

fn default_timelock() -> u32 {
    24
}

impl GovernancePolicy {
    /// Creates a default governance policy with standard constraints.
    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(&section);
        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);
    }
}