Skip to content
Merged
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
106 changes: 102 additions & 4 deletions contracts/predictify-hybrid/src/markets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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<Market> {
let key = DataKey::MarketCache(market_id.clone());
let result: Option<Market> = 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.
Expand Down Expand Up @@ -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<Market, Error> {
_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<Market> = _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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
13 changes: 13 additions & 0 deletions contracts/predictify-hybrid/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down
Loading