blvm-node 0.1.2

Bitcoin Commons BLVM: Minimal Bitcoin node implementation using blvm-protocol and blvm-consensus
//! Request validation for module API calls
//!
//! Validates that modules cannot request consensus-modifying operations.

use crate::module::ipc::protocol::RequestPayload;
use crate::module::traits::ModuleError;
use std::collections::HashMap;
use std::sync::Mutex;
use tracing::{debug, warn};

/// Result of request validation
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ValidationResult {
    /// Request is valid and allowed
    Allowed,
    /// Request is invalid and denied
    Denied(String),
}

/// Request validator that ensures modules cannot modify consensus
pub struct RequestValidator {
    /// Rate limiters per module (module_id -> RateLimiter)
    rate_limiters: Mutex<HashMap<String, RateLimiter>>,
    /// Maximum requests per second per module
    max_requests_per_second: u64,
    /// Time window for rate limiting (seconds)
    time_window_seconds: u64,
}

/// Rate limiter using sliding window approach
struct RateLimiter {
    /// Timestamps of recent requests (circular buffer)
    request_timestamps: Vec<u64>,
    /// Current index in circular buffer
    current_index: usize,
    /// Buffer size (number of timestamps to track)
    buffer_size: usize,
}

impl RateLimiter {
    /// Create a new rate limiter
    fn new(max_requests: u64, window_seconds: u64) -> Self {
        // Buffer size: track at least 2x the max requests to handle bursts
        // Also ensure we have enough capacity for the time window
        let buffer_size =
            ((max_requests * 2).max(100) as usize).max((window_seconds as usize).min(1000)); // Cap at 1000 for memory efficiency
        Self {
            request_timestamps: Vec::with_capacity(buffer_size),
            current_index: 0,
            buffer_size,
        }
    }

    /// Check if a request is allowed (rate limit not exceeded)
    fn check_rate_limit(&mut self, max_requests: u64, window_seconds: u64) -> bool {
        let now = crate::utils::current_timestamp();

        // Remove timestamps outside the time window
        let cutoff = now.saturating_sub(window_seconds);
        self.request_timestamps.retain(|&ts| ts > cutoff);

        // Check if we're under the limit
        if self.request_timestamps.len() < max_requests as usize {
            // Add current request timestamp
            if self.request_timestamps.len() < self.buffer_size {
                self.request_timestamps.push(now);
            } else {
                // Circular buffer: overwrite oldest entry
                self.request_timestamps[self.current_index] = now;
                self.current_index = (self.current_index + 1) % self.buffer_size;
            }
            true
        } else {
            false
        }
    }
}

impl RequestValidator {
    /// Create a new request validator with default rate limits
    pub fn new() -> Self {
        Self::with_rate_limit(100, 1) // Default: 100 requests per second
    }

    /// Create a new request validator with custom rate limits
    pub fn with_rate_limit(max_requests_per_second: u64, time_window_seconds: u64) -> Self {
        Self {
            rate_limiters: Mutex::new(HashMap::new()),
            max_requests_per_second,
            time_window_seconds,
        }
    }

    /// Validate a module request to ensure it doesn't modify consensus
    ///
    /// All current RequestPayload variants are read-only, so validation always passes.
    /// When write operations are added, they will be rejected here.
    #[inline]
    pub fn validate_request(
        &self,
        _module_id: &str,
        payload: &RequestPayload,
    ) -> Result<ValidationResult, ModuleError> {
        // Fast path: all current operations are read-only
        // Using match ensures exhaustiveness when new variants are added
        match payload {
            // Handshake is always allowed (first message)
            RequestPayload::Handshake { .. } => Ok(ValidationResult::Allowed),
            // Read-only operations - all allowed
            RequestPayload::GetBlock { .. }
            | RequestPayload::GetBlockHeader { .. }
            |             RequestPayload::GetTransaction { .. }
            | RequestPayload::HasTransaction { .. }
            | RequestPayload::GetChainTip
            | RequestPayload::GetBlockHeight
            | RequestPayload::GetUtxo { .. }
            | RequestPayload::SubscribeEvents { .. }
            // Mempool API - read-only
            | RequestPayload::GetMempoolTransactions
            | RequestPayload::GetMempoolTransaction { .. }
            | RequestPayload::GetMempoolSize
            // Network API - read-only
            | RequestPayload::GetNetworkStats
            | RequestPayload::GetNetworkPeers
            // Chain API - read-only
            | RequestPayload::GetChainInfo
            | RequestPayload::GetBlockByHeight { .. }
            // Lightning API - read-only
            | RequestPayload::GetLightningNodeUrl
            | RequestPayload::GetLightningInfo
            // Payment API - read-only
            | RequestPayload::GetPaymentState { .. }
            // Additional Mempool API - read-only
            | RequestPayload::CheckTransactionInMempool { .. }
            | RequestPayload::GetFeeEstimate { .. }
            // Filesystem API - validated by sandbox
            | RequestPayload::ReadFile { .. }
            | RequestPayload::WriteFile { .. }
            | RequestPayload::DeleteFile { .. }
            | RequestPayload::ListDirectory { .. }
            | RequestPayload::CreateDirectory { .. }
            | RequestPayload::GetFileMetadata { .. }
            // Module RPC Endpoint Registration - validated by permissions
            | RequestPayload::RegisterRpcEndpoint { .. }
            | RequestPayload::UnregisterRpcEndpoint { .. }
            // Timers and Scheduled Tasks - callbacks cannot be serialized, so these will error
            | RequestPayload::RegisterTimer { .. }
            | RequestPayload::CancelTimer { .. }
            | RequestPayload::ScheduleTask { .. }
            // Metrics and Telemetry - read-only reporting
            | RequestPayload::ReportMetric { .. }
            | RequestPayload::GetModuleMetrics { .. }
            | RequestPayload::GetAllMetrics
            // Module Health & Monitoring - read-only
            | RequestPayload::GetModuleHealth { .. }
            | RequestPayload::GetAllModuleHealth
            | RequestPayload::ReportModuleHealth { .. }
            // Module Discovery API - read-only
            | RequestPayload::DiscoverModules
            | RequestPayload::GetModuleInfo { .. }
            | RequestPayload::IsModuleAvailable { .. }
            // Module Event Publishing - validated by permissions
            | RequestPayload::PublishEvent { .. }
            // Module-to-Module Communication - validated by permissions
            | RequestPayload::CallModule { .. }
            | RequestPayload::RegisterModuleApi { .. }
            | RequestPayload::UnregisterModuleApi
            // Network Integration - requires validation (sending network packets)
            | RequestPayload::SendMeshPacketToPeer { .. } => Ok(ValidationResult::Allowed),
            | RequestPayload::SendStratumV2MessageToPeer { .. } => Ok(ValidationResult::Allowed),
            | RequestPayload::GetBlockTemplate { .. } => Ok(ValidationResult::Allowed),
            | RequestPayload::SubmitBlock { .. } => Ok(ValidationResult::Allowed),
            | RequestPayload::MergeBlockServeDenylist { .. }
            | RequestPayload::GetBlockServeDenylistSnapshot
            | RequestPayload::ClearBlockServeDenylist
            | RequestPayload::ReplaceBlockServeDenylist { .. }
            | RequestPayload::MergeTxServeDenylist { .. }
            | RequestPayload::GetTxServeDenylistSnapshot
            | RequestPayload::ClearTxServeDenylist
            | RequestPayload::ReplaceTxServeDenylist { .. }
            | RequestPayload::GetSyncStatus
            | RequestPayload::BanPeer { .. }
            | RequestPayload::SetBlockServeMaintenanceMode { .. }
            | RequestPayload::RegisterCliSpec { .. } => Ok(ValidationResult::Allowed),
        }
    }

    /// Validate that a module cannot modify consensus state
    ///
    /// This is a safeguard - modules should never have write access,
    /// but we validate explicitly to be defensive.
    pub fn validate_no_consensus_modification(
        &self,
        module_id: &str,
        operation: &str,
    ) -> Result<(), ModuleError> {
        // IPC paths are treated as read-only with respect to consensus; nothing here rejects yet.
        debug!(
            "Validated no consensus modification for module {} operation: {}",
            module_id, operation
        );
        Ok(())
    }

    /// Validate resource limits (rate limiting, etc.)
    ///
    /// Enforces rate limiting per module using a sliding window approach.
    /// Default limit: 100 requests per second per module.
    pub fn validate_resource_limits(
        &self,
        module_id: &str,
        _operation: &str,
    ) -> Result<(), ModuleError> {
        let mut limiters = self.rate_limiters.lock().unwrap();

        // Get or create rate limiter for this module
        let limiter = limiters.entry(module_id.to_string()).or_insert_with(|| {
            RateLimiter::new(self.max_requests_per_second, self.time_window_seconds)
        });

        // Check rate limit
        if !limiter.check_rate_limit(self.max_requests_per_second, self.time_window_seconds) {
            warn!(
                "Rate limit exceeded for module {}: {} requests per {} seconds",
                module_id, self.max_requests_per_second, self.time_window_seconds
            );
            return Err(ModuleError::RateLimitExceeded(format!(
                "Module {} exceeded rate limit: {} requests per {} seconds",
                module_id, self.max_requests_per_second, self.time_window_seconds
            )));
        }

        debug!(
            "Rate limit check passed for module {} operation: {}",
            module_id, _operation
        );
        Ok(())
    }
}

impl Default for RequestValidator {
    fn default() -> Self {
        Self::new()
    }
}