Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions contracts/predictify-hybrid/src/monitoring.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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<i128>,
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), &current_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"),
Expand Down
3 changes: 2 additions & 1 deletion contracts/predictify-hybrid/src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
pub mod oracle_differential_fuzz;
pub mod monitoring_mttr_tests;
57 changes: 57 additions & 0 deletions contracts/predictify-hybrid/src/tests/monitoring_mttr_tests.rs
Original file line number Diff line number Diff line change
@@ -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
}
1 change: 1 addition & 0 deletions docs/CAPABILITIES.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
Loading