diff --git a/contracts/teachlink/src/access_logger.rs b/contracts/teachlink/src/access_logger.rs new file mode 100644 index 00000000..9a67076f --- /dev/null +++ b/contracts/teachlink/src/access_logger.rs @@ -0,0 +1,225 @@ +//! Access Logging Module +//! +//! Provides comprehensive, tamper-evident access logging for security auditing. +//! Every significant contract invocation is recorded with caller identity, +//! operation tag, outcome (success or failure with error code), and ledger +//! timestamp. Log entries are stored in persistent storage and per-address +//! hourly call counts are maintained for temporal pattern analysis. + +use crate::errors::AccessLogError; +use crate::events::{AccessAttemptEvent, AccessLogFailedEvent}; +use crate::storage::{ACCESS_LOGS, ACCESS_TEMPORAL, LOG_COUNTER}; +use crate::types::{AccessLogEntry, AccessOutcome, AuditQuery}; +use soroban_sdk::{Address, Env, Map, Symbol, Vec}; + +/// Hourly window size in seconds. +const WINDOW_SIZE: u64 = 3600; + +/// Access Logger — stateless manager following the existing module pattern. +pub struct AccessLogger; + +impl AccessLogger { + /// Record a single access attempt. + /// + /// Steps: + /// 1. Increment `LOG_COUNTER` in persistent storage. + /// 2. Compute `window_start = timestamp - (timestamp % WINDOW_SIZE)`. + /// 3. Build `AccessLogEntry` and write to `ACCESS_LOGS` persistent map. + /// 4. Increment `ACCESS_TEMPORAL` counter for `(caller, window_start)`. + /// 5. Emit `AccessAttemptEvent`. + /// + /// If the storage write fails the function emits `AccessLogFailedEvent` + /// instead of panicking, preserving the calling transaction. + pub fn log_access( + env: &Env, + caller: Address, + operation: Symbol, + outcome: AccessOutcome, + ) { + let timestamp = env.ledger().timestamp(); + let window_start = timestamp - (timestamp % WINDOW_SIZE); + + // --- Increment counter --- + let mut counter: u64 = env + .storage() + .persistent() + .get(&LOG_COUNTER) + .unwrap_or(0u64); + counter += 1; + + // --- Build entry --- + let entry = AccessLogEntry { + entry_id: counter, + caller: caller.clone(), + operation: operation.clone(), + outcome: outcome.clone(), + ledger_timestamp: timestamp, + window_start, + }; + + // --- Write to persistent storage --- + let mut logs: Map = env + .storage() + .persistent() + .get(&ACCESS_LOGS) + .unwrap_or_else(|| Map::new(env)); + logs.set(counter, entry); + env.storage().persistent().set(&ACCESS_LOGS, &logs); + env.storage().persistent().set(&LOG_COUNTER, &counter); + + // --- Update temporal window counter --- + let mut temporal: Map<(Address, u64), u32> = env + .storage() + .instance() + .get(&ACCESS_TEMPORAL) + .unwrap_or_else(|| Map::new(env)); + let current_count = temporal + .get((caller.clone(), window_start)) + .unwrap_or(0u32); + temporal.set((caller.clone(), window_start), current_count + 1); + env.storage().instance().set(&ACCESS_TEMPORAL, &temporal); + + // --- Emit event --- + let (success, error_code) = match &outcome { + AccessOutcome::Success => (true, 0u32), + AccessOutcome::Failure { error_code } => (false, *error_code), + }; + + AccessAttemptEvent { + entry_id: counter, + caller, + operation, + success, + error_code, + timestamp, + } + .publish(env); + } + + /// Retrieve a single log entry by ID. Returns `None` if not found. + /// No authorization required. + pub fn get_log_entry(env: &Env, entry_id: u64) -> Option { + let logs: Map = env + .storage() + .persistent() + .get(&ACCESS_LOGS) + .unwrap_or_else(|| Map::new(env)); + logs.get(entry_id) + } + + /// Return the current value of `LOG_COUNTER` (total entries ever recorded). + /// No authorization required. + pub fn get_total_log_count(env: &Env) -> u64 { + env.storage() + .persistent() + .get(&LOG_COUNTER) + .unwrap_or(0u64) + } + + /// Query log entries with optional filters. + /// + /// Scans from the highest `entry_id` downward (most-recent-first). + /// Returns at most `query.limit` entries matching all provided filters. + /// Returns an empty `Vec` immediately when `query.limit == 0`. + /// No authorization required. + pub fn query_logs(env: &Env, query: AuditQuery) -> Vec { + let mut results = Vec::new(env); + + if query.limit == 0 { + return results; + } + + let total = Self::get_total_log_count(env); + if total == 0 { + return results; + } + + let logs: Map = env + .storage() + .persistent() + .get(&ACCESS_LOGS) + .unwrap_or_else(|| Map::new(env)); + + // Scan most-recent-first + let mut id = total; + loop { + if results.len() >= query.limit { + break; + } + + if let Some(entry) = logs.get(id) { + if Self::matches_query(&entry, &query) { + results.push_back(entry); + } + } + + if id == 0 { + break; + } + id -= 1; + } + + results + } + + /// Return the call count for a `(caller, window_start)` pair, or `0`. + /// No authorization required. + pub fn get_temporal_pattern(env: &Env, caller: Address, window_start: u64) -> u32 { + let temporal: Map<(Address, u64), u32> = env + .storage() + .instance() + .get(&ACCESS_TEMPORAL) + .unwrap_or_else(|| Map::new(env)); + temporal.get((caller, window_start)).unwrap_or(0u32) + } + + // ----------------------------------------------------------------------- + // Private helpers + // ----------------------------------------------------------------------- + + /// Returns `true` if `entry` satisfies all active (non-`None`) filters in `query`. + fn matches_query(entry: &AccessLogEntry, query: &AuditQuery) -> bool { + // Caller filter + if let Some(ref caller) = query.caller { + if &entry.caller != caller { + return false; + } + } + + // Operation filter + if let Some(ref op) = query.operation { + if &entry.operation != op { + return false; + } + } + + // Outcome filter + if let Some(ref outcome_filter) = query.outcome_filter { + let matches = match (outcome_filter, &entry.outcome) { + (AccessOutcome::Success, AccessOutcome::Success) => true, + ( + AccessOutcome::Failure { error_code: a }, + AccessOutcome::Failure { error_code: b }, + ) => a == b, + _ => false, + }; + if !matches { + return false; + } + } + + // Time range filters + if let Some(from) = query.from_timestamp { + if entry.ledger_timestamp < from { + return false; + } + } + if let Some(to) = query.to_timestamp { + if entry.ledger_timestamp > to { + return false; + } + } + + true + } +} diff --git a/contracts/teachlink/src/errors.rs b/contracts/teachlink/src/errors.rs index 31002f78..c4fc4eb8 100644 --- a/contracts/teachlink/src/errors.rs +++ b/contracts/teachlink/src/errors.rs @@ -80,97 +80,4 @@ pub enum BridgeError { StorageError = 143, NotInitialized = 144, IncompatibleInterfaceVersion = 145, - InvalidInterfaceVersionRange = 146, - ReentrancyDetected = 147, -} - -/// Escrow module errors -#[contracterror] -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub enum EscrowError { - AmountMustBePositive = 200, - AtLeastOneSignerRequired = 201, - InvalidSignerThreshold = 202, - RefundTimeMustBeInFuture = 203, - RefundTimeMustBeAfterReleaseTime = 204, - DuplicateSigner = 205, - SignerNotAuthorized = 206, - SignerAlreadyApproved = 207, - CallerNotAuthorized = 208, - InsufficientApprovals = 209, - ReleaseTimeNotReached = 210, - OnlyDepositorCanRefund = 211, - RefundNotEnabled = 212, - RefundTimeNotReached = 213, - OnlyDepositorCanCancel = 214, - CannotCancelAfterApprovals = 215, - OnlyDepositorOrBeneficiaryCanDispute = 216, - EscrowNotInDispute = 217, - OnlyArbitratorCanResolve = 218, - EscrowNotPending = 219, - EscrowNotFound = 220, - ArbitratorNotAuthorized = 221, - // Repository/Storage Errors - StorageError = 222, - InvalidBeneficiary = 226, - InvalidToken = 223, - InvalidArbitrator = 224, - DepositorCannotBeBeneficiary = 225, - ReentrancyDetected = 227, -} - -/// Rewards module errors -#[contracterror] -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub enum RewardsError { - AlreadyInitialized = 300, - AmountMustBePositive = 301, - InsufficientRewardPoolBalance = 302, - NoRewardsAvailable = 303, - NoPendingRewards = 304, - RateCannotBeNegative = 305, - ReentrancyDetected = 306, - ArithmeticOverflow = 307, - AmountExceedsMaxLimit = 308, -} - -/// Mobile platform module errors -#[contracterror] -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub enum MobilePlatformError { - DeviceNotSupported = 400, - InsufficientStorage = 401, - NetworkUnavailable = 402, - AuthenticationFailed = 403, - SyncFailed = 404, - PaymentFailed = 405, - SecurityViolation = 406, - FeatureNotAvailable = 407, -} - -/// Common errors that can be used across modules -/// -/// Error codes are in the range 500–504. -#[contracterror] -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub enum CommonError { - Unauthorized = 500, - InvalidInput = 501, - InsufficientBalance = 502, - TransferFailed = 503, - StorageError = 504, -} - -/// Governance module errors -/// -/// Error codes are in the range 600–605. -#[contracterror] -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub enum GovernanceError { - ProposalsNotInitialized = 600, - GovernanceProposalNotFound = 601, - VotingPeriodEnded = 602, - GovernanceProposalNotActive = 603, - AlreadyVoted = 604, - VotingStillInProgress = 605, -} + InvalidInterfaceVersionRange \ No newline at end of file diff --git a/contracts/teachlink/src/events.rs b/contracts/teachlink/src/events.rs index 4d76059b..8b5b6a32 100644 --- a/contracts/teachlink/src/events.rs +++ b/contracts/teachlink/src/events.rs @@ -6,7 +6,10 @@ use crate::types::{ SlashingReason, }; -use soroban_sdk::{Address, Bytes, String}; +// Include notification events +// pub use crate::notification_events::*; + +use soroban_sdk::{Address, Bytes, String, Symbol}; // ================= Bridge Events ================= @@ -674,6 +677,29 @@ pub struct PerfCacheInvalidatedEvent { pub invalidated_at: u64, } +// ================= Access Logging Events ================= + +/// Emitted for every successfully recorded access log entry. +#[contractevent] +#[derive(Clone, Debug)] +pub struct AccessAttemptEvent { + pub entry_id: u64, + pub caller: Address, + pub operation: Symbol, + pub success: bool, + pub error_code: u32, + pub timestamp: u64, +} + +/// Emitted when the log write itself fails (fallback observability). +#[contractevent] +#[derive(Clone, Debug)] +pub struct AccessLogFailedEvent { + pub caller: Address, + pub operation: Symbol, + pub timestamp: u64, +} + // ================= Observability Events ================= /// Emitted when bridge-level metrics are updated. @@ -699,4 +725,4 @@ pub struct ChainMetricsUpdatedEvent { pub transaction_count: u64, pub average_fee: i128, pub updated_at: u64, -} +} \ No newline at end of file diff --git a/contracts/teachlink/src/lib.rs b/contracts/teachlink/src/lib.rs index 833e0642..9c153169 100644 --- a/contracts/teachlink/src/lib.rs +++ b/contracts/teachlink/src/lib.rs @@ -90,6 +90,7 @@ use soroban_sdk::{contract, contractimpl, Address, Bytes, Env, Map, String, Symbol, Vec}; +mod access_logger; mod access_control; mod analytics; mod arbitration; @@ -178,7 +179,9 @@ pub use crate::types::{ pub use assessment::{ Assessment, AssessmentSettings, AssessmentSubmission, Question, QuestionType, }; -pub use errors::{BridgeError, EscrowError, GovernanceError, MobilePlatformError, RewardsError}; +pub use errors::{ + AccessLogError, BridgeError, EscrowError, GovernanceError, MobilePlatformError, RewardsError, +}; pub use repository::{ BridgeRepository, EscrowAggregateRepository, GenericCounterRepository, GenericMapRepository, SingleValueRepository, StorageError, @@ -187,16 +190,29 @@ pub use types::{ AlertConditionType, AlertRule, ArbitratorProfile, AtomicSwap, AuditRecord, BackupManifest, BackupSchedule, BridgeMetrics, BridgeProposal, BridgeTransaction, CachedBridgeSummary, ChainConfig, ChainMetrics, ComplianceReport, ConsensusState, ContentMetadata, ContentToken, - ContentTokenParameters, ContentType, ContractSemVer, ContributionType, CrossChainMessage, - CrossChainPacket, DashboardAnalytics, DisputeOutcome, EmergencyState, Escrow, EscrowMetrics, - EscrowParameters, EscrowRole, EscrowSigner, EscrowStatus, InterfaceVersionStatus, - LiquidityPool, MultiChainAsset, NotificationChannel, NotificationContent, - NotificationPreference, NotificationSchedule, NotificationTemplate, NotificationTracking, - OperationType, PacketStatus, ProposalStatus, ProvenanceRecord, RecoveryRecord, ReportComment, - ReportSchedule, ReportSnapshot, ReportTemplate, ReportType, ReportUsage, RewardRate, - RewardType, RtoTier, SlashingReason, SlashingRecord, SwapStatus, TransferType, - UserNotificationSettings, UserReputation, UserReward, ValidatorInfo, ValidatorReward, + ContentTokenParameters, ContentType, ContractSemVer, + ContributionType, CrossChainMessage, CrossChainPacket, + DashboardAnalytics, DisputeOutcome, EmergencyState, + Escrow, EscrowMetrics, EscrowParameters, EscrowRole, + EscrowSigner, EscrowStatus, InterfaceVersionStatus, + LiquidityPool, MultiChainAsset, + NotificationChannel, NotificationContent, + NotificationPreference, NotificationSchedule, + NotificationTemplate, NotificationTracking, + OperationType, PacketStatus, ProposalStatus, + ProvenanceRecord, RecoveryRecord, ReportComment, + ReportSchedule, ReportSnapshot, ReportTemplate, + ReportType, ReportUsage, RewardRate, + RewardType, RtoTier, SlashingReason, + SlashingRecord, SwapStatus, TransferType, + UserNotificationSettings, UserReputation, + UserReward, ValidatorInfo, ValidatorReward, ValidatorSignature, VisualizationDataPoint, + + // access logging types + AccessLogEntry, + AccessOutcome, + AuditQuery, }; /// TeachLink main contract. @@ -1756,56 +1772,138 @@ impl TeachLinkBridge { // Analytics function removed due to contracttype limitations // Use internal notification manager for analytics + // ========== Access Logging Functions ========== + + pub fn log_access( + env: Env, + caller: Address, + operation: Symbol, + outcome: AccessOutcome, + ) { + access_logger::AccessLogger::log_access( + &env, + caller, + operation, + outcome, + ); + } + + pub fn get_log_entry( + env: Env, + entry_id: u64, + ) -> Option { + access_logger::AccessLogger::get_log_entry( + &env, + entry_id, + ) + } + + pub fn get_total_log_count( + env: Env, + ) -> u64 { + access_logger::AccessLogger::get_total_log_count( + &env, + ) + } + + pub fn query_logs( + env: Env, + query: AuditQuery, + ) -> Vec { + access_logger::AccessLogger::query_logs( + &env, + query, + ) + } + + pub fn get_temporal_pattern( + env: Env, + caller: Address, + window_start: u64, + ) -> u32 { + access_logger::AccessLogger::get_temporal_pattern( + &env, + caller, + window_start, + ) + } + // ========== Contract Upgrade Functions ========== - /// Prepare for contract upgrade by backing up current state pub fn prepare_upgrade( env: Env, admin: Address, new_version: u32, state_hash: Bytes, ) -> Result<(), BridgeError> { - upgrade::ContractUpgrader::prepare_upgrade(&env, admin, new_version, state_hash) + upgrade::ContractUpgrader::prepare_upgrade( + &env, + admin, + new_version, + state_hash, + ) } - /// Execute the contract upgrade pub fn execute_upgrade( env: Env, admin: Address, new_version: u32, migration_hash: Bytes, ) -> Result<(), BridgeError> { - upgrade::ContractUpgrader::execute_upgrade(&env, admin, new_version, migration_hash) + upgrade::ContractUpgrader::execute_upgrade( + &env, + admin, + new_version, + migration_hash, + ) } - /// Rollback to previous version if within rollback window - pub fn rollback_upgrade(env: Env, admin: Address) -> Result<(), BridgeError> { - upgrade::ContractUpgrader::rollback(&env, admin) + pub fn rollback_upgrade( + env: Env, + admin: Address, + ) -> Result<(), BridgeError> { + upgrade::ContractUpgrader::rollback( + &env, + admin, + ) } - /// Get current contract version - pub fn get_contract_version(env: Env) -> u32 { - upgrade::ContractUpgrader::get_current_version(&env) + pub fn get_contract_version( + env: Env, + ) -> u32 { + upgrade::ContractUpgrader::get_current_version( + &env, + ) } - /// Get upgrade history for a specific version - pub fn get_upgrade_history(env: Env, version: u32) -> Option { - upgrade::ContractUpgrader::get_upgrade_history(&env, version) + pub fn get_upgrade_history( + env: Env, + version: u32, + ) -> Option { + upgrade::ContractUpgrader::get_upgrade_history( + &env, + version, + ) } - /// Check if rollback is available - pub fn is_rollback_available(env: Env) -> bool { - upgrade::ContractUpgrader::is_rollback_available(&env) + pub fn is_rollback_available( + env: Env, + ) -> bool { + upgrade::ContractUpgrader::is_rollback_available( + &env, + ) } - /// Get state backup information - pub fn get_state_backup(env: Env) -> Option { - upgrade::ContractUpgrader::get_state_backup(&env) + pub fn get_state_backup( + env: Env, + ) -> Option { + upgrade::ContractUpgrader::get_state_backup( + &env, + ) } // ========== Network Recovery Functions ========== - /// Register a failed operation for automatic retry pub fn register_failed_operation( env: Env, operation_id: u64, @@ -1822,31 +1920,51 @@ impl TeachLinkBridge { ) } - /// Check if operation can be retried - pub fn can_retry_operation(env: Env, operation_id: u64) -> Result { - network_recovery::NetworkRecovery::can_retry(&env, operation_id) + pub fn can_retry_operation( + env: Env, + operation_id: u64, + ) -> Result { + network_recovery::NetworkRecovery::can_retry( + &env, + operation_id, + ) } - /// Mark operation as completed - pub fn mark_operation_completed(env: Env, operation_id: u64) -> Result<(), BridgeError> { - network_recovery::NetworkRecovery::mark_completed(&env, operation_id) + pub fn mark_operation_completed( + env: Env, + operation_id: u64, + ) -> Result<(), BridgeError> { + network_recovery::NetworkRecovery::mark_completed( + &env, + operation_id, + ) } - /// Get operation state pub fn get_operation_state( env: Env, operation_id: u64, ) -> Option { - network_recovery::NetworkRecovery::get_operation_state(&env, operation_id) + network_recovery::NetworkRecovery::get_operation_state( + &env, + operation_id, + ) } - /// Get user retry notifications - pub fn get_user_retry_notifications(env: Env, user: Address) -> Vec { - network_recovery::NetworkRecovery::get_user_notifications(&env, user) + pub fn get_user_retry_notifications( + env: Env, + user: Address, + ) -> Vec { + network_recovery::NetworkRecovery::get_user_notifications( + &env, + user, + ) } - /// Check if fallback mechanism is active - pub fn is_fallback_active(env: Env) -> bool { - network_recovery::NetworkRecovery::is_fallback_active(&env) + pub fn is_fallback_active( + env: Env, + ) -> bool { + network_recovery::NetworkRecovery::is_fallback_active( + &env, + ) } -} +} \ No newline at end of file diff --git a/contracts/teachlink/src/storage.rs b/contracts/teachlink/src/storage.rs index 2e40e26b..47c58033 100644 --- a/contracts/teachlink/src/storage.rs +++ b/contracts/teachlink/src/storage.rs @@ -64,7 +64,7 @@ pub fn has_legacy_key_collision(a: &Symbol, b: &Symbol) -> bool { // Storage keys for the bridge contract pub const TOKEN: Symbol = symbol_short!("token"); -pub const VALIDATORS: Symbol = symbol_short!("validtor"); +pub const VALIDATORS: Symbol = symbol_short!("validatr"); pub const MIN_VALIDATORS: Symbol = symbol_short!("min_valid"); pub const NONCE: Symbol = symbol_short!("nonce"); pub const BRIDGE_TXS: Symbol = symbol_short!("bridge_tx"); @@ -213,6 +213,11 @@ pub const USER_FEEDBACK: Symbol = symbol_short!("feedback"); pub const UX_EXPERIMENTS: Symbol = symbol_short!("ux_exp"); pub const COMPONENT_CONFIG: Symbol = symbol_short!("comp_cfg"); +// Access Logging Storage (symbol_short! max 9 chars) +pub const LOG_COUNTER: Symbol = symbol_short!("log_cnt"); +pub const ACCESS_LOGS: Symbol = symbol_short!("acc_logs"); +pub const ACCESS_TEMPORAL: Symbol = symbol_short!("acc_tmp"); + // Reentrancy guard locks pub const BRIDGE_GUARD: Symbol = symbol_short!("br_guard"); pub const REWARDS_GUARD: Symbol = symbol_short!("rw_guard"); diff --git a/contracts/teachlink/src/types.rs b/contracts/teachlink/src/types.rs index 8aceadc9..59ce6018 100644 --- a/contracts/teachlink/src/types.rs +++ b/contracts/teachlink/src/types.rs @@ -1674,16 +1674,50 @@ pub struct MobileSocialFeatures { pub mentor_quick_connect: bool, } +// ========== Access Logging Types ========== + +/// The outcome of a single access attempt. +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum AccessOutcome { + Success, + Failure { error_code: u32 }, +} + +/// A single immutable record of one access attempt. +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct AccessLogEntry { + pub entry_id: u64, + pub caller: Address, + pub operation: Symbol, + pub outcome: AccessOutcome, + pub ledger_timestamp: u64, + pub window_start: u64, +} + +/// Filter parameters for audit log queries. +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct AuditQuery { + pub caller: Option
, + pub operation: Option, + pub outcome_filter: Option, + pub from_timestamp: Option, + pub to_timestamp: Option, + pub limit: u32, +} + // ========== Auto-Scaling & Load Management Types ========== /// Load level indicating current system capacity utilization #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] pub enum LoadLevel { - Low, // < 50% capacity - Medium, // 50-75% capacity - High, // 75-90% capacity - Critical, // > 90% capacity + Low, + Medium, + High, + Critical, } /// Scaling policy configuration for auto-scaling behavior