use serde::{Deserialize, Serialize};
use crate::consciousness_profile::{ConsciousnessCredential, ConsciousnessTier};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FreshnessAttestation {
pub attester_did: String,
pub timestamp: u64,
pub tier_at_attestation: ConsciousnessTier,
#[serde(default)]
pub signature: Vec<u8>,
}
impl FreshnessAttestation {
pub fn canonical_bytes(&self) -> Vec<u8> {
let mut buf = Vec::with_capacity(64);
buf.extend_from_slice(self.attester_did.as_bytes());
buf.extend_from_slice(&self.timestamp.to_le_bytes());
buf.push(self.tier_at_attestation as u8);
buf
}
pub fn sign_blake3(&mut self, key: &[u8; 32]) {
let data = self.canonical_bytes();
let mac = blake3::keyed_hash(key, &data);
self.signature = mac.as_bytes().to_vec();
}
pub fn verify_blake3(&self, key: &[u8; 32]) -> bool {
let data = self.canonical_bytes();
let mac = blake3::keyed_hash(key, &data);
self.signature == mac.as_bytes().as_slice()
}
}
const HOURS_24: u64 = 24 * 3600 * 1_000_000;
const HOURS_72: u64 = 72 * 3600 * 1_000_000;
const HOURS_168: u64 = 168 * 3600 * 1_000_000;
const CLOCK_SKEW_TOLERANCE_US: u64 = 900 * 1_000_000;
const MAX_GRACE_HOURS: u64 = 720;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OfflineCredential {
pub credential: ConsciousnessCredential,
pub last_online_verification: u64,
pub degradation_enabled: bool,
pub custom_grace_period: Option<u64>,
#[serde(default)]
pub attestation: Option<FreshnessAttestation>,
}
impl OfflineCredential {
pub fn new(credential: ConsciousnessCredential) -> Self {
Self {
last_online_verification: credential.issued_at,
degradation_enabled: true,
custom_grace_period: None,
attestation: None,
credential,
}
}
pub fn with_grace_hours(credential: ConsciousnessCredential, hours: u64) -> Self {
let clamped_hours = hours.min(MAX_GRACE_HOURS);
Self {
last_online_verification: credential.issued_at,
degradation_enabled: true,
custom_grace_period: Some(clamped_hours * 3600 * 1_000_000),
attestation: None,
credential,
}
}
pub fn effective_tier(&self, now_us: u64) -> ConsciousnessTier {
if !self.degradation_enabled {
return self.credential.tier;
}
let reference_time = match &self.attestation {
Some(att) if !att.signature.is_empty() => {
if att.timestamp < self.credential.issued_at {
return self.credential.tier.degrade(2);
}
att.timestamp
}
_ => self.last_online_verification,
};
if reference_time > now_us {
let gap = reference_time - now_us;
if gap > CLOCK_SKEW_TOLERANCE_US {
return self.credential.tier.degrade(2);
}
return self.credential.tier;
}
let elapsed = now_us - reference_time;
let grace = self.custom_grace_period.unwrap_or(HOURS_24);
if elapsed <= grace {
self.credential.tier
} else if elapsed <= HOURS_72 {
self.credential.tier.degrade(1)
} else if elapsed <= HOURS_168 {
self.credential.tier.degrade(2)
} else {
ConsciousnessTier::Observer
}
}
pub fn is_usable(&self, now_us: u64) -> bool {
self.effective_tier(now_us) > ConsciousnessTier::Observer
}
pub fn record_online_verification(&mut self, now_us: u64) {
self.last_online_verification = now_us;
}
pub fn hours_offline(&self, now_us: u64) -> f64 {
let elapsed_us = now_us.saturating_sub(self.last_online_verification);
elapsed_us as f64 / (3600.0 * 1_000_000.0)
}
pub fn freshness_timestamp(&self) -> u64 {
self.last_online_verification
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::consciousness_profile::ConsciousnessProfile;
fn make_credential(tier: ConsciousnessTier, issued_at: u64) -> ConsciousnessCredential {
ConsciousnessCredential {
did: "did:mycelix:test".to_string(),
profile: ConsciousnessProfile {
identity: 0.8,
reputation: 0.7,
community: 0.9,
engagement: 0.6,
},
tier,
issued_at,
expires_at: issued_at + HOURS_168, issuer: "did:mycelix:bridge".to_string(),
trajectory_commitment: None,
extensions: std::collections::HashMap::new(),
}
}
#[test]
fn within_grace_full_tier() {
let cred = make_credential(ConsciousnessTier::Guardian, 0);
let offline = OfflineCredential::new(cred);
let tier = offline.effective_tier(12 * 3600 * 1_000_000);
assert_eq!(tier, ConsciousnessTier::Guardian);
}
#[test]
fn after_24h_drops_one() {
let cred = make_credential(ConsciousnessTier::Guardian, 0);
let offline = OfflineCredential::new(cred);
let tier = offline.effective_tier(48 * 3600 * 1_000_000);
assert_eq!(tier, ConsciousnessTier::Steward);
}
#[test]
fn after_72h_drops_two() {
let cred = make_credential(ConsciousnessTier::Guardian, 0);
let offline = OfflineCredential::new(cred);
let tier = offline.effective_tier(120 * 3600 * 1_000_000);
assert_eq!(tier, ConsciousnessTier::Citizen);
}
#[test]
fn after_7_days_observer() {
let cred = make_credential(ConsciousnessTier::Guardian, 0);
let offline = OfflineCredential::new(cred);
let tier = offline.effective_tier(8 * 24 * 3600 * 1_000_000);
assert_eq!(tier, ConsciousnessTier::Observer);
}
#[test]
fn online_verification_resets_clock() {
let cred = make_credential(ConsciousnessTier::Guardian, 0);
let mut offline = OfflineCredential::new(cred);
assert_eq!(
offline.effective_tier(48 * 3600 * 1_000_000),
ConsciousnessTier::Steward
);
offline.record_online_verification(47 * 3600 * 1_000_000);
assert_eq!(
offline.effective_tier(48 * 3600 * 1_000_000),
ConsciousnessTier::Guardian
);
}
#[test]
fn degradation_disabled() {
let cred = make_credential(ConsciousnessTier::Guardian, 0);
let mut offline = OfflineCredential::new(cred);
offline.degradation_enabled = false;
let tier = offline.effective_tier(8 * 24 * 3600 * 1_000_000);
assert_eq!(tier, ConsciousnessTier::Guardian);
}
#[test]
fn participant_degrades_to_observer() {
let cred = make_credential(ConsciousnessTier::Participant, 0);
let offline = OfflineCredential::new(cred);
let tier = offline.effective_tier(48 * 3600 * 1_000_000);
assert_eq!(tier, ConsciousnessTier::Observer);
}
#[test]
fn hours_offline_calculation() {
let cred = make_credential(ConsciousnessTier::Guardian, 0);
let offline = OfflineCredential::new(cred);
let hours = offline.hours_offline(48 * 3600 * 1_000_000);
assert!((hours - 48.0).abs() < 0.01);
}
#[test]
fn tier_degrade_levels() {
assert_eq!(
ConsciousnessTier::Guardian.degrade(0),
ConsciousnessTier::Guardian
);
assert_eq!(
ConsciousnessTier::Guardian.degrade(1),
ConsciousnessTier::Steward
);
assert_eq!(
ConsciousnessTier::Guardian.degrade(2),
ConsciousnessTier::Citizen
);
assert_eq!(
ConsciousnessTier::Guardian.degrade(3),
ConsciousnessTier::Participant
);
assert_eq!(
ConsciousnessTier::Guardian.degrade(4),
ConsciousnessTier::Observer
);
assert_eq!(
ConsciousnessTier::Guardian.degrade(100),
ConsciousnessTier::Observer
);
}
#[test]
fn custom_grace_period() {
let cred = make_credential(ConsciousnessTier::Guardian, 0);
let offline = OfflineCredential::with_grace_hours(cred, 48);
assert_eq!(
offline.effective_tier(36 * 3600 * 1_000_000),
ConsciousnessTier::Guardian
);
assert_eq!(
offline.effective_tier(60 * 3600 * 1_000_000),
ConsciousnessTier::Steward
);
}
#[test]
fn attestation_overrides_self_reported() {
let cred = make_credential(ConsciousnessTier::Guardian, 0);
let mut offline = OfflineCredential::new(cred);
assert_eq!(
offline.effective_tier(48 * 3600 * 1_000_000),
ConsciousnessTier::Steward
);
offline.attestation = Some(FreshnessAttestation {
attester_did: "did:mycelix:peer".into(),
timestamp: 47 * 3600 * 1_000_000,
tier_at_attestation: ConsciousnessTier::Guardian,
signature: vec![1], });
assert_eq!(
offline.effective_tier(48 * 3600 * 1_000_000),
ConsciousnessTier::Guardian
);
}
#[test]
fn unsigned_attestation_treated_as_self_reported() {
let cred = make_credential(ConsciousnessTier::Guardian, 0);
let mut offline = OfflineCredential::new(cred);
offline.attestation = Some(FreshnessAttestation {
attester_did: "did:mycelix:peer".into(),
timestamp: 47 * 3600 * 1_000_000,
tier_at_attestation: ConsciousnessTier::Guardian,
signature: vec![], });
assert_eq!(
offline.effective_tier(48 * 3600 * 1_000_000),
ConsciousnessTier::Steward
);
}
#[test]
fn freshness_attestation_blake3_verification() {
let key = [99u8; 32];
let mut att = FreshnessAttestation {
attester_did: "did:mycelix:bridge".into(),
timestamp: 1_000_000,
tier_at_attestation: ConsciousnessTier::Guardian,
signature: vec![],
};
att.sign_blake3(&key);
assert!(att.verify_blake3(&key));
}
#[test]
fn freshness_attestation_tamper_rejection() {
let key = [99u8; 32];
let mut att = FreshnessAttestation {
attester_did: "did:mycelix:bridge".into(),
timestamp: 1_000_000,
tier_at_attestation: ConsciousnessTier::Guardian,
signature: vec![],
};
att.sign_blake3(&key);
att.timestamp = 2_000_000;
assert!(!att.verify_blake3(&key));
}
#[test]
fn is_usable_above_observer() {
let cred = make_credential(ConsciousnessTier::Participant, 0);
let offline = OfflineCredential::new(cred);
assert!(offline.is_usable(12 * 3600 * 1_000_000));
assert!(!offline.is_usable(48 * 3600 * 1_000_000));
}
#[test]
fn clock_skew_within_tolerance_preserves_tier() {
let base = 100 * 3600 * 1_000_000_u64;
let now = base - 10 * 60 * 1_000_000; let cred = make_credential(ConsciousnessTier::Citizen, base);
let offline = OfflineCredential::new(cred);
assert_eq!(offline.effective_tier(now), ConsciousnessTier::Citizen);
}
#[test]
fn clock_skew_at_15min_boundary_preserves_tier() {
let base = 100 * 3600 * 1_000_000_u64;
let now = base - 900 * 1_000_000; let cred = make_credential(ConsciousnessTier::Steward, base);
let offline = OfflineCredential::new(cred);
assert_eq!(offline.effective_tier(now), ConsciousnessTier::Steward);
}
#[test]
fn clock_skew_beyond_tolerance_degrades() {
let base = 100 * 3600 * 1_000_000_u64;
let now = base - 30 * 60 * 1_000_000; let cred = make_credential(ConsciousnessTier::Guardian, base);
let offline = OfflineCredential::new(cred);
assert_eq!(offline.effective_tier(now), ConsciousnessTier::Citizen);
}
#[test]
fn clock_skew_tiny_drift_preserves_tier() {
let base = 50 * 3600 * 1_000_000_u64;
let now = base - 1;
let cred = make_credential(ConsciousnessTier::Citizen, base);
let offline = OfflineCredential::new(cred);
assert_eq!(offline.effective_tier(now), ConsciousnessTier::Citizen);
}
}