From a28f46a9db6c30373168a365893e456b0775d1e9 Mon Sep 17 00:00:00 2001 From: Orioye Blessing Esther <210155349+ayaoba24@users.noreply.github.com> Date: Tue, 30 Jun 2026 10:32:37 +0100 Subject: [PATCH 1/3] Update CAPABILITIES.md with capabilities bitmap info Added a note about the capabilities bitmap in the Predictify Hybrid contract. --- docs/CAPABILITIES.md | 1 + 1 file changed, 1 insertion(+) 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. From 65b5ff1b9aa25af2f0f19ca24c68586f961c63d8 Mon Sep 17 00:00:00 2001 From: ayaoba24 Date: Thu, 2 Jul 2026 09:07:51 +0100 Subject: [PATCH 2/3] feat: add StateSnapshot diffing helper for ReportingManager --- contracts/predictify-hybrid/src/reporting.rs | 48 ++++++++++++++ contracts/predictify-hybrid/src/tests/mod.rs | 4 +- .../src/tests/snapshot_diffing_tests.rs | 64 +++++++++++++++++++ 3 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 contracts/predictify-hybrid/src/tests/snapshot_diffing_tests.rs diff --git a/contracts/predictify-hybrid/src/reporting.rs b/contracts/predictify-hybrid/src/reporting.rs index 388d2841..c6243d14 100644 --- a/contracts/predictify-hybrid/src/reporting.rs +++ b/contracts/predictify-hybrid/src/reporting.rs @@ -58,6 +58,54 @@ pub struct EventSnapshot { pub end_time: u64, } +// --------------------------------------------------------------------------- +// Snapshot Diffing +// --------------------------------------------------------------------------- + +/// A snapshot containing multiple event states for offline comparison. +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct StateSnapshot { + pub events: Map, +} + +/// A symmetric diff of two `StateSnapshot`s. +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct SnapshotDiff { + pub changed_markets: Vec, +} + +impl StateSnapshot { + /// Computes a symmetric difference between two `StateSnapshot`s. + /// Returns a deterministic, ordered list of market IDs whose snapshots differ + /// (either because they are in one but not the other, or their values differ). + pub fn diff(env: &Env, a: &Self, b: &Self) -> SnapshotDiff { + let mut unique_keys: Map = Map::new(env); + + for key in a.events.keys().into_iter() { + unique_keys.set(key.clone(), ()); + } + for key in b.events.keys().into_iter() { + unique_keys.set(key.clone(), ()); + } + + let mut changed = Vec::new(env); + for key in unique_keys.keys().into_iter() { + let val_a = a.events.get(key.clone()); + let val_b = b.events.get(key.clone()); + if val_a != val_b { + changed.push_back(key); + } + } + + SnapshotDiff { + changed_markets: changed, + } + } +} + + // --------------------------------------------------------------------------- // SnapshotEnvelope // --------------------------------------------------------------------------- diff --git a/contracts/predictify-hybrid/src/tests/mod.rs b/contracts/predictify-hybrid/src/tests/mod.rs index b5051898..07c561b9 100644 --- a/contracts/predictify-hybrid/src/tests/mod.rs +++ b/contracts/predictify-hybrid/src/tests/mod.rs @@ -27,4 +27,6 @@ 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; +pub mod snapshot_diffing_tests; \ No newline at end of file diff --git a/contracts/predictify-hybrid/src/tests/snapshot_diffing_tests.rs b/contracts/predictify-hybrid/src/tests/snapshot_diffing_tests.rs new file mode 100644 index 00000000..60a346ca --- /dev/null +++ b/contracts/predictify-hybrid/src/tests/snapshot_diffing_tests.rs @@ -0,0 +1,64 @@ +#![cfg(test)] + +use crate::reporting::{EventSnapshot, SnapshotDiff, StateSnapshot}; +use crate::types::MarketState; +use soroban_sdk::{Env, Map, String, Symbol, Vec}; + +fn create_event_snapshot(env: &Env, id: &str, pool: i128) -> EventSnapshot { + EventSnapshot { + id: Symbol::new(env, id), + question: String::from_str(env, "Q"), + outcomes: Vec::new(env), + state: MarketState::Active, + total_pool: pool, + outcome_pools: Map::new(env), + participant_count: 0, + end_time: 0, + } +} + +#[test] +fn test_state_snapshot_diff() { + let env = Env::default(); + + let mut events_a = Map::new(&env); + events_a.set( + Symbol::new(&env, "market1"), + create_event_snapshot(&env, "market1", 100), + ); + events_a.set( + Symbol::new(&env, "market2"), + create_event_snapshot(&env, "market2", 200), + ); + + let mut events_b = Map::new(&env); + events_b.set( + Symbol::new(&env, "market1"), + create_event_snapshot(&env, "market1", 100), // unchanged + ); + events_b.set( + Symbol::new(&env, "market2"), + create_event_snapshot(&env, "market2", 300), // changed + ); + events_b.set( + Symbol::new(&env, "market3"), + create_event_snapshot(&env, "market3", 500), // added + ); + + let snapshot_a = StateSnapshot { events: events_a }; + let snapshot_b = StateSnapshot { events: events_b }; + + // diff(A, B) + let diff_ab = StateSnapshot::diff(&env, &snapshot_a, &snapshot_b); + assert_eq!(diff_ab.changed_markets.len(), 2); + assert!(diff_ab.changed_markets.contains(&Symbol::new(&env, "market2"))); + assert!(diff_ab.changed_markets.contains(&Symbol::new(&env, "market3"))); + + // symmetric property: diff(B, A) == diff(A, B) + let diff_ba = StateSnapshot::diff(&env, &snapshot_b, &snapshot_a); + assert_eq!(diff_ab.changed_markets, diff_ba.changed_markets); + + // identity property: diff(A, A) is empty + let diff_aa = StateSnapshot::diff(&env, &snapshot_a, &snapshot_a); + assert_eq!(diff_aa.changed_markets.len(), 0); +} From de20b299ab98ec715efdf57ee82a402a92762059 Mon Sep 17 00:00:00 2001 From: ayaoba24 Date: Thu, 2 Jul 2026 09:09:50 +0100 Subject: [PATCH 3/3] fix: satisfy typed snapshot diff and symmetric inversion requirements --- contracts/predictify-hybrid/src/reporting.rs | 63 ++++++++++++++----- .../src/tests/snapshot_diffing_tests.rs | 47 +++++++++++--- 2 files changed, 87 insertions(+), 23 deletions(-) diff --git a/contracts/predictify-hybrid/src/reporting.rs b/contracts/predictify-hybrid/src/reporting.rs index c6243d14..feb81007 100644 --- a/contracts/predictify-hybrid/src/reporting.rs +++ b/contracts/predictify-hybrid/src/reporting.rs @@ -66,6 +66,7 @@ pub struct EventSnapshot { #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] pub struct StateSnapshot { + pub stats: PlatformStats, pub events: Map, } @@ -73,34 +74,68 @@ pub struct StateSnapshot { #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] pub struct SnapshotDiff { - pub changed_markets: Vec, + pub added: Vec, + pub removed: Vec, + pub changed: Vec, + pub fee_delta: i128, + pub total_pool_delta: i128, +} + +impl SnapshotDiff { + /// Inverts the diff such that `diff(a, b).invert(env) == diff(b, a)` + pub fn invert(&self, env: &Env) -> Self { + Self { + added: self.removed.clone(), + removed: self.added.clone(), + changed: self.changed.clone(), + fee_delta: -self.fee_delta, + total_pool_delta: -self.total_pool_delta, + } + } } impl StateSnapshot { - /// Computes a symmetric difference between two `StateSnapshot`s. - /// Returns a deterministic, ordered list of market IDs whose snapshots differ - /// (either because they are in one but not the other, or their values differ). - pub fn diff(env: &Env, a: &Self, b: &Self) -> SnapshotDiff { - let mut unique_keys: Map = Map::new(env); + /// Computes a typed difference between `prev` and `next` `StateSnapshot`s. + /// Returns a deterministic, ordered list of market IDs that were added, removed, or changed. + /// Also includes the fee and total-pool deltas. + pub fn diff(env: &Env, prev: &Self, next: &Self) -> SnapshotDiff { + let mut added = Vec::new(env); + let mut removed = Vec::new(env); + let mut changed = Vec::new(env); - for key in a.events.keys().into_iter() { + let mut unique_keys: Map = Map::new(env); + for key in prev.events.keys().into_iter() { unique_keys.set(key.clone(), ()); } - for key in b.events.keys().into_iter() { + for key in next.events.keys().into_iter() { unique_keys.set(key.clone(), ()); } - let mut changed = Vec::new(env); for key in unique_keys.keys().into_iter() { - let val_a = a.events.get(key.clone()); - let val_b = b.events.get(key.clone()); - if val_a != val_b { - changed.push_back(key); + let val_prev = prev.events.get(key.clone()); + let val_next = next.events.get(key.clone()); + + match (val_prev, val_next) { + (Some(_), None) => removed.push_back(key), + (None, Some(_)) => added.push_back(key), + (Some(p), Some(n)) => { + if p != n { + changed.push_back(key); + } + } + (None, None) => {} } } + let fee_delta = next.stats.total_fees_collected.saturating_sub(prev.stats.total_fees_collected); + let pool_delta = next.stats.total_pool_all_events.saturating_sub(prev.stats.total_pool_all_events); + SnapshotDiff { - changed_markets: changed, + added, + removed, + changed, + fee_delta, + total_pool_delta: pool_delta, } } } diff --git a/contracts/predictify-hybrid/src/tests/snapshot_diffing_tests.rs b/contracts/predictify-hybrid/src/tests/snapshot_diffing_tests.rs index 60a346ca..28cf3280 100644 --- a/contracts/predictify-hybrid/src/tests/snapshot_diffing_tests.rs +++ b/contracts/predictify-hybrid/src/tests/snapshot_diffing_tests.rs @@ -1,6 +1,6 @@ #![cfg(test)] -use crate::reporting::{EventSnapshot, SnapshotDiff, StateSnapshot}; +use crate::reporting::{EventSnapshot, PlatformStats, SnapshotDiff, StateSnapshot}; use crate::types::MarketState; use soroban_sdk::{Env, Map, String, Symbol, Vec}; @@ -17,6 +17,16 @@ fn create_event_snapshot(env: &Env, id: &str, pool: i128) -> EventSnapshot { } } +fn create_platform_stats(env: &Env, fees: i128, pool: i128) -> PlatformStats { + PlatformStats { + total_active_events: 0, + total_resolved_events: 0, + total_pool_all_events: pool, + total_fees_collected: fees, + version: String::from_str(env, "1.0.0"), + } +} + #[test] fn test_state_snapshot_diff() { let env = Env::default(); @@ -30,6 +40,7 @@ fn test_state_snapshot_diff() { Symbol::new(&env, "market2"), create_event_snapshot(&env, "market2", 200), ); + let stats_a = create_platform_stats(&env, 50, 300); let mut events_b = Map::new(&env); events_b.set( @@ -44,21 +55,39 @@ fn test_state_snapshot_diff() { Symbol::new(&env, "market3"), create_event_snapshot(&env, "market3", 500), // added ); + let stats_b = create_platform_stats(&env, 70, 900); - let snapshot_a = StateSnapshot { events: events_a }; - let snapshot_b = StateSnapshot { events: events_b }; + let snapshot_a = StateSnapshot { stats: stats_a.clone(), events: events_a }; + let snapshot_b = StateSnapshot { stats: stats_b.clone(), events: events_b }; // diff(A, B) let diff_ab = StateSnapshot::diff(&env, &snapshot_a, &snapshot_b); - assert_eq!(diff_ab.changed_markets.len(), 2); - assert!(diff_ab.changed_markets.contains(&Symbol::new(&env, "market2"))); - assert!(diff_ab.changed_markets.contains(&Symbol::new(&env, "market3"))); + assert_eq!(diff_ab.added.len(), 1); + assert!(diff_ab.added.contains(&Symbol::new(&env, "market3"))); + assert_eq!(diff_ab.removed.len(), 0); + assert_eq!(diff_ab.changed.len(), 1); + assert!(diff_ab.changed.contains(&Symbol::new(&env, "market2"))); + assert_eq!(diff_ab.fee_delta, 20); // 70 - 50 + assert_eq!(diff_ab.total_pool_delta, 600); // 900 - 300 - // symmetric property: diff(B, A) == diff(A, B) + // diff(B, A) let diff_ba = StateSnapshot::diff(&env, &snapshot_b, &snapshot_a); - assert_eq!(diff_ab.changed_markets, diff_ba.changed_markets); + assert_eq!(diff_ba.added.len(), 0); + assert_eq!(diff_ba.removed.len(), 1); + assert!(diff_ba.removed.contains(&Symbol::new(&env, "market3"))); + assert_eq!(diff_ba.changed.len(), 1); + assert!(diff_ba.changed.contains(&Symbol::new(&env, "market2"))); + assert_eq!(diff_ba.fee_delta, -20); // 50 - 70 + assert_eq!(diff_ba.total_pool_delta, -600); // 300 - 900 + + // symmetric property: diff(a, b).invert() == diff(b, a) + assert_eq!(diff_ab.invert(&env), diff_ba); // identity property: diff(A, A) is empty let diff_aa = StateSnapshot::diff(&env, &snapshot_a, &snapshot_a); - assert_eq!(diff_aa.changed_markets.len(), 0); + assert_eq!(diff_aa.added.len(), 0); + assert_eq!(diff_aa.removed.len(), 0); + assert_eq!(diff_aa.changed.len(), 0); + assert_eq!(diff_aa.fee_delta, 0); + assert_eq!(diff_aa.total_pool_delta, 0); }