diff --git a/contracts/predictify-hybrid/src/monitoring.rs b/contracts/predictify-hybrid/src/monitoring.rs index 68477118..0524520d 100644 --- a/contracts/predictify-hybrid/src/monitoring.rs +++ b/contracts/predictify-hybrid/src/monitoring.rs @@ -3,6 +3,7 @@ use alloc::format; use soroban_sdk::{contracttype, vec, Address, Env, Map, String, Symbol, Vec}; +use crate::admin::AdminAccessControl; use crate::err::Error; use crate::events::EventEmitter; use crate::types::{Market, MarketState, OracleConfig, OracleProvider}; @@ -204,12 +205,133 @@ pub struct TransitionHookEvent { pub timestamp: u64, } +/// Rolling Window Ring Buffer +#[derive(Clone, Debug)] +#[contracttype] +pub struct RollingWindow { + pub capacity: u32, + pub entries: Vec, + pub head: u32, + pub count: u32, +} + +impl RollingWindow { + pub fn new(env: &Env, capacity: u32) -> Self { + Self { + capacity, + entries: Vec::new(env), + head: 0, + count: 0, + } + } + + pub fn push(&mut self, env: &Env, value: i128) { + if self.capacity == 0 { + return; + } + if self.entries.len() < self.capacity { + self.entries.push_back(value); + self.count += 1; + } else { + self.entries.set(self.head, value); + } + self.head = (self.head + 1) % self.capacity; + } + + pub fn average(&self) -> i128 { + if self.count == 0 { + return 0; + } + let mut sum: i128 = 0; + for i in 0..self.entries.len() { + sum = sum.saturating_add(self.entries.get(i).unwrap_or(0)); + } + sum / (self.count as i128) + } +} + // ===== CONTRACT MONITOR STRUCT ===== /// Main contract monitoring system pub struct ContractMonitor; impl ContractMonitor { + const MTTR_WINDOW_KEY: &'static str = "MTTR_WINDOW"; + const MTBF_WINDOW_KEY: &'static str = "MTBF_WINDOW"; + const LAST_INCIDENT_TIME_KEY: &'static str = "LAST_INCIDENT_TIME"; + const WINDOW_CAPACITY_KEY: &'static str = "MON_WINDOW_CAPACITY"; + + pub fn set_window_capacity(env: &Env, admin: &Address, capacity: u32) -> Result<(), Error> { + AdminAccessControl::require_admin_auth(env, admin)?; + if capacity == 0 || capacity > 10000 { + return Err(Error::InvalidInput); + } + env.storage().persistent().set(&Symbol::new(env, Self::WINDOW_CAPACITY_KEY), &capacity); + Ok(()) + } + + pub fn get_window_capacity(env: &Env) -> u32 { + env.storage() + .persistent() + .get(&Symbol::new(env, Self::WINDOW_CAPACITY_KEY)) + .unwrap_or(10) + } + + pub fn get_mttr_window(env: &Env) -> RollingWindow { + env.storage() + .persistent() + .get(&Symbol::new(env, Self::MTTR_WINDOW_KEY)) + .unwrap_or_else(|| RollingWindow::new(env, Self::get_window_capacity(env))) + } + + pub fn get_mtbf_window(env: &Env) -> RollingWindow { + env.storage() + .persistent() + .get(&Symbol::new(env, Self::MTBF_WINDOW_KEY)) + .unwrap_or_else(|| RollingWindow::new(env, Self::get_window_capacity(env))) + } + + pub fn get_mttr(env: &Env) -> i128 { + Self::get_mttr_window(env).average() + } + + pub fn get_mtbf(env: &Env) -> i128 { + Self::get_mtbf_window(env).average() + } + + pub fn record_incident(env: &Env, current_time: u64) -> Result<(), Error> { + let last_time: u64 = env + .storage() + .persistent() + .get(&Symbol::new(env, Self::LAST_INCIDENT_TIME_KEY)) + .unwrap_or(0); + + if last_time > 0 && current_time >= last_time { + let mtbf_seconds = (current_time - last_time) as i128; + let mut window = Self::get_mtbf_window(env); + window.capacity = Self::get_window_capacity(env); + window.push(env, mtbf_seconds); + env.storage() + .persistent() + .set(&Symbol::new(env, Self::MTBF_WINDOW_KEY), &window); + } + + env.storage() + .persistent() + .set(&Symbol::new(env, Self::LAST_INCIDENT_TIME_KEY), ¤t_time); + Ok(()) + } + + pub fn record_recovery(env: &Env, recovery_time_seconds: i128) -> Result<(), Error> { + let mut window = Self::get_mttr_window(env); + window.capacity = Self::get_window_capacity(env); + window.push(env, recovery_time_seconds); + env.storage() + .persistent() + .set(&Symbol::new(env, Self::MTTR_WINDOW_KEY), &window); + Ok(()) + } + fn market_state_label(env: &Env, state: &MarketState) -> String { match state { MarketState::Active => String::from_str(env, "Active"), diff --git a/contracts/predictify-hybrid/src/tests/mod.rs b/contracts/predictify-hybrid/src/tests/mod.rs index b5051898..9ee6753d 100644 --- a/contracts/predictify-hybrid/src/tests/mod.rs +++ b/contracts/predictify-hybrid/src/tests/mod.rs @@ -27,4 +27,5 @@ pub mod dispute_stake_tests; pub mod fee_config_commit_reveal_tests; pub mod reflector_twap_cache_tests; pub mod dispute_anti_grief_tests; -pub mod oracle_differential_fuzz; \ No newline at end of file +pub mod oracle_differential_fuzz; +pub mod monitoring_mttr_tests; \ No newline at end of file diff --git a/contracts/predictify-hybrid/src/tests/monitoring_mttr_tests.rs b/contracts/predictify-hybrid/src/tests/monitoring_mttr_tests.rs new file mode 100644 index 00000000..cb9a8f5b --- /dev/null +++ b/contracts/predictify-hybrid/src/tests/monitoring_mttr_tests.rs @@ -0,0 +1,57 @@ +#![cfg(test)] + +use crate::monitoring::{ContractMonitor, RollingWindow}; +use soroban_sdk::{testutils::Address as _, Address, Env}; +use crate::admin::AdminInitializer; + +fn setup() -> (Env, Address) { + let env = Env::default(); + let admin = Address::generate(&env); + AdminInitializer::initialize(&env, &admin).unwrap(); + (env, admin) +} + +#[test] +fn test_rolling_window_brute_force() { + let env = Env::default(); + let mut window = RollingWindow::new(&env, 5); + + // push 1, 2, 3 + window.push(&env, 10); + window.push(&env, 20); + window.push(&env, 30); + assert_eq!(window.average(), 20); + + // push more to trigger replace + window.push(&env, 40); + window.push(&env, 50); + window.push(&env, 60); // replaces 10 + + // entries: [60, 20, 30, 40, 50] + // sum: 200, average: 40 + assert_eq!(window.average(), 40); +} + +#[test] +fn test_mttr_mtbf_queries() { + let (env, admin) = setup(); + + ContractMonitor::set_window_capacity(&env, &admin, 10).unwrap(); + assert_eq!(ContractMonitor::get_window_capacity(&env), 10); + + // Record incident 1 + ContractMonitor::record_incident(&env, 1000).unwrap(); + // Record recovery 1 + ContractMonitor::record_recovery(&env, 50).unwrap(); + + // Record incident 2 + ContractMonitor::record_incident(&env, 2000).unwrap(); // MTBF = 1000 + // Record recovery 2 + ContractMonitor::record_recovery(&env, 150).unwrap(); + + let mttr = ContractMonitor::get_mttr(&env); + let mtbf = ContractMonitor::get_mtbf(&env); + + assert_eq!(mttr, 100); // (50 + 150) / 2 + assert_eq!(mtbf, 1000); // 1000 +} diff --git a/docs/CAPABILITIES.md b/docs/CAPABILITIES.md index e7c2f650..ab160919 100644 --- a/docs/CAPABILITIES.md +++ b/docs/CAPABILITIES.md @@ -1,5 +1,6 @@ # Contract Capabilities + The Predictify Hybrid contract exposes a **u64 capabilities bitmap** that allows clients to discover which features are available without inspecting the Wasm binary or relying on version-number heuristics.