use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::proof::ProofState;
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
)]
#[serde(rename_all = "snake_case")]
pub enum RuntimeMode {
Unknown,
Dev,
RemoteUnsigned,
LocalUnsigned,
SignedLocalLedger,
ExternallyAnchored,
AuthorityGrade,
}
impl RuntimeMode {
#[must_use]
pub const fn claim_ceiling(self) -> ClaimCeiling {
match self {
Self::Unknown => ClaimCeiling::DevOnly,
Self::Dev => ClaimCeiling::DevOnly,
Self::RemoteUnsigned => ClaimCeiling::LocalUnsigned,
Self::LocalUnsigned => ClaimCeiling::LocalUnsigned,
Self::SignedLocalLedger => ClaimCeiling::SignedLocalLedger,
Self::ExternallyAnchored => ClaimCeiling::ExternallyAnchored,
Self::AuthorityGrade => ClaimCeiling::AuthorityGrade,
}
}
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
)]
#[serde(rename_all = "snake_case")]
pub enum AuthorityClass {
Untrusted,
Derived,
Observed,
Verified,
Operator,
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
)]
#[serde(rename_all = "snake_case")]
pub enum ClaimProofState {
Unknown,
Broken,
Partial,
FullChainVerified,
}
impl ClaimProofState {
#[must_use]
pub const fn claim_ceiling(self) -> ClaimCeiling {
match self {
Self::Unknown | Self::Broken => ClaimCeiling::DevOnly,
Self::Partial => ClaimCeiling::LocalUnsigned,
Self::FullChainVerified => ClaimCeiling::AuthorityGrade,
}
}
}
impl From<ProofState> for ClaimProofState {
fn from(value: ProofState) -> Self {
match value {
ProofState::FullChainVerified => Self::FullChainVerified,
ProofState::Partial => Self::Partial,
ProofState::Broken => Self::Broken,
}
}
}
impl AuthorityClass {
#[must_use]
pub const fn claim_ceiling(self) -> ClaimCeiling {
match self {
Self::Untrusted => ClaimCeiling::DevOnly,
Self::Derived | Self::Observed => ClaimCeiling::LocalUnsigned,
Self::Verified => ClaimCeiling::SignedLocalLedger,
Self::Operator => ClaimCeiling::AuthorityGrade,
}
}
#[must_use]
pub fn weakest<I>(classes: I) -> Option<Self>
where
I: IntoIterator<Item = Self>,
{
classes.into_iter().min()
}
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
)]
#[serde(rename_all = "snake_case")]
pub enum ClaimCeiling {
DevOnly,
LocalUnsigned,
SignedLocalLedger,
ExternallyAnchored,
AuthorityGrade,
}
impl ClaimCeiling {
#[must_use]
pub fn weakest<I>(ceilings: I) -> Option<Self>
where
I: IntoIterator<Item = Self>,
{
ceilings.into_iter().min()
}
#[must_use]
pub fn mix_to_weakest(self, other: Self) -> Self {
self.min(other)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct ReportableClaim {
claim: String,
runtime_mode: RuntimeMode,
authority_class: AuthorityClass,
proof_state: ClaimProofState,
requested_ceiling: ClaimCeiling,
effective_ceiling: ClaimCeiling,
downgrade_reasons: Vec<String>,
}
impl ReportableClaim {
#[must_use]
pub fn new(
claim: impl Into<String>,
runtime_mode: RuntimeMode,
authority_class: AuthorityClass,
proof_state: ClaimProofState,
requested_ceiling: ClaimCeiling,
) -> Self {
let mut downgrade_reasons = Vec::new();
let effective_ceiling = effective_ceiling(
runtime_mode,
authority_class,
proof_state,
requested_ceiling,
);
if effective_ceiling < requested_ceiling {
downgrade_reasons.push(format!(
"requested ceiling {requested_ceiling:?} downgraded to {effective_ceiling:?}"
));
}
if proof_state != ClaimProofState::FullChainVerified {
downgrade_reasons.push(format!(
"proof state {proof_state:?} limits authority claims"
));
}
Self {
claim: claim.into(),
runtime_mode,
authority_class,
proof_state,
requested_ceiling,
effective_ceiling,
downgrade_reasons,
}
}
#[must_use]
pub fn claim(&self) -> &str {
&self.claim
}
#[must_use]
pub const fn runtime_mode(&self) -> RuntimeMode {
self.runtime_mode
}
#[must_use]
pub const fn authority_class(&self) -> AuthorityClass {
self.authority_class
}
#[must_use]
pub const fn proof_state(&self) -> ClaimProofState {
self.proof_state
}
#[must_use]
pub const fn requested_ceiling(&self) -> ClaimCeiling {
self.requested_ceiling
}
#[must_use]
pub const fn effective_ceiling(&self) -> ClaimCeiling {
self.effective_ceiling
}
#[must_use]
pub fn downgrade_reasons(&self) -> &[String] {
&self.downgrade_reasons
}
#[must_use]
pub fn downgraded_to(mut self, ceiling: ClaimCeiling, reason: impl Into<String>) -> Self {
self.requested_ceiling = self.requested_ceiling.min(ceiling);
self.effective_ceiling = effective_ceiling(
self.runtime_mode,
self.authority_class,
self.proof_state,
self.requested_ceiling,
);
self.downgrade_reasons.push(reason.into());
self
}
#[must_use]
pub fn mix_to_weakest(mut self, other: &Self, claim: impl Into<String>) -> Self {
self.claim = claim.into();
self.runtime_mode = self.runtime_mode.min(other.runtime_mode);
self.authority_class = self.authority_class.min(other.authority_class);
self.proof_state = self.proof_state.min(other.proof_state);
self.requested_ceiling = self.requested_ceiling.min(other.requested_ceiling);
self.effective_ceiling = self.effective_ceiling.min(other.effective_ceiling);
self.downgrade_reasons
.extend(other.downgrade_reasons.iter().cloned());
self.downgrade_reasons
.push("mixed claim downgraded to weakest contributing claim".into());
self
}
}
#[must_use]
pub fn effective_ceiling(
runtime_mode: RuntimeMode,
authority_class: AuthorityClass,
proof_state: ClaimProofState,
requested_ceiling: ClaimCeiling,
) -> ClaimCeiling {
ClaimCeiling::weakest([
runtime_mode.claim_ceiling(),
authority_class.claim_ceiling(),
proof_state.claim_ceiling(),
requested_ceiling,
])
.expect("fixed-size ceiling set is non-empty")
}
#[must_use]
pub fn mix_authority_to_weakest<I>(classes: I) -> Option<AuthorityClass>
where
I: IntoIterator<Item = AuthorityClass>,
{
AuthorityClass::weakest(classes)
}
#[must_use]
pub fn mix_claims_to_weakest<I>(ceilings: I) -> Option<ClaimCeiling>
where
I: IntoIterator<Item = ClaimCeiling>,
{
ClaimCeiling::weakest(ceilings)
}
#[must_use]
pub fn mix_reportable_claims_to_weakest<'a, I>(claims: I) -> Option<ClaimCeiling>
where
I: IntoIterator<Item = &'a ReportableClaim>,
{
claims
.into_iter()
.map(ReportableClaim::effective_ceiling)
.min()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn runtime_mode_caps_claim_ceiling() {
assert_eq!(RuntimeMode::Unknown.claim_ceiling(), ClaimCeiling::DevOnly);
assert_eq!(RuntimeMode::Dev.claim_ceiling(), ClaimCeiling::DevOnly);
assert_eq!(
RuntimeMode::RemoteUnsigned.claim_ceiling(),
ClaimCeiling::LocalUnsigned
);
assert_eq!(
RuntimeMode::LocalUnsigned.claim_ceiling(),
ClaimCeiling::LocalUnsigned
);
assert_eq!(
RuntimeMode::SignedLocalLedger.claim_ceiling(),
ClaimCeiling::SignedLocalLedger
);
assert_eq!(
RuntimeMode::ExternallyAnchored.claim_ceiling(),
ClaimCeiling::ExternallyAnchored
);
assert_eq!(
RuntimeMode::AuthorityGrade.claim_ceiling(),
ClaimCeiling::AuthorityGrade
);
}
#[test]
fn authority_class_caps_claim_ceiling() {
assert_eq!(
AuthorityClass::Untrusted.claim_ceiling(),
ClaimCeiling::DevOnly
);
assert_eq!(
AuthorityClass::Derived.claim_ceiling(),
ClaimCeiling::LocalUnsigned
);
assert_eq!(
AuthorityClass::Observed.claim_ceiling(),
ClaimCeiling::LocalUnsigned
);
assert_eq!(
AuthorityClass::Verified.claim_ceiling(),
ClaimCeiling::SignedLocalLedger
);
assert_eq!(
AuthorityClass::Operator.claim_ceiling(),
ClaimCeiling::AuthorityGrade
);
}
#[test]
fn reportable_claim_clamps_to_weakest_signal() {
let claim = ReportableClaim::new(
"phase 2 mechanics verified",
RuntimeMode::LocalUnsigned,
AuthorityClass::Operator,
ClaimProofState::FullChainVerified,
ClaimCeiling::AuthorityGrade,
);
assert_eq!(claim.effective_ceiling(), ClaimCeiling::LocalUnsigned);
assert!(!claim.downgrade_reasons().is_empty());
}
#[test]
fn verified_source_cannot_lift_dev_runtime() {
let claim = ReportableClaim::new(
"trusted run history",
RuntimeMode::Dev,
AuthorityClass::Verified,
ClaimProofState::FullChainVerified,
ClaimCeiling::SignedLocalLedger,
);
assert_eq!(claim.effective_ceiling(), ClaimCeiling::DevOnly);
}
#[test]
fn proof_state_caps_claim_ceiling() {
let partial = ReportableClaim::new(
"operator action observed",
RuntimeMode::AuthorityGrade,
AuthorityClass::Operator,
ClaimProofState::Partial,
ClaimCeiling::AuthorityGrade,
);
let broken = ReportableClaim::new(
"trusted run history",
RuntimeMode::AuthorityGrade,
AuthorityClass::Operator,
ClaimProofState::Broken,
ClaimCeiling::AuthorityGrade,
);
let unknown = ReportableClaim::new(
"export-ready evidence",
RuntimeMode::AuthorityGrade,
AuthorityClass::Operator,
ClaimProofState::Unknown,
ClaimCeiling::AuthorityGrade,
);
assert_eq!(partial.effective_ceiling(), ClaimCeiling::LocalUnsigned);
assert_eq!(broken.effective_ceiling(), ClaimCeiling::DevOnly);
assert_eq!(unknown.effective_ceiling(), ClaimCeiling::DevOnly);
}
#[test]
fn mixed_claims_use_weakest_effective_ceiling() {
let strong = ReportableClaim::new(
"anchored ledger tip",
RuntimeMode::ExternallyAnchored,
AuthorityClass::Operator,
ClaimProofState::FullChainVerified,
ClaimCeiling::ExternallyAnchored,
);
let weak = ReportableClaim::new(
"development ledger append",
RuntimeMode::LocalUnsigned,
AuthorityClass::Observed,
ClaimProofState::Partial,
ClaimCeiling::AuthorityGrade,
);
assert_eq!(
mix_reportable_claims_to_weakest([&strong, &weak]),
Some(ClaimCeiling::LocalUnsigned)
);
let mixed = strong.mix_to_weakest(&weak, "combined claim");
assert_eq!(mixed.effective_ceiling(), ClaimCeiling::LocalUnsigned);
}
#[test]
fn runtime_mode_wire_strings_are_stable() {
assert_eq!(
serde_json::to_value(RuntimeMode::Unknown).unwrap(),
serde_json::json!("unknown")
);
assert_eq!(
serde_json::to_value(RuntimeMode::Dev).unwrap(),
serde_json::json!("dev")
);
assert_eq!(
serde_json::to_value(RuntimeMode::RemoteUnsigned).unwrap(),
serde_json::json!("remote_unsigned")
);
assert_eq!(
serde_json::to_value(RuntimeMode::LocalUnsigned).unwrap(),
serde_json::json!("local_unsigned")
);
assert_eq!(
serde_json::to_value(RuntimeMode::SignedLocalLedger).unwrap(),
serde_json::json!("signed_local_ledger")
);
assert_eq!(
serde_json::to_value(RuntimeMode::ExternallyAnchored).unwrap(),
serde_json::json!("externally_anchored")
);
assert_eq!(
serde_json::to_value(RuntimeMode::AuthorityGrade).unwrap(),
serde_json::json!("authority_grade")
);
}
}