diff --git a/contracts/predictify-hybrid/src/markets.rs b/contracts/predictify-hybrid/src/markets.rs index 65c356bb..5d3f85c5 100644 --- a/contracts/predictify-hybrid/src/markets.rs +++ b/contracts/predictify-hybrid/src/markets.rs @@ -4,6 +4,7 @@ use soroban_sdk::{contracttype, token, vec, Address, Env, Map, String, Symbol, V // use crate::config; // Unused import use crate::errors::Error; +use crate::storage::{DataKey, MARKET_CACHE_TTL_LEDGERS}; use crate::types::*; // Oracle imports removed - not currently used @@ -137,6 +138,9 @@ impl MarketCreator { env.storage().persistent().set(&market_id, &market); env.storage().persistent().extend_ttl(&market_id, min_rent_budget, min_rent_budget); + // CACHE INVALIDATION: ensure cache is empty for new market + MarketReadCache::new(env).invalidate(&market_id); + Ok(market_id) } @@ -699,6 +703,69 @@ impl MarketValidator { } } +// ===== MARKET READ CACHE ===== + +/// In-instance read cache for Market structs, keyed by market_id. +/// +/// Backed by env.storage().instance() which provides fast access +/// without the XDR deserialization cost of persistent storage reads. +/// +/// # TTL Behaviour +/// Instance storage TTL is shared across all keys in the instance. +/// Every cache write (population or invalidation) bumps the instance TTL +/// by MARKET_CACHE_TTL_LEDGERS. Cache misses do not bump TTL. +/// +/// # Invalidation +/// Cache entries are removed (not overwritten) on every write path +/// through MarketStateManager. +/// +/// # Security +/// The cache never serves as the source of truth for writes - all mutations +/// read from and write to persistent storage directly. The cache is +/// exclusively a read optimization. +pub struct MarketReadCache<'a> { + env: &'a Env, +} + +impl<'a> MarketReadCache<'a> { + pub fn new(env: &'a Env) -> Self { + Self { env } + } + + /// Returns the cached Market for market_id if present, or None on miss. + /// Bumps instance TTL on cache hit. + /// Never panics - returns None on any storage error. + pub fn get(&self, market_id: &Symbol) -> Option { + let key = DataKey::MarketCache(market_id.clone()); + let result: Option = self.env.storage().instance().get(&key); + if result.is_some() { + // HIT: bump TTL to keep the cache entry alive + self.env.storage().instance().bump(MARKET_CACHE_TTL_LEDGERS); + } + result + // NOTE: no unwrap() - get() returns Option, None on miss or type mismatch + } + + /// Populates the cache for market_id with the given Market. + /// Always bumps instance TTL after writing. + pub fn set(&self, market_id: Symbol, market: &Market) { + let key = DataKey::MarketCache(market_id); + self.env.storage().instance().set(&key, market); + self.env.storage().instance().bump(MARKET_CACHE_TTL_LEDGERS); + // CACHE: populate after persistent write - never before + } + + /// Removes the cache entry for market_id. + /// Called on every write path through MarketStateManager. + /// Does not bump TTL - invalidation should not extend cache lifetime. + pub fn invalidate(&self, market_id: &Symbol) { + let key = DataKey::MarketCache(market_id.clone()); + self.env.storage().instance().remove(&key); + // CACHE INVALIDATION: remove entirely - do not overwrite with sentinel + // A subsequent get() will miss and fall through to persistent storage + } +} + // ===== MARKET STATE MANAGEMENT ===== /// Market state management utilities for persistent storage operations. @@ -752,11 +819,38 @@ impl MarketStateManager { /// Err(e) => println!("Market not found: {:?}", e), /// } /// ``` + /// Retrieves a market by market_id. + /// + /// Read path (in order): + /// 1. Check MarketReadCache (instance storage) - O(1) if hot + /// 2. On miss: read from persistent storage and populate cache + /// 3. Return Error::MarketNotFound if not in persistent storage + /// + /// # Performance + /// Cache hits avoid full XDR deserialization of the Market struct. + /// Cache misses have the same cost as the original implementation plus + /// one instance storage write to populate the cache. pub fn get_market(_env: &Env, market_id: &Symbol) -> Result { - _env.storage() - .persistent() - .get(market_id) - .ok_or(Error::MarketNotFound) + let cache = MarketReadCache::new(_env); + + // CACHE: check instance cache first + if let Some(cached) = cache.get(market_id) { + // HIT: return without touching persistent storage + return Ok(cached); + } + + // MISS: read from persistent storage + let market: Option = _env.storage().persistent().get(market_id); + + match market { + Some(m) => { + // Populate cache for subsequent reads + cache.set(market_id.clone(), &m); + Ok(m) + } + None => Err(Error::MarketNotFound), + // NOTE: no unwrap() - explicit match on Option + } } /// Updates market data in persistent storage. @@ -789,6 +883,8 @@ impl MarketStateManager { /// ``` pub fn update_market(_env: &Env, market_id: &Symbol, market: &Market) { _env.storage().persistent().set(market_id, market); + // CACHE INVALIDATION: remove cache entry after persistent write + MarketReadCache::new(_env).invalidate(market_id); } /// Updates the market question/description. @@ -862,6 +958,8 @@ impl MarketStateManager { Self::update_market(env, market_id, &market); } env.storage().persistent().remove(market_id); + // CACHE INVALIDATION: remove cache entry after persistent removal + MarketReadCache::new(env).invalidate(market_id); } /// Adds a user's vote to a market with the specified stake amount. diff --git a/contracts/predictify-hybrid/src/storage.rs b/contracts/predictify-hybrid/src/storage.rs index e70f3c2d..6979919b 100644 --- a/contracts/predictify-hybrid/src/storage.rs +++ b/contracts/predictify-hybrid/src/storage.rs @@ -12,6 +12,12 @@ const MARKET_TTL_LEDGERS: u32 = 365 * LEDGERS_PER_DAY; const EVENT_TTL_LEDGERS: u32 = 90 * LEDGERS_PER_DAY; const ARCHIVE_TTL_LEDGERS: u32 = 365 * LEDGERS_PER_DAY; +/// TTL for instance storage cache entries, in ledgers. +/// At ~5 seconds per ledger on Soroban mainnet, 100 ledgers ≈ 8 minutes. +/// Instance TTL is shared - bumping extends all instance storage keys. +/// Increase for longer-lived deployments; decrease to reduce ledger rent costs. +pub const MARKET_CACHE_TTL_LEDGERS: u32 = 100; + #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum StorageTtlTier { Balance, @@ -37,6 +43,13 @@ pub enum DataKey { ArchivedMarket(Symbol, u64), /// Cumulative days extended for a given market (u32). MarketExtensionTotal(Symbol), + MarketMetadata(Symbol), + MarketScratch(Symbol), + DisputeHistoryCap, + DisputeHistory(Symbol), + /// Instance storage cache key for Market structs, keyed by market_id. + /// Used by MarketReadCache in markets.rs. + MarketCache(Symbol), } /// Storage format version for migration tracking