Skip to main content

kinetic_core/error/
governance.rs

1//! Governance action verification and voting error types (`KIN-GOV-NNN`).
2//!
3//! [`GovernanceError`] is returned by the active [`GovernanceEngine`](crate::traits::GovernanceEngine)
4//! when a [`SignedGovernanceMessage`](crate::governance::types::SignedGovernanceMessage) fails
5//! signature verification, threshold checks, or timelock constraints.
6//!
7//! ## Protocol Context
8//!
9//! Kinetic governance is pluggable: `network.json` selects one of four engines
10//! (`sovereign`, `council`, `permissionless`) at compile time. Each engine
11//! runs `verify_action()` before any state mutation occurs.
12//!
13//! Key roles:
14//! - **Root key**: Ultimate authority; can ratify any action in Founder phase.
15//! - **Guard key**: Emergency veto key for OTA updates and root key rotation.
16//! - **Council members**: Vote on proposals; majority/supermajority required.
17//!
18//! > Note: `KIN-GOV-010`, `KIN-GOV-011`, and `KIN-GOV-012` are intentionally
19//! > skipped in the stable code registry to allow for future expansion.
20use super::Severity;
21use thiserror::Error;
22
23/// Errors relating to Kinetic global governance actions.
24#[derive(Error, Debug, PartialEq, Eq)]
25pub enum GovernanceError {
26    /// The `ROOT_PUBLIC_KEY_HEX` environment variable is absent; no governance can proceed.
27    #[error("ROOT_PUBLIC_KEY_HEX is not configured. This is a fatal error.")]
28    MissingRootKey,
29
30    /// A supplied public key byte slice does not match the required length (e.g. 1,952 bytes for ML-DSA-65).
31    #[error("Key length mismatch")]
32    KeyLengthMismatch,
33    /// The governance proposal timestamp is older than the allowed replay window.
34    #[error("Governance action too old, replay rejected")]
35    StaleProposal,
36    /// The mandatory delay after a council vote has not elapsed yet.
37    #[error("Timelock has not expired yet")]
38    TimelockNotExpired,
39
40    /// The target action hash is not in a pending-or-vetoed state.
41    #[error("Target hash is not a pending timelock or was vetoed")]
42    NotPendingOrVetoed,
43
44    /// Governance modifications are completely disabled in this network environment.
45    #[error("Governance is disabled in permissionless mode")]
46    GovernanceDisabled,
47
48    /// The number of valid signatures does not meet the required threshold.
49    #[error("Insufficient valid signatures")]
50    InsufficientSignatures,
51    /// A premium name grant/revoke was attempted on a name that is not exactly 1 character long.
52    #[error("Premium name grants must be exactly 1 character long")]
53    InvalidPremiumNameLength,
54    /// An infrastructure name grant/revoke was attempted on a name not in the Category 2 list.
55    #[error("Infrastructure name grants must target a valid Category 2 infrastructure name")]
56    InvalidInfrastructureName,
57}
58
59impl GovernanceError {
60    /// Stable protocol error code. Part of the Kinetic error taxonomy.
61    pub fn code(&self) -> &'static str {
62        match self {
63            Self::MissingRootKey => "KIN-GOV-001",
64            Self::GovernanceDisabled => "KIN-GOV-002",
65            Self::KeyLengthMismatch => "KIN-GOV-003",
66            Self::StaleProposal => "KIN-GOV-004",
67            Self::TimelockNotExpired => "KIN-GOV-005",
68
69            Self::NotPendingOrVetoed => "KIN-GOV-007",
70
71            Self::InsufficientSignatures => "KIN-GOV-016",
72            Self::InvalidPremiumNameLength => "KIN-GOV-019",
73            Self::InvalidInfrastructureName => "KIN-GOV-020",
74        }
75    }
76
77    /// RFC 7807 type URI for this error.
78    pub fn error_type_uri(&self) -> String {
79        format!("{}/errors/{}", crate::constants::DOCS_URL, self.code())
80    }
81
82    /// Severity level for logging and monitoring.
83    pub fn severity(&self) -> Severity {
84        match self {
85            Self::MissingRootKey => Severity::Critical,
86            Self::StaleProposal | Self::TimelockNotExpired | Self::NotPendingOrVetoed => {
87                Severity::Info
88            }
89            Self::KeyLengthMismatch => Severity::Error,
90            Self::InsufficientSignatures
91            | Self::GovernanceDisabled
92            | Self::InvalidPremiumNameLength
93            | Self::InvalidInfrastructureName => Severity::Warning,
94        }
95    }
96
97    /// Whether the client should offer a retry action.
98    pub fn is_retryable(&self) -> bool {
99        matches!(
100            self,
101            Self::TimelockNotExpired | Self::InsufficientSignatures
102        )
103    }
104
105    /// Clean user-facing message with no developer details.
106    pub fn user_message(&self) -> String {
107        match self {
108            Self::MissingRootKey => "The ROOT_PUBLIC_KEY_HEX environment variable is not set. This is a fatal configuration error.".to_string(),
109
110            Self::KeyLengthMismatch => "The provided cryptographic key length is invalid.".to_string(),
111            Self::StaleProposal => "The proposed governance action is too old and has been rejected to prevent replay attacks.".to_string(),
112            Self::TimelockNotExpired => "The governance action is still in its mandatory waiting period and cannot be executed yet.".to_string(),
113
114            Self::NotPendingOrVetoed => {
115                "The requested governance hash is not in a modifiable pending state.".to_string()
116            }
117            Self::GovernanceDisabled => {
118                "The network is operating in permissionless mode where governance actions are universally rejected.".to_string()
119            }
120            Self::InsufficientSignatures => {
121                "The message lacks the required cryptographic signatures to meet the council quorum threshold.".to_string()
122            }
123            Self::InvalidPremiumNameLength => "Premium names governed by this action must be exactly 1 character long.".to_string(),
124            Self::InvalidInfrastructureName => "Infrastructure names governed by this action must be valid Category 2 names.".to_string(),
125        }
126    }
127}