package cache import ( "sync" "time" "github.com/google/uuid" "github.com/rwadurian/mpc-system/pkg/logger" "go.uber.org/zap" ) // ShareCacheEntry represents a cached share entry type ShareCacheEntry struct { SessionID uuid.UUID PartyID string Share []byte PublicKey []byte ExpiresAt time.Time RetrievedOnce bool // Track if share has been retrieved } // ShareCache provides in-memory caching for delegate party shares // Shares are stored temporarily and deleted after retrieval type ShareCache struct { entries map[string]*ShareCacheEntry // sessionID -> entry mu sync.RWMutex ttl time.Duration } // NewShareCache creates a new share cache func NewShareCache(ttl time.Duration) *ShareCache { cache := &ShareCache{ entries: make(map[string]*ShareCacheEntry), ttl: ttl, } // Start background cleanup goroutine go cache.cleanupExpired() return cache } // Store stores a share in the cache func (c *ShareCache) Store(sessionID uuid.UUID, partyID string, share, publicKey []byte) { c.mu.Lock() defer c.mu.Unlock() entry := &ShareCacheEntry{ SessionID: sessionID, PartyID: partyID, Share: share, PublicKey: publicKey, ExpiresAt: time.Now().Add(c.ttl), RetrievedOnce: false, } c.entries[sessionID.String()] = entry logger.Info("Share stored in cache", zap.String("session_id", sessionID.String()), zap.String("party_id", partyID), zap.Int("share_size", len(share)), zap.Time("expires_at", entry.ExpiresAt)) } // Get retrieves a share from the cache // The share is marked as retrieved but not deleted yet // Use Delete() to remove it after successful delivery to user func (c *ShareCache) Get(sessionID uuid.UUID) (*ShareCacheEntry, bool) { c.mu.Lock() defer c.mu.Unlock() entry, exists := c.entries[sessionID.String()] if !exists { return nil, false } // Check if expired if time.Now().After(entry.ExpiresAt) { delete(c.entries, sessionID.String()) logger.Warn("Share expired", zap.String("session_id", sessionID.String())) return nil, false } // Mark as retrieved entry.RetrievedOnce = true logger.Info("Share retrieved from cache", zap.String("session_id", sessionID.String()), zap.String("party_id", entry.PartyID)) return entry, true } // Delete deletes a share from the cache func (c *ShareCache) Delete(sessionID uuid.UUID) { c.mu.Lock() defer c.mu.Unlock() delete(c.entries, sessionID.String()) logger.Info("Share deleted from cache", zap.String("session_id", sessionID.String())) } // GetAndDelete retrieves and immediately deletes a share (atomic operation) // This ensures the share can only be retrieved once func (c *ShareCache) GetAndDelete(sessionID uuid.UUID) (*ShareCacheEntry, bool) { c.mu.Lock() defer c.mu.Unlock() entry, exists := c.entries[sessionID.String()] if !exists { return nil, false } // Check if expired if time.Now().After(entry.ExpiresAt) { delete(c.entries, sessionID.String()) logger.Warn("Share expired", zap.String("session_id", sessionID.String())) return nil, false } // Delete immediately delete(c.entries, sessionID.String()) logger.Info("Share retrieved and deleted from cache (one-time retrieval)", zap.String("session_id", sessionID.String()), zap.String("party_id", entry.PartyID)) return entry, true } // Size returns the number of entries in the cache func (c *ShareCache) Size() int { c.mu.RLock() defer c.mu.RUnlock() return len(c.entries) } // cleanupExpired removes expired entries periodically func (c *ShareCache) cleanupExpired() { ticker := time.NewTicker(1 * time.Minute) defer ticker.Stop() for range ticker.C { c.mu.Lock() now := time.Now() var expired []string for sessionID, entry := range c.entries { if now.After(entry.ExpiresAt) { expired = append(expired, sessionID) } } for _, sessionID := range expired { delete(c.entries, sessionID) } if len(expired) > 0 { logger.Info("Cleaned up expired shares", zap.Int("count", len(expired))) } c.mu.Unlock() } }